diff --git a/docs/modding.md b/docs/modding.md index 93c0ac4f14..722a83a5ae 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -373,6 +373,15 @@ buffer contract as `get_blob`. Pass a `NULL` buffer to either read function to q are cleared. Observers are removed automatically when the mod is detached, so the output handle is only needed for manual unregistration. Save callbacks run on the game thread. +### StageService (`mods/svc/stage.h`) + +Patches, removes, or adds serialized stage actor records as rooms load. ACTR records are 0x20 bytes and TGSC records +are 0x23 bytes. `record_crc` is the CRC-32 of the unmodified record. + +Stage names may contain up to 8 characters. For patches and deletions, room `0xff` and layer `-1` match any room or +layer; additions require a specific room. Edits are removed when the mod is detached. If multiple mods edit the same +record, the later-loaded mod wins. + ### UiService (`mods/svc/ui.h`) Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window, diff --git a/sdk/include/mods/svc/stage.h b/sdk/include/mods/svc/stage.h index 51e8314f20..3efef38ce3 100644 --- a/sdk/include/mods/svc/stage.h +++ b/sdk/include/mods/svc/stage.h @@ -10,24 +10,30 @@ #define STAGE_SERVICE_MAJOR 1u #define STAGE_SERVICE_MINOR 0u +/* 0 is never a valid handle. */ typedef uint64_t StageActorHandle; +/* + * Runtime edits to ACTR (0x20-byte) and TGSC (0x23-byte) stage records. + * + * stage must be a non-empty name of at most 8 characters. room 0xff and layer -1 match any room + * or layer for patch and delete operations; add_actor requires a specific room. Later-loaded mods + * win conflicts. record_crc is the CRC-32 of the unmodified record. Registrations are removed when + * the calling mod is detached. out_handle may be NULL. + */ + typedef struct StageService { ServiceHeader header; - /* Replace actor record that matches record_crc with given record */ ModResult (*patch_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer, uint32_t record_crc, const void* record, size_t record_size, StageActorHandle* out_handle); - /* Remove actor record matching record_crc */ ModResult (*delete_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer, uint32_t record_crc, StageActorHandle* out_handle); - /* Add new actor record */ ModResult (*add_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer, const void* record, size_t record_size, StageActorHandle* out_handle); - /* Remove an edit previously registered by the calling mod */ ModResult (*remove_actor_edit)(ModContext* ctx, StageActorHandle handle); } StageService; diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index 28ff1ddb6b..c92ce7493b 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -1593,13 +1593,17 @@ DUSK_GAME_DATA dStage_roomControl_c::roomDzs_c dStage_roomControl_c::m_roomDzs; u8 dStage_roomControl_c::mNoArcBank; #endif -static void dStage_actorCreate(stage_actor_data_class* i_actorData, fopAcM_prm_class* i_actorPrm) { #if TARGET_PC - if (!dusk::mods::svc::stage_apply_actor_edits(i_actorData, i_actorPrm, i_actorPrm->room_no)) { - DuskLog.error("Failed to apply stage actor edits!"); +static void dStage_actorCreate(stage_actor_data_class* i_actorData, fopAcM_prm_class* i_actorPrm, + size_t recordSize = sizeof(stage_actor_data_class)) { + if (!dusk::mods::svc::stage_apply_actor_edits(i_actorData, i_actorPrm, recordSize, + i_actorPrm->room_no)) + { JKRFree(i_actorPrm); return; } +#else +static void dStage_actorCreate(stage_actor_data_class* i_actorData, fopAcM_prm_class* i_actorPrm) { #endif dStage_objectNameInf* actorInf = dStage_searchName(i_actorData->name); @@ -2000,7 +2004,12 @@ static int dStage_tgscCommonLayerInit(dStage_dt_c* i_stage, void* i_data, int en appen->base = tgsc_data->base; appen->room_no = (int)i_stage->getRoomNo(); appen->scale = tgsc_data->scale; +#if TARGET_PC + dStage_actorCreate(actor_data, appen, + sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class)); +#else dStage_actorCreate(actor_data, appen); +#endif } } tgsc_data++; @@ -2071,7 +2080,12 @@ static int dStage_tgscInfoInit(dStage_dt_c* i_stage, void* i_data, int entryNum, appen->base = actor_data->base; appen->room_no = (int)i_stage->getRoomNo(); appen->scale = tgsc_data->scale; +#if TARGET_PC + dStage_actorCreate(actor_data, appen, + sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class)); +#else dStage_actorCreate(actor_data, appen); +#endif } } tgsc_data++; @@ -2094,7 +2108,12 @@ static int dStage_doorInfoInit(dStage_dt_c* i_stage, void* i_data, int entryNum, appen->base = actor_data->base; appen->room_no = (int)i_stage->getRoomNo(); appen->scale = tgsc_data->scale; +#if TARGET_PC + dStage_actorCreate(actor_data, appen, + sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class)); +#else dStage_actorCreate(actor_data, appen); +#endif } tgsc_data++; } @@ -2520,7 +2539,7 @@ static void dKankyo_create() { } #if TARGET_PC -static void duskStageSvc_newActorCreate(dStage_dt_c* i_stage) { +static void dusk_stage_svc_new_actor_create(dStage_dt_c* i_stage) { dusk::mods::svc::stage_create_new_actors(i_stage->getRoomNo(), [](void* user, const void* record, size_t size) { auto* stage = static_cast(user); @@ -2531,11 +2550,11 @@ static void duskStageSvc_newActorCreate(dStage_dt_c* i_stage) { if (appen != nullptr) { appen->base = object.base; appen->room_no = static_cast(stage->getRoomNo()); - // if size is greater than 0x20, must be a TGSC actor - if (size > 0x20) { + if (size > sizeof(stage_actor_data_class)) { appen->scale = object.scale; } - dStage_actorCreate(reinterpret_cast(&object), appen); + dStage_actorCreate( + reinterpret_cast(&object), appen, size); } }, i_stage); @@ -2739,7 +2758,7 @@ void dStage_dt_c_roomReLoader(void* i_data, dStage_dt_c* i_stage, int param_2) { dStage_dt_c_decode(i_data, i_stage, l_funcTable, ARRAY_SIZEU(l_funcTable)); #if TARGET_PC - duskStageSvc_newActorCreate(i_stage); + dusk_stage_svc_new_actor_create(i_stage); #endif layerActorLoader(i_data, i_stage, param_2); } diff --git a/src/dusk/mods/svc/stage.cpp b/src/dusk/mods/svc/stage.cpp index 26a97e8321..10af84a238 100644 --- a/src/dusk/mods/svc/stage.cpp +++ b/src/dusk/mods/svc/stage.cpp @@ -10,18 +10,18 @@ #include "dusk/utilities.hpp" #include +#include #include #include #include -#include namespace dusk::mods::svc { namespace { -aurora::Module Log("dusk::mods::stage"); +aurora::Module Log{"dusk::mods::stage"}; -constexpr size_t kACTREntrySize = 0x20; -constexpr size_t kTGSCEntrySize = 0x23; +constexpr size_t kActrEntrySize = 0x20; +constexpr size_t kTgscEntrySize = 0x23; constexpr uint8_t kRoomDefault = 0xFF; constexpr int8_t kLayerDefault = -1; @@ -34,16 +34,15 @@ struct ActorEditRecord { uint8_t room = 0; int8_t layer = kLayerDefault; EditKind kind = EditKind::Patch; - uint32_t crc = 0; // Patch/Delete - std::vector record; // Patch/Add + uint32_t crc = 0; + std::vector record; }; std::unordered_map> s_edits; uint64_t s_nextHandle = 1; uint64_t s_nextSeq = 1; -std::unordered_set s_warnedRecords; // (crc | seq-of-stage-hash) collision warnings +std::unordered_set s_warnedRecords; -// Position in m_mods (dependency-sorted load order) + 1; later-loaded mods win. int32_t compute_mod_priority(const LoadedMod& mod) { int32_t index = 0; for (const auto& other : ModLoader::instance().mods()) { @@ -77,21 +76,19 @@ bool layer_matches(const ActorEditRecord& record, int8_t layer) { } // namespace -bool stage_apply_actor_edits(void* actorData, void* actorPrm, int8_t roomNo) { +bool stage_apply_actor_edits(void* actorData, void* actorPrm, size_t recordSize, int8_t roomNo) { const auto* edits = current_stage_edits(); if (edits == nullptr) { return true; } - // PLYR spawns use room -1 to signify using the start room ID uint8_t room = static_cast(roomNo); if (roomNo == -1) { room = static_cast(dComIfGp_getStartStageRoomNo()); } const auto layer = static_cast(dComIfG_play_c::getLayerNo(0)); - const auto crcActr = utils::crc32(actorData, kACTREntrySize); - const auto crcTgsc = utils::crc32(actorData, kTGSCEntrySize); + const auto crc = utils::crc32(actorData, recordSize); const ActorEditRecord* winner = nullptr; int32_t winnerPriority = 0; @@ -103,10 +100,8 @@ bool stage_apply_actor_edits(void* actorData, void* actorPrm, int8_t roomNo) { continue; } - // check record type based on size - const bool matches = record.kind == EditKind::Delete - ? record.crc == crcActr || record.crc == crcTgsc - : record.crc == (record.record.size() == kTGSCEntrySize ? crcTgsc : crcActr); + const bool matches = record.crc == crc && (record.kind == EditKind::Delete || + record.record.size() == recordSize); if (!matches) { continue; } @@ -158,8 +153,8 @@ void stage_create_new_actors( namespace { -ModResult register_edit(LoadedMod& mod, const char* stage, ActorEditRecord&& record, - uint64_t& outHandle) { +ModResult register_edit( + LoadedMod& mod, const char* stage, ActorEditRecord&& record, uint64_t& outHandle) { record.handle = s_nextHandle++; record.mod = &mod; record.seq = s_nextSeq++; @@ -202,9 +197,8 @@ ModResult stage_add_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_ ModResult stage_remove_actor_edit(LoadedMod& mod, uint64_t handle) { for (auto it = s_edits.begin(); it != s_edits.end(); ++it) { - const auto removed = std::erase_if(it->second, [&](const auto& record) { - return record.handle == handle && record.mod == &mod; - }); + const auto removed = std::erase_if(it->second, + [&](const auto& record) { return record.handle == handle && record.mod == &mod; }); if (removed != 0) { if (it->second.empty()) { s_edits.erase(it); @@ -234,7 +228,7 @@ bool is_valid_stage_name(const char* stage) { } bool is_valid_record_size(size_t size) { - return size == kACTREntrySize || size == kTGSCEntrySize; + return size == kActrEntrySize || size == kTgscEntrySize; } ModResult stage_patch_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer, diff --git a/src/dusk/mods/svc/stage.hpp b/src/dusk/mods/svc/stage.hpp index 20f05c8276..f27f70e646 100644 --- a/src/dusk/mods/svc/stage.hpp +++ b/src/dusk/mods/svc/stage.hpp @@ -5,11 +5,9 @@ namespace dusk::mods::svc { -// Apply registered patch/delete edits for the current stage/layer to the record about to spawn -bool stage_apply_actor_edits(void* actorData, void* actorPrm, int8_t roomNo); +bool stage_apply_actor_edits(void* actorData, void* actorPrm, size_t recordSize, int8_t roomNo); -// Creates registered new actors from their record's on room load void stage_create_new_actors( int8_t roomNo, void (*createFn)(void* user, const void* record, size_t size), void* user); -} // namespace dusk::mods +} // namespace dusk::mods::svc