From 5a92780d42401e08e3ad60f2f4619509f52350b8 Mon Sep 17 00:00:00 2001 From: jdflyer Date: Thu, 3 Sep 2026 10:02:15 -0600 Subject: [PATCH] Mod SDK: Actor service (#2270) * Mod SDK: Actor Service * Modding doc fix * Update naming for actor service. Begin custom actor example mod * Example Mod Updates, On unregister: Only delete requested actors of the right type. Update docs * Cleanup & fixes * Rename to custom_actor_demo * tweaks * Finish mine demo actor --------- Co-authored-by: MelonSpeedruns Co-authored-by: Luke Street --- CMakeLists.txt | 1 + docs/modding.md | 76 ++++ files.cmake | 1 + include/f_op/f_op_actor_mng.h | 4 +- include/f_pc/f_pc_deletor.h | 1 + mods/custom_actor_demo/CMakeLists.txt | 21 + mods/custom_actor_demo/mod.json | 7 + mods/custom_actor_demo/res/.gitkeep | 0 mods/custom_actor_demo/src/m_a_mine.cpp | 227 +++++++++++ mods/custom_actor_demo/src/m_a_mine.hpp | 43 ++ mods/custom_actor_demo/src/m_a_obj_wrock.cpp | 195 +++++++++ mods/custom_actor_demo/src/m_a_obj_wrock.hpp | 39 ++ mods/custom_actor_demo/src/mod.cpp | 85 ++++ sdk/include/mods/svc/actor.h | 102 +++++ src/d/d_stage.cpp | 18 + src/dusk/mods/svc/actor.cpp | 404 +++++++++++++++++++ src/dusk/mods/svc/actor.hpp | 24 ++ src/dusk/mods/svc/registry.cpp | 1 + src/dusk/mods/svc/registry.hpp | 1 + src/dusk/mods/svc/slot_map.hpp | 5 + src/f_pc/f_pc_profile.cpp | 20 +- 21 files changed, 1271 insertions(+), 4 deletions(-) create mode 100644 mods/custom_actor_demo/CMakeLists.txt create mode 100644 mods/custom_actor_demo/mod.json create mode 100644 mods/custom_actor_demo/res/.gitkeep create mode 100644 mods/custom_actor_demo/src/m_a_mine.cpp create mode 100644 mods/custom_actor_demo/src/m_a_mine.hpp create mode 100644 mods/custom_actor_demo/src/m_a_obj_wrock.cpp create mode 100644 mods/custom_actor_demo/src/m_a_obj_wrock.hpp create mode 100644 mods/custom_actor_demo/src/mod.cpp create mode 100644 sdk/include/mods/svc/actor.h create mode 100644 src/dusk/mods/svc/actor.cpp create mode 100644 src/dusk/mods/svc/actor.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f06ae435f..95eff8fbfb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -571,6 +571,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR add_subdirectory(mods/shadow_mod) add_subdirectory(mods/window_demo) add_subdirectory(mods/flow_demo) + add_subdirectory(mods/custom_actor_demo) add_subdirectory(mods/cosmetics) add_subdirectory(mods/randomizer) endif () diff --git a/docs/modding.md b/docs/modding.md index 9adb92f8f6..d8cc8c9d75 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -1136,6 +1136,82 @@ svc_game_mode->register_game_mode(mod_ctx, &gameModeDesc); ``` +### ActorService (`mods/svc/actor.h`) + +A service that manages registering and creating custom actors. These actors will be run by the game as if they are part of the engine. These actors can be created by the game either by its 16-bit actor name, or a 7-character long name that can +be loaded by a stage. + +```cpp +#include "mods/svc/actor.h" +IMPORT_SERVICE(ActorService, svc_actor); + +class myActor_c : public fopAc_ac_c {}; + +int myActor_Create(void* i_this) { + // Ran several times, until it returns cPhs_COMPLEATE_e to allow for async loading + return cPhs_COMPLEATE_e; +} + +int myActor_Delete(void* i_this) { + // Free resources here + return 1; +} + +int myActor_Execute(void* i_this) { + // Ran once per game tick + return 1; +} + +int myActor_IsDelete(void* i_this) { + // Returns 1 when the actor can be deleted + return 1; +} + +int myActor_Draw(void* i_this) { + // Code to draw the actor + return 1; +} + +s16 actor_name; // The process name that can be used by the game to load the actor +ActorHandle actor_handle; +ActorProfileDesc profDesc = { + .name = "AUnique", // The name used by the stage loader to load the actor with. + // It has a character limit of 7 and must be unique among active + // mod actors. Matching a game actor name overrides stage lookup. + .priority_group = 7, // When, relative to other actors _Execute should run + // See: mods/svc/actor.h + .process_size = sizeof(myActor_c), + .draw_priority = fpcDwPi_OBJ_LBOX_e, // Defines when the actor should be drawn relative + // to other actors (see f_pc_draw_priority.h) + .status = fopAcStts_CULL_e | fopAcStts_UNK_0x4000_e | fopAcStts_UNK_0x40000_e, + .group = fopAc_ACTOR_e, // Can be fopAc_ACTOR_e, fopAc_PLAYER_e, fopAc_ENEMY_e, or fopAc_NPC_e + .cull_type = fopAc_CULLBOX_CUSTOM_e, + .create_function = myActor_Create, + .delete_function = myActor_Delete, + .execute_function = myActor_Execute, + .is_delete_function = myActor_IsDelete, + .draw_function = myActor_Draw, +}; +svc_actor->register_actor(mod_ctx, &profDesc, &actor_name, &actor_handle); + +// Spawn the actor at the player's position +fopAc_ac_c* plr = dComIfGp_getPlayer(0); +if (plr) { + ActorSpawnParams spawnParams = { + .parameters = 0, + .argument = 0, + .room_num = fopAcM_GetRoomNo(plr), + .position = {plr->current.pos.x, plr->current.pos.y, plr->current.pos.z}, + .angle = {plr->current.angle.x, plr->current.angle.y, plr->current.angle.z}, + .scale = {1.0f, 1.0f, 1.0f} + }; + ActorId created_actor_id; + svc_actor->create_actor(mod_ctx, actor_name, &spawnParams, &created_actor_id); +} +``` + +See `mods/custom_actor_demo` for a more complete example. + --- ## Hooking Game Functions diff --git a/files.cmake b/files.cmake index 38087f28b8..3296b53064 100644 --- a/files.cmake +++ b/files.cmake @@ -1490,6 +1490,7 @@ set(DUSK_FILES src/dusk/mods/log_buffer.hpp src/dusk/mods/manifest.cpp src/dusk/mods/manifest.hpp + src/dusk/mods/svc/actor.cpp src/dusk/mods/svc/camera.cpp src/dusk/mods/svc/config.cpp src/dusk/mods/svc/config.hpp diff --git a/include/f_op/f_op_actor_mng.h b/include/f_op/f_op_actor_mng.h index 7aa3bf2156..5cf357b778 100644 --- a/include/f_op/f_op_actor_mng.h +++ b/include/f_op/f_op_actor_mng.h @@ -23,12 +23,10 @@ #endif #define fopAcM_ct(ptr, ClassName) \ - if ((ptr)->layer_tag.layer == NULL) { OSPanic(__FILE__, __LINE__, "UH OH"); } \ if (!fopAcM_CheckCondition(ptr, fopAcCnd_INIT_e)) { \ fopAcM_ct_placement(ptr, ClassName); \ fopAcM_OnCondition(ptr, fopAcCnd_INIT_e); \ - } \ - if ((ptr)->layer_tag.layer == NULL) { OSPanic(__FILE__, __LINE__, "Oh come on"); } + } #define fopAcM_RegisterDeleteID(i_this, actor_name_str) \ diff --git a/include/f_pc/f_pc_deletor.h b/include/f_pc/f_pc_deletor.h index c0b7da0fc2..1667303fc4 100644 --- a/include/f_pc/f_pc_deletor.h +++ b/include/f_pc/f_pc_deletor.h @@ -7,6 +7,7 @@ typedef struct base_process_class base_process_class; BOOL fpcDt_IsComplete(); +int fpcDt_deleteMethod(base_process_class* i_proc); int fpcDt_ToDeleteQ(base_process_class* i_proc); int fpcDt_ToQueue(base_process_class* i_proc); void fpcDt_Handler(); diff --git a/mods/custom_actor_demo/CMakeLists.txt b/mods/custom_actor_demo/CMakeLists.txt new file mode 100644 index 0000000000..d7e55536d4 --- /dev/null +++ b/mods/custom_actor_demo/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.25) +project(custom_actor_demo CXX) +set(CMAKE_CXX_STANDARD 20) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root") + option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + if (DUSK_MOD_USE_FULL_TREE) + add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL) + else () + add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL) + endif () +endif () + +add_mod(custom_actor_demo + FEATURES game fmt + SOURCES src/mod.cpp src/m_a_obj_wrock.cpp src/m_a_mine.cpp + MOD_JSON mod.json + RES_DIR res +) diff --git a/mods/custom_actor_demo/mod.json b/mods/custom_actor_demo/mod.json new file mode 100644 index 0000000000..55f57c264d --- /dev/null +++ b/mods/custom_actor_demo/mod.json @@ -0,0 +1,7 @@ +{ + "id": "dev.twilitrealm.custom_actor_demo", + "name": "[Demo] Custom Actor", + "version": "1.0.0", + "author": "Twilit Realm", + "description": "A mod to demonstrate how to create and use custom actors. Spawns a \"wrock\" in South Faron Woods." +} diff --git a/mods/custom_actor_demo/res/.gitkeep b/mods/custom_actor_demo/res/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mods/custom_actor_demo/src/m_a_mine.cpp b/mods/custom_actor_demo/src/m_a_mine.cpp new file mode 100644 index 0000000000..fd7fa77b65 --- /dev/null +++ b/mods/custom_actor_demo/src/m_a_mine.cpp @@ -0,0 +1,227 @@ +#include "m_a_mine.hpp" +#include "d/d_com_inf_game.h" +#include "res/Object/O_mD_jira.h" + +// The name of the archive in /res/Object/ +static const char* l_resName = "O_mD_jira"; + +// The actor's heap (for resources) should be enough to hold the data for the model and collision +static constexpr u32 heap_size = ALIGN_NEXT(16832, 0x20); + +ma_Mine_c::~ma_Mine_c() { + // Called every time the actor is deleted + + // Delete the sound object + mSound.deleteObject(); + + // Request to unload the archive (the data acts as a shared pointer, and only gets deleted when + // the reference counter goes to zero) + dComIfG_resDelete(&mPhase, l_resName); +} + +cPhs_Step ma_Mine_c::create() { + // Because of how the actor system works, an actor's constructor doesn't get called when an + // actor is created. We need to manually do it here with the following ma: + fopAcM_ct(this, ma_Mine_c); + + // The create function gets called until we return cPhs_COMPLEATE_e while an actor is loading. + // We request to load the archive here, and wait until the dvd completes loading it + cPhs_Step step = dComIfG_resLoad(&mPhase, l_resName); + if (step == cPhs_COMPLEATE_e) { + // Initialize the solid heap for the actor's resources, if needed + if (!fopAcM_entrySolidHeap(this, createHeapCallBack, heap_size)) { + return cPhs_ERROR_e; + } + + // Setup our collision sphere and register it to the world + // Set a circle "wall" of 30 units around the actor + mAcchCir.SetWall(30.0f, 30.0f); + mAcch.Set(this, 1, &mAcchCir); + mAcch.ClrWaterNone(); + mAcch.SetRoofCrrHeight(60.0f); + mAcch.SetWaterCheckOffset(10000.0f); + mAcch.SetWtrChkMode(2); + mAcch.OnLineCheck(); + + mCcStts.Init(30, 0xFF, this); + + // Collision Sphere for the actor (copied from Bomb Actor) + static const dCcD_SrcSph + l_sphSrc = {.mObjInf = + { + .mObj = {.mFlags = 0x0, + .mSrcObjHitInf = {.mObjAt = {.mType = AT_TYPE_BOMB, + .mAtp = 0x4, + .mBase = {.mSPrm = 0x1e}}, + .mObjTg = {.mType = 0xd8fbffef, .mBase = {.mSPrm = 0x11}}, + .mObjCo = {.mBase = {.mSPrm = 0x79}}}}, + .mGObjAt{.mSe = dCcD_SE_NONE, + .mHitMark = 0x0, + .mSpl = 0x1, + .mMtrl = 0x0, + .mBase = {.mGFlag = 0x0}}, + .mGObjTg{.mSe = dCcD_SE_NONE, + .mHitMark = 0x0, + .mSpl = 0x0, + .mMtrl = 0x0, + .mBase = {.mGFlag = 0x4}}, + .mGObjCo{.mBase = {.mGFlag = 0x0}}, + }, + .mSphAttr = {.mSph = {.mCenter = {0.0f, 0.0f, 0.0f}, .mRadius = 80.0f}}}; + + mCollisionSphere.Set(l_sphSrc); + mCollisionSphere.SetStts(&mCcStts); + + // We register a callback anytime the actor is hit (both attacks and is hit) + mCollisionSphere.SetAtHitCallback(atHitCallback); + mCollisionSphere.SetTgHitCallback(atHitCallback); + mCollisionSphere.OffTgSetBit(); + mCollisionSphere.OffCoSetBit(); + mCollisionSphere.OnAtSetBit(); // Enable the attack sphere + + // Set the initial matrix and cull box for the actor + fopAcM_SetMtx(this, mpModel->getBaseTRMtx()); + fopAcM_SetMin(this, -36.0f, 0.0f, -36.0f); + fopAcM_SetMax(this, 36.0f, 66.0f, 36.0f); + + // Call execute so the actor's information in the world can be updated + Execute(); + } + return step; +} + +// Initializes the heap that all instances of this actor will use for resources +// Gets called from the callback in fopAcM_entrySolidHeap +int ma_Mine_c::CreateHeap() { + // Get the bmd data from the archive and initialize it + J3DModelData* model_data = + (J3DModelData*)dComIfG_getObjectRes(l_resName, dRes_INDEX_O_MD_JIRA_BMD_O_MD_JIRAI_e); + if (model_data == NULL) { + return 0; + } + mpModel = mDoExt_J3DModel__create(model_data, 0x80000, 0x11000084); + if (mpModel == NULL) { + return 0; + } + + // Create the sound object the actor will use + mSound.init(¤t.pos, 1); + + return 1; +} + +int ma_Mine_c::createHeapCallBack(fopAc_ac_c* i_this) { + return static_cast(i_this)->CreateHeap(); +} + +int ma_Mine_c::Delete() { + // Call the destructor anytime we delete the actor + this->~ma_Mine_c(); + return 1; +} + +int ma_Mine_c::Execute() { + // Update collision with the world + mAcch.CrrPos(dComIfG_Bgsp()); + mCollisionSphere.SetC(attention_info.position); + dComIfG_Ccsp()->Set(&mCollisionSphere); + + // Get the collision below the actor + cBgS_GndChk groundChunk = mAcch.m_gnd; + f32 groundH = mAcch.GetGroundH(); + if (groundH != -G_CM3D_F_INF) { + // Set the actor's environment colors to match the room's current ones + int roomNo = dComIfG_Bgsp().GetRoomId(groundChunk); + tevStr.YukaCol = dComIfG_Bgsp().GetPolyColor(groundChunk); + tevStr.room_no = roomNo; + + // Set the actor's room to be where it is sitting + mCcStts.SetRoomId(roomNo); + fopAcM_SetRoomNo(this, roomNo); + + // Get the reverb info here that the actor can use when exploding + mReverb = dComIfGp_getReverb(roomNo); + } + + // Update the model's transformation matrix to match the actor's transform + mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z); + mDoMtx_stack_c::ZXYrotM(shape_angle); + mDoMtx_stack_c::scaleM(scale); + mpModel->setBaseTRMtx(mDoMtx_stack_c::get()); + + // Set any attention flags (if needed) + eyePos = attention_info.position = current.pos; + attention_info.flags = 0; + return 1; +} + +int ma_Mine_c::Draw() { + // Update the model's lighting with the scene + g_env_light.settingTevStruct(0x20, ¤t.pos, &tevStr); + g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); + + // Set the bmd model to be drawn when the display list is executed + mDoExt_modelUpdateDL(mpModel); + + return 1; +} + +void ma_Mine_c::atHit(dCcD_GObjInf* i_atObjInf) { + // Create particles with these IDs at the actor's position + static const u16 normalNameID[] = { + 0x161, 0x162, 0x163, 0x164, 0x165, 0x166, 0x167, 0x168, 0x1EC}; + for (int i = 0; i < ARRAY_SIZE(normalNameID); i++) { + dComIfGp_particle_setColor(normalNameID[i], ¤t.pos, &tevStr, NULL, NULL, 0.0f, 0xFF, + &shape_angle, &scale, NULL, -1, NULL); + } + + // Create an explosion sound + mSound.startSound(Z2SE_OBJ_BOMB_EXPLODE, 0, mReverb); + + // Vibrate the controller + dComIfGp_getVibration().StartShock(4, 31, cXyz(0.0f, 1.0f, 0.0f)); + + // Request to delete the actor so it disappears + fopAcM_delete(this); +} + +void ma_Mine_c::atHitCallback(fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf, + fopAc_ac_c* i_atActor, dCcD_GObjInf* i_atObjInf) { + // This callback gets triggered anytime an intersection happens with the object's collision sphere + ((ma_Mine_c*)i_tgActor)->atHit(i_atObjInf); +} + +static cPhs_Step ma_Mine_create(void* i_this) { + return static_cast(i_this)->create(); +} + +static int maMine_Delete(void* i_this) { + return static_cast(i_this)->Delete(); +} + +static int maMine_Execute(void* i_this) { + return static_cast(i_this)->Execute(); +} + +static int maMine_Draw(void* i_this) { + return static_cast(i_this)->Draw(); +} + +static int maMine_IsDelete(void*) { + return 1; +} + +s16 ma_Mine_c::sProcName = -1; +ActorHandle ma_Mine_c::sActorHandle = -1; +const ActorProfileDesc ma_Mine_c::sProfile = {.name = MA_MINE_NAME, + .priority_group = 7, + .process_size = sizeof(ma_Mine_c), + .draw_priority = fpcDwPi_OBJ_LBOX_e, // An unused draw priority + .status = fopAcStts_UNK_0x40000_e | fopAcStts_UNK_0x4000_e | fopAcStts_CULL_e, + .group = fopAc_ACTOR_e, + .cull_type = fopAc_CULLBOX_CUSTOM_e, + .create_function = ma_Mine_create, + .delete_function = maMine_Delete, + .execute_function = maMine_Execute, + .is_delete_function = maMine_IsDelete, + .draw_function = maMine_Draw}; diff --git a/mods/custom_actor_demo/src/m_a_mine.hpp b/mods/custom_actor_demo/src/m_a_mine.hpp new file mode 100644 index 0000000000..147938473f --- /dev/null +++ b/mods/custom_actor_demo/src/m_a_mine.hpp @@ -0,0 +1,43 @@ +#pragma once +#include "mods/svc/actor.h" + +// Base actor class definitions +#include "f_op/f_op_actor.h" + +// Definitions for request_of_phase_process_class and cPhs_Step +#include "SSystem/SComponent/c_phase.h" + +// Definitions for collision +#include "d/d_bg_s_acch.h" +#include "d/d_bg_w.h" +#include "d/d_cc_d.h" + +#define MA_MINE_NAME "m_mine" + +class ma_Mine_c : public fopAc_ac_c { +public: + request_of_phase_process_class mPhase; + J3DModel* mpModel; + dBgS_ObjAcch mAcch; + dBgS_AcchCir mAcchCir; + Mtx mColliderMtx; + dCcD_Stts mCcStts; + dCcD_Sph mCollisionSphere; + Z2SoundObjSimple mSound; + s8 mReverb; + + virtual ~ma_Mine_c(); + cPhs_Step create(); + int CreateHeap(); + int Delete(); + int Execute(); + int Draw(); + void atHit(dCcD_GObjInf* i_atObjInf); + static int createHeapCallBack(fopAc_ac_c*); + static void atHitCallback(fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf, + fopAc_ac_c* i_atActor, dCcD_GObjInf* i_atObjInf); + + static s16 sProcName; + static ActorHandle sActorHandle; + static const ActorProfileDesc sProfile; +}; diff --git a/mods/custom_actor_demo/src/m_a_obj_wrock.cpp b/mods/custom_actor_demo/src/m_a_obj_wrock.cpp new file mode 100644 index 0000000000..bb40d10cf3 --- /dev/null +++ b/mods/custom_actor_demo/src/m_a_obj_wrock.cpp @@ -0,0 +1,195 @@ +/* + * m_a_obj_wrock.cpp + * An example actor for a rock that can be placed in the world. + */ + +#include "m_a_obj_wrock.hpp" +#include "d/d_com_inf_game.h" +#include "res/Object/WRock.h" + +// The name of the archive in /res/Object/ +static const char* l_resName = "Wrock"; + +// The actor's heap (for resources) should be enough to hold the data for the model and collision +static constexpr u32 heap_size = ALIGN_NEXT(13952, 0x20) + ALIGN_NEXT(1920, 0x20); + +maObj_Wrock_c::~maObj_Wrock_c() { + // Called every time the actor is deleted + + // Remove the collider from the world's collision + if (mpCollider != NULL) { + dComIfG_Bgsp().Release(mpCollider); + } + + // Request to unload the archive (the data acts as a shared pointer, and only gets deleted when + // the reference counter goes to zero) + dComIfG_resDelete(&mPhase, l_resName); +} + +cPhs_Step maObj_Wrock_c::create() { + // Because of how the actor system works, an actor's constructor doesn't get called when an + // actor is created. We need to manually do it here with the following macro: + fopAcM_ct(this, maObj_Wrock_c); + + // The create function gets called until we return cPhs_COMPLEATE_e while an actor is loading. + // We request to load the archive here, and wait until the dvd completes loading it + cPhs_Step step = dComIfG_resLoad(&mPhase, l_resName); + if (step == cPhs_COMPLEATE_e) { + // Initialize the solid heap for the actor's resources, if needed + if (!fopAcM_entrySolidHeap(this, createHeapCallBack, heap_size)) { + return cPhs_ERROR_e; + } + + // Register the actor's collider to the current world's collision + if (mpCollider != NULL) { + if (dComIfG_Bgsp().Regist(mpCollider, this) == true) { + return cPhs_ERROR_e; + } + } + + // Set the initial matrix and cull box for the actor + fopAcM_SetMtx(this, mpModel->getBaseTRMtx()); + fopAcM_setCullSizeBox(this, -400.0f, -400.0f, -400.0f, 400.0f, 400.0f, 400.0f); + + // Setup collision info (will be used in execute) + mAcch.Set(¤t.pos, &old.pos, this, 1, &mAcchCir, &speed, ¤t.angle, &shape_angle); + + // Call execute so the actor's information in the world can be updated + Execute(); + } + return step; +} + +// Initializes the heap that all instances of this actor will use for resources +// Gets called from the callback in fopAcM_entrySolidHeap +int maObj_Wrock_c::CreateHeap() { + // Get the bmd data from the archive and initialize it + J3DModelData* model_data = + (J3DModelData*)dComIfG_getObjectRes(l_resName, dRes_INDEX_WROCK_BMD_WROCK_e); + if (model_data == NULL) { + return 0; + } + mpModel = mDoExt_J3DModel__create(model_data, 0x80000, 0x11000084); + if (mpModel == NULL) { + return 0; + } + + // Get the dzb collision data from the archive and initialize it + mpCollider = JKR_NEW dBgW(); + if (mpCollider == NULL) { + return 0; + } + cBgD_t* dzb = (cBgD_t*)dComIfG_getObjectRes(l_resName, dRes_INDEX_WROCK_DZB_WROCK_e); + if (mpCollider->Set(dzb, 1, &mColliderMtx) == true) { + return 0; + } + mpCollider->SetCrrFunc(dBgS_MoveBGProc_Typical); + return 1; +} + +int maObj_Wrock_c::createHeapCallBack(fopAc_ac_c* i_this) { + return static_cast(i_this)->CreateHeap(); +} + +int maObj_Wrock_c::Delete() { + // Call the destructor anytime we delete the actor + this->~maObj_Wrock_c(); + return 1; +} + +int maObj_Wrock_c::Execute() { + // Update collision with the world + mAcch.CrrPos(dComIfG_Bgsp()); + + // Get the collision below the actor + mGndChk = mAcch.m_gnd; + mGroundH = mAcch.GetGroundH(); + if (mGroundH != -G_CM3D_F_INF) { + // Set the actor's environment colors to match the room's current ones + tevStr.YukaCol = dComIfG_Bgsp().GetPolyColor(mGndChk); + tevStr.room_no = dComIfG_Bgsp().GetRoomId(mGndChk); + + // Set the actor's room to be where it is sitting + fopAcM_SetRoomNo(this, dComIfG_Bgsp().GetRoomId(mGndChk)); + } + + // Update the model's transformation matrix to match the actor's transform + mDoMtx_stack_c::transS(current.pos.x, current.pos.y, current.pos.z); + mDoMtx_stack_c::ZXYrotM(shape_angle); + mDoMtx_stack_c::scaleM(scale); + mpModel->setBaseTRMtx(mDoMtx_stack_c::get()); + + // Copy the model's transformation matrix to the collider's transformation matrix and update the + // collider + if (mpCollider != NULL) { + PSMTXCopy(mpModel->getBaseTRMtx(), mColliderMtx); + mpCollider->Move(); + } + + // Set any attention flags (if needed) + eyePos = attention_info.position = current.pos; + attention_info.flags = 0; + return 1; +} + +int maObj_Wrock_c::Draw() { + // Update the model's lighting with the scene + g_env_light.settingTevStruct(0x20, ¤t.pos, &tevStr); + g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); + + // Set the current dlist to BG Which means that shadows can be cast on it + // Because of the messy collider, the shadows don't look great, but this is here as an example + dComIfGd_setListBG(); + + // Set the bmd model to be drawn when the display list is executed + mDoExt_modelUpdateDL(mpModel); + + // Cast a shadow for the actor onto the ground. + // We can only do this if we are drawing to the normal dlist, not the BG dlist + // if (mGroundH != -G_CM3D_F_INF) { + // mShadow = dComIfGd_setShadow(mShadow, 1, mpModel, ¤t.pos, + // 2000.0f, 0.0f, + // current.pos.y, mGroundH, mGndChk, &tevStr, 0, + // 1.0f, &dDlst_shadowControl_c::mSimpleTexObj); + // } + + // Reset the active dlist + dComIfGd_setList(); + + return 1; +} + +static cPhs_Step maObj_Wrock_Create(void* i_this) { + return static_cast(i_this)->create(); +} + +static int maObj_Wrock_Delete(void* i_this) { + return static_cast(i_this)->Delete(); +} + +static int maObj_Wrock_Execute(void* i_this) { + return static_cast(i_this)->Execute(); +} + +static int maObj_Wrock_Draw(void* i_this) { + return static_cast(i_this)->Draw(); +} + +static int maObj_Wrock_IsDelete(void*) { + return 1; +} + +s16 maObj_Wrock_c::sProcName = -1; +ActorHandle maObj_Wrock_c::sActorHandle = -1; +const ActorProfileDesc maObj_Wrock_c::sProfile = {.name = MAOBJ_WROCK_NAME, + .priority_group = 7, + .process_size = sizeof(maObj_Wrock_c), + .draw_priority = fpcDwPi_OBJ_LBOX_e, // An unused draw priority + .status = fopAcStts_UNK_0x40000_e | fopAcStts_UNK_0x4000_e | fopAcStts_CULL_e, + .group = fopAc_ACTOR_e, + .cull_type = fopAc_CULLBOX_CUSTOM_e, + .create_function = maObj_Wrock_Create, + .delete_function = maObj_Wrock_Delete, + .execute_function = maObj_Wrock_Execute, + .is_delete_function = maObj_Wrock_IsDelete, + .draw_function = maObj_Wrock_Draw}; diff --git a/mods/custom_actor_demo/src/m_a_obj_wrock.hpp b/mods/custom_actor_demo/src/m_a_obj_wrock.hpp new file mode 100644 index 0000000000..1e18d36f5f --- /dev/null +++ b/mods/custom_actor_demo/src/m_a_obj_wrock.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "mods/svc/actor.h" + +// Base actor class definitions +#include "f_op/f_op_actor.h" + +// Definitions for request_of_phase_process_class and cPhs_Step +#include "SSystem/SComponent/c_phase.h" + +// Definitions for collision +#include "d/d_bg_s_acch.h" +#include "d/d_bg_w.h" + +#define MAOBJ_WROCK_NAME "wrock" + +class maObj_Wrock_c : public fopAc_ac_c { +public: + request_of_phase_process_class mPhase; + J3DModel* mpModel; + dBgS_ObjAcch mAcch; + cBgS_GndChk mGndChk; + dBgS_AcchCir mAcchCir; + Mtx mColliderMtx; + dBgW* mpCollider; + f32 mGroundH; + int mShadow; + + virtual ~maObj_Wrock_c(); + cPhs_Step create(); + int CreateHeap(); + int Delete(); + int Execute(); + int Draw(); + static int createHeapCallBack(fopAc_ac_c*); + + static s16 sProcName; + static ActorHandle sActorHandle; + static const ActorProfileDesc sProfile; +}; diff --git a/mods/custom_actor_demo/src/mod.cpp b/mods/custom_actor_demo/src/mod.cpp new file mode 100644 index 0000000000..971ad6e4f0 --- /dev/null +++ b/mods/custom_actor_demo/src/mod.cpp @@ -0,0 +1,85 @@ +#include "mods/service.hpp" +#include "mods/svc/actor.h" +#include "mods/svc/log.hpp" +#include "mods/svc/stage.h" + +#include "d/d_com_inf_game.h" + +#include "m_a_mine.hpp" +#include "m_a_obj_wrock.hpp" + +#include + +DEFINE_MOD(); +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(ActorService, svc_actor); +IMPORT_SERVICE(StageService, svc_stage); + +extern "C" { +MOD_EXPORT ModResult mod_initialize(ModError*) { + if (svc_actor->register_actor(mod_ctx, &maObj_Wrock_c::sProfile, &maObj_Wrock_c::sProcName, + &maObj_Wrock_c::sActorHandle) != MOD_OK) + { + mods::log::error("Failed to register actor wrock!"); + return MOD_ERROR; + } + + const stage_actor_data_class wrockParams{ + .name = MAOBJ_WROCK_NAME, + .base = + { + .parameters = 0, + .position = {-14324.0f, 0.0f, 341.0f}, + .angle = {0, -16595, 0}, + .setID = 0xFFFF, + }, + }; + if (svc_stage->add_actor( + mod_ctx, "F_SP108", 0, -1, &wrockParams, sizeof(wrockParams), nullptr) != MOD_OK) + { + mods::log::error("Adding wrock to F_SP108 Failed!"); + return MOD_ERROR; + } + + if (svc_actor->register_actor(mod_ctx, &ma_Mine_c::sProfile, &ma_Mine_c::sProcName, + &ma_Mine_c::sActorHandle) != MOD_OK) + { + mods::log::error("Failed to register actor " MA_MINE_NAME); + return MOD_ERROR; + } + + static const std::array minePositions = { + {{-14445.0f, 11.0f, 1304.0f}, {-14557.0f, 7.0f, 990.0f}, {-14790.0f, 7.0f, 634.0f}, + {-14829.0f, 0.0f, 144.0f}, {-14615.0f, 0.0f, -170.0f}, {-14283.0f, 10.0f, -420.0f}}}; + for (const auto& pos : minePositions) { + const stage_actor_data_class mineParams{ + .name = MA_MINE_NAME, + .base = + { + .parameters = 0, + .position = {pos.x, pos.y, pos.z}, + .angle = {0x2000, (s16)(pos.x*100000), 0}, // Adjusted and seemingly random angle + .setID = 0xFFFF, + }, + }; + if (svc_stage->add_actor( + mod_ctx, "F_SP108", 0, -1, &mineParams, sizeof(mineParams), nullptr) != MOD_OK) + { + mods::log::error("Adding mine to F_SP108 Failed!"); + return MOD_ERROR; + } + } + + mods::log::info("custom_actor_demo initialized"); + return MOD_OK; +} + +MOD_EXPORT ModResult mod_update(ModError*) { + return MOD_OK; +} + +MOD_EXPORT ModResult mod_shutdown(ModError*) { + mods::log::info("custom_actor_demo shutdown"); + return MOD_OK; +} +} diff --git a/sdk/include/mods/svc/actor.h b/sdk/include/mods/svc/actor.h new file mode 100644 index 0000000000..5876ddcaf2 --- /dev/null +++ b/sdk/include/mods/svc/actor.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +#define ACTOR_SERVICE_ID "dev.twilitrealm.dusklight.actor" +#define ACTOR_SERVICE_MAJOR 1u +#define ACTOR_SERVICE_MINOR 0u + +typedef int16_t ProfileName; +typedef uint32_t ActorId; +typedef uint64_t ActorHandle; + +typedef struct { + const char name[8]; // Canonical stage name. Must be unique among active mod actors. Matching a + // game actor name intentionally overrides stage lookup for that actor. + + uint16_t priority_group; /* priorityGroup is the priority for when execute will be called on the + actor. Here are the main groups: + 0: The room manager actor + 1: Game scenes + 2: Various room change actors + 3: Most objects to be executed before Link + 4: Some bosses, canoe, epona, spinner, chests + 5: Link's actor + 6: Boomerang, midna + 7: Most objects, actors, bosses, triggers to be executed after link (most actors go here) + 8: Various objects and enemies + 9: Various actors + 10: Timer, Scene Exit actor + 11: Grass, Suspend Actors + */ + + size_t process_size; // Size of the actor class (use sizeof(my_actor_class)) + int16_t draw_priority; // an enum value that is prefixed with fpcDwPi. Select an existing value + // from the fpcDwPi to pick a draw priority matching the actor you wish + // to match priorities with. + uint32_t status; // Flags from fopAc_Status_e enum (all have fopAcStts_UNK_0x40000_e, a lot + // have fopAcStts_UNK_0x4000_e, add fopAcStts_CULL_e to enable culling) + uint8_t group; // The actor type. An enum value from fopAc_Group_e (fopAc_ACTOR_e, + // fopAc_ENEMY_e, fopAc_NPC_e) + uint8_t cull_type; // Enum value from fopAc_Cull_e + int (*create_function)( + void*); // Called after the actor is spawned, return type is a enum value of cPhs_Step + int (*delete_function)(void*); // Releases resources; returns 1 when deletion is complete + int (*execute_function)(void*); // Called once per game tick, the actor's priorityGroup + // determines when it will run relative to other actors + int (*is_delete_function)(void*); // Returns 1 when normal deletion may begin + int (*draw_function)(void*); // Called to draw the actor +} ActorProfileDesc; + +typedef struct { + uint32_t parameters; // The parameters to be passed to the actor + int8_t argument; // The argument to be passed to the actor (acts as an extra byte for a parameter) + int8_t room_num; // The room to spawn the actor in + struct { + float x; + float y; + float z; + } position; + struct { + int16_t x; + int16_t y; + int16_t z; + } angle; + struct { + float x; + float y; + float z; + } scale; + int (*create_function)(void*); // Optional: A custom function to run when the actor is created. +} ActorSpawnParams; + +typedef struct ActorService { + ServiceHeader header; + ModResult (*register_actor)(ModContext* ctx, const ActorProfileDesc* desc, + ProfileName* outProfileName, ActorHandle* outActorHandle); + ModResult (*unregister_actor)(ModContext* ctx, ActorHandle handle); + ModResult (*create_actor_from_name)( + ModContext* ctx, const char* name, const ActorSpawnParams* params, ActorId* outId); + ModResult (*create_actor)( + ModContext* ctx, ProfileName name, const ActorSpawnParams* params, ActorId* outId); + ModResult (*create_child_actor_from_name)(ModContext* ctx, const char* name, ActorId parentID, + const ActorSpawnParams* params, ActorId* outId); + ModResult (*create_child_actor)(ModContext* ctx, ProfileName name, ActorId parentID, + const ActorSpawnParams* params, ActorId* outId); + ModResult (*get_actor_id)(ModContext* ctx, ProfileName name, ActorId* outId); + ModResult (*get_actor_room_num)(ModContext* ctx, ActorId actorId, int8_t* outRoomNum); + /* Returns MOD_UNAVAILABLE if the actor cannot be queued for deletion immediately. */ + ModResult (*delete_actor)(ModContext* ctx, ActorId actorId); +} ActorService; + +#ifdef __cplusplus +#include "mods/service.hpp" + +template <> +struct mods::ServiceTraits { + static constexpr const char* id = ACTOR_SERVICE_ID; + static constexpr uint16_t major_version = ACTOR_SERVICE_MAJOR; + static constexpr uint16_t minor_version = ACTOR_SERVICE_MINOR; +}; +#endif diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index 6055d8a9ae..0de9a13f75 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -28,6 +28,7 @@ #include "dusk/mods/svc/stage.hpp" #include #include +#include "dusk/mods/svc/actor.hpp" #endif void dStage_nextStage_c::set(const char* i_stage, s8 i_roomId, s16 i_point, s8 i_layer, s8 i_wipe, @@ -1524,6 +1525,14 @@ static void dummy0() { } dStage_objectNameInf* dStage_searchName(char const* objName) { +#if TARGET_PC + dStage_objectNameInf* info = + dusk::mods::svc::actor_impl::get_stageinfo_from_full_name(objName); + if (info != nullptr) { + return info; + } +#endif + dStage_objectNameInf* obj = l_objectName; for (u32 i = 0; i < ARRAY_SIZEU(l_objectName); i++) { @@ -1561,6 +1570,15 @@ dStage_objectNameInf* dStage_searchNameCI(char const* objName) { const char* dStage_getName(s16 procName, s8 argument) { static char tmp_name[dStage_NAME_LENGTH]; +#if TARGET_PC + const char* name = dusk::mods::svc::actor_impl::get_full_name_from_proc_name(procName); + if (name[0] != '\0') { + strncpy(tmp_name, name, sizeof(tmp_name) - 1); + tmp_name[sizeof(tmp_name) - 1] = '\0'; + return tmp_name; + } +#endif + dStage_objectNameInf* obj = l_objectName; char* tmp = NULL; diff --git a/src/dusk/mods/svc/actor.cpp b/src/dusk/mods/svc/actor.cpp new file mode 100644 index 0000000000..237181e827 --- /dev/null +++ b/src/dusk/mods/svc/actor.cpp @@ -0,0 +1,404 @@ +#include "mods/svc/actor.h" +#include "dusk/mods/svc/actor.hpp" + +#include "config.hpp" +#include "registry.hpp" +#include "slot_map.hpp" + +#include "aurora/lib/logging.hpp" +#include "dusk/mod_loader.hpp" +#include "dusk/mods/loader/loader.hpp" + +#include + +#include "d/d_stage.h" +#include "f_op/f_op_actor_tag.h" +#include "f_pc/f_pc_deletor.h" + +#include +#include + +namespace dusk::mods::svc::actor_impl { +namespace { + +aurora::Module Log("dusk::mods::actor"); + +SlotMap> s_slots; +std::unordered_map procNameToHandle; +std::unordered_map fullNameToHandle; + +ActorSlot* get_actor_slot(void* actorPtr) { + auto* actor = static_cast(actorPtr); + const auto handle = procNameToHandle.find(actor->name); + if (handle == procNameToHandle.end()) { + return nullptr; + } + auto* slot = s_slots.find(handle->second); + return slot != nullptr ? slot->value.get() : nullptr; +} + +int actor_is_delete(void* actorPtr) { + auto* slot = get_actor_slot(actorPtr); + if (slot == nullptr || slot->forceDelete) { + return 1; + } + return slot->isDeleteFunction(actorPtr); +} + +int actor_delete(void* actorPtr) { + auto* slot = get_actor_slot(actorPtr); + if (slot == nullptr) { + return 1; + } + const bool forceDelete = slot->forceDelete; + const int result = slot->deleteFunction(actorPtr); + return forceDelete ? 1 : result; +} + +ModResult register_actor(ModContext* ctx, const ActorProfileDesc* desc, ProfileName* outProfileName, + ActorHandle* outActorHandle) { + auto* owner = mod_from_context(ctx); + if (owner == nullptr || desc == nullptr || outProfileName == nullptr || + outActorHandle == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + if (desc->name[0] == '\0' || std::memchr(desc->name, '\0', sizeof(desc->name)) == nullptr || + desc->process_size < sizeof(fopAc_ac_c) || desc->create_function == nullptr || + desc->delete_function == nullptr || desc->execute_function == nullptr || + desc->is_delete_function == nullptr || desc->draw_function == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + + // If another mod has already registered an actor with this name, note a conflict + if (get_stageinfo_from_full_name(desc->name)) { + return MOD_CONFLICT; + } + + const auto handle = s_slots.emplace(*owner, + std::make_unique( + ActorSlot{{desc->create_function, actor_delete, desc->execute_function, actor_is_delete, + desc->draw_function}, + desc->delete_function, desc->is_delete_function, false, + { + {}, // Set after registered + 0, // Set after registered to a slot + -1 // Use the default argument + }, + { + /* Layer ID */ fpcLy_CURRENT_e, + /* List ID */ desc->priority_group, + /* List Prio */ fpcPi_CURRENT_e, // Always fpcPi_CURRENT_e + /* Proc Name */ 0, + /* Proc SubMtd */ &g_fpcLf_Method.base, // usually: &g_fpcLf_Method.base + /* Size */ (u32)desc->process_size, + /* Size Other */ 0, // Always 0 + /* Parameters */ 0, // Always 0 + /* Leaf SubMtd */ &g_fopAc_Method.base, // usually &g_fopAc_Method.base + /* Draw Prio */ desc->draw_priority, + /* Actor SubMtd */ nullptr, // set this later + /* Status */ desc->status, + /* Group */ desc->group, + /* Cull Type */ desc->cull_type, + }})); + + s32 procNameFull = fpcNm_MAX_NUM + s_slots.index_of(handle); + if (procNameFull >= 0x7FFF) { + s_slots.erase(handle); + Log.error("Hit registered actor limit (0x7FFF) while registering actor {}", desc->name); + return MOD_ERROR; + } + + ActorSlot& slot = *s_slots.find(handle)->value.get(); + strncpy(slot.objNameInf.name, desc->name, sizeof(slot.objNameInf.name) - 1); + slot.profile.sub_method = &slot.methodTable; + s16 procName = (s16)procNameFull; + procNameToHandle[procName] = handle; + fullNameToHandle[std::string(slot.objNameInf.name)] = handle; + slot.objNameInf.procname = (s16)procName; + slot.profile.base.base.name = procName; + *outProfileName = procName; + *outActorHandle = handle; + + return MOD_OK; +} + +// Must be called before the slot associated with the handle is erased +void remove_handle_from_maps(ActorHandle handle) { + auto entry = s_slots.find(handle); + if (entry == nullptr) { + return; + } + + std::string fullName = entry->value->objNameInf.name; + + const auto it = fullNameToHandle.find(fullName); + if (it != fullNameToHandle.end()) { + fullNameToHandle.erase(it); + } + + const auto it2 = procNameToHandle.find(entry->value->profile.base.base.name); + if (it2 != procNameToHandle.end()) { + procNameToHandle.erase(it2); + } +} + +void request_delete_all_actors_of_name(s16 procName) { + node_class* node = g_fopAcTg_Queue.mpHead; + while (node != nullptr) { + node_class* next = NODE_GET_NEXT(node); + auto* actor = + static_cast(reinterpret_cast(node)->mpTagData); + if (actor->name == procName) { + fopAcM_delete(actor); + } + node = next; + } +} + +bool finish_delete_all_actors_of_name(s16 procName) { + bool complete = true; + + node_class* node = g_fpcDtTg_Queue.mpHead; + while (node != nullptr) { + node_class* next = NODE_GET_NEXT(node); + auto* tag = reinterpret_cast(node); + auto* proc = static_cast(tag->base.mpTagData); + if (proc->name == procName) { + tag->timer = 0; + const int deleteStatus = fpcDtTg_Do( + tag, [](void* proc) { return fpcDt_deleteMethod((base_process_class*)proc); }); + if (deleteStatus == 0) { + complete = false; + } + } + node = next; + } + return complete; +} + +bool has_actor_of_name(s16 procName) { + node_class* node = g_fopAcTg_Queue.mpHead; + while (node != nullptr) { + auto* actor = + static_cast(reinterpret_cast(node)->mpTagData); + if (actor->name == procName) { + return true; + } + node = NODE_GET_NEXT(node); + } + return false; +} + +ModResult unregister_actor(ModContext* ctx, ActorHandle handle) { + auto* owner = mod_from_context(ctx); + if (owner == nullptr) { + return MOD_INVALID_ARGUMENT; + } + auto* slot = s_slots.find_owned(handle, *owner); + if (slot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + + const s16 actorProcName = slot->value->profile.base.base.name; + slot->value->forceDelete = true; + request_delete_all_actors_of_name(actorProcName); + const bool deletesFinished = finish_delete_all_actors_of_name(actorProcName); + if (!deletesFinished || has_actor_of_name(actorProcName)) { + slot->value->forceDelete = false; + Log.warn("Actor profile '{}' could not be unregistered synchronously", + slot->value->objNameInf.name); + return MOD_UNAVAILABLE; + } + + remove_handle_from_maps(handle); + s_slots.erase_owned(handle, *owner); + + return MOD_OK; +} + +ModResult create_actor( + ModContext* ctx, ProfileName name, const ActorSpawnParams* params, ActorId* outId) { + cXyz pos = {params->position.x, params->position.y, params->position.z}; + csXyz angle = {params->angle.x, params->angle.y, params->angle.z}; + cXyz scale = {params->scale.x, params->scale.y, params->scale.z}; + fpc_ProcID id = fopAcM_create(name, 0xFFFF, params->parameters, &pos, params->room_num, &angle, + &scale, params->argument, params->create_function); + + if (id == fpcM_ERROR_PROCESS_ID_e) { + Log.error("Error Creating Actor with profile name {}", name); + return MOD_ERROR; + } + + if (outId) { + *outId = id; + } + return MOD_OK; +} + +ModResult create_actor_from_name( + ModContext* ctx, const char* name, const ActorSpawnParams* params, ActorId* outId) { + dStage_objectNameInf* objectName = dStage_searchName(name); + if (objectName == nullptr) { + Log.error("Attempted to create actor ({}) but it can't be found!", name); + return MOD_ERROR; + } + + ActorSpawnParams copy = *params; + copy.argument = objectName->argument; + + return create_actor(ctx, objectName->procname, ©, outId); +} + +ModResult create_child_actor(ModContext* ctx, ProfileName name, ActorId parentID, + const ActorSpawnParams* params, ActorId* outId) { + cXyz pos = {params->position.x, params->position.y, params->position.z}; + csXyz angle = {params->angle.x, params->angle.y, params->angle.z}; + cXyz scale = {params->scale.x, params->scale.y, params->scale.z}; + fpc_ProcID id = fopAcM_createChild(name, parentID, params->parameters, &pos, params->room_num, + &angle, &scale, params->argument, params->create_function); + + if (id == fpcM_ERROR_PROCESS_ID_e) { + Log.error("Error Creating Actor with profile name {} as child of actor with id {}", name, + parentID); + return MOD_ERROR; + } + + if (outId) { + *outId = id; + } + return MOD_OK; +} + +ModResult create_child_actor_from_name(ModContext* ctx, const char* name, ActorId parentID, + const ActorSpawnParams* params, ActorId* outId) { + dStage_objectNameInf* objectName = dStage_searchName(name); + if (objectName == nullptr) { + Log.error("Attempted to create actor ({}) but it can't be found!", name); + return MOD_ERROR; + } + + ActorSpawnParams copy = *params; + copy.argument = objectName->argument; + + return create_child_actor(ctx, objectName->procname, parentID, ©, outId); +} + +ModResult get_actor_id(ModContext* ctx, ProfileName name, ActorId* outId) { + fopAc_ac_c* actor = fopAcM_SearchByName(name); + if (actor == nullptr) { + Log.error("Attempting to get actor by profile name ({}) but it doesn't exist!", name); + return MOD_ERROR; + } + + *outId = fopAcM_GetID(actor); + return MOD_OK; +} + +ModResult get_actor_room_num(ModContext* ctx, ActorId actorId, int8_t* outRoomNum) { + fopAc_ac_c* actor = fopAcM_SearchByID(actorId); + if (actor == nullptr) { + Log.error("Attempted to get room number of actor Id ({}) but it doesn't exist!", actorId); + return MOD_ERROR; + } + + *outRoomNum = fopAcM_GetRoomNo(actor); + return MOD_OK; +} + +ModResult delete_actor(ModContext* ctx, ActorId actorId) { + if (mod_from_context(ctx) == nullptr) { + return MOD_INVALID_ARGUMENT; + } + fopAc_ac_c* actor = fopAcM_SearchByID(actorId); + if (actor == nullptr) { + Log.warn("Attempted to delete actor with ID ({}) but it doesn't exist!", actorId); + return MOD_OK; + } + return fopAcM_delete(actor) != 0 ? MOD_OK : MOD_UNAVAILABLE; +} + +} // namespace + +process_profile_definition* get_profile_from_proc_name(s16 name) { + const auto& it = procNameToHandle.find(name); + if (it == procNameToHandle.end()) { + return nullptr; + } + auto entry = s_slots.find(it->second); + if (entry) { + return &entry->value->profile.base.base; + } + return nullptr; +} + +const char* get_full_name_from_proc_name(s16 name) { + const auto& it = procNameToHandle.find(name); + if (it == procNameToHandle.end()) { + return ""; + } + auto entry = s_slots.find(it->second); + if (entry) { + return entry->value->objNameInf.name; + } + return ""; +} + +dStage_objectNameInf* get_stageinfo_from_full_name(const std::string& name) { + const auto& it = fullNameToHandle.find(name); + if (it == fullNameToHandle.end()) { + return nullptr; + } + auto entry = s_slots.find(it->second); + if (entry) { + return &entry->value->objNameInf; + } + return nullptr; +} + +void actor_remove_mod(LoadedMod& mod) { + std::vector handles; + s_slots.for_each([&](const ActorHandle handle, const auto& slot) { + if (slot.owner == &mod) { + handles.push_back(handle); + } + }); + + for (const auto handle : handles) { + const auto result = unregister_actor(mod.context.get(), handle); + if (result != MOD_OK) { + Log.error("Actor profile could not be removed during mod deactivation"); + } + } +} + +} // namespace dusk::mods::svc::actor_impl + +namespace dusk::mods::svc { +namespace { + +constexpr ActorService s_actorService{ + .header = SERVICE_HEADER(ActorService, ACTOR_SERVICE_MAJOR, ACTOR_SERVICE_MINOR), + .register_actor = actor_impl::register_actor, + .unregister_actor = actor_impl::unregister_actor, + .create_actor_from_name = actor_impl::create_actor_from_name, + .create_actor = actor_impl::create_actor, + .create_child_actor_from_name = actor_impl::create_child_actor_from_name, + .create_child_actor = actor_impl::create_child_actor, + .get_actor_id = actor_impl::get_actor_id, + .get_actor_room_num = actor_impl::get_actor_room_num, + .delete_actor = actor_impl::delete_actor, +}; + +} + +constinit const ServiceModule g_actorModule{ + .id = ACTOR_SERVICE_ID, + .majorVersion = ACTOR_SERVICE_MAJOR, + .minorVersion = ACTOR_SERVICE_MINOR, + .service = &s_actorService, + .modDeactivating = actor_impl::actor_remove_mod, +}; + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/actor.hpp b/src/dusk/mods/svc/actor.hpp new file mode 100644 index 0000000000..7e68760503 --- /dev/null +++ b/src/dusk/mods/svc/actor.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include "f_op/f_op_actor_mng.h" + +namespace dusk::mods::svc::actor_impl { + +struct ActorSlot { + actor_method_class methodTable; + int (*deleteFunction)(void*); + int (*isDeleteFunction)(void*); + bool forceDelete; + dStage_objectNameInf objNameInf; + actor_process_profile_definition profile; +}; + +process_profile_definition* get_profile_from_proc_name(s16 name); + +dStage_objectNameInf* get_stageinfo_from_full_name(const std::string& name); + +const char* get_full_name_from_proc_name(s16 name); // Returns "" when not found + +}; // namespace dusk::mods::svc::actor_impl diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 13c042af3d..a26e2971dc 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -231,6 +231,7 @@ void ModLoader::init_services() { &svc::g_flowModule, &svc::g_messageModule, &svc::g_gamemodeModule, + &svc::g_actorModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 68ec9353cd..6f501c3d99 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -88,5 +88,6 @@ extern const ServiceModule g_itemModule; extern const ServiceModule g_flowModule; extern const ServiceModule g_messageModule; extern const ServiceModule g_gamemodeModule; +extern const ServiceModule g_actorModule; } // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/slot_map.hpp b/src/dusk/mods/svc/slot_map.hpp index 3e45635d79..a97a4b80a5 100644 --- a/src/dusk/mods/svc/slot_map.hpp +++ b/src/dusk/mods/svc/slot_map.hpp @@ -122,6 +122,11 @@ public: } } + // Returns the index of the handle within the slot map. + static constexpr uint32_t index_of(Handle handle) { + return handle_index(handle); + } + private: struct Slot { uint32_t generation = 1; diff --git a/src/f_pc/f_pc_profile.cpp b/src/f_pc/f_pc_profile.cpp index 4828ca4433..34eafc1701 100644 --- a/src/f_pc/f_pc_profile.cpp +++ b/src/f_pc/f_pc_profile.cpp @@ -5,18 +5,36 @@ #include "f_pc/f_pc_profile.h" +#if TARGET_PC +#include "dusk/mods/svc/actor.hpp" +#include "f_pc/f_pc_name.h" +#endif #ifndef __MWERKS__ // Forward declare the static list from f_pc_profile_lst.cpp DUSK_GAME_EXTERN process_profile_definition DUSK_CONST* DUSK_CONST g_fpcPfLst_ProfileList[]; // On PC: Direct pointer to static array -DUSK_GAME_DATA process_profile_definition DUSK_CONST* DUSK_CONST* DUSK_CONST g_fpcPf_ProfileList_p = g_fpcPfLst_ProfileList; +DUSK_GAME_DATA process_profile_definition DUSK_CONST* DUSK_CONST* DUSK_CONST g_fpcPf_ProfileList_p = + g_fpcPfLst_ProfileList; #else // On Console: Pointer initialized by REL module prolog process_profile_definition** g_fpcPf_ProfileList_p; #endif process_profile_definition DUSK_CONST* fpcPf_Get(s16 i_profname) { +#if TARGET_PC + // Check if a mod has registered an actor with i_profname. Fallback to the profile list if it + // doesn't exist. + process_profile_definition* profile = + dusk::mods::svc::actor_impl::get_profile_from_proc_name(i_profname); + if (profile != nullptr) { + return profile; + } + if (i_profname < 0 || i_profname >= fpcNm_MAX_NUM) { + return nullptr; + } +#endif + int index = i_profname; return g_fpcPf_ProfileList_p[index]; }