Add camera operator API to CameraService

This commit is contained in:
Luke Street
2026-08-07 00:34:57 -06:00
parent 2c7ab755a0
commit c5933be631
5 changed files with 189 additions and 2 deletions
+3
View File
@@ -625,6 +625,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
+43 -1
View File
@@ -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(
+2 -1
View File
@@ -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;
+9
View File
@@ -0,0 +1,9 @@
#pragma once
class dCamera_c;
namespace dusk::mods {
bool camera_run_operators(dCamera_c* camera);
} // namespace dusk::mods
+132
View File
@@ -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 <algorithm>
#include <aurora/gfx.hpp>
#include <cstring>
#include <exception>
#include <fmt/format.h>
#include <string>
#include <vector>
namespace dusk::mods::svc {
namespace {
@@ -117,9 +128,125 @@ ModResult camera_get_camera_from_view(
return snapshot_view(static_cast<const view_class*>(gameView), outInfo);
}
struct CameraOperator {
std::string debugName;
int32_t priority = 0;
uint64_t sequence = 0;
CameraOperateFn operate = nullptr;
void* userData = nullptr;
};
SlotMap<CameraOperator> 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<PendingOperator> 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);
}