From fcf0de72af8f5d42db92d9e41ae9da0fbf111dce Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Mon, 31 Aug 2026 07:02:19 -0500 Subject: [PATCH] Fixed a bunch of code inconsistencies --- src/dusk/asset/assetfile.c | 6 +- src/dusk/asset/assetfile.h | 59 +- .../asset/loader/chunk/assetchunkloader.c | 27 +- .../loader/cutscene/assetcutsceneloader.c | 40 +- .../loader/cutscene/assetcutsceneloader.h | 72 ++ src/dusk/rpg/entity/entity.c | 235 +++--- src/dusk/rpg/entity/entity.h | 43 ++ src/dusk/rpg/overworld/map.c | 3 +- src/dusk/rpg/overworld/map.h | 9 +- src/dusk/rpg/overworld/worldpos.c | 25 +- src/dusk/rpg/overworld/worldpos.h | 27 +- src/dusk/save/savedevice.c | 54 +- src/dusk/save/savedevice.h | 163 ++++ src/dusk/ui/overlay/uifullbox.c | 2 +- src/dusk/ui/overlay/uifullbox.h | 9 + src/dusk/util/crypt.c | 4 +- src/dusk/util/crypt.h | 8 + src/duskdolphin/log/log.c | 3 +- src/duskdolphin/log/logdolphin.h | 18 + src/duskdolphin/save/savedevicedolphincard.c | 15 +- src/duskdolphin/save/savedevicedolphincard.h | 27 + src/duskdolphin/save/savedevicedolphinnand.c | 2 +- src/duskdolphin/save/savedevicedolphinnand.h | 12 + src/duskdolphin/save/savedevicedolphinsd.c | 2 +- src/duskdolphin/save/savedevicedolphinsd.h | 9 + src/duskpsp/save/savedevicepsp.c | 2 +- src/duskpsp/save/savedevicepsp.h | 9 + tools/asset/chunk/__main__.py | 695 +++++++++--------- tools/asset/cutscene/__main__.py | 16 +- tools/asset/dmf/__main__.py | 56 +- 30 files changed, 1033 insertions(+), 619 deletions(-) create mode 100644 src/duskdolphin/log/logdolphin.h diff --git a/src/dusk/asset/assetfile.c b/src/dusk/asset/assetfile.c index fafc8028..c6dd77a6 100644 --- a/src/dusk/asset/assetfile.c +++ b/src/dusk/asset/assetfile.c @@ -218,7 +218,7 @@ const uint8_t *assetFileLineReaderUnreadPtr( return reader->readBuffer + reader->bufferStart; } -static errorret_t assetFileLineReaderAppend( +errorret_t assetFileLineReaderAppend( assetfilelinereader_t *reader, const uint8_t *src, size_t srcLength @@ -240,7 +240,7 @@ static errorret_t assetFileLineReaderAppend( errorOk(); } -static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) { +void assetFileLineReaderTerminate(assetfilelinereader_t *reader) { assertNotNull(reader, "Reader cannot be NULL."); assertNotNull(reader->outBuffer, "Out buffer cannot be NULL."); assertTrue( @@ -250,7 +250,7 @@ static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) { reader->outBuffer[reader->lineLength] = '\0'; } -static ssize_t assetFileLineReaderFindNewline( +ssize_t assetFileLineReaderFindNewline( const assetfilelinereader_t *reader ) { size_t i; diff --git a/src/dusk/asset/assetfile.h b/src/dusk/asset/assetfile.h index 679baac5..a64e0aa6 100644 --- a/src/dusk/asset/assetfile.h +++ b/src/dusk/asset/assetfile.h @@ -150,10 +150,67 @@ void assetFileLineReaderInit( const size_t outBufferSize ); +/** + * Returns the number of bytes still unread in the line reader's read buffer. + * + * @param reader The line reader to check. + * @return Number of unread bytes remaining in the read buffer. + */ +size_t assetFileLineReaderUnreadBytes(const assetfilelinereader_t *reader); + +/** + * Returns a pointer to the first unread byte in the line reader's read + * buffer. + * + * @param reader The line reader to check. + * @return Pointer to the first unread byte. + */ +const uint8_t *assetFileLineReaderUnreadPtr( + const assetfilelinereader_t *reader +); + +/** + * Appends src to the line reader's current line buffer, growing lineLength. + * + * @param reader The line reader whose out buffer to append to. + * @param src Bytes to append. + * @param srcLength Number of bytes in src. + * @return Error state if any (the line would exceed the output buffer). + */ +errorret_t assetFileLineReaderAppend( + assetfilelinereader_t *reader, + const uint8_t *src, + size_t srcLength +); + +/** + * Null-terminates the line reader's current line buffer at lineLength. + * + * @param reader The line reader whose out buffer to terminate. + */ +void assetFileLineReaderTerminate(assetfilelinereader_t *reader); + +/** + * Searches the line reader's unread buffered bytes for a newline character. + * + * @param reader The line reader to search. + * @return The index of the newline within readBuffer, or -1 if not found. + */ +ssize_t assetFileLineReaderFindNewline(const assetfilelinereader_t *reader); + +/** + * Refills the line reader's read buffer from the underlying file once its + * buffered bytes are fully consumed. A no-op once the file is at EOF. + * + * @param reader The line reader to refill. + * @return Error state if any. + */ +errorret_t assetFileLineReaderFill(assetfilelinereader_t *reader); + /** * Reads the next line from the asset file into the line buffer. The line * buffer is null-terminated and does not include the newline character. - * + * * @param reader The line reader to read from. * @return An error code if a failure occurs, or errorOk() if a line was read * successfully. If the end of the file is reached, errorEndOfFile() is diff --git a/src/dusk/asset/loader/chunk/assetchunkloader.c b/src/dusk/asset/loader/chunk/assetchunkloader.c index 467987a8..b1fb4d77 100644 --- a/src/dusk/asset/loader/chunk/assetchunkloader.c +++ b/src/dusk/asset/loader/chunk/assetchunkloader.c @@ -13,26 +13,7 @@ #include "asset/loader/assetentry.h" #include "asset/loader/assetloader.h" #include "asset/asset.h" - -// Reads a little-endian int16 from a potentially-unaligned offset into a -// worldunit_t, advancing *offset past it. -static worldunit_t assetChunkReadWorldUnit( - const uint8_t *data, - size_t *offset -) { - int16_t value; - memoryCopy(&value, data + *offset, sizeof(int16_t)); - *offset += sizeof(int16_t); - return (worldunit_t)endianLittleToHost16((uint16_t)value); -} - -static worldpos_t assetChunkReadWorldPos(const uint8_t *data, size_t *offset) { - worldpos_t pos; - pos.x = assetChunkReadWorldUnit(data, offset); - pos.y = assetChunkReadWorldUnit(data, offset); - pos.z = assetChunkReadWorldUnit(data, offset); - return pos; -} +#include "rpg/overworld/worldpos.h" errorret_t assetChunkLoaderAsync(assetloading_t *loading) { assertNotNull(loading, "Loading cannot be NULL"); @@ -196,7 +177,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { spawn->itemQuantity = 0; } - spawn->position = assetChunkReadWorldPos(data, &offset); + spawn->position = worldPosReadLE(data, &offset); } out->areaSpawnCount = data[offset]; @@ -208,8 +189,8 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) { for(uint8_t s = 0; s < out->areaSpawnCount; s++) { chunkareaspawn_t *area = &out->areaSpawns[s]; - area->min = assetChunkReadWorldPos(data, &offset); - area->max = assetChunkReadWorldPos(data, &offset); + area->min = worldPosReadLE(data, &offset); + area->max = worldPosReadLE(data, &offset); uint16_t callbackId; memoryCopy(&callbackId, data + offset, sizeof(uint16_t)); diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 9defbe91..7068f148 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -13,62 +13,44 @@ #include "asset/loader/assetentry.h" #include "asset/loader/assetloader.h" #include "asset/asset.h" +#include "rpg/overworld/worldpos.h" // DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1), // itemCount u8 (1), poolSize u16 LE (2) = 12 bytes. #define ASSET_CUTSCENE_HEADER_SIZE 12 -static uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset) { +uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset) { uint8_t value = data[*offset]; *offset += sizeof(uint8_t); return value; } -static uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset) { +uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset) { uint16_t value; memoryCopy(&value, data + *offset, sizeof(uint16_t)); *offset += sizeof(uint16_t); return endianLittleToHost16(value); } -static uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset) { +uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset) { uint32_t value; memoryCopy(&value, data + *offset, sizeof(uint32_t)); *offset += sizeof(uint32_t); return endianLittleToHost32(value); } -static float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset) { +float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset) { float_t value; memoryCopy(&value, data + *offset, sizeof(float_t)); *offset += sizeof(float_t); return endianLittleToHostFloat(value); } -static worldunit_t assetCutsceneReadWorldUnit( - const uint8_t *data, - size_t *offset -) { - uint16_t value = assetCutsceneReadU16(data, offset); - return (worldunit_t)value; -} - -static worldpos_t assetCutsceneReadWorldPos( - const uint8_t *data, - size_t *offset -) { - worldpos_t pos; - pos.x = assetCutsceneReadWorldUnit(data, offset); - pos.y = assetCutsceneReadWorldUnit(data, offset); - pos.z = assetCutsceneReadWorldUnit(data, offset); - return pos; -} - // Copies a length-prefixed string directly into an item's own embedded // char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these // are never pool references, see the item-field inventory in the runtime // cutscene file design. -static void assetCutsceneReadEmbeddedString( +void assetCutsceneReadEmbeddedString( const uint8_t *data, size_t *offset, char_t *dest, @@ -83,7 +65,7 @@ static void assetCutsceneReadEmbeddedString( // Resolves a u16 pool offset (read from the item stream) to a real pointer // into the entry's own persistent pool allocation. -static const char_t * assetCutsceneReadPoolString( +const char_t * assetCutsceneReadPoolString( const uint8_t *data, size_t *offset, const char_t *pool @@ -205,7 +187,7 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT: item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset); - item->entityTeleport.target = assetCutsceneReadWorldPos(data, &offset); + item->entityTeleport.target = worldPosReadLE(data, &offset); break; case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: { @@ -246,7 +228,7 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { case CUTSCENE_ITEM_TYPE_ENTITY_ADD: item->entityAdd.entityType = assetCutsceneReadU8(data, &offset); - item->entityAdd.position = assetCutsceneReadWorldPos(data, &offset); + item->entityAdd.position = worldPosReadLE(data, &offset); break; case CUTSCENE_ITEM_TYPE_ENTITY_TURN: @@ -261,9 +243,9 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { item->entityWalkToEntity.targetEntityIndex = assetCutsceneReadU8(data, &offset); item->entityWalkToEntity.offsetX = - assetCutsceneReadWorldUnit(data, &offset); + worldUnitReadLE(data, &offset); item->entityWalkToEntity.offsetY = - assetCutsceneReadWorldUnit(data, &offset); + worldUnitReadLE(data, &offset); break; case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE: diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h index 403f4e5a..44b88b83 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.h +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.h @@ -42,6 +42,78 @@ typedef struct { char_t *pool; } assetcutsceneoutput_t; +/** + * Reads a uint8_t from the current offset, advancing *offset past it. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded uint8_t. + */ +uint8_t assetCutsceneReadU8(const uint8_t *data, size_t *offset); + +/** + * Reads a little-endian uint16_t from a potentially-unaligned offset, + * advancing *offset past it. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded uint16_t. + */ +uint16_t assetCutsceneReadU16(const uint8_t *data, size_t *offset); + +/** + * Reads a little-endian uint32_t from a potentially-unaligned offset, + * advancing *offset past it. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded uint32_t. + */ +uint32_t assetCutsceneReadU32(const uint8_t *data, size_t *offset); + +/** + * Reads a little-endian float_t from a potentially-unaligned offset, + * advancing *offset past it. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded float_t. + */ +float_t assetCutsceneReadFloat(const uint8_t *data, size_t *offset); + +/** + * Copies a length-prefixed string directly into an item's own embedded + * char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these + * are never pool references, see the item-field inventory in the runtime + * cutscene file design. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the string read. + * @param dest Destination buffer to copy the string into. + * @param destCapacity Capacity of dest, including the null terminator. + */ +void assetCutsceneReadEmbeddedString( + const uint8_t *data, + size_t *offset, + char_t *dest, + const size_t destCapacity +); + +/** + * Resolves a u16 pool offset (read from the item stream) to a real pointer + * into the entry's own persistent pool allocation. + * + * @param data The buffer to read the pool offset from. + * @param offset In/out cursor into data, advanced past the pool offset. + * @param pool The base pointer of the entry's persistent pool allocation. + * @return Pointer to the string within pool. + */ +const char_t * assetCutsceneReadPoolString( + const uint8_t *data, + size_t *offset, + const char_t *pool +); + /** * Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes * into the loading buffer so the sync phase can parse without blocking the diff --git a/src/dusk/rpg/entity/entity.c b/src/dusk/rpg/entity/entity.c index 12fd2eec..6ffaa95f 100644 --- a/src/dusk/rpg/entity/entity.c +++ b/src/dusk/rpg/entity/entity.c @@ -74,6 +74,123 @@ void entityTurn(entity_t *entity, const entitydir_t direction) { entity->animTime = ENTITY_ANIM_TURN_DURATION; } +bool_t entityWalkCheckRampUp( + const tile_t tileCurrent, + const entitydir_t direction, + const worldpos_t newPos, + tile_t *tileNew +) { + if(!tileShapeIsRamp(tileCurrent.shape)) return false; + + bool_t facingRamp = + // Can only walk UP the direction the ramp faces. + (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape || + // If diagonal ramp, can go up one of two ways only. Inner ramps + // share the same allowed directions as their outer counterparts. + ( + ( + tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST || + tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER + ) && + (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) + ) || + ( + ( + tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST || + tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER + ) && + (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) + ) || + ( + ( + tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST || + tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER + ) && + (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) + ) || + ( + ( + tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST || + tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER + ) && + (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) + ); + if(!facingRamp) return false; + + tile_t tileNewSaved = *tileNew; + *tileNew = TILE_NULL; + worldpos_t abovePos = newPos; + abovePos.z += 1; + tile_t tileAbove = mapGetTile(abovePos); + + if(tileAbove.shape != TILE_SHAPE_NULL && tileShapeIsWalkable(tileAbove.shape)) { + return true; + } + + *tileNew = tileNewSaved; + return false; +} + +bool_t entityWalkCheckFall( + const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction +) { + if(tileNew.shape != TILE_SHAPE_NULL || newPos.z <= 0) return false; + + worldpos_t belowPos = newPos; + belowPos.z -= 1; + tile_t tileBelow = mapGetTile(belowPos); + if(tileBelow.shape == TILE_SHAPE_NULL || !tileShapeIsRamp(tileBelow.shape)) { + return false; + } + + return ( + // This handles regular cardinal ramps + (entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH) == tileBelow.shape || + // This handles diagonal ramps. Inner ramps share the same + // allowed directions as their outer counterparts. + ( + ( + tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST || + tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER + ) && + (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) + ) || + ( + ( + tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST || + tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER + ) && + (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) + ) || + ( + ( + tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST || + tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER + ) && + (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) + ) || + ( + ( + tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST || + tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER + ) && + (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) + ) + ); +} + +bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos) { + entity_t *other = ENTITIES; + do { + if(other == entity) continue; + if(other->type == ENTITY_TYPE_NULL) continue; + if(!worldPosIsEqual(other->position, newPos)) continue; + return true; + } while(++other, other < &ENTITIES[ENTITY_COUNT]); + + return false; +} + void entityWalk(entity_t *entity, const entitydir_t direction) { if(!entityCanWalk(entity)) return; // TODO: Animation, delay, etc. @@ -91,115 +208,9 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { // Get tile under foot tile_t tileCurrent = mapGetTile(entity->position); tile_t tileNew = mapGetTile(newPos); - bool_t fall = false; - bool_t raise = false; - // Are we walking up a ramp? - if( - tileShapeIsRamp(tileCurrent.shape) && - ( - // Can only walk UP the direction the ramp faces. - (direction+TILE_SHAPE_RAMP_NORTH) == tileCurrent.shape || - // If diagonal ramp, can go up one of two ways only. Inner ramps - // share the same allowed directions as their outer counterparts. - ( - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST || - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST || - tileCurrent.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST || - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST || - tileCurrent.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) - ) - ) - // Must be able to walk up. - ) - ) { - tile_t tileNewSaved = tileNew; - tileNew = TILE_NULL; - worldpos_t abovePos = newPos; - abovePos.z += 1; - tile_t tileAbove = mapGetTile(abovePos); - - if( - tileAbove.shape != TILE_SHAPE_NULL && - tileShapeIsWalkable(tileAbove.shape) - ) { - raise = true; - } else { - tileNew = tileNewSaved; - } - } else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) { - // Falling down? - worldpos_t belowPos = newPos; - belowPos.z -= 1; - tile_t tileBelow = mapGetTile(belowPos); - if( - tileBelow.shape != TILE_SHAPE_NULL && - tileShapeIsRamp(tileBelow.shape) && - ( - // This handles regular cardinal ramps - ( - entityDirGetOpposite(direction)+TILE_SHAPE_RAMP_NORTH - ) == tileBelow.shape || - // This handles diagonal ramps. Inner ramps share the same - // allowed directions as their outer counterparts. - ( - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST || - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHEAST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST || - tileBelow.shape == TILE_SHAPE_RAMP_SOUTHWEST_INNER - ) && - (direction == ENTITY_DIR_NORTH || direction == ENTITY_DIR_EAST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST || - tileBelow.shape == TILE_SHAPE_RAMP_NORTHEAST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_WEST) - ) || - ( - ( - tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST || - tileBelow.shape == TILE_SHAPE_RAMP_NORTHWEST_INNER - ) && - (direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST) - ) - ) - ) - ) { - // We will fall to this tile. - fall = true; - } - } + bool_t raise = entityWalkCheckRampUp(tileCurrent, direction, newPos, &tileNew); + bool_t fall = !raise && entityWalkCheckFall(tileNew, newPos, direction); // Can we walk here? if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked @@ -213,13 +224,7 @@ void entityWalk(entity_t *entity, const entitydir_t direction) { } // Entity in way? - entity_t *other = ENTITIES; - do { - if(other == entity) continue; - if(other->type == ENTITY_TYPE_NULL) continue; - if(!worldPosIsEqual(other->position, newPos)) continue; - return;// Blocked - } while(++other, other < &ENTITIES[ENTITY_COUNT]); + if(entityWalkIsBlockedByEntity(entity, newPos)) return;// Blocked entity->lastPosition = entity->position; entity->position = newPos; diff --git a/src/dusk/rpg/entity/entity.h b/src/dusk/rpg/entity/entity.h index dc5e75b0..b676cf7e 100644 --- a/src/dusk/rpg/entity/entity.h +++ b/src/dusk/rpg/entity/entity.h @@ -11,6 +11,7 @@ #include "interact/entityinteract.h" #include "entitytype.h" #include "npc/npc.h" +#include "rpg/overworld/tile.h" typedef struct map_s map_t; @@ -100,6 +101,48 @@ bool_t entityCanUnload(entity_t *entity); */ void entityTurn(entity_t *entity, const entitydir_t direction); +/** + * Checks whether an entity standing on a ramp tile can walk up it towards + * newPos. If so, tileNew is cleared to TILE_NULL so the caller treats the + * move as a raise rather than a normal walk onto whatever tile is there; + * if the tile above isn't walkable, tileNew is restored to its original + * value. + * + * @param tileCurrent The tile the entity is currently standing on. + * @param direction The direction the entity is moving in. + * @param newPos The world position the entity is moving to. + * @param tileNew In/out: the tile at newPos, possibly cleared to TILE_NULL. + * @returns True if the entity should be raised up onto the ramp. + */ +bool_t entityWalkCheckRampUp( + const tile_t tileCurrent, + const entitydir_t direction, + const worldpos_t newPos, + tile_t *tileNew +); + +/** + * Checks whether an entity moving onto an empty tile should fall down onto + * a ramp one z-level below that faces back towards where it came from. + * + * @param tileNew The tile at the position the entity is moving to. + * @param newPos The world position the entity is moving to. + * @param direction The direction the entity is moving in. + * @returns True if the entity should fall to the tile below newPos. + */ +bool_t entityWalkCheckFall( + const tile_t tileNew, const worldpos_t newPos, const entitydir_t direction +); + +/** + * Checks whether any other loaded entity already occupies a world position. + * + * @param entity The entity that is moving (excluded from the check). + * @param newPos The world position to check for occupancy. + * @returns True if another entity occupies newPos. + */ +bool_t entityWalkIsBlockedByEntity(entity_t *entity, const worldpos_t newPos); + /** * Make an entity walk in a direction. * diff --git a/src/dusk/rpg/overworld/map.c b/src/dusk/rpg/overworld/map.c index c3c3b56c..337b946c 100644 --- a/src/dusk/rpg/overworld/map.c +++ b/src/dusk/rpg/overworld/map.c @@ -18,8 +18,7 @@ map_t MAP; -// Clears chunk's mid-load slot, if it currently holds one. -static void mapChunkLoadingSlotClear(chunk_t *chunk) { +void mapChunkLoadingSlotClear(chunk_t *chunk) { for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) { if(MAP.loadingChunks[i] != chunk) continue; MAP.loadingChunks[i] = NULL; diff --git a/src/dusk/rpg/overworld/map.h b/src/dusk/rpg/overworld/map.h index 35e221d7..29be32f6 100644 --- a/src/dusk/rpg/overworld/map.h +++ b/src/dusk/rpg/overworld/map.h @@ -66,9 +66,16 @@ errorret_t mapDispose(); */ errorret_t mapPositionSet(const chunkpos_t newPos); +/** + * Clears chunk's mid-load slot, if it currently holds one. + * + * @param chunk The chunk whose mid-load slot to clear. + */ +void mapChunkLoadingSlotClear(chunk_t *chunk); + /** * Unloads a chunk. - * + * * @param chunk The chunk to unload. */ void mapChunkUnload(chunk_t* chunk); diff --git a/src/dusk/rpg/overworld/worldpos.c b/src/dusk/rpg/overworld/worldpos.c index 57bf5118..6112bf9a 100644 --- a/src/dusk/rpg/overworld/worldpos.c +++ b/src/dusk/rpg/overworld/worldpos.c @@ -7,6 +7,8 @@ #include "worldpos.h" #include "assert/assert.h" +#include "util/endian.h" +#include "util/memory.h" bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b) { return a.x == b.x && a.y == b.y && a.z == b.z; @@ -93,6 +95,27 @@ chunkindex_t chunkPosToIndex(const chunkpos_t* pos) { (pos->y * MAP_CHUNK_WIDTH) + pos->x ); - + return chunkIndex; +} + +worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset) { + assertNotNull(data, "Data pointer cannot be NULL"); + assertNotNull(offset, "Offset pointer cannot be NULL"); + + int16_t value; + memoryCopy(&value, data + *offset, sizeof(int16_t)); + *offset += sizeof(int16_t); + return (worldunit_t)endianLittleToHost16((uint16_t)value); +} + +worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset) { + assertNotNull(data, "Data pointer cannot be NULL"); + assertNotNull(offset, "Offset pointer cannot be NULL"); + + worldpos_t pos; + pos.x = worldUnitReadLE(data, offset); + pos.y = worldUnitReadLE(data, offset); + pos.z = worldUnitReadLE(data, offset); + return pos; } \ No newline at end of file diff --git a/src/dusk/rpg/overworld/worldpos.h b/src/dusk/rpg/overworld/worldpos.h index 32758288..b6235150 100644 --- a/src/dusk/rpg/overworld/worldpos.h +++ b/src/dusk/rpg/overworld/worldpos.h @@ -97,8 +97,31 @@ uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos); /** * Converts a chunk position to a world position. - * + * * @param worldPos The world position. * @param out The output chunk position. */ -chunkindex_t chunkPosToIndex(const chunkpos_t* pos); \ No newline at end of file +chunkindex_t chunkPosToIndex(const chunkpos_t* pos); + +/** + * Reads a little-endian worldunit_t from a potentially-unaligned offset + * into a binary asset buffer, advancing *offset past it. Shared by the + * chunk and cutscene asset loaders' binary formats, both of which encode + * world positions the same way. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded worldunit_t. + */ +worldunit_t worldUnitReadLE(const uint8_t *data, size_t *offset); + +/** + * Reads a little-endian worldpos_t (x, then y, then z) from a + * potentially-unaligned offset into a binary asset buffer, advancing + * *offset past it. + * + * @param data The buffer to read from. + * @param offset In/out cursor into data, advanced past the value read. + * @return The decoded worldpos_t. + */ +worldpos_t worldPosReadLE(const uint8_t *data, size_t *offset); \ No newline at end of file diff --git a/src/dusk/save/savedevice.c b/src/dusk/save/savedevice.c index 131ec1e0..b7e8cb6e 100644 --- a/src/dusk/save/savedevice.c +++ b/src/dusk/save/savedevice.c @@ -98,45 +98,11 @@ void saveDeviceCheckAvailability( static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'}; -#pragma pack(push, 1) -typedef struct { - char_t magic[4]; - uint32_t version; - uint32_t uncompressedSize;// size of the logical blob, pre-compression - uint32_t compressedSize;// size of the payload following this header - uint32_t checksum;// cryptCRC32() over the compressed payload - // Monotonically increasing with every store. Unused by single-file - // platforms (rename already guarantees the current file is the latest), - // but load-bearing for platforms with no rename primitive and multiple - // physical copies - e.g. GameCube memory cards ping-ponging between two - // fixed files, where this is how saveDeviceRawIsValid() picks the newest - // valid copy. - uint32_t generation; -} savedevicerawheader_t; -#pragma pack(pop) +// savedevicerawheader_t/savedevicerawspan_t/savedevicerawspans_t/ +// savedevicerawitem_t are declared in savedevice.h, since they appear in +// the signatures of the (non-static) functions below. -typedef struct { - const uint8_t *ptr; - uint32_t len; -} savedevicerawspan_t; - -typedef struct { - savedevicerawspan_t settings; - savedevicerawspan_t slots[SAVE_SLOT_COUNT]; -} savedevicerawspans_t; - -// A single settings/slot item's JSON bytes, either borrowed from an existing -// decompressed blob (owned == NULL) or freshly serialized just now via -// yyjson_mut_write (owned != NULL, must be freed with plain free()). -typedef struct { - const uint8_t *ptr; - uint32_t len; - char_t *owned; -} savedevicerawitem_t; - -// Parses the length-prefixed span table at the front of a decompressed -// logical blob. Spans point directly into `logical`, nothing is copied. -static void saveDeviceRawParseSpans( +void saveDeviceRawParseSpans( const uint8_t *logical, savedevicerawspans_t *spans ) { @@ -200,7 +166,7 @@ bool_t saveDeviceRawIsValid( // Validates the header and inflates the compressed payload that follows it. // On success, *outLogical is a memoryAllocate'd buffer the caller must free. -static errorret_t saveDeviceRawInflate( +errorret_t saveDeviceRawInflate( const uint8_t *raw, const size_t rawSize, uint8_t **outLogical, @@ -233,7 +199,7 @@ static errorret_t saveDeviceRawInflate( // Serializes `settings` to a freshly malloc'd JSON string via yyjson - the // result must be freed with plain free(), not memoryFree(). -static errorret_t saveDeviceRawSerializeSettings( +errorret_t saveDeviceRawSerializeSettings( savesettings_t *settings, char_t **outJson, size_t *outLen @@ -255,7 +221,7 @@ static errorret_t saveDeviceRawSerializeSettings( // Serializes `slot` to a freshly malloc'd JSON string via yyjson - the // result must be freed with plain free(), not memoryFree(). -static errorret_t saveDeviceRawSerializeSlot( +errorret_t saveDeviceRawSerializeSlot( saveslot_t *slot, char_t **outJson, size_t *outLen @@ -277,7 +243,7 @@ static errorret_t saveDeviceRawSerializeSlot( // Frees whatever's been collected so far - safe to call at any point since // unset items are zero-initialized (owned == NULL is a no-op skip). -static void saveDeviceRawCleanupStore( +void saveDeviceRawCleanupStore( uint8_t *oldRaw, uint8_t *oldLogical, savedevicerawitem_t *settingsItem, @@ -297,7 +263,7 @@ static void saveDeviceRawCleanupStore( // item's JSON is carried through byte-for-byte from what was already // stored (or freshly defaulted if nothing was stored yet), so a save never // reconstructs data it wasn't given. -static errorret_t saveDeviceRawStoreItem( +errorret_t saveDeviceRawStoreItem( savedevice_t *device, savesettings_t *settings, saveslot_t *slot, @@ -450,7 +416,7 @@ static errorret_t saveDeviceRawStoreItem( // Reads the combined blob and populates exactly one item - `settings`, or // the slot at `slotIndex` when `slot` is given, exactly one of the two must // be non-NULL. Leaves the destination untouched if nothing's been saved yet. -static errorret_t saveDeviceRawFetchItem( +errorret_t saveDeviceRawFetchItem( savedevice_t *device, savesettings_t *settings, saveslot_t *slot, diff --git a/src/dusk/save/savedevice.h b/src/dusk/save/savedevice.h index f153a597..f2f20e70 100644 --- a/src/dusk/save/savedevice.h +++ b/src/dusk/save/savedevice.h @@ -162,6 +162,169 @@ errorret_t saveDeviceSettingsRead( */ errorret_t saveDeviceDispose(savedevice_t *device); +#if defined(SAVE_DEVICE_DATA_RAW) + +// Mirrors the same guarded default in saveslot.h - can't #include that +// header here to get it directly, since saveslot.h itself includes this +// header (savedevice.h), and relying on include order to resolve the cycle +// correctly would be fragile. +#ifndef SAVE_SLOT_COUNT + #define SAVE_SLOT_COUNT 3 +#endif + +#pragma pack(push, 1) +typedef struct { + char_t magic[4]; + uint32_t version; + uint32_t uncompressedSize;// size of the logical blob, pre-compression + uint32_t compressedSize;// size of the payload following this header + uint32_t checksum;// cryptCRC32() over the compressed payload + // Monotonically increasing with every store. Unused by single-file + // platforms (rename already guarantees the current file is the latest), + // but load-bearing for platforms with no rename primitive and multiple + // physical copies - e.g. GameCube memory cards ping-ponging between two + // fixed files, where this is how saveDeviceRawIsValid() picks the newest + // valid copy. + uint32_t generation; +} savedevicerawheader_t; +#pragma pack(pop) + +typedef struct { + const uint8_t *ptr; + uint32_t len; +} savedevicerawspan_t; + +typedef struct { + savedevicerawspan_t settings; + savedevicerawspan_t slots[SAVE_SLOT_COUNT]; +} savedevicerawspans_t; + +// A single settings/slot item's JSON bytes, either borrowed from an existing +// decompressed blob (owned == NULL) or freshly serialized just now via +// yyjson_mut_write (owned != NULL, must be freed with plain free()). +typedef struct { + const uint8_t *ptr; + uint32_t len; + char_t *owned; +} savedevicerawitem_t; + +/** + * Parses the length-prefixed span table at the front of a decompressed + * logical blob. Spans point directly into `logical`, nothing is copied. + * + * @param logical The decompressed logical blob to parse. + * @param spans Output spans, filled in to point into `logical`. + */ +void saveDeviceRawParseSpans( + const uint8_t *logical, + savedevicerawspans_t *spans +); + +/** + * Validates the header and inflates the compressed payload that follows it. + * + * @param raw The raw framed blob (header + compressed payload). + * @param rawSize Number of bytes in raw. + * @param outLogical Receives a memoryAllocate'd buffer the caller must free. + * @param outLogicalSize Receives the size of *outLogical. + * @param outGeneration Receives the blob's generation counter, may be NULL. + * @return Error state if any. + */ +errorret_t saveDeviceRawInflate( + const uint8_t *raw, + const size_t rawSize, + uint8_t **outLogical, + size_t *outLogicalSize, + uint32_t *outGeneration +); + +/** + * Serializes `settings` to a freshly malloc'd JSON string via yyjson - the + * result must be freed with plain free(), not memoryFree(). + * + * @param settings The settings to serialize. + * @param outJson Receives the malloc'd JSON string. + * @param outLen Receives the length of *outJson. + * @return Error state if any. + */ +errorret_t saveDeviceRawSerializeSettings( + savesettings_t *settings, + char_t **outJson, + size_t *outLen +); + +/** + * Serializes `slot` to a freshly malloc'd JSON string via yyjson - the + * result must be freed with plain free(), not memoryFree(). + * + * @param slot The slot to serialize. + * @param outJson Receives the malloc'd JSON string. + * @param outLen Receives the length of *outJson. + * @return Error state if any. + */ +errorret_t saveDeviceRawSerializeSlot( + saveslot_t *slot, + char_t **outJson, + size_t *outLen +); + +/** + * Frees whatever's been collected so far - safe to call at any point since + * unset items are zero-initialized (owned == NULL is a no-op skip). + * + * @param oldRaw Previously read raw blob, or NULL. + * @param oldLogical Previously inflated logical blob, or NULL. + * @param settingsItem Settings item collected so far. + * @param slotItems Slot items collected so far (SAVE_SLOT_COUNT entries). + */ +void saveDeviceRawCleanupStore( + uint8_t *oldRaw, + uint8_t *oldLogical, + savedevicerawitem_t *settingsItem, + savedevicerawitem_t *slotItems +); + +/** + * Reads the existing combined blob (if any), replaces exactly one item - + * `settings`, or the slot at `slotIndex` when `slot` is given, exactly one + * of the two must be non-NULL - and rewrites the whole blob. Every other + * item's JSON is carried through byte-for-byte from what was already + * stored (or freshly defaulted if nothing was stored yet), so a save never + * reconstructs data it wasn't given. + * + * @param device The save device to write to. + * @param settings The settings to store, or NULL to leave settings as-is. + * @param slot The slot to store, or NULL to leave every slot as-is. + * @param slotIndex Which slot `slot` belongs to, ignored if slot is NULL. + * @return Error state if any. + */ +errorret_t saveDeviceRawStoreItem( + savedevice_t *device, + savesettings_t *settings, + saveslot_t *slot, + const uint8_t slotIndex +); + +/** + * Reads the combined blob and populates exactly one item - `settings`, or + * the slot at `slotIndex` when `slot` is given, exactly one of the two must + * be non-NULL. Leaves the destination untouched if nothing's been saved yet. + * + * @param device The save device to read from. + * @param settings The settings to populate, or NULL to skip. + * @param slot The slot to populate, or NULL to skip. + * @param slotIndex Which slot to populate `slot` from, ignored if NULL. + * @return Error state if any. + */ +errorret_t saveDeviceRawFetchItem( + savedevice_t *device, + savesettings_t *settings, + saveslot_t *slot, + const uint8_t slotIndex +); + +#endif// defined(SAVE_DEVICE_DATA_RAW) + /** * Only relevant to SAVE_DEVICE_DATA_RAW platforms that keep more than one * physical copy of the save blob (see the note above) - cheaply checks diff --git a/src/dusk/ui/overlay/uifullbox.c b/src/dusk/ui/overlay/uifullbox.c index dab307ba..d1751f0b 100644 --- a/src/dusk/ui/overlay/uifullbox.c +++ b/src/dusk/ui/overlay/uifullbox.c @@ -35,7 +35,7 @@ void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta) { } } -static color_t uiFullboxGetColor(const uifullbox_t *fullbox) { +color_t uiFullboxGetColor(const uifullbox_t *fullbox) { if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) { return fullbox->toColor; } diff --git a/src/dusk/ui/overlay/uifullbox.h b/src/dusk/ui/overlay/uifullbox.h index 46950a43..f27a1466 100644 --- a/src/dusk/ui/overlay/uifullbox.h +++ b/src/dusk/ui/overlay/uifullbox.h @@ -49,6 +49,15 @@ void uiFullboxInit(uifullbox_t *fullbox); */ void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta); +/** + * Computes the fullbox's current color, interpolating between fromColor and + * toColor based on the transition's elapsed time and easing function. + * + * @param fullbox The fullbox to compute the color for. + * @return The current interpolated color. + */ +color_t uiFullboxGetColor(const uifullbox_t *fullbox); + /** * Renders the fullbox. Skipped entirely when the current alpha is zero. * diff --git a/src/dusk/util/crypt.c b/src/dusk/util/crypt.c index cec0909b..32734e31 100644 --- a/src/dusk/util/crypt.c +++ b/src/dusk/util/crypt.c @@ -7,7 +7,7 @@ #include "util/crypt.h" -static void _cryptCRC32Step(uint32_t *crc, const uint8_t byte) { +void cryptCRC32Step(uint32_t *crc, const uint8_t byte) { *crc ^= byte; for(int j = 0; j < 8; j++) { *crc = (*crc >> 1) ^ (0xEDB88320u & -(*crc & 1u)); @@ -21,7 +21,7 @@ uint32_t cryptCRC32Begin(void) { void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size) { const uint8_t *bytes = (const uint8_t *)data; for(size_t i = 0; i < size; i++) { - _cryptCRC32Step(crc, bytes[i]); + cryptCRC32Step(crc, bytes[i]); } } diff --git a/src/dusk/util/crypt.h b/src/dusk/util/crypt.h index b9484341..971bfa7d 100644 --- a/src/dusk/util/crypt.h +++ b/src/dusk/util/crypt.h @@ -24,6 +24,14 @@ uint32_t cryptCRC32(const void *data, const size_t size); */ uint32_t cryptCRC32Begin(void); +/** + * Feeds a single byte into a running CRC32 accumulator. + * + * @param crc Pointer to the current accumulator (updated in place). + * @param byte The byte to feed in. + */ +void cryptCRC32Step(uint32_t *crc, const uint8_t byte); + /** * Feeds bytes into a running CRC32 accumulator. * diff --git a/src/duskdolphin/log/log.c b/src/duskdolphin/log/log.c index d06663d0..7cd7b7e1 100644 --- a/src/duskdolphin/log/log.c +++ b/src/duskdolphin/log/log.c @@ -6,6 +6,7 @@ */ #include "log/log.h" +#include "log/logdolphin.h" #include "display/display.h" #include #include @@ -20,7 +21,7 @@ static bool_t fatTried = false; static bool_t fatReady = false; - static void logInitFAT(void) { + void logInitFAT(void) { if(fatTried) return; fatTried = true; fatReady = fatInitDefault(); diff --git a/src/duskdolphin/log/logdolphin.h b/src/duskdolphin/log/logdolphin.h new file mode 100644 index 00000000..ba820ba4 --- /dev/null +++ b/src/duskdolphin/log/logdolphin.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#if DUSK_DOLPHIN_BUILD_TYPE == DOL + /** + * Lazily mounts the FAT filesystem (via fatInitDefault) the first time + * it's needed, so logDebug/logError can write to a file. Safe to call + * repeatedly - only the first call actually attempts the mount. + */ + void logInitFAT(void); +#endif diff --git a/src/duskdolphin/save/savedevicedolphincard.c b/src/duskdolphin/save/savedevicedolphincard.c index af6d35a2..b1fd1034 100644 --- a/src/duskdolphin/save/savedevicedolphincard.c +++ b/src/duskdolphin/save/savedevicedolphincard.c @@ -129,20 +129,7 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) { return usedBlocks < (uint32_t)blockCount; } -// One probe result for one of the two ping-pong slots. -typedef struct { - bool_t valid; - uint32_t generation; - uint8_t *buffer;// only set if keepBuffer was requested, else NULL - uint32_t len; -} savedevicedolphincardslot_t; - -// Opens the given card file (if it exists) and validates it as a raw save -// blob via saveDeviceRawIsValid(). CARD_Read requires a 32-byte aligned -// buffer, so the read (and any buffer kept for the caller) uses memoryAlign -// rather than memoryAllocate. If keepBuffer is false, or the file doesn't -// exist/validate, no buffer is left allocated. -static savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( +savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( const int32_t channel, const char_t *filename, const bool_t keepBuffer diff --git a/src/duskdolphin/save/savedevicedolphincard.h b/src/duskdolphin/save/savedevicedolphincard.h index a5a720ba..889d279a 100644 --- a/src/duskdolphin/save/savedevicedolphincard.h +++ b/src/duskdolphin/save/savedevicedolphincard.h @@ -97,6 +97,33 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel); */ errorret_t saveDeviceDolphinCardDispose(savedevice_t *device); +// One probe result for one of the two ping-pong slots. +typedef struct { + bool_t valid; + uint32_t generation; + uint8_t *buffer;// only set if keepBuffer was requested, else NULL + uint32_t len; +} savedevicedolphincardslot_t; + +/** + * Opens the given card file (if it exists) and validates it as a raw save + * blob via saveDeviceRawIsValid(). CARD_Read requires a 32-byte aligned + * buffer, so the read (and any buffer kept for the caller) uses memoryAlign + * rather than memoryAllocate. If keepBuffer is false, or the file doesn't + * exist/validate, no buffer is left allocated. + * + * @param channel The CARD slot (CARD_SLOTA or CARD_SLOTB) to probe. + * @param filename The card file name to open. + * @param keepBuffer Whether to keep the read buffer allocated on success. + * @return The probe result - valid is false if the file doesn't exist or + * doesn't validate as a raw save blob. + */ +savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot( + const int32_t channel, + const char_t *filename, + const bool_t keepBuffer +); + /** * Writes the combined save data blob out to whichever of the two fixed * card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not diff --git a/src/duskdolphin/save/savedevicedolphinnand.c b/src/duskdolphin/save/savedevicedolphinnand.c index 65f75802..52e0a42a 100644 --- a/src/duskdolphin/save/savedevicedolphinnand.c +++ b/src/duskdolphin/save/savedevicedolphinnand.c @@ -66,7 +66,7 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } -static errorret_t saveDeviceDolphinNandGetDataPath( +errorret_t saveDeviceDolphinNandGetDataPath( char_t *dest, const size_t destSize ) { diff --git a/src/duskdolphin/save/savedevicedolphinnand.h b/src/duskdolphin/save/savedevicedolphinnand.h index 74204628..aea2c572 100644 --- a/src/duskdolphin/save/savedevicedolphinnand.h +++ b/src/duskdolphin/save/savedevicedolphinnand.h @@ -62,6 +62,18 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device); */ errorret_t saveDeviceDolphinNandDispose(savedevice_t *device); +/** + * Builds the absolute path to the combined save data file. + * + * @param dest The destination buffer to write the path into. + * @param destSize The size of the destination buffer, exc. null terminator. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinNandGetDataPath( + char_t *dest, + const size_t destSize +); + /** * Writes the combined save data blob out to the NAND, replacing it * atomically (write to a temp file, then swap it in via ISFS_Rename) so a diff --git a/src/duskdolphin/save/savedevicedolphinsd.c b/src/duskdolphin/save/savedevicedolphinsd.c index 4096e0dc..a8febbd2 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.c +++ b/src/duskdolphin/save/savedevicedolphinsd.c @@ -67,7 +67,7 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } -static errorret_t saveDeviceDolphinSDGetDataPath( +errorret_t saveDeviceDolphinSDGetDataPath( char_t *dest, const size_t destSize ) { diff --git a/src/duskdolphin/save/savedevicedolphinsd.h b/src/duskdolphin/save/savedevicedolphinsd.h index 192de3fd..a684be8b 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.h +++ b/src/duskdolphin/save/savedevicedolphinsd.h @@ -57,6 +57,15 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device); */ errorret_t saveDeviceDolphinSDDispose(savedevice_t *device); +/** + * Builds the absolute path to the combined save data file. + * + * @param dest The destination buffer to write the path into. + * @param destSize The size of the destination buffer, exc. null terminator. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinSDGetDataPath(char_t *dest, const size_t destSize); + /** * Writes the combined save data blob out to the SD card, replacing it * atomically (write to a temp file, then swap it in) so a crash mid-write diff --git a/src/duskpsp/save/savedevicepsp.c b/src/duskpsp/save/savedevicepsp.c index 6bfb8589..19f94d38 100644 --- a/src/duskpsp/save/savedevicepsp.c +++ b/src/duskpsp/save/savedevicepsp.c @@ -56,7 +56,7 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) { saveDeviceFireCallback(device); } -static errorret_t saveDevicePSPGetDataPath( +errorret_t saveDevicePSPGetDataPath( char_t *dest, const size_t destSize ) { diff --git a/src/duskpsp/save/savedevicepsp.h b/src/duskpsp/save/savedevicepsp.h index 8da644a5..43717d5d 100644 --- a/src/duskpsp/save/savedevicepsp.h +++ b/src/duskpsp/save/savedevicepsp.h @@ -27,6 +27,15 @@ typedef struct savedevice_s savedevice_t; */ errorret_t saveDevicePSPInit(savedevice_t *device); +/** + * Builds the absolute path to the combined save data file. + * + * @param dest The destination buffer to write the path into. + * @param destSize The size of the destination buffer, exc. null terminator. + * @return Error state if any. + */ +errorret_t saveDevicePSPGetDataPath(char_t *dest, const size_t destSize); + /** * Writes the combined save data blob out to the memory stick, replacing it * atomically (write to a temp file, then swap it in) so a crash mid-write diff --git a/tools/asset/chunk/__main__.py b/tools/asset/chunk/__main__.py index 1aa767b1..e7d28bdc 100644 --- a/tools/asset/chunk/__main__.py +++ b/tools/asset/chunk/__main__.py @@ -90,6 +90,7 @@ Usage: python3 -m tools.asset.chunk # upgrade legacy (v1/v2) DCF in-place """ +import argparse import json import math import os @@ -160,121 +161,121 @@ DMF_VERSION = 1 # --------------------------------------------------------------------------- def tile_index(x, y): - return x + y * CHUNK_WIDTH + return x + y * CHUNK_WIDTH def find_model(filename): - """Search ASSETS_DIR/models/ recursively for filename. - Returns the path relative to ASSETS_DIR (forward slashes), or None.""" - models_root = os.path.join(ASSETS_DIR, 'models') - for dirpath, _, filenames in os.walk(models_root): - if filename in filenames: - rel = os.path.relpath( - os.path.join(dirpath, filename), ASSETS_DIR - ) - return rel.replace('\\', '/') - return None + """Search ASSETS_DIR/models/ recursively for filename. + Returns the path relative to ASSETS_DIR (forward slashes), or None.""" + models_root = os.path.join(ASSETS_DIR, 'models') + for dirpath, _, filenames in os.walk(models_root): + if filename in filenames: + rel = os.path.relpath( + os.path.join(dirpath, filename), ASSETS_DIR + ) + return rel.replace('\\', '/') + return None def write_model_json(path, mesh_rel, texture_rel, color): - """Write a model JSON descriptor file.""" - obj = {'mesh': mesh_rel, 'color': color} - if texture_rel: - obj['texture'] = texture_rel - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, 'w') as f: - json.dump(obj, f, indent=2) - print(f' Wrote model JSON {path}') + """Write a model JSON descriptor file.""" + obj = {'mesh': mesh_rel, 'color': color} + if texture_rel: + obj['texture'] = texture_rel + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w') as f: + json.dump(obj, f, indent=2) + print(f' Wrote model JSON {path}') def derive_dcf_path(json_path): - """assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf""" - base = os.path.splitext(os.path.basename(json_path))[0] - if base.startswith('chunk_'): - base = base[len('chunk_'):] - rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR) - return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf') + """assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf""" + base = os.path.splitext(os.path.basename(json_path))[0] + if base.startswith('chunk_'): + base = base[len('chunk_'):] + rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR) + return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf') def write_dmf(path, vertex_bytes): - vert_count = len(vertex_bytes) // VERTEX_SIZE - buf = bytearray() - buf += DMF_MAGIC - buf += struct.pack(' CHUNK_ENTITY_SPAWN_COUNT_MAX: - raise ValueError( - f"Too many entity spawns ({len(entity_spawns)}) - max " - f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}" - ) - if len(area_spawns) > CHUNK_AREA_COUNT_MAX: - raise ValueError( - f"Too many area spawns ({len(area_spawns)}) - max " - f"{CHUNK_AREA_COUNT_MAX}" - ) - - buf = bytearray() - buf += FILE_MAGIC - buf += b'\x00' - buf += struct.pack('= CHUNK_MESH_NAME_MAX: - raise ValueError( - f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}" - ) - buf += encoded + b'\x00' - buf += struct.pack('<3f', offset[0], offset[1], offset[2]) - - buf += struct.pack(' CHUNK_ENTITY_SPAWN_COUNT_MAX: + raise ValueError( + f"Too many entity spawns ({len(entity_spawns)}) - max " + f"{CHUNK_ENTITY_SPAWN_COUNT_MAX}" ) + if len(area_spawns) > CHUNK_AREA_COUNT_MAX: + raise ValueError( + f"Too many area spawns ({len(area_spawns)}) - max " + f"{CHUNK_AREA_COUNT_MAX}" + ) + + buf = bytearray() + buf += FILE_MAGIC + buf += b'\x00' + buf += struct.pack('= CHUNK_MESH_NAME_MAX: + raise ValueError( + f"Mesh name too long (>= {CHUNK_MESH_NAME_MAX}): {name}" + ) + buf += encoded + b'\x00' + buf += struct.pack('<3f', offset[0], offset[1], offset[2]) + + buf += struct.pack(' look up matching model JSON - for mesh in data.get('meshes', []): - filename = mesh['file'] - pos = mesh.get('pos', [0, 0, 0]) - model_filename = os.path.splitext(filename)[0] + '.json' - rel = find_model(model_filename) - if rel is None: - raise ValueError( - f"Model not found under assets/models/: {model_filename}" - ) - model_names.append(rel) - mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2]))) - print(f' Resolved {filename} -> {rel}') - - entity_spawns = [] - for spawn in data.get('entities', []): - pos = tuple(int(v) for v in spawn['pos']) - if spawn['type'] == 'global': - entity_spawns.append({ - 'kind': ENTITY_SPAWN_KIND_GLOBAL, - 'globalId': int(spawn['globalId']), - 'pos': pos, - }) - elif spawn['type'] == 'item': - entity_spawns.append({ - 'kind': ENTITY_SPAWN_KIND_ITEM, - 'itemId': int(spawn['itemId']), - 'quantity': int(spawn['quantity']), - 'pos': pos, - }) - else: - raise ValueError(f"Unknown entity spawn type: {spawn['type']}") - - area_spawns = [] - for area in data.get('areas', []): - area_spawns.append({ - 'min': tuple(int(v) for v in area['min']), - 'max': tuple(int(v) for v in area['max']), - 'callbackId': int(area['callbackId']), - 'notify': int(area['notify']), - 'trigger': int(area['trigger']), - }) - - write_dcf( - dcf_path, bytes(tiles), model_names, mesh_offsets, - entity_spawns, area_spawns + tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE) + column_z = {} + for tile in data.get('tiles', []): + pos = tile['pos'] + x, y, z = int(pos[0]), int(pos[1]), int(pos[2]) + if (x, y) in column_z and column_z[(x, y)] != z: + print( + f' WARNING: {json_path}: column ({x}, {y}) has tiles at ' + f'z={column_z[(x, y)]} and z={z} - only one tile per ' + f'column is kept, z={z} wins (last one in the array)' + ) + column_z[(x, y)] = z + struct.pack_into( + TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE, + int(tile['type']), z ) + terrain_verts = build_terrain_verts(tiles) + model_names = [] + mesh_offsets = [] + + dcf_base = os.path.splitext(os.path.basename(dcf_path))[0] + + # Terrain mesh + model (only written if non-empty) + terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') + models_dir = os.path.join(ASSETS_DIR, 'models', 'chunks') + os.makedirs(terrain_dir, exist_ok=True) + os.makedirs(models_dir, exist_ok=True) + if terrain_verts: + dmf_name = f'chunk_{dcf_base}_0.dmf' + write_dmf(os.path.join(terrain_dir, dmf_name), terrain_verts) + mesh_rel = f'meshes/chunks/{dmf_name}' + json_name = f'chunk_{dcf_base}_0.json' + write_model_json( + os.path.join(models_dir, json_name), + mesh_rel, + 'tiles.png', + [255, 255, 255, 255] + ) + model_names.append(f'models/chunks/{json_name}') + mesh_offsets.append((0.0, 0.0, 0.0)) + + # External mesh references -> look up matching model JSON + for mesh in data.get('meshes', []): + filename = mesh['file'] + pos = mesh.get('pos', [0, 0, 0]) + model_filename = os.path.splitext(filename)[0] + '.json' + rel = find_model(model_filename) + if rel is None: + raise ValueError( + f"Model not found under assets/models/: {model_filename}" + ) + model_names.append(rel) + mesh_offsets.append((float(pos[0]), float(pos[1]), float(pos[2]))) + print(f' Resolved {filename} -> {rel}') + + entity_spawns = [] + for spawn in data.get('entities', []): + pos = tuple(int(v) for v in spawn['pos']) + if spawn['type'] == 'global': + entity_spawns.append({ + 'kind': ENTITY_SPAWN_KIND_GLOBAL, + 'globalId': int(spawn['globalId']), + 'pos': pos, + }) + elif spawn['type'] == 'item': + entity_spawns.append({ + 'kind': ENTITY_SPAWN_KIND_ITEM, + 'itemId': int(spawn['itemId']), + 'quantity': int(spawn['quantity']), + 'pos': pos, + }) + else: + raise ValueError(f"Unknown entity spawn type: {spawn['type']}") + + area_spawns = [] + for area in data.get('areas', []): + area_spawns.append({ + 'min': tuple(int(v) for v in area['min']), + 'max': tuple(int(v) for v in area['max']), + 'callbackId': int(area['callbackId']), + 'notify': int(area['notify']), + 'trigger': int(area['trigger']), + }) + + write_dcf( + dcf_path, bytes(tiles), model_names, mesh_offsets, + entity_spawns, area_spawns + ) + def process_json(json_path): - dcf_path = derive_dcf_path(json_path) - os.makedirs(os.path.dirname(dcf_path), exist_ok=True) - print(f"{json_path} -> {dcf_path}") - from_json(json_path, dcf_path) + dcf_path = derive_dcf_path(json_path) + os.makedirs(os.path.dirname(dcf_path), exist_ok=True) + print(f"{json_path} -> {dcf_path}") + from_json(json_path, dcf_path) # --------------------------------------------------------------------------- @@ -456,90 +457,90 @@ def process_json(json_path): # --------------------------------------------------------------------------- def collapse_legacy_tiles(legacy_tiles, path): - """Collapse the old one-tile-per-(x,y,z) grid into the current - one-tile-per-(x,y) format. If a column has more than one non-null tile - across its Z layers, the highest Z wins and a warning is printed.""" - tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE) - for y in range(CHUNK_HEIGHT): - for x in range(CHUNK_WIDTH): - found = [] - for z in range(_LEGACY_CHUNK_DEPTH): - legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT - shape = struct.unpack_from( - ' 1: - print( - f' WARNING: {path}: column ({x}, {y}) has tiles at ' - f'z={[z for z, _ in found]} - only one tile per column ' - f'is kept, z={found[-1][0]} wins (highest Z)' - ) - z, shape = found[-1] - struct.pack_into( - TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE, - shape, z - ) - return bytes(tiles) + """Collapse the old one-tile-per-(x,y,z) grid into the current + one-tile-per-(x,y) format. If a column has more than one non-null tile + across its Z layers, the highest Z wins and a warning is printed.""" + tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE) + for y in range(CHUNK_HEIGHT): + for x in range(CHUNK_WIDTH): + found = [] + for z in range(_LEGACY_CHUNK_DEPTH): + legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT + shape = struct.unpack_from( + ' 1: + print( + f' WARNING: {path}: column ({x}, {y}) has tiles at ' + f'z={[z for z, _ in found]} - only one tile per column ' + f'is kept, z={found[-1][0]} wins (highest Z)' + ) + z, shape = found[-1] + struct.pack_into( + TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE, + shape, z + ) + return bytes(tiles) def read_legacy_dcf(path): - with open(path, 'rb') as f: - data = f.read() - if data[:3] != FILE_MAGIC: - raise ValueError(f"{path}: not a DCF file") - version = struct.unpack_from(' 0: - meshes.append(verts) - else: - mesh_count = data[offset] - offset += 1 - for _ in range(mesh_count): - vert_count = struct.unpack_from(' 0: - meshes.append(verts) - return tiles, meshes + meshes = [] + if version == 1: + vert_count = struct.unpack_from(' 0: + meshes.append(verts) + else: + mesh_count = data[offset] + offset += 1 + for _ in range(mesh_count): + vert_count = struct.unpack_from(' 0: + meshes.append(verts) + return tiles, meshes def upgrade_dcf(path): - print(f"Upgrading legacy DCF {path} ...") - tiles, meshes = read_legacy_dcf(path) - print( - f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, " - f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}" - ) - base = os.path.splitext(os.path.basename(path))[0] - terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') - os.makedirs(terrain_dir, exist_ok=True) - mesh_names = [] - for idx, verts in enumerate(meshes): - dmf_name = f'chunk_{base}_{idx}.dmf' - write_dmf(os.path.join(terrain_dir, dmf_name), verts) - mesh_names.append(f'meshes/chunks/{dmf_name}') - write_dcf(path, tiles, mesh_names) + print(f"Upgrading legacy DCF {path} ...") + tiles, meshes = read_legacy_dcf(path) + print( + f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, " + f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}" + ) + base = os.path.splitext(os.path.basename(path))[0] + terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks') + os.makedirs(terrain_dir, exist_ok=True) + mesh_names = [] + for idx, verts in enumerate(meshes): + dmf_name = f'chunk_{base}_{idx}.dmf' + write_dmf(os.path.join(terrain_dir, dmf_name), verts) + mesh_names.append(f'meshes/chunks/{dmf_name}') + write_dcf(path, tiles, mesh_names) # --------------------------------------------------------------------------- @@ -547,31 +548,39 @@ def upgrade_dcf(path): # --------------------------------------------------------------------------- def main(): - args = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Generate DCF + companion DMF files from raw chunk JSON, " + "or upgrade legacy DCF files to the current version" + ) + parser.add_argument( + 'path', nargs='?', + help='Chunk JSON file to process, or legacy DCF file to upgrade. ' + 'If omitted, processes all assetsraw/chunks/*.json' + ) + args = parser.parse_args() - if not args: - chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks') - if not os.path.isdir(chunks_dir): - print(f"No directory found: {chunks_dir}") - sys.exit(1) - json_files = sorted( - os.path.join(chunks_dir, f) - for f in os.listdir(chunks_dir) - if f.endswith('.json') - ) - if not json_files: - print(f"No JSON files found in {chunks_dir}") - sys.exit(0) - for p in json_files: - process_json(p) - return + if args.path is None: + chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks') + if not os.path.isdir(chunks_dir): + print(f"No directory found: {chunks_dir}") + sys.exit(1) + json_files = sorted( + os.path.join(chunks_dir, f) + for f in os.listdir(chunks_dir) + if f.endswith('.json') + ) + if not json_files: + print(f"No JSON files found in {chunks_dir}") + sys.exit(0) + for p in json_files: + process_json(p) + return - src = args[0] - if os.path.splitext(src)[1].lower() == '.json': - process_json(src) - else: - upgrade_dcf(src) + if os.path.splitext(args.path)[1].lower() == '.json': + process_json(args.path) + else: + upgrade_dcf(args.path) if __name__ == '__main__': - main() + main() diff --git a/tools/asset/cutscene/__main__.py b/tools/asset/cutscene/__main__.py index db4545aa..06a57f32 100644 --- a/tools/asset/cutscene/__main__.py +++ b/tools/asset/cutscene/__main__.py @@ -46,6 +46,7 @@ DCTS format (little-endian throughout): item stream. Every pool entry starts 4-byte aligned. """ +import argparse import json import os import struct @@ -428,9 +429,18 @@ def process_json(json_path): def main(): - args = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Generate DCTS binary cutscene files from raw JSONC " + "cutscene definitions" + ) + parser.add_argument( + 'paths', nargs='*', + help='JSONC cutscene file(s) to process. If omitted, processes all ' + 'assetsraw/cutscenes/*.jsonc' + ) + args = parser.parse_args() - if not args: + if not args.paths: cutscenes_dir = os.path.join(ASSETSRAW_DIR, 'cutscenes') if not os.path.isdir(cutscenes_dir): print(f"No directory found: {cutscenes_dir}") @@ -447,7 +457,7 @@ def main(): process_json(p) return - for p in args: + for p in args.paths: process_json(p) diff --git a/tools/asset/dmf/__main__.py b/tools/asset/dmf/__main__.py index 97604d9c..e761e9e1 100644 --- a/tools/asset/dmf/__main__.py +++ b/tools/asset/dmf/__main__.py @@ -18,8 +18,8 @@ Usage: Reads raw vertex bytes from vertices.bin and writes a DMF file. """ +import argparse import struct -import sys import os MAGIC = b'DMF\x00' @@ -28,42 +28,36 @@ VERTEX_SIZE = 20 def write_dmf(path, vertex_bytes): - if len(vertex_bytes) % VERTEX_SIZE != 0: - raise ValueError( - f"Vertex data size {len(vertex_bytes)} is not a " - f"multiple of {VERTEX_SIZE}" - ) - vert_count = len(vertex_bytes) // VERTEX_SIZE - buf = bytearray() - buf += MAGIC - buf += struct.pack(' " - ) - sys.exit(1) + parser = argparse.ArgumentParser(description="Write a DMF (Dusk Mesh Format) file") + parser.add_argument('output', help='Path to output .dmf file') + parser.add_argument('vertices', help='Path to raw vertex bytes file') + args = parser.parse_args() - dst = args[0] - src = args[1] + with open(args.vertices, 'rb') as f: + vertex_bytes = f.read() - with open(src, 'rb') as f: - vertex_bytes = f.read() - - write_dmf(dst, vertex_bytes) + write_dmf(args.output, vertex_bytes) if __name__ == '__main__': - main() + main()