Map saving first pass

This commit is contained in:
2025-09-18 17:01:10 -05:00
parent a45a2a5bd7
commit 2f40724258
22 changed files with 355 additions and 24 deletions

View File

@@ -7,4 +7,5 @@ add_subdirectory(palette)# Palette asset needs to be added before any images.
add_subdirectory(config) add_subdirectory(config)
add_subdirectory(entity) add_subdirectory(entity)
add_subdirectory(map)
add_subdirectory(ui) add_subdirectory(ui)

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.1" name="entities" tilewidth="16" tileheight="16" tilecount="64" columns="8"> <tileset version="1.10" tiledversion="1.11.2" name="entities" tilewidth="16" tileheight="16" tilecount="64" columns="8">
<image source="entities.png" width="128" height="128"/> <image source="entities.png" width="128" height="128"/>
</tileset> </tileset>

View File

@@ -0,0 +1,6 @@
# Copyright (c) 2025 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
add_asset(MAP untitled.tmx)

28
assets/map/untitled.tmx Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" source="../tileset/prarie.tsx"/>
<layer id="1" name="Tile Layer 1" width="30" height="20">
<data encoding="csv">
2,3,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
9,10,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
16,17,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
</map>

View File

@@ -3,4 +3,4 @@
# This software is released under the MIT License. # This software is released under the MIT License.
# https://opensource.org/licenses/MIT # https://opensource.org/licenses/MIT
add_asset(PALETTE pallet0.png) add_asset(PALETTE palette0.png)

BIN
assets/palette/palette0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

BIN
assets/palette/palette0.pxo Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

BIN
assets/tileset/prarie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

BIN
assets/tileset/prarie.pxo Normal file

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.2" name="prarie" tilewidth="16" tileheight="16" tilecount="21" columns="7">
<image source="prarie.png" width="112" height="48"/>
</tileset>

View File

@@ -0,0 +1,14 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"properties": [
],
"propertyTypes": [
]
}

View File

@@ -0,0 +1,56 @@
{
"Map/SizeTest": {
"height": 4300,
"width": 2
},
"activeFile": "map/untitled.tmx",
"expandedProjectPaths": [
"map",
".",
"tileset"
],
"file.lastUsedOpenFilter": "All Files (*)",
"fileStates": {
"entity/entities.tsx": {
"scaleInDock": 1,
"scaleInEditor": 1
},
"map/prarie.tsx": {
"scaleInDock": 1,
"scaleInEditor": 11
},
"map/untitled.tmx": {
"scale": 3.8326562499999994,
"selectedLayer": 0,
"viewCenter": {
"x": 169.5951730604591,
"y": 108.41045293326268
}
},
"tileset/prarie.tsx": {
"scaleInDock": 1,
"scaleInEditor": 1
}
},
"last.imagePath": "/home/yourwishes/htdocs/dusk/assets/tileset",
"map.lastUsedFormat": "tmx",
"map.tileHeight": 16,
"map.tileWidth": 16,
"openFiles": [
"entity/entities.tsx",
"map/untitled.tmx",
"tileset/prarie.tsx"
],
"project": "untitled.tiled-project",
"recentFiles": [
"entity/entities.tsx",
"tileset/prarie.tsx",
"map/untitled.tmx",
"map/prarie.tsx"
],
"tileset.lastUsedFormat": "tsx",
"tileset.tileSize": {
"height": 16,
"width": 16
}
}

View File

