mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-25 01:48:17 +00:00
trackplacer3 a utility to export and import tmx files (#4365)
The python2 trackplacer included both the handling of the file format, and the GUI application. This trackplacer3 is a library for the file format, without the GUI. The new tmx_trackplacer is a command-line tool for exporting the data to Tiled's .tmx format, and re-importing it back to .cfg files, so that the GUI of Tiled can be used to avoid reimplementing the GUI of Trackplacer in Python 3. The implementation uses Tiled's Object Layers (not Tile Layers). This allows additional journey markers to be added with the "Insert Tile" tool, and additional journeys to be added as new layers. It can also read in a .cfg and then re-export it to a new .cfg file, to see if the data is preserved. The format is chosen by the output filename. The old trackplacer2 isn't removed in this commit - before removing it, I think trackplacer3 needs some way to preview the animation. ----- Comments on the mainline campaigns: ----- AToTB, DM, LoW, NR and THoT will work with this. But: Northern Rebirth's bigmap.cfg has a track RECOVERY whose STAGE1 starts with an OLD_REST - that isn't handled by trackplacer, it must have been hand-edited. That OLD_REST will be lost when read by either trackplacer2 or trackplacer3, similarly the OLD_BATTLE of LoW's SAURIANS track will be lost. Delfador's Memoirs SEARCH_STAGE1 is omitted from all subsequent parts of SEARCH. Also in DM, SEARCH_STAGE3 has a point which is then moved in STAGE4 onwards - I guess a hand edit. Both of this will be overwritten if the file is edited with either this tool or with the python2 trackplacer. SotA's journey_chapter*.cfg files and WoV's bigmap.cfg file have some of the trackplacer comments removed, they won't be handled by this tool, at least not until better error handling is added.
This commit is contained in:
parent
a8a5812928
commit
3a8dc9c361
@ -61,6 +61,7 @@
|
||||
always be used.
|
||||
* Make wmllint ignore race= keys if they are part of filters inside [unit_type] (issue #4105)
|
||||
* Removed a few assserts from wmllint and postponed a few unit sanity checks to the closing of a [unit_type] tag (issue #4102)
|
||||
* Added `tmx_trackplacer` tool, a file converter for editing map tracks with Tiled (PR #4464)
|
||||
|
||||
## Version 1.15.2
|
||||
### AI:
|
||||
|
71
data/tools/tmx_trackplacer
Executable file
71
data/tools/tmx_trackplacer
Executable file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
"""
|
||||
tmx_trackplacer -- a tool for placing map tracks using Tiled
|
||||
|
||||
This creates .tmx files (Tiled's format) based on the trackplacer's journey data.
|
||||
The journey can be edited in Tiled using the "Insert Tile" tool, and then the .tmx
|
||||
data converted back to the trackplacer parts of the .cfg file.
|
||||
|
||||
It can also read in a .cfg and then re-export it to a new .cfg file, to see if the
|
||||
data is preserved. The format is chosen by the filename given to the --output option.
|
||||
|
||||
If a .jpg or .png file is given as the input, then the output will be a template .tmx
|
||||
or .cfg file for drawing a track on top of that image.
|
||||
|
||||
Example usage:
|
||||
* data/tools/tmx_trackplacer images/campaign_map.png --output temp.tmx
|
||||
* data/tools/tmx_trackplacer data/campaigns/Northern_Rebirth/utils/bigmap.cfg --output temp.tmx
|
||||
* data/tools/tmx_trackplacer data/campaigns/Northern_Rebirth/utils/bigmap.cfg --output temp.cfg
|
||||
"""
|
||||
|
||||
import wesnoth.trackplacer3 as tp3
|
||||
|
||||
import argparse
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser(usage=__doc__)
|
||||
ap.add_argument("file", metavar="string", help="Read input from this file")
|
||||
ap.add_argument("-o", "--output", metavar="string",
|
||||
help='Write output into the specified file')
|
||||
ap.add_argument("--data-dir", metavar="dir",
|
||||
help='Same as Wesnoth’s “--data-dir” argument')
|
||||
options = ap.parse_args()
|
||||
|
||||
if options.data_dir is None:
|
||||
import os, sys
|
||||
APP_DIR,APP_NAME=os.path.split(os.path.realpath(sys.argv[0]))
|
||||
WESNOTH_ROOT_DIR=os.sep.join(APP_DIR.split(os.sep)[:-2]) # pop out "data" and "tools"
|
||||
options.data_dir=os.path.join(WESNOTH_ROOT_DIR,"data")
|
||||
|
||||
journey = None
|
||||
if options.file:
|
||||
if options.file.endswith(".cfg"):
|
||||
reader = tp3.CfgFileFormat()
|
||||
(journey, metadata) = reader.read(options.file)
|
||||
elif options.file.endswith(".tmx"):
|
||||
reader = tp3.TmxFileFormat(wesnoth_data_dir=options.data_dir)
|
||||
(journey, metadata) = reader.read(options.file)
|
||||
elif options.file.endswith(".png") or options.file.endswith(".jpg"):
|
||||
journey = tp3.Journey()
|
||||
journey.mapfile = options.file
|
||||
metadata = None
|
||||
else:
|
||||
raise RuntimeError("Don't know how to handle input from this file type")
|
||||
|
||||
if journey:
|
||||
print("Read data:", str(journey))
|
||||
else:
|
||||
raise RuntimeError("Failed to read journey data")
|
||||
|
||||
if options.output:
|
||||
if options.output.endswith(".cfg"):
|
||||
print("Exporting as cfg")
|
||||
exporter = tp3.CfgFileFormat()
|
||||
exporter.write(options.output, journey, metadata)
|
||||
elif options.output.endswith(".tmx"):
|
||||
print("Exporting as tmx")
|
||||
exporter = tp3.TmxFileFormat(wesnoth_data_dir=options.data_dir)
|
||||
exporter.write(options.output, journey, metadata)
|
||||
else:
|
||||
raise RuntimeError("Don't know how to handle output to this file type")
|
11
data/tools/wesnoth/trackplacer3/__init__.py
Normal file
11
data/tools/wesnoth/trackplacer3/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# When Python looks for a package, it considers all directories with
|
||||
# a file named __init__.py inside them. Therefore we need this file.
|
||||
# The code below is executed on "import wesnoth.trackplacer3", importing
|
||||
# the main classes that are intended to be public.
|
||||
|
||||
from wesnoth.trackplacer3.datatypes import Journey
|
||||
from wesnoth.trackplacer3.datatypes import Track
|
||||
from wesnoth.trackplacer3.datatypes import Waypoint
|
||||
|
||||
from wesnoth.trackplacer3.cfgfileformat import CfgFileFormat
|
||||
from wesnoth.trackplacer3.tmxfileformat import TmxFileFormat
|
208
data/tools/wesnoth/trackplacer3/cfgfileformat.py
Normal file
208
data/tools/wesnoth/trackplacer3/cfgfileformat.py
Normal file
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
"""Module for reading and writing .cfg files containing journey data.
|
||||
|
||||
It will look for track information enclosed in special comments that look like
|
||||
this:
|
||||
|
||||
# trackplacer: tracks begin
|
||||
# trackplacer: tracks end
|
||||
|
||||
trackplacer will alter only what it finds inside these comments, except that it
|
||||
will also generate a file epilog for undefining local symbols. The
|
||||
epilog will begin with this comment:
|
||||
|
||||
# trackplacer: epilog begins
|
||||
|
||||
TODO: what to do about the epilog? It needs to come after the scenarios, otherwise
|
||||
the undefs happen before the scenarios can use the journey data.
|
||||
|
||||
Special comments may appear in the track section, looking like this:
|
||||
|
||||
# trackplacer: <property>=<value>
|
||||
|
||||
These set properties that trackplacer may use. At present there is
|
||||
only one such property: "map", which records the name of the mapfile on
|
||||
which your track is laid.
|
||||
|
||||
Original (python2 + pygtk) implementation by Eric S. Raymond for the Battle For Wesnoth project, October 2008
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from wesnoth.trackplacer3.datatypes import *
|
||||
|
||||
class IOException(Exception):
|
||||
"""Exception thrown while reading a track file."""
|
||||
def __init__(self, message, path, lineno=None):
|
||||
self.message = message
|
||||
self.path = path
|
||||
self.lineno = lineno
|
||||
|
||||
class CfgFileMetadata:
|
||||
"""Trackplacer is intended to write .cfg files that may also contain other information.
|
||||
|
||||
When it reads a file, in addition to the Journey it keeps some other information so that
|
||||
it can write back to the same file while preserving the other data in the .cfg."""
|
||||
def __init__(self):
|
||||
self.properties = {}
|
||||
self.before = self.after = ""
|
||||
|
||||
class CfgFileFormat(FileFormatHandler):
|
||||
"""Translate a Journey to/from a .cfg file, preserving non-trackplacer data."""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def read(self, fp):
|
||||
if type(fp) == type(""):
|
||||
try:
|
||||
fp = open(fp, "r")
|
||||
except IOError:
|
||||
raise IOException("Cannot read file.", fp)
|
||||
journey = Journey()
|
||||
metadata = CfgFileMetadata()
|
||||
selected_track = None
|
||||
if not fp.name.endswith(".cfg"):
|
||||
raise IOException("Cannot read this filetype.", fp.name)
|
||||
waypoint_re = re.compile("{NEW_(" + "|".join(icon_presentation_order) + ")" \
|
||||
+ " +([0-9]+) +([0-9]+)}")
|
||||
property_re = re.compile("# *trackplacer: ([^=]+)=(.*)")
|
||||
define_re = re.compile("#define (.*)_STAGE[0-9]+(_END|_COMPLETE)?")
|
||||
state = "before"
|
||||
ignore = True # True when the most recent define_re match found an END or COMPLETE group, or before any track has been defined
|
||||
for line in fp:
|
||||
if line.startswith("# trackplacer: epilog begins"):
|
||||
break
|
||||
# This is how we ignore stuff outside of track sections
|
||||
if state == "before":
|
||||
if line.startswith("# trackplacer: tracks begin"):
|
||||
state = "tracks" # And fall through...
|
||||
else:
|
||||
metadata.before += line
|
||||
continue
|
||||
elif state == "after":
|
||||
metadata.after += line
|
||||
continue
|
||||
elif line.startswith("# trackplacer: tracks end"):
|
||||
state = "after"
|
||||
continue
|
||||
# Which track are we appending to?
|
||||
m = re.search(define_re, line)
|
||||
if m:
|
||||
selected_track = journey.findTrack(m.group(1))
|
||||
if selected_track == None:
|
||||
selected_track = Track(m.group(1))
|
||||
journey.tracks.append(selected_track)
|
||||
ignore = bool(m.group(2))
|
||||
continue
|
||||
# Is this a track marker?
|
||||
m = re.search(waypoint_re, line)
|
||||
if m and not ignore:
|
||||
try:
|
||||
tag = m.group(1)
|
||||
x = int(m.group(2))
|
||||
y = int(m.group(3))
|
||||
selected_track.waypoints.append(Waypoint(tag, x, y))
|
||||
continue
|
||||
except ValueError:
|
||||
raise IOException("Invalid coordinate field.", fp.name, i+1)
|
||||
# \todo: Northern Rebirth has some tracks that start with an OLD_REST
|
||||
# before any of the NEW_JOURNEY markers. Maybe add a special-case for
|
||||
# that, that adds them if and only if len(selected_track.waypoints)==0
|
||||
|
||||
# Is it a property setting?
|
||||
m = re.search(property_re, line)
|
||||
if m:
|
||||
metadata.properties[m.group(1)] = m.group(2)
|
||||
continue
|
||||
if "map" in metadata.properties:
|
||||
journey.mapfile = metadata.properties['map']
|
||||
else:
|
||||
raise IOException("Missing map declaration.", fp.name)
|
||||
fp.close()
|
||||
return (journey, metadata)
|
||||
|
||||
def write(self, filename, journey, metadata=None):
|
||||
if metadata is None:
|
||||
metadata = CfgFileMetadata()
|
||||
|
||||
if not filename.endswith(".cfg"):
|
||||
raise IOException("File must have .cfg extension.", fp.name)
|
||||
|
||||
# If we're writing to an existing file, preserve the non-trackplacer parts
|
||||
# by ignoring the provided metadata and re-reading it from the file.
|
||||
try:
|
||||
ignored, metadata = self.read(filename)
|
||||
print("Preserving non-trackplacer data from the destination file")
|
||||
except:
|
||||
pass
|
||||
|
||||
fp = open(filename, "w")
|
||||
fp.write(metadata.before)
|
||||
fp.write("# trackplacer: tracks begin\n#\n")
|
||||
fp.write("# Hand-hack this section strictly at your own risk.\n")
|
||||
fp.write("#\n")
|
||||
if not metadata.before and not metadata.after:
|
||||
fp.write("#\n# wmllint: no translatables\n\n")
|
||||
if journey.mapfile:
|
||||
metadata.properties["map"] = journey.mapfile
|
||||
for (key, val) in list(metadata.properties.items()):
|
||||
fp.write("# trackplacer: %s=%s\n" % (key, val))
|
||||
fp.write("#\n")
|
||||
definitions = []
|
||||
for track in journey.tracks:
|
||||
if len(track.waypoints) == 0:
|
||||
print("Warning: track {name} has no waypoints".format(name=track.name))
|
||||
continue
|
||||
name = track.name
|
||||
endpoints = []
|
||||
for i in range(0, len(track.waypoints)):
|
||||
if track.waypoints[i].action in segmenters:
|
||||
endpoints.append(i)
|
||||
if track.waypoints[-1].action not in segmenters:
|
||||
endpoints.append(len(track.waypoints)-1)
|
||||
outname = name.replace(" ", "_").upper()
|
||||
for (i, e) in enumerate(endpoints):
|
||||
stagename = "%s_STAGE%d" % (outname, i+1,)
|
||||
definitions.append(stagename)
|
||||
fp.write("#define %s\n" % stagename)
|
||||
for j in range(0, e+1):
|
||||
age="OLD"
|
||||
if i == 0 or j > endpoints[i-1]:
|
||||
age = "NEW"
|
||||
waypoint = track.waypoints[j]
|
||||
marker = " {%s_%s %d %d}\n" % (age, waypoint.action, waypoint.x, waypoint.y)
|
||||
fp.write(marker)
|
||||
fp.write("#enddef\n\n")
|
||||
endname = "%s_END" % stagename
|
||||
fp.write("#define %s\n" % endname)
|
||||
definitions.append(endname)
|
||||
for j in range(0, e+1):
|
||||
age="OLD"
|
||||
if j == endpoints[i]:
|
||||
age = "NEW"
|
||||
waypoint = track.waypoints[j]
|
||||
marker = " {%s_%s %d %d}\n" % (age, waypoint.action, waypoint.x, waypoint.y)
|
||||
fp.write(marker)
|
||||
fp.write("#enddef\n\n")
|
||||
completename = "%s_COMPLETE" % outname
|
||||
fp.write("#define %s\n" % completename)
|
||||
definitions.append(completename)
|
||||
for waypoint in track.waypoints:
|
||||
marker = " {%s_%s %d %d}\n" % ("OLD", waypoint.action, waypoint.x, waypoint.y)
|
||||
fp.write(marker)
|
||||
fp.write("#enddef\n\n")
|
||||
fp.write("# trackplacer: tracks end\n")
|
||||
fp.write(metadata.after)
|
||||
|
||||
# \todo: what to do about the epilogue? It must wait until after the scenarios have used the journey data.
|
||||
# fp.write ("# trackplacer: epilog begins\n\n")
|
||||
# for name in definitions:
|
||||
# if "{" + name + "}" not in metadata.after:
|
||||
# fp.write("#undef %s\n" % name)
|
||||
# fp.write ("\n# trackplacer: epilog ends\n")
|
||||
|
||||
fp.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("This isn't intended to be run directly")
|
128
data/tools/wesnoth/trackplacer3/datatypes.py
Executable file
128
data/tools/wesnoth/trackplacer3/datatypes.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
"""
|
||||
trackplacer3.datatypes -- file-format-independent handling of journeys
|
||||
|
||||
A journey is an object containing a map file name and a (possibly
|
||||
empty) list of tracks, each with a name and each consisting of a
|
||||
sequence of track markers.
|
||||
|
||||
Original (python2 + pygtk) implementation by Eric S. Raymond for the Battle For Wesnoth project, October 2008
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
# All dependencies on the shape of the data tree live here
|
||||
# The code does no semantic interpretation of these icons at all;
|
||||
# to add new ones, just fill in a dictionary entry.
|
||||
imagedir = "core/images/"
|
||||
selected_icon_dictionary = {
|
||||
"JOURNEY": imagedir + "misc/new-journey.png",
|
||||
"BATTLE": imagedir + "misc/new-battle.png",
|
||||
"REST": imagedir + "misc/flag-red.png",
|
||||
}
|
||||
unselected_icon_dictionary = {
|
||||
"JOURNEY": imagedir + "misc/dot-white.png",
|
||||
"BATTLE": imagedir + "misc/cross-white.png",
|
||||
"REST": imagedir + "misc/flag-white.png",
|
||||
}
|
||||
icon_presentation_order = ("JOURNEY", "BATTLE", "REST")
|
||||
segmenters = ("BATTLE","REST")
|
||||
|
||||
# Basic functions for bashing points and rectangles
|
||||
|
||||
def _distance(point1, point2):
|
||||
"Euclidean distance between two waypoints."
|
||||
return math.sqrt((point1.x - point2.x)**2 + (point1.y - point2.y)**2)
|
||||
|
||||
class Waypoint:
|
||||
"""Represents a single dot, battle or restpoint."""
|
||||
def __init__(self, action, x, y):
|
||||
self.action = action
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __str__(self):
|
||||
return "<Waypoint '{action}' at {x},{y}>".format(action=self.action, x=self.x, y=self.y)
|
||||
|
||||
class Track:
|
||||
"""An ordered list of Waypoints, users are expected to directly access the data members."""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.waypoints = []
|
||||
|
||||
def insert_at_best_fit(self, w):
|
||||
"""Utility function to add a new Waypoint, working out from from its coordinates where in the sequence it should be added.
|
||||
|
||||
If the new point should definitely be at the end, you can use waypounts.append instead of this method.
|
||||
"""
|
||||
if len(self.waypoints) < 2:
|
||||
self.waypoints.append(w)
|
||||
return
|
||||
|
||||
# Find the index of the member of self.waypoints nearest to the new point
|
||||
closest = min(range(len(self.waypoints)), key=lambda i: _distance(w, self.waypoints[i]))
|
||||
if closest == 0:
|
||||
if _distance(self.waypoints[0], self.waypoints[1]) < _distance(w, self.waypoints[1]):
|
||||
self.waypoints.insert(0, w)
|
||||
else:
|
||||
self.waypoints.insert(1, w)
|
||||
elif closest == len(self.waypoints)-1:
|
||||
if _distance(self.waypoints[-1], self.waypoints[-2]) < _distance(w, self.waypoints[-2]):
|
||||
self.waypoints.append(w)
|
||||
else:
|
||||
self.waypoints.insert(-1, w)
|
||||
elif len(self.waypoints) == 2:
|
||||
self.waypoints.insert(1, w)
|
||||
elif _distance(w, self.waypoints[closest-1]) < _distance(self.waypoints[closest], self.waypoints[closest-1]):
|
||||
self.waypoints.insert(closest, w)
|
||||
else:
|
||||
self.waypoints.insert(closest+1, w)
|
||||
|
||||
class Journey:
|
||||
"""Collection of all Tracks, and the corresponding background image"""
|
||||
def __init__(self):
|
||||
self.mapfile = None # Map background of the journey
|
||||
self.tracks = [] # ordered list of Tracks
|
||||
|
||||
def findTrack(self, name):
|
||||
for track in self.tracks:
|
||||
if name == track.name:
|
||||
return track
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return "<Journey based on map file '%s', with tracks {%s}>" % (self.mapfile,
|
||||
",".join([track.name for track in self.tracks]))
|
||||
|
||||
class FileFormatHandler:
|
||||
"""Interface for reading and writing files"""
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def read(self, file_or_filename):
|
||||
"""Return a (Journey, metadata) pair.
|
||||
|
||||
The metadata may be None, and is information about the source file that
|
||||
isn't represented in the Journey object. The purpose of it is to check
|
||||
whether data is lost by reading a file and then writing the data to a new
|
||||
file.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def write(self, file_or_filename, journey, metadata=None):
|
||||
"""Create a new file or overwriting an existing file.
|
||||
|
||||
When overwriting an existing file, this may try to preserve non-journey
|
||||
data.
|
||||
|
||||
When creating a new file, if the metadata is non-None then this may try
|
||||
to recreate the non-journey data from the original file. This is intended
|
||||
to be used from checking what data is lost in a round-trip, by making the
|
||||
copy of a file with only the journey parts changed.
|
||||
|
||||
If metadata is non-None when overwriting an existing file, it's currently
|
||||
implementation defined which set of data (or neither) will be preserved.
|
||||
"""
|
||||
raise NotImplementedError()
|
219
data/tools/wesnoth/trackplacer3/tmxfileformat.py
Normal file
219
data/tools/wesnoth/trackplacer3/tmxfileformat.py
Normal file
@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
|
||||
"""Module for Tiled (Tile Editor) .tmx files containing journey data.
|
||||
|
||||
Uses Tiled's Object Layers (not Tile Layers). This allows additional journey
|
||||
markers to be added with the "Insert Tile" tool, and additional journeys to be
|
||||
added as new layers.
|
||||
"""
|
||||
|
||||
from wesnoth.trackplacer3.datatypes import *
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Although this doesn't show images, it needs to read their width and height
|
||||
import PIL.Image
|
||||
|
||||
class ParseException(Exception):
|
||||
"""There's a lot of expectations about the .tmx file, this generally
|
||||
means that one of those assumptions didn't hold.
|
||||
"""
|
||||
def __init__(self, message, element=None):
|
||||
self.message = message
|
||||
self.element = element
|
||||
|
||||
class _IdCounter:
|
||||
"""It seems that .tmx has several independent sequences of ids, and can have the same id used for different purposes. However, they can all have gaps in the sequence, for simplicity this code has only one ID counter, and makes each id unique."""
|
||||
def __init__(self):
|
||||
self.counter = 1
|
||||
|
||||
def peek(self):
|
||||
"""Return the id that the next call to get() will return, without changing the id"""
|
||||
return str(self.counter)
|
||||
|
||||
def get(self):
|
||||
counter = self.counter
|
||||
self.counter += 1
|
||||
return str(counter)
|
||||
|
||||
class _TmxTileset:
|
||||
def read(tileset):
|
||||
"""Returns a dict mapping tiles' gid attributes to actions.
|
||||
|
||||
Argument should be the ETree element for the tileset."""
|
||||
base_id = int(tileset.attrib["firstgid"])
|
||||
tileset_to_action = {}
|
||||
for tile in tileset.findall("tile"):
|
||||
tile_id = base_id + int(tile.attrib["id"])
|
||||
image_source = tile.find("image").attrib["source"]
|
||||
action = None
|
||||
# This matches by endswith, so that changes to the wesnoth_data_dir won't
|
||||
# cause unnecessary breakage of .tmx files.
|
||||
for k in selected_icon_dictionary:
|
||||
if image_source.endswith(selected_icon_dictionary[k]):
|
||||
action = k
|
||||
break
|
||||
if action is None:
|
||||
raise ParseException("unrecognised action in tileset")
|
||||
tileset_to_action[str(tile_id)] = action
|
||||
return tileset_to_action
|
||||
|
||||
class TmxFileFormat(FileFormatHandler):
|
||||
"""Translate a Journey to and from a Tiled (Tile Editor) TMX file."""
|
||||
def __init__(self, wesnoth_data_dir):
|
||||
"""The data dir is the same as wesnoth's --data-dir argument.
|
||||
|
||||
The data dir is only used for the journey marker images.
|
||||
"""
|
||||
self.wesnoth_data_dir = wesnoth_data_dir
|
||||
|
||||
def write(self, file_or_filename, journey, metadata=None):
|
||||
id_counter = _IdCounter()
|
||||
|
||||
# Read the size of the background image
|
||||
try:
|
||||
with PIL.Image.open(journey.mapfile) as image:
|
||||
background_width, background_height = image.size
|
||||
except:
|
||||
print("Can't open background image, assuming 1024x768", journey.mapfile)
|
||||
background_width, background_height = 1024, 768
|
||||
|
||||
tmxmap = ET.Element("map", attrib={
|
||||
"version": "1.2",
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
# There's no problem if the width and height don't exactly match the image, this
|
||||
# just determines the size of the grid shown in the UI and used for tile layers.
|
||||
# For track placement, tmx_trackplacer uses object layers instead of tile layers.
|
||||
"width": str(int(background_width / 32)),
|
||||
"height": str(int(background_height / 32)),
|
||||
"tilewidth":"32",
|
||||
"tileheight":"32",
|
||||
"infinite":"0"
|
||||
})
|
||||
|
||||
# Wesnoth's NEW_JOURNEY (etc) macros use the coordinates as the center of the image,
|
||||
# Tiled uses them as the bottom-left corner of the image. This is a dictionary of
|
||||
# [adjustment to x, adjustment to y] pairs, in the direction wesnoth -> tmx.
|
||||
# If any of these images can't be found then there's no point continuing.
|
||||
image_offset = {}
|
||||
for action in selected_icon_dictionary:
|
||||
with PIL.Image.open(self.wesnoth_data_dir + "/" + selected_icon_dictionary[action]) as image:
|
||||
image_offset[action] = [int (- image.size[0] / 2), int (image.size[1] / 2)]
|
||||
|
||||
# embed a tileset in the map, corresponding to the journey icons
|
||||
action_to_tileset = {}
|
||||
base_id = id_counter.get()
|
||||
tileset = ET.SubElement(tmxmap, "tileset", attrib={
|
||||
"firstgid":base_id,
|
||||
"name":"wesnoth journey icons",
|
||||
"tilewidth":"1",
|
||||
"tileheight":"1",
|
||||
"tilecount":"3"
|
||||
})
|
||||
ET.SubElement(tileset, "grid", attrib={"orientation":"orthogonal", "width":"1", "height":"1"})
|
||||
for i, action in enumerate(selected_icon_dictionary):
|
||||
action_to_tileset[action] = {"gid":str(int(base_id) + i)}
|
||||
tile = ET.SubElement(tileset, "tile", attrib={"id":str(i)})
|
||||
ET.SubElement(tile, "image", attrib={
|
||||
"source":self.wesnoth_data_dir + "/" + selected_icon_dictionary[action]
|
||||
})
|
||||
# increment the id_counter
|
||||
id_counter.get()
|
||||
|
||||
# background image
|
||||
layer = ET.SubElement(tmxmap, "imagelayer", attrib={"id": "1", "name": "background"})
|
||||
ET.SubElement(layer, "image", attrib={"source": journey.mapfile})
|
||||
|
||||
# journey tracks
|
||||
for track in journey.tracks:
|
||||
name = track.name
|
||||
layer = ET.SubElement(tmxmap, "objectgroup", attrib={"id": id_counter.get(), "name": name})
|
||||
for point in track.waypoints:
|
||||
if point.action not in action_to_tileset:
|
||||
raise KeyError("Unknown action: " + point.action)
|
||||
attrib = action_to_tileset[point.action]
|
||||
attrib["id"] = id_counter.get()
|
||||
attrib["x"] = str(point.x + image_offset[point.action][0])
|
||||
attrib["y"] = str(point.y + image_offset[point.action][1])
|
||||
o = ET.SubElement(layer, "object", attrib=attrib)
|
||||
|
||||
# The points in each journey need to be kept in the correct order, so that the animation
|
||||
# shows movement in the correct direction. If we know which points are newly-added, then the
|
||||
# logic in Track.insert_at_best_fit will handle them. With Tiled, ids are never reused, so
|
||||
# we can use the id_counter to work out which points are newly added.
|
||||
custom_properties = ET.SubElement(tmxmap, "properties")
|
||||
ET.SubElement(custom_properties, "property", attrib={"name":"tmx_trackplacer_export_id", "type":"int", "value":id_counter.get()})
|
||||
|
||||
# These need to be higher than all ids used elsewhere in the file, so add these after everything else's id is assigned
|
||||
tmxmap.set("nextlayerid", id_counter.get())
|
||||
tmxmap.set("nextobjectid", id_counter.get())
|
||||
|
||||
tree = ET.ElementTree(tmxmap)
|
||||
tree.write(file_or_filename, encoding="UTF-8", xml_declaration=True)
|
||||
|
||||
def read(self, fp):
|
||||
if type(fp) == type(""):
|
||||
# if this raises IOError, let it pass to the caller
|
||||
fp = open(fp, "r")
|
||||
|
||||
tree = ET.parse(fp)
|
||||
tmxmap = tree.getroot()
|
||||
|
||||
journey = Journey()
|
||||
|
||||
if tmxmap.attrib["orientation"] != "orthogonal":
|
||||
raise ParseException("expected an orthogonal TMX map")
|
||||
|
||||
# parse the tileset
|
||||
if len(tmxmap.findall("tileset")) != 1:
|
||||
raise ParseException("expected exactly one tileset")
|
||||
tileset_to_action = _TmxTileset.read(tmxmap.find("tileset"))
|
||||
|
||||
# Wesnoth's NEW_JOURNEY (etc) macros use the coordinates as the center of the image,
|
||||
# Tiled uses them as the bottom-left corner of the image. This is a dictionary of
|
||||
# [adjustment to x, adjustment to y] pairs, in the direction tmx -> wesnoth.
|
||||
# If any of these images can't be found then there's no point continuing.
|
||||
image_offset = {}
|
||||
for action in selected_icon_dictionary:
|
||||
with PIL.Image.open(self.wesnoth_data_dir + "/" + selected_icon_dictionary[action]) as image:
|
||||
image_offset[action] = [int (image.size[0] / 2), int (- image.size[1] / 2)]
|
||||
|
||||
|
||||
# background image
|
||||
if len(tmxmap.findall("imagelayer")) != 1 or tmxmap.find("imagelayer").attrib["name"] != "background":
|
||||
raise ParseException("expected exactly one imagelayer")
|
||||
if len(tmxmap.findall("imagelayer/image")) != 1:
|
||||
raise ParseException("expected exactly one image in the imagelayer")
|
||||
if tmxmap.find("imagelayer/image").attrib["source"] is None:
|
||||
raise ParseException("expected a background image")
|
||||
journey.mapfile = tmxmap.find("imagelayer/image").attrib["source"]
|
||||
|
||||
# metadata that was added in write(), to track which points have been added in Tiled
|
||||
export_id_prop = tmxmap.find("properties/property[@name='tmx_trackplacer_export_id']")
|
||||
if export_id_prop is not None:
|
||||
export_id = int(export_id_prop.attrib["value"])
|
||||
def added_in_tiled(item):
|
||||
return export_id < int(item.attrib["id"])
|
||||
else:
|
||||
def added_in_tiled(item):
|
||||
return true
|
||||
|
||||
# journey tracks
|
||||
for layer in tmxmap.findall("objectgroup"):
|
||||
track = Track(layer.attrib["name"])
|
||||
for point in layer.findall("object"):
|
||||
gid = point.attrib["gid"]
|
||||
if gid not in tileset_to_action:
|
||||
raise KeyError("Unknown action gid: " + gid)
|
||||
action = tileset_to_action[gid]
|
||||
x = round(float(point.attrib["x"])) + image_offset[action][0]
|
||||
y = round(float(point.attrib["y"])) + image_offset[action][1]
|
||||
if added_in_tiled(point):
|
||||
track.insert_at_best_fit(Waypoint(action, x, y))
|
||||
else:
|
||||
track.waypoints.append(Waypoint(action, x, y))
|
||||
journey.tracks.append(track)
|
||||
|
||||
return (journey, None)
|
Loading…
x
Reference in New Issue
Block a user