diff options
-rwxr-xr-x | client/add-git-attributes | 2 | ||||
-rw-r--r-- | client/adler32/.gitignore | 1 | ||||
-rw-r--r-- | client/adler32/Makefile | 16 | ||||
-rw-r--r-- | client/adler32/adler32.c | 68 | ||||
-rwxr-xr-x | client/edit-all-to-export-tilesets.sh | 19 | ||||
-rwxr-xr-x | client/edit-map-tileset-names.sh | 15 | ||||
-rwxr-xr-x | client/formatXML.sh | 13 | ||||
-rw-r--r-- | client/indent.xsl | 7 | ||||
-rwxr-xr-x | client/list-tileset-order | 15 | ||||
-rwxr-xr-x | client/make-updates | 125 | ||||
-rwxr-xr-x | client/map-db.py | 59 | ||||
-rwxr-xr-x | client/map-diff.py | 192 | ||||
-rwxr-xr-x | client/minimap-render.py | 167 |
13 files changed, 699 insertions, 0 deletions
diff --git a/client/add-git-attributes b/client/add-git-attributes new file mode 100755 index 0000000..80cc30c --- /dev/null +++ b/client/add-git-attributes @@ -0,0 +1,2 @@ +#!/bin/sh +git config diff.csv2tsv.textconv 'sed s/,/\\t/g' diff --git a/client/adler32/.gitignore b/client/adler32/.gitignore new file mode 100644 index 0000000..cfcde45 --- /dev/null +++ b/client/adler32/.gitignore @@ -0,0 +1 @@ +/adler32 diff --git a/client/adler32/Makefile b/client/adler32/Makefile new file mode 100644 index 0000000..42f101a --- /dev/null +++ b/client/adler32/Makefile @@ -0,0 +1,16 @@ +prefix=/usr/local +bindir=${prefix}/bin + +LDLIBS=-lz + +all: adler32 + +adler32: adler32.c + +clean: + rm -f adler32 + +install: + install -D adler32 ${bindir}/ + +.PHONY: clean install diff --git a/client/adler32/adler32.c b/client/adler32/adler32.c new file mode 100644 index 0000000..5dd7e4c --- /dev/null +++ b/client/adler32/adler32.c @@ -0,0 +1,68 @@ +/* + * adler32.c (c) 2006 Bjorn Lindeijer + * License: GPL, v2 or later + * + * Calculates Adler-32 checksums for all files passed as argument. + * + * Usage: adler32 [file]... + */ + +#include <stdlib.h> +#include <stdio.h> +#include <zlib.h> + +/** + * Calculates the Adler-32 checksum for the given file. + */ +unsigned long fadler32(FILE *file) +{ + // Obtain file size + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + rewind(file); + + // Calculate Adler-32 checksum + char *buffer = (char*) malloc(fileSize); + fread(buffer, 1, fileSize, file); + unsigned long adler = adler32(0L, Z_NULL, 0); + adler = adler32(adler, (Bytef*) buffer, fileSize); + free(buffer); + + return adler; +} + +/** + * Prints out usage and exists. + */ +void print_usage() +{ + printf("Usage: adler32 [file]...\n"); + exit(0); +} + +int main(int argc, char *argv[]) +{ + int i; /**< Loops through arguments. */ + + if (argc == 1) + { + print_usage(); + } + + for (i = 1; i < argc; ++i) + { + FILE *file = fopen(argv[i], "r"); + + if (!file) + { + printf("Error while opening '%s' for reading!\n", argv[i]); + exit(1); + } + + unsigned long adler = fadler32(file); + printf("%s %lx\n", argv[i], adler); + fclose(file); + } + + return 0; +} diff --git a/client/edit-all-to-export-tilesets.sh b/client/edit-all-to-export-tilesets.sh new file mode 100755 index 0000000..7be3411 --- /dev/null +++ b/client/edit-all-to-export-tilesets.sh @@ -0,0 +1,19 @@ +#!/bin/bash +for MAP in $(ls maps | grep '\.tmx$') +do + TILESETS=$( + grep '<tileset' "maps/$MAP" | + while read TILESET + do + TILESET=${TILESET#*name=\"} + TILESET=${TILESET%%\"*} + echo tilesets/${TILESET}.tsx + done + ) + rm -f ${TILESETS} + (cd tilesets; tiled ../maps/$MAP;) + git add -N tilesets/ + git add --patch +done + + diff --git a/client/edit-map-tileset-names.sh b/client/edit-map-tileset-names.sh new file mode 100755 index 0000000..6c359e2 --- /dev/null +++ b/client/edit-map-tileset-names.sh @@ -0,0 +1,15 @@ +#!/bin/bash +for MAP in maps/*.tmx +do + grep '<tileset\|<image' "$MAP" | + while read TILESET && read IMAGE + do + LINE=$TILESET + TILESET=${TILESET#*name=\"} + TILESET=${TILESET%%\"*} + IMAGE=${IMAGE#*source=\"} + IMAGE=${IMAGE%%.png\"*} + IMAGE=${IMAGE##*/} + sed "/$LINE/s/$TILESET/$IMAGE/" -i "$MAP" + done +done diff --git a/client/formatXML.sh b/client/formatXML.sh new file mode 100755 index 0000000..c830f30 --- /dev/null +++ b/client/formatXML.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +cd .. + +for f in $(find -name "*.xml") +do + if [ ${f} != "./items.xml" ]; then + xsltproc tools/indent.xsl ${f} > ${f}__ + mv ${f}__ ${f} + fi +done +xmlindent items.xml > items.xml_ +sed 's/[[:space:]]*$//' items.xml_ > items.xml diff --git a/client/indent.xsl b/client/indent.xsl new file mode 100644 index 0000000..7d747ab --- /dev/null +++ b/client/indent.xsl @@ -0,0 +1,7 @@ +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="xml" indent="yes"/> + <xsl:strip-space elements="*"/> + <xsl:template match="/"> + <xsl:copy-of select="."/> + </xsl:template> +</xsl:stylesheet> diff --git a/client/list-tileset-order b/client/list-tileset-order new file mode 100755 index 0000000..b8d3f85 --- /dev/null +++ b/client/list-tileset-order @@ -0,0 +1,15 @@ +#!/bin/bash +DIR=${1:-maps} +for MAP in $(ls "$DIR" | grep '\.tmx$' ) +do + echo -n "${MAP%.tmx}:" + grep '<tileset' "$DIR/$MAP" | + while read TILESET + do + TILESET=${TILESET#*source=\"} + TILESET=${TILESET%%.tsx\"*} + TILESET=${TILESET##*/} + echo -n " $TILESET" + done + echo +done diff --git a/client/make-updates b/client/make-updates new file mode 100755 index 0000000..aef0aaf --- /dev/null +++ b/client/make-updates @@ -0,0 +1,125 @@ +#!/bin/bash -e +# This is a tool to automatically generate and ship client updates. +# It is entirely self-contained, storing its own state in the git repo. +# It is called by running 'make updates' in server-data. +# It also supports manually calls for maintenance work. +# TODO: make auto-mode work with music too. + +# local branch to keep update info on +# will be created on first run; updated thereafter +branch=update-zips +# must already exist +# must NOT be a relative path +output=~/www/updates + +function apply_function() +{ + $1 misc-xml charcreation.xml ea-skills.xml effects.xml emotes.xml hair.xml paths.xml settings.xml skills.xml status-effects.xml units.xml + $1 emotes graphics/emotes/ + $1 images graphics/images/ + $1 items graphics/items/ + $1 minimaps graphics/minimaps/ + $1 particles graphics/particles/ + $1 skills graphics/skills/ + $1 sprites graphics/sprites/ + $1 tiles graphics/tiles/ + $1 item-xml items.xml + $1 map-xml maps.xml + $1 maps maps/ + $1 mob-xml monsters.xml + $1 npc-xml npcs.xml + $1 sfx sfx/ + $1 tsx tilesets/ +} + +function add_resource() +{ + pushd $output >/dev/null + adler32 $1 | tee -a resources2.txt | { + read name hash + chmod a+r $name + sed '/<\/updates>/i <update type="data" file="'$name'" hash="'$hash'" />' -i resources.xml + } + popd >/dev/null +} + +# TODO actually use this +function add_music() +{ + adler32 $1 | { + read name hash + chmod a+r $name + sed '/<\/updates>/i <update type="music" required="no" file="'$name'" hash="'$hash'" />' -i resources.xml + } +} + +function do_initial_zip() +{ + zip=$1-$this_update.zip; shift + git ls-files --with-tree=HEAD -- "$@" \ + | zip -q $output/$zip -@ + add_resource $zip +} + +function git_diff_tree() +{ + git diff-tree -r --diff-filter=AM "$@" +} + +function do_delta_zip() +{ + zip=$1-$last_update..$this_update.zip; shift + if git_diff_tree --quiet $last_update $this_update -- "$@" + then + return + fi + git_diff_tree --name-only $last_update $this_update -- "$@" \ + | zip -q $output/$zip -@ + add_resource $zip +} + +function do_initial_zips() +{ + apply_function do_initial_zip +} + +function do_delta_zips() +{ + apply_function do_delta_zip +} + +function main() +{ + if ! test -d $output + then + echo 'Fatal error: output directory does not exist' + echo "$output" + return 1 + fi + + this_update=$(git rev-parse --short HEAD) + if ! last_update=$(git rev-parse --short $branch 2>/dev/null) + then + echo 'Doing initial updates' + do_initial_zips + elif test "$this_update" = "$last_update" + then + echo 'No commits since last update generation ...' + else + echo 'Doing incremental updates' + do_delta_zips + fi + git branch -f $branch +} + +if test "$0" = "$BASH_SOURCE" +then + echo 'Generating updates automatically' + main +elif test "$0" = 'bash' +then + echo 'sourcing detected - you can do manual updates' +else + echo 'How did you get here?' + exit 3 +fi diff --git a/client/map-db.py b/client/map-db.py new file mode 100755 index 0000000..5215e72 --- /dev/null +++ b/client/map-db.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + +import sys +import os +import subprocess +import tempfile +import re + +CLIENT_DATA_ROOT = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + u'..', + ) +) + +MAP_RE = re.compile(r'^(\d{3})-(\d{1})$') + +def list_missing_minimaps(maps, minimaps): + def minimap_wanted(m): + match = MAP_RE.match(m) + if match: + d = match.group(2) + # We ignore indoor maps + if not d == '2': + return True + return False + + missing_minimaps = set([m for m in maps if minimap_wanted(m)]) - set(minimaps) + retcode = len(missing_minimaps) + print '\n'.join(sorted(missing_minimaps)) + return retcode + +def usage(): + sys.stderr.write(u'''Usage: %(prgm_name)s CMD + + Where CMD is one of: + list-missing-minimaps, lm: Lists all maps which do not + have a minimap. + + \n''' % {'prgm_name': sys.argv[0]}) + +def main(): + if not len(sys.argv) > 1: + usage() + return 127 + action = sys.argv[1].lower() + maps = [os.path.splitext(p)[0] for p in os.listdir(os.path.join(CLIENT_DATA_ROOT, u'maps'))] + minimaps = [os.path.splitext(p)[0] for p in os.listdir(os.path.join(CLIENT_DATA_ROOT, u'graphics', u'minimaps'))] + status = 0 + if action in ('list-missing-minimaps', 'lm'): + status = list_missing_minimaps(maps, minimaps) + else: + usage() + return 127 + return status + +if __name__ == '__main__': + sys.exit(main()) diff --git a/client/map-diff.py b/client/map-diff.py new file mode 100755 index 0000000..1da5cde --- /dev/null +++ b/client/map-diff.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + +import sys +import os +import subprocess +import re +import tempfile + +class MapDiff(object): + + @staticmethod + def check_programs(): + """ + Checks the require programs are available + """ + def which(program): + import os + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + return None + + platform_programs = MapDiff.PROGRAMS.get(sys.platform, MapDiff.PROGRAMS.get('default')) + for program in platform_programs.values(): + if not which(program): + raise Exception('The required "%s" program is missing from your PATH.' % program) + + MAP_RE = re.compile(r'^\d{3}-\d{1}(\.tmx)?$') + PROGRAMS = { + 'default': { + 'tmxrasterizer': 'tmxrasterizer', + 'im_convert': 'convert', + 'im_display': 'display', + 'git': 'git', + }, + 'win32': { + 'tmxrasterizer': 'tmxrasterizer.exe', + 'im_convert': 'convert.exe', + 'im_display': 'display.exe', + 'git': 'git.exe', + }, + } + + def __init__(self): + self.platform_programs = MapDiff.PROGRAMS.get(sys.platform, MapDiff.PROGRAMS.get('default')) + + def _diffmaps(self, tmx1, tmx2, tmxdiffpath): + tmxraster1 = self._rastermap(tmx1) + tmxraster2 = self._rastermap(tmx2) + tmxf, tmxdiff = tempfile.mkstemp(suffix='.png') + subprocess.check_call([ + self.platform_programs.get('im_convert'), tmxraster1, tmxraster2, + '-compose', 'Difference', + '-auto-level', + '-composite', + tmxraster2, + '-compose', 'Screen', + '-composite', + tmxdiffpath + ]) + os.unlink(tmxdiff) + os.unlink(tmxraster1) + os.unlink(tmxraster2) + sys.stdout.write((u'Map diff written to %s\n' % tmxdiffpath).encode('utf-8')) + subprocess.check_call([self.platform_programs.get('im_display'), tmxdiffpath]) + + def _rastermap(self, tmx): + tmxf, tmxraster = tempfile.mkstemp(suffix='.png') + subprocess.check_call([ + self.platform_programs.get('tmxrasterizer'), + '--scale', '1.0', + tmx, tmxraster + ]) + if os.stat(tmxraster).st_size == 0: + raise Exception('A problem was encountered when rendering a map') + return tmxraster + + +class MapGitRevDiff(MapDiff): + + def __init__(self, map_name): + super(MapGitRevDiff, self).__init__() + self.map_name = map_name + + def diff(self): + if not MapDiff.MAP_RE.match(self.map_name): + sys.stderr.write(u'Invalid map name: %s.\n' % self.map_name) + return 1 + if not self.map_name.endswith(u'.tmx'): + self.map_name = self.map_name+u'.tmx' + self.tmx_path = os.path.join(u'..', u'maps', self.map_name) + self.map_number = os.path.splitext(os.path.basename(self.map_name))[0] + p = subprocess.Popen([self.platform_programs.get('git'), '--no-pager', 'log', '-n', '2', '--oneline', '--follow', self.tmx_path], stdout=subprocess.PIPE) + log = p.communicate()[0].splitlines() + if not len(log) == 2: + raise Exception('This map has only one version') + c1 = log[0].split(' ')[0] + c2 = log[1].split(' ')[0] + + # We have the 2 revs to compare. Let's extract the related tmx file + p1 = self._mktmx_from_rev(c1) + p2 = self._mktmx_from_rev(c2) + try: + difftmxpath = '%s_%s-%s.png' % (self.map_number, c1, c2) + self._diffmaps(p1, p2, difftmxpath) + finally: + os.unlink(p1) + os.unlink(p2) + + def _mktmx_from_rev(self, rev): + p = subprocess.Popen([self.platform_programs.get('git'), '--no-pager', 'show', '%s:%s' % (rev, self.tmx_path)], stdout=subprocess.PIPE) + contents = p.communicate()[0] + revtmx = '%s-%s.tmx' % (self.map_number, rev) + f = open(revtmx, 'w') + f.write(contents) + f.close() + return revtmx + + +class MapFileDiff(MapDiff): + + def __init__(self, map1, map2): + super(MapFileDiff, self).__init__() + self.map1 = map1 + self.map2 = map2 + + def diff(self): + b1 = os.path.splitext(os.path.basename(self.map1))[0] + b2 = os.path.splitext(os.path.basename(self.map2))[0] + difftmxpath = '%s__%s.png' % (b1, b2) + self._diffmaps(self.map1, self.map2, difftmxpath) + + +def usage(): + sys.stderr.write(u'''Usage: %s MAP_NAME + %s CHANGED_TMX REFERENCE_TMX + + Example: + $ ./map-diff.py 007-1 + will highlight the changes between the current 007-1 map and its previous version + + $ ./map-diff.py changes-made-by-someone-007-1.tmx ../maps-007-1.tmx + will highlight the changes between the two tmx maps. + Note that these 2 tmx to compare have to satisfy their dependancies, e.g tilesets. + Hence they should be in a sibling directory of the client-data/maps folder. + \n''' % (sys.argv[0], sys.argv[0])) + +def main(): + if not len(sys.argv) > 1: + usage() + return 127 + if not os.path.basename(os.path.dirname(os.getcwdu())) == u'client-data': + sys.stderr.write(u'This script must be run from client-data/tools.\n') + return 1 + try: + MapDiff.check_programs() + except Exception as e: + sys.stderr.write(u'%s\n' % e) + return 126 + if len(sys.argv) == 2: + map_name = sys.argv[1] + mapdiff = MapGitRevDiff(map_name) + try: + mapdiff.diff() + except Exception as e: + sys.stderr.write(u'\x1b[31m\x1b[1mError while generating the diff for map %s: %s\x1b[0m\n' % (map_name, e)) + return 1 + else: + return 0 + else: + map1 = sys.argv[1] + map2 = sys.argv[2] + mapdiff = MapFileDiff(map1, map2) + try: + mapdiff.diff() + except Exception as e: + sys.stderr.write(u'\x1b[31m\x1b[1mError while generating the diff for %s and %s: %s\x1b[0m\n' % (map1, map2, e)) + return 1 + else: + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/client/minimap-render.py b/client/minimap-render.py new file mode 100755 index 0000000..c9cb4a1 --- /dev/null +++ b/client/minimap-render.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + +import sys +import os +import subprocess +import tempfile +import re + +CLIENT_DATA_ROOT = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + u'..', + ) +) + +class MinimapRenderer(object): + + MAP_RE = re.compile(r'^\d{3}-\d{1}(\.tmx)?$') + PROGRAMS = { + 'default': { + 'tmxrasterizer': 'tmxrasterizer', + 'im_convert': 'convert', + }, + 'win32': { + 'tmxrasterizer': 'tmxrasterizer.exe', + 'im_convert': 'convert.exe', + }, + } + + def __init__(self, map_name, tilesize, useAntiAliasing): + self.map_name = map_name + self.tilesize = tilesize + self.useAntiAliasing = useAntiAliasing + + def render(self): + """ + Processes a map + """ + if not MinimapRenderer.MAP_RE.match(self.map_name): + sys.stderr.write(u'Invalid map name: %s. Skipping.\n' % self.map_name) + return 1 + if not self.map_name.endswith(u'.tmx'): + self.map_name = self.map_name+u'.tmx' + + map_number = os.path.splitext(os.path.basename(self.map_name))[0] + tmx_file = os.path.join(CLIENT_DATA_ROOT, u'maps', self.map_name) + minimap_file = os.path.join(CLIENT_DATA_ROOT, u'graphics', u'minimaps', map_number+u'.png') + + prefix = os.path.commonprefix((tmx_file, minimap_file)) + sys.stdout.write(u'%s -> %s\n' % (os.path.relpath(tmx_file, prefix), os.path.relpath(minimap_file, prefix))) + + try: + self.do_render(tmx_file, minimap_file) + except Exception as e: + sys.stderr.write(u'\x1b[31m\x1b[1mError while rendering %s: %s\x1b[0m\n' % (self.map_name, e)) + return 1 + else: + return 0 + + def do_render(self, tmx_file, bitmap_file): + """ + The map rendering implementation + """ + platform_programs = MinimapRenderer.PROGRAMS.get(sys.platform, MinimapRenderer.PROGRAMS.get('default')) + # tmx rasterize + mrf, map_raster = tempfile.mkstemp(suffix='.png') + tmxrasterizer_cmd = [ + platform_programs.get('tmxrasterizer'), + '--tilesize', str(self.tilesize), + ] + if self.useAntiAliasing: + tmxrasterizer_cmd.append('--anti-aliasing') + tmxrasterizer_cmd += [tmx_file, map_raster] + subprocess.check_call(tmxrasterizer_cmd) + if os.stat(map_raster).st_size == 0: + raise Exception('A problem was encountered when rendering a map') + # add cell-shading to the minimap to improve readability + ebf, edges_bitmap = tempfile.mkstemp(suffix='.png') + subprocess.check_call([ + platform_programs.get('im_convert'), map_raster, + '-set', 'option:convolve:scale', '-1!', + '-morphology', 'Convolve', 'Laplacian:0', + '-colorspace', 'gray', + '-auto-level', + '-threshold', '2.8%', + '-negate', + '-transparent', 'white', + edges_bitmap + ]) + subprocess.check_call([ + platform_programs.get('im_convert'), map_raster, edges_bitmap, + '-compose', 'Dissolve', + '-define', 'compose:args=35', + '-composite', + bitmap_file + ]) + os.unlink(map_raster) + os.unlink(edges_bitmap) + + @staticmethod + def check_programs(): + """ + Checks the require programs are available + """ + def which(program): + import os + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + return None + + platform_programs = MinimapRenderer.PROGRAMS.get(sys.platform, MinimapRenderer.PROGRAMS.get('default')) + for program in platform_programs.values(): + if not which(program): + raise Exception('The required "%s" program is missing from your PATH.' % program) + +def usage(): + sys.stderr.write(u'''Usage: %s MAP_NAME... + + Example: + $ ./minimap-render.py 007-1 + will render the map at maps/007-1.tmx in the graphics/minimaps directory. + $ ./minimap-render.py all + will render all existing maps found in the maps directory. + $ ./minimap-render.py update + will update all existing minimaps found in the graphics/minimaps directory. + + For convenience, + $ ./minimap-render.py 007-1.tmx + is also accepted. + \n''' % sys.argv[0]) + +def main(): + if not len(sys.argv) > 1: + usage() + return 127 + try: + MinimapRenderer.check_programs() + except Exception as e: + sys.stderr.write(u'%s\n' % e) + return 126 + + status = 0 + if sys.argv[1].lower() == 'all': + map_names = sorted([os.path.splitext(p)[0] for p in os.listdir(os.path.join(CLIENT_DATA_ROOT, u'maps'))]) + elif sys.argv[1].lower() == 'update': + map_names = sorted([os.path.splitext(p)[0] for p in os.listdir(os.path.join(CLIENT_DATA_ROOT, u'graphics', u'minimaps'))]) + else: + map_names = sys.argv[1:] + + for map_name in map_names: + # Render tiles at 1 pixel size + map_renderer = MinimapRenderer(map_name, 1, True) + status += map_renderer.render() + return status + +if __name__ == '__main__': + sys.exit(main()) |