diff --git a/CMakeLists.txt b/CMakeLists.txt index fa3e489561..9d5d63d621 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ if (DUSK_MOVIE_SUPPORT) -DENABLE_SHARED=OFF -DWITH_TURBOJPEG=ON -DWITH_JAVA=OFF + -DCMAKE_INSTALL_LIBDIR=lib ) if (CMAKE_TOOLCHAIN_FILE) get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}") diff --git a/docs/modding.md b/docs/modding.md index 93c0ac4f14..c05e736d8b 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -373,6 +373,55 @@ 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`) + +Allows making changes to a stage's "stage info" (contents of .dzs/.dzr files). +(Currently only supports editing actor nodes.) + +```cpp +IMPORT_SERVICE(StageService, svc_stage); + +stage_actor_data_class record = { + "carry00", + 0xFF000000, + cXyz(0.0f, 0.0f, 0.0f), + csXyz(0, 0, 0), + 0, +}; + +StageActorHandle handle{}; +svc_stage->patch_actor(mod_ctx, "F_SP102", 0, -1, record_crc, &record, sizeof(record), &handle); +``` + +``` +StageActorHandle handle{}; +svc_stage->delete_actor(mod_ctx, "F_SP102", 0, -1, record_crc, &handle); +``` + +Patch or remove actors from the original actor list as the room loads. +Given records must be of either `stage_actor_data_class` or `stage_tgsc_data_class` types. +`record_crc` is the CRC-32 of the unmodified original record used to identify the record to replace or remove. + +``` +stage_actor_data_class record = { + "carry00", + 0xFF000000, + cXyz(0.0f, 0.0f, 0.0f), + csXyz(0, 0, 0), + 0, +}; + +StageActorHandle handle{}; +svc_stage->add_actor(mod_ctx, "F_SP102", 0, -1, &record, sizeof(record), &handle); +``` + +Add a new actor to the actor list as the room loads. +Given records must be of either `stage_actor_data_class` or `stage_tgsc_data_class` types. + +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/files.cmake b/files.cmake index 3fda4fefbe..9867b2d39a 100644 --- a/files.cmake +++ b/files.cmake @@ -1504,6 +1504,8 @@ set(DUSK_FILES src/dusk/mods/svc/window.hpp src/dusk/mods/svc/save.cpp src/dusk/mods/svc/save.hpp + src/dusk/mods/svc/stage.cpp + src/dusk/mods/svc/stage.hpp src/dusk/mouse.cpp src/dusk/scope_guard.hpp src/dusk/settings.cpp diff --git a/sdk/include/mods/svc/stage.h b/sdk/include/mods/svc/stage.h new file mode 100644 index 0000000000..a9f8198919 --- /dev/null +++ b/sdk/include/mods/svc/stage.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define STAGE_SERVICE_ID "dev.twilitrealm.dusklight.stage" +#define STAGE_SERVICE_MAJOR 1u +#define STAGE_SERVICE_MINOR 0u + +/* 0 is never a valid handle. */ +typedef uint64_t StageActorHandle; + +/* + * Runtime edits to Stage Info (.dzs/.dzr) data. + */ + +typedef struct StageService { + ServiceHeader header; + + /* + * Actor Node (ACTR/TGSC/SCOB/Door) Editing: + * 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. + */ + 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); + + ModResult (*delete_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer, + uint32_t record_crc, StageActorHandle* out_handle); + + ModResult (*add_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer, + const void* record, size_t record_size, StageActorHandle* out_handle); + + ModResult (*remove_actor_edit)(ModContext* ctx, StageActorHandle handle); +} StageService; + +MOD_DECLARE_SERVICE( + StageService, svc_stage, STAGE_SERVICE_ID, STAGE_SERVICE_MAJOR, STAGE_SERVICE_MINOR); diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index a27f0482f8..c92ce7493b 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -25,6 +25,7 @@ #include "dusk/logging.h" #include "helpers/string.hpp" #if TARGET_PC +#include "dusk/mods/svc/stage.hpp" #include #include #endif @@ -1592,7 +1593,18 @@ DUSK_GAME_DATA dStage_roomControl_c::roomDzs_c dStage_roomControl_c::m_roomDzs; u8 dStage_roomControl_c::mNoArcBank; #endif +#if TARGET_PC +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); if (actorInf == NULL) { @@ -1992,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++; @@ -2063,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++; @@ -2086,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++; } @@ -2511,6 +2538,29 @@ static void dKankyo_create() { fopKyM_fastCreate(fpcNm_ENVSE_e, 0, NULL, NULL, NULL); } +#if TARGET_PC +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); + stage_tgsc_data_class object{}; + std::memcpy(&object, record, size); + + fopAcM_prm_class* appen = fopAcM_CreateAppend(); + if (appen != nullptr) { + appen->base = object.base; + appen->room_no = static_cast(stage->getRoomNo()); + if (size > sizeof(stage_actor_data_class)) { + appen->scale = object.scale; + } + dStage_actorCreate( + reinterpret_cast(&object), appen, size); + } + }, + i_stage); +} +#endif + static void layerMemoryInfoLoader(void* i_data, dStage_dt_c* i_stage, int param_2) { UNUSED(param_2); static FuncTable l_layerFuncTable[] = { @@ -2707,6 +2757,9 @@ 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 + dusk_stage_svc_new_actor_create(i_stage); +#endif layerActorLoader(i_data, i_stage, param_2); } diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 67e1fe01d3..34134c90c9 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -212,6 +212,7 @@ void ModLoader::init_services() { &svc::g_windowModule, &svc::g_gfxModule, &svc::g_saveModule, + &svc::g_stageModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 165375d29a..df0ca7bdbb 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -74,5 +74,6 @@ extern const ServiceModule g_cameraModule; extern const ServiceModule g_windowModule; extern const ServiceModule g_gfxModule; extern const ServiceModule g_saveModule; +extern const ServiceModule g_stageModule; } // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/stage.cpp b/src/dusk/mods/svc/stage.cpp new file mode 100644 index 0000000000..10af84a238 --- /dev/null +++ b/src/dusk/mods/svc/stage.cpp @@ -0,0 +1,323 @@ +#include "stage.hpp" + +#include "registry.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/mods/loader/loader.hpp" +#include "mods/svc/stage.h" + +#include "d/d_com_inf_game.h" +#include "dusk/utilities.hpp" + +#include +#include +#include +#include +#include + +namespace dusk::mods::svc { +namespace { + +aurora::Module Log{"dusk::mods::stage"}; + +constexpr size_t kActrEntrySize = 0x20; +constexpr size_t kTgscEntrySize = 0x23; +constexpr uint8_t kRoomDefault = 0xFF; +constexpr int8_t kLayerDefault = -1; + +enum class EditKind : uint8_t { Patch, Delete, Add }; + +struct ActorEditRecord { + uint64_t handle = 0; + LoadedMod* mod = nullptr; + uint64_t seq = 0; + uint8_t room = 0; + int8_t layer = kLayerDefault; + EditKind kind = EditKind::Patch; + 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; + +int32_t compute_mod_priority(const LoadedMod& mod) { + int32_t index = 0; + for (const auto& other : ModLoader::instance().mods()) { + ++index; + if (&other == &mod) { + return index; + } + } + return index + 1; +} + +const std::vector* current_stage_edits() { + if (s_edits.empty()) { + return nullptr; + } + const char* stageName = dComIfGp_getStartStageName(); + if (stageName == nullptr) { + return nullptr; + } + const auto it = s_edits.find(stageName); + return it != s_edits.end() ? &it->second : nullptr; +} + +bool room_matches(const ActorEditRecord& record, uint8_t roomNo) { + return record.room == roomNo || record.room == kRoomDefault; +} + +bool layer_matches(const ActorEditRecord& record, int8_t layer) { + return record.layer == layer || record.layer == kLayerDefault; +} + +} // namespace + +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; + } + + 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 crc = utils::crc32(actorData, recordSize); + + const ActorEditRecord* winner = nullptr; + int32_t winnerPriority = 0; + const LoadedMod* firstOwner = nullptr; + for (const auto& record : *edits) { + if (record.kind == EditKind::Add || !record.mod->active || !layer_matches(record, layer) || + !room_matches(record, room)) + { + continue; + } + + const bool matches = record.crc == crc && (record.kind == EditKind::Delete || + record.record.size() == recordSize); + if (!matches) { + continue; + } + + if (firstOwner == nullptr) { + firstOwner = record.mod; + } else if (firstOwner != record.mod && s_warnedRecords.insert(record.crc).second) { + Log.warn("actor record {:#010x} edited by multiple mods; the later-loaded one wins", + record.crc); + } + + const auto priority = compute_mod_priority(*record.mod); + if (winner == nullptr || priority > winnerPriority || + (priority == winnerPriority && record.seq > winner->seq)) + { + winner = &record; + winnerPriority = priority; + } + } + + if (winner == nullptr) { + return true; + } + if (winner->kind == EditKind::Delete) { + return false; + } + + std::memcpy(actorData, winner->record.data(), winner->record.size()); + std::memcpy(actorPrm, winner->record.data() + 8, winner->record.size() - 8); + return true; +} + +void stage_create_new_actors( + int8_t roomNo, void (*createFn)(void* user, const void* record, size_t size), void* user) { + const auto* edits = current_stage_edits(); + if (edits == nullptr) { + return; + } + + const auto layer = static_cast(dComIfG_play_c::getLayerNo(0)); + for (const auto& record : *edits) { + if (record.kind == EditKind::Add && record.mod->active && + record.room == static_cast(roomNo) && layer_matches(record, layer)) + { + createFn(user, record.record.data(), record.record.size()); + } + } +} + +namespace { + +ModResult register_edit( + LoadedMod& mod, const char* stage, ActorEditRecord&& record, uint64_t& outHandle) { + record.handle = s_nextHandle++; + record.mod = &mod; + record.seq = s_nextSeq++; + outHandle = record.handle; + s_edits[stage].push_back(std::move(record)); + return MOD_OK; +} + +} // namespace + +ModResult stage_patch_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer, + uint32_t crc, const void* record, size_t recordSize, uint64_t& outHandle) { + const auto* bytes = static_cast(record); + return register_edit(mod, stage, + ActorEditRecord{.room = room, + .layer = layer, + .kind = EditKind::Patch, + .crc = crc, + .record = {bytes, bytes + recordSize}}, + outHandle); +} + +ModResult stage_delete_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer, + uint32_t crc, uint64_t& outHandle) { + return register_edit(mod, stage, + ActorEditRecord{.room = room, .layer = layer, .kind = EditKind::Delete, .crc = crc}, + outHandle); +} + +ModResult stage_add_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer, + const void* record, size_t recordSize, uint64_t& outHandle) { + const auto* bytes = static_cast(record); + return register_edit(mod, stage, + ActorEditRecord{.room = room, + .layer = layer, + .kind = EditKind::Add, + .record = {bytes, bytes + recordSize}}, + outHandle); +} + +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; }); + if (removed != 0) { + if (it->second.empty()) { + s_edits.erase(it); + } + return MOD_OK; + } + } + return MOD_INVALID_ARGUMENT; +} + +void stage_remove_mod(LoadedMod& mod) { + for (auto it = s_edits.begin(); it != s_edits.end();) { + std::erase_if(it->second, [&](const auto& record) { return record.mod == &mod; }); + it = it->second.empty() ? s_edits.erase(it) : std::next(it); + } +} + +namespace { +constexpr size_t kMaxStageNameLength = 8; + +bool is_valid_stage_name(const char* stage) { + if (stage == nullptr) { + return false; + } + const std::string_view view{stage}; + return !view.empty() && view.size() <= kMaxStageNameLength; +} + +bool is_valid_record_size(size_t size) { + return size == kActrEntrySize || size == kTgscEntrySize; +} + +ModResult stage_patch_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer, + uint32_t recordCrc, const void* record, size_t recordSize, StageActorHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || !is_valid_stage_name(stage) || record == nullptr || + !is_valid_record_size(recordSize)) + { + return MOD_INVALID_ARGUMENT; + } + + uint64_t handle = 0; + const auto result = + stage_patch_actor(*mod, stage, room, layer, recordCrc, record, recordSize, handle); + if (outHandle != nullptr) { + *outHandle = handle; + } + + return result; +} + +ModResult stage_delete_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer, + uint32_t recordCrc, StageActorHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || !is_valid_stage_name(stage)) { + return MOD_INVALID_ARGUMENT; + } + + uint64_t handle = 0; + const auto result = stage_delete_actor(*mod, stage, room, layer, recordCrc, handle); + if (outHandle != nullptr) { + *outHandle = handle; + } + return result; +} + +ModResult stage_add_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer, + const void* record, size_t recordSize, StageActorHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || !is_valid_stage_name(stage) || room == kRoomDefault || + record == nullptr || !is_valid_record_size(recordSize)) + { + return MOD_INVALID_ARGUMENT; + } + + uint64_t handle = 0; + const auto result = stage_add_actor(*mod, stage, room, layer, record, recordSize, handle); + if (outHandle != nullptr) { + *outHandle = handle; + } + return result; +} + +ModResult stage_remove_actor_edit_(ModContext* context, StageActorHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return stage_remove_actor_edit(*mod, handle); +} + +constexpr StageService s_stageService{ + .header = SERVICE_HEADER(StageService, STAGE_SERVICE_MAJOR, STAGE_SERVICE_MINOR), + .patch_actor = stage_patch_actor_, + .delete_actor = stage_delete_actor_, + .add_actor = stage_add_actor_, + .remove_actor_edit = stage_remove_actor_edit_, +}; + +} // namespace + +constinit const ServiceModule g_stageModule{ + .id = STAGE_SERVICE_ID, + .majorVersion = STAGE_SERVICE_MAJOR, + .minorVersion = STAGE_SERVICE_MINOR, + .service = &s_stageService, + .modDetached = stage_remove_mod, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/stage.hpp b/src/dusk/mods/svc/stage.hpp new file mode 100644 index 0000000000..f27f70e646 --- /dev/null +++ b/src/dusk/mods/svc/stage.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace dusk::mods::svc { + +bool stage_apply_actor_edits(void* actorData, void* actorPrm, size_t recordSize, int8_t roomNo); + +void stage_create_new_actors( + int8_t roomNo, void (*createFn)(void* user, const void* record, size_t size), void* user); + +} // namespace dusk::mods::svc