载入中...
搜索中...
未找到
NpcAi.cpp
浏览该文件的文档.
1#include "npc_ai/NpcAi.h"
2
3#include "common/Status.h"
4
5#include <algorithm>
6#include <cmath>
7#include <set>
8
9namespace eve::npc_ai {
10namespace {
11bool compareValues(const BlackboardValue& lhs, CompareOp op, const BlackboardValue& rhs) {
12 if (lhs.index() != rhs.index()) return op == CompareOp::NotEqual;
13 if (op == CompareOp::Equal) return lhs == rhs;
14 if (op == CompareOp::NotEqual) return lhs != rhs;
15 return std::visit(
16 [op](const auto& a, const auto& b) {
17 using A = std::decay_t<decltype(a)>;
18 using B = std::decay_t<decltype(b)>;
19 if constexpr (!std::is_same_v<A, B> || std::is_same_v<A, bool>)
20 return false;
21 else {
22 switch (op) {
23 case CompareOp::Less: return a < b;
24 case CompareOp::LessEqual: return a <= b;
25 case CompareOp::Greater: return a > b;
26 case CompareOp::GreaterEqual: return a >= b;
27 default: return false;
28 }
29 }
30 },
31 lhs, rhs);
32}
33
34BlackboardType valueType(const BlackboardValue& value) {
35 if (std::holds_alternative<bool>(value)) return BlackboardType::Boolean;
36 if (std::holds_alternative<std::int64_t>(value)) return BlackboardType::Integer;
37 if (std::holds_alternative<double>(value)) return BlackboardType::Number;
39}
40
41bool validPredicate(const BlackboardPredicate& predicate) {
42 return !predicate.key.empty() && (predicate.op == CompareOp::Exists || predicate.value.has_value());
43}
44} // namespace
45
46NpcAiWorld::NpcAiWorld(NpcAiWorldConfig config) : config_(config) {}
47
49 if (definition.schemaVersion != 1)
51 "unsupported NPC behavior schema version", {}, {}, "npc_ai"));
52 if (definition.id.empty() || definition.initialState.empty())
54 "behavior id and initial state are required", {}, {}, "npc_ai"));
55 std::set<std::string> ids;
56 std::set<std::string> schemaKeys;
57 for (const auto& key : definition.blackboardSchema) {
58 if (key.key.empty() || !schemaKeys.insert(key.key).second)
60 "blackboard schema keys must be non-empty and unique", {},
61 {}, "npc_ai"));
62 if (key.defaultValue && valueType(*key.defaultValue) != key.type)
64 "blackboard default value does not match its declared type",
65 {}, {}, "npc_ai"));
66 if (key.required && !key.defaultValue)
68 DiagnosticCode::InvalidArgument, "required blackboard keys need a default for atomic agent creation",
69 {}, {}, "npc_ai"));
70 }
71 for (const auto& state : definition.states) {
72 if (state.id.empty() || !ids.insert(state.id).second)
74 "state ids must be non-empty and unique", {}, {}, "npc_ai"));
75 std::set<std::string> taskIds;
76 for (const auto& task : state.tasks)
77 if (task.id.empty() || task.type.empty() || !taskIds.insert(task.id).second)
79 DiagnosticCode::InvalidArgument, "task ids must be unique per state and task types are required",
80 {}, {}, "npc_ai"));
81 }
82 if (!ids.contains(definition.initialState))
84 Diagnostic::error(DiagnosticCode::NotFound, "initial state does not exist", {}, {}, "npc_ai"));
85 for (const auto& state : definition.states) {
86 if (state.parent && (!ids.contains(*state.parent) || *state.parent == state.id))
88 DiagnosticCode::InvalidArgument, "state parent is missing or self-referential", {}, {}, "npc_ai"));
89 for (const auto& transition : state.transitions) {
90 if (!ids.contains(transition.targetState))
92 Diagnostic::error(DiagnosticCode::NotFound, "transition target does not exist", {}, {}, "npc_ai"));
93 if (transition.targetState == state.id && transition.signal.empty())
96 "an unconditional self-transition would exhaust the transition budget", {}, {}, "npc_ai"));
97 for (const auto& predicate : transition.conditions)
98 if (!validPredicate(predicate))
100 "invalid transition predicate", {}, {}, "npc_ai"));
101 }
102 for (const auto& predicate : state.enterConditions)
103 if (!validPredicate(predicate))
105 Diagnostic::error(DiagnosticCode::InvalidArgument, "invalid enter predicate", {}, {}, "npc_ai"));
106 }
107 for (const auto& origin : definition.states) {
108 std::set<std::string> chain;
110 while (current->parent) {
111 if (!chain.insert(current->id).second)
113 "state parent cycle detected", {}, {}, "npc_ai"));
114 auto it = std::find_if(definition.states.begin(), definition.states.end(),
115 [&](const auto& s) { return s.id == *current->parent; });
116 current = &*it;
117 }
118 }
119 return Result<void>::success();
120}
121
123 auto checked = validate(definition);
124 if (!checked.ok()) return Result<void>::failure(checked.status());
125 if (behaviors_.contains(definition.id)) {
128 "behavior definition already exists; replacement requires an explicit migration", {}, {}, "npc_ai"));
129 }
130 behaviors_.emplace(definition.id, std::move(definition));
132}
133
134Result<void> NpcAiWorld::registerTaskService(std::string type, std::unique_ptr<ITaskService> service) {
135 if (type.empty() || !service)
137 "task service type and owner are required", {}, {}, "npc_ai"));
138 taskServices_.insert_or_assign(std::move(type), std::move(service));
140}
141
142Result<AgentHandle> NpcAiWorld::createAgent(std::string_view behaviorId) {
143 auto behavior = behaviors_.find(std::string(behaviorId));
144 if (behavior == behaviors_.end())
146 Diagnostic::error(DiagnosticCode::NotFound, "behavior definition was not registered", {}, {}, "npc_ai"));
147 std::uint32_t index;
148 if (freeSlots_.empty()) {
149 index = static_cast<std::uint32_t>(slots_.size());
150 slots_.push_back({});
151 } else {
152 index = freeSlots_.back();
153 freeSlots_.pop_back();
154 }
155 Agent candidate;
156 candidate.behaviorId = behavior->first;
157 candidate.activeState = behavior->second.initialState;
158 for (const auto& key : behavior->second.blackboardSchema)
159 if (key.defaultValue) candidate.blackboard.emplace(key.key, *key.defaultValue);
160 for (const auto state : activePath(behavior->second, candidate.activeState)) {
161 if (!predicatesPass(state.get().enterConditions, candidate)) {
162 freeSlots_.push_back(index);
164 "initial state path enter conditions are false", {},
165 {}, "npc_ai"));
166 }
167 }
168 slots_[index].agent = std::move(candidate);
169 const AgentHandle handle(index, slots_[index].generation);
170 pushTrace({0, handle, TraceKind::AgentCreated, behavior->first, {}});
172}
173
175 if (!handle.isValid() || handle.index() >= slots_.size())
177 Diagnostic::error(DiagnosticCode::StaleHandle, "NPC agent handle is stale", {}, {}, "npc_ai"));
178 auto& slot = slots_[handle.index()];
179 if (!slot.agent || slot.generation != handle.generation())
180 return Result<std::reference_wrapper<Agent>>::failure(
181 Diagnostic::error(DiagnosticCode::StaleHandle, "NPC agent handle is stale", {}, {}, "npc_ai"));
182 return Result<std::reference_wrapper<Agent>>::success(std::ref(*slot.agent));
183}
184Result<std::reference_wrapper<const NpcAiWorld::Agent>> NpcAiWorld::resolve(AgentHandle handle) const {
185 if (!handle.isValid() || handle.index() >= slots_.size())
186 return Result<std::reference_wrapper<const Agent>>::failure(
187 Diagnostic::error(DiagnosticCode::StaleHandle, "NPC agent handle is stale", {}, {}, "npc_ai"));
188 const auto& slot = slots_[handle.index()];
189 if (!slot.agent || slot.generation != handle.generation())
190 return Result<std::reference_wrapper<const Agent>>::failure(
191 Diagnostic::error(DiagnosticCode::StaleHandle, "NPC agent handle is stale", {}, {}, "npc_ai"));
192 return Result<std::reference_wrapper<const Agent>>::success(std::cref(*slot.agent));
193}
194
196 auto resolved = resolve(handle);
197 if (!resolved.ok()) return Result<void>::failure(resolved.status());
198 Agent& agent = resolved.value().get();
199 const auto& behavior = behaviors_.at(agent.behaviorId);
200 auto path = activePath(behavior, agent.activeState);
201 for (auto state = path.rbegin(); state != path.rend(); ++state)
202 stopStateTasks(handle, agent, state->get(), StopReason::AgentDestroyed);
203 pushTrace({agent.lastTick, handle, TraceKind::AgentDestroyed, agent.behaviorId, {}});
204 auto& slot = slots_[handle.index()];
205 slot.agent.reset();
206 auto next = AgentHandle::nextGeneration(slot.generation);
207 if (next) {
208 slot.generation = *next;
209 freeSlots_.push_back(handle.index());
210 }
212}
213
215 if (key.empty())
217 Diagnostic::error(DiagnosticCode::InvalidArgument, "blackboard key is required", {}, {}, "npc_ai"));
218 const std::string traceKey = key;
219 auto resolved = resolve(handle);
220 if (!resolved.ok()) return Result<void>::failure(resolved.status());
221 auto& agent = resolved.value().get();
222 const auto& behavior = behaviors_.at(agent.behaviorId);
223 const auto schema = std::find_if(behavior.blackboardSchema.begin(), behavior.blackboardSchema.end(),
224 [&](const auto& item) { return item.key == key; });
225 if (!behavior.blackboardSchema.empty() && schema == behavior.blackboardSchema.end())
227 DiagnosticCode::NotFound, "blackboard key is not declared by the behavior schema", {}, {}, "npc_ai"));
228 if (schema != behavior.blackboardSchema.end() && schema->type != valueType(value))
230 DiagnosticCode::InvalidArgument, "blackboard value type does not match its schema", {}, {}, "npc_ai"));
231 agent.blackboard.insert_or_assign(std::move(key), std::move(value));
232 pushTrace({agent.lastTick, handle, TraceKind::BlackboardChanged, traceKey, {}});
234}
235
237 if (signalName.empty())
239 Diagnostic::error(DiagnosticCode::InvalidArgument, "signal name is required", {}, {}, "npc_ai"));
240 auto resolved = resolve(handle);
241 if (!resolved.ok()) return Result<void>::failure(resolved.status());
242 auto& agent = resolved.value().get();
243 agent.signals.push_back(std::move(signalName));
244 pushTrace({agent.lastTick, handle, TraceKind::SignalQueued, agent.signals.back(), {}});
246}
247
249 if (memory.subject.empty() || memory.sense.empty() || !std::isfinite(memory.confidence) ||
250 memory.confidence < 0.0 || memory.confidence > 1.0 || memory.forgetAfterTicks == 0)
252 "perception memory fields are invalid", {}, {}, "npc_ai"));
253 auto resolved = resolve(handle);
254 if (!resolved.ok()) return Result<void>::failure(resolved.status());
255 auto& agent = resolved.value().get();
256 auto found = std::find_if(agent.perception.begin(), agent.perception.end(), [&](const auto& item) {
257 return item.subject == memory.subject && item.sense == memory.sense;
258 });
259 if (found != agent.perception.end()) {
260 *found = memory;
261 } else {
262 if (config_.maxMemoriesPerAgent == 0)
264 DiagnosticCode::Unsupported, "perception memory is disabled by world configuration", {}, {}, "npc_ai"));
265 if (agent.perception.size() == config_.maxMemoriesPerAgent) {
266 auto oldest =
267 std::min_element(agent.perception.begin(), agent.perception.end(), [](const auto& a, const auto& b) {
268 return std::pair{a.observedTick, a.subject + "\n" + a.sense} <
269 std::pair{b.observedTick, b.subject + "\n" + b.sense};
270 });
271 pushTrace({memory.observedTick, handle, TraceKind::PerceptionForgotten, oldest->subject, oldest->sense});
272 *oldest = memory;
273 } else {
274 agent.perception.push_back(memory);
275 }
276 }
277 std::sort(agent.perception.begin(), agent.perception.end(), [](const auto& a, const auto& b) {
278 return std::pair{a.subject, a.sense} < std::pair{b.subject, b.sense};
279 });
280 agent.signals.push_back("perception." + memory.sense);
281 pushTrace({memory.observedTick, handle, TraceKind::PerceptionUpdated, memory.subject, memory.sense});
283}
284
285Result<void> NpcAiWorld::forget(AgentHandle handle, std::string_view subject, std::string_view sense) {
286 if (subject.empty() || sense.empty())
288 "perception subject and sense are required", {}, {}, "npc_ai"));
289 auto resolved = resolve(handle);
290 if (!resolved.ok()) return Result<void>::failure(resolved.status());
291 auto& agent = resolved.value().get();
292 auto& memories = agent.perception;
293 const auto found = std::find_if(memories.begin(), memories.end(),
294 [&](const auto& item) { return item.subject == subject && item.sense == sense; });
295 if (found == memories.end())
297 Diagnostic::error(DiagnosticCode::NotFound, "perception memory was not found", {}, {}, "npc_ai"));
298 pushTrace({agent.lastTick, handle, TraceKind::PerceptionForgotten, found->subject, found->sense});
299 memories.erase(found);
301}
302
303bool NpcAiWorld::predicatesPass(std::span<const BlackboardPredicate> predicates, const Agent& agent) const {
304 for (const auto& predicate : predicates) {
305 const auto found = agent.blackboard.find(predicate.key);
306 if (predicate.op == CompareOp::Exists) {
307 if (found == agent.blackboard.end()) return false;
308 continue;
309 }
310 if (found == agent.blackboard.end() || !predicate.value ||
311 !compareValues(found->second, predicate.op, *predicate.value))
312 return false;
313 }
314 return true;
315}
316
317std::optional<std::reference_wrapper<const StateDefinition>> NpcAiWorld::findState(const BehaviorDefinition& behavior,
318 std::string_view id) const {
319 const auto found =
320 std::find_if(behavior.states.begin(), behavior.states.end(), [&](const auto& state) { return state.id == id; });
321 if (found == behavior.states.end()) return std::nullopt;
322 return std::cref(*found);
323}
324
325std::vector<std::reference_wrapper<const StateDefinition>> NpcAiWorld::activePath(const BehaviorDefinition& behavior,
326 std::string_view leaf) const {
327 std::vector<std::reference_wrapper<const StateDefinition>> path;
328 auto current = findState(behavior, leaf);
329 while (current) {
330 path.push_back(*current);
331 current = current->get().parent ? findState(behavior, *current->get().parent) : std::nullopt;
332 }
333 std::reverse(path.begin(), path.end());
334 return path;
335}
336
337std::string NpcAiWorld::taskRuntimeKey(std::string_view state, std::string_view task) {
338 std::string result;
339 result.reserve(state.size() + task.size() + 1);
340 result.append(state);
341 result.push_back('\n');
342 result.append(task);
343 return result;
344}
345
346void NpcAiWorld::pushTrace(TraceEvent event) {
347 if (config_.traceCapacity == 0) return;
348 if (trace_.size() == config_.traceCapacity) trace_.pop_front();
349 trace_.push_back(std::move(event));
350}
351
352void NpcAiWorld::stopStateTasks(AgentHandle handle, Agent& agent, const StateDefinition& state,
353 StopReason reason) noexcept {
354 for (const auto& task : state.tasks) {
355 const auto key = taskRuntimeKey(state.id, task.id);
356 auto runtime = agent.taskRuntime.find(key);
357 auto service = taskServices_.find(task.type);
358 if (runtime != agent.taskRuntime.end() && runtime->second.started && service != taskServices_.end()) {
359 const TaskContext context{handle, state.id, agent.lastTick, 0.0, agent.blackboard, agent.perception};
360 service->second->stop(context, task, reason, runtime->second.memoryJson);
361 }
362 agent.taskRuntime.erase(key);
363 }
364}
365
366Result<TickReport> NpcAiWorld::tick(const TickContext& context) {
367 if (!std::isfinite(context.deltaSeconds) || context.deltaSeconds < 0.0 || context.maxTransitionsPerAgent == 0)
369 DiagnosticCode::InvalidArgument, "tick dt and transition budget are invalid", {}, {}, "npc_ai"));
370 TickReport report;
371 if (slots_.empty() || context.maxAgents == 0) return Result<TickReport>::success(report);
372 std::uint32_t visited = 0;
373 while (visited < slots_.size() && report.agentsUpdated < context.maxAgents) {
374 const std::uint32_t index = (scheduleCursor_ + visited) % static_cast<std::uint32_t>(slots_.size());
375 ++visited;
376 auto& slot = slots_[index];
377 if (!slot.agent) continue;
378 AgentHandle handle(index, slot.generation);
379 Agent& agent = *slot.agent;
380 auto& behavior = behaviors_.at(agent.behaviorId);
381 auto path = activePath(behavior, agent.activeState);
382 if (path.empty())
384 "agent active state is missing", {}, {}, "npc_ai"));
385
386 for (auto memory = agent.perception.begin(); memory != agent.perception.end();) {
387 const bool expired = context.simulationTick >= memory->observedTick &&
388 context.simulationTick - memory->observedTick >= memory->forgetAfterTicks;
389 if (!expired) {
390 ++memory;
391 continue;
392 }
393 const std::string signalName = "perception.forgotten." + memory->sense;
394 agent.signals.push_back(signalName);
395 pushTrace({context.simulationTick, handle, TraceKind::PerceptionForgotten, memory->subject, memory->sense});
396 memory = agent.perception.erase(memory);
397 }
398
399 std::uint32_t transitions = 0;
400 while (transitions < context.maxTransitionsPerAgent) {
401 const Transition* selected = nullptr;
402 std::vector<std::reference_wrapper<const StateDefinition>> selectedPath;
403 for (auto state = path.rbegin(); state != path.rend(); ++state) {
404 for (const auto& candidate : state->get().transitions) {
405 const bool signalMatches =
406 candidate.signal.empty() ||
407 std::find(agent.signals.begin(), agent.signals.end(), candidate.signal) != agent.signals.end();
408 if (!signalMatches || !predicatesPass(candidate.conditions, agent) ||
409 (selected && candidate.priority <= selected->priority))
410 continue;
411 auto candidatePath = activePath(behavior, candidate.targetState);
412 const bool canEnter = std::all_of(
413 candidatePath.begin(), candidatePath.end(),
414 [&](const auto target) { return predicatesPass(target.get().enterConditions, agent); });
415 if (canEnter) {
416 selected = &candidate;
417 selectedPath = std::move(candidatePath);
418 }
419 }
420 }
421 if (!selected) break;
422
423 std::size_t commonPrefix = 0;
424 while (commonPrefix < path.size() && commonPrefix < selectedPath.size() &&
425 path[commonPrefix].get().id == selectedPath[commonPrefix].get().id)
426 ++commonPrefix;
427 for (std::size_t exiting = path.size(); exiting > commonPrefix; --exiting)
428 stopStateTasks(handle, agent, path[exiting - 1].get(), StopReason::Transition);
429
430 const std::string previousState = agent.activeState;
431 agent.activeState = selected->targetState;
432 path = std::move(selectedPath);
433 ++transitions;
434 ++report.transitionsApplied;
435 pushTrace({context.simulationTick, handle, TraceKind::TransitionApplied, previousState, agent.activeState});
436 if (!selected->signal.empty())
437 agent.signals.erase(std::remove(agent.signals.begin(), agent.signals.end(), selected->signal),
438 agent.signals.end());
439 }
440 if (transitions == context.maxTransitionsPerAgent) ++report.transitionBudgetsExhausted;
441 agent.signals.clear();
442 agent.lastTick = context.simulationTick;
443
444 for (const auto state : path)
445 for (const auto& task : state.get().tasks)
446 if (!taskServices_.contains(task.type))
448 DiagnosticCode::NotFound, "task service is not registered: " + task.type, {}, {}, "npc_ai"));
449
450 struct StartedTask {
451 std::reference_wrapper<const StateDefinition> state;
452 std::reference_wrapper<const TaskSpec> task;
453 std::string runtimeKey;
454 };
455 std::vector<StartedTask> startedThisTick;
456 for (const auto state : path) {
457 for (const auto& task : state.get().tasks) {
458 auto& service = taskServices_.at(task.type);
459 const auto runtimeKey = taskRuntimeKey(state.get().id, task.id);
460 auto& runtime = agent.taskRuntime[runtimeKey];
461 if (runtime.completed) continue;
462 TaskContext taskContext{handle, state.get().id, context.simulationTick, context.deltaSeconds,
463 agent.blackboard, agent.perception};
464 if (!runtime.started) {
465 auto started = service->start(taskContext, task, runtime.memoryJson);
466 if (!started.ok()) {
467 agent.taskRuntime.erase(runtimeKey);
468 for (auto rollback = startedThisTick.rbegin(); rollback != startedThisTick.rend(); ++rollback) {
469 auto active = agent.taskRuntime.find(rollback->runtimeKey);
470 if (active == agent.taskRuntime.end()) continue;
471 auto& rollbackService = taskServices_.at(rollback->task.get().type);
472 const TaskContext rollbackContext{handle,
473 rollback->state.get().id,
475 context.deltaSeconds,
476 agent.blackboard,
477 agent.perception};
478 rollbackService->stop(rollbackContext, rollback->task.get(), StopReason::StartupRollback,
479 active->second.memoryJson);
480 agent.taskRuntime.erase(active);
481 }
482 return Result<TickReport>::failure(started.status());
483 }
484 runtime.started = true;
485 startedThisTick.push_back({state, std::cref(task), runtimeKey});
486 pushTrace({context.simulationTick, handle, TraceKind::TaskStarted, state.get().id, task.id});
487 }
488 }
489 }
490
491 for (const auto state : path) {
492 for (const auto& task : state.get().tasks) {
493 auto& service = taskServices_.at(task.type);
494 auto& runtime = agent.taskRuntime.at(taskRuntimeKey(state.get().id, task.id));
495 if (runtime.completed) continue;
496 TaskContext taskContext{handle, state.get().id, context.simulationTick, context.deltaSeconds,
497 agent.blackboard, agent.perception};
498 auto taskResult = service->tick(taskContext, task, runtime.memoryJson);
499 if (!taskResult.ok()) return Result<TickReport>::failure(taskResult.status());
500 ++report.tasksTicked;
501 if (taskResult.value() != TaskStatus::Running) {
502 service->stop(taskContext, task, StopReason::Completed, runtime.memoryJson);
503 runtime.started = false;
504 runtime.completed = true;
505 const bool succeeded = taskResult.value() == TaskStatus::Succeeded;
506 agent.signals.push_back(std::string("task.") + (succeeded ? "succeeded." : "failed.") + task.id);
507 pushTrace({context.simulationTick, handle,
508 succeeded ? TraceKind::TaskCompleted : TraceKind::TaskFailed, state.get().id, task.id});
509 }
510 }
511 }
512 ++report.agentsUpdated;
513 }
514 scheduleCursor_ = (scheduleCursor_ + visited) % static_cast<std::uint32_t>(slots_.size());
515 for (const auto& slot : slots_)
516 if (slot.agent) ++report.agentsDeferred;
517 report.agentsDeferred -= report.agentsUpdated;
518 return Result<TickReport>::success(report);
519}
520
521Result<AgentSnapshot> NpcAiWorld::snapshot(AgentHandle handle) const {
522 auto resolved = resolve(handle);
523 if (!resolved.ok()) return Result<AgentSnapshot>::failure(resolved.status());
524 const Agent& agent = resolved.value().get();
525 std::vector<std::string> path;
526 for (const auto state : activePath(behaviors_.at(agent.behaviorId), agent.activeState))
527 path.push_back(state.get().id);
528 return Result<AgentSnapshot>::success({handle, agent.behaviorId, agent.activeState, std::move(path),
529 agent.blackboard, agent.perception, agent.lastTick});
530}
531
532Result<AgentArchive> NpcAiWorld::archive(AgentHandle handle) const {
533 auto resolved = resolve(handle);
534 if (!resolved.ok()) return Result<AgentArchive>::failure(resolved.status());
535 const Agent& agent = resolved.value().get();
536 AgentArchive result;
537 result.behaviorId = agent.behaviorId;
538 result.activeState = agent.activeState;
539 result.blackboard = agent.blackboard;
540 result.pendingSignals = agent.signals;
541 result.perception = agent.perception;
542 result.lastTick = agent.lastTick;
543 const auto& behavior = behaviors_.at(agent.behaviorId);
544 for (const auto state : activePath(behavior, agent.activeState)) {
545 for (const auto& task : state.get().tasks) {
546 const auto runtime = agent.taskRuntime.find(taskRuntimeKey(state.get().id, task.id));
547 if (runtime == agent.taskRuntime.end()) continue;
548 result.tasks.push_back({state.get().id, task.id, runtime->second.completed, runtime->second.memoryJson});
549 }
550 }
551 return Result<AgentArchive>::success(std::move(result));
552}
553
554Result<AgentHandle> NpcAiWorld::restoreAgent(const AgentArchive& archive) {
555 if (archive.schemaId != AgentArchive::SchemaId || archive.schemaVersion != AgentArchive::SchemaVersion)
557 Diagnostic::error(DiagnosticCode::Unsupported, "unsupported NPC agent archive schema", {}, {}, "npc_ai"));
558 const auto behaviorIt = behaviors_.find(archive.behaviorId);
559 if (behaviorIt == behaviors_.end())
561 DiagnosticCode::NotFound, "archive behavior definition was not registered", {}, {}, "npc_ai"));
562 const auto& behavior = behaviorIt->second;
563 if (!findState(behavior, archive.activeState))
565 Diagnostic::error(DiagnosticCode::NotFound, "archive active state does not exist", {}, {}, "npc_ai"));
566 if (archive.perception.size() > config_.maxMemoriesPerAgent)
568 DiagnosticCode::PreconditionViolation, "archive exceeds the perception memory budget", {}, {}, "npc_ai"));
569
570 Agent candidate;
571 candidate.behaviorId = archive.behaviorId;
572 candidate.activeState = archive.activeState;
573 candidate.blackboard = archive.blackboard;
574 candidate.signals = archive.pendingSignals;
575 candidate.perception = archive.perception;
576 candidate.lastTick = archive.lastTick;
577 for (const auto& signalName : candidate.signals)
578 if (signalName.empty())
580 DiagnosticCode::InvalidArgument, "archive contains an empty signal", {}, {}, "npc_ai"));
581 std::set<std::pair<std::string, std::string>> memoryKeys;
582 for (const auto& memory : candidate.perception) {
583 if (memory.subject.empty() || memory.sense.empty() || !std::isfinite(memory.confidence) ||
584 memory.confidence < 0.0 || memory.confidence > 1.0 || memory.forgetAfterTicks == 0)
586 DiagnosticCode::InvalidArgument, "archive contains invalid perception memory", {}, {}, "npc_ai"));
587 if (!memoryKeys.emplace(memory.subject, memory.sense).second)
589 DiagnosticCode::InvalidArgument, "archive contains duplicate perception memory", {}, {}, "npc_ai"));
590 }
591
592 if (!behavior.blackboardSchema.empty()) {
593 if (candidate.blackboard.size() > behavior.blackboardSchema.size())
595 DiagnosticCode::InvalidArgument, "archive contains undeclared blackboard keys", {}, {}, "npc_ai"));
596 for (const auto& [key, value] : candidate.blackboard) {
597 const auto spec = std::find_if(behavior.blackboardSchema.begin(), behavior.blackboardSchema.end(),
598 [&](const auto& item) { return item.key == key; });
599 if (spec == behavior.blackboardSchema.end() || spec->type != valueType(value))
602 "archive blackboard does not match the behavior schema", {}, {}, "npc_ai"));
603 }
604 for (const auto& spec : behavior.blackboardSchema)
605 if (spec.required && !candidate.blackboard.contains(spec.key))
607 DiagnosticCode::InvalidArgument, "archive is missing a required blackboard key", {}, {}, "npc_ai"));
608 }
609
610 const auto path = activePath(behavior, candidate.activeState);
611 if (!std::all_of(path.begin(), path.end(),
612 [&](const auto state) { return predicatesPass(state.get().enterConditions, candidate); }))
614 DiagnosticCode::PreconditionViolation, "archive active path enter conditions are false", {}, {}, "npc_ai"));
615 std::set<std::string> validTasks;
616 for (const auto state : path)
617 for (const auto& task : state.get().tasks) validTasks.insert(taskRuntimeKey(state.get().id, task.id));
618 std::set<std::string> archivedTasks;
619 for (const auto& task : archive.tasks) {
620 const auto key = taskRuntimeKey(task.stateId, task.taskId);
621 if (!validTasks.contains(key) || !archivedTasks.insert(key).second)
624 "archive task is duplicated or outside the active path", {}, {}, "npc_ai"));
625 candidate.taskRuntime.emplace(key, TaskRuntime{false, task.completed, task.memoryJson});
626 }
627 std::sort(candidate.perception.begin(), candidate.perception.end(), [](const auto& a, const auto& b) {
628 return std::pair{a.subject, a.sense} < std::pair{b.subject, b.sense};
629 });
630
631 std::uint32_t index;
632 if (freeSlots_.empty()) {
633 index = static_cast<std::uint32_t>(slots_.size());
634 slots_.push_back({});
635 } else {
636 index = freeSlots_.back();
637 freeSlots_.pop_back();
638 }
639 slots_[index].agent = std::move(candidate);
640 const AgentHandle handle(index, slots_[index].generation);
641 pushTrace({archive.lastTick, handle, TraceKind::AgentRestored, archive.behaviorId, archive.activeState});
643}
644
645bool NpcAiWorld::isStale(AgentHandle handle) const noexcept {
646 if (!handle.isValid() || handle.index() >= slots_.size()) return true;
647 const auto& slot = slots_[handle.index()];
648 return !slot.agent || slot.generation != handle.generation();
649}
650
651Result<std::vector<TraceEvent>> NpcAiWorld::trace(AgentHandle handle, std::uint64_t fromTick) const {
652 auto resolved = resolve(handle);
653 if (!resolved.ok()) return Result<std::vector<TraceEvent>>::failure(resolved.status());
654 std::vector<TraceEvent> result;
655 for (const auto& event : trace_)
656 if (event.agent == handle && event.tick >= fromTick) result.push_back(event);
657 return Result<std::vector<TraceEvent>>::success(std::move(result));
658}
659
660} // namespace eve::npc_ai
LogicalId target
double value
bool & active
int subject
Definition AnimSmr.cpp:163
const std::string & s
void * service
std::uint32_t key
vk::UniqueDeviceMemory memory
glm::uvec4 ids
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::string path
Definition PlayHost.cpp:110
std::uint32_t generation
PrimitiveHandle handle
V3 origin
Definition RoadBake.cpp:138
bool found
double current
Structured operation status used by the common Result foundation.
uint32_t index
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
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
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
constexpr std::optional< RuntimeHandle > nextGeneration() const noexcept
Returns this slot at its next generation.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Result< AgentHandle > createAgent(std::string_view behaviorId)
Creates an agent bound to an existing behavior definition.
Definition NpcAi.cpp:142
Result< void > remember(AgentHandle handle, PerceptionMemory memory)
Upserts one bounded memory and queues a sense-specific wake-up signal.
Definition NpcAi.cpp:248
Result< void > setBlackboard(AgentHandle handle, std::string key, BlackboardValue value)
Writes one authoritative blackboard value.
Definition NpcAi.cpp:214
static Result< void > validate(const BehaviorDefinition &definition)
Validates definition shape without publishing it.
Definition NpcAi.cpp:48
Result< void > signal(AgentHandle handle, std::string signalName)
Queues a named wake-up signal for evaluation on the next tick.
Definition NpcAi.cpp:236
NpcAiWorld(NpcAiWorldConfig config={})
Npc ai world.
Definition NpcAi.cpp:46
Result< void > registerBehavior(BehaviorDefinition definition)
Validates and atomically publishes a behavior definition.
Definition NpcAi.cpp:122
Result< void > registerTaskService(std::string type, std::unique_ptr< ITaskService > service)
Transfers ownership of a task provider into this world.
Definition NpcAi.cpp:134
Result< void > destroyAgent(AgentHandle handle)
Stops active tasks and invalidates the agent handle.
Definition NpcAi.cpp:195
eve::RuntimeHandle< AgentHandleTag > AgentHandle
Definition NpcAi.h:97
std::variant< bool, std::int64_t, double, std::string > BlackboardValue
Definition NpcAi.h:22
BlackboardType
BlackboardType public API.
Definition NpcAi.h:25
CompareOp
CompareOp public API.
Definition NpcAi.h:27
Versioned, owning NPC state used for save/load and hot-reload handoff.
Definition NpcAi.h:165
std::map< std::string, BlackboardValue > blackboard
Definition NpcAi.h:173
std::string behaviorId
Definition NpcAi.h:171
std::string activeState
Definition NpcAi.h:172
std::vector< TaskArchive > tasks
Definition NpcAi.h:176
std::vector< std::string > pendingSignals
Definition NpcAi.h:174
std::vector< PerceptionMemory > perception
Definition NpcAi.h:175
std::string schemaId
Definition NpcAi.h:169
std::uint32_t schemaVersion
Definition NpcAi.h:170
std::uint64_t lastTick
Definition NpcAi.h:177
Immutable, validated source data for one hierarchical NPC state tree.
Definition NpcAi.h:87
NpcAiWorldConfig public API.
Definition NpcAi.h:119
std::size_t maxMemoriesPerAgent
Definition NpcAi.h:121
One bounded, expiring item of NPC perception memory.
Definition NpcAi.h:100
StateDefinition public API.
Definition NpcAi.h:78
TaskContext public API.
Definition NpcAi.h:181
std::uint64_t simulationTick
Definition NpcAi.h:184
Input time and work limits for one deterministic simulation update.
Definition NpcAi.h:125
TickReport public API.
Definition NpcAi.h:133
std::uint32_t transitionsApplied
Definition NpcAi.h:136
std::uint32_t agentsUpdated
Definition NpcAi.h:134
std::uint32_t transitionBudgetsExhausted
Definition NpcAi.h:137
std::uint32_t tasksTicked
Definition NpcAi.h:138
std::uint32_t agentsDeferred
Definition NpcAi.h:135
Transition public API.
Definition NpcAi.h:70
std::string targetState
Definition NpcAi.h:71
std::uint32_t priority
Definition NpcAi.h:74
std::string signal
Definition NpcAi.h:72
bool started
Definition Graphics.cpp:183