Index: ps/trunk/source/tools/selectiontexgen/selectiontexgen.py =================================================================== --- ps/trunk/source/tools/selectiontexgen/selectiontexgen.py (revision 24891) +++ ps/trunk/source/tools/selectiontexgen/selectiontexgen.py (nonexistent) @@ -1,288 +0,0 @@ -""" -Generates basic square and circle selection overlay textures by parsing all the entity XML files and reading -their Footprint components. - -For usage, invoke this script with --help. -""" - -# This script uses PyCairo for plotting, since PIL (Python Imaging Library) is absolutely horrible. On Linux, -# this should be merely a matter of installing a package (e.g. 'python-cairo' for Debian/Ubuntu), but on Windows -# it's kind of tricky and requires some Google-fu. Fortunately, I have saved the working instructions below: -# -# Grab a Win32 binary from http://ftp.gnome.org/pub/GNOME/binaries/win32/pycairo/1.8/ and install PyCairo using -# the installer. The installer extracts the necessary files into Lib\site-packages\cairo within the folder where -# Python is installed. There are some extra DLLs which are required to make Cairo work, so we have to get these -# as well. -# -# Head to http://ftp.gnome.org/pub/gnome/binaries/win32/dependencies/ and get the binary versions of Cairo -# (cairo_1.8.10-3_win32.zip at the time of writing), Fontconfig (fontconfig_2.8.0-2_win32.zip), Freetype -# (freetype_2.4.4-1_win32.zip), Expat (expat_2.0.1-1_win32.zip), libpng (libpng_1.4.3-1_win32.zip) and zlib -# (zlib_1.2.5-2_win32.zip). Version numbers may vary, so be adaptive! Each ZIP file will contain a bin subfolder -# with a DLL file in it. Put the following DLLs in Lib\site-packages\cairo within your Python installation: -# -# freetype6.dll (from freetype_2.4.4-1_win32.zip) -# libcairo-2.dll (from cairo_1.8.10-3_win32.zip) -# libexpat-1.dll (from expat_2.0.1-1_win32.zip) -# libfontconfig-1.dll (from fontconfig_2.8.0-2_win32.zip) -# libpng14-14.dll (from libpng_1.4.3-1_win32.zip) -# zlib1.dll (from zlib_1.2.5-2_win32.zip). -# -# Should be all set now. - -import optparse -import sys, os -import math -import operator -import cairo # Requires PyCairo (see notes above) -from os.path import * -from xml.dom import minidom - -def geqPow2(x): - """Returns the smallest power of two that's equal to or greater than x""" - return int(2**math.ceil(math.log(x, 2))) - -def generateSelectionTexture(shape, textureW, textureH, outerStrokeW, innerStrokeW, outputDir): - - outputBasename = "%dx%d" % (textureW, textureH) - - # size of the image canvas containing the texture (may be larger to ensure power-of-two dimensions) - canvasW = geqPow2(textureW) - canvasH = geqPow2(textureH) - - # draw texture - texture = cairo.ImageSurface(cairo.FORMAT_ARGB32, canvasW, canvasH) - textureMask = cairo.ImageSurface(cairo.FORMAT_RGB24, canvasW, canvasH) - - ctxTexture = cairo.Context(texture) - ctxTextureMask = cairo.Context(textureMask) - - # fill entire image with transparent pixels - ctxTexture.set_source_rgba(1.0, 1.0, 1.0, 0.0) # transparent - ctxTexture.rectangle(0, 0, textureW, textureH) - ctxTexture.fill() # fill current path - - ctxTextureMask.set_source_rgb(0.0, 0.0, 0.0) # black - ctxTextureMask.rectangle(0, 0, canvasW, canvasH) # (!) - ctxTextureMask.fill() - - pasteX = (canvasW - textureW)//2 # integer division, floored result - pasteY = (canvasH - textureH)//2 # integer division, floored result - ctxTexture.translate(pasteX, pasteY) # translate all drawing so that the result is centered - ctxTextureMask.translate(pasteX, pasteY) - - # outer stroke width should always be >= inner stroke width, but let's play it safe - maxStrokeW = max(outerStrokeW, innerStrokeW) - - if shape == "square": - - rectW = textureW - rectH = textureH - - # draw texture (4px white outline, then overlay a 2px black outline) - ctxTexture.rectangle(maxStrokeW/2, maxStrokeW/2, rectW - maxStrokeW, rectH - maxStrokeW) - ctxTexture.set_line_width(outerStrokeW) - ctxTexture.set_source_rgba(1.0, 1.0, 1.0, 1.0) # white - ctxTexture.stroke_preserve() # stroke and maintain path - ctxTexture.set_line_width(innerStrokeW) - ctxTexture.set_source_rgba(0.0, 0.0, 0.0, 1.0) # black - ctxTexture.stroke() # stroke and clear path - - # draw mask (2px white) - ctxTextureMask.rectangle(maxStrokeW/2, maxStrokeW/2, rectW - maxStrokeW, rectH - maxStrokeW) - ctxTextureMask.set_line_width(innerStrokeW) - ctxTextureMask.set_source_rgb(1.0, 1.0, 1.0) - ctxTextureMask.stroke() - - elif shape == "circle": - - centerX = textureW//2 - centerY = textureH//2 - radius = textureW//2 - maxStrokeW/2 # allow for the strokes to fit - - # draw texture - ctxTexture.arc(centerX, centerY, radius, 0, 2*math.pi) - ctxTexture.set_line_width(outerStrokeW) - ctxTexture.set_source_rgba(1.0, 1.0, 1.0, 1.0) # white - ctxTexture.stroke_preserve() # stroke and maintain path - ctxTexture.set_line_width(innerStrokeW) - ctxTexture.set_source_rgba(0.0, 0.0, 0.0, 1.0) # black - ctxTexture.stroke() - - # draw mask - ctxTextureMask.arc(centerX, centerY, radius, 0, 2*math.pi) - ctxTextureMask.set_line_width(innerStrokeW) - ctxTextureMask.set_source_rgb(1.0, 1.0, 1.0) - ctxTextureMask.stroke() - - finalOutputDir = outputDir + "/" + shape - if not isdir(finalOutputDir): - os.makedirs(finalOutputDir) - - print "Generating " + os.path.normcase(finalOutputDir + "/" + outputBasename + ".png") - - texture.write_to_png(finalOutputDir + "/" + outputBasename + ".png") - textureMask.write_to_png(finalOutputDir + "/" + outputBasename + "_mask.png") - - -def generateSelectionTextures(xmlTemplateDir, outputDir, outerStrokeScale, innerStrokeScale, snapSizes = False): - - # recursively list XML files - xmlFiles = [] - - for dir, subdirs, basenames in os.walk(xmlTemplateDir): - for basename in basenames: - filename = join(dir, basename) - if filename[-4:] == ".xml": - xmlFiles.append(filename) - - textureTypesRaw = set() # set of (type, w, h) tuples (so we can eliminate duplicates) - - # parse the XML files, and look for nodes that are a child of and - # that do not have the disable attribute defined - for xmlFile in xmlFiles: - xmlDoc = minidom.parse(xmlFile) - rootNode = xmlDoc.childNodes[0] - - # we're only interested in entity templates - if not rootNode.nodeName == "Entity": - continue - - # check if this entity has a footprint definition - rootChildNodes = [n for n in rootNode.childNodes if n.localName is not None] # remove whitespace text nodes - footprintNodes = filter(lambda x: x.localName == "Footprint", rootChildNodes) - if not len(footprintNodes) == 1: - continue - - footprintNode = footprintNodes[0] - if footprintNode.hasAttribute("disable"): - continue - - # parse the footprint declaration - # Footprints can either have either one of these children: - # - # - # There's also a node, but we don't care about it here. - - squareNodes = footprintNode.getElementsByTagName("Square") - circleNodes = footprintNode.getElementsByTagName("Circle") - - numSquareNodes = len(squareNodes) - numCircleNodes = len(circleNodes) - - if not (numSquareNodes + numCircleNodes == 1): - print "Invalid Footprint definition: insufficient or too many Square and/or Circle definitions in %s" % xmlFile - - texShape = None - texW = None # in world-space units - texH = None # in world-space units - - if numSquareNodes == 1: - texShape = "square" - texW = float(squareNodes[0].getAttribute("width")) - texH = float(squareNodes[0].getAttribute("depth")) - - elif numCircleNodes == 1: - texShape = "circle" - texW = float(circleNodes[0].getAttribute("radius")) - texH = texW - - textureTypesRaw.add((texShape, texW, texH)) - - # endfor xmlFiles - - print "Found: %d footprints (%d square, %d circle)" % ( - len(textureTypesRaw), - len([x for x in textureTypesRaw if x[0] == "square"]), - len([x for x in textureTypesRaw if x[0] == "circle"]) - ) - - textureTypes = set() - - for type, w, h in textureTypesRaw: - if snapSizes: - # "snap" texture sizes to close-enough neighbours that will still look good enough so we can get away with fewer - # actual textures than there are unique footprint outlines - w = 1*math.ceil(w/1) # round up to the nearest world-space unit - h = 1*math.ceil(h/1) # round up to the nearest world-space unit - - textureTypes.add((type, w, h)) - - if snapSizes: - print "Reduced: %d footprints (%d square, %d circle)" % ( - len(textureTypes), - len([x for x in textureTypes if x[0] == "square"]), - len([x for x in textureTypes if x[0] == "circle"]) - ) - - # create list from texture types set (so we can sort and have prettier output) - textureTypes = sorted(list(textureTypes), key=operator.itemgetter(0,1,2)) # sort by the first tuple element, then by the second, then the third - - # ------------------------------------------------------------------------------------ - # compute the size of the actual texture we want to generate (in px) - - scale = 8 # world-space-units-to-pixels scale - for type, w, h in textureTypes: - - # if we have a circle, update the w and h so that they're the full width and height of the texture - # and not just the radius - if type == "circle": - assert w == h - w *= 2 - h *= 2 - - w = int(math.ceil(w*scale)) - h = int(math.ceil(h*scale)) - - # apply a minimum size for really small textures - w = max(24, w) - h = max(24, h) - - generateSelectionTexture(type, w, h, w/outerStrokeScale, innerStrokeScale * (w/outerStrokeScale), outputDir) - - -if __name__ == "__main__": - - parser = optparse.OptionParser(usage="Usage: %prog [filenames]") - - parser.add_option("--template-dir", type="str", default=None, help="Path to simulation template XML definition folder. Will be searched recursively for templates containing Footprint definitions. If not specified and this script is run from its directory, it will be automatically determined.") - parser.add_option("--output-dir", type="str", default=".", help="Output directory. Will be created if it does not already exist. Defaults to the current directory.") - parser.add_option("--oss", "--outer-stroke-scale", type="float", default=12.0, dest="outer_stroke_scale", metavar="SCALE", help="Width of the outer (white) stroke, as a divisor of each generated texture's width. Defaults to 12. Larger values produce thinner overall outlines.") - parser.add_option("--iss", "--inner-stroke-scale", type="float", default=0.5, dest="inner_stroke_scale", metavar="PERCENTAGE", help="Width of the inner (black) stroke, as a percentage of the outer stroke's calculated width. Must be between 0 and 1. Higher values produce thinner black/player color strokes inside the surrounding outer white stroke. Defaults to 0.5.") - - (options, args) = parser.parse_args() - - templateDir = options.template_dir - if templateDir is None: - - scriptDir = dirname(abspath(__file__)) - - # 'autodetect' location if run from its own dir - if normcase(scriptDir).replace('\\', '/').endswith("source/tools/selectiontexgen"): - templateDir = "../../../binaries/data/mods/public/simulation/templates" - else: - print "No template dir specified; use the --template-dir command line argument." - sys.exit() - - # check if the template dir exists - templateDir = abspath(templateDir) - if not isdir(templateDir): - print "No such template directory: %s" % templateDir - sys.exit() - - # check if the output dir exists, create it if needed - outputDir = abspath(options.output_dir) - print outputDir - if not isdir(outputDir): - print "Creating output directory: %s" % outputDir - os.makedirs(outputDir) - - print "Template directory:\t%s" % templateDir - print "Output directory: \t%s" % outputDir - print "------------------------------------------------" - - generateSelectionTextures( - templateDir, - outputDir, - max(0.0, options.outer_stroke_scale), - min(1.0, max(0.0, options.inner_stroke_scale)), - ) \ No newline at end of file Property changes on: ps/trunk/source/tools/selectiontexgen/selectiontexgen.py ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/collada/tests/tests.py =================================================================== --- ps/trunk/source/collada/tests/tests.py (revision 24891) +++ ps/trunk/source/collada/tests/tests.py (revision 24892) @@ -1,136 +1,138 @@ +#!/usr/bin/env python3 + from ctypes import * import sys import os import xml.etree.ElementTree as ET binaries = '../../../binaries' # Work out the platform-dependent library filename dll_filename = { 'posix': './libCollada_dbg.so', 'nt': 'Collada_dbg.dll', }[os.name] # The DLL may need other DLLs which are in its directory, so set the path to that # (Don't care about clobbering the old PATH - it doesn't have anything important) os.environ['PATH'] = '%s/system/' % binaries # Load the actual library library = cdll.LoadLibrary('%s/system/%s' % (binaries, dll_filename)) def log(severity, message): - print '[%s] %s' % (('INFO', 'WARNING', 'ERROR')[severity], message) + print('[%s] %s' % (('INFO', 'WARNING', 'ERROR')[severity], message)) clog = CFUNCTYPE(None, c_int, c_char_p)(log) # (the CFUNCTYPE must not be GC'd, so try to keep a reference) library.set_logger(clog) -skeleton_definitions = open('%s/data/tools/collada/skeletons.xml' % binaries).read() +skeleton_definitions = open('%s/data/tests/collada/skeletons.xml' % binaries).read() library.set_skeleton_definitions(skeleton_definitions, len(skeleton_definitions)) def _convert_dae(func, filename, expected_status=0): output = [] def cb(cbdata, str, len): output.append(string_at(str, len)) cbtype = CFUNCTYPE(None, POINTER(None), POINTER(c_char), c_uint) status = func(filename, cbtype(cb), None) assert(status == expected_status) return ''.join(output) def convert_dae_to_pmd(*args, **kwargs): return _convert_dae(library.convert_dae_to_pmd, *args, **kwargs) def convert_dae_to_psa(*args, **kwargs): return _convert_dae(library.convert_dae_to_psa, *args, **kwargs) def clean_dir(path): # Remove all files first try: for f in os.listdir(path): os.remove(path+'/'+f) os.rmdir(path) except OSError: pass # (ignore errors if files are in use) # Make sure the directory exists try: os.makedirs(path) except OSError: pass # (ignore errors if it already exists) def create_actor(mesh, texture, anims, props_): actor = ET.Element('actor', version='1') ET.SubElement(actor, 'castshadow') group = ET.SubElement(actor, 'group') variant = ET.SubElement(group, 'variant', frequency='100', name='Base') ET.SubElement(variant, 'mesh').text = mesh+'.pmd' ET.SubElement(variant, 'texture').text = texture+'.dds' animations = ET.SubElement(variant, 'animations') for name, file in anims: ET.SubElement(animations, 'animation', file=file+'.psa', name=name, speed='100') props = ET.SubElement(variant, 'props') for name, file in props_: ET.SubElement(props, 'prop', actor=file+'.xml', attachpoint=name) return ET.tostring(actor) def create_actor_static(mesh, texture): actor = ET.Element('actor', version='1') ET.SubElement(actor, 'castshadow') group = ET.SubElement(actor, 'group') variant = ET.SubElement(group, 'variant', frequency='100', name='Base') ET.SubElement(variant, 'mesh').text = mesh+'.pmd' ET.SubElement(variant, 'texture').text = texture+'.dds' return ET.tostring(actor) ################################ # Error handling if False: convert_dae_to_pmd('This is not well-formed XML', expected_status=-2) convert_dae_to_pmd('This is not COLLADA', expected_status=-2) convert_dae_to_pmd('This is still not valid COLLADA', expected_status=-2) # Do some real conversions, so the output can be tested in the Actor Viewer test_data = binaries + '/data/tests/collada' test_mod = binaries + '/data/mods/_test.collada' clean_dir(test_mod + '/art/meshes') clean_dir(test_mod + '/art/actors') clean_dir(test_mod + '/art/animation') #for test_file in ['cube', 'jav2', 'jav2b', 'teapot_basic', 'teapot_skin', 'plane_skin', 'dude_skin', 'mergenonbone', 'densemesh']: #for test_file in ['teapot_basic', 'jav2b', 'jav2d']: for test_file in ['xsitest3c','xsitest3e','jav2d','jav2d2']: #for test_file in ['xsitest3']: #for test_file in []: - print "* Converting PMD %s" % (test_file) + print("* Converting PMD %s" % (test_file)) input_filename = '%s/%s.dae' % (test_data, test_file) output_filename = '%s/art/meshes/%s.pmd' % (test_mod, test_file) input = open(input_filename).read() output = convert_dae_to_pmd(input) open(output_filename, 'wb').write(output) xml = create_actor(test_file, 'male', [('Idle','dudeidle'),('Corpse','dudecorpse'),('attack1',test_file),('attack2','jav2d')], [('helmet','teapot_basic_static')]) open('%s/art/actors/%s.xml' % (test_mod, test_file), 'w').write(xml) xml = create_actor_static(test_file, 'male') open('%s/art/actors/%s_static.xml' % (test_mod, test_file), 'w').write(xml) #for test_file in ['jav2','jav2b', 'jav2d']: for test_file in ['xsitest3c','xsitest3e','jav2d','jav2d2']: #for test_file in []: - print "* Converting PSA %s" % (test_file) + print("* Converting PSA %s" % (test_file)) input_filename = '%s/%s.dae' % (test_data, test_file) output_filename = '%s/art/animation/%s.psa' % (test_mod, test_file) input = open(input_filename).read() output = convert_dae_to_psa(input) open(output_filename, 'wb').write(output) Index: ps/trunk/source/tools/fontbuilder2/FontLoader.py =================================================================== --- ps/trunk/source/tools/fontbuilder2/FontLoader.py (revision 24891) +++ ps/trunk/source/tools/fontbuilder2/FontLoader.py (revision 24892) @@ -1,66 +1,66 @@ # Adapted from http://cairographics.org/freetypepython/ import ctypes import cairo import sys CAIRO_STATUS_SUCCESS = 0 FT_Err_Ok = 0 FT_LOAD_DEFAULT = 0x0 FT_LOAD_NO_HINTING = 0x2 FT_LOAD_FORCE_AUTOHINT = 0x20 FT_LOAD_NO_AUTOHINT = 0x8000 # find required libraries (platform specific) if sys.platform == "win32": ft_lib = "freetype6.dll" lc_lib = "libcairo-2.dll" else: ft_lib = "libfreetype.so.6" lc_lib = "libcairo.so.2" _freetype_so = ctypes.CDLL (ft_lib) _cairo_so = ctypes.CDLL (lc_lib) _cairo_so.cairo_ft_font_face_create_for_ft_face.restype = ctypes.c_void_p _cairo_so.cairo_ft_font_face_create_for_ft_face.argtypes = [ ctypes.c_void_p, ctypes.c_int ] _cairo_so.cairo_set_font_face.argtypes = [ ctypes.c_void_p, ctypes.c_void_p ] _cairo_so.cairo_font_face_status.argtypes = [ ctypes.c_void_p ] _cairo_so.cairo_status.argtypes = [ ctypes.c_void_p ] # initialize freetype _ft_lib = ctypes.c_void_p () if FT_Err_Ok != _freetype_so.FT_Init_FreeType (ctypes.byref (_ft_lib)): raise Exception("Error initialising FreeType library.") _surface = cairo.ImageSurface (cairo.FORMAT_A8, 0, 0) class PycairoContext(ctypes.Structure): _fields_ = [("PyObject_HEAD", ctypes.c_byte * object.__basicsize__), ("ctx", ctypes.c_void_p), ("base", ctypes.c_void_p)] def create_cairo_font_face_for_file (filename, faceindex=0, loadoptions=0): # create freetype face ft_face = ctypes.c_void_p() cairo_ctx = cairo.Context (_surface) cairo_t = PycairoContext.from_address(id(cairo_ctx)).ctx - if FT_Err_Ok != _freetype_so.FT_New_Face (_ft_lib, filename, faceindex, ctypes.byref(ft_face)): + if FT_Err_Ok != _freetype_so.FT_New_Face (_ft_lib, filename.encode('ascii'), faceindex, ctypes.byref(ft_face)): raise Exception("Error creating FreeType font face for " + filename) # create cairo font face for freetype face cr_face = _cairo_so.cairo_ft_font_face_create_for_ft_face (ft_face, loadoptions) if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_font_face_status (cr_face): raise Exception("Error creating cairo font face for " + filename) _cairo_so.cairo_set_font_face (cairo_t, cr_face) if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_status (cairo_t): raise Exception("Error creating cairo font face for " + filename) face = cairo_ctx.get_font_face () indexes = lambda char: _freetype_so.FT_Get_Char_Index(ft_face, ord(char)) return (face, indexes) Index: ps/trunk/source/tools/fontbuilder2/Packer.py =================================================================== --- ps/trunk/source/tools/fontbuilder2/Packer.py (revision 24891) +++ ps/trunk/source/tools/fontbuilder2/Packer.py (revision 24892) @@ -1,272 +1,272 @@ # Adapted from: # http://enichan.darksiren.net/wordpress/?p=49 # which was adapted from some version of # https://devel.nuclex.org/framework/browser/game/Nuclex.Game/trunk/Source/Packing/CygonRectanglePacker.cs # which has the following license: # # Copyright (C) 2002-2009 Nuclex Development Labs # # This library is free software; you can redistribute it and/or # modify it under the terms of the IBM Common Public License as # published by the IBM Corporation; either version 1.0 of the # License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # IBM Common Public License for more details. from bisect import bisect_left class OutOfSpaceError(Exception): pass class Point(object): def __init__(self, x, y): self.x = x self.y = y def __cmp__(self, other): """Compares the starting position of height slices""" return self.x - other.x class RectanglePacker(object): """Base class for rectangle packing algorithms By uniting all rectangle packers under this common base class, you can easily switch between different algorithms to find the most efficient or performant one for a given job. An almost exhaustive list of packing algorithms can be found here: http://www.csc.liv.ac.uk/~epa/surveyhtml.html""" def __init__(self, packingAreaWidth, packingAreaHeight): """Initializes a new rectangle packer packingAreaWidth: Maximum width of the packing area packingAreaHeight: Maximum height of the packing area""" self.packingAreaWidth = packingAreaWidth self.packingAreaHeight = packingAreaHeight def Pack(self, rectangleWidth, rectangleHeight): """Allocates space for a rectangle in the packing area rectangleWidth: Width of the rectangle to allocate rectangleHeight: Height of the rectangle to allocate Returns the location at which the rectangle has been placed""" point = self.TryPack(rectangleWidth, rectangleHeight) if not point: raise OutOfSpaceError("Rectangle does not fit in packing area") return point def TryPack(self, rectangleWidth, rectangleHeight): """Tries to allocate space for a rectangle in the packing area rectangleWidth: Width of the rectangle to allocate rectangleHeight: Height of the rectangle to allocate Returns a Point instance if space for the rectangle could be allocated be found, otherwise returns None""" raise NotImplementedError class DumbRectanglePacker(RectanglePacker): def __init__(self, packingAreaWidth, packingAreaHeight): RectanglePacker.__init__(self, packingAreaWidth, packingAreaHeight) self.x = 0 self.y = 0 self.rowh = 0 def TryPack(self, rectangleWidth, rectangleHeight): if self.x + rectangleWidth >= self.packingAreaWidth: self.x = 0 self.y += self.rowh self.rowh = 0 if self.y + rectangleHeight >= self.packingAreaHeight: return None r = Point(self.x, self.y) self.x += rectangleWidth self.rowh = max(self.rowh, rectangleHeight) return r class CygonRectanglePacker(RectanglePacker): """ Packer using a custom algorithm by Markus 'Cygon' Ewald Algorithm conceived by Markus Ewald (cygon at nuclex dot org), though I'm quite sure I'm not the first one to come up with it :) The algorithm always places rectangles as low as possible in the packing area. So, for any new rectangle that is to be added, the packer has to determine the X coordinate at which the rectangle can have the lowest overall height without intersecting any other rectangles. To quickly discover these locations, the packer uses a sophisticated data structure that stores the upper silhouette of the packing area. When a new rectangle needs to be added, only the silouette edges need to be analyzed to find the position where the rectangle would achieve the lowest""" def __init__(self, packingAreaWidth, packingAreaHeight): """Initializes a new rectangle packer packingAreaWidth: Maximum width of the packing area packingAreaHeight: Maximum height of the packing area""" RectanglePacker.__init__(self, packingAreaWidth, packingAreaHeight) # Stores the height silhouette of the rectangles self.heightSlices = [] # At the beginning, the packing area is a single slice of height 0 self.heightSlices.append(Point(0,0)) def TryPack(self, rectangleWidth, rectangleHeight): """Tries to allocate space for a rectangle in the packing area rectangleWidth: Width of the rectangle to allocate rectangleHeight: Height of the rectangle to allocate Returns a Point instance if space for the rectangle could be allocated be found, otherwise returns None""" placement = None # If the rectangle is larger than the packing area in any dimension, # it will never fit! if rectangleWidth > self.packingAreaWidth or rectangleHeight > \ self.packingAreaHeight: return None # Determine the placement for the new rectangle placement = self.tryFindBestPlacement(rectangleWidth, rectangleHeight) # If a place for the rectangle could be found, update the height slice # table to mark the region of the rectangle as being taken. if placement: self.integrateRectangle(placement.x, rectangleWidth, placement.y \ + rectangleHeight) return placement def tryFindBestPlacement(self, rectangleWidth, rectangleHeight): """Finds the best position for a rectangle of the given dimensions rectangleWidth: Width of the rectangle to find a position for rectangleHeight: Height of the rectangle to find a position for Returns a Point instance if a valid placement for the rectangle could be found, otherwise returns None""" # Slice index, vertical position and score of the best placement we # could find bestSliceIndex = -1 # Slice index where the best placement was found bestSliceY = 0 # Y position of the best placement found # lower == better! bestScore = self.packingAreaHeight # This is the counter for the currently checked position. The search # works by skipping from slice to slice, determining the suitability # of the location for the placement of the rectangle. leftSliceIndex = 0 # Determine the slice in which the right end of the rectangle is located rightSliceIndex = bisect_left(self.heightSlices, Point(rectangleWidth, 0)) while rightSliceIndex <= len(self.heightSlices): # Determine the highest slice within the slices covered by the # rectangle at its current placement. We cannot put the rectangle # any lower than this without overlapping the other rectangles. highest = self.heightSlices[leftSliceIndex].y - for index in xrange(leftSliceIndex + 1, rightSliceIndex): + for index in range(leftSliceIndex + 1, rightSliceIndex): if self.heightSlices[index].y > highest: highest = self.heightSlices[index].y # Only process this position if it doesn't leave the packing area if highest + rectangleHeight < self.packingAreaHeight: score = highest if score < bestScore: bestSliceIndex = leftSliceIndex bestSliceY = highest bestScore = score # Advance the starting slice to the next slice start leftSliceIndex += 1 if leftSliceIndex >= len(self.heightSlices): break # Advance the ending slice until we're on the proper slice again, # given the new starting position of the rectangle. rightRectangleEnd = self.heightSlices[leftSliceIndex].x + rectangleWidth while rightSliceIndex <= len(self.heightSlices): if rightSliceIndex == len(self.heightSlices): rightSliceStart = self.packingAreaWidth else: rightSliceStart = self.heightSlices[rightSliceIndex].x # Is this the slice we're looking for? if rightSliceStart > rightRectangleEnd: break rightSliceIndex += 1 # If we crossed the end of the slice array, the rectangle's right # end has left the packing area, and thus, our search ends. if rightSliceIndex > len(self.heightSlices): break # Return the best placement we found for this rectangle. If the # rectangle didn't fit anywhere, the slice index will still have its # initialization value of -1 and we can report that no placement # could be found. if bestSliceIndex == -1: return None else: return Point(self.heightSlices[bestSliceIndex].x, bestSliceY) def integrateRectangle(self, left, width, bottom): """Integrates a new rectangle into the height slice table left: Position of the rectangle's left side width: Width of the rectangle bottom: Position of the rectangle's lower side""" # Find the first slice that is touched by the rectangle startSlice = bisect_left(self.heightSlices, Point(left, 0)) # We scored a direct hit, so we can replace the slice we have hit firstSliceOriginalHeight = self.heightSlices[startSlice].y self.heightSlices[startSlice] = Point(left, bottom) right = left + width startSlice += 1 # Special case, the rectangle started on the last slice, so we cannot # use the start slice + 1 for the binary search and the possibly # already modified start slice height now only remains in our temporary # firstSliceOriginalHeight variable if startSlice >= len(self.heightSlices): # If the slice ends within the last slice (usual case, unless it # has the exact same width the packing area has), add another slice # to return to the original height at the end of the rectangle. if right < self.packingAreaWidth: self.heightSlices.append(Point(right, firstSliceOriginalHeight)) else: # The rectangle doesn't start on the last slice endSlice = bisect_left(self.heightSlices, Point(right,0), \ startSlice, len(self.heightSlices)) # Another direct hit on the final slice's end? if endSlice < len(self.heightSlices) and not (Point(right, 0) < self.heightSlices[endSlice]): del self.heightSlices[startSlice:endSlice] else: # No direct hit, rectangle ends inside another slice # Find out to which height we need to return at the right end of # the rectangle if endSlice == startSlice: returnHeight = firstSliceOriginalHeight else: returnHeight = self.heightSlices[endSlice - 1].y # Remove all slices covered by the rectangle and begin a new # slice at its end to return back to the height of the slice on # which the rectangle ends. del self.heightSlices[startSlice:endSlice] if right < self.packingAreaWidth: self.heightSlices.insert(startSlice, Point(right, returnHeight)) Index: ps/trunk/source/tools/fontbuilder2/dumpfontchars.py =================================================================== --- ps/trunk/source/tools/fontbuilder2/dumpfontchars.py (revision 24891) +++ ps/trunk/source/tools/fontbuilder2/dumpfontchars.py (revision 24892) @@ -1,18 +1,19 @@ # Dumps lines containing the name of a font followed by a space-separated # list of decimal codepoints (from U+0001 to U+FFFF) for which that font # contains some glyph data. import FontLoader def dump_font(ttf): (face, indexes) = FontLoader.create_cairo_font_face_for_file("../../../binaries/data/tools/fontbuilder/fonts/%s" % ttf, 0, FontLoader.FT_LOAD_DEFAULT) - mappings = [ (c, indexes(unichr(c))) for c in range(1, 65535) ] - print ttf, - print ' '.join(str(c) for (c, g) in mappings if g != 0) + mappings = [ (c, indexes(chr(c))) for c in range(1, 65535) ] + print(ttf, end=' ') + print(' '.join(str(c) for (c, g) in mappings if g != 0)) dump_font("DejaVuSansMono.ttf") -dump_font("DejaVuSans.ttf") +dump_font("FreeSans.ttf") +dump_font("LinBiolinum_Rah.ttf") dump_font("texgyrepagella-regular.otf") dump_font("texgyrepagella-bold.otf") Index: ps/trunk/source/tools/fontbuilder2/fontbuilder.py =================================================================== --- ps/trunk/source/tools/fontbuilder2/fontbuilder.py (revision 24891) +++ ps/trunk/source/tools/fontbuilder2/fontbuilder.py (revision 24892) @@ -1,234 +1,236 @@ +#!/usr/bin/env python3 + import cairo import codecs import math import FontLoader import Packer # Representation of a rendered glyph class Glyph(object): def __init__(self, ctx, renderstyle, char, idx, face, size): self.renderstyle = renderstyle self.char = char self.idx = idx self.face = face self.size = size self.glyph = (idx, 0, 0) if not ctx.get_font_face() == self.face: ctx.set_font_face(self.face) ctx.set_font_size(self.size) extents = ctx.glyph_extents([self.glyph]) self.xadvance = round(extents[4]) # Find the bounding box of strokes and/or fills: inf = 1e300 * 1e300 bb = [inf, inf, -inf, -inf] if "stroke" in self.renderstyle: for (c, w) in self.renderstyle["stroke"]: ctx.set_line_width(w) ctx.glyph_path([self.glyph]) e = ctx.stroke_extents() bb = (min(bb[0], e[0]), min(bb[1], e[1]), max(bb[2], e[2]), max(bb[3], e[3])) ctx.new_path() if "fill" in self.renderstyle: ctx.glyph_path([self.glyph]) e = ctx.fill_extents() bb = (min(bb[0], e[0]), min(bb[1], e[1]), max(bb[2], e[2]), max(bb[3], e[3])) ctx.new_path() bb = (math.floor(bb[0]), math.floor(bb[1]), math.ceil(bb[2]), math.ceil(bb[3])) self.x0 = -bb[0] self.y0 = -bb[1] self.w = bb[2] - bb[0] self.h = bb[3] - bb[1] # Force multiple of 4, to avoid leakage across S3TC blocks # (TODO: is this useful?) #self.w += (4 - (self.w % 4)) % 4 #self.h += (4 - (self.h % 4)) % 4 def pack(self, packer): self.pos = packer.Pack(self.w, self.h) def render(self, ctx): if not ctx.get_font_face() == self.face: ctx.set_font_face(self.face) ctx.set_font_size(self.size) ctx.save() ctx.translate(self.x0, self.y0) ctx.translate(self.pos.x, self.pos.y) # Render each stroke, and then each fill on top of it if "stroke" in self.renderstyle: for ((r, g, b, a), w) in self.renderstyle["stroke"]: ctx.set_line_width(w) ctx.set_source_rgba(r, g, b, a) ctx.glyph_path([self.glyph]) ctx.stroke() if "fill" in self.renderstyle: for (r, g, b, a) in self.renderstyle["fill"]: ctx.set_source_rgba(r, g, b, a) ctx.glyph_path([self.glyph]) ctx.fill() ctx.restore() # Load the set of characters contained in the given text file def load_char_list(filename): f = codecs.open(filename, "r", "utf-8") chars = f.read() f.close() return set(chars) # Construct a Cairo context and surface for rendering text with the given parameters def setup_context(width, height, renderstyle): format = (cairo.FORMAT_ARGB32 if "colour" in renderstyle else cairo.FORMAT_A8) surface = cairo.ImageSurface(format, width, height) ctx = cairo.Context(surface) ctx.set_line_join(cairo.LINE_JOIN_ROUND) return ctx, surface def generate_font(outname, ttfNames, loadopts, size, renderstyle, dsizes): faceList = [] indexList = [] for i in range(len(ttfNames)): (face, indices) = FontLoader.create_cairo_font_face_for_file("../../../binaries/data/tools/fontbuilder/fonts/%s" % ttfNames[i], 0, loadopts) faceList.append(face) if not ttfNames[i] in dsizes: dsizes[ttfNames[i]] = 0 indexList.append(indices) (ctx, _) = setup_context(1, 1, renderstyle) # TODO this gets the line height from the default font # while entire texts can be in the fallback font - ctx.set_font_face(faceList[0]); + ctx.set_font_face(faceList[0]) ctx.set_font_size(size + dsizes[ttfNames[0]]) (_, _, linespacing, _, _) = ctx.font_extents() # Estimate the 'average' height of text, for vertical center alignment charheight = round(ctx.glyph_extents([(indexList[0]("I"), 0.0, 0.0)])[3]) # Translate all the characters into glyphs # (This is inefficient if multiple characters have the same glyph) glyphs = [] #for c in chars: for c in range(0x20, 0xFFFE): for i in range(len(indexList)): - idx = indexList[i](unichr(c)) + idx = indexList[i](chr(c)) if c == 0xFFFD and idx == 0: # use "?" if the missing-glyph glyph is missing idx = indexList[i]("?") if idx: - glyphs.append(Glyph(ctx, renderstyle, unichr(c), idx, faceList[i], size + dsizes[ttfNames[i]])) + glyphs.append(Glyph(ctx, renderstyle, chr(c), idx, faceList[i], size + dsizes[ttfNames[i]])) break # Sort by decreasing height (tie-break on decreasing width) glyphs.sort(key = lambda g: (-g.h, -g.w)) # Try various sizes to pack the glyphs into sizes = [] for h in [32, 64, 128, 256, 512, 1024, 2048, 4096]: sizes.append((h, h)) sizes.append((h*2, h)) - sizes.sort(key = lambda (w, h): (w*h, max(w, h))) # prefer smaller and squarer + sizes.sort(key = lambda w_h: (w_h[0]*w_h[1], max(w_h[0], w_h[1]))) # prefer smaller and squarer for w, h in sizes: try: # Using the dump pacher usually creates bigger textures, but runs faster # In practice the size difference is so small it always ends up in the same size packer = Packer.DumbRectanglePacker(w, h) #packer = Packer.CygonRectanglePacker(w, h) for g in glyphs: g.pack(packer) except Packer.OutOfSpaceError: continue ctx, surface = setup_context(w, h, renderstyle) for g in glyphs: - g.render(ctx) + g.render(ctx) surface.write_to_png("%s.png" % outname) # Output the .fnt file with all the glyph positions etc fnt = open("%s.fnt" % outname, "w") fnt.write("101\n") fnt.write("%d %d\n" % (w, h)) fnt.write("%s\n" % ("rgba" if "colour" in renderstyle else "a")) fnt.write("%d\n" % len(glyphs)) fnt.write("%d\n" % linespacing) fnt.write("%d\n" % charheight) # sorting unneeded, as glyphs are added in increasing order #glyphs.sort(key = lambda g: ord(g.char)) for g in glyphs: x0 = g.x0 y0 = g.y0 # UGLY HACK: see http://trac.wildfiregames.com/ticket/1039 ; # to handle a-macron-acute characters without the hassle of # doing proper OpenType GPOS layout (which the font # doesn't support anyway), we'll just shift the combining acute # glyph by an arbitrary amount to make it roughly the right # place when used after an a-macron glyph. if ord(g.char) == 0x0301: y0 += charheight/3 fnt.write("%d %d %d %d %d %d %d %d\n" % (ord(g.char), g.pos.x, h-g.pos.y, g.w, g.h, -x0, y0, g.xadvance)) fnt.close() return - print "Failed to fit glyphs in texture" + print("Failed to fit glyphs in texture") filled = { "fill": [(1, 1, 1, 1)] } stroked1 = { "colour": True, "stroke": [((0, 0, 0, 1), 2.0), ((0, 0, 0, 1), 2.0)], "fill": [(1, 1, 1, 1)] } stroked2 = { "colour": True, "stroke": [((0, 0, 0, 1), 2.0)], "fill": [(1, 1, 1, 1), (1, 1, 1, 1)] } stroked3 = { "colour": True, "stroke": [((0, 0, 0, 1), 2.5)], "fill": [(1, 1, 1, 1), (1, 1, 1, 1)] } # For extra glyph support, add your preferred font to the font array Sans = (["LinBiolinum_Rah.ttf","FreeSans.ttf"], FontLoader.FT_LOAD_DEFAULT) Sans_Bold = (["LinBiolinum_RBah.ttf","FreeSansBold.ttf"], FontLoader.FT_LOAD_DEFAULT) Sans_Italic = (["LinBiolinum_RIah.ttf","FreeSansOblique.ttf"], FontLoader.FT_LOAD_DEFAULT) SansMono = (["DejaVuSansMono.ttf","FreeMono.ttf"], FontLoader.FT_LOAD_DEFAULT) Serif = (["texgyrepagella-regular.otf","FreeSerif.ttf"], FontLoader.FT_LOAD_NO_HINTING) Serif_Bold = (["texgyrepagella-bold.otf","FreeSerifBold.ttf"], FontLoader.FT_LOAD_NO_HINTING) # Define the size differences used to render different fallback fonts # I.e. when adding a fallback font has smaller glyphs than the original, you can bump it dsizes = {'HanaMinA.ttf': 2} # make the glyphs for the (chinese font 2 pts bigger) fonts = ( ("mono-10", SansMono, 10, filled), ("mono-stroke-10", SansMono, 10, stroked2), ("sans-9", Sans, 9, filled), ("sans-10", Sans, 10, filled), ("sans-12", Sans, 12, filled), ("sans-13", Sans, 13, filled), ("sans-14", Sans, 14, filled), ("sans-16", Sans, 16, filled), ("sans-bold-12", Sans_Bold, 12, filled), ("sans-bold-13", Sans_Bold, 13, filled), ("sans-bold-14", Sans_Bold, 14, filled), ("sans-bold-16", Sans_Bold, 16, filled), ("sans-bold-18", Sans_Bold, 18, filled), ("sans-bold-20", Sans_Bold, 20, filled), ("sans-bold-22", Sans_Bold, 22, filled), ("sans-bold-24", Sans_Bold, 24, filled), ("sans-stroke-12", Sans, 12, stroked2), ("sans-bold-stroke-12", Sans_Bold, 12, stroked3), ("sans-stroke-13", Sans, 13, stroked2), ("sans-bold-stroke-13", Sans_Bold, 13, stroked3), ("sans-stroke-14", Sans, 14, stroked2), ("sans-bold-stroke-14", Sans_Bold, 14, stroked3), ("sans-stroke-16", Sans, 16, stroked2), ) for (name, (fontnames, loadopts), size, style) in fonts: - print "%s..." % name + print("%s..." % name) generate_font("../../../binaries/data/mods/mod/fonts/%s" % name, fontnames, loadopts, size, style, dsizes) Index: ps/trunk/source/tools/templatesanalyzer/Readme.txt =================================================================== --- ps/trunk/source/tools/templatesanalyzer/Readme.txt (revision 24891) +++ ps/trunk/source/tools/templatesanalyzer/Readme.txt (revision 24892) @@ -1,29 +1,29 @@ Template Analyzer. This python tool has been written by wraitii. Its purpose is to help with unit and civ balancing by allowing quick comparison between important template data. Run it using "python unitTables.py" or "pypy unitTables.py" if you have pypy installed. The output will be located in an HTML file called "unit_summary_table.html" in this folder. The script gives 3 informations: -A comparison table of generic templates. -A comparison table of civilization units (it shows the differences with the generic templates) -A comparison of civilization rosters. The script can be customized to change the units that are considered, since loading all units make sit slightly unreadable. By default it loads all citizen soldiers and all champions. To change this, change the "LoadTemplatesIfParent" variable. You can also consider only some civilizations. You may also filter some templates based on their name, if you want to remove specific templates. The HTML page comes with a JS extension that allows to filter and sort in-place, to help with comparisons. You can disable this by disabling javascript or by changing the "AddSortingOverlay" parameter in the script. This extension, called TableFilter, is released under the MIT license. The version I used was the one found at https://github.com/koalyptus/TableFilter/ All contents of this folder are under the MIT License. -Enjoy! \ No newline at end of file +Enjoy! Index: ps/trunk/source/tools/templatesanalyzer/unitTables.py =================================================================== --- ps/trunk/source/tools/templatesanalyzer/unitTables.py (revision 24891) +++ ps/trunk/source/tools/templatesanalyzer/unitTables.py (revision 24892) @@ -1,566 +1,568 @@ +#!/usr/bin/env python3 + # Copyright (C) 2015 Wildfire Games. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import xml.etree.ElementTree as ET import os import glob AttackTypes = ["Hack","Pierce","Crush"] Resources = ["food", "wood", "stone", "metal"] # Generic templates to load # The way this works is it tries all generic templates # But only loads those who have one of the following parents # EG adding "template_unit.xml" will load all units. LoadTemplatesIfParent = ["template_unit_infantry.xml", "template_unit_cavalry.xml", "template_unit_champion.xml", "template_unit_hero.xml"] # Those describe Civs to analyze. # The script will load all entities that derive (to the nth degree) from one of the above templates. Civs = ["athen", "brit", "cart", "gaul", "iber", "kush", "mace", "maur", "pers", "ptol", "rome", "sele", "spart"] # Remote Civ templates with those strings in their name. FilterOut = ["marian", "thureophoros", "thorakites", "kardakes"] # Sorting parameters for the "roster variety" table ComparativeSortByCav = True ComparativeSortByChamp = True SortTypes = ["Support", "Pike", "Spear", "Sword", "Archer", "Javelin", "Sling", "Elephant"] # Classes # Disable if you want the more compact basic data. Enable to allow filtering and sorting in-place. AddSortingOverlay = True # This is the path to the /templates/ folder to consider. Change this for mod support. basePath = os.path.realpath(__file__).replace("unitTables.py","") + "../../../binaries/data/mods/public/simulation/templates/" # For performance purposes, cache opened templates files. globalTemplatesList = {} def htbout(file, balise, value): file.write("<" + balise + ">" + value + "\n" ) def htout(file, value): file.write("

