diff --git a/src/asset/asset.c b/src/asset/asset.c index 9768349..949f374 100644 --- a/src/asset/asset.c +++ b/src/asset/asset.c @@ -20,7 +20,10 @@ assetdef_t ASSET_DEFINITIONS[ASSET_TYPE_COUNT] = { }, [ASSET_TYPE_CONFIG] = { "DCF", assetConfigLoad, assetConfigDispose - } + }, + [ASSET_TYPE_RPG_MAP] = { + "DRM", assetRPGMapLoad, assetRPGMapDispose + }, }; errorret_t assetInit(asset_t *asset, const char_t *filename) { diff --git a/src/asset/asset.h b/src/asset/asset.h index 5a3e97c..d652b5f 100644 --- a/src/asset/asset.h +++ b/src/asset/asset.h @@ -13,6 +13,7 @@ #include "asset/type/assetpaletteimage.h" #include "asset/type/assetalphaimage.h" #include "asset/type/assetconfig.h" +#include "asset/type/assetrpgmap.h" #define ASSET_HEADER_SIZE 3 #define ASSET_REFERENCE_COUNT_MAX 8 @@ -33,6 +34,7 @@ typedef enum { ASSET_TYPE_PALETTE_IMAGE, ASSET_TYPE_ALPHA_IMAGE, ASSET_TYPE_CONFIG, + ASSET_TYPE_RPG_MAP, ASSET_TYPE_COUNT } assettype_t; @@ -47,8 +49,9 @@ typedef struct asset_s { union { assetpaletteimage_t paletteImage; - assetalphaimager_t alphaImage; + assetalphaimage_t alphaImage; assetconfig_t config; + assetrpgmap_t rpgMap; }; } asset_t; diff --git a/src/asset/type/CMakeLists.txt b/src/asset/type/CMakeLists.txt index 7c2e9ea..aef79ac 100644 --- a/src/asset/type/CMakeLists.txt +++ b/src/asset/type/CMakeLists.txt @@ -9,4 +9,5 @@ target_sources(${DUSK_TARGET_NAME} assetalphaimage.c assetconfig.c assetpaletteimage.c + assetrpgmap.c ) \ No newline at end of file diff --git a/src/asset/type/assetalphaimage.h b/src/asset/type/assetalphaimage.h index 380822c..219a186 100644 --- a/src/asset/type/assetalphaimage.h +++ b/src/asset/type/assetalphaimage.h @@ -27,7 +27,7 @@ typedef struct { typedef struct { texture_t texture; -} assetalphaimager_t; +} assetalphaimage_t; /** * Loads an alpha image asset from the given asset structure. The asset must diff --git a/src/asset/type/assetrpgmap.c b/src/asset/type/assetrpgmap.c new file mode 100644 index 0000000..e39f0ab --- /dev/null +++ b/src/asset/type/assetrpgmap.c @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "assetrpgmap.h" +#include "asset/asset.h" +#include "assert/assert.h" +#include "display/tileset/tilesetlist.h" + +errorret_t assetRPGMapLoad(asset_t *asset) { + assertNotNull(asset, "Asset cannot be NULL."); + assertTrue( + asset->type == ASSET_TYPE_RPG_MAP, + "Asset is not of type ASSET_TYPE_RPG_MAP." + ); + + assetrpgmapraw_t raw; + + // Read map header info + zip_int64_t bytesRead = zip_fread( + asset->file, &raw.header, sizeof(assetrpgmapheader_t) + ); + if(bytesRead != sizeof(raw.header)) { + errorThrow("Failed to read RPG map header."); + } + + if(raw.header.mapWidth == 0 || raw.header.mapWidth > MAP_WIDTH_MAX) { + errorThrow("Invalid RPG map width."); + } + + if(raw.header.mapHeight == 0 || raw.header.mapHeight > MAP_HEIGHT_MAX) { + errorThrow("Invalid RPG map height."); + } + + if(raw.header.tilesetCount == 0) { + errorThrow("Invalid RPG map tileset count."); + } + + if(raw.header.tilesetCount > ASSET_RPG_MAP_TILESET_COUNT_MAX) { + errorThrow("Invalid RPG map tileset count."); + } + + if(raw.header.tileLayerCount == 0) { + errorThrow("Invalid RPG map layer count."); + } + + if(raw.header.tileLayerCount > MAP_LAYER_COUNT_MAX) { + errorThrow("Invalid RPG map layer count."); + } + + // Read layers + for(uint32_t layer = 0; layer < raw.header.tileLayerCount; layer++) { + // Read width * height tiles + bytesRead = zip_fread( + asset->file, + &raw.layers[layer].tiles, + sizeof(assetrpgmaptile_t) * raw.header.mapWidth * raw.header.mapHeight + ); + if(bytesRead != ( + sizeof(assetrpgmaptile_t) * raw.header.mapWidth * raw.header.mapHeight + )) errorThrow("Failed to read RPG map layer %u.", layer); + } + + // For each tileset + for(uint32_t tileset = 0; tileset < raw.header.tilesetCount; tileset++) { + // Read tileset + bytesRead = zip_fread( + asset->file, + &raw.tilesets[tileset], + sizeof(assetrpgmaptileset_t) + ); + if(bytesRead != sizeof(assetrpgmaptileset_t)) { + errorThrow("Failed to read RPG map tileset %u.", tileset); + } + } + + // Map data is loaded, we can load it into the map structure. + mapInit(&asset->rpgMap.map); + + entity_t *ent; + ent = mapEntityAdd(&asset->rpgMap.map); + entityInit(ent, ENTITY_TYPE_PLAYER, &asset->rpgMap.map); + + errorOk(); +} + +errorret_t assetRPGMapDispose(asset_t *asset) { + assertNotNull(asset, "Asset cannot be NULL."); + assertTrue( + asset->type == ASSET_TYPE_RPG_MAP, + "Asset is not of type ASSET_TYPE_RPG_MAP." + ); + + errorOk(); +} \ No newline at end of file diff --git a/src/asset/type/assetrpgmap.h b/src/asset/type/assetrpgmap.h new file mode 100644 index 0000000..6b3da24 --- /dev/null +++ b/src/asset/type/assetrpgmap.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2025 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + + +#pragma once +#include "error/error.h" +#include "rpg/world/map.h" + +typedef struct asset_s asset_t; + +#define ASSET_RPG_MAP_TILESET_COUNT_MAX 16 + +#pragma pack(push, 1) +typedef struct { + uint32_t mapWidth; + uint32_t mapHeight; + uint32_t tilesetCount; + uint32_t tileLayerCount; +} assetrpgmapheader_t; + +typedef struct { + uint32_t firstGid; + uint32_t tilesetIndex; +} assetrpgmaptileset_t; + +typedef struct { + uint32_t tilesetIndex; +} assetrpgmaptile_t; + +typedef struct { + assetrpgmaptile_t tiles[MAP_TILE_COUNT_MAX]; +} assetrpgmaplayer_t; + +typedef struct { + assetrpgmapheader_t header; + assetrpgmaplayer_t layers[MAP_LAYER_COUNT_MAX]; + assetrpgmaptileset_t tilesets[ASSET_RPG_MAP_TILESET_COUNT_MAX]; +} assetrpgmapraw_t; +#pragma pack(pop) + +typedef struct { + map_t map; +} assetrpgmap_t; + +/** + * Loads an RPG map asset from the given asset structure. The asset must be of + * type ASSET_TYPE_RPG_MAP and must be loaded. + * + * @param asset The asset to load the RPG map from. + * @return An error code. + */ +errorret_t assetRPGMapLoad(asset_t *asset); + +/** + * Disposes of an RPG map asset, freeing any allocated resources. + * + * @param asset The asset to dispose of. + * @return An error code. + */ +errorret_t assetRPGMapDispose(asset_t *asset); \ No newline at end of file diff --git a/src/display/scene/overworld/sceneoverworld.c b/src/display/scene/overworld/sceneoverworld.c index ed37077..27a3a53 100644 --- a/src/display/scene/overworld/sceneoverworld.c +++ b/src/display/scene/overworld/sceneoverworld.c @@ -37,9 +37,11 @@ errorret_t sceneOverworldInit(void) { } void sceneOverworldUpdate(void) { + if(RPG.map == NULL) return; + // Move camera to player. - const entity_t *start = &testMap.entities[0]; - const entity_t *end = &testMap.entities[testMap.entityCount]; + const entity_t *start = &RPG.map->entities[0]; + const entity_t *end = &RPG.map->entities[RPG.map->entityCount]; while(start < end) { if(start->type == ENTITY_TYPE_PLAYER) { SCENE_OVERWORLD.camera.lookat.target[0] = start->position[0]; @@ -70,7 +72,7 @@ void sceneOverworldRender(void) { cameraPushMatrix(&SCENE_OVERWORLD.camera); - sceneOverworldRenderMap(&testMap); + if(RPG.map != NULL) sceneOverworldRenderMap(RPG.map); spriteBatchFlush(); cameraPopMatrix(); diff --git a/src/display/scene/overworld/sceneoverworld.h b/src/display/scene/overworld/sceneoverworld.h index a1a948b..7297125 100644 --- a/src/display/scene/overworld/sceneoverworld.h +++ b/src/display/scene/overworld/sceneoverworld.h @@ -7,7 +7,7 @@ #pragma once #include "display/camera.h" -#include "rpg/world/map.h" +#include "rpg/rpg.h" #include "error/error.h" typedef struct { diff --git a/src/rpg/rpg.c b/src/rpg/rpg.c index 32b1f4e..0be87fb 100644 --- a/src/rpg/rpg.c +++ b/src/rpg/rpg.c @@ -6,28 +6,44 @@ */ #include "rpg.h" - #include "rpg/world/map.h" +#include "util/memory.h" +#include "asset/assetmanager.h" -map_t testMap; +rpg_t RPG; + +asset_t *asset; +ref_t assetRef; void rpgInit() { - mapInit(&testMap); - testMap.width = 2; - testMap.height = 2; - for(uint32_t i = 0; i < testMap.width * testMap.height; i++) { - testMap.base.tiles[i].id = 1; + memoryZero(&RPG, sizeof(RPG)); + + errorret_t ret = assetManagerLoadAsset("map/untitled.drm", &asset, &assetRef); + if(ret.code != ERROR_OK) { + errorPrint(ret); + errorCatch(ret); + return; } + RPG.map = &asset->rpgMap.map; - entity_t *ent = mapEntityAdd(&testMap); - entityInit(ent, ENTITY_TYPE_PLAYER, &testMap); + // mapInit(&testMap); + // testMap.width = 2; + // testMap.height = 2; + // for(uint32_t i = 0; i < testMap.width * testMap.height; i++) { + // testMap.base.tiles[i].id = 1; + // } - entity_t *npc = mapEntityAdd(&testMap); - entityInit(npc, ENTITY_TYPE_NPC, &testMap); - npc->position[0] = 32.0f; - npc->position[1] = 32.0f; + // entity_t *ent = mapEntityAdd(&testMap); + // entityInit(ent, ENTITY_TYPE_PLAYER, &testMap); + + // entity_t *npc = mapEntityAdd(&testMap); + // entityInit(npc, ENTITY_TYPE_NPC, &testMap); + // npc->position[0] = 32.0f; + // npc->position[1] = 32.0f; } void rpgUpdate() { - mapUpdate(&testMap); + if(RPG.map != NULL) { + mapUpdate(RPG.map); + } } \ No newline at end of file diff --git a/src/rpg/rpg.h b/src/rpg/rpg.h index 9caf524..e7b1b3c 100644 --- a/src/rpg/rpg.h +++ b/src/rpg/rpg.h @@ -6,6 +6,13 @@ */ #pragma once +#include "rpg/world/map.h" + +typedef struct { + map_t *map; +} rpg_t; + +extern rpg_t RPG; /** * Initializes the RPG subsystem. diff --git a/src/rpg/world/map.h b/src/rpg/world/map.h index fce231d..2c9834b 100644 --- a/src/rpg/world/map.h +++ b/src/rpg/world/map.h @@ -13,6 +13,7 @@ #define MAP_WIDTH_MAX 64 #define MAP_HEIGHT_MAX 64 #define MAP_TILE_COUNT_MAX (MAP_WIDTH_MAX * MAP_HEIGHT_MAX) +#define MAP_LAYER_COUNT_MAX 2 typedef struct { uint8_t id; diff --git a/tools/assetstool/main.py b/tools/assetstool/main.py index 4465015..6a37698 100644 --- a/tools/assetstool/main.py +++ b/tools/assetstool/main.py @@ -2,6 +2,7 @@ import sys, os from args import inputAssets, args from processasset import processAsset from processpalette import processPaletteList +from processtileset import processTilesetList from assethelpers import getBuiltAssetsRelativePath import zipfile @@ -29,6 +30,7 @@ with zipfile.ZipFile(outputFileName, 'w') as zipf: # Generate additional headers. processPaletteList() +processTilesetList() # Finalize build if args.build_type == 'header': diff --git a/tools/assetstool/processmap.py b/tools/assetstool/processmap.py index 15764bb..64b9f7d 100644 --- a/tools/assetstool/processmap.py +++ b/tools/assetstool/processmap.py @@ -109,42 +109,21 @@ def processMap(asset): # Now we have our layers all parsed out. data = bytearray() - data += b'drm' # Dusk RPG Map + 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) + data += gid.to_bytes(4, 'little') # Tileset index - 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 + # For each tileset + for tileset in tilesets: + data += tileset['firstGid'].to_bytes(4, 'little') # First GID + data += tileset['tileset']['tilesetIndex'].to_bytes(4, 'little') # Tileset index relative = getAssetRelativePath(asset['path']) fileNameWithoutExt = os.path.splitext(os.path.basename(asset['path']))[0] diff --git a/tools/assetstool/processtileset.py b/tools/assetstool/processtileset.py index 970ab65..1e159e8 100644 --- a/tools/assetstool/processtileset.py +++ b/tools/assetstool/processtileset.py @@ -8,6 +8,8 @@ from args import args from xml.etree import ElementTree from assetcache import assetGetCache, assetCache +tilesets = [] + def loadTilesetFromTSX(asset): # Load the TSX file tree = ElementTree.parse(asset['path']) @@ -138,10 +140,36 @@ def processTileset(asset): os.makedirs(os.path.dirname(outputFile), exist_ok=True) with open(outputFile, 'w') as f: f.write(data) - - return assetCache(asset['path'], { + + + tileset = { "files": [], "image": tilesetData['image'], - "headerFile": outputFile, + "headerFile": os.path.relpath(outputFile, args.headers_dir), + "tilesetName": tilesetName, + "tilesetNameUpper": tilesetNameUpper, + "tilesetIndex": len(tilesets), + "tilesetData": tilesetData, "files": tilesetData['image']['files'], - }) \ No newline at end of file + } + + tilesets.append(tileset) + return assetCache(asset['path'], tileset) + +def processTilesetList(): + data = f"// Tileset List Generated at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + data += f"#pragma once\n" + for tileset in tilesets: + data += f"#include \"{tileset['headerFile']}\"\n" + data += f"\n" + data += f"#define TILESET_LIST_COUNT {len(tilesets)}\n\n" + data += f"static const tileset_t* TILESET_LIST[TILESET_LIST_COUNT] = {{\n" + for tileset in tilesets: + data += f" &TILESET_{tileset['tilesetNameUpper']},\n" + data += f"}};\n" + + # Write header. + outputFile = os.path.join(args.headers_dir, "display", "tileset", f"tilesetlist.h") + os.makedirs(os.path.dirname(outputFile), exist_ok=True) + with open(outputFile, 'w') as f: + f.write(data) \ No newline at end of file