Fixed a bunch of code inconsistencies
This commit is contained in:
@@ -218,7 +218,7 @@ const uint8_t *assetFileLineReaderUnreadPtr(
|
|||||||
return reader->readBuffer + reader->bufferStart;
|
return reader->readBuffer + reader->bufferStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorret_t assetFileLineReaderAppend(
|
errorret_t assetFileLineReaderAppend(
|
||||||
assetfilelinereader_t *reader,
|
assetfilelinereader_t *reader,
|
||||||
const uint8_t *src,
|
const uint8_t *src,
|
||||||
size_t srcLength
|
size_t srcLength
|
||||||
@@ -240,7 +240,7 @@ static errorret_t assetFileLineReaderAppend(
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
|
void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
|
||||||
assertNotNull(reader, "Reader cannot be NULL.");
|
assertNotNull(reader, "Reader cannot be NULL.");
|
||||||
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
|
assertNotNull(reader->outBuffer, "Out buffer cannot be NULL.");
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@@ -250,7 +250,7 @@ static void assetFileLineReaderTerminate(assetfilelinereader_t *reader) {
|
|||||||
reader->outBuffer[reader->lineLength] = '\0';
|
reader->outBuffer[reader->lineLength] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
static ssize_t assetFileLineReaderFindNewline(
|
ssize_t assetFileLineReaderFindNewline(
|
||||||
const assetfilelinereader_t *reader
|
const assetfilelinereader_t *reader
|
||||||
) {
|
) {
|
||||||
size_t i;
|
size_t i;
|
||||||
|
|||||||
@@ -150,10 +150,67 @@ void assetFileLineReaderInit(
|
|||||||
const size_t outBufferSize
|
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
|
* 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.
|
* buffer is null-terminated and does not include the newline character.
|
||||||
*
|
*
|
||||||
* @param reader The line reader to read from.
|
* @param reader The line reader to read from.
|
||||||
* @return An error code if a failure occurs, or errorOk() if a line was read
|
* @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
|
* successfully. If the end of the file is reached, errorEndOfFile() is
|
||||||
|
|||||||
@@ -13,26 +13,7 @@
|
|||||||
#include "asset/loader/assetentry.h"
|
#include "asset/loader/assetentry.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "rpg/overworld/worldpos.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
errorret_t assetChunkLoaderAsync(assetloading_t *loading) {
|
||||||
assertNotNull(loading, "Loading cannot be NULL");
|
assertNotNull(loading, "Loading cannot be NULL");
|
||||||
@@ -196,7 +177,7 @@ errorret_t assetChunkLoaderSync(assetloading_t *loading) {
|
|||||||
spawn->itemQuantity = 0;
|
spawn->itemQuantity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn->position = assetChunkReadWorldPos(data, &offset);
|
spawn->position = worldPosReadLE(data, &offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
out->areaSpawnCount = 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++) {
|
for(uint8_t s = 0; s < out->areaSpawnCount; s++) {
|
||||||
chunkareaspawn_t *area = &out->areaSpawns[s];
|
chunkareaspawn_t *area = &out->areaSpawns[s];
|
||||||
area->min = assetChunkReadWorldPos(data, &offset);
|
area->min = worldPosReadLE(data, &offset);
|
||||||
area->max = assetChunkReadWorldPos(data, &offset);
|
area->max = worldPosReadLE(data, &offset);
|
||||||
|
|
||||||
uint16_t callbackId;
|
uint16_t callbackId;
|
||||||
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
memoryCopy(&callbackId, data + offset, sizeof(uint16_t));
|
||||||
|
|||||||
@@ -13,62 +13,44 @@
|
|||||||
#include "asset/loader/assetentry.h"
|
#include "asset/loader/assetentry.h"
|
||||||
#include "asset/loader/assetloader.h"
|
#include "asset/loader/assetloader.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
|
#include "rpg/overworld/worldpos.h"
|
||||||
|
|
||||||
// DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1),
|
// DCTS header: magic "DCTS" (4), version u32 LE (4), pauseType u8 (1),
|
||||||
// itemCount u8 (1), poolSize u16 LE (2) = 12 bytes.
|
// itemCount u8 (1), poolSize u16 LE (2) = 12 bytes.
|
||||||
#define ASSET_CUTSCENE_HEADER_SIZE 12
|
#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];
|
uint8_t value = data[*offset];
|
||||||
*offset += sizeof(uint8_t);
|
*offset += sizeof(uint8_t);
|
||||||
return value;
|
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;
|
uint16_t value;
|
||||||
memoryCopy(&value, data + *offset, sizeof(uint16_t));
|
memoryCopy(&value, data + *offset, sizeof(uint16_t));
|
||||||
*offset += sizeof(uint16_t);
|
*offset += sizeof(uint16_t);
|
||||||
return endianLittleToHost16(value);
|
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;
|
uint32_t value;
|
||||||
memoryCopy(&value, data + *offset, sizeof(uint32_t));
|
memoryCopy(&value, data + *offset, sizeof(uint32_t));
|
||||||
*offset += sizeof(uint32_t);
|
*offset += sizeof(uint32_t);
|
||||||
return endianLittleToHost32(value);
|
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;
|
float_t value;
|
||||||
memoryCopy(&value, data + *offset, sizeof(float_t));
|
memoryCopy(&value, data + *offset, sizeof(float_t));
|
||||||
*offset += sizeof(float_t);
|
*offset += sizeof(float_t);
|
||||||
return endianLittleToHostFloat(value);
|
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
|
// Copies a length-prefixed string directly into an item's own embedded
|
||||||
// char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
|
// char_t[destCapacity] field (CUTSCENE_TEXT_MAX_CHARS and friends) - these
|
||||||
// are never pool references, see the item-field inventory in the runtime
|
// are never pool references, see the item-field inventory in the runtime
|
||||||
// cutscene file design.
|
// cutscene file design.
|
||||||
static void assetCutsceneReadEmbeddedString(
|
void assetCutsceneReadEmbeddedString(
|
||||||
const uint8_t *data,
|
const uint8_t *data,
|
||||||
size_t *offset,
|
size_t *offset,
|
||||||
char_t *dest,
|
char_t *dest,
|
||||||
@@ -83,7 +65,7 @@ static void assetCutsceneReadEmbeddedString(
|
|||||||
|
|
||||||
// Resolves a u16 pool offset (read from the item stream) to a real pointer
|
// Resolves a u16 pool offset (read from the item stream) to a real pointer
|
||||||
// into the entry's own persistent pool allocation.
|
// into the entry's own persistent pool allocation.
|
||||||
static const char_t * assetCutsceneReadPoolString(
|
const char_t * assetCutsceneReadPoolString(
|
||||||
const uint8_t *data,
|
const uint8_t *data,
|
||||||
size_t *offset,
|
size_t *offset,
|
||||||
const char_t *pool
|
const char_t *pool
|
||||||
@@ -205,7 +187,7 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
|
case CUTSCENE_ITEM_TYPE_ENTITY_TELEPORT:
|
||||||
item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset);
|
item->entityTeleport.entityIndex = assetCutsceneReadU8(data, &offset);
|
||||||
item->entityTeleport.target = assetCutsceneReadWorldPos(data, &offset);
|
item->entityTeleport.target = worldPosReadLE(data, &offset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
|
case CUTSCENE_ITEM_TYPE_ENTITY_WALK_TO: {
|
||||||
@@ -246,7 +228,7 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
|||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
|
case CUTSCENE_ITEM_TYPE_ENTITY_ADD:
|
||||||
item->entityAdd.entityType = assetCutsceneReadU8(data, &offset);
|
item->entityAdd.entityType = assetCutsceneReadU8(data, &offset);
|
||||||
item->entityAdd.position = assetCutsceneReadWorldPos(data, &offset);
|
item->entityAdd.position = worldPosReadLE(data, &offset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
|
case CUTSCENE_ITEM_TYPE_ENTITY_TURN:
|
||||||
@@ -261,9 +243,9 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
|||||||
item->entityWalkToEntity.targetEntityIndex =
|
item->entityWalkToEntity.targetEntityIndex =
|
||||||
assetCutsceneReadU8(data, &offset);
|
assetCutsceneReadU8(data, &offset);
|
||||||
item->entityWalkToEntity.offsetX =
|
item->entityWalkToEntity.offsetX =
|
||||||
assetCutsceneReadWorldUnit(data, &offset);
|
worldUnitReadLE(data, &offset);
|
||||||
item->entityWalkToEntity.offsetY =
|
item->entityWalkToEntity.offsetY =
|
||||||
assetCutsceneReadWorldUnit(data, &offset);
|
worldUnitReadLE(data, &offset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
|
case CUTSCENE_ITEM_TYPE_MAP_AREA_REMOVE:
|
||||||
|
|||||||
@@ -42,6 +42,78 @@ typedef struct {
|
|||||||
char_t *pool;
|
char_t *pool;
|
||||||
} assetcutsceneoutput_t;
|
} 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
|
* Asynchronous loader for cutscene assets. Reads the raw DCTS file bytes
|
||||||
* into the loading buffer so the sync phase can parse without blocking the
|
* into the loading buffer so the sync phase can parse without blocking the
|
||||||
|
|||||||
+120
-115
@@ -74,6 +74,123 @@ void entityTurn(entity_t *entity, const entitydir_t direction) {
|
|||||||
entity->animTime = ENTITY_ANIM_TURN_DURATION;
|
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) {
|
void entityWalk(entity_t *entity, const entitydir_t direction) {
|
||||||
if(!entityCanWalk(entity)) return;
|
if(!entityCanWalk(entity)) return;
|
||||||
// TODO: Animation, delay, etc.
|
// TODO: Animation, delay, etc.
|
||||||
@@ -91,115 +208,9 @@ void entityWalk(entity_t *entity, const entitydir_t direction) {
|
|||||||
// Get tile under foot
|
// Get tile under foot
|
||||||
tile_t tileCurrent = mapGetTile(entity->position);
|
tile_t tileCurrent = mapGetTile(entity->position);
|
||||||
tile_t tileNew = mapGetTile(newPos);
|
tile_t tileNew = mapGetTile(newPos);
|
||||||
bool_t fall = false;
|
|
||||||
bool_t raise = false;
|
|
||||||
|
|
||||||
// Are we walking up a ramp?
|
bool_t raise = entityWalkCheckRampUp(tileCurrent, direction, newPos, &tileNew);
|
||||||
if(
|
bool_t fall = !raise && entityWalkCheckFall(tileNew, newPos, direction);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can we walk here?
|
// Can we walk here?
|
||||||
if(!raise && !fall && !tileShapeIsWalkable(tileNew.shape)) return;// Blocked
|
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 in way?
|
||||||
entity_t *other = ENTITIES;
|
if(entityWalkIsBlockedByEntity(entity, newPos)) return;// Blocked
|
||||||
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]);
|
|
||||||
|
|
||||||
entity->lastPosition = entity->position;
|
entity->lastPosition = entity->position;
|
||||||
entity->position = newPos;
|
entity->position = newPos;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "interact/entityinteract.h"
|
#include "interact/entityinteract.h"
|
||||||
#include "entitytype.h"
|
#include "entitytype.h"
|
||||||
#include "npc/npc.h"
|
#include "npc/npc.h"
|
||||||
|
#include "rpg/overworld/tile.h"
|
||||||
|
|
||||||
typedef struct map_s map_t;
|
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);
|
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.
|
* Make an entity walk in a direction.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -18,8 +18,7 @@
|
|||||||
|
|
||||||
map_t MAP;
|
map_t MAP;
|
||||||
|
|
||||||
// Clears chunk's mid-load slot, if it currently holds one.
|
void mapChunkLoadingSlotClear(chunk_t *chunk) {
|
||||||
static void mapChunkLoadingSlotClear(chunk_t *chunk) {
|
|
||||||
for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
|
for(uint32_t i = 0; i < MAP_CHUNK_LOAD_CONCURRENCY; i++) {
|
||||||
if(MAP.loadingChunks[i] != chunk) continue;
|
if(MAP.loadingChunks[i] != chunk) continue;
|
||||||
MAP.loadingChunks[i] = NULL;
|
MAP.loadingChunks[i] = NULL;
|
||||||
|
|||||||
@@ -66,9 +66,16 @@ errorret_t mapDispose();
|
|||||||
*/
|
*/
|
||||||
errorret_t mapPositionSet(const chunkpos_t newPos);
|
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.
|
* Unloads a chunk.
|
||||||
*
|
*
|
||||||
* @param chunk The chunk to unload.
|
* @param chunk The chunk to unload.
|
||||||
*/
|
*/
|
||||||
void mapChunkUnload(chunk_t* chunk);
|
void mapChunkUnload(chunk_t* chunk);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
#include "worldpos.h"
|
#include "worldpos.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/endian.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b) {
|
bool_t worldPosIsEqual(const worldpos_t a, const worldpos_t b) {
|
||||||
return a.x == b.x && a.y == b.y && a.z == b.z;
|
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->y * MAP_CHUNK_WIDTH) +
|
||||||
pos->x
|
pos->x
|
||||||
);
|
);
|
||||||
|
|
||||||
return chunkIndex;
|
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;
|
||||||
}
|
}
|
||||||
@@ -97,8 +97,31 @@ uint8_t worldPosToChunkLocalZ(const worldpos_t* worldPos);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a chunk position to a world position.
|
* Converts a chunk position to a world position.
|
||||||
*
|
*
|
||||||
* @param worldPos The world position.
|
* @param worldPos The world position.
|
||||||
* @param out The output chunk position.
|
* @param out The output chunk position.
|
||||||
*/
|
*/
|
||||||
chunkindex_t chunkPosToIndex(const chunkpos_t* pos);
|
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
@@ -98,45 +98,11 @@ void saveDeviceCheckAvailability(
|
|||||||
|
|
||||||
static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'};
|
static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'};
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
// savedevicerawheader_t/savedevicerawspan_t/savedevicerawspans_t/
|
||||||
typedef struct {
|
// savedevicerawitem_t are declared in savedevice.h, since they appear in
|
||||||
char_t magic[4];
|
// the signatures of the (non-static) functions below.
|
||||||
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 {
|
void saveDeviceRawParseSpans(
|
||||||
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(
|
|
||||||
const uint8_t *logical,
|
const uint8_t *logical,
|
||||||
savedevicerawspans_t *spans
|
savedevicerawspans_t *spans
|
||||||
) {
|
) {
|
||||||
@@ -200,7 +166,7 @@ bool_t saveDeviceRawIsValid(
|
|||||||
|
|
||||||
// Validates the header and inflates the compressed payload that follows it.
|
// Validates the header and inflates the compressed payload that follows it.
|
||||||
// On success, *outLogical is a memoryAllocate'd buffer the caller must free.
|
// On success, *outLogical is a memoryAllocate'd buffer the caller must free.
|
||||||
static errorret_t saveDeviceRawInflate(
|
errorret_t saveDeviceRawInflate(
|
||||||
const uint8_t *raw,
|
const uint8_t *raw,
|
||||||
const size_t rawSize,
|
const size_t rawSize,
|
||||||
uint8_t **outLogical,
|
uint8_t **outLogical,
|
||||||
@@ -233,7 +199,7 @@ static errorret_t saveDeviceRawInflate(
|
|||||||
|
|
||||||
// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the
|
// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the
|
||||||
// result must be freed with plain free(), not memoryFree().
|
// result must be freed with plain free(), not memoryFree().
|
||||||
static errorret_t saveDeviceRawSerializeSettings(
|
errorret_t saveDeviceRawSerializeSettings(
|
||||||
savesettings_t *settings,
|
savesettings_t *settings,
|
||||||
char_t **outJson,
|
char_t **outJson,
|
||||||
size_t *outLen
|
size_t *outLen
|
||||||
@@ -255,7 +221,7 @@ static errorret_t saveDeviceRawSerializeSettings(
|
|||||||
|
|
||||||
// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the
|
// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the
|
||||||
// result must be freed with plain free(), not memoryFree().
|
// result must be freed with plain free(), not memoryFree().
|
||||||
static errorret_t saveDeviceRawSerializeSlot(
|
errorret_t saveDeviceRawSerializeSlot(
|
||||||
saveslot_t *slot,
|
saveslot_t *slot,
|
||||||
char_t **outJson,
|
char_t **outJson,
|
||||||
size_t *outLen
|
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
|
// 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).
|
// unset items are zero-initialized (owned == NULL is a no-op skip).
|
||||||
static void saveDeviceRawCleanupStore(
|
void saveDeviceRawCleanupStore(
|
||||||
uint8_t *oldRaw,
|
uint8_t *oldRaw,
|
||||||
uint8_t *oldLogical,
|
uint8_t *oldLogical,
|
||||||
savedevicerawitem_t *settingsItem,
|
savedevicerawitem_t *settingsItem,
|
||||||
@@ -297,7 +263,7 @@ static void saveDeviceRawCleanupStore(
|
|||||||
// item's JSON is carried through byte-for-byte from what was already
|
// 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
|
// stored (or freshly defaulted if nothing was stored yet), so a save never
|
||||||
// reconstructs data it wasn't given.
|
// reconstructs data it wasn't given.
|
||||||
static errorret_t saveDeviceRawStoreItem(
|
errorret_t saveDeviceRawStoreItem(
|
||||||
savedevice_t *device,
|
savedevice_t *device,
|
||||||
savesettings_t *settings,
|
savesettings_t *settings,
|
||||||
saveslot_t *slot,
|
saveslot_t *slot,
|
||||||
@@ -450,7 +416,7 @@ static errorret_t saveDeviceRawStoreItem(
|
|||||||
// Reads the combined blob and populates exactly one item - `settings`, or
|
// 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
|
// 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.
|
// be non-NULL. Leaves the destination untouched if nothing's been saved yet.
|
||||||
static errorret_t saveDeviceRawFetchItem(
|
errorret_t saveDeviceRawFetchItem(
|
||||||
savedevice_t *device,
|
savedevice_t *device,
|
||||||
savesettings_t *settings,
|
savesettings_t *settings,
|
||||||
saveslot_t *slot,
|
saveslot_t *slot,
|
||||||
|
|||||||
@@ -162,6 +162,169 @@ errorret_t saveDeviceSettingsRead(
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDispose(savedevice_t *device);
|
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
|
* 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
|
* physical copy of the save blob (see the note above) - cheaply checks
|
||||||
|
|||||||
@@ -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) {
|
if(fullbox->duration <= 0.0f || fullbox->time >= fullbox->duration) {
|
||||||
return fullbox->toColor;
|
return fullbox->toColor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,15 @@ void uiFullboxInit(uifullbox_t *fullbox);
|
|||||||
*/
|
*/
|
||||||
void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta);
|
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.
|
* Renders the fullbox. Skipped entirely when the current alpha is zero.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "util/crypt.h"
|
#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;
|
*crc ^= byte;
|
||||||
for(int j = 0; j < 8; j++) {
|
for(int j = 0; j < 8; j++) {
|
||||||
*crc = (*crc >> 1) ^ (0xEDB88320u & -(*crc & 1u));
|
*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) {
|
void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size) {
|
||||||
const uint8_t *bytes = (const uint8_t *)data;
|
const uint8_t *bytes = (const uint8_t *)data;
|
||||||
for(size_t i = 0; i < size; i++) {
|
for(size_t i = 0; i < size; i++) {
|
||||||
_cryptCRC32Step(crc, bytes[i]);
|
cryptCRC32Step(crc, bytes[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ uint32_t cryptCRC32(const void *data, const size_t size);
|
|||||||
*/
|
*/
|
||||||
uint32_t cryptCRC32Begin(void);
|
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.
|
* Feeds bytes into a running CRC32 accumulator.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
|
#include "log/logdolphin.h"
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
static bool_t fatTried = false;
|
static bool_t fatTried = false;
|
||||||
static bool_t fatReady = false;
|
static bool_t fatReady = false;
|
||||||
|
|
||||||
static void logInitFAT(void) {
|
void logInitFAT(void) {
|
||||||
if(fatTried) return;
|
if(fatTried) return;
|
||||||
fatTried = true;
|
fatTried = true;
|
||||||
fatReady = fatInitDefault();
|
fatReady = fatInitDefault();
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -129,20 +129,7 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) {
|
|||||||
return usedBlocks < (uint32_t)blockCount;
|
return usedBlocks < (uint32_t)blockCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One probe result for one of the two ping-pong slots.
|
savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot(
|
||||||
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(
|
|
||||||
const int32_t channel,
|
const int32_t channel,
|
||||||
const char_t *filename,
|
const char_t *filename,
|
||||||
const bool_t keepBuffer
|
const bool_t keepBuffer
|
||||||
|
|||||||
@@ -97,6 +97,33 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel);
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device);
|
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
|
* 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
|
* card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(device);
|
saveDeviceFireCallback(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorret_t saveDeviceDolphinNandGetDataPath(
|
errorret_t saveDeviceDolphinNandGetDataPath(
|
||||||
char_t *dest,
|
char_t *dest,
|
||||||
const size_t destSize
|
const size_t destSize
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device);
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinNandDispose(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
|
* 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
|
* atomically (write to a temp file, then swap it in via ISFS_Rename) so a
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(device);
|
saveDeviceFireCallback(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorret_t saveDeviceDolphinSDGetDataPath(
|
errorret_t saveDeviceDolphinSDGetDataPath(
|
||||||
char_t *dest,
|
char_t *dest,
|
||||||
const size_t destSize
|
const size_t destSize
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device);
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinSDDispose(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
|
* 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
|
* atomically (write to a temp file, then swap it in) so a crash mid-write
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(device);
|
saveDeviceFireCallback(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorret_t saveDevicePSPGetDataPath(
|
errorret_t saveDevicePSPGetDataPath(
|
||||||
char_t *dest,
|
char_t *dest,
|
||||||
const size_t destSize
|
const size_t destSize
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ typedef struct savedevice_s savedevice_t;
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDevicePSPInit(savedevice_t *device);
|
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
|
* 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
|
* atomically (write to a temp file, then swap it in) so a crash mid-write
|
||||||
|
|||||||
+352
-343
@@ -90,6 +90,7 @@ Usage:
|
|||||||
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
python3 -m tools.asset.chunk <file.dcf> # upgrade legacy (v1/v2) DCF in-place
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
@@ -160,121 +161,121 @@ DMF_VERSION = 1
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def tile_index(x, y):
|
def tile_index(x, y):
|
||||||
return x + y * CHUNK_WIDTH
|
return x + y * CHUNK_WIDTH
|
||||||
|
|
||||||
|
|
||||||
def find_model(filename):
|
def find_model(filename):
|
||||||
"""Search ASSETS_DIR/models/ recursively for filename.
|
"""Search ASSETS_DIR/models/ recursively for filename.
|
||||||
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
Returns the path relative to ASSETS_DIR (forward slashes), or None."""
|
||||||
models_root = os.path.join(ASSETS_DIR, 'models')
|
models_root = os.path.join(ASSETS_DIR, 'models')
|
||||||
for dirpath, _, filenames in os.walk(models_root):
|
for dirpath, _, filenames in os.walk(models_root):
|
||||||
if filename in filenames:
|
if filename in filenames:
|
||||||
rel = os.path.relpath(
|
rel = os.path.relpath(
|
||||||
os.path.join(dirpath, filename), ASSETS_DIR
|
os.path.join(dirpath, filename), ASSETS_DIR
|
||||||
)
|
)
|
||||||
return rel.replace('\\', '/')
|
return rel.replace('\\', '/')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def write_model_json(path, mesh_rel, texture_rel, color):
|
def write_model_json(path, mesh_rel, texture_rel, color):
|
||||||
"""Write a model JSON descriptor file."""
|
"""Write a model JSON descriptor file."""
|
||||||
obj = {'mesh': mesh_rel, 'color': color}
|
obj = {'mesh': mesh_rel, 'color': color}
|
||||||
if texture_rel:
|
if texture_rel:
|
||||||
obj['texture'] = texture_rel
|
obj['texture'] = texture_rel
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w') as f:
|
||||||
json.dump(obj, f, indent=2)
|
json.dump(obj, f, indent=2)
|
||||||
print(f' Wrote model JSON {path}')
|
print(f' Wrote model JSON {path}')
|
||||||
|
|
||||||
|
|
||||||
def derive_dcf_path(json_path):
|
def derive_dcf_path(json_path):
|
||||||
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
"""assetsraw/chunks/chunk_X_Y_Z.json -> assets/chunks/X_Y_Z.dcf"""
|
||||||
base = os.path.splitext(os.path.basename(json_path))[0]
|
base = os.path.splitext(os.path.basename(json_path))[0]
|
||||||
if base.startswith('chunk_'):
|
if base.startswith('chunk_'):
|
||||||
base = base[len('chunk_'):]
|
base = base[len('chunk_'):]
|
||||||
rel_dir = os.path.relpath(os.path.dirname(os.path.abspath(json_path)), ASSETSRAW_DIR)
|
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')
|
return os.path.join(ASSETS_DIR, rel_dir, base + '.dcf')
|
||||||
|
|
||||||
|
|
||||||
def write_dmf(path, vertex_bytes):
|
def write_dmf(path, vertex_bytes):
|
||||||
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
buf += DMF_MAGIC
|
buf += DMF_MAGIC
|
||||||
buf += struct.pack('<I', DMF_VERSION)
|
buf += struct.pack('<I', DMF_VERSION)
|
||||||
buf += struct.pack('<I', vert_count)
|
buf += struct.pack('<I', vert_count)
|
||||||
buf += vertex_bytes
|
buf += vertex_bytes
|
||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
f.write(buf)
|
f.write(buf)
|
||||||
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
print(f' Wrote DMF {path}: {vert_count} vertices, {len(buf)} bytes')
|
||||||
|
|
||||||
|
|
||||||
def write_dcf(
|
def write_dcf(
|
||||||
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
dcf_path, tiles, mesh_names, mesh_offsets=None,
|
||||||
entity_spawns=None, area_spawns=None
|
entity_spawns=None, area_spawns=None
|
||||||
):
|
):
|
||||||
"""Write a current-version DCF referencing the given DMF asset paths."""
|
"""Write a current-version DCF referencing the given DMF asset paths."""
|
||||||
mesh_count = len(mesh_names)
|
mesh_count = len(mesh_names)
|
||||||
if mesh_offsets is None:
|
if mesh_offsets is None:
|
||||||
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
mesh_offsets = [(0.0, 0.0, 0.0)] * mesh_count
|
||||||
if entity_spawns is None:
|
if entity_spawns is None:
|
||||||
entity_spawns = []
|
entity_spawns = []
|
||||||
if area_spawns is None:
|
if area_spawns is None:
|
||||||
area_spawns = []
|
area_spawns = []
|
||||||
|
|
||||||
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
if len(entity_spawns) > CHUNK_ENTITY_SPAWN_COUNT_MAX:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
f"Too many entity spawns ({len(entity_spawns)}) - max "
|
||||||
f"{CHUNK_ENTITY_SPAWN_COUNT_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('<I', VERSION_OUT)
|
|
||||||
buf += tiles
|
|
||||||
buf += struct.pack('<B', mesh_count)
|
|
||||||
for name, offset in zip(mesh_names, mesh_offsets):
|
|
||||||
encoded = name.encode('ascii')
|
|
||||||
if len(encoded) >= 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('<B', len(entity_spawns))
|
|
||||||
for spawn in entity_spawns:
|
|
||||||
kind = spawn['kind']
|
|
||||||
x, y, z = spawn['pos']
|
|
||||||
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
|
||||||
a, b = spawn['globalId'], 0
|
|
||||||
else:
|
|
||||||
a, b = spawn['itemId'], spawn['quantity']
|
|
||||||
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
|
||||||
|
|
||||||
buf += struct.pack('<B', len(area_spawns))
|
|
||||||
for area in area_spawns:
|
|
||||||
minX, minY, minZ = area['min']
|
|
||||||
maxX, maxY, maxZ = area['max']
|
|
||||||
buf += struct.pack(
|
|
||||||
'<6hHBB',
|
|
||||||
minX, minY, minZ, maxX, maxY, maxZ,
|
|
||||||
area['callbackId'], area['notify'], area['trigger']
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(dcf_path, 'wb') as f:
|
|
||||||
f.write(buf)
|
|
||||||
print(
|
|
||||||
f' Wrote DCF {dcf_path}: '
|
|
||||||
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
|
||||||
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
|
||||||
f'area(s), {len(buf)} bytes'
|
|
||||||
)
|
)
|
||||||
|
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('<I', VERSION_OUT)
|
||||||
|
buf += tiles
|
||||||
|
buf += struct.pack('<B', mesh_count)
|
||||||
|
for name, offset in zip(mesh_names, mesh_offsets):
|
||||||
|
encoded = name.encode('ascii')
|
||||||
|
if len(encoded) >= 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('<B', len(entity_spawns))
|
||||||
|
for spawn in entity_spawns:
|
||||||
|
kind = spawn['kind']
|
||||||
|
x, y, z = spawn['pos']
|
||||||
|
if kind == ENTITY_SPAWN_KIND_GLOBAL:
|
||||||
|
a, b = spawn['globalId'], 0
|
||||||
|
else:
|
||||||
|
a, b = spawn['itemId'], spawn['quantity']
|
||||||
|
buf += struct.pack('<BHB3h', kind, a, b, x, y, z)
|
||||||
|
|
||||||
|
buf += struct.pack('<B', len(area_spawns))
|
||||||
|
for area in area_spawns:
|
||||||
|
minX, minY, minZ = area['min']
|
||||||
|
maxX, maxY, maxZ = area['max']
|
||||||
|
buf += struct.pack(
|
||||||
|
'<6hHBB',
|
||||||
|
minX, minY, minZ, maxX, maxY, maxZ,
|
||||||
|
area['callbackId'], area['notify'], area['trigger']
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(dcf_path, 'wb') as f:
|
||||||
|
f.write(buf)
|
||||||
|
print(
|
||||||
|
f' Wrote DCF {dcf_path}: '
|
||||||
|
f'version {VERSION_OUT}, {mesh_count} mesh(es), '
|
||||||
|
f'{len(entity_spawns)} entity spawn(s), {len(area_spawns)} '
|
||||||
|
f'area(s), {len(buf)} bytes'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -282,21 +283,21 @@ def write_dcf(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
||||||
"""Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
"""Six vertices (2 CCW triangles) for a quad with per-corner Z heights.
|
||||||
Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
Corner order: SW=(fx,fy), SE=(fx+1,fy), NE=(fx+1,fy+1), NW=(fx,fy+1).
|
||||||
NORTH=+Y, EAST=+X (matches entityDirGetRelative)."""
|
NORTH=+Y, EAST=+X (matches entityDirGetRelative)."""
|
||||||
verts = [
|
verts = [
|
||||||
(u0, v0, fx, fy, sw_z),
|
(u0, v0, fx, fy, sw_z),
|
||||||
(u1, v0, fx+1, fy, se_z),
|
(u1, v0, fx+1, fy, se_z),
|
||||||
(u1, v1, fx+1, fy+1, ne_z),
|
(u1, v1, fx+1, fy+1, ne_z),
|
||||||
(u0, v0, fx, fy, sw_z),
|
(u0, v0, fx, fy, sw_z),
|
||||||
(u1, v1, fx+1, fy+1, ne_z),
|
(u1, v1, fx+1, fy+1, ne_z),
|
||||||
(u0, v1, fx, fy+1, nw_z),
|
(u0, v1, fx, fy+1, nw_z),
|
||||||
]
|
]
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
for v in verts:
|
for v in verts:
|
||||||
buf += struct.pack('<5f', *v)
|
buf += struct.pack('<5f', *v)
|
||||||
return bytes(buf)
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
# Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z.
|
# Per-shape corner heights as (sw, se, ne, nw) offsets from tile base Z.
|
||||||
@@ -305,150 +306,150 @@ def _tile_quad(u0, u1, v0, v1, fx, fy, sw_z, se_z, ne_z, nw_z):
|
|||||||
# Diagonal inner-corner ramps: only the corner opposite the named one is
|
# Diagonal inner-corner ramps: only the corner opposite the named one is
|
||||||
# lowered to 0, the rest (including the named corner) stay raised at 1.
|
# lowered to 0, the rest (including the named corner) stay raised at 1.
|
||||||
_RAMP_CORNERS = {
|
_RAMP_CORNERS = {
|
||||||
TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0),
|
TILE_SHAPE_GROUND: (0.0, 0.0, 0.0, 0.0),
|
||||||
TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high
|
TILE_SHAPE_RAMP_NORTH: (0.0, 0.0, 1.0, 1.0), # south=low, north=high
|
||||||
TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low
|
TILE_SHAPE_RAMP_SOUTH: (1.0, 1.0, 0.0, 0.0), # south=high, north=low
|
||||||
TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high
|
TILE_SHAPE_RAMP_EAST: (0.0, 1.0, 1.0, 0.0), # west=low, east=high
|
||||||
TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low
|
TILE_SHAPE_RAMP_WEST: (1.0, 0.0, 0.0, 1.0), # west=high, east=low
|
||||||
TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised
|
TILE_SHAPE_RAMP_NORTHEAST: (0.0, 0.0, 1.0, 0.0), # only NE raised
|
||||||
TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised
|
TILE_SHAPE_RAMP_NORTHWEST: (0.0, 0.0, 0.0, 1.0), # only NW raised
|
||||||
TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised
|
TILE_SHAPE_RAMP_SOUTHEAST: (0.0, 1.0, 0.0, 0.0), # only SE raised
|
||||||
TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised
|
TILE_SHAPE_RAMP_SOUTHWEST: (1.0, 0.0, 0.0, 0.0), # only SW raised
|
||||||
TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low
|
TILE_SHAPE_RAMP_NORTHEAST_INNER: (0.0, 1.0, 1.0, 1.0), # only SW low
|
||||||
TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low
|
TILE_SHAPE_RAMP_NORTHWEST_INNER: (1.0, 0.0, 1.0, 1.0), # only SE low
|
||||||
TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low
|
TILE_SHAPE_RAMP_SOUTHEAST_INNER: (1.0, 1.0, 1.0, 0.0), # only NW low
|
||||||
TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low
|
TILE_SHAPE_RAMP_SOUTHWEST_INNER: (1.0, 1.0, 0.0, 1.0), # only NE low
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_terrain_verts(tiles_bytes):
|
def build_terrain_verts(tiles_bytes):
|
||||||
"""Generate quads for every non-null tile, shaped to match tile type."""
|
"""Generate quads for every non-null tile, shaped to match tile type."""
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
for y in range(CHUNK_HEIGHT):
|
for y in range(CHUNK_HEIGHT):
|
||||||
for x in range(CHUNK_WIDTH):
|
for x in range(CHUNK_WIDTH):
|
||||||
idx = tile_index(x, y)
|
idx = tile_index(x, y)
|
||||||
tile_type, z = struct.unpack_from(
|
tile_type, z = struct.unpack_from(
|
||||||
'<IB', tiles_bytes, idx * TILE_SIZE
|
'<IB', tiles_bytes, idx * TILE_SIZE
|
||||||
)
|
)
|
||||||
corners = _RAMP_CORNERS.get(tile_type)
|
corners = _RAMP_CORNERS.get(tile_type)
|
||||||
if corners is None:
|
if corners is None:
|
||||||
continue
|
continue
|
||||||
fx, fy, fz = float(x), float(y), float(z)
|
fx, fy, fz = float(x), float(y), float(z)
|
||||||
u0 = x / CHUNK_WIDTH
|
u0 = x / CHUNK_WIDTH
|
||||||
u1 = (x + 1) / CHUNK_WIDTH
|
u1 = (x + 1) / CHUNK_WIDTH
|
||||||
v0 = y / CHUNK_HEIGHT
|
v0 = y / CHUNK_HEIGHT
|
||||||
v1 = (y + 1) / CHUNK_HEIGHT
|
v1 = (y + 1) / CHUNK_HEIGHT
|
||||||
sw, se, ne, nw = corners
|
sw, se, ne, nw = corners
|
||||||
buf += _tile_quad(
|
buf += _tile_quad(
|
||||||
u0, u1, v0, v1, fx, fy,
|
u0, u1, v0, v1, fx, fy,
|
||||||
(fz + sw) * WORLD_LAYER_HEIGHT,
|
(fz + sw) * WORLD_LAYER_HEIGHT,
|
||||||
(fz + se) * WORLD_LAYER_HEIGHT,
|
(fz + se) * WORLD_LAYER_HEIGHT,
|
||||||
(fz + ne) * WORLD_LAYER_HEIGHT,
|
(fz + ne) * WORLD_LAYER_HEIGHT,
|
||||||
(fz + nw) * WORLD_LAYER_HEIGHT
|
(fz + nw) * WORLD_LAYER_HEIGHT
|
||||||
)
|
)
|
||||||
return bytes(buf)
|
return bytes(buf)
|
||||||
|
|
||||||
|
|
||||||
def from_json(json_path, dcf_path):
|
def from_json(json_path, dcf_path):
|
||||||
with open(json_path) as f:
|
with open(json_path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||||
column_z = {}
|
column_z = {}
|
||||||
for tile in data.get('tiles', []):
|
for tile in data.get('tiles', []):
|
||||||
pos = tile['pos']
|
pos = tile['pos']
|
||||||
x, y, z = int(pos[0]), int(pos[1]), int(pos[2])
|
x, y, z = int(pos[0]), int(pos[1]), int(pos[2])
|
||||||
if (x, y) in column_z and column_z[(x, y)] != z:
|
if (x, y) in column_z and column_z[(x, y)] != z:
|
||||||
print(
|
print(
|
||||||
f' WARNING: {json_path}: column ({x}, {y}) has tiles at '
|
f' WARNING: {json_path}: column ({x}, {y}) has tiles at '
|
||||||
f'z={column_z[(x, y)]} and z={z} - only one tile per '
|
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)'
|
f'column is kept, z={z} wins (last one in the array)'
|
||||||
)
|
)
|
||||||
column_z[(x, y)] = z
|
column_z[(x, y)] = z
|
||||||
struct.pack_into(
|
struct.pack_into(
|
||||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||||
int(tile['type']), z
|
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def process_json(json_path):
|
||||||
dcf_path = derive_dcf_path(json_path)
|
dcf_path = derive_dcf_path(json_path)
|
||||||
os.makedirs(os.path.dirname(dcf_path), exist_ok=True)
|
os.makedirs(os.path.dirname(dcf_path), exist_ok=True)
|
||||||
print(f"{json_path} -> {dcf_path}")
|
print(f"{json_path} -> {dcf_path}")
|
||||||
from_json(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):
|
def collapse_legacy_tiles(legacy_tiles, path):
|
||||||
"""Collapse the old one-tile-per-(x,y,z) grid into the current
|
"""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
|
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."""
|
across its Z layers, the highest Z wins and a warning is printed."""
|
||||||
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
tiles = bytearray(CHUNK_TILE_COUNT * TILE_SIZE)
|
||||||
for y in range(CHUNK_HEIGHT):
|
for y in range(CHUNK_HEIGHT):
|
||||||
for x in range(CHUNK_WIDTH):
|
for x in range(CHUNK_WIDTH):
|
||||||
found = []
|
found = []
|
||||||
for z in range(_LEGACY_CHUNK_DEPTH):
|
for z in range(_LEGACY_CHUNK_DEPTH):
|
||||||
legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
legacy_idx = x + y * CHUNK_WIDTH + z * CHUNK_WIDTH * CHUNK_HEIGHT
|
||||||
shape = struct.unpack_from(
|
shape = struct.unpack_from(
|
||||||
'<I', legacy_tiles, legacy_idx * _LEGACY_TILE_SIZE
|
'<I', legacy_tiles, legacy_idx * _LEGACY_TILE_SIZE
|
||||||
)[0]
|
)[0]
|
||||||
if shape != TILE_SHAPE_NULL:
|
if shape != TILE_SHAPE_NULL:
|
||||||
found.append((z, shape))
|
found.append((z, shape))
|
||||||
if not found:
|
if not found:
|
||||||
continue
|
continue
|
||||||
if len(found) > 1:
|
if len(found) > 1:
|
||||||
print(
|
print(
|
||||||
f' WARNING: {path}: column ({x}, {y}) has tiles at '
|
f' WARNING: {path}: column ({x}, {y}) has tiles at '
|
||||||
f'z={[z for z, _ in found]} - only one tile per column '
|
f'z={[z for z, _ in found]} - only one tile per column '
|
||||||
f'is kept, z={found[-1][0]} wins (highest Z)'
|
f'is kept, z={found[-1][0]} wins (highest Z)'
|
||||||
)
|
)
|
||||||
z, shape = found[-1]
|
z, shape = found[-1]
|
||||||
struct.pack_into(
|
struct.pack_into(
|
||||||
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
TILE_STRUCT_FORMAT, tiles, tile_index(x, y) * TILE_SIZE,
|
||||||
shape, z
|
shape, z
|
||||||
)
|
)
|
||||||
return bytes(tiles)
|
return bytes(tiles)
|
||||||
|
|
||||||
|
|
||||||
def read_legacy_dcf(path):
|
def read_legacy_dcf(path):
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
if data[:3] != FILE_MAGIC:
|
if data[:3] != FILE_MAGIC:
|
||||||
raise ValueError(f"{path}: not a DCF file")
|
raise ValueError(f"{path}: not a DCF file")
|
||||||
version = struct.unpack_from('<I', data, 4)[0]
|
version = struct.unpack_from('<I', data, 4)[0]
|
||||||
if version not in (1, 2):
|
if version not in (1, 2):
|
||||||
raise ValueError(f"{path}: expected version 1 or 2, got {version}")
|
raise ValueError(f"{path}: expected version 1 or 2, got {version}")
|
||||||
|
|
||||||
offset = 8
|
offset = 8
|
||||||
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
tiles_size = _LEGACY_CHUNK_TILE_COUNT * _LEGACY_TILE_SIZE
|
||||||
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
tiles = collapse_legacy_tiles(data[offset:offset + tiles_size], path)
|
||||||
offset += tiles_size
|
offset += tiles_size
|
||||||
|
|
||||||
meshes = []
|
meshes = []
|
||||||
if version == 1:
|
if version == 1:
|
||||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||||
if len(verts) != vert_count * VERTEX_SIZE:
|
if len(verts) != vert_count * VERTEX_SIZE:
|
||||||
raise ValueError(f"{path}: truncated vertex data")
|
raise ValueError(f"{path}: truncated vertex data")
|
||||||
if vert_count > 0:
|
if vert_count > 0:
|
||||||
meshes.append(verts)
|
meshes.append(verts)
|
||||||
else:
|
else:
|
||||||
mesh_count = data[offset]
|
mesh_count = data[offset]
|
||||||
offset += 1
|
offset += 1
|
||||||
for _ in range(mesh_count):
|
for _ in range(mesh_count):
|
||||||
vert_count = struct.unpack_from('<I', data, offset)[0]
|
vert_count = struct.unpack_from('<I', data, offset)[0]
|
||||||
offset += 4
|
offset += 4
|
||||||
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
verts = data[offset:offset + vert_count * VERTEX_SIZE]
|
||||||
if len(verts) != vert_count * VERTEX_SIZE:
|
if len(verts) != vert_count * VERTEX_SIZE:
|
||||||
raise ValueError(f"{path}: truncated vertex data")
|
raise ValueError(f"{path}: truncated vertex data")
|
||||||
offset += vert_count * VERTEX_SIZE
|
offset += vert_count * VERTEX_SIZE
|
||||||
if vert_count > 0:
|
if vert_count > 0:
|
||||||
meshes.append(verts)
|
meshes.append(verts)
|
||||||
return tiles, meshes
|
return tiles, meshes
|
||||||
|
|
||||||
|
|
||||||
def upgrade_dcf(path):
|
def upgrade_dcf(path):
|
||||||
print(f"Upgrading legacy DCF {path} ...")
|
print(f"Upgrading legacy DCF {path} ...")
|
||||||
tiles, meshes = read_legacy_dcf(path)
|
tiles, meshes = read_legacy_dcf(path)
|
||||||
print(
|
print(
|
||||||
f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, "
|
f" tiles={CHUNK_TILE_COUNT}, meshes={len(meshes)}, "
|
||||||
f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}"
|
f"total_verts={sum(len(m) // VERTEX_SIZE for m in meshes)}"
|
||||||
)
|
)
|
||||||
base = os.path.splitext(os.path.basename(path))[0]
|
base = os.path.splitext(os.path.basename(path))[0]
|
||||||
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
terrain_dir = os.path.join(ASSETS_DIR, 'meshes', 'chunks')
|
||||||
os.makedirs(terrain_dir, exist_ok=True)
|
os.makedirs(terrain_dir, exist_ok=True)
|
||||||
mesh_names = []
|
mesh_names = []
|
||||||
for idx, verts in enumerate(meshes):
|
for idx, verts in enumerate(meshes):
|
||||||
dmf_name = f'chunk_{base}_{idx}.dmf'
|
dmf_name = f'chunk_{base}_{idx}.dmf'
|
||||||
write_dmf(os.path.join(terrain_dir, dmf_name), verts)
|
write_dmf(os.path.join(terrain_dir, dmf_name), verts)
|
||||||
mesh_names.append(f'meshes/chunks/{dmf_name}')
|
mesh_names.append(f'meshes/chunks/{dmf_name}')
|
||||||
write_dcf(path, tiles, mesh_names)
|
write_dcf(path, tiles, mesh_names)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -547,31 +548,39 @@ def upgrade_dcf(path):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def main():
|
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')
|
chunks_dir = os.path.join(ASSETSRAW_DIR, 'chunks')
|
||||||
if not os.path.isdir(chunks_dir):
|
if not os.path.isdir(chunks_dir):
|
||||||
print(f"No directory found: {chunks_dir}")
|
print(f"No directory found: {chunks_dir}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
json_files = sorted(
|
json_files = sorted(
|
||||||
os.path.join(chunks_dir, f)
|
os.path.join(chunks_dir, f)
|
||||||
for f in os.listdir(chunks_dir)
|
for f in os.listdir(chunks_dir)
|
||||||
if f.endswith('.json')
|
if f.endswith('.json')
|
||||||
)
|
)
|
||||||
if not json_files:
|
if not json_files:
|
||||||
print(f"No JSON files found in {chunks_dir}")
|
print(f"No JSON files found in {chunks_dir}")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
for p in json_files:
|
for p in json_files:
|
||||||
process_json(p)
|
process_json(p)
|
||||||
return
|
return
|
||||||
|
|
||||||
src = args[0]
|
if os.path.splitext(args.path)[1].lower() == '.json':
|
||||||
if os.path.splitext(src)[1].lower() == '.json':
|
process_json(args.path)
|
||||||
process_json(src)
|
else:
|
||||||
else:
|
upgrade_dcf(args.path)
|
||||||
upgrade_dcf(src)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ DCTS format (little-endian throughout):
|
|||||||
item stream. Every pool entry starts 4-byte aligned.
|
item stream. Every pool entry starts 4-byte aligned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
@@ -428,9 +429,18 @@ def process_json(json_path):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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')
|
cutscenes_dir = os.path.join(ASSETSRAW_DIR, 'cutscenes')
|
||||||
if not os.path.isdir(cutscenes_dir):
|
if not os.path.isdir(cutscenes_dir):
|
||||||
print(f"No directory found: {cutscenes_dir}")
|
print(f"No directory found: {cutscenes_dir}")
|
||||||
@@ -447,7 +457,7 @@ def main():
|
|||||||
process_json(p)
|
process_json(p)
|
||||||
return
|
return
|
||||||
|
|
||||||
for p in args:
|
for p in args.paths:
|
||||||
process_json(p)
|
process_json(p)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-31
@@ -18,8 +18,8 @@ Usage:
|
|||||||
Reads raw vertex bytes from vertices.bin and writes a DMF file.
|
Reads raw vertex bytes from vertices.bin and writes a DMF file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import struct
|
import struct
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
MAGIC = b'DMF\x00'
|
MAGIC = b'DMF\x00'
|
||||||
@@ -28,42 +28,36 @@ VERTEX_SIZE = 20
|
|||||||
|
|
||||||
|
|
||||||
def write_dmf(path, vertex_bytes):
|
def write_dmf(path, vertex_bytes):
|
||||||
if len(vertex_bytes) % VERTEX_SIZE != 0:
|
if len(vertex_bytes) % VERTEX_SIZE != 0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Vertex data size {len(vertex_bytes)} is not a "
|
f"Vertex data size {len(vertex_bytes)} is not a "
|
||||||
f"multiple of {VERTEX_SIZE}"
|
f"multiple of {VERTEX_SIZE}"
|
||||||
)
|
|
||||||
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
|
||||||
buf = bytearray()
|
|
||||||
buf += MAGIC
|
|
||||||
buf += struct.pack('<I', VERSION)
|
|
||||||
buf += struct.pack('<I', vert_count)
|
|
||||||
buf += vertex_bytes
|
|
||||||
with open(path, 'wb') as f:
|
|
||||||
f.write(buf)
|
|
||||||
print(
|
|
||||||
f'Wrote {path}: {vert_count} vertices, {len(buf)} bytes'
|
|
||||||
)
|
)
|
||||||
return vert_count
|
vert_count = len(vertex_bytes) // VERTEX_SIZE
|
||||||
|
buf = bytearray()
|
||||||
|
buf += MAGIC
|
||||||
|
buf += struct.pack('<I', VERSION)
|
||||||
|
buf += struct.pack('<I', vert_count)
|
||||||
|
buf += vertex_bytes
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(buf)
|
||||||
|
print(
|
||||||
|
f'Wrote {path}: {vert_count} vertices, {len(buf)} bytes'
|
||||||
|
)
|
||||||
|
return vert_count
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
parser = argparse.ArgumentParser(description="Write a DMF (Dusk Mesh Format) file")
|
||||||
if len(args) != 2:
|
parser.add_argument('output', help='Path to output .dmf file')
|
||||||
print(
|
parser.add_argument('vertices', help='Path to raw vertex bytes file')
|
||||||
"Usage: python3 -m tools.asset.dmf "
|
args = parser.parse_args()
|
||||||
"<output.dmf> <vertices.bin>"
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
dst = args[0]
|
with open(args.vertices, 'rb') as f:
|
||||||
src = args[1]
|
vertex_bytes = f.read()
|
||||||
|
|
||||||
with open(src, 'rb') as f:
|
write_dmf(args.output, vertex_bytes)
|
||||||
vertex_bytes = f.read()
|
|
||||||
|
|
||||||
write_dmf(dst, vertex_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user