" + value + "

\n" ) def fastParse(templateName): if templateName in globalTemplatesList: return globalTemplatesList[templateName] globalTemplatesList[templateName] = ET.parse(templateName) return globalTemplatesList[templateName] # This function checks that a template has the given parent. def hasParentTemplate(UnitName, parentName): Template = fastParse(UnitName) found = False Name = UnitName while found != True and Template.getroot().get("parent") != None: Name = Template.getroot().get("parent") + ".xml" if Name == parentName: return True Template = ET.parse(Name) return False def NumericStatProcess(unitValue, templateValue): if not "op" in templateValue.attrib: return float(templateValue.text) if (templateValue.attrib["op"] == "add"): unitValue += float(templateValue.text) elif (templateValue.attrib["op"] == "sub"): unitValue -= float(templateValue.text) elif (templateValue.attrib["op"] == "mul"): unitValue *= float(templateValue.text) elif (templateValue.attrib["op"] == "div"): unitValue /= float(templateValue.text) return unitValue # This function parses the entity values manually. def CalcUnit(UnitName, existingUnit = None): unit = { 'HP' : "0", "BuildTime" : "0", "Cost" : { 'food' : "0", "wood" : "0", "stone" : "0", "metal" : "0", "population" : "0"}, 'Attack' : { "Melee" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0 }, "Ranged" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0 } }, 'RepeatRate' : {"Melee" : "0", "Ranged" : "0"},'PrepRate' : {"Melee" : "0", "Ranged" : "0"}, "Armour" : { "Hack" : 0, "Pierce" : 0, "Crush" : 0}, "Ranged" : False, "Classes" : [], "AttackBonuses" : {}, "Restricted" : [], "WalkSpeed" : 0, "Range" : 0, "Spread" : 0, "Civ" : None } if (existingUnit != None): unit = existingUnit Template = fastParse(UnitName) # Recursively get data from our parent which we'll override. if (Template.getroot().get("parent") != None): unit = CalcUnit(Template.getroot().get("parent") + ".xml", unit) unit["Parent"] = Template.getroot().get("parent") + ".xml" if (Template.find("./Identity/Civ") != None): unit['Civ'] = Template.find("./Identity/Civ").text if (Template.find("./Health/Max") != None): unit['HP'] = NumericStatProcess(unit['HP'], Template.find("./Health/Max")) if (Template.find("./Cost/BuildTime") != None): unit['BuildTime'] = NumericStatProcess(unit['BuildTime'], Template.find("./Cost/BuildTime")) if (Template.find("./Cost/Resources") != None): for type in list(Template.find("./Cost/Resources")): unit['Cost'][type.tag] = NumericStatProcess(unit['Cost'][type.tag], type) if (Template.find("./Cost/Population") != None): unit['Cost']["population"] = NumericStatProcess(unit['Cost']["population"], Template.find("./Cost/Population")) if (Template.find("./Attack/Melee") != None): if (Template.find("./Attack/Melee/RepeatTime") != None): unit['RepeatRate']["Melee"] = NumericStatProcess(unit['RepeatRate']["Melee"], Template.find("./Attack/Melee/RepeatTime")) if (Template.find("./Attack/Melee/PrepareTime") != None): unit['PrepRate']["Melee"] = NumericStatProcess(unit['PrepRate']["Melee"], Template.find("./Attack/Melee/PrepareTime")) for atttype in AttackTypes: if (Template.find("./Attack/Melee/"+atttype) != None): unit['Attack']['Melee'][atttype] = NumericStatProcess(unit['Attack']['Melee'][atttype], Template.find("./Attack/Melee/"+atttype)) if (Template.find("./Attack/Melee/Bonuses") != None): for Bonus in Template.find("./Attack/Melee/Bonuses"): Against = [] CivAg = [] if (Bonus.find("Classes") != None and Bonus.find("Classes").text != None): Against = Bonus.find("Classes").text.split(" ") if (Bonus.find("Civ") != None and Bonus.find("Civ").text != None): CivAg = Bonus.find("Civ").text.split(" ") Val = float(Bonus.find("Multiplier").text) unit["AttackBonuses"][Bonus.tag] = {"Classes" : Against, "Civs" : CivAg, "Multiplier" : Val} if (Template.find("./Attack/Melee/RestrictedClasses") != None): newClasses = Template.find("./Attack/Melee/RestrictedClasses").text.split(" ") for elem in newClasses: if (elem.find("-") != -1): newClasses.pop(newClasses.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(newClasses.index(elem)) unit["Restricted"] += newClasses if (Template.find("./Attack/Ranged") != None): unit['Ranged'] = True if (Template.find("./Attack/Ranged/MaxRange") != None): unit['Range'] = NumericStatProcess(unit['Range'], Template.find("./Attack/Ranged/MaxRange")) if (Template.find("./Attack/Ranged/Spread") != None): unit['Spread'] = NumericStatProcess(unit['Spread'], Template.find("./Attack/Ranged/Spread")) if (Template.find("./Attack/Ranged/RepeatTime") != None): unit['RepeatRate']["Ranged"] = NumericStatProcess(unit['RepeatRate']["Ranged"], Template.find("./Attack/Ranged/RepeatTime")) if (Template.find("./Attack/Ranged/PrepareTime") != None): unit['PrepRate']["Ranged"] = NumericStatProcess(unit['PrepRate']["Ranged"], Template.find("./Attack/Ranged/PrepareTime")) for atttype in AttackTypes: if (Template.find("./Attack/Ranged/"+atttype) != None): unit['Attack']['Ranged'][atttype] = NumericStatProcess(unit['Attack']['Ranged'][atttype], Template.find("./Attack/Ranged/"+atttype)) if (Template.find("./Attack/Ranged/Bonuses") != None): for Bonus in Template.find("./Attack/Ranged/Bonuses"): Against = [] CivAg = [] if (Bonus.find("Classes") != None and Bonus.find("Classes").text != None): Against = Bonus.find("Classes").text.split(" ") if (Bonus.find("Civ") != None and Bonus.find("Civ").text != None): CivAg = Bonus.find("Civ").text.split(" ") Val = float(Bonus.find("Multiplier").text) unit["AttackBonuses"][Bonus.tag] = {"Classes" : Against, "Civs" : CivAg, "Multiplier" : Val} if (Template.find("./Attack/Melee/RestrictedClasses") != None): newClasses = Template.find("./Attack/Melee/RestrictedClasses").text.split(" ") for elem in newClasses: if (elem.find("-") != -1): newClasses.pop(newClasses.index(elem)) if elem in unit["Restricted"]: unit["Restricted"].pop(newClasses.index(elem)) unit["Restricted"] += newClasses if (Template.find("./Armour") != None): for atttype in AttackTypes: if (Template.find("./Armour/"+atttype) != None): unit['Armour'][atttype] = NumericStatProcess(unit['Armour'][atttype], Template.find("./Armour/"+atttype)) if (Template.find("./UnitMotion") != None): if (Template.find("./UnitMotion/WalkSpeed") != None): unit['WalkSpeed'] = NumericStatProcess(unit['WalkSpeed'], Template.find("./UnitMotion/WalkSpeed")) if (Template.find("./Identity/VisibleClasses") != None): newClasses = Template.find("./Identity/VisibleClasses").text.split(" ") for elem in newClasses: if (elem.find("-") != -1): newClasses.pop(newClasses.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(newClasses.index(elem)) unit["Classes"] += newClasses if (Template.find("./Identity/Classes") != None): newClasses = Template.find("./Identity/Classes").text.split(" ") for elem in newClasses: if (elem.find("-") != -1): newClasses.pop(newClasses.index(elem)) if elem in unit["Classes"]: unit["Classes"].pop(newClasses.index(elem)) unit["Classes"] += newClasses return unit def WriteUnit(Name, UnitDict): ret = "" ret += "" + Name + "" ret += "" + str(int(UnitDict["HP"])) + "" ret += "" +str("%.0f" % float(UnitDict["BuildTime"])) + "" ret += "" + str("%.1f" % float(UnitDict["WalkSpeed"])) + "" for atype in AttackTypes: PercentValue = 1.0 - (0.9 ** float(UnitDict["Armour"][atype])) ret += "" + str("%.0f" % float(UnitDict["Armour"][atype])) + " / " + str("%.0f" % (PercentValue*100.0)) + "%" attType = ("Ranged" if UnitDict["Ranged"] == True else "Melee") if UnitDict["RepeatRate"][attType] != "0": for atype in AttackTypes: repeatTime = float(UnitDict["RepeatRate"][attType])/1000.0 ret += "" + str("%.1f" % (float(UnitDict["Attack"][attType][atype])/repeatTime)) + "" ret += "" + str("%.1f" % (float(UnitDict["RepeatRate"][attType])/1000.0)) + "" else: for atype in AttackTypes: ret += " - " ret += " - " if UnitDict["Ranged"] == True and UnitDict["Range"] > 0: ret += "" + str("%.1f" % float(UnitDict["Range"])) + "" spread = float(UnitDict["Spread"]) ret += "" + str("%.1f" % spread) + "" else: ret += " - - " for rtype in Resources: ret += "" + str("%.0f" % float(UnitDict["Cost"][rtype])) + "" ret += "" + str("%.0f" % float(UnitDict["Cost"]["population"])) + "" ret += "" for Bonus in UnitDict["AttackBonuses"]: ret += "[" for classe in UnitDict["AttackBonuses"][Bonus]["Classes"]: ret += classe + " " ret += ': ' + str(UnitDict["AttackBonuses"][Bonus]["Multiplier"]) + "] " ret += "" ret += "\n" return ret # Sort the templates dictionary. def SortFn(A): sortVal = 0 for classe in SortTypes: sortVal += 1 if classe in A[1]["Classes"]: break if ComparativeSortByChamp == True and A[0].find("champion") == -1: sortVal -= 20 if ComparativeSortByCav == True and A[0].find("cavalry") == -1: sortVal -= 10 if A[1]["Civ"] != None and A[1]["Civ"] in Civs: sortVal += 100 * Civs.index(A[1]["Civ"]) return sortVal # helper to write coloured text. def WriteColouredDiff(file, diff, PositOrNegat): def cleverParse(diff): if float(diff) - int(diff) < 0.001: return str(int(diff)) else: return str("%.1f" % float(diff)) if (PositOrNegat == "positive"): file.write(" 0 else "0,150,0")) + ");\">" + cleverParse(diff) + "") elif (PositOrNegat == "negative"): file.write("" + cleverParse(diff) + "") else: complain ############################################################ ############################################################ # Create the HTML file f = open(os.path.realpath(__file__).replace("unitTables.py","") + 'unit_summary_table.html', 'w') f.write("\n\n\n Unit Tables\n \n\n") htbout(f,"h1","Unit Summary Table") f.write("\n") os.chdir(basePath) ############################################################ # Load generic templates templates = {} htbout(f,"h2", "Units") f.write("\n") f.write("") f.write("\n") f.write("") f.write("\n\n") for template in list(glob.glob('template_*.xml')): if os.path.isfile(template): found = False for possParent in LoadTemplatesIfParent: if hasParentTemplate(template, possParent): found = True break if found == True: templates[template] = CalcUnit(template) f.write(WriteUnit(template, templates[template])) f.write("
HP BuildTime Speed(walk) Armour Attack (DPS) Costs Efficient Against
HPC HPCRateRangeSpread\n(/100m) FWSMP
") ############################################################ # Load Civ specific templates CivTemplates = {} for Civ in Civs: CivTemplates[Civ] = {} # Load all templates that start with that civ indicator for template in list(glob.glob('units/' + Civ + '_*.xml')): if os.path.isfile(template): # filter based on FilterOut breakIt = False for filter in FilterOut: if template.find(filter) != -1: breakIt = True if breakIt: continue # filter based on loaded generic templates breakIt = True for possParent in LoadTemplatesIfParent: if hasParentTemplate(template, possParent): breakIt = False break if breakIt: continue unit = CalcUnit(template) # Remove variants for now if unit["Parent"].find("template_") == -1: continue # load template CivTemplates[Civ][template] = unit ############################################################ f.write("\n\n

