#!/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 ['<>','<>'] # -------------------------------------------------------------------- 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_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_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_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 or 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 CTRL = 130 ALT = 520 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): return add_node(root,elem,MODULE+'properties.GlobalProperties') # -------------------------------------------------------------------- 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_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'), 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',195), 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 = name, 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 = '79,195', 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 = '61,195', 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) # -------------------------------------------------------------------- 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 = 'Eliminate', key = key('E'), restoreName = 'Restore', restoreKey = key('R'), x = 200, y = 200, xidx = 0, yidx = 0, xoff = 1, yoff = 1, description = 'Eliminate this unit', 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) # ==================================================================== # # 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'), 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'
  • {s}
  • ' for s in sides]) notes = f'''

    Draft VASSAL module of {title} (Version {version})

    This is a draft VASSAL module of {title} generated from

    • images in {pdffile}, and
    • information in {infofile}

    both generated by LaTeX using the wargame [1] package, and processed by the Python script export.py from that package.

    The module is not complete and the developer should edit the module as needed in the VASSAL module editor.

    [1] https://gitlab.com/wargames_tex/wargame_tex

    Content of the module

    The export.py Python script has identified the factions of the game to be

      '''+factions+f'''

    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.

    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.

    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.

    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.

    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.

    If the {infofile} and {pdffile} identified a front page, then that has been added to module as the splash screen.

    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.

    Key-bindings

    The following key bindings have been defined

    Alt-A Show the charts panel
    Alt-B Show the OOBs
    Alt-C Show the counters panel
    Alt-E Show the eliminated units
    Alt-6 Roll the die
    Ctrl-O Hide/show counters
    Ctrl-F Flip selected counters
    Ctrl-E Eliminate selected counters
    Ctrl-+ Zoom in
    Ctrl-- Zoom out
    Ctrl-= Select zoom
    Ctrl-Shift-O Show overview map
    Counter short cuts (may not work, right click and use context menu)
    Ctrl-F Flip counter
    Ctrl-D Delete counter
    Ctrl-M Toggle "moved" marker
    Ctrl-T Toggle move trail
    Ctrl-E Eliminate unit
    Ctrl-R Restore unit (from dead pool)

    After finalising the module

    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. You may want to keep key bindings information in some help file.

    ''' 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'''

    {title} (Version {version}) Key bindings

    Key Where Effect
    Alt-A - Show the charts panel
    Alt-B - Show the OOBs
    Alt-C - Show the counters panel
    Alt-E - Show the eliminated units
    Alt-6 - Roll the dice
    Ctrl-O Board Hide/show counters
    Ctrl-F Board Flip selected counters
    Ctrl-E Board Eliminate selected counters
    Ctrl-+ Board Zoom in
    Ctrl-- Board Zoom out
    Ctrl-= Board Select zoom
    Ctrl-Shift-O Board Show overview map
    Ctrl-F Counter Flip counter
    Ctrl-D Counter Delete counter
    Ctrl-M Counter Toggle "moved" marker
    Ctrl-T Counter Toggle move trail
    Ctrl-E Counter Eliminate unit
    Ctrl-R Counter Restore unit (from dead pool)

    Counter short cuts (may not work, right click and use context menu)

    ''' 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_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) 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 pll = picture['lower left'] pur = 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 + pb * y, pc * x + pd * y) pr = lambda x,y: (pa * x + pc * y, pb * x + pd * y) # Globally transform (rotate) picture bounding box pll = pr(*pll) pur = pr(*pur) # 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_zoomer (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_zoomer (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, 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) 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 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, 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' vmod.writestr('images/'+i['filename'],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, 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, 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') info = convert_pages(pdfname=pdfname,infoname=infoname) return create_vmod(vmodname = vmodname, info = info, title = title, version = version, desc = desc, rules = rules, 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('-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 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, 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