Create attractive VASL scenarios, with loads of useful information embedded to assist with game play.
https://vasl-templates.org
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
9.3 KiB
229 lines
9.3 KiB
""" Serve files from a VASL module file. """
|
|
|
|
import os
|
|
import json
|
|
import zipfile
|
|
import re
|
|
import xml.etree.ElementTree
|
|
|
|
import logging
|
|
_logger = logging.getLogger( "vasl_mod" )
|
|
|
|
from vasl_templates.webapp.file_server.utils import get_vo_gpids, get_effective_gpid
|
|
|
|
SUPPORTED_VASL_MOD_VERSIONS = [ "6.4.0", "6.4.1", "6.4.2", "6.4.3" ]
|
|
SUPPORTED_VASL_MOD_VERSIONS_DISPLAY = "6.4.0-6.4.3"
|
|
|
|
# ---------------------------------------------------------------------
|
|
|
|
class VaslMod:
|
|
"""Serve files from a VASL module file."""
|
|
|
|
def __init__( self, fname, data_dir ) :
|
|
# initialize
|
|
self.pieces = {}
|
|
# parse the VASL module file
|
|
_logger.info( "Loading VASL module: %s", fname )
|
|
self.zip_file = zipfile.ZipFile( fname, "r" )
|
|
self.vasl_version = self._parse_vmod( data_dir )
|
|
if self.vasl_version not in SUPPORTED_VASL_MOD_VERSIONS:
|
|
_logger.warning( "Unsupported VASL version: %s", self.vasl_version )
|
|
|
|
def get_piece_image( self, gpid, side, index ):
|
|
"""Get the image for the specified piece."""
|
|
|
|
# get the image path
|
|
gpid = get_effective_gpid( gpid )
|
|
if gpid not in self.pieces:
|
|
return None, None
|
|
entry = self.pieces[ get_effective_gpid( gpid ) ]
|
|
assert side in ("front","back")
|
|
image_paths = entry[ side+"_images" ]
|
|
if not image_paths:
|
|
return None, None
|
|
if not isinstance( image_paths, list ):
|
|
image_paths = [ image_paths ]
|
|
image_path = image_paths[ index ]
|
|
if not os.path.splitext( image_path )[1]:
|
|
image_path += ".gif"
|
|
|
|
# load the image data
|
|
image_path = os.path.join( "images", image_path )
|
|
image_path = re.sub( r"[\\/]+", "/", image_path ) # nb: in case we're on Windows :-/
|
|
image_data = self.zip_file.read( image_path )
|
|
|
|
return image_path, image_data
|
|
|
|
def get_piece_info( self ):
|
|
"""Get information about each piece."""
|
|
def image_count( piece, key ):
|
|
"""Return the number of images the specified piece has."""
|
|
if not piece[key]:
|
|
return 0
|
|
return len(piece[key]) if isinstance( piece[key], list ) else 1
|
|
return {
|
|
p["gpid"]: {
|
|
"name": p["name"],
|
|
"front_images": image_count( p, "front_images" ),
|
|
"back_images": image_count( p, "back_images" ),
|
|
"is_small": p["is_small"],
|
|
}
|
|
for p in self.pieces.values()
|
|
}
|
|
|
|
def _parse_vmod( self, data_dir ): #pylint: disable=too-many-branches,too-many-locals
|
|
"""Parse a .vmod file."""
|
|
|
|
# load our overrides
|
|
fname = os.path.join( data_dir, "vasl-overrides.json" )
|
|
vasl_overrides = json.load( open( fname, "r" ) )
|
|
fname = os.path.join( data_dir, "expected-multiple-images.json" )
|
|
expected_multiple_images = json.load( open( fname, "r" ) )
|
|
|
|
# figure out which pieces we're interested in
|
|
target_gpids = get_vo_gpids( data_dir )
|
|
|
|
def check_override( gpid, piece, override ):
|
|
"""Check that the values in an override entry match what we have."""
|
|
for key in override:
|
|
if piece[key] != override[key]:
|
|
_logger.warning( "Unexpected value in VASL override for '%s' (gpid=%d): %s", key, gpid, piece[key] )
|
|
return False
|
|
return True
|
|
|
|
# parse the VASL build info
|
|
build_info = self.zip_file.read( "buildFile" )
|
|
doc = xml.etree.ElementTree.fromstring( build_info )
|
|
for node in doc.iter( "VASSAL.build.widget.PieceSlot" ):
|
|
|
|
# load the next entry
|
|
# FUDGE! 6.4.3 introduced weird GPID's for "Hex Grid" pieces :-/
|
|
if node.attrib["gpid"].startswith( "4d0:" ):
|
|
continue
|
|
gpid = int( node.attrib["gpid"] )
|
|
if gpid not in target_gpids:
|
|
continue
|
|
if gpid in self.pieces:
|
|
_logger.warning( "Found duplicate GPID: %d", gpid )
|
|
front_images, back_images = self._get_image_paths( gpid, node.text )
|
|
piece = {
|
|
"gpid": gpid,
|
|
"name": node.attrib["entryName"],
|
|
"front_images": front_images,
|
|
"back_images": back_images,
|
|
"is_small": int(node.attrib["height"]) <= 48,
|
|
}
|
|
|
|
# check if we want to override any values
|
|
override = vasl_overrides.get( str(gpid) )
|
|
if override:
|
|
if check_override( gpid, piece, override["expected"] ):
|
|
for key in override["updated"]:
|
|
piece[key] = override["updated"][key]
|
|
del vasl_overrides[ str(gpid) ]
|
|
|
|
# save the loaded entry
|
|
self.pieces[gpid] = piece
|
|
target_gpids.remove( gpid )
|
|
_logger.debug( "- Loaded piece: %s", piece )
|
|
|
|
# check for multiple images
|
|
if isinstance(piece["front_images"],list) or isinstance(piece["back_images"],list):
|
|
expected = expected_multiple_images.get( str(gpid) )
|
|
if expected:
|
|
check_override( gpid, piece, expected )
|
|
del expected_multiple_images[ str(gpid) ]
|
|
else:
|
|
_logger.warning( "Found multiple images: %s", piece )
|
|
|
|
# make sure we found all the pieces we need
|
|
_logger.info( "Loaded %d pieces.", len(self.pieces) )
|
|
if target_gpids:
|
|
_logger.warning( "Couldn't find pieces: %s", target_gpids )
|
|
|
|
# make sure all the overrides defined were used
|
|
if vasl_overrides:
|
|
gpids = ", ".join( vasl_overrides.keys() )
|
|
_logger.warning( "Unused VASL overrides: %s", gpids )
|
|
if expected_multiple_images:
|
|
gpids = ", ".join( expected_multiple_images.keys() )
|
|
_logger.warning( "Expected multiple images but didn't find them: %s", gpids )
|
|
|
|
return doc.attrib.get( "version" )
|
|
|
|
@staticmethod
|
|
def _get_image_paths( gpid, val ): #pylint: disable=too-many-branches
|
|
"""Get the image path(s) for a piece."""
|
|
|
|
# FUDGE! The data in the build file looks like a serialized object, so we use
|
|
# a bunch of heuristics to try to identify the fields we want :-/
|
|
|
|
# split the data into fields
|
|
val = val.replace( "\\/", "/" )
|
|
fields = val.split( ";" )
|
|
|
|
# identify image paths
|
|
def is_image_path( val ): #pylint: disable=missing-docstring
|
|
if val == "white X 60.png": # nb: a lot of Finnish pieces have this
|
|
return False
|
|
if val.endswith( (".gif",".png") ):
|
|
return True
|
|
if val.startswith( ("ru/","ge/","am/","br/","it/","ja/","ch/","sh/","fr/","al/","ax/","hu/","fi/") ):
|
|
return True
|
|
return False
|
|
fields = [ f for f in fields if is_image_path(f) ]
|
|
|
|
# figure out what we've got
|
|
def split_fields( val ):
|
|
"""Split out individual fields in a VASL build info entry."""
|
|
fields = [ v.strip() for v in val.split(",") ]
|
|
fields = [ f for f in fields if f ]
|
|
return fields
|
|
if not fields:
|
|
_logger.warning( "Couldn't find any image paths for gpid=%d.", gpid )
|
|
return None, None
|
|
if len(fields) == 1:
|
|
# the piece only has front image(s)
|
|
front_images, back_images = split_fields(fields[0]), None
|
|
else:
|
|
# the piece has front and back image(s)
|
|
if len(fields) > 2:
|
|
_logger.warning( "Found > 2 image paths for gpid=%d", gpid )
|
|
front_images, back_images = split_fields(fields[1]), split_fields(fields[0])
|
|
|
|
# ignore dismantled ordnance
|
|
if len(front_images) > 1:
|
|
if front_images[-1].endswith( "dm" ):
|
|
if back_images[-1].endswith( "dmb" ):
|
|
_logger.debug( "Ignoring dismantled images: gpid=%d, front=%s, back=%s",
|
|
gpid, front_images, back_images
|
|
)
|
|
front_images.pop()
|
|
back_images.pop()
|
|
else:
|
|
_logger.warning( "Unexpected dismantled images: %s %s", front_images, back_images )
|
|
|
|
# ignore limbered ordnance
|
|
if len(front_images) > 1:
|
|
if front_images[-1].endswith( "l" ):
|
|
if back_images[-1].endswith( ("lb","l-b") ):
|
|
_logger.debug( "Ignoring limbered images: gpid=%d, front=%s, back=%s",
|
|
gpid, front_images, back_images
|
|
)
|
|
front_images.pop()
|
|
back_images.pop()
|
|
else:
|
|
_logger.warning( "Unexpected limbered images: %s %s", front_images, back_images )
|
|
elif front_images[-1].endswith( "B.png" ) and front_images[0] == front_images[-1][:-5]+".png":
|
|
# nb: this is for Finnish Guns
|
|
_logger.debug( "Ignoring limbered images: gpid=%d, front=%s, back=%s",
|
|
gpid, front_images, back_images
|
|
)
|
|
front_images.pop()
|
|
assert not back_images
|
|
|
|
def delistify( val ): #pylint: disable=missing-docstring
|
|
if val is None:
|
|
return None
|
|
return val[0] if len(val) == 1 else val
|
|
return delistify(front_images), delistify(back_images)
|
|
|