This commit is contained in:
TakaRikka
2026-08-05 00:13:24 -07:00
9 changed files with 486 additions and 0 deletions
+1
View File
@@ -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}")
+49
View File
@@ -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,
+2
View File
@@ -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
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#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);
+53
View File
@@ -25,6 +25,7 @@
#include "dusk/logging.h"
#include "helpers/string.hpp"
#if TARGET_PC
#include "dusk/mods/svc/stage.hpp"
#include <format>
#include <fmt/ranges.h>
#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<dStage_dt_c*>(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<int>(stage->getRoomNo());
if (size > sizeof(stage_actor_data_class)) {
appen->scale = object.scale;
}
dStage_actorCreate(
reinterpret_cast<stage_actor_data_class*>(&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);
}
+1
View File
@@ -212,6 +212,7 @@ void ModLoader::init_services() {
&svc::g_windowModule,
&svc::g_gfxModule,
&svc::g_saveModule,
&svc::g_stageModule,
})
{
svc::register_module(*module);
+1
View File
@@ -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
+323
View File
@@ -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 <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
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<uint8_t> record;
};
std::unordered_map<std::string, std::vector<ActorEditRecord>> s_edits;
uint64_t s_nextHandle = 1;
uint64_t s_nextSeq = 1;
std::unordered_set<uint32_t> 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<ActorEditRecord>* 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<uint8_t>(roomNo);
if (roomNo == -1) {
room = static_cast<uint8_t>(dComIfGp_getStartStageRoomNo());
}
const auto layer = static_cast<int8_t>(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<int8_t>(dComIfG_play_c::getLayerNo(0));
for (const auto& record : *edits) {
if (record.kind == EditKind::Add && record.mod->active &&
record.room == static_cast<uint8_t>(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<const uint8_t*>(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<const uint8_t*>(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
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstddef>
#include <cstdint>
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