Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions files.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,9 @@ set(DUSK_FILES
src/dusk/mods/manifest.cpp
src/dusk/mods/manifest.hpp
src/dusk/mods/svc/actor.cpp
src/dusk/mods/svc/actor_attribute.cpp
src/dusk/mods/svc/actor_attribute.hpp
src/dusk/mods/svc/actor_attribute_helpers.hpp
src/dusk/mods/svc/audio_res/audio_res.hpp
src/dusk/mods/svc/audio_res/audio_res.cpp
src/dusk/mods/svc/audio_res/bst.cpp
Expand Down
65 changes: 65 additions & 0 deletions sdk/include/mods/svc/actor_attribute.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#pragma once

#include <mods/api.h>

#ifdef __cplusplus
#include <mods/service.hpp>
#endif

#define ACTOR_ATTRIBUTE_SERVICE_ID "dev.twilitrealm.dusklight.actor_attribute"
#define ACTOR_ATTRIBUTE_SERVICE_MAJOR 1u
#define ACTOR_ATTRIBUTE_SERVICE_MINOR 5u

/* 0 is never a valid handle. */
typedef uint64_t ActorAttributeResolverHandle;

/*
* Semantic actor values exposed by the host. A game integration should pass a
* freshly calculated vanilla value immediately before it is consumed. Mods
* may then transform that value without replacing the owning actor function.
*/
typedef enum ActorAttribute {
ACTOR_ATTRIBUTE_MOVEMENT_SPEED = 0,
ACTOR_ATTRIBUTE_SIZE = 1,
ACTOR_ATTRIBUTE_HEALTH = 2,
ACTOR_ATTRIBUTE_ATTACK_DAMAGE = 3,
/* Per-frame downward acceleration magnitude, not a stored actor field. */
ACTOR_ATTRIBUTE_GRAVITY = 4,
/* Enemy awareness/aggro distance, separate from physical reach. */
ACTOR_ATTRIBUTE_NOTICE_RANGE = 5,
/* Horizontal distance Link is pushed when this actor damages him. */
ACTOR_ATTRIBUTE_PLAYER_KNOCKBACK = 6,
/* Duration of a genuine living-enemy stun/vulnerability window. */
ACTOR_ATTRIBUTE_STUN_DURATION = 7,
} ActorAttribute;

/* Host-owned callback data, valid only for the duration of the callback. */
typedef struct ActorAttributeInfo {
const void* actor; /* const fopAc_ac_c* */
ActorAttribute attribute;
float vanilla_value;
float current_value;
} ActorAttributeInfo;

/*
* Return true and write out_value to replace current_value, or return false to
* leave it unchanged. Resolvers are chained in mod load order.
*/
typedef bool (*ActorAttributeResolveFn)(ModContext* ctx, const ActorAttributeInfo* info,
float* out_value, void* user_data);

typedef struct ActorAttributeService {
ServiceHeader header;

ModResult (*register_resolver)(ModContext* ctx, ActorAttributeResolveFn fn, void* user_data,
ActorAttributeResolverHandle* out_handle);

ModResult (*unregister_resolver)(ModContext* ctx, ActorAttributeResolverHandle handle);

/* An actor-specific mod hook asks for the currently resolved semantic
* value where it is actually used. No actor-source switch is required. */
float (*resolve)(const void* actor, ActorAttribute attribute, float vanilla_value);
} ActorAttributeService;

MOD_DECLARE_SERVICE(ActorAttributeService, svc_actor_attribute, ACTOR_ATTRIBUTE_SERVICE_ID,
ACTOR_ATTRIBUTE_SERVICE_MAJOR, ACTOR_ATTRIBUTE_SERVICE_MINOR);
173 changes: 173 additions & 0 deletions src/dusk/mods/svc/actor_attribute.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include "actor_attribute.hpp"

#include "registry.hpp"

#include "dusk/mods/loader/loader.hpp"

#include <algorithm>
#include <cmath>
#include <exception>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>

namespace dusk::mods::svc {
namespace {

struct Resolver {
ActorAttributeResolverHandle handle = 0;
ActorAttributeResolveFn fn = nullptr;
void* userData = nullptr;
};

struct PendingResolver {
LoadedMod* owner = nullptr;
ActorAttributeResolveFn fn = nullptr;
void* userData = nullptr;
};

std::unordered_map<LoadedMod*, std::vector<Resolver>> s_resolvers;
ActorAttributeResolverHandle s_nextHandle = 1;

bool valid_attribute(const ActorAttribute attribute) {
return attribute >= ACTOR_ATTRIBUTE_MOVEMENT_SPEED &&
attribute <= ACTOR_ATTRIBUTE_STUN_DURATION;
}

ModResult register_resolver(ModContext* context, ActorAttributeResolveFn fn, void* userData,
ActorAttributeResolverHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}

auto* owner = mod_from_context(context);
if (owner == nullptr || fn == nullptr) {
return MOD_INVALID_ARGUMENT;
}
if (s_nextHandle == 0 || s_nextHandle == std::numeric_limits<uint64_t>::max()) {
return MOD_UNAVAILABLE;
}

const ActorAttributeResolverHandle handle = s_nextHandle++;
s_resolvers[owner].push_back(Resolver{
.handle = handle,
.fn = fn,
.userData = userData,
});

if (outHandle != nullptr) {
*outHandle = handle;
}
return MOD_OK;
}

ModResult unregister_resolver(
ModContext* context, const ActorAttributeResolverHandle handle) {
auto* owner = mod_from_context(context);
if (owner == nullptr || handle == 0) {
return MOD_INVALID_ARGUMENT;
}

const auto it = s_resolvers.find(owner);
if (it == s_resolvers.end()) {
return MOD_UNSUPPORTED;
}

auto& resolvers = it->second;
const auto oldSize = resolvers.size();
std::erase_if(resolvers, [handle](const Resolver& resolver) {
return resolver.handle == handle;
});

const bool removed = resolvers.size() != oldSize;
if (resolvers.empty()) {
s_resolvers.erase(it);
}
return removed ? MOD_OK : MOD_UNSUPPORTED;
}

constexpr ActorAttributeService s_actorAttributeService{
.header = SERVICE_HEADER(ActorAttributeService, ACTOR_ATTRIBUTE_SERVICE_MAJOR,
ACTOR_ATTRIBUTE_SERVICE_MINOR),
.register_resolver = register_resolver,
.unregister_resolver = unregister_resolver,
.resolve = [](const void* actor, ActorAttribute attribute, float vanillaValue) {
return resolve_actor_attribute(static_cast<const fopAc_ac_c*>(actor),
attribute, vanillaValue);
},
};

} // namespace

float resolve_actor_attribute(
const fopAc_ac_c* actor, const ActorAttribute attribute, const float vanillaValue) {
if (actor == nullptr || !valid_attribute(attribute) || s_resolvers.empty()) {
return vanillaValue;
}

/* A callback may unregister itself, so snapshot the active chain first. */
std::vector<PendingResolver> pending;
for (auto& mod : ModLoader::instance().mods()) {
if (!mod.active) {
continue;
}

const auto it = s_resolvers.find(&mod);
if (it == s_resolvers.end()) {
continue;
}

for (const Resolver& resolver : it->second) {
pending.push_back(PendingResolver{
.owner = &mod,
.fn = resolver.fn,
.userData = resolver.userData,
});
}
}

ActorAttributeInfo info{
.actor = actor,
.attribute = attribute,
.vanilla_value = vanillaValue,
.current_value = vanillaValue,
};

for (const PendingResolver& resolver : pending) {
if (!resolver.owner->active) {
continue;
}

float candidate = info.current_value;
try {
if (resolver.fn(resolver.owner->context.get(), &info, &candidate,
resolver.userData) &&
std::isfinite(candidate))
{
info.current_value = candidate;
}
} catch (const std::exception& exception) {
fail_mod(*resolver.owner, MOD_ERROR,
std::string{"Exception in actor attribute resolver: "} + exception.what());
} catch (...) {
fail_mod(*resolver.owner, MOD_ERROR, "Unknown exception in actor attribute resolver");
}
}

return info.current_value;
}

constinit const ServiceModule g_actorAttributeModule{
.id = ACTOR_ATTRIBUTE_SERVICE_ID,
.majorVersion = ACTOR_ATTRIBUTE_SERVICE_MAJOR,
.minorVersion = ACTOR_ATTRIBUTE_SERVICE_MINOR,
.service = &s_actorAttributeService,
.modDetached = [](LoadedMod& mod) { s_resolvers.erase(&mod); },
.shutdown = []() {
s_resolvers.clear();
s_nextHandle = 1;
},
};

} // namespace dusk::mods::svc
14 changes: 14 additions & 0 deletions src/dusk/mods/svc/actor_attribute.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include "mods/svc/actor_attribute.h"

class fopAc_ac_c;

namespace dusk::mods::svc {

/* Host-side entry point used at semantic value-consumption sites. */
float resolve_actor_attribute(
const fopAc_ac_c* actor, ActorAttribute attribute, float vanillaValue);


} // namespace dusk::mods::svc
Loading
Loading