diff --git a/docs/modding.md b/docs/modding.md index 73457ae7fa..ab100b5add 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -633,6 +633,9 @@ if (svc_camera->get_camera(mod_ctx, game_view, &camera) == MOD_OK) { first in-game frame. Projection matrices match the renderer's WebGPU clip convention and renderer depth convention (reversed-Z by default). +Camera operators allow overriding the main camera. When an operator callback returns true, its values replace the camera +state for the current frame. Register and unregister using `register_camera_operator` / `unregister_camera_operator`. + --- ## Hooking Game Functions diff --git a/sdk/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h index 8934340cd6..1dccecfba6 100644 --- a/sdk/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -8,7 +8,7 @@ #define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera" #define CAMERA_SERVICE_MAJOR 1u -#define CAMERA_SERVICE_MINOR 0u +#define CAMERA_SERVICE_MINOR 1u /* * Snapshot of a game camera for the frame currently being recorded. @@ -46,6 +46,37 @@ typedef struct CameraInfo { #define CAMERA_INFO_INIT {sizeof(CameraInfo)} +/* 0 is never a valid handle. */ +typedef uint64_t CameraOperatorHandle; + +typedef struct CameraOperatorState { + uint32_t struct_size; + + /* Host inputs. */ + uint64_t frame_counter; + uint64_t ticks; + float aspect; + + /* Initial camera state and callback output. */ + float eye[3]; + float center[3]; + float fovy; + float bank_degrees; +} CameraOperatorState; + +/* Return true to use state for the current frame. Game thread only. */ +typedef bool (*CameraOperateFn)(ModContext* ctx, CameraOperatorState* state, void* user_data); + +typedef struct CameraOperatorDesc { + uint32_t struct_size; + const char* debug_name; + int32_t priority; + CameraOperateFn operate; + void* user_data; +} CameraOperatorDesc; + +#define CAMERA_OPERATOR_DESC_INIT {sizeof(CameraOperatorDesc), NULL, 0, NULL, NULL} + typedef struct CameraService { ServiceHeader header; @@ -55,6 +86,17 @@ typedef struct CameraService { * perspective camera. */ ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info); + + /* Minor version 1 */ + + /* + * Register an operator for the main camera. Operators run by descending priority, then + * registration order, until one returns true. debug_name and operate must be set; debug_name + * is copied. out_handle must not be NULL. + */ + ModResult (*register_camera_operator)( + ModContext* ctx, const CameraOperatorDesc* desc, CameraOperatorHandle* out_handle); + ModResult (*unregister_camera_operator)(ModContext* ctx, CameraOperatorHandle handle); } CameraService; MOD_DECLARE_SERVICE( diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 888f9d9ccf..7dc4fd21ce 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -29,6 +29,7 @@ #endif #if TARGET_PC +#include "dusk/camera_operators.hpp" #include "dusk/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/action_bindings.h" @@ -1053,7 +1054,7 @@ void dCamera_c::debugDrawInit() { bool dCamera_c::Run() { #if TARGET_PC ResetView(); - if (executeDebugFlyCam()) { + if (executeDebugFlyCam() || dusk::mods::camera_run_operators(this)) { mFrameCounter++; mTicks++; return true; diff --git a/src/dusk/camera_operators.hpp b/src/dusk/camera_operators.hpp new file mode 100644 index 0000000000..c4ba650dfd --- /dev/null +++ b/src/dusk/camera_operators.hpp @@ -0,0 +1,9 @@ +#pragma once + +class dCamera_c; + +namespace dusk::mods { + +bool camera_run_operators(dCamera_c* camera); + +} // namespace dusk::mods diff --git a/src/dusk/mods/svc/camera.cpp b/src/dusk/mods/svc/camera.cpp index 37057e2397..60fbf42499 100644 --- a/src/dusk/mods/svc/camera.cpp +++ b/src/dusk/mods/svc/camera.cpp @@ -1,13 +1,24 @@ #include "registry.hpp" +#include "slot_map.hpp" +#include "dusk/camera_operators.hpp" #include "dusk/mods/loader/loader.hpp" #include "mods/svc/camera.h" +#include "SSystem/SComponent/c_angle.h" +#include "d/d_camera.h" +#include "d/d_com_inf_game.h" +#include "f_op/f_op_camera_mng.h" #include "f_op/f_op_view.h" #include "m_Do/m_Do_mtx.h" +#include #include #include +#include +#include +#include +#include namespace dusk::mods::svc { namespace { @@ -117,9 +128,125 @@ ModResult camera_get_camera_from_view( return snapshot_view(static_cast(gameView), outInfo); } +struct CameraOperator { + std::string debugName; + int32_t priority = 0; + uint64_t sequence = 0; + CameraOperateFn operate = nullptr; + void* userData = nullptr; +}; + +SlotMap sOperators; +uint64_t sNextOperatorSequence = 0; + +ModResult camera_register_operator( + ModContext* context, const CameraOperatorDesc* desc, CameraOperatorHandle* outHandle) { + if (outHandle != nullptr) { + *outHandle = 0; + } + + auto* mod = mod_from_context(context); + if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(CameraOperatorDesc) || + desc->debug_name == nullptr || desc->debug_name[0] == '\0' || desc->operate == nullptr || + outHandle == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + + *outHandle = sOperators.emplace(*mod, CameraOperator{ + .debugName = desc->debug_name, + .priority = desc->priority, + .sequence = sNextOperatorSequence++, + .operate = desc->operate, + .userData = desc->user_data, + }); + return MOD_OK; +} + +ModResult camera_unregister_operator(ModContext* context, CameraOperatorHandle handle) { + auto* mod = mod_from_context(context); + if (mod == nullptr || handle == 0) { + return MOD_INVALID_ARGUMENT; + } + return sOperators.erase_owned(handle, *mod) ? MOD_OK : MOD_INVALID_ARGUMENT; +} + +CameraOperatorState make_operator_state(const dCamera_c& camera) { + return CameraOperatorState{ + .struct_size = sizeof(CameraOperatorState), + .frame_counter = camera.mFrameCounter, + .ticks = camera.mTicks, + .aspect = mDoGph_gInf_c::getAspect(), + .eye = {camera.mEye.x, camera.mEye.y, camera.mEye.z}, + .center = {camera.mCenter.x, camera.mCenter.y, camera.mCenter.z}, + .fovy = camera.mFovy, + .bank_degrees = camera.mBank.Degree(), + }; +} + +bool run_operators(dCamera_c* camera) { + auto* mainCamera = dComIfGp_getCamera(0); + if (camera == nullptr || mainCamera == nullptr || &mainCamera->mCamera != camera) { + return false; + } + + struct PendingOperator { + CameraOperatorHandle handle; + CameraOperator value; + LoadedMod* owner; + }; + std::vector operators; + sOperators.for_each([&](CameraOperatorHandle handle, const auto& entry) { + operators.push_back({.handle = handle, .value = entry.value, .owner = entry.owner}); + }); + std::ranges::sort(operators, [](const PendingOperator& lhs, const PendingOperator& rhs) { + if (lhs.value.priority != rhs.value.priority) { + return lhs.value.priority > rhs.value.priority; + } + return lhs.value.sequence < rhs.value.sequence; + }); + + const auto initialState = make_operator_state(*camera); + for (const auto& entry : operators) { + if (!entry.owner->active || sOperators.find(entry.handle) == nullptr) { + continue; + } + + auto state = initialState; + bool useState = false; + try { + useState = + entry.value.operate(entry.owner->context.get(), &state, entry.value.userData); + } catch (const std::exception& e) { + fail_mod(*entry.owner, MOD_ERROR, + fmt::format( + "exception in camera operator '{}': {}", entry.value.debugName, e.what())); + } catch (...) { + fail_mod(*entry.owner, MOD_ERROR, + fmt::format("unknown exception in camera operator '{}'", entry.value.debugName)); + } + + if (!useState) { + continue; + } + + camera->Reset(cXyz{state.center[0], state.center[1], state.center[2]}, + cXyz{state.eye[0], state.eye[1], state.eye[2]}, state.fovy, + cAngle::Degree_to_SAngle(state.bank_degrees)); + return true; + } + return false; +} + +void camera_mod_detached(LoadedMod& mod) { + sOperators.erase_all(mod); +} + constexpr CameraService s_cameraService{ .header = SERVICE_HEADER(CameraService, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR), .get_camera = camera_get_camera_from_view, + .register_camera_operator = camera_register_operator, + .unregister_camera_operator = camera_unregister_operator, }; } // namespace @@ -129,6 +256,11 @@ constinit const ServiceModule g_cameraModule{ .majorVersion = CAMERA_SERVICE_MAJOR, .minorVersion = CAMERA_SERVICE_MINOR, .service = &s_cameraService, + .modDetached = camera_mod_detached, }; } // namespace dusk::mods::svc + +bool dusk::mods::camera_run_operators(dCamera_c* camera) { + return svc::run_operators(camera); +}