@@ -1,10 +1,26 @@
{ {
"activeFile": "map.tmj", "Map/SizeTest": {
"height": 4300,
"width": 2
},
"activeFile": "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx",
"expandedProjectPaths": [ "expandedProjectPaths": [
".", "templates",
"templates" "."
], ],
"fileStates": { "fileStates": {
"/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx": {
"scaleInDock": 1,
"scaleInEditor": 1
},
"/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx": {
"scale": 1.9163281249999997,
"selectedLayer": 0,
"viewCenter": {
"x": 546.8792042072649,
"y": 320.14350360797425
}
},
":/automap-tiles.tsx": { ":/automap-tiles.tsx": {
"scaleInDock": 1 "scaleInDock": 1
}, },
@@ -16,8 +32,8 @@
"scale": 3, "scale": 3,
"selectedLayer": 2, "selectedLayer": 2,
"viewCenter": { "viewCenter": {
"x": 6603.333333333333, "x": 6912,
"y": 6846.5 "y": 6911.833333333333
} }
}, },
"minogram.tsx": { "minogram.tsx": {
@@ -32,17 +48,20 @@
"last.externalTilesetPath": "/home/yourwishes/htdocs/dusk/data", "last.externalTilesetPath": "/home/yourwishes/htdocs/dusk/data",
"last.imagePath": "/home/yourwishes/htdocs/dusk/data", "last.imagePath": "/home/yourwishes/htdocs/dusk/data",
"last.objectTemplatePath": "/home/yourwishes/htdocs/dusk/data/templates", "last.objectTemplatePath": "/home/yourwishes/htdocs/dusk/data/templates",
"map.lastUsedFormat": "tmx",
"openFiles": [ "openFiles": [
"map.tmj", "/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx",
"overworld.tsx" "/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx"
], ],
"project": "map project.tiled-project", "project": "map project.tiled-project",
"property.type": "string", "property.type": "string",
"recentFiles": [ "recentFiles": [
"overworld.tsx", "/home/yourwishes/htdocs/dusk/assets/entity/entities.tsx",
"/home/yourwishes/htdocs/dusk/assets/map/untitled.tmx",
"map.tmj", "map.tmj",
"minogram.tsx", "overworld.tsx",
"entities.tsx" "entities.tsx",
"minogram.tsx"
], ],
"tileset.lastUsedFilter": "Tiled tileset files (*.tsx *.xml)", "tileset.lastUsedFilter": "Tiled tileset files (*.tsx *.xml)",
"tileset.lastUsedFormat": "tsx", "tileset.lastUsedFormat": "tsx",

View File

@@ -455,7 +455,7 @@
"nextobjectid":14, "nextobjectid":14,
"orientation":"orthogonal", "orientation":"orthogonal",
"renderorder":"right-down", "renderorder":"right-down",
"tiledversion":"1.11.1", "tiledversion":"1.11.2",
"tileheight":16, "tileheight":16,
"tilesets":[ "tilesets":[
{ {

View File

@@ -0,0 +1,12 @@
processedAssets = {}
def assetGetCache(assetPath):
if assetPath in processedAssets:
return processedAssets[assetPath]
return None
def assetCache(assetPath, processedData):
if assetPath in processedAssets:
return processedAssets[assetPath]
processedAssets[assetPath] = processedData
return processedData

View File

@@ -4,13 +4,13 @@ from processimage import processImage
from processpalette import processPalette from processpalette import processPalette
from processconfig import processConfig from processconfig import processConfig
from processtileset import processTileset from processtileset import processTileset
from processmap import processMap
processedAssets = [] processedAssets = []
def processAsset(asset): def processAsset(asset):
if asset['path'] in processedAssets: if asset['path'] in processedAssets:
return return
processedAssets.append(asset['path']) processedAssets.append(asset['path'])
# Handle tiled tilesets # Handle tiled tilesets
@@ -23,6 +23,8 @@ def processAsset(asset):
return processConfig(asset) return processConfig(asset)
elif t == 'tileset': elif t == 'tileset':
return processTileset(asset) return processTileset(asset)
elif t == 'map':
return processMap(asset)
else: else:
print(f"Error: Unknown asset type '{asset['type']}' for path '{asset['path']}'") print(f"Error: Unknown asset type '{asset['type']}' for path '{asset['path']}'")
sys.exit(1) sys.exit(1)

View File

@@ -2,9 +2,14 @@ import os
import sys import sys
from args import args from args import args
from assethelpers import getAssetRelativePath from assethelpers import getAssetRelativePath
from assetcache import assetGetCache, assetCache
def processConfig(asset): def processConfig(asset):
assetPath = asset['path'] assetPath = asset['path']
cache = assetGetCache(assetPath)
if cache is not None:
return cache
print(f"Processing config: {assetPath}") print(f"Processing config: {assetPath}")
# Takes each line, seperates it by either semicolon or newline, # Takes each line, seperates it by either semicolon or newline,
@@ -41,4 +46,4 @@ def processConfig(asset):
outConfig = { outConfig = {
"files": [ outputFilePath ], "files": [ outputFilePath ],
} }
return outConfig return assetCache(assetPath, outConfig)

View File

@@ -4,24 +4,32 @@ from PIL import Image
from processpalette import extractPaletteFromImage, palettes from processpalette import extractPaletteFromImage, palettes
from args import args from args import args
from assethelpers import getAssetRelativePath from assethelpers import getAssetRelativePath
from assetcache import assetGetCache, assetCache
images = [] images = []
def processImage(asset): def processImage(asset):
cache = assetGetCache(asset['path'])
if cache is not None:
return cache
type = None type = None
if 'type' in asset['options']: if 'type' in asset['options']:
type = asset['options'].get('type', 'PALETTIZED').upper() type = asset['options'].get('type', 'PALETTIZED').upper()
if type == 'PALETTIZED' or type is None: if type == 'PALETTIZED' or type is None:
return processPalettizedImage(asset) return assetCache(asset['path'], processPalettizedImage(asset))
elif type == 'ALPHA': elif type == 'ALPHA':
return processAlphaImage(asset) return assetCache(asset['path'], processAlphaImage(asset))
else: else:
print(f"Error: Unknown image type {type} for asset {asset['path']}") print(f"Error: Unknown image type {type} for asset {asset['path']}")
sys.exit(1) sys.exit(1)
def processPalettizedImage(asset): def processPalettizedImage(asset):
assetPath = asset['path'] assetPath = asset['path']
cache = assetGetCache(assetPath)
if cache is not None:
return cache
image = Image.open(assetPath) image = Image.open(assetPath)
imagePalette = extractPaletteFromImage(image) imagePalette = extractPaletteFromImage(image)
@@ -72,10 +80,14 @@ def processPalettizedImage(asset):
'width': image.width, 'width': image.width,
'height': image.height, 'height': image.height,
} }
return outImage return assetCache(assetPath, outImage)
def processAlphaImage(asset): def processAlphaImage(asset):
assetPath = asset['path'] assetPath = asset['path']
cache = assetGetCache(assetPath)
if cache is not None:
return cache
print(f"Processing alpha image: {assetPath}") print(f"Processing alpha image: {assetPath}")
data = bytearray() data = bytearray()
@@ -101,4 +113,4 @@ def processAlphaImage(asset):
'width': image.width, 'width': image.width,
'height': image.height, 'height': image.height,
} }
return outImage return assetCache(assetPath, outImage)

View File

@@ -0,0 +1,164 @@
import sys
import os
from args import args
from xml.etree import ElementTree as ET
from processtileset import processTileset
from assetcache import assetCache, assetGetCache
from assethelpers import getAssetRelativePath
def processMap(asset):
cache = assetGetCache(asset['path'])
if cache is not None:
return cache
# Load the TMX file
tree = ET.parse(asset['path'])
root = tree.getroot()
# Root needs to be "map" element.
if root.tag != 'map':
print(f"Error: TMX file {asset['path']} does not have a <map> root element")
sys.exit(1)
# Root needs to be orientation="orthogonal"
if 'orientation' not in root.attrib or root.attrib['orientation'] != 'orthogonal':
print(f"Error: TMX file {asset['path']} does not have orientation='orthogonal'")
sys.exit(1)
# Extract width, height, tilewidth, tileheight attributes
if 'width' not in root.attrib or 'height' not in root.attrib or 'tilewidth' not in root.attrib or 'tileheight' not in root.attrib:
print(f"Error: TMX file {asset['path']} is missing required attributes (width, height, tilewidth, tileheight)")
sys.exit(1)
mapWidth = int(root.attrib['width'])
mapHeight = int(root.attrib['height'])
tileWidth = int(root.attrib['tilewidth'])
tileHeight = int(root.attrib['tileheight'])
# Find all tileset elements
tilesets = []
for tilesetElement in root.findall('tileset'):
# Tileset must have a source attribute
if 'source' not in tilesetElement.attrib:
print(f"Error: <tileset> element in {asset['path']} is missing a source attribute")
sys.exit(1)
# Must have a firstgid attribute
if 'firstgid' not in tilesetElement.attrib:
print(f"Error: <tileset> element in {asset['path']} is missing a firstgid attribute")
sys.exit(1)
firstGid = int(tilesetElement.attrib['firstgid'])
source = tilesetElement.attrib['source']
# Get source path relative to the tmx file's working directory.
# Needs normalizing also since ".." is often used.
source = os.path.normpath(os.path.join(os.path.dirname(asset['path']), source))
tileset = processTileset({ 'path': source, 'type': 'tileset', 'options': {} })
tilesets.append({
'firstGid': firstGid,
'source': source,
'tileset': tileset
})
# Sort tilesets by firstGid, highest first
tilesets.sort(key=lambda x: x['firstGid'], reverse=True)
# Layer types
# objectLayers = [] # Not implemented
tileLayers = []
for layerElement in root.findall('layer'):
# Assume tile layer for now
# Must have id, name, width, height attributes
if 'id' not in layerElement.attrib or 'name' not in layerElement.attrib or 'width' not in layerElement.attrib or 'height' not in layerElement.attrib:
print(f"Error: <layer> element in {asset['path']} is missing required attributes (id, name, width, height)")
sys.exit(1)
id = int(layerElement.attrib['id'])
name = layerElement.attrib['name']
width = int(layerElement.attrib['width'])
height = int(layerElement.attrib['height'])
# Need exactly one data element
dataElements = layerElement.findall('data')
if len(dataElements) != 1:
print(f"Error: <layer> element in {asset['path']} must have exactly one <data> child element")
sys.exit(1)
# Get text, remove whitespace, split by comman and convert to int
dataElement = dataElements[0]
if dataElement.attrib.get('encoding', '') != 'csv':
print(f"Error: <data> element in {asset['path']} must have encoding='csv'")
sys.exit(1)
dataText = dataElement.text.strip()
data = [int(gid) for gid in dataText.split(',') if gid.strip().isdigit()]
# Should be exactly width * height entries
if len(data) != width * height:
print(f"Error: <data> element in {asset['path']} has {len(data)} entries but expected {width * height} (width * height)")
sys.exit(1)
tileLayers.append({
'id': id,
'name': name,
'width': width,
'height': height,
'data': data,
})
# Now we have our layers all parsed out.
data = bytearray()
data += b'drm' # Dusk RPG Map
data += mapWidth.to_bytes(4, 'little') # Map width in tiles
data += mapHeight.to_bytes(4, 'little') # Map height in tiles
data += len(tilesets).to_bytes(4, 'little') # Number of tilesets
data += len(tileLayers).to_bytes(4, 'little') # Number of layers
# For each tileset
for tileset in tilesets:
data += tileset['firstGid'].to_bytes(4, 'little') # First GID
# For each layer...
for layer in tileLayers:
for gid in layer['data']:
# Get tileset for this gid, since the tilesets are already sorted we can
# simply find the first one that has firstGid <= gid
if gid == 0:
# Empty tile
localIndex = 0xFF
tilesetIndex = 0xFFFFFFFF
else:
for tileset in tilesets:
if gid >= tileset['firstGid']:
tilesetIndex = tilesets.index(tileset)
localIndex = gid - tileset['firstGid']
break
else:
# If no tileset was found, this is an invalid gid
print(f"Error: Invalid tile GID {gid} in {asset['path']}")
sys.exit(1)
if localIndex > 255:
print(f"Error: Local tile index {localIndex} exceeds 255 in {asset['path']}")
sys.exit(1)
data += tilesetIndex.to_bytes(4, 'little') # Tileset index
data += localIndex.to_bytes(1, 'little') # Local tile index
relative = getAssetRelativePath(asset['path'])
fileNameWithoutExt = os.path.splitext(os.path.basename(asset['path']))[0]
outputFileRelative = os.path.join(os.path.dirname(relative), f"{fileNameWithoutExt}.drm")
outputFilePath = os.path.join(args.output_assets, outputFileRelative)
os.makedirs(os.path.dirname(outputFilePath), exist_ok=True)
with open(outputFilePath, "wb") as f:
f.write(data)
outMap = {
'mapPath': outputFileRelative,
'files': [ outputFilePath ],
'width': mapWidth,
'height': mapHeight,
}
return assetCache(asset['path'], outMap)

View File

@@ -3,6 +3,7 @@ from PIL import Image
from args import args from args import args
import sys import sys
import datetime import datetime
from assetcache import assetCache, assetGetCache
palettes = [] palettes = []
@@ -22,6 +23,9 @@ def extractPaletteFromImage(image):
def processPalette(asset): def processPalette(asset):
print(f"Processing palette: {asset['path']}") print(f"Processing palette: {asset['path']}")
cache = assetGetCache(asset['path'])
if cache is not None:
return cache
paletteIndex = len(palettes) paletteIndex = len(palettes)
image = Image.open(asset['path']) image = Image.open(asset['path'])
@@ -72,7 +76,7 @@ def processPalette(asset):
} }
palettes.append(palette) palettes.append(palette)
return palette return assetCache(asset['path'], palette)
def processPaletteList(): def processPaletteList():
data = f"// Auto-generated palette list\n" data = f"// Auto-generated palette list\n"

View File

@@ -6,6 +6,7 @@ import os
import datetime import datetime
from args import args from args import args
from xml.etree import ElementTree from xml.etree import ElementTree
from assetcache import assetGetCache, assetCache
def loadTilesetFromTSX(asset): def loadTilesetFromTSX(asset):
# Load the TSX file # Load the TSX file
@@ -99,8 +100,11 @@ def loadTilesetFromArgs(asset):
} }
def processTileset(asset): def processTileset(asset):
print(f"Processing tileset: {asset['path']}") cache = assetGetCache(asset['path'])
if cache is not None:
return cache
print(f"Processing tileset: {asset['path']}")
tilesetData = None tilesetData = None
if asset['path'].endswith('.tsx'): if asset['path'].endswith('.tsx'):
tilesetData = loadTilesetFromTSX(asset) tilesetData = loadTilesetFromTSX(asset)
@@ -135,9 +139,9 @@ def processTileset(asset):
with open(outputFile, 'w') as f: with open(outputFile, 'w') as f:
f.write(data) f.write(data)
outTileset = { return assetCache(asset['path'], {
"files": [],
"image": tilesetData['image'], "image": tilesetData['image'],
"headerFile": outputFile, "headerFile": outputFile,
"files": tilesetData['image']['files'], "files": tilesetData['image']['files'],
} })
return outTileset