Fixed a bunch of code inconsistencies

This commit is contained in:
2026-08-31 07:02:19 -05:00
parent 320c5e6ce5
commit fcf0de72af
30 changed files with 1033 additions and 619 deletions
+3 -3
View File
@@ -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;
+57
View File
@@ -150,6 +150,63 @@ 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.
+4 -23
View File
@@ -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));
@@ -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:
@@ -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
+65 -60
View File
@@ -74,35 +74,19 @@ void entityTurn(entity_t *entity, const entitydir_t direction) {
entity->animTime = ENTITY_ANIM_TURN_DURATION;
}
void entityWalk(entity_t *entity, const entitydir_t direction) {
if(!entityCanWalk(entity)) return;
// TODO: Animation, delay, etc.
entity->direction = direction;
bool_t entityWalkCheckRampUp(
const tile_t tileCurrent,
const entitydir_t direction,
const worldpos_t newPos,
tile_t *tileNew
) {
if(!tileShapeIsRamp(tileCurrent.shape)) return false;
// Where are we moving?
worldpos_t newPos = entity->position;
worldunits_t relX, relY;
{
entityDirGetRelative(direction, &relX, &relY);
newPos.x += relX;
newPos.y += relY;
}
// 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) &&
(
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 ||
@@ -130,41 +114,40 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
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;
);
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)
) {
raise = true;
} else {
tileNew = tileNewSaved;
if(tileAbove.shape != TILE_SHAPE_NULL && tileShapeIsWalkable(tileAbove.shape)) {
return true;
}
} else if(tileNew.shape == TILE_SHAPE_NULL && newPos.z > 0) {
// Falling down?
*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) &&
(
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 ||
(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 ||
@@ -193,14 +176,42 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
) &&
(direction == ENTITY_DIR_SOUTH || direction == ENTITY_DIR_EAST)
)
)
)
) {
// We will fall to this tile.
fall = true;
);
}
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.
entity->direction = direction;
// Where are we moving?
worldpos_t newPos = entity->position;
worldunits_t relX, relY;
{
entityDirGetRelative(direction, &relX, &relY);
newPos.x += relX;
newPos.y += relY;
}
// Get tile under foot
tile_t tileCurrent = mapGetTile(entity->position);
tile_t tileNew = mapGetTile(newPos);
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;
+43
View File
@@ -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.
*
+1 -2
View File
@@ -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;
+7
View File
@@ -66,6 +66,13 @@ 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.
*
+23
View File
@@ -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;
@@ -96,3 +98,24 @@ chunkindex_t chunkPosToIndex(const chunkpos_t* pos) {
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;
}
+23
View File
@@ -102,3 +102,26 @@ uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos);
* @param out The output chunk position.
*/
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);
+10 -44
View File
@@ -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,
+163
View File
@@ -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
+1 -1
View File
@@ -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;
}
+9
View File
@@ -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.
*
+2 -2
View File
@@ -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]);
}
}
+8
View File
@@ -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.
*
+2 -1
View File
@@ -6,6 +6,7 @@
*/
#include "log/log.h"
#include "log/logdolphin.h"
#include "display/display.h"
#include <stdio.h>
#include <stdlib.h>
@@ -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();
+18
View File
@@ -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
+1 -14
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
) {
@@ -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
+1 -1
View File
@@ -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
) {
@@ -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
+1 -1
View File
@@ -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
) {
+9
View File
@@ -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
+15 -6
View File
@@ -90,6 +90,7 @@ Usage:
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
"""
import argparse
import json
import math
import os
@@ -547,9 +548,18 @@ 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:
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}")
@@ -566,11 +576,10 @@ def main():
process_json(p)
return
src = args[0]
if os.path.splitext(src)[1].lower() == '.json':
process_json(src)
if os.path.splitext(args.path)[1].lower() == '.json':
process_json(args.path)
else:
upgrade_dcf(src)
upgrade_dcf(args.path)
if __name__ == '__main__':
+13 -3
View File
@@ -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)
+7 -13
View File
@@ -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'
@@ -48,21 +48,15 @@ def write_dmf(path, vertex_bytes):
def main():
args = sys.argv[1:]
if len(args) != 2:
print(
"Usage: python3 -m tools.asset.dmf "
"<output.dmf> <vertices.bin>"
)
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(src, 'rb') as f:
with open(args.vertices, 'rb') as f:
vertex_bytes = f.read()
write_dmf(dst, vertex_bytes)
write_dmf(args.output, vertex_bytes)
if __name__ == '__main__':