7#include <simplesquirrel/simplesquirrel.hpp>
22Result<BlackboardType> parseBlackboardType(
const std::string&
text) {
28 "npc_ai blackboard type is invalid",
"type", {},
32Result<CompareOp> parseCompareOp(
const std::string&
text) {
41 "npc_ai compare op is invalid",
"op", {},
"npc_ai.squirrel"));
44Result<BlackboardValue> parseBlackboardValue(
const Value&
value) {
50 "npc_ai blackboard value type is unsupported",
"value",
51 {},
"npc_ai.squirrel"));
54Result<std::uint32_t> parseUInt32Field(std::int64_t
value,
const std::string&
path) {
55 constexpr auto kMax =
static_cast<std::int64_t
>(std::numeric_limits<std::uint32_t>::max());
56 if (value < 0 || value > kMax)
63Result<BlackboardPredicate> parsePredicate(
const Value&
value,
const std::string&
path) {
68 BlackboardPredicate predicate;
69 const auto key =
object->find(
"key");
70 const auto op =
object->find(
"op");
71 if (
key ==
object->end() || !
key->second.getIf<std::string>())
74 if (op ==
object->end() || !op->second.getIf<std::string>())
77 predicate.key = *
key->second.getIf<std::string>();
78 auto parsedOp = parseCompareOp(*op->second.getIf<std::string>());
80 predicate.op = parsedOp.value();
82 auto parsed = parseBlackboardValue(
found->second);
84 predicate.value = std::move(parsed).takeValue();
89Result<BehaviorDefinition> parseBehaviorDefinition(
const Value&
value) {
93 "npc_ai behavior must be an object",
"behavior",
94 {},
"npc_ai.squirrel"));
95 BehaviorDefinition definition;
96 const auto id =
root->find(
"id");
97 if (
id ==
root->end() || !
id->second.getIf<std::string>())
100 definition.id = *
id->second.getIf<std::string>();
101 if (
const auto version =
root->find(
"schemaVersion"); version !=
root->end()) {
102 if (
const auto* integer = version->second.getIf<std::int64_t>()) {
103 auto parsedVersion = parseUInt32Field(*integer,
"schemaVersion");
105 definition.schemaVersion = parsedVersion.value();
108 "npc_ai schemaVersion must be an integer",
109 "schemaVersion", {},
"npc_ai.squirrel"));
111 const auto initial =
root->find(
"initialState");
112 if (initial ==
root->end() || !initial->second.getIf<std::string>())
114 "npc_ai initialState is required",
"initialState",
115 {},
"npc_ai.squirrel"));
116 definition.initialState = *initial->second.getIf<std::string>();
118 if (
const auto schema =
root->find(
"blackboardSchema"); schema !=
root->end()) {
119 const auto* array = schema->second.getIf<
Value::Array>();
122 "npc_ai blackboardSchema must be an array",
123 "blackboardSchema", {},
"npc_ai.squirrel"));
129 "blackboardSchema[" + std::to_string(
index) +
"]", {},
"npc_ai.squirrel"));
130 BlackboardKeySpec keySpec;
131 const auto key = item->find(
"key");
132 const auto type = item->find(
"type");
133 if (
key == item->end() || !
key->second.getIf<std::string>() ||
type == item->end() ||
134 !
type->second.getIf<std::string>())
137 "blackboardSchema[" + std::to_string(
index) +
"]", {},
"npc_ai.squirrel"));
138 keySpec.key = *
key->second.getIf<std::string>();
139 auto parsed = parseBlackboardType(*
type->second.getIf<std::string>());
141 keySpec.type = parsed.value();
142 if (
const auto required = item->find(
"required");
required != item->end()) {
143 if (
const auto* flag =
required->second.getIf<
bool>())
144 keySpec.required = *flag;
148 "blackboardSchema[" + std::to_string(
index) +
"].required", {},
"npc_ai.squirrel"));
151 auto parsedDefault = parseBlackboardValue(
defaultValue->second);
153 keySpec.defaultValue = std::move(parsedDefault).takeValue();
155 definition.blackboardSchema.push_back(std::move(keySpec));
159 const auto states =
root->find(
"states");
163 const auto& stateArray = *states->second.getIf<
Value::Array>();
165 const std::string
path =
"states[" + std::to_string(
index) +
"]";
170 StateDefinition
state;
171 const auto stateId = item->find(
"id");
172 if (stateId == item->end() || !stateId->second.getIf<std::string>())
175 state.id = *stateId->second.getIf<std::string>();
176 if (
const auto parent = item->find(
"parent");
parent != item->end()) {
177 if (
const auto*
text =
parent->second.getIf<std::string>())
181 "npc_ai state parent must be a string",
182 path +
".parent", {},
"npc_ai.squirrel"));
184 if (
const auto enter = item->find(
"enterConditions"); enter != item->end()) {
185 const auto* array = enter->second.getIf<
Value::Array>();
189 path +
".enterConditions", {},
"npc_ai.squirrel"));
190 for (std::size_t predicateIndex = 0; predicateIndex < array->size(); ++predicateIndex) {
192 parsePredicate((*array)[predicateIndex],
path +
".enterConditions[" + std::to_string(predicateIndex) +
"]");
194 state.enterConditions.push_back(std::move(predicate).takeValue());
197 if (
const auto tasks = item->find(
"tasks"); tasks != item->end()) {
198 const auto* array = tasks->second.getIf<
Value::Array>();
203 for (std::size_t taskIndex = 0; taskIndex < array->size(); ++taskIndex) {
204 const auto* taskObject = (*array)[taskIndex].getIf<
Value::Object>();
208 path +
".tasks[" + std::to_string(taskIndex) +
"]", {},
"npc_ai.squirrel"));
210 const auto taskId = taskObject->find(
"id");
211 const auto taskType = taskObject->find(
"type");
212 if (
taskId == taskObject->end() || !
taskId->second.getIf<std::string>() ||
213 taskType == taskObject->end() || !taskType->second.getIf<std::string>())
216 path +
".tasks[" + std::to_string(taskIndex) +
"]", {},
"npc_ai.squirrel"));
217 task.id = *
taskId->second.getIf<std::string>();
218 task.type = *taskType->second.getIf<std::string>();
219 if (
const auto parameters = taskObject->find(
"parametersJson"); parameters != taskObject->end()) {
220 if (
const auto*
text = parameters->second.getIf<std::string>())
221 task.parametersJson = *
text;
225 path +
".tasks[" + std::to_string(taskIndex) +
"].parametersJson", {},
"npc_ai.squirrel"));
227 state.tasks.push_back(std::move(task));
230 if (
const auto transitions = item->find(
"transitions"); transitions != item->end()) {
231 const auto* array = transitions->second.getIf<
Value::Array>();
235 path +
".transitions", {},
"npc_ai.squirrel"));
236 for (std::size_t transitionIndex = 0; transitionIndex < array->size(); ++transitionIndex) {
237 const std::string transitionPath =
238 path +
".transitions[" + std::to_string(transitionIndex) +
"]";
239 const auto* transitionObject = (*array)[transitionIndex].getIf<
Value::Object>();
240 if (!transitionObject)
245 const auto target = transitionObject->find(
"targetState");
246 if (
target == transitionObject->end() || !
target->second.getIf<std::string>())
249 transitionPath +
".targetState", {},
"npc_ai.squirrel"));
250 transition.targetState = *
target->second.getIf<std::string>();
251 if (
const auto signal = transitionObject->find(
"signal"); signal != transitionObject->end()) {
252 if (
const auto*
text = signal->second.getIf<std::string>())
253 transition.signal = *
text;
257 transitionPath +
".signal", {},
"npc_ai.squirrel"));
259 if (
const auto priority = transitionObject->find(
"priority");
priority != transitionObject->end()) {
260 if (
const auto* integer =
priority->second.getIf<std::int64_t>()) {
261 auto parsedPriority = parseUInt32Field(*integer, transitionPath +
".priority");
263 transition.priority = parsedPriority.value();
267 transitionPath +
".priority", {},
"npc_ai.squirrel"));
269 if (
const auto conditions = transitionObject->find(
"conditions");
270 conditions != transitionObject->end()) {
271 const auto* conditionArray = conditions->second.getIf<
Value::Array>();
275 transitionPath +
".conditions", {},
"npc_ai.squirrel"));
276 for (std::size_t conditionIndex = 0; conditionIndex < conditionArray->size(); ++conditionIndex) {
277 auto predicate = parsePredicate(
278 (*conditionArray)[conditionIndex],
279 transitionPath +
".conditions[" + std::to_string(conditionIndex) +
"]");
281 transition.conditions.push_back(std::move(predicate).takeValue());
284 state.transitions.push_back(std::move(transition));
287 definition.states.push_back(std::move(state));
294 [](
const auto& item) -> Value {
295 using T = std::decay_t<
decltype(item)>;
296 if constexpr (std::is_same_v<T, bool>)
298 else if constexpr (std::is_same_v<T, std::int64_t>)
300 else if constexpr (std::is_same_v<T, double>)
308Result<AgentHandle> parseAgentHandle(std::int64_t packed) {
315class ScriptNpcAiWorld {
317 explicit ScriptNpcAiWorld(std::int64_t traceCapacity, std::int64_t maxMemoriesPerAgent) {
318 NpcAiWorldConfig config;
319 if (traceCapacity > 0) config.traceCapacity =
static_cast<std::size_t
>(traceCapacity);
321 if (maxMemoriesPerAgent >= 0) config.maxMemoriesPerAgent =
static_cast<std::size_t
>(maxMemoriesPerAgent);
322 world_ = std::make_unique<NpcAiWorld>(config);
325 Result<Value> registerBehavior(
const std::string& definitionJson) {
328 auto definition = parseBehaviorDefinition(decoded.value());
330 const std::string
id = definition.value().id;
331 auto registered = world_->registerBehavior(std::move(definition).takeValue());
334 result[
"behaviorId"] =
id;
338 Result<Value> validateBehavior(
const std::string& definitionJson)
const {
341 auto definition = parseBehaviorDefinition(decoded.value());
346 result[
"behaviorId"] = definition.value().id;
350 Result<Value> createAgent(
const std::string& behaviorId) {
351 auto created = world_->createAgent(behaviorId);
357 Result<Value> destroyAgent(std::int64_t packed) {
358 auto handle = parseAgentHandle(packed);
360 auto destroyed = world_->destroyAgent(
handle.value());
365 bool isAgentStale(std::int64_t packed)
const {
366 auto handle = parseAgentHandle(packed);
368 return world_->isStale(
handle.value());
371 Result<Value> setBlackboardBool(std::int64_t packed,
const std::string&
key,
bool value) {
374 Result<Value> setBlackboardInt(std::int64_t packed,
const std::string&
key, std::int64_t
value) {
377 Result<Value> setBlackboardNumber(std::int64_t packed,
const std::string&
key,
double value) {
380 Result<Value> setBlackboardString(std::int64_t packed,
const std::string&
key,
const std::string&
value) {
384 Result<Value> signal(std::int64_t packed,
const std::string& signalName) {
385 auto handle = parseAgentHandle(packed);
387 auto signaled = world_->signal(
handle.value(), signalName);
392 Result<Value> remember(std::int64_t packed,
const std::string&
subject,
const std::string& sense,
double confidence,
393 std::int64_t observedTick, std::int64_t forgetAfterTicks,
const std::string& payloadJson) {
394 auto handle = parseAgentHandle(packed);
396 if (observedTick < 0 || forgetAfterTicks <= 0)
398 "npc_ai remember ticks are invalid",
"tick", {},
403 memory.confidence = confidence;
404 memory.observedTick =
static_cast<std::uint64_t
>(observedTick);
405 memory.forgetAfterTicks =
static_cast<std::uint64_t
>(forgetAfterTicks);
406 memory.payloadJson = payloadJson.empty() ?
"{}" : payloadJson;
407 auto remembered = world_->remember(
handle.value(), std::move(
memory));
412 Result<Value> forget(std::int64_t packed,
const std::string&
subject,
const std::string& sense) {
413 auto handle = parseAgentHandle(packed);
415 auto forgotten = world_->forget(
handle.value(),
subject, sense);
420 Result<Value>
tick(std::int64_t simulationTick,
double deltaSeconds, std::int64_t maxAgents,
421 std::int64_t maxTransitionsPerAgent) {
422 auto parsedMaxAgents = parseUInt32Field(maxAgents,
"maxAgents");
424 auto parsedMaxTransitions = parseUInt32Field(maxTransitionsPerAgent,
"maxTransitionsPerAgent");
426 if (simulationTick < 0 || !(deltaSeconds >= 0.0))
428 "npc_ai tick arguments are invalid",
"tick", {},
431 context.simulationTick =
static_cast<std::uint64_t
>(simulationTick);
432 context.deltaSeconds = deltaSeconds;
433 context.maxAgents = parsedMaxAgents.value();
434 context.maxTransitionsPerAgent = parsedMaxTransitions.value();
435 auto report = world_->tick(
context);
438 result[
"agentsUpdated"] =
static_cast<std::int64_t
>(report.value().agentsUpdated);
439 result[
"agentsDeferred"] =
static_cast<std::int64_t
>(report.value().agentsDeferred);
440 result[
"transitionsApplied"] =
static_cast<std::int64_t
>(report.value().transitionsApplied);
441 result[
"transitionBudgetsExhausted"] =
static_cast<std::int64_t
>(report.value().transitionBudgetsExhausted);
442 result[
"tasksTicked"] =
static_cast<std::int64_t
>(report.value().tasksTicked);
446 Result<Value> snapshotValue(std::int64_t packed)
const {
447 auto handle = parseAgentHandle(packed);
449 auto snapshot = world_->snapshot(
handle.value());
452 result[
"handle"] =
static_cast<std::int64_t
>(snapshot.value().handle.packed());
453 result[
"behaviorId"] = snapshot.value().behaviorId;
454 result[
"activeState"] = snapshot.value().activeState;
455 result[
"lastTick"] =
static_cast<std::int64_t
>(snapshot.value().lastTick);
457 for (
const auto& state : snapshot.
value().activePath)
path.emplace_back(
state);
458 result[
"activePath"] = std::move(
path);
461 result[
"blackboard"] = std::move(
board);
463 for (
const auto&
memory : snapshot.
value().perception) {
465 item[
"subject"] =
memory.subject;
466 item[
"sense"] =
memory.sense;
467 item[
"confidence"] =
memory.confidence;
468 item[
"observedTick"] =
static_cast<std::int64_t
>(
memory.observedTick);
469 item[
"forgetAfterTicks"] =
static_cast<std::int64_t
>(
memory.forgetAfterTicks);
470 item[
"payloadJson"] =
memory.payloadJson;
471 perception.emplace_back(std::move(item));
473 result[
"perception"] = std::move(perception);
477 Result<Value> snapshotJson(std::int64_t packed)
const {
478 auto snapshot = snapshotValue(packed);
479 if (!snapshot)
return snapshot;
480 auto json = snapshot.value().toJson();
487 auto handle = parseAgentHandle(packed);
489 auto written = world_->setBlackboard(
handle.value(),
key, std::move(
value));
494 std::unique_ptr<NpcAiWorld> world_;
503 "npc_ai world must not be null",
"world", {},
504 "npc_ai.squirrel")));
507ssq::Table newWorld(
HSQUIRRELVM vm, std::int64_t traceCapacity, std::int64_t maxMemoriesPerAgent) {
508 auto world = std::make_unique<ScriptNpcAiWorld>(traceCapacity, maxMemoriesPerAgent);
509 auto object = script::makeOwnedSquirrelInstance<ScriptNpcAiWorld>(
vm, std::move(
world));
512 result.set(
"value", std::move(
object).takeValue());
513 result.set(
"ownership", std::string(
"owned"));
521void NpcAi::expose(ssq::Table& table) {
523 auto world = table.addClass<ScriptNpcAiWorld>(
524 "NpcAiWorld", std::function<ScriptNpcAiWorld*()>([] {
return nullptr; }),
true);
525 world.addFunc(
"ownership", [](ScriptNpcAiWorld*) {
return std::string(
"owned"); });
526 world.addFunc(
"registerBehavior", [
vm](ScriptNpcAiWorld* self,
const std::string& json) {
527 return self ? project(
vm, self->registerBehavior(json)) : nullWorld(
vm);
529 world.addFunc(
"validateBehavior", [
vm](ScriptNpcAiWorld* self,
const std::string& json) {
530 return self ?
project(
vm, self->validateBehavior(json)) : nullWorld(
vm);
532 world.addFunc(
"createAgent", [
vm](ScriptNpcAiWorld* self,
const std::string& behaviorId) {
533 return self ?
project(
vm, self->createAgent(behaviorId)) : nullWorld(
vm);
535 world.addFunc(
"destroyAgent", [
vm](ScriptNpcAiWorld* self, std::int64_t agent) {
536 return self ?
project(
vm, self->destroyAgent(agent)) : nullWorld(
vm);
538 world.addFunc(
"isAgentStale", [](ScriptNpcAiWorld* self, std::int64_t agent) {
539 return !self || self->isAgentStale(agent);
541 world.addFunc(
"setBlackboardBool",
542 [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
key,
bool value) {
545 world.addFunc(
"setBlackboardInt",
546 [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
key, std::int64_t
value) {
549 world.addFunc(
"setBlackboardNumber",
550 [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
key,
float value) {
553 world.addFunc(
"setBlackboardString",
554 [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
key,
const std::string&
value) {
557 world.addFunc(
"signal", [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string& signalName) {
558 return self ?
project(
vm, self->signal(agent, signalName)) : nullWorld(
vm);
560 world.addFunc(
"remember", [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
subject,
561 const std::string& sense,
float confidence, std::int64_t observedTick,
562 std::int64_t forgetAfterTicks,
const std::string& payloadJson) {
563 return self ?
project(
vm, self->remember(agent,
subject, sense, confidence, observedTick, forgetAfterTicks,
567 world.addFunc(
"forget", [
vm](ScriptNpcAiWorld* self, std::int64_t agent,
const std::string&
subject,
568 const std::string& sense) {
571 world.addFunc(
"tick", [
vm](ScriptNpcAiWorld* self, std::int64_t simulationTick,
float deltaSeconds,
572 std::int64_t maxAgents, std::int64_t maxTransitionsPerAgent) {
573 return self ?
project(
vm, self->tick(simulationTick, deltaSeconds, maxAgents, maxTransitionsPerAgent))
576 world.addFunc(
"snapshot", [
vm](ScriptNpcAiWorld* self, std::int64_t agent) {
577 return self ?
project(
vm, self->snapshotValue(agent)) : nullWorld(
vm);
579 world.addFunc(
"snapshotJson", [
vm](ScriptNpcAiWorld* self, std::int64_t agent) {
580 return self ?
project(
vm, self->snapshotJson(agent)) : nullWorld(
vm);
583 auto module = table.addClass(name, NpcAi::create, false);
584 module.addFunc("getName", &NpcAi::getName);
585 module.addFunc("newWorld", [vm](NpcAi*, std::int64_t traceCapacity, std::int64_t maxMemoriesPerAgent) {
586 return newWorld(vm, traceCapacity, maxMemoriesPerAgent);
590void NpcAi::expose(ssq::Class&
cls) {
591 cls.addFunc(
"getName", &NpcAi::getName);
592 cls.addFunc(
"newWorld", [
vm =
cls.getHandle()](NpcAi*, std::int64_t traceCapacity,
593 std::int64_t maxMemoriesPerAgent) {
594 return newWorld(vm, traceCapacity, maxMemoriesPerAgent);
struct SQVM * HSQUIRRELVM
vk::UniqueDeviceMemory memory
wgpu::PopErrorScopeStatus status
#define Module_IMPL(ModuleName, newExpr)
Script-facing factory for NpcAiWorld instances.
The single Squirrel projection for common Result, Status and Value.
Explicit Value/Owned/Borrowed semantics at the Squirrel boundary.
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.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static constexpr RuntimeHandle fromPacked(std::uint64_t value) noexcept
Reconstructs a handle from an explicit packed boundary value.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
std::vector< Value > Array
static Result< void > validate(const BehaviorDefinition &definition)
Validates definition shape without publishing it.
Factory module for script-owned NPC AI worlds.
const char * defaultValue
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
ssq::Table project(HSQUIRRELVM vm, const eve::Result< void > &result)
Project a completed editing result that has no payload.
std::variant< bool, std::int64_t, double, std::string > BlackboardValue
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.