summaryrefslogtreecommitdiff
path: root/macros/latex/contrib/wargame/source/utils/wgexport.py
diff options
context:
space:
mode:
Diffstat (limited to 'macros/latex/contrib/wargame/source/utils/wgexport.py')
-rwxr-xr-xmacros/latex/contrib/wargame/source/utils/wgexport.py3750
1 files changed, 3750 insertions, 0 deletions
diff --git a/macros/latex/contrib/wargame/source/utils/wgexport.py b/macros/latex/contrib/wargame/source/utils/wgexport.py
new file mode 100755
index 0000000000..9ad370cfcd
--- /dev/null
+++ b/macros/latex/contrib/wargame/source/utils/wgexport.py
@@ -0,0 +1,3750 @@
+#!/usr/bin/env python
+#
+from pprint import pprint
+
+# ====================================================================
+def add_pws(opw=None,upw=None):
+ '''Add password options to the poppler commands
+
+ Parameters
+ ----------
+ opw : str
+ Owner password
+ upw : str
+ User password
+
+ Returns
+ -------
+ opts : list
+ List of arguments
+ '''
+ args = []
+ if upw is not None:
+ args.extend(['-upw',upw])
+
+ if opw is not None:
+ args.extend(['-opw',opw])
+
+ return args
+
+# --------------------------------------------------------------------
+def create_proc(args):
+ '''Create a process
+
+ Parameters
+ ----------
+ args : list
+ List of commmand line arguments
+
+ Returns
+ -------
+ proc : subprocess.Process
+ Process handle
+ '''
+ from os import environ
+ from subprocess import Popen, PIPE, TimeoutExpired
+
+ return Popen(args,env=environ.copy(),stdout=PIPE,stderr=PIPE)
+
+# --------------------------------------------------------------------
+def get_pdf_info(pdfname='export.pdf',upw=None,opw=None,timeout=None):
+ '''Get dictionary of PDF information
+
+ Parameters
+ ----------
+ pdfname : str
+ File name
+ opw : str
+ Owner password
+ upw : str
+ User password
+ timeout : int
+ Timeout in seconds or None
+
+ Returns
+ -------
+ info : dict
+ PDF information
+ '''
+ from subprocess import TimeoutExpired
+
+ args = ['pdfinfo', pdfname]
+ args.extend(add_pws(opw=opw,upw=upw))
+
+ proc = create_proc(args)
+
+ try:
+ out, err = proc.communicate(timeout=timeout)
+ except Exception as e:
+ proc.kill()
+ proc.communicate()
+ raise RuntimeError(f'Failed to get PDF info: {e}')
+
+ d = {}
+ for field in out.decode("utf8", "ignore").split("\n"):
+ sf = field.split(":")
+ key, value = sf[0], ":".join(sf[1:])
+ if key != "":
+ d[key] = (int(value.strip()) if key == 'Pages' else value.strip())
+
+ if "Pages" not in d:
+ raise ValueError
+
+ return d
+
+# --------------------------------------------------------------------
+def get_images_info(infofile):
+ '''Read in image information from file
+
+ Parameters
+ ----------
+ infofile : str
+ file name
+
+ Returns
+ -------
+ images : list
+ List of image information
+ '''
+ #from csv import DictReader
+ from json import load
+
+ with open(infofile,'r') as file:
+ info = load(file)
+ #reader = DictReader(file,fieldnames=['page','name','type','sub type'])
+ #return [l for l in reader]
+
+ return info
+
+
+# --------------------------------------------------------------------
+def convert_page(pdfname,page,opw=None,upw=None,timeout=None):
+ '''Convert a single PDF page to an image
+
+ Parameters
+ ----------
+ pdfname : str
+ File name
+ page : int
+ Page number
+ opw : str
+ Owner password
+ upw : str
+ User password
+ timeout : int
+ Timeout in seconds or None
+
+ Returns
+ -------
+ img : bytes
+ The bytes of the image
+ '''
+ args = ['pdftocairo',
+ '-singlefile',
+ '-f', str(page),
+ '-l', str(page),
+ '-png']
+ args.extend(add_pws(opw=opw,upw=upw))
+ args.append(pdfname)
+ args.append('-')
+
+ proc = create_proc(args)
+
+ try:
+ out, err = proc.communicate(timeout=timeout)
+ except Exception as e:
+ proc.kill()
+ proc.communicate()
+ raise RuntimeError(f'Failed to convert page {page} of {pdfname}: {e}')
+
+ from io import BytesIO
+ from PIL import Image
+
+ img = out #Image.open(BytesIO(out))
+
+ return img
+
+# --------------------------------------------------------------------
+def ignore_entry(entry):
+ return entry['category'] in ['<<dummy>>','<<eol>>']
+
+# --------------------------------------------------------------------
+def convert_pages(pdfname,infoname,
+ opw=None,upw=None,timeout=None):
+ '''Convert pages in a PDF to images
+
+ This creates a list of dictionaries with the image name and data
+ (as bytes)
+
+ Parameters
+ ----------
+ pdfname : str
+ File name of PDF to read images from
+ infoname : str
+ File name of CSV to read image info from
+ opw : str
+ Owner password
+ upw : str
+ User password
+ timeout : int
+ Timeout in seconds or None
+
+ Returns
+ -------
+ imgs : list
+ List of images as dicts, info and the bytes of the images
+ '''
+
+ oargs = {'opw':opw,'upw':upw,'timeout':timeout}
+ docinfo = get_pdf_info(pdfname,**oargs)
+ info = get_images_info(infoname)
+
+ if len(info)-1 != docinfo['Pages']:
+ raise RuntimeError(f'Number of pages in {pdfname} '
+ f'({docinfo["Pages"]}) not {len(info)-1}')
+
+ for i in info:
+ if ignore_entry(i): continue
+ i['img'] = convert_page(pdfname,i['number'],**oargs)
+
+ return info
+
+# ====================================================================
+def remove_and_get(zipfname,*filenames):
+ '''Open zip file, and make a copy of it without the files listed
+ in entries. Return the data of files listed in entry.'''
+ from tempfile import mkdtemp
+ from zipfile import ZipFile
+ from shutil import move, rmtree
+ from os import path
+
+ tempdir = mkdtemp()
+ ret = {}
+ try:
+ tempname = path.join(tempdir, 'new.zip')
+ with ZipFile(zipfname, 'r') as zipread:
+
+ with ZipFile(tempname, 'w') as zipwrite:
+
+ for item in zipread.infolist():
+ data = zipread.read(item.filename)
+
+ if item.filename not in filenames:
+ zipwrite.writestr(item, data)
+ else:
+ ret[item.filename] = data
+
+ move(tempname, zipfname)
+ finally:
+ rmtree(tempdir)
+
+ return ret
+
+# --------------------------------------------------------------------
+def add_back(zipfname,files):
+ '''Open a zip file for appending and add data in files
+
+ Files is a dictionary of filenames mapping to file content,
+ as returned by remove_and_get
+ '''
+ from zipfile import ZipFile
+
+ with ZipFile(zipfname, 'a') as zipadd:
+ for filename,data in files.items():
+ zipadd.writestr(filename,data)
+
+# ====================================================================
+# Contants used for tag names
+BUILD = 'VASSAL.build.'
+MODULE = BUILD + 'module.'
+WIDGET = BUILD + 'widget.'
+MAP = MODULE + 'map.'
+PICKER = MAP + 'boardPicker.'
+
+# ====================================================================
+def get_asdict(elem,tag,key,enable=True):
+ cont = elem.getElementsByTagName(tag)
+ if not enable:
+ return cont
+
+ return {e.getAttribute(key): e for e in cont}
+
+# --------------------------------------------------------------------
+def get_asone(elem,tag,enable=True):
+ cont = elem.getElementsByTagName(tag)
+ if enable and len(cont) != 1:
+ return None
+ return cont
+
+# --------------------------------------------------------------------
+def get_doc(string):
+ from xml.dom.minidom import parseString
+
+ return parseString(string)
+
+# --------------------------------------------------------------------
+def get_game(doc):
+ return doc.getElementsByTagName('VASSAL.build.GameModule')[0]
+
+# --------------------------------------------------------------------
+def get_globalproperties(doc,one=True):
+ return get_asone(doc,MODULE+'properties.GlobalProperties',one)
+
+# --------------------------------------------------------------------
+def get_globalproperty(doc,asdict=True):
+ '''A little confusing - this returns a dictionary'''
+ return get_asdict(doc,MODULE+'properties.GlobalProperty','name',asdict)
+
+# --------------------------------------------------------------------
+def get_turntrack(doc,asdict=True):
+ return get_asdict(doc,MODULE+'turn.TurnTracker','name',asdict)
+
+# --------------------------------------------------------------------
+def get_turncounter(doc,asdict=True):
+ return get_asdict(doc,MODULE+'turn.CounterTurnLevel','property',asdict)
+
+# --------------------------------------------------------------------
+def get_turnlist(doc,asdict=True):
+ return get_asdict(doc,MODULE+'turn.ListTurnLevel','property',asdict)
+
+# --------------------------------------------------------------------
+def get_turnhotkey(doc,asdict=True):
+ return get_asdict(doc,MODULE+'turn.TurnGlobalHotkey','hotkey',asdict)
+
+# --------------------------------------------------------------------
+def get_documentation(doc,one=True):
+ return get_asone(doc,MODULE+'Documentation',one)
+
+# --------------------------------------------------------------------
+def get_helpfiles(doc,key='fileName',asdict=True):
+ return get_asdict(doc,MODULE+'documentation.HelpFile',key,asdict)
+
+# --------------------------------------------------------------------
+def get_tutorials(doc,key='name',asdict=True):
+ return get_asdict(doc,MODULE+'documentation.Tutorial',key,asdict)
+
+# --------------------------------------------------------------------
+def get_inventories(doc,key='name',asdict=False):
+ return get_asdict(doc,MODULE+'Inventory',key,asdict)
+
+# --------------------------------------------------------------------
+def get_maps(doc,asdict=True):
+ return get_asdict(doc,MODULE+'Map','mapName',asdict)
+
+# --------------------------------------------------------------------
+def get_widgetmap(doc,asdict=True):
+ return get_asdict(doc,WIDGET+'WidgetMap','mapName',asdict)
+
+# --------------------------------------------------------------------
+def get_chartwindows(doc,asdict=True):
+ return get_asdict(doc,MODULE+'ChartWindow','name',asdict)
+
+# --------------------------------------------------------------------
+def get_map(doc,name):
+ maps = get_maps(doc,True)
+ return maps.get(name, None) if maps else None
+
+# --------------------------------------------------------------------
+def get_boards(doc,asdict=True):
+ return get_asdict(doc,PICKER+'Board','name',asdict)
+
+# --------------------------------------------------------------------
+def get_zoned(doc,one=True):
+ return get_asone(doc,PICKER+'board.ZonedGrid',one)
+
+# --------------------------------------------------------------------
+def get_zone(doc,asdict=True):
+ return get_asdict(doc,PICKER+'board.mapgrid.Zone','name',asdict)
+
+# --------------------------------------------------------------------
+def get_hexgrid(doc,one=True):
+ return get_asone(doc,PICKER+'board.HexGrid',one)
+
+# --------------------------------------------------------------------
+def get_squaregrid(doc,one=True):
+ return get_asone(doc,PICKER+'board.SquareGrid',one)
+
+# --------------------------------------------------------------------
+def get_regiongrid(doc,one=True):
+ return get_asone(doc,PICKER+'board.RegionGrid',one)
+
+# --------------------------------------------------------------------
+def get_hexnumbering(doc,one=True):
+ return get_asone(doc,PICKER+'board.mapgrid.HexGridNumbering',one)
+
+# --------------------------------------------------------------------
+def get_regions(doc,asdict=True):
+ return get_asdict(doc,PICKER+'board.Region', 'name', asdict)
+
+# --------------------------------------------------------------------
+def get_squarenumbering(doc,one=True):
+ return get_asone(doc,PICKER+'board.mapgrid.SquareGridNumbering', one)
+
+# --------------------------------------------------------------------
+def get_pieces(doc,asdict=True):
+ return get_asdict(doc,WIDGET+'PieceSlot','entryName',asdict)
+
+# --------------------------------------------------------------------
+def get_prototypecontainer(doc,one=True):
+ return get_asone(doc,MODULE+'PrototypesContainer',one)
+
+# --------------------------------------------------------------------
+def get_prototypes(doc,asdict=True):
+ return get_asdict(doc,MODULE+'PrototypeDefinition', 'name',asdict)
+
+# --------------------------------------------------------------------
+def get_masskey(map,asdict=True):
+ return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+
+# --------------------------------------------------------------------
+def get_masskey(map,asdict=True):
+ return get_asdict(map,MAP+'MassKeyCommand','name', asdict)
+
+
+# --------------------------------------------------------------------
+def get_piece_parts(code):
+ '''Takes the code part of a pieceslot (the text node) and decodes
+ it into traits.
+
+ One can use this to modify the pieces. One could for example
+ remove a trait by calling this function, and remove the element
+ corresponding to the trait from the returned list. Then one must
+ collect up the definitions and states separately and pass each of
+ them to `enc_parts', which gives us two strings that can be passed
+ to `add_piece' (or `add_proto') function to get the new text node
+ content of the piece slot or prototype definition. For example
+
+ piece = get_pieceslots(root,pieces)['foo']
+ code = piece.childNodes[0].nodeValue
+ traits = get_piece_parts(code)
+ ... # Modify the list
+
+ # Separate out definitions and states
+ defs = [enc_def(e['def']) for e in traits]
+ states = [enc_def(e['state']) for e in traits]
+
+ # Encode definitions and states as a single string, and then form body
+ def = enc_parts(*defs)
+ state = enc_parts(*states)
+ body = add_piece(def,state)
+
+ # Set new definition
+ piece.childNodes[0].nodeValue = body
+
+ See also dicts_to_piece
+
+ Parameters
+ ----------
+ code : str
+ Text node content of <PieceSlot> or <PrototypeDefinition> tags
+
+ Returns
+ -------
+ traits : list of dict
+ All the traits in a list. Each element of the list is a
+ dictionary with the keys 'def' and 'status'.
+
+ The key 'def' points to a list of the trait attributes used to
+ define the trait type. The first element of the 'def' list is
+ the trait identifier. The rest are the arguments for that
+ trait type. Note, all fields are encoded as strings.
+
+ The key 'status' encodes the status of the trait. The format
+ of this depends on the trait.Note, all fields are encoded as
+ strings.
+
+ '''
+ cmd,iden,typ,sta = code.split('/')
+ defs = typ.split(r' ')
+ stas = sta.split(r' ')
+ defs = [d.rstrip("\\").split(';') for d in defs]
+ stas = [s.rstrip("\\").split(';') for s in stas]
+ ret = [{'def': d, 'state': s} for d,s in zip(defs,stas)]
+ return ret
+
+# --------------------------------------------------------------------
+def dicts_to_piece(traits):
+ # Separate out definitions and states
+ defs = [enc_def(e['def']) for e in traits]
+ states = [enc_def(e['state']) for e in traits]
+
+ # Encode definitions and states as a single string, and then form body
+ df = enc_parts(*defs)
+ state = enc_parts(*states)
+ body = add_piece(df,state)
+
+ return body
+
+
+# ====================================================================
+# Key encoding
+SHIFT = 65
+CTRL = 130
+ALT = 520
+CTRL_SHIFT = CTRL+SHIFT
+ALT_SHIFT = ALT+SHIFT
+NONE = '\ue004'
+NONE_MOD = 0
+def key(let,mod=CTRL):
+
+ '''Encode a key sequence
+
+ Parameters
+ ----------
+ let : str
+ Key code (Letter)
+ mod : int
+ Modifier mask
+ '''
+ return f'{ord(let)},{mod}'
+# Colour encoding
+def rgb(r,g,b):
+ return ','.join([str(r),str(g),str(b)])
+
+def rgba(r,g,b,a):
+ return ','.join([str(r),str(g),str(b),str(a)])
+
+
+# --------------------------------------------------------------------
+def set_node_attr(node,**attr):
+ for k,v in attr.items():
+ node.setAttribute(k,str(v).lower() if isinstance(v,bool) else str(v))
+
+# --------------------------------------------------------------------
+def add_node(root,parent,tag,**attr):
+ e = root.createElement(tag)
+ if parent: parent.appendChild(e)
+ set_node_attr(e,**attr)
+
+ return e
+
+
+# --------------------------------------------------------------------
+def add_text(root,parent,text):
+ t = root.createTextNode(text)
+ parent.appendChild(t)
+ return t
+
+# --------------------------------------------------------------------
+def add_game(root,doc,
+ name,
+ version,
+ ModuleOther1 = "",
+ ModuleOther2 = "",
+ VassalVersion = "3.6.7",
+ description = "",
+ nextPieceSlotId = 0):
+ return add_node(root,doc,BUILD+'GameModule',
+ name = name,
+ version = version,
+ ModuleOther1 = ModuleOther1,
+ ModuleOther2 = ModuleOther2,
+ VassalVersion = VassalVersion,
+ description = description,
+ nextPieceSlotId = nextPieceSlotId)
+
+# --------------------------------------------------------------------
+def add_basiccommandencoder(root,doc):
+ return add_node(root,doc,MODULE+'BasicCommandEncoder')
+
+# --------------------------------------------------------------------
+def add_globalproperties(root,elem,*props,**named):
+ gp = add_node(root,elem,MODULE+'properties.GlobalProperties')
+ # Add elements where each is a dict with _at least_
+ # "{'name':NAME, 'initialValue':VALUE}"
+ for p in props:
+ add_globalproperty(root,gp,**p)
+ # Add elements where each is a named dict with _at least_
+ # "{'initialValue':VALUE}"
+ for n, p in named:
+ add_globalproperty(root,gp,name=n,**p)
+
+ return gp
+
+
+# --------------------------------------------------------------------
+def add_globalproperty(root,elem,name,initialValue,
+ isNumeric = False,
+ min = "null",
+ max = "null",
+ wrap = False,
+ description = ""):
+ return add_node(root,elem,MODULE+'properties.GlobalProperty',
+ name = name,
+ initialValue = initialValue,
+ isNumeric = isNumeric,
+ min = min,
+ max = max,
+ wrap = wrap,
+ description = description)
+
+# --------------------------------------------------------------------
+def add_turntrack(root, elem, name,
+ buttonText = 'Turn',
+ hotkey = '',
+ icon = '',
+ length = -1,
+ lengthStyle = 'Maximum',
+ nexthotkey = key('T',ALT),
+ plusButtonSize = 22,
+ prevhotkey = key('T',ALT_SHIFT),
+ reportFormat = '<$PlayerId$> Turn updated from $oldTurn$ to $newTurn$',
+ turnButtonHeight = 22,
+ turnFormat = None,
+ counter = None,
+ phases = None):
+
+ levels = (counter if counter is not None else
+ phases if phases is not None else None)
+ if levels is not None:
+ lvl = 1
+ lvls = [f'$level{lvl}$']
+ sub = levels
+ while True:
+ sub = sub.get('counter',sub.get('phases',None))
+ if sub is None:
+ break
+ lvl += 1
+ lvls.append(f'$level{lvl}$')
+
+ turnFormat = ' '.join(lvls)
+
+ if turnFormat is None:
+ turnFormat = '$level1$ $level2$ $level3$ $level4$'
+
+ t = add_node(root, elem, MODULE+'turn.TurnTracker',
+ name = name,
+ buttonText = buttonText,
+ hotkey = hotkey,
+ icon = icon,
+ length = length,
+ lengthStyle = lengthStyle,
+ nexthotkey = nexthotkey,
+ plusButtonSize = plusButtonSize,
+ prevhotkey = prevhotkey,
+ reportFormat = reportFormat,
+ turnButtonHeight = turnButtonHeight,
+ turnFormat = turnFormat)
+
+ add_turnlevel(root, t, counter=counter, phases=phases)
+
+ return t
+
+# --------------------------------------------------------------------
+def add_turnlevel(root,parent,counter=None,phases=None):
+ f,s = ((add_turncounter,counter) if counter is not None else
+ (add_turnlist,phases) if phases is not None else (None,None))
+ if f is None: return parent
+
+ subcounter = s.pop('counter',None)
+ subphases = s.pop('phases',None)
+
+ sub = f(root,parent,**s)
+
+ return add_turnlevel(root,sub,counter=subcounter,phases=subphases)
+
+# --------------------------------------------------------------------
+def add_turncounter(root,elem,property,
+ incr = 1,
+ loop = False,
+ loopLimit = -1,
+ turnFormat = "$value$"):
+ c = add_node(root,elem,MODULE+"turn.CounterTurnLevel",
+ property = property,
+ incr = incr,
+ loop = loop,
+ loopLimit = loopLimit,
+ turnFormat = turnFormat)
+ return c
+
+# --------------------------------------------------------------------
+def add_turnlist(root,elem,property,names,
+ configFirst = False,
+ configList = False,
+ turnFormat = '$value$'):
+ c = add_node(root,elem,MODULE+"turn.ListTurnLevel",
+ property = property,
+ list = ','.join([str(p) for p in names]),
+ configFirst = configFirst,
+ configList = configList,
+ turnFormat = turnFormat)
+ return c
+
+# --------------------------------------------------------------------
+def add_turnhotkey(root,elem,
+ hotkey,
+ match = '{phase=="first"}',
+ reportFormat = '{PlayerName}',
+ name = ''):
+ return add_node(root,elem,MODULE+'turn.TurnGlobalHotkey',
+ hotkey = hotkey,
+ match = match,
+ reportFormat = reportFormat,
+ name = name)
+
+
+
+# --------------------------------------------------------------------
+def add_translatable(root,elem):
+ return add_node(root,elem,MODULE+'properties.GlobalTranslatableMessages')
+
+# --------------------------------------------------------------------
+def add_language(root,elem):
+ return add_node(root,elem,'VASSAL.i18n.Language')
+
+# --------------------------------------------------------------------
+def add_chatter(root,elem):
+ return add_node(root,elem,MODULE+'Chatter')
+
+# --------------------------------------------------------------------
+def add_keynamer(root,elem):
+ return add_node(root,elem,MODULE+'KeyNamer')
+
+# --------------------------------------------------------------------
+def add_documentation(root,doc):
+ return add_node(root,doc,MODULE+'Documentation')
+
+# --------------------------------------------------------------------
+def add_about(root,doc,title,fileName=""):
+ return add_node(root,doc,MODULE+'documentation.AboutScreen',
+ fileName = fileName,
+ title = title)
+# --------------------------------------------------------------------
+def add_pdffile(root,doc,title,pdfFile):
+ return add_node(root,doc,MODULE+'documentation.BrowserPDFFile',
+ pdfFile = pdfFile, title=title)
+# --------------------------------------------------------------------
+def add_helpfile(root,doc,title,fileName,fileType="archive"):
+ return add_node(root,doc,MODULE+'documentation.HelpFile',
+ fileName = fileName,
+ fileType = fileType,
+ title = title)
+
+# --------------------------------------------------------------------
+def add_htmlfile(root,doc,title,startingPage='index.html'):
+ return add_node(root,doc,MODULE+'documentation.BrowserHelpFile',
+ startingPage=startingPage,
+ title=title)
+
+# --------------------------------------------------------------------
+def add_tutorial(root,doc,
+ name = 'Tutorial',
+ logfile = 'tutorial.vlog',
+ promptMessage = 'Load the tutorial?',
+ welcomeMessage = 'Press "Step forward" (PnDn) to step through the tutorial',
+ launchOnStartup = True):
+ return add_node(root,doc,MODULE+'documentation.Tutorial',
+ name = name,
+ logfile = logfile,
+ promptMessage = promptMessage,
+ welcomeMessage = welcomeMessage,
+ launchOnStartup = launchOnStartup)
+
+# --------------------------------------------------------------------
+def add_roster(root,doc,buttonKeyStroke='',
+ buttonText='Retire',
+ buttonToolTip='Switch sides, become observer, or release faction'):
+ return add_node(root,doc,MODULE+'PlayerRoster',
+ buttonKeyStroke = buttonKeyStroke,
+ buttonText = buttonText,
+ buttonToolTip = buttonToolTip)
+
+# --------------------------------------------------------------------
+def add_entry(root,doc,text):
+ n = add_node(root,doc,'entry')
+ add_text(root,n,text)
+ return n
+
+# --------------------------------------------------------------------
+def add_globaloptions(root,doc,
+ autoReport = "Use Preferences Setting",
+ centerOnMove = "Use Preferences Setting",
+ chatterHTMLSupport = "Always",
+ hotKeysOnClosedWindows = "Never",
+ inventoryForAll = "Always",
+ nonOwnerUnmaskable = "Use Preferences Setting",
+ playerIdFormat = "$playerName$",
+ promptString = "Opponents can unmask pieces",
+ sendToLocationMoveTrails = "Never"):
+ return add_node(root,doc,MODULE+'GlobalOptions',
+ autoReport = autoReport,
+ centerOnMove = centerOnMove,
+ chatterHTMLSupport = chatterHTMLSupport,
+ hotKeysOnClosedWindows = hotKeysOnClosedWindows,
+ inventoryForAll = inventoryForAll,
+ nonOwnerUnmaskable = nonOwnerUnmaskable,
+ playerIdFormat = playerIdFormat,
+ promptString = promptString,
+ sendToLocationMoveTrails = sendToLocationMoveTrails)
+
+# --------------------------------------------------------------------
+def add_option(root,doc,name,value):
+ n = add_node(root,doc,'option',name=name)
+ add_text(n,root,value)
+ return n
+
+# --------------------------------------------------------------------
+# CurrentMap == &quot;Board&quot;
+def add_inventory(root,doc,
+ canDisable = False,
+ centerOnPiece = True,
+ disabledIcon = '',
+ drawPieces = True,
+ foldersOnly = False,
+ forwardKeystroke = True,
+ groupBy = '',
+ hotkey = key('I',ALT),
+ icon = '/images/inventory.gif',
+ include = '{}',
+ launchFunction = 'functionHide',
+ leafFormat = '$PieceName$',
+ name = '',
+ nonLeafFormat = '$PropertyValue$',
+ pieceZoom = '0.33',
+ pieceZoom2 = '0.5',
+ pieceZoom3 = '0.6',
+ propertyGate = '',
+ refreshHotkey = key('I',ALT_SHIFT),
+ showMenu = True,
+ sides = '',
+ sortFormat = '$PieceName$',
+ sortPieces = True,
+ sorting = 'alpha',
+ text = '',
+ tooltip = 'Show inventory of all pieces',
+ zoomOn = False):
+ return add_node(root,doc,MODULE+'Inventory',
+ canDisable = canDisable,
+ centerOnPiece = centerOnPiece,
+ disabledIcon = disabledIcon,
+ drawPieces = drawPieces,
+ foldersOnly = foldersOnly,
+ forwardKeystroke = forwardKeystroke,
+ groupBy = groupBy,
+ hotkey = hotkey,
+ icon = icon,
+ include = include,
+ launchFunction = launchFunction,
+ leafFormat = leafFormat,
+ name = name,
+ nonLeafFormat = nonLeafFormat,
+ pieceZoom = pieceZoom,
+ pieceZoom2 = pieceZoom2,
+ pieceZoom3 = pieceZoom3,
+ propertyGate = propertyGate,
+ refreshHotkey = refreshHotkey,
+ showMenu = showMenu,
+ sides = sides,
+ sortFormat = sortFormat,
+ sortPieces = sortPieces,
+ sorting = sorting,
+ text = text,
+ tooltip = tooltip,
+ zoomOn = zoomOn)
+
+
+
+# --------------------------------------------------------------------
+def add_map(root,doc,
+ mapName,
+ allowMultiple = 'false',
+ backgroundcolor = rgb(255,255,255),
+ buttonName = '',
+ changeFormat = '$message$',
+ color = rgb(0,0,0),
+ createFormat = '$pieceName$ created in $location$ *',
+ edgeHeight = '0',
+ edgeWidth = '0',
+ hideKey = '',
+ hotkey = key('M',ALT),
+ icon = '/images/map.gif',
+ launch = 'false',
+ markMoved = 'Always',
+ markUnmovedHotkey = '',
+ markUnmovedIcon = '/images/unmoved.gif',
+ markUnmovedReport = '',
+ markUnmovedText = '',
+ markUnmovedTooltip = 'Mark all pieces on this map as not moved',
+ moveKey = '',
+ moveToFormat = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
+ moveWithinFormat = '$pieceName$ moves $previousLocation$ &amp;rarr; $location$ *',
+ showKey = '',
+ thickness = '3',
+ cls = MODULE+'Map'):
+ return add_node(root,doc,cls,
+ allowMultiple = allowMultiple,
+ backgroundcolor = backgroundcolor,
+ buttonName = buttonName,
+ changeFormat = changeFormat,
+ color = color,
+ createFormat = createFormat,
+ edgeHeight = edgeHeight,
+ edgeWidth = edgeWidth,
+ hideKey = hideKey,
+ hotkey = hotkey,
+ icon = icon,
+ launch = launch,
+ mapName = mapName,
+ markMoved = markMoved,
+ markUnmovedHotkey = markUnmovedHotkey,
+ markUnmovedIcon = markUnmovedIcon,
+ markUnmovedReport = markUnmovedReport,
+ markUnmovedText = markUnmovedText,
+ markUnmovedTooltip = markUnmovedTooltip,
+ moveKey = moveKey,
+ moveToFormat = moveToFormat,
+ moveWithinFormat = moveWithinFormat,
+ showKey = showKey,
+ thickness = thickness)
+
+# --------------------------------------------------------------------
+def add_widgetmap(root,doc, mapName,**attr):
+ return add_map(root,doc,mapName,cls=WIDGET+'WidgetMap',**attr)
+
+# --------------------------------------------------------------------
+def add_boardpicker(root,doc,
+ addColumnText = 'Add column',
+ addRowText = 'Add row',
+ boardPrompt = 'Select board',
+ slotHeight = 125,
+ slotScale = 0.2,
+ slotWidth = 350,
+ title = 'Choose Boards'):
+ return add_node(root,doc,MAP+'BoardPicker',
+ addColumnText = addColumnText,
+ addRowText = addRowText,
+ boardPrompt = boardPrompt,
+ slotHeight = slotHeight,
+ slotScale = slotScale,
+ slotWidth = slotWidth,
+ title = title)
+
+# --------------------------------------------------------------------
+def add_setup(root,picker,mapName,maxColumns,boardNames):
+ '''Add given boards in row-first up to maxColumns per row'''
+ s = add_node(root,picker,'setup')
+ col = 0
+ row = 0
+ lst = [f'{mapName}BoardPicker']
+ for bn in boardNames:
+ lst.extend([bn,str(col),str(row)])
+ col += 1
+ if col >= maxColumns:
+ col = 0
+ row += 1
+ txt = r' '.join(lst)
+ add_text(root,s,txt)
+
+# --------------------------------------------------------------------
+def add_board(root,picker,name,
+ image = '',
+ reversible = False,
+ color = rgb(255,255,255),
+ width = 0,
+ height = 0):
+ return add_node(root,picker,PICKER+'Board',
+ image = image,
+ name = name,
+ reversible = reversible,
+ color = color,
+ width = width,
+ height = height)
+
+# --------------------------------------------------------------------
+def add_zoned(root,board):
+ return add_node(root,board,PICKER+'board.ZonedGrid')
+
+# --------------------------------------------------------------------
+def add_zonehighlighter(root,zoned):
+ return add_node(root,zoned,PICKER+'board.mapgrid.ZonedGridHighlighter')
+
+# --------------------------------------------------------------------
+def add_zone(root,zoned,
+ name = "",
+ highlightProperty = "",
+ locationFormat = "$gridLocation$",
+ path = "0,0;976,0;976,976;0,976",
+ useHighlight = False,
+ useParentGrid = False):
+ return add_node(root,zoned,PICKER+'board.mapgrid.Zone',
+ name = name,
+ highlightProperty = highlightProperty,
+ locationFormat = locationFormat,
+ path = path,
+ useHighlight = useHighlight,
+ useParentGrid = useParentGrid)
+
+# --------------------------------------------------------------------
+HEX_WIDTH = 88.50779626676963
+HEX_HEIGHT = 102.2
+def add_hexgrid(root,zone,
+ color = rgb(0,0,0),
+ cornersLegal = False,
+ dotsVisible = False,
+ dx = HEX_WIDTH,
+ dy = HEX_HEIGHT,
+ edgesLegal = False,
+ sideways = False,
+ snapTo = True,
+ visible = True,
+ x0 = 0,
+ y0 = 32,
+ cls = PICKER+'board.HexGrid'):
+ return add_node(root,zone,cls,
+ color = color,
+ cornersLegal = cornersLegal,
+ dotsVisible = dotsVisible,
+ dx = dx,
+ dy = dy,
+ edgesLegal = edgesLegal,
+ sideways = sideways,
+ snapTo = snapTo,
+ visible = visible,
+ x0 = x0,
+ y0 = y0)
+
+# --------------------------------------------------------------------
+def add_hexnumbering(root,grid,
+ color = rgb(255,0,0),
+ first = 'H',
+ fontSize = 24,
+ hDescend = False,
+ hDrawOff = 0,
+ hLeading = 1,
+ hOff = 0,
+ hType = 'A',
+ locationFormat = '$gridLocation$',
+ rotateText = 0,
+ sep = '',
+ stagger = True,
+ vDescend = False,
+ vDrawOff = 32,
+ vLeading = 0,
+ vOff = 0,
+ vType = 'N',
+ visible = True,
+ cls = PICKER+'board.mapgrid.HexGridNumbering'):
+ return add_node(root,grid,cls,
+ color = color,
+ first = first,
+ fontSize = fontSize,
+ hDescend = hDescend,
+ hDrawOff = hDrawOff,
+ hLeading = hLeading,
+ hOff = hOff,
+ hType = hType,
+ locationFormat = locationFormat,
+ rotateText = rotateText,
+ sep = sep,
+ stagger = stagger,
+ vDescend = vDescend,
+ vDrawOff = vDrawOff,
+ vLeading = vLeading,
+ vOff = vOff,
+ vType = vType,
+ visible = visible)
+
+# --------------------------------------------------------------------
+RECT_WIDTH = 80
+RECT_HEIGHT = 80
+def add_squaregrid(root,zone,
+ dx = RECT_WIDTH,
+ dy = RECT_HEIGHT,
+ edgesLegal = False,
+ x0 = 0,
+ y0 = int(0.4*RECT_HEIGHT),
+ **attr):
+ return add_hexgrid(root,zone,cls=PICKER+'board.SquareGrid',
+ dx = dx,
+ dy = dy,
+ edgesLegal = edgesLegal,
+ x0 = x0,
+ y0 = y0,
+ **attr)
+
+# --------------------------------------------------------------------
+def add_squarenumbering(root,grid,
+ hType = 'N',
+ **attr):
+ return add_hexnumbering(root,grid,
+ cls=PICKER+'board.mapgrid.SquareGridNumbering',
+ hType = hType,
+ **attr)
+
+# --------------------------------------------------------------------
+def add_regiongrid(root,zone,snapto=True,fontsize=9,visible=True):
+ # print(f'Make region grid visible: {visible}')
+ return add_node(root,zone,PICKER+'board.RegionGrid',
+ fontsize = fontsize,
+ snapto = snapto,
+ visble = visible)
+
+# --------------------------------------------------------------------
+def get_region_piece(root,grid,name,alsoPiece=True,piece=None,verbose=False):
+ if not alsoPiece:
+ return None, None
+
+ # Find the parent map element by stepping up the tree
+ from functools import reduce
+
+ map = reduce(lambda nn, _: None if nn is None else nn.parentNode,
+ range(5), grid)
+ if map is None or \
+ map.tagName not in [MODULE+'Map',WIDGET+'WidgetMap']:
+ if map is None:
+ print(f' = Map not found from grid')
+ return None, None
+
+ if piece is not None:
+ print(f' Using supplied piece')
+ return piece, map
+
+ # Find the piece by searching from the top of the tree
+ piece = get_pieces(root,root).get(name,None)
+ # if piece is None:
+ # print(list(get_pieces(root,root).keys()))
+ if verbose:
+ print(f' Possible piece for region {name}: {piece}')
+ return piece, map
+
+# --------------------------------------------------------------------
+def check_region_name(grid,name):
+ post = ''
+ targ = name
+ same = 0
+ regs = list(get_regions(grid).keys())
+ poss = len([e for e in regs if e.startswith(name)])
+ if poss == 0:
+ return name
+
+ return name + f' {poss}'
+ # Loop over keys, sorted
+ # regs.sort()
+ # for r in regs:
+ # if targ == r:
+ # same += 1
+ # targ = name + f' {same:02d}'
+ # print(f' Found region named {r} -> {targ}')
+
+ return targ
+
+# --------------------------------------------------------------------
+def add_region(root,grid,name,originx,originy,gpid,
+ alsoPiece=True,piece=None,verbose=True):
+ # First thing is to check if we have a piece
+ piece, map = get_region_piece(root,grid,name,alsoPiece,piece,verbose)
+ if verbose:
+ print(f' Region {name} -> piece {piece}')
+
+
+ nam = check_region_name(grid,name)
+ if verbose:
+ print(f' Region {name} -> real name {nam}')
+ n = add_node(root,grid,PICKER+'board.Region',
+ name = nam,
+ originx = originx,
+ originy = originy)
+ if piece is None:
+ return n, gpid
+
+ if verbose:
+ print(f' Add piece {name} to region')
+ _, gpid = add_atstart(root,map,name,piece,
+ location = nam,
+ useGridLocation = True,
+ owningBoard = map.getAttribute('mapName'),
+ x = 0,
+ y = 0,
+ gpid = gpid)
+ return n, gpid
+
+
+# --------------------------------------------------------------------
+def add_stacking(root,map,
+ bottom = '40,0',
+ disabled = False,
+ down = '37,0',
+ exSepX = 6,
+ exSepY = 18,
+ top = '38,0',
+ unexSepX = 8,
+ unexSepY = 16,
+ up = '39,0'):
+ return add_node(root,map,MAP+'StackMetrics',
+ bottom = bottom,
+ disabled = disabled,
+ down = down,
+ exSepX = exSepX,
+ exSepY = exSepY,
+ top = top,
+ unexSepX = unexSepX,
+ unexSepY = unexSepY,
+ up = up)
+
+# --------------------------------------------------------------------
+def add_camera(root,map,
+ buttonText = '',
+ canDisable = False,
+ hotkey = '',
+ icon = '/images/camera.gif',
+ propertyGate = '',
+ tooltip = 'Save map as PNG image'):
+ return add_node(root,map,MAP+'ImageSaver',
+ buttonText = buttonText,
+ canDisable = canDisable,
+ hotkey = hotkey,
+ icon = icon,
+ propertyGate = propertyGate,
+ tooltip = tooltip)
+
+# --------------------------------------------------------------------
+def add_forwardtochatter(root,map):
+ return add_node(root,map,MAP+'ForwardToChatter')
+
+# --------------------------------------------------------------------
+def add_menudisplayer(root,map):
+ return add_node(root,map,MAP+'MenuDisplayer')
+
+# --------------------------------------------------------------------
+def add_mapcenterer(root,map):
+ return add_node(root,map,MAP+'MapCenterer')
+
+# --------------------------------------------------------------------
+def add_stackexpander(root,map):
+ return add_node(root,map,MAP+'StackExpander')
+
+# --------------------------------------------------------------------
+def add_piecemover(root,map):
+ return add_node(root,map,MAP+'PieceMover')
+
+# --------------------------------------------------------------------
+def add_selectionhighlighters(root,map):
+ return add_node(root,map,MAP+'SelectionHighlighters')
+
+# --------------------------------------------------------------------
+def add_keybufferer(root,map):
+ return add_node(root,map,MAP+'KeyBufferer')
+
+# --------------------------------------------------------------------
+def add_highlightlastmoved(root,map,color=rgb(255,0,0),enabled=True,thickness=2):
+ return add_node(root,map,MAP+'HighlightLastMoved',
+ color=color,enabled=enabled,thickness=thickness)
+
+# --------------------------------------------------------------------
+def add_counterviewer(root,map,
+ borderWidth = 0,
+ centerAll = False,
+ centerText = False,
+ combineCounterSummary = False,
+ counterReportFormat = '',
+ delay = 700,
+ description = '',
+ display = 'from top-most layer only',
+ emptyHexReportForma = '$LocationName$',
+ enableHTML = True,
+ extraTextPadding = 0,
+ fgColor = rgb(0,0,0),
+ fontSize = 9,
+ graphicsZoom = 1.0,
+ hotkey = key('\n',0),
+ layerList = '',
+ minDisplayPieces = 2,
+ propertyFilter = '',
+ showDeck = False,
+ showDeckDepth = 1,
+ showDeckMasked = False,
+ showMoveSelectde = False,
+ showNoStack = False,
+ showNonMovable = False,
+ showOverlap = False,
+ showgraph = True,
+ showgraphsingle = False,
+ showtext = False,
+ showtextsingle = False,
+ stretchWidthSummary = False,
+ summaryReportFormat = '$LocationName$',
+ unrotatePieces = False,
+ version = 3,
+ verticalOffset = 0,
+ verticalTopText = 5,
+ zoomlevel = 1.0):
+ return add_node(root,map,MAP+'CounterDetailViewer',
+ borderWidth = borderWidth,
+ centerAll = centerAll,
+ centerText = centerText,
+ combineCounterSummary = combineCounterSummary,
+ counterReportFormat = counterReportFormat,
+ delay = delay,
+ description = description,
+ display = display,
+ emptyHexReportForma = emptyHexReportForma,
+ enableHTML = enableHTML,
+ extraTextPadding = extraTextPadding,
+ fgColor = fgColor,
+ fontSize = fontSize,
+ graphicsZoom = graphicsZoom,
+ hotkey = hotkey,
+ layerList = layerList,
+ minDisplayPieces = minDisplayPieces,
+ propertyFilter = propertyFilter,
+ showDeck = showDeck,
+ showDeckDepth = showDeckDepth,
+ showDeckMasked = showDeckMasked,
+ showMoveSelectde = showMoveSelectde,
+ showNoStack = showNoStack,
+ showNonMovable = showNonMovable,
+ showOverlap = showOverlap,
+ showgraph = showgraph,
+ showgraphsingle = showgraphsingle,
+ showtext = showtext,
+ showtextsingle = showtextsingle,
+ stretchWidthSummary = stretchWidthSummary,
+ summaryReportFormat = summaryReportFormat,
+ unrotatePieces = unrotatePieces,
+ version = version,
+ verticalOffset = verticalOffset,
+ verticalTopText = verticalTopText,
+ zoomlevel = zoomlevel)
+
+# --------------------------------------------------------------------
+def add_globalmap(root,map,
+ buttonText = '',
+ color = rgb(255,0,0),
+ hotkey = key('O',CTRL_SHIFT),
+ icon = '/images/overview.gif',
+ scale = 0.2,
+ tooltip = 'Show/Hide overview window'):
+ return add_node(root,map,MAP+'GlobalMap',
+ buttonText = buttonText,
+ color = color,
+ hotkey = hotkey,
+ icon = icon,
+ scale = scale,
+ tooltip = 'Show/Hide overview window')
+
+# --------------------------------------------------------------------
+def add_zoomer(root,map,
+ inButtonText = '',
+ inIconName = '/images/zoomIn.gif',
+ inTooltip = 'Zoom in',
+ outButtonText = '',
+ outIconName = '/images/zoomOut.gif',
+ outTooltip = 'Zoom out',
+ pickButtonText = '',
+ pickIconName = '/images/zoom.png',
+ pickTooltip = 'Select Zoom',
+ zoomInKey = key('=',CTRL_SHIFT),
+ zoomLevels = [0.2,0.25,0.333,0.4,0.5,0.555,0.625,0.75,1.0,1.25,1.6],
+ zoomOutKey = key('-'),
+ zoomPickKey = key('='),
+ zoomStart = 3):
+ '''Zoom start is counting from the back (with default zoom levels,
+ and zoom start, the default zoom is 1'''
+ lvls = ','.join([str(z) for z in zoomLevels])
+ return add_node(root,map,MAP+'Zoomer',
+ inButtonText = inButtonText,
+ inIconName = inIconName,
+ inTooltip = inTooltip,
+ outButtonText = outButtonText,
+ outIconName = outIconName,
+ outTooltip = outTooltip,
+ pickButtonText = pickButtonText,
+ pickIconName = pickIconName,
+ pickTooltip = pickTooltip,
+ zoomInKey = zoomInKey,
+ zoomLevels = lvls,
+ zoomOutKey = zoomOutKey,
+ zoomPickKey = zoomPickKey,
+ zoomStart = zoomStart)
+
+# --------------------------------------------------------------------
+def add_hidepieces(root,map,
+ buttonText = '',
+ hiddenIcon = '/images/globe_selected.gif',
+ hotkey = key('O'),
+ showingIcon = '/images/globe_unselected.gif',
+ tooltip = 'Hide all pieces on this map'):
+ return add_node(root,map,MAP+'HidePiecesButton',
+ buttonText = buttonText,
+ hiddenIcon = hiddenIcon,
+ hotkey = hotkey,
+ showingIcon = showingIcon,
+ tooltip = tooltip)
+# --------------------------------------------------------------------
+def add_masskey(root,map,
+ name,
+ buttonHotkey,
+ hotkey,
+ buttonText = '',
+ canDisable = False,
+ deckCount = '-1',
+ filter = '',
+ propertyGate = '',
+ reportFormat = '',
+ reportSingle = False,
+ singleMap = True,
+ target = 'MAP|false|MAP|||||0|0||true|Selected|true|EQUALS',
+ tooltip = '',
+ icon = ''):
+ '''Default targets are selected units'''
+ return add_node(root,map,MAP+'MassKeyCommand',
+ name = name,
+ buttonHotkey = buttonHotkey, # This hot key
+ hotkey = hotkey, # Target hot key
+ buttonText = buttonText,
+ canDisable = canDisable,
+ deckCount = deckCount,
+ filter = filter,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportSingle = reportSingle,
+ singleMap = singleMap,
+ target = target,
+ tooltip = tooltip,
+ icon = icon)
+
+
+# --------------------------------------------------------------------
+def add_flare(root,map,
+ circleColor = rgb(255,0,0),
+ circleScale = True,
+ circleSize = 100,
+ flareKey = 'keyAlt',
+ flareName = 'Map Flare',
+ flarePulses = 6,
+ flarePulsesPerSec = 3,
+ reportFormat = ''):
+ return add_node(root,map,MAP+'Flare',
+ circleColor = circleColor,
+ circleScale = circleScale,
+ circleSize = circleSize,
+ flareKey = flareKey,
+ flareName = flareName,
+ flarePulses = flarePulses,
+ flarePulsesPerSec = flarePulsesPerSec,
+ reportFormat = '')
+
+# --------------------------------------------------------------------
+def add_atstart(root,map,name,*pieces,
+ location = '',
+ useGridLocation = True,
+ owningBoard = '',
+ x = 0,
+ y = 0,
+ gpid = None):
+ '''Pieces are existing PieceSlot elements'''
+ a = add_node(root,map,MODULE+'map.SetupStack',
+ name = name,
+ location = location,
+ owningBoard = owningBoard,
+ useGridLocation = useGridLocation,
+ x = x,
+ y = y)
+ # copy pieces here
+ for p in pieces:
+ c = p.cloneNode(True)
+ a.appendChild(c)
+ if gpid is not None:
+ # Update gpid in element
+ c.setAttribute('gpid',str(gpid))
+ # Update gpid in traits state
+ traits = get_piece_parts(p.childNodes[0].nodeValue)
+ for t in traits:
+ code = t['def']
+ stat = t['state']
+ if code[0] == 'piece':
+ stat[3] = gpid
+ # Put back the traits
+ c.childNodes[0].nodeValue = dicts_to_piece(traits)
+
+ gpid += 1
+
+ return a, gpid
+
+# --------------------------------------------------------------------
+def add_pieceslot(root,elem,
+ entryName,
+ gpid,
+ code,
+ height=72,
+ width=72,
+ icon = ''):
+ p = add_node(root,elem,WIDGET+'PieceSlot',
+ entryName = entryName,
+ gpid = gpid,
+ height = height,
+ width = width,
+ icon = icon)
+ add_text(root,p,code)
+ return p
+
+# --------------------------------------------------------------------
+def add_prototypes(root,elem,*defs):
+ '''Defs are existing definition elements'''
+ p = add_node(root,elem,MODULE+'PrototypesContainer')
+
+ for d in defs:
+ c = d.cloneNode(True)
+ p.appendChild(c)
+
+ return p
+
+# --------------------------------------------------------------------
+def add_prototype(root,elem,name,code,description=''):
+ d = add_node(root,elem,MODULE+'PrototypeDefinition',
+ name = name,
+ description = description)
+ add_text(root,d,code)
+ return d
+
+# --------------------------------------------------------------------
+def add_piecewindow(root,elem,name,
+ defaultWidth = 0,
+ hidden = False,
+ hotkey = key('C',ALT),
+ scale = 1.,
+ text = '',
+ tooltip = 'Show/hide piece window',
+ icon = '/images/counter.gif'):
+ return add_node(root,elem,MODULE+'PieceWindow',
+ name = name,
+ defaultWidth = defaultWidth,
+ hidden = hidden,
+ hotkey = hotkey,
+ scale = scale,
+ text = text,
+ tooltip = tooltip,
+ icon = icon)
+
+# --------------------------------------------------------------------
+def add_tabs(root,elem,entryName):
+ return add_node(root,elem,WIDGET+'TabWidget',entryName=entryName)
+
+# --------------------------------------------------------------------
+def add_list(root,elem,
+ entryName,
+ height = 0,
+ width = 0,
+ scale = 1.,
+ divider = 100):
+ return add_node(root,elem,WIDGET+'ListWidget',
+ entryName = entryName,
+ height = height,
+ width = width,
+ scale = scale,
+ divider = divider)
+
+# --------------------------------------------------------------------
+def add_panel(root,elem,
+ entryName,
+ fixed = False,
+ nColumns = 1,
+ vert = False):
+ return add_node(root,elem,WIDGET+'PanelWidget',
+ entryName = entryName,
+ fixed = fixed,
+ nColumns = nColumns,
+ vert = vert)
+
+# --------------------------------------------------------------------
+def add_mapwidget(root,elem,entryName=''):
+ return add_node(root,elem,WIDGET+'MapWidget',entryName=entryName)
+
+# --------------------------------------------------------------------
+def add_chartwindow(root,elem,
+ name,
+ hotkey = key('A',ALT),
+ description = '',
+ text = '',
+ tooltip = 'Show/hide Charts',
+ icon = '/images/chart.gif'):
+ return add_node(root,elem,MODULE+'ChartWindow',
+ name = name,
+ hotkey = hotkey,
+ description = description,
+ text = text,
+ tooltip = tooltip,
+ icon = icon)
+
+# --------------------------------------------------------------------
+def add_chart(root,elem,
+ chartName,
+ fileName,
+ description = ''):
+ return add_node(root,elem,WIDGET+'Chart',
+ chartName = chartName,
+ description = description,
+ fileName = fileName)
+
+# --------------------------------------------------------------------
+def add_dice(root,elem,
+ addToTotal = 0,
+ canDisable = False,
+ hotkey = key('6',ALT),
+ icon = '/images/die.gif',
+ keepCount = 1,
+ keepDice = False,
+ keepOption = '&gt;',
+ lockAdd = False,
+ lockDice = False,
+ lockPlus = False,
+ lockSides = False,
+ nDice = 1,
+ nSides = 6,
+ name = '1d6',
+ plus = 0,
+ prompt = False,
+ propertyGate = '',
+ reportFormat = '** $name$ = $result$ *** &amp;lt;$PlayerName$&amp;gt;',
+ reportTotal = False,
+ sortDice = False,
+ text = '1d6',
+ tooltip = 'Roll a 1d6'):
+ return add_node(root,elem,MODULE+'DiceButton',
+ addToTotal = addToTotal,
+ canDisable = canDisable,
+ hotkey = hotkey,
+ icon = icon,
+ keepCount = keepCount,
+ keepDice = keepDice,
+ keepOption = keepOption,
+ lockAdd = lockAdd,
+ lockDice = lockDice,
+ lockPlus = lockPlus,
+ lockSides = lockSides,
+ nDice = nDice,
+ nSides = nSides,
+ name = name,
+ plus = plus,
+ prompt = prompt,
+ propertyGate = propertyGate,
+ reportFormat = reportFormat,
+ reportTotal = reportTotal,
+ sortDice = sortDice,
+ text = text,
+ tooltip = tooltip)
+
+# ====================================================================
+def enc_def(args,term=False):
+ '''Encode a piece (decorator) definition
+
+ Parameters
+ ----------
+ args : list
+ List of fields
+ term : bool
+ If true, add terminating ';'
+
+ Returns
+ -------
+ code : str
+ The encoded definition
+ '''
+ return ';'.join([str(e).lower() if isinstance(e,bool) else str(e)
+ for e in args])+(';' if term else '')
+
+# --------------------------------------------------------------------
+def layer_trait(images,
+ newNames = None,
+ activateName = 'Activate',
+ activateMask = CTRL,
+ activateChar = 'A',
+ increaseName = 'Increase',
+ increaseMask = CTRL,
+ increaseChar = '[',
+ decreaseName = '',
+ decreaseMask = CTRL,
+ decreaseChar = ']',
+ resetName = '',
+ resetKey = '',
+ resetLevel = 1,
+ under = False,
+ underXoff = 0,
+ underYoff = 0,
+ loop = True,
+ name = '',
+ description = '',
+ randomKey = '',
+ randomName = '',
+ follow = False,
+ expression = '',
+ first = 1,
+ version = 1, # 1:new, 0:old
+ always = 'true',
+ activateKey = key('A'),
+ increaseKey = key('['),
+ decreaseKey = key(']'),
+ scale = 1.):
+ '''Create a layer trait (VASSAL.counter.Embellishment)
+
+ Parameters
+ ----------
+ main : dict
+ Main image dictionary
+ flipped : dict
+ Flipped image dictionary
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state
+ '''
+ if newNames is None:
+ newNames = ['']*len(images)
+ args = ['emb2', # Code
+ activateName, activateMask, activateChar, # ACTIVATE ; MASK ; KEY
+ increaseName, increaseMask, increaseChar,
+ decreaseName, decreaseMask, decreaseChar,
+ resetName, resetKey, resetLevel,
+ under, underXoff, underYoff,
+ ','.join(images),
+ ','.join(newNames),
+ loop,
+ name,
+ randomKey, randomName,
+ follow, expression,
+ first,
+ version,
+ always,
+ activateKey,
+ increaseKey,
+ decreaseKey,
+ description,
+ scale ]
+ stat = '1' # CURRENT LEVEL
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def prototype_trait(name):
+ '''Create a prototype trait (VASSAL.counter.UsePrototype)
+
+ Parameters
+ ----------
+ family : str
+ Side in game
+
+ Return
+ ------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['prototype', name ]
+ stat = '' # State is nothing
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def mark_trait(key,value):
+ '''Create a marker trait (VASSAL.counter.Marker)
+
+ Parameters
+ ----------
+ family : str
+ Side in game
+
+ Return
+ ------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['mark', key ]
+ stat = value # State is nothing
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def report_trait(*keys,
+ nosuppress = True,
+ description = '',
+ report = '$location$: $newPieceName$ $menuCommand$ *',
+ cyclekeys = '',
+ cyclereps = ''):
+ '''Create a report trait (VASSAL.counters.ReportActon)
+
+ Parameters
+ ----------
+ *keys : tuple of str
+ The keys to report actions for
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ esckeys = ','.join([k.replace(',',r'\,') for k in keys])
+ esccycl = ','.join([k.replace(',',r'\,') for k in cyclekeys])
+ escreps = ','.join([k.replace(',',r'\,') for k in cyclereps])
+ args = ['report', # CODE
+ esckeys, # ESCAPED KEYS (commas escaped)
+ report, # REPORT
+ esccycl, # CYCLE KEYS (commas escaped)
+ escreps, # CYCKE REPORTS (commas escaped)
+ description, # DESCRIPTION
+ nosuppress] # NOSUPPRESS
+ stat = '-1' # CYCLE STATE
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def moved_trait(image='moved.gif',
+ xoff = 36,
+ yoff = -38,
+ name = 'Mark Moved',
+ key = key('M')):
+ '''Create a moved trait (VASSAL.counters.MovementMarkable)
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['markmoved', # CODE
+ image, # IMAGE
+ xoff, yoff, # XOFF ; YOFF
+ name, # MENU
+ key, # KEY, MODIFIER
+ ''] # ?
+ stat = False # MOVED?
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def trail_trait(key = key('T'),
+ name = 'Movement Trail',
+ localVisible = False,
+ globalVisible = False,
+ radius = 10,
+ fillColor = rgb(255,255,255),
+ lineColor = rgb(0,0,0),
+ activeOpacity = 100,
+ inactiveOpacity = 50,
+ edgesBuffer = 20,
+ displayBuffer = 30,
+ lineWidth = 1,
+ turnOn = '',
+ turnOff = '',
+ reset = '',
+ description = ''):
+ ''' Create a movement trail trait ( VASSAL.counters.Footprint)
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state
+ '''
+ args = ['footprint', # CODE
+ key, # ENABLE KEY
+ name, # MENU
+ localVisible, # LOCAL VISABLE
+ globalVisible, # GLOBAL VISABLE
+ radius, # RADIUS
+ fillColor, # FILL COLOR
+ lineColor, # LINE COLOR
+ activeOpacity, # ACTIVE OPACITY
+ inactiveOpacity, # INACTIVE OPACITY
+ edgesBuffer, # EDGES BUFFER
+ displayBuffer, # DISPLAY BUFFER
+ lineWidth, # LINE WIDTH
+ turnOn, # TURN ON KEY
+ turnOff, # TURN OFF KEY
+ reset, # RESET KEY
+ description] # DESC
+ stat = [False, # GLOBAL VIS
+ '', # MAP
+ 0] # POINTS (followed by [; [X,Y]*]
+ return enc_def(args), enc_def(stat)
+
+# --------------------------------------------------------------------
+def delete_trait(name = 'Delete', key = key('D')):
+ '''Create a delete trait (VASSAL.counters.Delete)
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['delete', name, key, '']
+ stat = '' # Nothing
+ return enc_def(args), stat
+
+# --------------------------------------------------------------------
+def sendto_trait(mapName,
+ boardName,
+ name = '',
+ key = key('E'),
+ restoreName = 'Restore',
+ restoreKey = key('R'),
+ x = 200,
+ y = 200,
+ xidx = 0,
+ yidx = 0,
+ xoff = 1,
+ yoff = 1,
+ description = '',
+ destination = 'L',
+ zone = '',
+ region = '',
+ expression = '',
+ position = ''):
+ '''Create a send to trait (VASSAL.counter.SendToLocation)
+
+ Parameters
+ ----------
+ side : str
+ Sub-map to move to
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['sendto', # CODE
+ name, # NAME
+ key, # KEY , MODIFIER
+ mapName, # MAP
+ boardName, # BOARD
+ x, y, # X ; Y
+ restoreName, # BACK
+ restoreKey, # KEY , MODIFIER
+ xidx, yidx, # XIDX ; YIDX
+ xoff, yoff, # XOFF ; YOFF
+ description, # DESC
+ destination, # DEST
+ zone, # ZONE
+ region, # REGION
+ expression, # EXPRESSION
+ position ] # GRIDPOS
+ stat = ['', # BACKMAP
+ '', # X
+ ''] # Y
+ return enc_def(args), enc_def(stat)
+
+# --------------------------------------------------------------------
+def basic_trait(name,
+ filename, # Can be empty
+ gpid, # Can be empty
+ cloneKey='', # Deprecated
+ deleteKey=''):# Deprecated
+ '''Create a basic unit (VASSAL.counters.BasicPiece)
+
+ Parameters
+ ----------
+ main : dict
+ Information
+ gpid : int
+ Piece global identifier
+ image : bool
+ If true, add image
+
+ Returns
+ -------
+ code : str
+ Encoded trait
+ stat : str
+ Encoded state (empty string)
+ '''
+ args = ['piece', # CODE
+ cloneKey, # CLONEKEY
+ deleteKey, # DELETEKEY
+ filename, # IMAGE
+ name] # NAME
+ stat = ['null', # MAPID (possibly 'null')
+ 0, # X
+ 0, # Y
+ gpid, # GLOBAL PIECE ID
+ 0] # PROPERTY COUNT (followed by [; KEY; VALUE]+)
+ return enc_def(args), enc_def(stat)
+
+# --------------------------------------------------------------------
+def enc_parts(*parts):
+ '''Encode parts of a full piece definition
+
+ Each trait (VASSAL.counter.Decorator, VASSAL.counter.BasicPiece)
+ definition or state is separated by a litteral TAB character.
+ Beyond the first TAB separator, additional escape characters
+ (BACKSLAH) are added in front of the separator. This is to that
+ VASSAL.utils.SequenceDecoder does not see consequitive TABs as a
+ single TAB.
+
+ Parameters
+ ----------
+ parts : tuple
+ The various trait definitions or states
+
+ Returns
+ -------
+ code : str
+ The encoded piece
+ '''
+ ret = ''
+ sep = r' '
+ for i, p in enumerate(parts):
+ if i != 0:
+ ret += '\\'*(i-1) + sep
+ ret += str(p)
+
+ return ret
+
+# --------------------------------------------------------------------
+def add_piece(typ, state):
+ '''Create and add piece command (VASSAL.command.AddPiece)
+
+ Each part is separated by a SLASH ('/'). The first part is the
+ command type (PLUS '+') to perform. Second is the IDENTIFIER,
+ possibly 'null'. Then comes the fully encoded type of the piece,
+ followed by the state of the piece (also fully encoded).
+
+ Parameters
+ ----------
+ typ : str
+ Encoded piece definition
+ state : str
+ Encoded state definition
+
+ Returns
+ -------
+ cmd : str
+ The add command encoded
+ '''
+ args = ['+', # Type
+ 'null', # IDENTIFER
+ typ, # DEFINITION
+ state] # STATE
+ return '/'.join(args)
+
+# --------------------------------------------------------------------
+def add_proto(typ,state):
+ '''Create an add prototype command. This is the same as 'add_piece'
+
+ Parameters
+ ----------
+ typ : str
+ Encoded piece definition
+ state : str
+ Encoded state definition
+
+ Returns
+ -------
+ cmd : str
+ The add command encoded
+ '''
+ return add_piece(typ,state)
+
+
+# --------------------------------------------------------------------
+def add_proto_code(decs):
+ typs = [d[0] for d in decs]
+ stts = [d[1] for d in decs]
+ typ = enc_parts(*typs)
+ stt = enc_parts(*stts)
+
+ return add_proto(typ,stt)
+
+# ====================================================================
+#
+# Below specific to the export app
+#
+# ====================================================================
+def get_bb(buffer):
+ '''Get bounding box of image
+
+ Parameters
+ ----------
+ buffer : bytes
+ The image bytes
+
+ Returns
+ -------
+ ulx, uly, lrx, lry : tuple
+ The coordinates of the bounding box
+ '''
+ from PIL import Image
+ from io import BytesIO
+
+ with Image.open(BytesIO(buffer)) as img:
+ bb = img.getbbox()
+
+ return bb
+
+# ====================================================================
+def piece_body(family,main,flipped,gpid):
+ '''Create an full add piece command from the side, image, and ID
+ information. This adds (in reverse order)
+
+ - BasicPiece
+ - Side prototype
+ - Flip layer (if 'flipped' is not none)
+
+ Parameters
+ ----------
+ family : str
+ Side in the game
+ main : dict
+ Main image information
+ flipped : dict
+ Flipped image information, possibly None
+ gpid : int
+ Global piece identifier
+
+ Returns
+ -------
+ cmd : str
+ The command to add the piece
+ '''
+ # Create the layer, prototype, and basic piece traits
+ decs = []
+ if flipped is not None:
+ imgs = [ main['filename'], flipped['filename'] ]
+ decs.extend([ layer_trait(images=imgs,
+ newNames=['','Reduced +'],
+ activateName = '',
+ decreaseName = '',
+ increaseName = 'Flip',
+ increaseKey = key('F'),
+ decreaseKey = '',
+ name='Step'),
+ report_trait(key('F')) ])
+ for m in main.get('mains',[]):
+ decs.append(prototype_trait(m + ' prototype'))
+ ech = main.get('echelon',None)
+ cmd = main.get('command',None)
+ if ech is not None: decs.append(prototype_trait(ech + ' prototype'))
+ if cmd is not None: decs.append(prototype_trait(cmd + ' prototype'))
+
+ decs.extend([ prototype_trait(family+' prototype'),
+ basic_trait(main['name'], main['filename'],gpid)
+ ])
+
+ typs = [d[0] for d in decs]
+ stts = [d[1] for d in decs]
+ typ = enc_parts(*typs)
+ stt = enc_parts(*stts)
+
+ return add_piece(typ,stt)
+
+# --------------------------------------------------------------------
+def proto_body(family):
+ '''Create an full add prototype com from the side information.
+ This adds (in reverse order)
+
+ - An empty BasicPiece
+ - An eliminate (or sendto) trait
+ - A delete trait
+ - A mark moved trait
+ - A move trail trait
+
+ Parameters
+ ----------
+ family : str
+ Side in the game
+
+ Returns
+ -------
+ cmd : str
+ The command to add the piece
+ '''
+ # Dummy image
+ decs = [ report_trait(key('E'),key('R'),key('M')),
+ moved_trait(),
+ trail_trait(),
+ delete_trait(),
+ sendto_trait('DeadMap',family+' pool','Eliminate',key('E'),
+ 'Restore',key('R'),description="Eliminate unit"),
+ mark_trait('Faction',family),
+ basic_trait('','','') ]
+ typs = [d[0] for d in decs]
+ stts = [d[1] for d in decs]
+ typ = enc_parts(*typs)
+ stt = enc_parts(*stts)
+
+ return add_proto(typ,stt)
+
+
+# --------------------------------------------------------------------
+def add_splash(root,doc,categories,title='',verbose=False):
+ '''Add an about page
+
+ Parametersg
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ doc : xml.dom.minidom.Element
+ The documentation element
+ categories : dict
+ Catalog of images
+ title : str
+ Title of the game
+ '''
+ fronts = categories.get('front',{}).get('all',[])
+ front = list(fronts.values())[0] if len(fronts) > 0 else None
+ if front is None:
+ return
+
+ if verbose:
+ print(f'Adding about page with {title}')
+
+ add_about(root,doc, f'About {title}', fileName = front['filename'])
+
+# --------------------------------------------------------------------
+def add_rules(root,doc,zipfile,rules,verbose=False):
+ '''Add rules to the module
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ doc : xml.dom.minidom.Element
+ The documentation element
+ zipfile : zipfile.ZipFile
+ The module archive
+ rules : str
+ File name of rules (PDF)
+ '''
+ if rules is None or rules == '':
+ return
+
+ if verbose:
+ print('Adding rules PDF to module')
+
+ zipfile.write(rules,'rules.pdf')
+
+ add_pdffile(root,doc,title='Show rules',pdfFile='rules.pdf')
+
+# --------------------------------------------------------------------
+def add_notes(root,doc,zipfile,title,version,sides,pdffile,infofile,rules,
+ verbose=False):
+ '''Add rules to the module
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ doc : xml.dom.minidom.Element
+ The documentation element
+ zipfile : zipfile.ZipFile
+ The module archive
+ sides : list
+ Sides in the game
+ pdffile : str
+ The images PDF
+ infofile : str
+ The images information
+ rules : str
+ File name of rules (PDF)
+ '''
+ factions = "".join([f'<li>{s}</li>' for s in sides])
+ notes = f'''
+ <html>
+ <body>
+ <h1>Draft VASSAL module of {title} (Version {version})</h1>
+ <p>
+ This is a draft VASSAL module of {title} generated from
+ </p>
+ <ul>
+ <li>images in {pdffile}, and</li>
+ <li>information in {infofile}</li>
+ </ul>
+ <p>
+ both generated by LaTeX using the <b>wargame</b> [1] package, and
+ processed by the Python script <code>wgexport.py</code> from that
+ package.
+ </p>
+ <p>
+ The module is <i>not</i> complete and the developer should edit
+ the module as needed in the VASSAL module editor.
+ </p>
+ <p>
+ [1] <code>https://gitlab.com/wargames_tex/wargame_tex</code>
+ </p>
+ <h2>Content of the module</h2>
+ <p>
+ The <code>wgexport.py</code> Python script has identified the
+ factions of the game to be
+ </p>
+ <ul>
+ '''+factions+f'''
+ </ul>
+ <p>
+ and generated prototype pieces for these (along with possible
+ non-faction pieces. The script also generated a map ("dead map")
+ that contains boards for each faction and is meant to store
+ eliminated units ("&lt;faction&gt; pool"). These pools have a
+ generic shape and colour, and the developer should take care to
+ correct these as needed.
+ </p>
+ <p>
+ Boards of the game present in the {pdffile} and {infofile} files
+ have been generated. Each board has a single zone in it that
+ encompasses the full board, and a hex grid is added to that
+ zone. The module developer should adjust the zone and embedded
+ hex grid according to the actual map, using the VASSAL module
+ editor. The hex grid (as well as coordinates) is drawn by
+ default so as to make this obvious to the developer. The hex
+ sizes should be correct, and mainly the offset needs to be
+ adjusted.
+ </p>
+ <p>
+ Images marked as OOBs have also been added as maps to the
+ module. These also have a single zone with a rectangular grid
+ imposed in it. This grid, and the grid coordinates are drawn by
+ default, so it is easier to see and to remember to adjust it. The
+ module developer should adjust this, possibly adding zones, so
+ that it matches up with the graphics. The developer can add
+ units to the OOB via 'At-start' stacks in the VASSAL editor.
+ </p>
+ <p>
+ All pieces identified by {infofile} and present in {pdffile} have
+ been generated. Double sided pieces (identified by images which
+ also has a suffix-"flipped" image) are created as such.
+ </p>
+ <p>
+ Charts identified by the {infofile} and {pdffile} have been added
+ to the module. The are added as separate (pop-up) frames, but
+ the module developer can change this in the VASSAL module editor.
+ </p>
+ <p>
+ If the {infofile} and {pdffile} identified a front page, then that
+ has been added to module as the splash screen.
+ </p>
+ <p>
+ If a PDF rules file ({rules}) was passed to the Python script,
+ then that has been included in the module and is accessible from
+ the "Help" menu.
+ </p>
+ <h2>After finalising the module</h2>
+ <p>
+ After the developer has finalised the module (added more pieces,
+ fixed board zones a and grids, add in more commands), then this
+ Help menu entry should be deleted.
+ </p>
+ </body>
+ </html>
+ '''
+ if version.lower() == 'draft':
+ if verbose:
+ print(f' Adding draft notes {version}')
+ zipfile.writestr('help/notes.html',notes)
+ add_helpfile(root,doc,title='Draft Module notes',
+ fileName='help/notes.html')
+
+ keys = f'''
+ <html>
+ <body>
+ <h1>{title} (Version {version}) Key bindings</h1>
+
+ <table>
+ <tr>
+ <th>Key </th>
+ <th>Where </th>
+ <th>Effect</th>
+ </tr>
+ <tr>
+ <td>Alt-A</td>
+ <td>-</td>
+ <td>Show the charts panel</td>
+ </tr>
+ <tr>
+ <td>Alt-B</td>
+ <td>-</td>
+ <td>Show the OOBs</td>
+ </tr>
+ <tr>
+ <td>Alt-C</td>
+ <td>-</td>
+ <td>Show the counters panel</td>
+ </tr>
+ <tr>
+ <td>Alt-E</td>
+ <td>-</td>
+ <td>Show the eliminated units</td>
+ </tr>
+ <tr>
+ <td>Alt-I</td>
+ <td>-</td>
+ <td>Show/refresh inventory window</td>
+ </tr>
+ <tr>
+ <td>Alt-M</td>
+ <td>-</td>
+ <td>Show map</td>
+ </tr>
+ <tr>
+ <td>Alt-T</td>
+ <td>-</td>
+ <td>Increase turn track</td>
+ </tr>
+ <tr>
+ <td>Alt-Shift-T</td>
+ <td>-</td>
+ <td>Decrease turn track</td>
+ </tr>
+ <tr>
+ <td>Alt-6</td>
+ <td>-</td>
+ <td>Roll the dice</td>
+ </tr>
+ <tr>
+ <td>Ctrl-D</td>
+ <td>Board,Counter</td>
+ <td>Delete selected counters</td>
+ </tr>
+ <tr>
+ <td>Ctrl-E</td>
+ <td>Board,Counter</td>
+ <td>Eliminate selected counters</td>
+ </tr>
+ <tr>
+ <td>Ctrl-F</td>
+ <td>Board,Counter</td>
+ <td>Flip selected counters</td>
+ </tr>
+ <tr>
+ <td>Ctrl-M</td>
+ <td>Board,Counter</td>
+ <td>Toggle "moved" markers on selected counters</td>
+ </tr>
+ <tr>
+ <td>Ctrl-O</td>
+ <td>Board</td>
+ <td>Hide/show counters</td>
+ </tr>
+ <tr>
+ <td>Ctrl-R</td>
+ <td>Board,Counter</td>
+ <td>Restore unit (from dead pool)</td>
+ </tr>
+ <tr>
+ <td>Ctrl-T</td>
+ <td>Board,Counter</td>
+ <td>Toggle move trail</td>
+ </tr>
+ <tr>
+ <td>Ctrl-+</td>
+ <td>Board</td>
+ <td>Zoom in</td>
+ </tr>
+ <tr>
+ <td>Ctrl--</td>
+ <td>Board</td>
+ <td>Zoom out</td>
+ </tr>
+ <tr>
+ <td>Ctrl-=</td>
+ <td>Board</td>
+ <td>Select zoom</td>
+ </tr>
+ <tr>
+ <td>Ctrl-Shift-O</td>
+ <td>Board</td>
+ <td>Show overview map</td>
+ </tr>
+ <tr>
+ <td>&larr;,&rarr;,&uarr;&darr;</td>
+ <td>Board</td>
+ <td>Scroll board left, right, up, down (slowly)</td>
+ </tr>
+ <tr>
+ <td>PnUp,PnDn</td>
+ <td>Board</td>
+ <td>Scroll board up/down (fast)</td>
+ </tr>
+ <tr>
+ <td>Ctrl-PnUp,Ctrl-PnDn</td>
+ <td>Board</td>
+ <td>Scroll board left/right (fast)</td>
+ </tr>
+ <tr>
+ <td>Mouse-scroll up/down</td>
+ <td>Board</td>
+ <td>Scroll board up//down</td>
+ </tr>
+ <tr>
+ <td>Shift-Mouse-scroll up/down</td>
+ <td>Board</td>
+ <td>Scroll board right/leftown</td>
+ </tr>
+ <tr>
+ <td>Ctrl-Mouse-scroll up/down</td>
+ <td>Board</td>
+ <td>Zoom board out/in</td>
+ </tr>
+ <tr>
+ <td>Mouse-2</td>
+ <td>Board</td>
+ <td>Centre on mouse</td>
+ </tr>
+ </table>
+ </body>
+ </html>
+ '''
+ if verbose:
+ print(f'Add key bindings help file')
+ zipfile.writestr('help/keys.html',keys)
+ add_helpfile(root,doc,title='Key bindings',
+ fileName='help/keys.html')
+
+ return notes
+
+# --------------------------------------------------------------------
+def add_tut(root,doc,zipfile,tutorial,verbose=False):
+ if tutorial is None:
+ return None
+
+ if verbose:
+ print(f'Adding tutorial {tutorial}')
+ zipfile.write(tutorial,'tutorial.vlog')
+
+ return add_tutorial(root,doc,name='Tutorial',logfile='tutorial.vlog')
+
+# --------------------------------------------------------------------
+def add_basics_to_map(root,map,**attr):
+ '''Add a map to the document
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ parent : xml.dom.minidom.Element
+ The parent element
+ button : str
+ Button text
+ name : str
+ Name of the map
+ stacking : bool
+ Whether to enable stacking
+
+ Returns
+ -------
+ map : xml.dom.minidom.Element
+ The map element
+ bpicker : xml.dom.minidom.Element
+ The board picker in map
+ '''
+ stacking = add_stacking(root,map)
+ add_camera (root,map)
+ add_forwardtochatter (root,map)
+ add_menudisplayer (root,map)
+ add_mapcenterer (root,map)
+ add_stackexpander (root,map)
+ add_piecemover (root,map)
+ add_keybufferer (root,map)
+ add_selectionhighlighters(root,map) # TBD
+ add_highlightlastmoved (root,map)
+ add_zoomer (root,map)
+
+ return stacking
+
+# --------------------------------------------------------------------
+def add_elim(root,game,sides,categories,verbose=False):
+ '''Add an eliminated map
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The parent element
+ sides : list of str
+ List of sides in the game
+ '''
+ if verbose:
+ print(' Adding eliminated map and boards')
+
+ if sides is None or len(sides) == 0:
+ return
+
+ map = add_map(root,game,'DeadMap',
+ buttonName = '', #'Eliminated units',
+ icon = '/images/playerAway.gif',
+ markMoved = 'Never',
+ launch = True,
+ allowMultiple = True,
+ hotkey=key('E',ALT))
+ bpicker = add_boardpicker(root,map,
+ slotHeight = 400,
+ slotWidth = 400)
+ add_setup(root,bpicker,'DeadMap',2,[s+' pool' for s in sides])
+ add_masskey(root,map,'Restore',
+ key('R'),
+ key('R'),
+ icon = '/images/Undo16.gif',
+ tooltip = "Restore selected units")
+
+ for i,s in enumerate(sides):
+ if verbose:
+ print(f' Adding {s} pool')
+
+ color = [0,0,0,64]
+ color[i % 3] = 255
+
+ w = 400
+ h = 400
+ c = ','.join([str(c) for c in color])
+ dimg = categories.get('pool',{}).get('all',{}).get(s,None)
+ img = ''
+
+ if dimg:
+ bb = get_bb(dimg['img'])
+ w = bb[2] - bb[0]
+ h = bb[3] - bb[1]
+ c = ''
+ img = dimg['filename']
+ if verbose:
+ print(f' Using image provided by the user: {img}')
+
+ add_board(root,bpicker,f'{s} pool',
+ image = img,
+ color = c,
+ width = w,
+ height = h)
+
+ add_basics_to_map(root,map)
+
+# --------------------------------------------------------------------
+def add_hex_grid(root,parent,color=rgb(255,0,0),visible=True,grid=True):
+ '''Add a hex grid, either to a zone or to the multizone container'''
+
+ hexes = add_hexgrid(root,parent,color=color,visible=visible,
+ edgesLegal=True)
+ if not grid:
+ return hexes
+
+ add_hexnumbering(root,hexes,
+ color = color,
+ hType = 'A',
+ hOff = -1,
+ vType = 'N',
+ vOff = -1,
+ visible = visible)
+
+ return hexes
+
+# --------------------------------------------------------------------
+def get_picture_info(picture,name,width,height,verbose):
+ '''
+ Returns
+ -------
+ hex_width, hex_height : float, float
+ Scale hex width
+ scx, scy : float, float, float, float
+ Scale to image and picture (x,y)
+ rot90 : bool
+ True if rotated +/-90 degrees
+ tran : callable
+ Translation function
+ '''
+ if picture is None:
+ print(f'WARNING: No Tikz picture information.'
+ f"Are you sure you used the `[zoned]' option for the "
+ f"tikzpicture environment of {name}?")
+ f = lambda x,y: (x,y)
+ return HEX_WIDTH,HEX_HEIGHT,1,1,False,f
+
+ # Get picture bounding box
+ tll = picture['lower left']
+ tur = picture['upper right']
+ # Get picture transformation
+ pa = picture['xx']
+ pb = picture['xy']
+ pc = picture['yx']
+ pd = picture['yy']
+ # Get picture offset (always 0,0?)
+ pdx = picture['dx']
+ pdy = picture['dy']
+ # Define picture global transformation
+ pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y)
+ # Globally transform (rotate) picture bounding box
+ pll = pr(*tll)
+ pur = pr(*tur)
+ # Calculate widht, height, and scaling factors
+ pw = pur[0] - pll[0]
+ ph = pur[1] - pll[1]
+ scw = width / pw
+ sch = height / ph
+ # Extract picture scales and rotation
+ # Courtesy of
+ # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
+ from math import sqrt, atan2, degrees, isclose
+ psx = sqrt(pa**2 + pb**2) # * (-1 if pa < 0 else 1)
+ psy = sqrt(pc**2 + pd**2) # * (-1 if pd < 0 else 1)
+ prt = degrees(atan2(pc,pd))
+ if not any([isclose(abs(prt),a) for a in [0,90,180,270]]):
+ raise RuntimeException('Rotations of Tikz pictures other than '
+ '0 or +/-90,+/- 180, or +/-270 not supported. '
+ 'found {prt}')
+ rot90 = isclose(abs(prt),90)
+ if any([isclose(prt,a) for a in [90,270,180,-180]]):
+ print(f'WARNING: rotations by {prt} not fully supported')
+
+ hex_width = psx * HEX_WIDTH
+ hex_height = psy * HEX_HEIGHT
+ if verbose:
+ print(f' Picture transformations: {pa},{pb},{pc},{pd}\n'
+ f' Scale (x,y): {psx},{psy}\n'
+ f' Rotation (degrees): {prt}')
+
+ # When translating the Tikz coordinates, it is important to note
+ # that the Tikz y-axis point upwards, while the picture y-axis
+ # point downwards. This means that the upper right corner is at
+ # (width,0) and the lower left corner is at (0,height).
+ def tranx(x,off=-pll[0]):
+ # print(f'x: {x} + {off} -> {x+off} -> {int(scw*(x+off))}')
+ return int(scw * (x + off)+.5)
+ def trany(y,off=-pur[1]):
+ # print(f'y: {y} + {off} -> {y+off} -> {-int(sch*(y+off))}')
+ return -int(sch * (y + off)+.5)
+ tran = lambda x,y : (tranx(x), trany(y))
+
+ return hex_width, hex_height, scw * psx, sch * psy, rot90, tran
+
+# --------------------------------------------------------------------
+def get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
+ labels,coords,targs,nargs):
+ targs['dx'] = hex_width
+ targs['dy'] = hex_height
+ if labels is not None:
+ labmap = {
+ 'auto': {
+ 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
+ 'auto=numbers' : {
+ 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N' },
+ 'auto=alpha column': {
+ 'hLeading': 0, 'vLeading': 0, 'hType': 'A', 'vType': 'N',
+ 'hOff': -1 },
+ 'auto=alpha 2 column': {# Not supported
+ 'hLeading': 1, 'vLeading': 1, 'hType': 'A', 'vType': 'N' },
+ 'auto=inv y x plus 1': {
+ 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
+ 'hOff': 0, 'vDescend': True },
+ 'auto=x and y plus 1': {
+ 'hLeading': 1, 'vLeading': 1, 'hType': 'N', 'vType': 'N',
+ 'hOff': 0, 'vOff': 0 }
+ }
+ for l in labels.split(','):
+ nargs.update(labmap.get(l,{}))
+
+ # Field/Direction | Normal | Rotated |
+ # ----------------|--------|---------|
+ # hOff | column | column |
+ # vOff | row | row |
+ # hDescend | column | row | True, incr to left
+ # vDescend | row | column | True, incr upward
+ # First='H' | column | column |
+
+ # In wargame offset=0 means that the first coordinates are 0,0.
+ # But offset 0,0 in VASSAL means start at 1,1
+ horiz = 'column'
+ vert = 'row'
+ nargs['vOff'] = nargs.get('vOff',0)-coords[vert] ['offset']-1
+ nargs['hOff'] = nargs.get('hOff',0)-coords[horiz]['offset']-1
+
+ # In VASSAL, vDesend=False means rows increase downward,
+ # while in wargame, factor=-1 means rows increase downward
+ horiz = 'row' if rot90 else 'column'
+ vert = 'column' if rot90 else 'row'
+ nargs['hDescend'] = coords[horiz]['factor'] != 1
+ nargs['vDescend'] = coords[vert] ['factor'] != -1 # flipped y
+ targs['edgesLegal'] = True
+
+ # Check for inversed columns
+ inv_col = coords['column']['factor'] == -1
+ ell = hex_width if inv_col else 0
+
+ # Some special stuff in case of 90 degree rotation
+ if rot90:
+ targs.update({'sideways': True})
+ nargs['hOff'] -= 1
+ nargs['hDescend'] = not nargs['hDescend']
+ if inv_col:
+ nargs['hOff'] += 3
+
+ margin = i.get('board frame',{}).get('margin',0)
+ mx = scx * margin
+ my = scy * margin
+ if inv_col: mx = 0 # Bug in wargame?
+
+ # Grid offset is defined relative to the full image,
+ # not the zone:
+ #
+ # X,Y offset: The horizontal and vertical position of
+ # the center of the first hex of the grid.
+ #
+ # That is, (x,y)=(0,0) is in the top-left corner of the
+ # image, while in TikZ it is in the bottom left corner.
+ # This means we shift by the boudning box offsets plus
+ # some stuff to align with the zone.
+ #
+ # In case of inversed columns, we need to add some extra stuff.
+ x0, y0 = llx-hex_width/3 + ell + mx, ury + my
+
+ # Rotated?
+ if rot90:
+ x0, y0 = ury+2*hex_width/3 + ell + mx, llx + my
+
+ # Below should be double-checked.
+
+ if not rot90:
+ if coords.get('column',{}).get('top short','') == 'isodd' \
+ and coords.get('column',{}).get('offset',0) == -1:
+ y0 -= hex_width/2
+ elif coords.get('column',{}).get('top short','') == 'iseven' \
+ and coords.get('column',{}).get('offset',-1) == 0:
+ y0 -= hex_width/2
+
+ targs.update({'x0': int(x0),'y0': int(y0)})
+
+# --------------------------------------------------------------------
+def get_rectparams(i,llx,ury,width,height,rot90,targs,nargs):
+ targs['dx'] = width
+ targs['dy'] = height
+ targs['x0'] = int(llx - width/2)
+ targs['y0'] = int(ury + height/2)
+ targs['color'] = rgb(0,255,0)
+ nargs['color'] = rgb(0,255,0)
+ nargs['vDescend'] = True
+ nargs['vOff'] = -3
+ nargs.update({'sep':',','vLeading':0,'hLeading':0})
+
+# --------------------------------------------------------------------
+def add_zones(root,zoned,name,board,width,height,gpid,
+ labels=None,coords=None,picinfo=None,
+ verbose=False,visible=True):
+ grids = []
+ picture = None
+ if verbose:
+ print(f' Adding zones to {name} zoned={"zoned" in board}')
+
+ for k, v in board.items():
+ if k == 'labels': labels = v
+ if k == 'coords': coords = v
+ if k == 'zoned': picture = v
+ if 'zone' not in k or k == 'zoned':
+ continue
+
+ gridtype = (add_hexgrid
+ if 'hex' in k.lower()
+ else add_squaregrid)
+ gridnum = (add_hexnumbering
+ if 'hex' in k.lower()
+ else add_squarenumbering)
+ grids.append([k,v,gridtype,gridnum])
+
+ if len(grids) < 1: return gpid
+ if verbose:
+ print(f' Got zones: {[k[0] for k in grids]}')
+ # Either get picture information from argument, or deduce from the
+ # found 'zoned' key.
+ if picinfo is None:
+ picinfo = get_picture_info(picture,name,width,height,verbose)
+
+ hex_width, hex_height, scx, scy, rot90, tran = picinfo
+
+
+ for g in grids:
+ n, i, typ, num = g
+ if verbose:
+ print(f' Adding zone {n}')
+ if 'scope' in n:
+ llx,lly = tran(*i['global lower left'])
+ urx,ury = tran(*i['global upper right'])
+ path = [[llx,ury],[urx,ury],[urx,lly],[llx,lly]]
+ nm = n.replace('zone scope ','')
+ elif 'path' in n:
+ path = [tran(*ii) for ii in i['path']]
+ llx = min([px for px,py in path])
+ ury = max([py for px,py in path])
+ nm = n.replace('zone path ','')
+
+ # Checkf if we have "point" type elements in this object and
+ # add them to dict.
+ points = [ v for k,v in i.items()
+ if (k.startswith('point') and
+ isinstance(v,dict) and \
+ v.get('type','') == 'point')]
+
+ pathstr = ';'.join([f'{s[0]},{s[1]}' for s in path])
+ if verbose:
+ print(f' Zone path ({llx},{ury}): {pathstr}')
+ zone = add_zone(root,zoned,
+ name=nm,
+ locationFormat = ("$name$"
+ if 'pool' in n.lower() else
+ "$gridLocation$"),
+ useParentGrid=False,
+ path=pathstr)
+
+ # Do not add grids to pools
+ if 'pool' in n.lower():
+ continue
+
+ targs = {'color':rgb(255,0,0),'visible':visible}
+ nargs = {'color':rgb(255,0,0),'visible':visible}
+ if 'hex' in n.lower():
+ get_hexparams(i,llx,ury,hex_width,hex_height,scx,scy,rot90,
+ labels,coords,targs,nargs)
+ else:
+ width = hex_width / HEX_WIDTH * RECT_WIDTH
+ height = hex_height / HEX_HEIGHT * RECT_HEIGHT
+ get_rectparams(i,llx,ury,width,height,rot90,targs,nargs)
+
+ # print(targs,nargs)
+ if 'turn' in n.lower(): nargs['sep'] = 'T'
+ if 'oob' in n.lower(): nargs['sep'] = 'O'
+
+ if verbose:
+ print(f' Adding grid')
+
+ if len(points) <= 0:
+ grid = typ(root,zone,**targs)
+ num(root,grid,**nargs)
+ else:
+ if verbose:
+ print(f' Use a region grid')
+ grid = add_regiongrid(root,zone,snapto=True,visible=visible)
+ for p in points:
+ pn = p["name"]
+ pc = p["coords"]
+ if verbose:
+ print(f' Add region {pn} {p}')
+
+ n, gpid = add_region(root,grid,pn,*tran(*pc),gpid,
+ alsoPiece=True,verbose=verbose)
+
+
+ # Once we've dealt with this grid, we should see if we have
+ # any embedded zones we should deal with.
+ add_zones(root,zoned,name,i,width,height,gpid,
+ labels=labels,coords=coords,picinfo=picinfo,
+ verbose=verbose)
+
+ # Adding a global grid
+ #targs['color'] = rgb(0,255,0)
+ #nargs['color'] = rgb(0,255,0)
+ #grid = typ(root,zoned,**targs)
+ #num(root,grid,**nargs)
+
+ return gpid
+
+
+# --------------------------------------------------------------------
+def add_inv(root,game,sides,**attr):
+ '''Add inventory to module'''
+ filt = '{' + '||'.join([f'Faction=="{s}"' for s in sides])+'}'
+ grp = 'Faction,Command,Echelon,Type'
+ inv = add_inventory(root,game,
+ include = filt,
+ groupBy = grp,
+ sortFormat = '$PieceName$',
+ tooltip = 'Show inventory of all pieces',
+ zoomOn = True,
+ **attr)
+ return inv
+
+
+# --------------------------------------------------------------------
+def add_map_board(root,game,name,board,gpid,verbose=False,visible=False):
+ '''A add a single board to the module
+ '''
+
+ map = add_map(root,game, name)
+ bpicker = add_boardpicker(root,map)
+
+ add_counterviewer (root,map)
+ add_hidepieces (root,map)
+ add_globalmap (root,map)
+ add_basics_to_map (root,map)
+ add_masskey (root,map,'Flip', key('F'),key('F'),
+ icon = '/images/Undo16.gif',
+ tooltip='Flip selected units')
+ add_masskey (root,map,'Eliminate',key('E'),key('E'),
+ icon = '/images/playerAway.gif',
+ tooltip='Eliminate selected units')
+
+ # Get the image so we may make a rectangle
+ if verbose:
+ print(f' Adding board {board["name"]} -> {board["filename"]}')
+ ulx,uly,lrx,lry = get_bb(board['img'])
+ eboard = add_board(root,bpicker,name,image=board['filename'])
+ zoned = add_zoned(root,eboard)
+ #hexes = add_hexgrid(root,zoned)
+ add_zonehighlighter(root,zoned)
+
+ if not 'zones' in board:
+ full = add_zone(root,zoned,
+ name='full',
+ useParentGrid=False,
+ path=(f'{ulx},{uly};' +
+ f'{lrx},{uly};' +
+ f'{lrx},{lry};' +
+ f'{ulx},{lry}'))
+
+ # We do not have board info, so add a grid to the whole thing
+ add_hex_grid(root,full,rgb(255,0,0),True,True,visible=visible)
+ return
+
+ # If we get here, we have board info!
+ w = abs(ulx-lrx)
+ h = abs(uly-lry)
+ return add_zones(root,zoned,name,board['zones'],w,h,gpid,
+ verbose=verbose,visible=visible)
+
+# --------------------------------------------------------------------
+def add_boards(root,game,categories,sides,gpid,verbose=False,visible=False):
+ '''Add all boards to the module, including eliminated map
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The parent element
+ categories : dict
+ Catalogue of images
+ sides : list of str
+ List of sides in the game
+ Name of the map
+ '''
+ if verbose:
+ print('Adding maps and boards')
+
+ # Loop over defined boards
+ for bn, b in categories.get('board',{}).get('all',{}).items():
+ gpid = add_map_board(root,game,bn,b,gpid,verbose,visible)
+
+ add_elim(root,game,sides,categories,verbose)
+
+ return gpid
+
+# --------------------------------------------------------------------
+def add_oob(root,widget,name,oob,gpid,hotkey='',verbose=False,visible=False):
+ '''A add a single board to the module
+ '''
+ # Short hand
+ cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
+
+ if verbose:
+ print(f' Adding oob {name}')
+
+ map = add_widgetmap(root,widget, name,markMoved='Never',hotkey='')
+ bpicker = add_boardpicker(root,map)
+
+ add_counterviewer (root,map)
+ add_basics_to_map (root,map)
+
+
+ # Get the image so we may make a rectangle
+ if verbose:
+ print(f' Adding OOB {oob["name"]} -> {oob["filename"]}')
+
+ ulx,uly,lrx,lry = get_bb(oob['img'])
+ board = add_board(root,bpicker,name,image=oob['filename'])
+ zoned = add_zoned(root,board)
+ add_zonehighlighter(root,zoned)
+
+ if not 'zones' in oob:
+ zone = add_zone(root,zoned,
+ name='full',
+ useParentGrid=False,
+ path=(f'{ulx},{uly};' +
+ f'{lrx},{uly};' +
+ f'{lrx},{lry};' +
+ f'{ulx},{lry}'))
+ # We do not have board info, so add a grid to the whole thing
+ grid = add_squaregrid(root,zone,visible=visible)
+ add_squarenumbering(root,grid,visible=visible)
+ return
+
+ # If we get here, we have board info!
+ w = abs(ulx-lrx)
+ h = abs(uly-lry)
+ return add_zones(root,zoned,name,oob['zones'],w,h,gpid,
+ verbose=verbose,visible=visible)
+
+# --------------------------------------------------------------------
+def add_oobs(root,game,categories,gpid,verbose=False,visible=False):
+ '''Add all OObs to the module
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The parent element
+ categories : dict
+ Catalogue of images
+ '''
+ # Short hand
+ oobc = categories.get('oob',{}).get('all',{}).items()
+ if len(oobc) < 1:
+ return gpid
+
+ oobs = add_chartwindow(root,game, 'OOBs',
+ hotkey = key('B',ALT),
+ description = 'OOBs',
+ text = '',
+ icon = '/images/inventory.gif',
+ tooltip = 'Show/hide OOBs')
+ tabs = add_tabs(root,oobs,'OOBs')
+
+ if verbose:
+ print('Adding OOBs')
+
+ # Loop over defined boards
+ for on, o in oobc:
+ widget = add_mapwidget(root,tabs, entryName=on)
+ gpid = add_oob(root,widget,on,o,gpid,
+ verbose = verbose,
+ hotkey = key('B',ALT),
+ visible = visible)
+
+ return gpid
+
+# --------------------------------------------------------------------
+def add_markproto(root,protos,key,value):
+ decs = [ mark_trait(key,value.capitalize()),
+ basic_trait('','','') ]
+ typs = [d[0] for d in decs]
+ stts = [d[1] for d in decs]
+ typ = enc_parts(*typs)
+ stt = enc_parts(*stts)
+ body = add_proto(typ,stt)
+ add_prototype(root,protos,value+' prototype',body)
+
+# --------------------------------------------------------------------
+def add_counters(root,game,categories,
+ unittypes,echelons,commands,verbose=False):
+ '''Add all counters to the module.
+
+ This will add all counters to the module, as well as prototypes
+ for each sub-category.
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The game element
+ categories : dict
+ Catalogue of images
+ '''
+ # Short hand
+ # Create prototypes container before adding pieces
+ protos = add_prototypes(root,game)
+ for utype in unittypes: add_markproto(root,protos,'Type',utype)
+ for ech in echelons: add_markproto(root,protos,'Echelon',ech)
+ for cmd in commands: add_markproto(root,protos,'Command',cmd)
+
+ # Stating global piece ID
+ gpid = 20
+
+ # A window to hold the pieces
+ piecew = add_piecewindow(root,game,'Counters',
+ hotkey = key('C',ALT))
+ # Tab container for each sub-category
+ piecet = add_tabs(root,piecew,entryName='Counters')
+
+ if verbose:
+ print('Adding counters and prototypes')
+
+ # Loop over sub-categories
+ for subn, subc in categories.get('counter',{}).items():
+ # Friendly message
+ if verbose:
+ print(f' Adding {subn} counters and prototype')
+
+ # Create a panel for this sub-category
+ piecep = add_panel(root,piecet,
+ entryName = subn,
+ fixed = False)
+ # Add a list to the panel
+ piecel = add_list(root,piecep,
+ entryName=f'{subn} counters')
+
+ if verbose:
+ print(f' Prototype: {subn} prototype')
+ # Create prototype for this sub-category
+ proto = add_prototype(root,protos,
+ name = f'{subn} prototype',
+ code = proto_body(subn),
+ description = f'Prototype for {subn} counters')
+
+ # Loop over counters in this sub-category
+ for cn, c in subc.items():
+ # Flipped counters are not added directly
+ if cn.endswith('flipped'): continue
+
+ if verbose:
+ print(f' counter: {cn}')
+
+ # Get image bounding box
+ bb = get_bb(c['img'])
+
+ # Get flipped image, if any
+ cf = subc.get(cn + ' flipped', None)
+
+ # Create a slot for this piece
+ sl = add_pieceslot(root,piecel,
+ entryName = cn,
+ gpid = gpid,
+ code = piece_body(subn,c,cf,gpid),
+ height = bb[3]-bb[1],
+ width = bb[2]-bb[0])
+
+ # Store the global piece identifier - do we need this?
+ c['gpid'] = gpid
+
+ # Increment global piece identifier
+ gpid += 1
+
+ # Return next free piece identifier
+ return gpid
+
+# --------------------------------------------------------------------
+def add_charts(root,game,categories,verbose=False):
+ '''Add all boards to the module
+
+ This will select all 'chart' elements from the catalogue and add
+ them as individual (popup) panels to the module.
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The game element
+ categories : dict
+ Catalogue of images
+
+ '''
+ chartc = categories.get('chart',{}).get('all',{}).items()
+ if len(chartc) < 1:
+ return
+
+ # Short hand
+ cr = lambda parent,tag,**attr : create_elem(root,parent,vn(tag),**attr)
+ cw = lambda parent,tag,**attr : create_elem(root,parent,vw(tag),**attr)
+
+ if verbose:
+ print('Adding charts')
+
+ charts = add_chartwindow(root,game, 'Charts',
+ hotkey = key('A',ALT),
+ description = 'Charts',
+ tooltip = 'Show/hide Charts')
+ tabs = add_tabs(root,charts,'Charts')
+
+
+ # Loop over defined charts
+ for cn, c in chartc:
+ if verbose:
+ print(f' Chart {cn} ({c["filename"]})')
+ add_chart(root,tabs,
+ chartName = cn,
+ description = cn,
+ fileName = c['filename'])
+
+# --------------------------------------------------------------------
+def add_diceroll(root,game,categories,verbose=False):
+ '''Add a dice button (1d6)
+
+ Parameters
+ ----------
+ root : xml.dom.minidom.Document
+ Root of the document
+ game : xml.dom.minidom.Element
+ The game element
+ categories : dict
+ Catalogue of images
+ '''
+ if verbose:
+ print('Adding dice')
+
+ add_dice(root,game,
+ hotkey = key('6',ALT),
+ nDice = 1,
+ nSides = 6,
+ text = '1d6',
+ tooltip = 'Roll a 1d6',
+ name = '1d6')
+
+
+# ====================================================================
+def create_build(info,
+ categories,
+ unittypes,
+ echelons,
+ commands,
+ zipfile,
+ title,
+ version,
+ desc = '',
+ rules = None,
+ tutorial = None,
+ pdffile = '',
+ infofile = '',
+ verbose = False,
+ visible = False):
+ '''Create the buildFile.xml document
+
+ Parameters
+ ----------
+ info : dict
+ Information dicationary
+ categories : dict
+ Catalogue of images
+ unittypes : set
+ Set of used unit types
+ zipfile : zipfile.ZipFile
+ Module archive
+ title : str
+ Title of module
+ version : str
+ Version number of module
+ desc : str
+ Short description of the game
+ rules : str
+ Filename of PDF rules document (if any)
+
+ Returns
+ -------
+ xml : str
+ The encoded document
+ '''
+ from xml.dom.minidom import Document
+
+ if verbose:
+ print(f'Creating buildFile.xml: {title},{version}')
+
+ # Create document
+ root = Document()
+
+ # Add game element
+ game = add_game(root,root,
+ name = title,
+ version = version,
+ description = desc)
+
+ # Extract the sides of the game from the counter sub-categories
+ # Should be a little more clever about the case here
+ dsides = {k.lower() : k for k in categories.get('counter',{}).keys()}
+ sides = None
+ for skip in ['all','common', 'marker', 'markers']:
+ if skip in dsides: del dsides[skip]
+ sides = list(dsides.values())
+ if verbose:
+ print('Identified factions: ',sides)
+
+ # Use the standard command encoder
+ add_basiccommandencoder(root,game)
+
+ # Add document parts
+ doc = add_documentation(root,game)
+ add_splash(root, doc, categories, title,verbose)
+ add_rules(root, doc, zipfile, rules, verbose)
+ add_tut(root, doc, zipfile, tutorial, verbose)
+ notes = add_notes(root,doc,zipfile,title,version,
+ sides,pdffile,infofile,rules,verbose)
+
+
+ # Create roster or player sides
+ roster = add_roster(root,game)
+ for s in sides:
+ add_entry(root,roster,s)
+
+ # Add global options - mainly defer to user settings
+ add_globaloptions(root,game,
+ autoReport = 'Use Preferences Setting',
+ centerOnMove = 'Use Preferences Setting',
+ nonOwnerUnmaskable = 'Use Preferences Setting',
+ playerIdFormat = '$playerName$')
+ # Add global properties
+ gp = add_globalproperties(root,game)
+ # Add a turn tracker
+ tt = add_turntrack(root,game,'Turn',counter={
+ 'property': 'Turn',
+ 'phases': {
+ 'property': 'Phase',
+ 'names': sides }})
+ add_globalproperty(root,gp,'TurnTracker.defaultDocked',True)
+
+ # Add all pieces to the module (including prototypes)
+ gpid = add_counters(root,game,categories,
+ unittypes,echelons,commands,verbose)
+ # Add an inventory
+ add_inv(root,game,sides)
+ # Add all boards to the module (including eliminated map)
+ gpid = add_boards(root,game,categories,sides,gpid,
+ verbose=verbose,visible=visible)
+ # Add all OOBs to the module
+ gpid = add_oobs(root,game,categories,gpid,
+ verbose=verbose,visible=visible)
+ # Add charts to the module
+ add_charts(root,game,categories,verbose=verbose)
+ # Add a dice rool
+ add_diceroll(root,game,categories,verbose=verbose)
+
+ # Set next available piece identifier on the game
+ game.setAttribute('nextPieceSlotId',str(gpid+1))
+
+ # Return the document
+ return root, notes
+
+# --------------------------------------------------------------------
+def create_data(title,version,desc='',verbose=False):
+ '''Create the module data XML
+
+ Parameters
+ ----------
+ title : str
+ Title of the game
+ version : str
+ Version number of the module
+ desc : str
+ Short description
+
+ Returns
+ -------
+ xml : str
+ The encoded document
+ '''
+ from xml.dom.minidom import Document
+ from time import time
+
+ if verbose:
+ print('Create module data')
+
+ # The document
+ root = Document()
+
+ # Top of the document
+ data = root.createElement('data')
+ data.setAttribute("version","1")
+ root.appendChild(data)
+
+ # Version number of module
+ vers = root.createElement('version')
+ vers.appendChild(root.createTextNode(str(version)))
+ data.appendChild(vers)
+
+ # Name of the game
+ name = root.createElement('name')
+ name.appendChild(root.createTextNode(title))
+ data.appendChild(name)
+
+ # VASSAL version (should get this in some other way)
+ vvers = root.createElement('VassalVersion')
+ vvers.appendChild(root.createTextNode("3.6.7"))
+ data.appendChild(vvers)
+
+ # Description, if any
+ desc = root.createElement('description')
+ data.appendChild(desc)
+
+ # Automatically set the save data
+ save = root.createElement('dateSaved')
+ save.appendChild(root.createTextNode(f'{int(time())}'))
+ data.appendChild(save)
+
+ return root
+
+
+# ====================================================================
+def create_vmod(vmodname,
+ info,
+ title,
+ version,
+ desc = '',
+ rules = None,
+ tutorial = None,
+ pdfname = '',
+ infoname = '',
+ patch = None,
+ verbose = False,
+ visible = False):
+ '''Create a VASSAL module from the information passed
+
+ Parameters
+ ----------
+ vmodname : str
+ Name of the module
+ info : list
+ List of images and image data
+ title : str
+ Name of the game
+ version : str
+ Version of the module
+ desc : str
+ Short description of the game
+ rules : str
+ Rules PDF (optional)
+
+ Returns
+ -------
+ '''
+ from zipfile import ZipFile, ZIP_DEFLATED
+ from pprint import pprint
+
+ categories = {}
+ unittypes = []
+ echelons = []
+ commands = []
+ with ZipFile(vmodname,'w',compression=ZIP_DEFLATED) as vmod:
+ # Create images in ZIP file
+ for i in info:
+ if ignore_entry(i): continue
+
+ # Store the name of the image file in catalogue
+ i['filename'] = i['name'].replace(' ','_') + '.png'
+ imgfn = 'images/'+i['filename']
+ if imgfn not in vmod.namelist():
+ vmod.writestr(imgfn,i['img'])
+
+ # Categorize - assume counter
+ typ = i.get('category','counter')
+ sub = i.get('subcategory','all')
+ if sub == '':
+ i['subcategory'] = 'all'
+ sub = 'all'
+
+ # Add to catalogue
+ if typ not in categories:
+ categories[typ] = {}
+ cat = categories[typ]
+
+ if sub not in cat:
+ cat[sub] = {}
+ tgt = cat[sub]
+
+ tgt[i['name']] = i
+
+ natoapp6c = i.get('natoapp6c',None)
+ if natoapp6c is not None:
+ from re import sub
+ mains = natoapp6c.get('main',None)
+ echelon = natoapp6c.get('echelon',None)
+ command = natoapp6c.get('command',None)
+
+ if mains is not None:
+ mains = sub(r'\[[^]]+\]','',mains)\
+ .replace('{','').replace('}','').split(',')
+ unittypes.extend(mains)
+ i['mains'] = mains
+
+ if echelon is not None:
+ echelons.append(natoapp6c['echelon'])
+ i['echelon'] = echelon
+
+ if command is not None:
+ commands.append(command)
+ i['command'] = command
+
+ # Friendly message
+ # if verbose:
+ # print(f'{i["name"]} -> {i["type"]}/{i["sub type"]} {typ}/{sub}')
+
+ unittypes = set(unittypes)
+ echelons = set(echelons)
+ commands = set(commands)
+
+ # Now create the `buildFile.xml` file
+ build, notes = create_build(info,
+ categories,
+ unittypes,
+ echelons,
+ commands,
+ vmod,
+ title,
+ version,
+ desc,
+ rules,
+ tutorial,
+ pdfname,
+ infoname,
+ verbose,
+ visible)
+ # Now create the `moduledata` file
+ data = create_data(title, version, desc,verbose)
+
+
+ if patch:
+ from importlib.util import spec_from_file_location, \
+ module_from_spec
+ from pathlib import Path
+ from sys import modules
+
+ p = Path(patch)
+ if verbose:
+ pn = str(p.stem)
+ print(f'Will patch module with {pn}.patch function')
+
+ spec = spec_from_file_location(p.stem, p.absolute())
+ module = module_from_spec(spec)
+ spec.loader.exec_module(module)
+ modules[p.stem] = module
+
+
+ # Patch must accept xml.dom.document,xml.dom.document,ZipFile
+ module.patch(build,data,vmod,verbose)
+
+ buildstr = build.toprettyxml(indent=' ',
+ encoding="UTF-8",
+ standalone=False)
+ datastr = data.toprettyxml(indent=' ',
+ encoding="UTF-8",
+ standalone=False)
+
+ vmod.writestr('buildFile.xml',buildstr)
+ vmod.writestr('moduledata',datastr)
+
+ return notes
+
+# ====================================================================
+def export(vmodname = 'Draft.vmod',
+ pdfname = 'export.pdf',
+ infoname = 'export.json',
+ title = 'Test',
+ version = 'Draft',
+ desc = '',
+ rules = None,
+ tutorial = None,
+ patch = None,
+ verbose = False,
+ visible = False):
+ '''Do the whole thing
+
+ - Read in information form the 'infoname' file (a CSV file
+ generated by LaTeX)
+
+ - Convert each page in the 'pdfname' file (a PDF generated by
+ LaTex) into images
+
+ - Create the VASSAL module
+
+ '''
+ if verbose:
+ print(f'Module name : {vmodname}\n'
+ f'PDF name : {pdfname}\n'
+ f'Info name : {infoname}\n'
+ f'Title : {title}\n'
+ f'Version : {version}\n'
+ f'Description : {desc}\n'
+ f'Rules : {rules}\n'
+ f'Tutorial : {tutorial}')
+ info = convert_pages(pdfname=pdfname,infoname=infoname)
+ return create_vmod(vmodname = vmodname,
+ info = info,
+ title = title,
+ version = version,
+ desc = desc,
+ rules = rules,
+ tutorial = tutorial,
+ pdfname = pdfname,
+ infoname = infoname,
+ patch = patch,
+ verbose = verbose,
+ visible = visible)
+
+# ====================================================================
+if __name__ == '__main__':
+ from argparse import ArgumentParser, FileType
+
+ ap = ArgumentParser(description='Create draft VASSAL module')
+ ap.add_argument('pdffile',
+ help='The PDF file to read images from',
+ type=FileType('r'),
+ default='export.pdf',
+ nargs='?')
+ ap.add_argument('infofile',
+ help='The JSON file to read image information from',
+ type=FileType('r'),
+ default='export.json',
+ nargs='?')
+ ap.add_argument('-p','--patch',
+ help='A python script to patch generated module',
+ type=FileType('r'))
+ ap.add_argument('-o','--output',
+ help='Output file to write module to',
+ type=FileType('w'),
+ default='Draft.vmod')
+ ap.add_argument('-t','--title',
+ help='Module title', default='Draft',
+ type=str)
+ ap.add_argument('-v','--version',
+ help='Module version',
+ type=str,
+ default='draft')
+ ap.add_argument('-r','--rules',
+ help='Rules PDF file',
+ type=FileType('r'))
+ ap.add_argument('-T','--tutorial',
+ help='Tutorial (v)log file',
+ type=FileType('r'))
+ ap.add_argument('-d','--description',
+ help='Short description of module',
+ type=str,
+ default='draft of module')
+ ap.add_argument('-V','--verbose',
+ help='Be verbose',
+ action='store_true')
+ ap.add_argument('-G','--visible-grids',
+ action='store_true',
+ help='Make grids visible in the module')
+ ap.add_argument('-n','--notes',
+ help='Show notes',
+ action='store_true')
+
+
+ args = ap.parse_args()
+
+ vmodname = args.output.name
+ rulesname = args.rules.name if args.rules is not None else None
+ tutname = args.tutorial.name if args.tutorial is not None else None
+ args.output.close()
+
+ patchname = args.patch.name if args.patch is not None else None
+ if args.patch is not None:
+ args.patch.close()
+
+ if args.version.lower() == 'draft':
+ args.visible_grids = True
+
+ try:
+ notes = export(vmodname = vmodname,
+ pdfname = args.pdffile.name,
+ infoname = args.infofile.name,
+ title = args.title,
+ version = args.version,
+ desc = args.description,
+ rules = rulesname,
+ tutorial = tutname,
+ patch = patchname,
+ verbose = args.verbose,
+ visible = args.visible_grids)
+
+ if args.notes:
+ print(f'Module {vmodname} created. '
+ 'Here are the notes from the module')
+ print(notes)
+
+ except Exception as e:
+ from sys import stderr
+ print(f'Failed to build {vmodname}: e',file=stderr)
+ from os import unlink
+ try:
+ unlink(vmodname)
+ except:
+ pass
+
+ raise e