diff options
Diffstat (limited to 'macros/latex/contrib/wargame/source/utils/wgexport.py')
-rwxr-xr-x | macros/latex/contrib/wargame/source/utils/wgexport.py | 3750 |
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 == "Board" +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$ &rarr; $location$ *', + moveWithinFormat = '$pieceName$ moves $previousLocation$ &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 = '>', + lockAdd = False, + lockDice = False, + lockPlus = False, + lockSides = False, + nDice = 1, + nSides = 6, + name = '1d6', + plus = 0, + prompt = False, + propertyGate = '', + reportFormat = '** $name$ = $result$ *** &lt;$PlayerName$&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 ("<faction> 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>←,→,↑↓</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 |