Units Specializations

\n") f.write("

This table compares each template to its parent, showing the differences between the two.
Note that like any table, you can copy/paste this in Excel (or Numbers or ...) and sort it.

") TemplatesByParent = {} #Get them in the array for Civ in Civs: for CivUnitTemplate in CivTemplates[Civ]: parent = CivTemplates[Civ][CivUnitTemplate]["Parent"] if parent in templates and templates[parent]["Civ"] == None: if parent not in TemplatesByParent: TemplatesByParent[parent] = [] TemplatesByParent[parent].append( (CivUnitTemplate,CivTemplates[Civ][CivUnitTemplate])) #Sort them by civ and write them in a table. f.write("\n") f.write("") f.write("\n") f.write("") f.write("\n") for parent in TemplatesByParent: TemplatesByParent[parent].sort(key=lambda x : Civs.index(x[1]["Civ"])) for tp in TemplatesByParent[parent]: f.write("") f.write("") # HP diff = int(tp[1]["HP"]) - int(templates[parent]["HP"]) WriteColouredDiff(f, diff, "negative") # Build Time diff = int(tp[1]["BuildTime"]) - int(templates[parent]["BuildTime"]) WriteColouredDiff(f, diff, "positive") # walk speed diff = float(tp[1]["WalkSpeed"]) - float(templates[parent]["WalkSpeed"]) WriteColouredDiff(f, diff, "negative") # Armor for atype in AttackTypes: diff = float(tp[1]["Armour"][atype]) - float(templates[parent]["Armour"][atype]) WriteColouredDiff(f, diff, "negative") # Attack types (DPS) and rate. attType = ("Ranged" if tp[1]["Ranged"] == True else "Melee") if tp[1]["RepeatRate"][attType] != "0": for atype in AttackTypes: myDPS = float(tp[1]["Attack"][attType][atype]) / (float(tp[1]["RepeatRate"][attType])/1000.0) parentDPS = float(templates[parent]["Attack"][attType][atype]) / (float(templates[parent]["RepeatRate"][attType])/1000.0) WriteColouredDiff(f, myDPS - parentDPS, "negative") WriteColouredDiff(f, float(tp[1]["RepeatRate"][attType])/1000.0 - float(templates[parent]["RepeatRate"][attType])/1000.0, "negative") # range and spread if tp[1]["Ranged"] == True: WriteColouredDiff(f, float(tp[1]["Range"]) - float(templates[parent]["Range"]), "negative") mySpread = float(tp[1]["Spread"]) parentSpread = float(templates[parent]["Spread"]) WriteColouredDiff(f, mySpread - parentSpread, "positive") else: f.write("") else: f.write("") for rtype in Resources: WriteColouredDiff(f, float(tp[1]["Cost"][rtype]) - float(templates[parent]["Cost"][rtype]), "positive") WriteColouredDiff(f, float(tp[1]["Cost"]["population"]) - float(templates[parent]["Cost"]["population"]), "positive") f.write("") f.write("\n") f.write("
HP BuildTime Speed Armour Attack Costs Civ
HPC HPCRateRangeSpread FWSMP
" + parent.replace(".xml","").replace("template_","") + "" + tp[0].replace(".xml","").replace("units/","") + "" + tp[1]["Civ"] + "
") # Table of unit having or not having some units. f.write("\n\n

