载入中...
搜索中...
未找到
ActionNotifyRegistry.cpp
浏览该文件的文档.
2
12
13#include "common/Capability.h"
14
15#include <algorithm>
16#include <map>
17#include <set>
18#include <utility>
19
20namespace eve::action {
21namespace {
22
23bool validType(std::string_view type) { return LogicalId::parse(type).has_value(); }
24
25class ActionCameraCueHandler final : public IActionNotifyHandler {
26public:
27 Result<void> handle(const ActionTimelineEvent& event, const ActionNotifyContext& context) override {
28 if (event.kind != ActionTimelineEventKind::Notify)
29 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "camera cue must be instantaneous", "event.kind"));
30 auto binding = ActionCameraCueBinding::fromPayload(event.payload);
31 if (!binding) return Result<void>::failure(binding.status());
32 std::optional<Result<void>> result;
33 cap::forEachUntil<IActionCameraCueSink>([&](IActionCameraCueSink* sink) {
34 if (!sink->supports(binding.value().cue)) return false;
35 result.emplace(sink->trigger(binding.value(), context));
36 return true;
37 });
38 if (result) return std::move(*result);
39 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "no camera sink accepts the action cue", binding.value().cue.format()));
40 }
41};
42
43class ActionDamageHandler final : public IActionNotifyHandler {
44public:
45 Result<void> handle(const ActionTimelineEvent& event, const ActionNotifyContext& context) override {
46 if (event.kind != ActionTimelineEventKind::Notify)
47 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "damage block must be instantaneous", "event.kind"));
48 auto binding = ActionDamageBinding::fromPayload(event.payload);
49 if (!binding) return Result<void>::failure(binding.status());
50 if (binding.value().targetIndex >= context.targets.size())
51 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "damage target index is unavailable", "targetIndex"));
52 std::optional<Result<void>> result;
53 cap::forEachUntil<IActionDamageSink>([&](IActionDamageSink* sink) {
54 if (!sink->supports(context.targets[binding.value().targetIndex])) return false;
55 result.emplace(sink->apply(binding.value(), context));
56 return true;
57 });
58 if (result) return std::move(*result);
59 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "no damage sink accepts the action target", "target"));
60 }
61};
62
63class ActionGameplayEventHandler final : public IActionNotifyHandler {
64public:
65 Result<void> handle(const ActionTimelineEvent& event, const ActionNotifyContext& context) override {
66 if (event.kind != ActionTimelineEventKind::Notify)
67 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "gameplay event block must be instantaneous", "event.kind"));
69 if (!binding) return Result<void>::failure(binding.status());
70 std::optional<Result<void>> result;
71 cap::forEachUntil<IActionGameplayEventSink>([&](IActionGameplayEventSink* sink) {
72 result.emplace(sink->emit(binding.value(), context));
73 return true;
74 });
75 if (result) return std::move(*result);
76 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "no gameplay event sink is registered", binding.value().tag));
77 }
78};
79
80class ActionStateWindowHandler final : public IActionNotifyHandler {
81public:
82 Result<void> handle(const ActionTimelineEvent& event, const ActionNotifyContext& context) override {
84 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "state window requires enter or exit", "event.kind"));
85 auto binding = ActionStateWindowBinding::fromPayload(event.type.format(), event.payload);
86 if (!binding) return Result<void>::failure(binding.status());
87 std::optional<Result<void>> result;
88 cap::forEachUntil<IActionStateWindowSink>([&](IActionStateWindowSink* sink) {
89 if (!sink->supports(binding.value().kind)) return false;
90 result.emplace(event.kind == ActionTimelineEventKind::StateEnter
91 ? sink->enter(binding.value(), event, context)
92 : sink->exit(binding.value(), event, context));
93 return true;
94 });
95 if (result) return std::move(*result);
96 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "no state-window sink accepts the action block", event.type.format()));
97 }
98};
99
100class ActionParameterCurveHandler final : public IActionNotifyHandler {
101public:
102 Result<void> handle(const ActionTimelineEvent& event, const ActionNotifyContext& context) override {
104 if (!binding) return Result<void>::failure(binding.status());
105 const ActiveKey key{context.executionId, event.itemId.format()};
106 if (event.kind == ActionTimelineEventKind::StateEnter)
108 if (event.kind != ActionTimelineEventKind::StateExit)
109 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "parameter curve requires state boundaries", "event.kind"));
110 const auto found = active_.find(key);
111 if (found == active_.end())
113 ActionParameterSample sample{ActionParameterPhase::End, context.executionId, event.itemId,
114 found->second.target, found->second.operation, found->second.value,
115 context.preview};
116 auto applied = dispatch(sample);
117 if (!applied) return applied;
118 active_.erase(found);
119 return applied;
120 }
121
122 Result<void> update(const ActionActiveBlock& block, const ActionNotifyContext& context) override {
124 if (!binding) return Result<void>::failure(binding.status());
125 if (block.duration <= Duration::zero())
126 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "parameter curve block duration must be positive", "duration"));
127 const double progress = std::clamp(block.localTime.seconds() / block.duration.seconds(), 0.0, 1.0);
128 const double value = binding.value().sample(progress);
129 const ActiveKey key{context.executionId, block.itemId.format()};
130 const auto found = active_.find(key);
132 ActionParameterSample sample{phase, context.executionId, block.itemId, binding.value().target,
133 binding.value().operation, value, context.preview};
134 auto applied = dispatch(sample);
135 if (!applied) return applied;
136 active_[key] = ActiveParameter{binding.value().target, binding.value().operation, value};
137 return applied;
138 }
139
140 Result<void> sample(const ActionActiveBlock& block, const ActionNotifyContext&) const override {
142 if (!binding) return Result<void>::failure(binding.status());
143 if (block.duration <= Duration::zero())
144 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "parameter curve block duration must be positive", "duration"));
146 }
147
148private:
149 using ActiveKey = std::pair<ActionExecutionId, std::string>;
150 struct ActiveParameter {
151 LogicalId target;
153 double value = 0.0;
154 };
155
156 static Result<void> dispatch(const ActionParameterSample& sample) {
157 std::optional<Result<void>> result;
158 cap::forEachUntil<IActionParameterSink>([&](IActionParameterSink* sink) {
159 if (!sink->supports(sample.target)) return false;
160 result.emplace(sink->apply(sample));
161 return true;
162 });
163 if (result) return std::move(*result);
164 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "no parameter sink accepts the action target", sample.target.format()));
165 }
166
167 std::map<ActiveKey, ActiveParameter> active_;
168};
169
170} // namespace
171
173 ActionNotifyRegistry registry;
174 const std::vector<ActionNotifyDescriptor> builtins = {
175 {"gameplay:event", "Gameplay Event", "Gameplay", ActionNotifyShape::Instant, {"tag"}},
176 {"combat:damage", "Apply Damage", "Combat", ActionNotifyShape::Instant, {"damageType", "amount"}},
177 {"presentation:vfx", "Spawn VFX", "Presentation", ActionNotifyShape::Instant, {"uri", "lifetimeSeconds"}},
178 {"presentation:vfx-state", "VFX State", "Presentation", ActionNotifyShape::State, {"uri"}},
179 {"presentation:attack-vfx", "Attack VFX", "Presentation", ActionNotifyShape::Instant, {"lifetimeSeconds"}},
180 {"presentation:attack-vfx-state", "Attack VFX State", "Presentation", ActionNotifyShape::State, {}},
181 {"presentation:audio", "Play Audio", "Presentation", ActionNotifyShape::Instant, {"uri"}},
182 {"presentation:audio-state", "Audio State", "Presentation", ActionNotifyShape::State, {"uri"}},
183 {"gameplay:prefab-spawn", "Spawn Prefab", "Gameplay", ActionNotifyShape::State, {"uri"}},
184 {"presentation:camera", "Camera Cue", "Presentation", ActionNotifyShape::Instant, {"cue"}},
185 {"combat:hitbox-window", "Hitbox Window", "Combat", ActionNotifyShape::State, {"hitbox"}},
186 {"combat:invulnerability-window", "Invulnerability Window", "Combat", ActionNotifyShape::State, {}},
187 {"combat:guard-window", "Guard Window", "Combat", ActionNotifyShape::State, {"mode"}},
188 {"input:combo-window", "Combo Window", "Input", ActionNotifyShape::State, {"input"}},
189 {"input:cancel-window", "Cancel Window", "Input", ActionNotifyShape::State, {"allows"}},
190 {"collision:ignore-window", "Collision Ignore", "Collision", ActionNotifyShape::State, {"channel"}},
191 {"movement:root-motion-window", "Root Motion", "Movement", ActionNotifyShape::State, {"mode"}},
192 {"presentation:parameter-curve",
193 "Parameter Curve",
194 "Presentation",
196 {"target", "keys"}},
197 };
198 for (auto descriptor : builtins) {
199 auto registered = registry.registerDescriptor(std::move(descriptor));
200 if (!registered) return Result<ActionNotifyRegistry>::failure(registered.status());
201 }
202 auto cameraHandler = registry.registerHandler("presentation:camera", std::make_shared<ActionCameraCueHandler>());
203 if (!cameraHandler) return Result<ActionNotifyRegistry>::failure(cameraHandler.status());
204 auto damageHandler = registry.registerHandler("combat:damage", std::make_shared<ActionDamageHandler>());
205 if (!damageHandler) return Result<ActionNotifyRegistry>::failure(damageHandler.status());
206 auto gameplayEventHandler =
207 registry.registerHandler("gameplay:event", std::make_shared<ActionGameplayEventHandler>());
208 if (!gameplayEventHandler) return Result<ActionNotifyRegistry>::failure(gameplayEventHandler.status());
209 auto stateWindowHandler = std::make_shared<ActionStateWindowHandler>();
210 for (const char* type : {"collision:ignore-window", "combat:hitbox-window", "combat:invulnerability-window",
211 "combat:guard-window", "input:combo-window", "input:cancel-window"}) {
212 auto registered = registry.registerHandler(type, stateWindowHandler);
213 if (!registered) return Result<ActionNotifyRegistry>::failure(registered.status());
214 }
215 auto parameterHandler = registry.registerHandler(
216 "presentation:parameter-curve", std::make_shared<ActionParameterCurveHandler>());
217 if (!parameterHandler) return Result<ActionNotifyRegistry>::failure(parameterHandler.status());
219 cap::forEach<IActionNotifyProvider>([&](IActionNotifyProvider* provider) {
220 if (!providers) return;
221 providers = provider->install(registry);
222 });
223 if (!providers) return Result<ActionNotifyRegistry>::failure(providers.status());
224 return Result<ActionNotifyRegistry>::success(std::move(registry));
225}
226
228 if (!validType(descriptor.type)) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Notify type is invalid", "type"));
229 if (descriptor.displayName.empty())
230 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Notify display name is empty", "displayName"));
231 if (descriptor.category.empty())
232 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Notify category is empty", "category"));
233 for (const auto& field : descriptor.requiredPayloadFields)
234 if (field.empty())
235 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Required payload field is empty", "requiredPayloadFields"));
236 const std::string key = descriptor.type;
237 if (descriptors_.contains(key))
238 return Result<void>::failure(Diagnostic::error(DiagnosticCode::AlreadyExists, "Notify descriptor is already registered", key));
239 descriptors_.emplace(key, std::move(descriptor));
241}
242
244 std::shared_ptr<IActionNotifyHandler> handler) {
245 if (!descriptors_.contains(type))
246 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Notify descriptor is not registered", std::string(type)));
247 if (!handler) return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Notify handler is null", "handler"));
248 if (handlers_.contains(type))
249 return Result<void>::failure(Diagnostic::error(DiagnosticCode::AlreadyExists, "Notify handler is already registered", std::string(type)));
250 handlers_.emplace(std::string(type), std::move(handler));
252}
253
255 const auto found = handlers_.find(type);
256 if (found == handlers_.end())
257 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Notify handler is not registered", std::string(type)));
258 handlers_.erase(found);
260}
261
263 const auto found = descriptors_.find(type);
264 if (found == descriptors_.end())
266 Diagnostic::error(DiagnosticCode::NotFound, "Notify descriptor is not registered", std::string(type)));
268}
269
270std::vector<ActionNotifyDescriptor> ActionNotifyRegistry::descriptors() const {
271 std::vector<ActionNotifyDescriptor> result;
272 result.reserve(descriptors_.size());
273 for (const auto& entry : descriptors_) result.push_back(entry.second);
274 return result;
275}
276
277bool ActionNotifyRegistry::hasHandler(std::string_view type) const noexcept { return handlers_.contains(type); }
278
280 auto found = descriptors_.find(event.type.format());
281 if (found == descriptors_.end())
282 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "Timeline event notify type is not registered", event.type.format()));
283 const bool instant = event.kind == ActionTimelineEventKind::Notify;
284 if ((found->second.shape == ActionNotifyShape::Instant) != instant)
285 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Timeline event boundary does not match notify shape", event.type.format()));
286 for (const auto& field : found->second.requiredPayloadFields)
287 if (!event.payload.contains(field))
288 return Result<void>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "Timeline event is missing required payload field", field));
289 if (event.type.format() == "gameplay:prefab-spawn") {
291 if (!prefab) return Result<void>::failure(prefab.status());
292 }
293 if (event.type.format() == "presentation:audio" || event.type.format() == "presentation:audio-state") {
297 if (!audio) return Result<void>::failure(audio.status());
298 }
299 if (event.type.format() == "presentation:vfx" || event.type.format() == "presentation:vfx-state") {
303 if (!vfx) return Result<void>::failure(vfx.status());
304 }
305 if (event.type.format() == "presentation:attack-vfx" ||
306 event.type.format() == "presentation:attack-vfx-state") {
310 if (!attack) return Result<void>::failure(attack.status());
311 }
312 if (event.type.format() == "presentation:parameter-curve") {
314 if (!curve) return Result<void>::failure(curve.status());
315 }
316 if (event.type.format() == "presentation:camera") {
318 if (!camera) return Result<void>::failure(camera.status());
319 }
320 if (event.type.format() == "combat:damage") {
321 auto damage = ActionDamageBinding::fromPayload(event.payload);
322 if (!damage) return Result<void>::failure(damage.status());
323 }
324 if (event.type.format() == "gameplay:event") {
325 auto gameplayEvent = ActionGameplayEventBinding::fromPayload(event.payload);
326 if (!gameplayEvent) return Result<void>::failure(gameplayEvent.status());
327 }
328 if (event.type.format() == "collision:ignore-window" || event.type.format() == "combat:hitbox-window" ||
329 event.type.format() == "combat:invulnerability-window" || event.type.format() == "combat:guard-window" ||
330 event.type.format() == "input:combo-window" || event.type.format() == "input:cancel-window") {
331 auto window = ActionStateWindowBinding::fromPayload(event.type.format(), event.payload);
332 if (!window) return Result<void>::failure(window.status());
333 }
334 return Result<void>::success();
335}
336
338 auto valid = validate(event);
339 if (!valid) return valid;
340 const auto handler = handlers_.find(event.type.format());
341 if (handler == handlers_.end())
342 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "No runtime handler is registered for notify type", event.type.format()));
343 return handler->second->handle(event, context);
344}
345
348 context.time, block.payload};
349 auto valid = validate(event);
350 if (!valid) return valid;
351 const auto handler = handlers_.find(block.type.format());
352 if (handler == handlers_.end())
353 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "No runtime handler is registered for notify type", block.type.format()));
354 return handler->second->update(block, context);
355}
356
358 const ActionNotifyContext& context) const {
360 context.time, block.payload};
361 auto valid = validate(event);
362 if (!valid) return valid;
363 const auto handler = handlers_.find(block.type.format());
364 if (handler == handlers_.end())
365 return Result<void>::failure(Diagnostic::error(DiagnosticCode::NotFound, "No preview handler is registered for notify type", block.type.format()));
366 return handler->second->sample(block, context);
367}
368
370 std::set<IActionNotifyHandler*> advanced;
372 for (const auto& [type, handler] : handlers_) {
373 (void)type;
374 if (!advanced.insert(handler.get()).second) continue;
375 auto result = handler->advance(context);
376 if (!result) return result;
377 if (result.status().code() == StatusCode::Applied) outcome = StatusCode::Applied;
378 }
379 return Result<void>::success(Status::success(outcome));
380}
381
382} // namespace eve::action
Typed AttackVfx action-block payload contract.
Typed audio action-block payload contract.
Typed camera-cue action block contract.
Typed instantaneous combat-damage action contract.
Typed instantaneous gameplay-event action contract.
LogicalId target
ActionParameterOperation operation
double value
Extensible notify descriptors, validation and runtime routing.
Typed continuous parameter-curve action block.
Typed payload contract for prefab-spawn action states.
Typed combat/input/collision action-state window contracts.
Typed VFX action-block payload contract.
std::string prefab
std::string descriptor
OwnedAudio audio
float phase
Definition CaveMesh.cpp:58
float camera[2]
std::uint32_t key
ShaderImageInput shape
std::int32_t second
HSQOBJECT handler
bool valid
eve::action::ActionVfxBinding binding
PrimitiveHandle handle
bool found
Heightmap curve
const VegetationPresetContext & context
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
const Status & status() const noexcept
Inspect the structured operation status.
Definition Result.h:269
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Canonical notify descriptor owner and runtime handler router.
Result< void > registerHandler(std::string_view type, std::shared_ptr< IActionNotifyHandler > handler)
Register an owning handler for an existing descriptor.
Result< void > unregisterHandler(std::string_view type)
Remove a handler while retaining its descriptor for authoring.
Result< void > dispatchSample(const ActionActiveBlock &block, const ActionNotifyContext &context) const
Route one side-effect-free preview sample to its registered handler.
bool hasHandler(std::string_view type) const noexcept
Return whether this registry has a runtime handler for the exact canonical type.
Result< void > validate(const ActionTimelineEvent &event) const
Validate type, shape and required payload fields.
std::vector< ActionNotifyDescriptor > descriptors() const
Return descriptors in lexical type order.
Result< void > dispatchUpdate(const ActionActiveBlock &block, const ActionNotifyContext &context)
Route one authoritative active-state update to its registered handler.
Result< void > registerDescriptor(ActionNotifyDescriptor descriptor)
Register a unique descriptor after validating its canonical type.
Result< void > advanceHandlers(const ActionNotifyContext &context)
Advance each unique registered handler once in lexical type order.
Result< void > dispatch(const ActionTimelineEvent &event, const ActionNotifyContext &context)
Validate and route an event, failing observably when no handler is present.
static Result< ActionNotifyRegistry > withBuiltins()
Build a registry containing the engine's standard semantic notify types.
Result< ActionNotifyDescriptor > descriptor(std::string_view type) const
Find an owning descriptor copy.
Optional module provider that installs handlers into a new registry.
virtual Result< void > install(ActionNotifyRegistry &registry)=0
Install provider-owned handler instances for known descriptors.
ActionParameterOperation
Composition operation applied by a target-specific parameter sink.
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
double sample(const Heightmap &map, double u, double v)
Sample.
WidgetDesc progress(float fraction, std::string id, std::string overlay)
Progress bar; fraction is clamped to [0,1].
Definition Widget.cpp:395
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
Owning per-step sample of one currently active timeline state.
static Result< ActionAttackVfxBinding > fromPayload(const Value::Object &payload, ActionAttackVfxShape shape)
Decode one dynamic AttackVfx payload transactionally.
static Result< ActionAudioBinding > fromPayload(const Value::Object &payload, ActionAudioShape shape)
Decode an audio payload transactionally.
static Result< ActionCameraCueBinding > fromPayload(const Value::Object &payload)
Decode a camera cue transactionally.
static Result< ActionDamageBinding > fromPayload(const Value::Object &payload)
Decode and validate a damage payload without mutating combat state.
static Result< ActionGameplayEventBinding > fromPayload(const Value::Object &payload)
Decode and validate an event payload without emitting it.
Context linking a routed event to its action execution.
Owning editor/runtime contract for one notify type.
static Result< ActionParameterCurveBinding > fromPayload(const Value::Object &payload)
Decode and validate target, operation and normalized ordered keys.
static Result< ActionPrefabSpawnBinding > fromPayload(const Value::Object &payload)
Decode a prefab-spawn payload without mutating runtime state.
static Result< ActionStateWindowBinding > fromPayload(std::string_view type, const Value::Object &payload)
Decode one known state-window payload without mutating domain state.
Owning event projection returned by the runtime sampler.
static Result< ActionVfxBinding > fromPayload(const Value::Object &payload, ActionVfxShape shape)
Decode one dynamic VFX payload transactionally.