Roster Variety

\n") f.write("

This table show which civilizations have units who derive from each loaded generic template.
Green means 1 deriving unit, blue means 2, black means 3 or more.
The total is the total number of loaded units for this civ, which may be more than the total of units inheriting from loaded templates.

") f.write("
\n") f.write("\n") for civ in Civs: f.write("\n") f.write("\n") sortedDict = sorted(templates.items(), key=SortFn) for tp in sortedDict: if tp[0] not in TemplatesByParent: continue f.write("\n") for civ in Civs: found = 0 for temp in TemplatesByParent[tp[0]]: if temp[1]["Civ"] == civ: found += 1 if found == 1: f.write("") elif found == 2: f.write("") elif found >= 3: f.write("") else: f.write("") f.write("\n") f.write("\n") for civ in Civs: count = 0 for units in CivTemplates[civ]: count += 1 f.write("\n") f.write("\n") f.write("
Template" + civ + "
" + tp[0] +"
Total:" + str(count) + "
") # Add a simple script to allow filtering on sorting directly in the HTML page. if AddSortingOverlay: f.write("\n\ \n\ \n") f.write("\n") Index: ps/trunk/source/tools/xmlvalidator/validator.py =================================================================== --- ps/trunk/source/tools/xmlvalidator/validator.py (revision 24891) +++ ps/trunk/source/tools/xmlvalidator/validator.py (revision 24892) @@ -1,156 +1,156 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import argparse import os import re import sys import time import xml.etree.ElementTree class Actor: def __init__(self, mod_name, vfs_path): self.mod_name = mod_name self.vfs_path = vfs_path self.name = os.path.basename(vfs_path) self.textures = [] self.material = '' def read(self, physical_path): try: tree = xml.etree.ElementTree.parse(physical_path) except xml.etree.ElementTree.ParseError as err: sys.stderr.write('Error in "%s": %s\n' % (physical_path, err.msg)) return False root = tree.getroot() for element in root.findall('.//material'): self.material = element.text for element in root.findall('.//texture'): self.textures.append(element.get('name')) return True class Material: def __init__(self, mod_name, vfs_path): self.mod_name = mod_name self.vfs_path = vfs_path self.name = os.path.basename(vfs_path) self.required_textures = [] def read(self, physical_path): try: root = xml.etree.ElementTree.parse(physical_path).getroot() except xml.etree.ElementTree.ParseError as err: sys.stderr.write('Error in "%s": %s\n' % (physical_path, err.msg)) return False for element in root.findall('.//required_texture'): texture_name = element.get('name') self.required_textures.append(texture_name) return True class Validator: def __init__(self, vfs_root, mods=None): if mods is None: mods = ['mod', 'public'] self.vfs_root = vfs_root self.mods = mods self.materials = {} self.invalid_materials = {} self.actors = [] def get_physical_path(self, mod_name, vfs_path): return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path)) def find_mod_files(self, mod_name, vfs_path, pattern): physical_path = self.get_physical_path(mod_name, vfs_path) result = [] if not os.path.isdir(physical_path): return result for file_name in os.listdir(physical_path): if file_name == '.git' or file_name == '.svn': continue vfs_file_path = os.path.join(vfs_path, file_name) physical_file_path = os.path.join(physical_path, file_name) if os.path.isdir(physical_file_path): result += self.find_mod_files(mod_name, vfs_file_path, pattern) elif os.path.isfile(physical_file_path) and pattern.match(file_name): result.append({ 'mod_name': mod_name, 'vfs_path': vfs_file_path }) return result def find_all_mods_files(self, vfs_path, pattern): result = [] for mod_name in reversed(self.mods): result += self.find_mod_files(mod_name, vfs_path, pattern) return result def find_materials(self, vfs_path): material_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml')) for material_file in material_files: material_name = os.path.basename(material_file['vfs_path']) if material_name in self.materials: continue material = Material(material_file['mod_name'], material_file['vfs_path']) if material.read(self.get_physical_path(material_file['mod_name'], material_file['vfs_path'])): self.materials[material_name] = material else: self.invalid_materials[material_name] = material def find_actors(self, vfs_path): actor_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml')) for actor_file in actor_files: actor = Actor(actor_file['mod_name'], actor_file['vfs_path']) if actor.read(self.get_physical_path(actor_file['mod_name'], actor_file['vfs_path'])): self.actors.append(actor) def run(self): start_time = time.time() sys.stdout.write('Collecting list of files to check\n') self.find_materials(os.path.join('art', 'materials')) self.find_actors(os.path.join('art', 'actors')) for actor in self.actors: if not actor.material: continue if actor.material not in self.materials and actor.material not in self.invalid_materials: sys.stderr.write('Error in "%s": unknown material "%s"' % ( self.get_physical_path(actor.mod_name, actor.vfs_path), actor.material )) if actor.material not in self.materials: continue material = self.materials[actor.material] for required_texture in material.required_textures: if required_texture in actor.textures: continue sys.stderr.write('Error in "%s": actor does not contain required texture "%s" from "%s"\n' % ( self.get_physical_path(actor.mod_name, actor.vfs_path), required_texture, material.name )) for texture in actor.textures: if texture in material.required_textures: continue sys.stderr.write('Warning in "%s": actor contains unnecessary texture "%s" from "%s"\n' % ( self.get_physical_path(actor.mod_name, actor.vfs_path), texture, material.name )) finish_time = time.time() sys.stdout.write('Total execution time: %.3f seconds.\n' % (finish_time - start_time)) if __name__ == '__main__': script_dir = os.path.dirname(os.path.realpath(__file__)) default_root = os.path.join(script_dir, '..', '..', '..', 'binaries', 'data', 'mods') parser = argparse.ArgumentParser(description='Actors/materials validator.') parser.add_argument('-r', '--root', action='store', dest='root', default=default_root) parser.add_argument('-m', '--mods', action='store', dest='mods', default='mod,public') args = parser.parse_args() validator = Validator(args.root, args.mods.split(',')) validator.run()