mission_script_runtime_edges.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. #include <algorithm>
  2. #include <array>
  3. #include <cmath>
  4. #include <cstddef>
  5. #include <cstdint>
  6. #include <cstdio>
  7. #include <span>
  8. #include "../../../middleware/bap/activity_message/damage_monitor_auth.h"
  9. #include "../../../middleware/bap/activity_message/ghost_link_auth.h"
  10. #include "../../../middleware/bap/activity_message/scriptable_auth_body.h"
  11. #include "../../../state/activity_sdk/scene_triggers.h"
  12. #include "../activity_sdk_mission_runtime.h"
  13. #include "mission_script_runtime_internal.h"
  14. #include "mission_script_runtime_objective.h"
  15. #include "mission_script_slot_index.h"
  16. // Sense events retain levels within the owning mission attempt.
  17. namespace sunrise::server::activity::mission {
  18. namespace {
  19. /** Root ordinal of the squad removal flag. The rest of the root is inert on this build. */
  20. constexpr std::uint16_t kSquadRemovalOrdinal = 8;
  21. /** Root ordinal 0 echoes the spawn generation the squad's placement Auth carries. */
  22. constexpr std::uint16_t kSquadSpawnGenerationOrdinal = 0;
  23. /** Root ordinals of the type-20 damage Sense body: health, shield, then the echoed revision. */
  24. constexpr std::uint16_t kDamageHealthOrdinal = 0;
  25. constexpr std::uint16_t kDamageShieldOrdinal = 1;
  26. constexpr std::uint16_t kDamageRevisionOrdinal = 2;
  27. /** Root ordinal of the authored scene's activation token. Signed, bias -2^31. */
  28. constexpr std::uint16_t kSceneTokenOrdinal = 0;
  29. /** Root ordinal of the authored scene's completion latch. A zero-width boolean. */
  30. constexpr std::uint16_t kSceneCompletionOrdinal = 1;
  31. /** The block's ordinal 1 is its one-bit client flag, which separates it from the task list. */
  32. constexpr std::uint16_t kObjectiveBlockFlagOrdinal = 1;
  33. constexpr std::uint8_t kObjectiveBlockFlagWidth = 1;
  34. /** The client clamps a task counter here, so nothing above it is reachable. */
  35. constexpr std::int32_t kObjectiveTaskCountLimit = 80;
  36. namespace sense_values = middleware::bap::activity_message::sense_update;
  37. namespace scenes = activity_sdk_mission;
  38. /** @return The value one schema owns at one ordinal, or null when the body omits it. */
  39. [[nodiscard]] const sense_values::DecodedValue*
  40. sense_value(std::span<const sense_values::DecodedValue> body,
  41. std::uint32_t schemaRow,
  42. std::uint16_t ordinal) noexcept {
  43. for (const sense_values::DecodedValue& value : body) {
  44. if (value.schemaRow == schemaRow && value.fieldOrdinal == ordinal) {
  45. return &value;
  46. }
  47. }
  48. return nullptr;
  49. }
  50. /** @return The boolean at one ordinal. An omitted field asserts false. */
  51. [[nodiscard]] bool sense_flag(std::span<const sense_values::DecodedValue> body,
  52. std::uint32_t schemaRow,
  53. std::uint16_t ordinal) noexcept {
  54. const sense_values::DecodedValue* const value = sense_value(body, schemaRow, ordinal);
  55. return value != nullptr && value->present && value->unsignedValue != 0;
  56. }
  57. /** @return The signed number at one ordinal. An omitted field asserts zero. */
  58. [[nodiscard]] std::int32_t sense_number(std::span<const sense_values::DecodedValue> body,
  59. std::uint32_t schemaRow,
  60. std::uint16_t ordinal) noexcept {
  61. const sense_values::DecodedValue* const value = sense_value(body, schemaRow, ordinal);
  62. return value != nullptr && value->present ? static_cast<std::int32_t>(value->signedValue) : 0;
  63. }
  64. /** @return The identity every derived Sense edge carries. */
  65. [[nodiscard]] host::Event sense_edge_event(const RuntimeInstance& instance,
  66. const host::SenseObservation& observation) noexcept {
  67. host::Event event{};
  68. event.binding = instance.view.binding;
  69. event.sequence = observation.sequence;
  70. event.tick = observation.tick;
  71. event.sourceGeneration = instance.view.activityClientGeneration;
  72. event.missionSequence = instance.lastMissionSequence;
  73. event.firstRegistryKey = observation.key.registryKey;
  74. event.firstSlotIndex = observation.key.slotIndex;
  75. event.firstSlotType = observation.key.slotType;
  76. event.slotObjectTag = observation.key.objectTag;
  77. event.slotSenseSchema = observation.key.senseSchema;
  78. event.senseGenerationPlusOne = observation.generationPlusOne;
  79. event.hasSenseGeneration = observation.hasGeneration;
  80. return event;
  81. }
  82. /** @return A squad edge carrying the squad's retained spawn generation. */
  83. [[nodiscard]] host::Event squad_edge_event(const RuntimeInstance& instance,
  84. const host::SenseObservation& observation,
  85. const SquadObservation& squad) noexcept {
  86. host::Event event = sense_edge_event(instance, observation);
  87. event.squadSpawnGeneration = squad.spawnGeneration;
  88. event.squadHasSpawnGeneration = squad.hasSpawnGeneration;
  89. return event;
  90. }
  91. /** @return The retained record for one squad, allocating on a first observation. */
  92. [[nodiscard]] SquadObservation* find_squad(RuntimeInstance& instance,
  93. const host::SenseObservationKey& key) noexcept {
  94. SquadObservation* spare = nullptr;
  95. for (SquadObservation& retained : instance.squadObservations) {
  96. if (retained.used && retained.registryKey == key.registryKey
  97. && retained.objectTag == key.objectTag && retained.slotIndex == key.slotIndex) {
  98. return &retained;
  99. }
  100. if (!retained.used && spare == nullptr) {
  101. spare = &retained;
  102. }
  103. }
  104. if (spare == nullptr) {
  105. for (SquadObservation& retained : instance.squadObservations) {
  106. bool empty = retained.aliveCount == 0;
  107. for (const auto count : retained.slotCounts) {
  108. empty = empty && count == 0;
  109. }
  110. if (empty) {
  111. retained = {};
  112. spare = &retained;
  113. break;
  114. }
  115. }
  116. }
  117. if (spare == nullptr) {
  118. log_line(core::log::Level::warn, &instance, "squad", "watch_capacity");
  119. return nullptr;
  120. }
  121. spare->registryKey = key.registryKey;
  122. spare->objectTag = key.objectTag;
  123. spare->slotIndex = key.slotIndex;
  124. return spare;
  125. }
  126. /** @return The retained record for one objective sensor, allocating on a first observation. */
  127. [[nodiscard]] ObjectiveObservation* find_objective(RuntimeInstance& instance,
  128. const host::SenseObservationKey& key) noexcept {
  129. ObjectiveObservation* spare = nullptr;
  130. for (ObjectiveObservation& retained : instance.objectiveObservations) {
  131. if (retained.used && retained.registryKey == key.registryKey
  132. && retained.objectTag == key.objectTag && retained.slotIndex == key.slotIndex) {
  133. return &retained;
  134. }
  135. if (!retained.used && spare == nullptr) {
  136. spare = &retained;
  137. }
  138. }
  139. if (spare == nullptr) {
  140. log_line(core::log::Level::warn, &instance, "objective", "watch_capacity");
  141. return nullptr;
  142. }
  143. spare->registryKey = key.registryKey;
  144. spare->objectTag = key.objectTag;
  145. spare->slotIndex = key.slotIndex;
  146. return spare;
  147. }
  148. /** One objective sensor's task counters and the exact set the body carried. */
  149. struct ObjectiveCounters final {
  150. std::array<std::array<std::uint8_t, kObjectiveTaskCapacity>, kObjectiveCapacity> value{};
  151. std::array<std::array<bool, kObjectiveTaskCapacity>, kObjectiveCapacity> present{};
  152. std::uint8_t blocks{};
  153. };
  154. /**
  155. * Copies one objective sensor's task counters out of its two nested schemas.
  156. * The 24 blocks share one schema row, so their order is the only separator: a block opens with its
  157. * own ordinal 0, then its task list. An omitted counter is absent, because zero is a real value.
  158. */
  159. void objective_task_counters(std::span<const sense_values::DecodedValue> body,
  160. std::uint32_t rootSchemaRow,
  161. ObjectiveCounters& output) noexcept {
  162. output = {};
  163. std::uint32_t blockSchemaRow = sense_values::kAbsentRuntimeRow;
  164. for (const sense_values::DecodedValue& value : body) {
  165. if (value.schemaRow != rootSchemaRow && value.fieldOrdinal == kObjectiveBlockFlagOrdinal
  166. && value.width == kObjectiveBlockFlagWidth) {
  167. blockSchemaRow = value.schemaRow;
  168. break;
  169. }
  170. }
  171. if (blockSchemaRow == sense_values::kAbsentRuntimeRow) {
  172. return;
  173. }
  174. std::size_t blocks = 0;
  175. std::size_t tasks = 0;
  176. for (const sense_values::DecodedValue& value : body) {
  177. if (value.schemaRow == rootSchemaRow) {
  178. continue;
  179. }
  180. if (value.schemaRow == blockSchemaRow) {
  181. if (value.fieldOrdinal == 0) {
  182. ++blocks;
  183. tasks = 0;
  184. }
  185. continue;
  186. }
  187. if (blocks == 0 || blocks > output.value.size() || tasks >= kObjectiveTaskCapacity) {
  188. continue;
  189. }
  190. if (value.present) {
  191. const std::int64_t raw = value.signedValue;
  192. const std::int64_t clamped =
  193. raw <= 0 ? 0 : (std::min)(raw, std::int64_t{kObjectiveTaskCountLimit});
  194. output.value[blocks - 1][tasks] = static_cast<std::uint8_t>(clamped);
  195. output.present[blocks - 1][tasks] = true;
  196. }
  197. ++tasks;
  198. }
  199. output.blocks = static_cast<std::uint8_t>((std::min)(blocks, output.value.size()));
  200. }
  201. /**
  202. * Raises one triggerState event for a report its tracker does not drop. The event only informs;
  203. * entered and exited follow the level alone.
  204. * @param event The report's level, already filled in.
  205. */
  206. void push_trigger_state(RuntimeInstance& instance,
  207. const host::SenseObservationSnapshot& sense,
  208. const host::SenseObservation& observation,
  209. TriggerOccupancy& slot,
  210. host::Event event) noexcept {
  211. trigger_observation::Snapshot level{};
  212. const bool valid =
  213. observation.valueCount <= sense.valueCount - observation.firstValue
  214. && trigger_observation::read(std::span(sense.values.data(), sense.valueCount)
  215. .subspan(observation.firstValue, observation.valueCount),
  216. level);
  217. const auto result = slot.tracker.observe(valid ? &level : nullptr,
  218. instance.view.activityClientGeneration,
  219. observation.sourceGeneration,
  220. observation.sequence,
  221. observation.generationPlusOne,
  222. observation.hasGeneration);
  223. if (!result.notify) {
  224. return;
  225. }
  226. event.kind = host::EventKind::triggerState;
  227. event.triggerAvailable = result.available;
  228. event.triggerContinuity = static_cast<std::uint8_t>(result.continuity);
  229. push_script_event(instance, event);
  230. }
  231. } // namespace
  232. /**
  233. * Raises one event per watched volume whose occupancy changed, after its triggerState report.
  234. * The client publishes occupancy as a level, so the edge is ours to derive. A volume seen for the
  235. * first time only records its level, because a first observation is not an entry.
  236. */
  237. void push_trigger_edges(RuntimeInstance& instance,
  238. const host::SenseObservationSnapshot& sense) noexcept {
  239. namespace trigger = trigger_observation;
  240. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  241. const host::SenseObservation& observation = sense.observations[index];
  242. if (observation.key.slotType != format::kOccupancySlotType
  243. || observation.valueCount < trigger::kValueCount
  244. || observation.firstValue + trigger::kValueCount > sense.valueCount) {
  245. continue;
  246. }
  247. const std::span<const sense_values::DecodedValue> body(
  248. &sense.values[observation.firstValue], trigger::kValueCount);
  249. const std::uint32_t root = observation.key.schemaRow;
  250. const bool occupied = sense_flag(body, root, trigger::kOccupiedOrdinal);
  251. TriggerOccupancy* slot = nullptr;
  252. TriggerOccupancy* spare = nullptr;
  253. for (TriggerOccupancy& retained : instance.triggerOccupancy) {
  254. if (retained.used && retained.registryKey == observation.key.registryKey
  255. && retained.objectTag == observation.key.objectTag
  256. && retained.slotIndex == observation.key.slotIndex) {
  257. slot = &retained;
  258. break;
  259. }
  260. if (!retained.used && spare == nullptr) {
  261. spare = &retained;
  262. }
  263. }
  264. const bool first = slot == nullptr;
  265. if (first) {
  266. if (spare == nullptr) {
  267. log_line(core::log::Level::warn, &instance, "trigger", "watch_capacity");
  268. continue;
  269. }
  270. slot = spare;
  271. slot->registryKey = observation.key.registryKey;
  272. slot->objectTag = observation.key.objectTag;
  273. slot->slotIndex = observation.key.slotIndex;
  274. slot->occupied = occupied;
  275. slot->used = true;
  276. }
  277. host::Event event = sense_edge_event(instance, observation);
  278. event.triggerOccupied = occupied;
  279. event.triggerAll = sense_flag(body, root, trigger::kAllOrdinal);
  280. event.triggerCount = sense_number(body, root, trigger::kCountOrdinal);
  281. event.triggerValue = sense_number(body, root, trigger::kThresholdOrdinal);
  282. push_trigger_state(instance, sense, observation, *slot, event);
  283. if (first || slot->occupied == occupied) {
  284. continue;
  285. }
  286. slot->occupied = occupied;
  287. event.kind = occupied ? host::EventKind::triggerEntered : host::EventKind::triggerExited;
  288. push_script_event(instance, event);
  289. }
  290. }
  291. namespace {
  292. /** @return True when the observation is one complete body of the given slot type and schema. */
  293. [[nodiscard]] bool observation_of(const host::SenseObservation& observation,
  294. const host::SenseObservationSnapshot& sense,
  295. std::uint32_t slotType,
  296. std::uint32_t senseSchema) noexcept {
  297. return observation.key.slotType == slotType && observation.key.senseSchema == senseSchema
  298. && observation.valueCount != 0 && observation.firstValue <= sense.valueCount
  299. && observation.valueCount <= sense.valueCount - observation.firstValue;
  300. }
  301. /** @return The decoded values of one observation. */
  302. [[nodiscard]] std::span<const sense_values::DecodedValue>
  303. observation_values(const host::SenseObservation& observation,
  304. const host::SenseObservationSnapshot& sense) noexcept {
  305. return std::span(sense.values).subspan(observation.firstValue, observation.valueCount);
  306. }
  307. } // namespace
  308. /** Raises one event per named actor whose movement or delivery level changed. */
  309. void push_actor_path_edges(RuntimeInstance& instance,
  310. const host::SenseObservationSnapshot& sense) noexcept {
  311. namespace auth = middleware::bap::activity_message::scriptable_auth;
  312. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  313. const host::SenseObservation& observation = sense.observations[index];
  314. if (!observation_of(observation, sense, auth::kType2SlotType, auth::kType2SenseSchema)) {
  315. continue;
  316. }
  317. ActorPathObservation* const slot =
  318. find_slot_row(instance.actorPathObservations, observation.key);
  319. if (slot == nullptr
  320. || !update_actor_path_level(
  321. slot->level, observation_values(observation, sense), observation.key.schemaRow)) {
  322. continue;
  323. }
  324. host::Event event = sense_edge_event(instance, observation);
  325. event.kind = host::EventKind::actorPathState;
  326. event.actorGeneration = slot->level.generation;
  327. event.actorPathRevision = slot->level.revision;
  328. event.actorPathState = slot->level.state;
  329. event.actorDeliveryRevision = slot->level.deliveryRevision;
  330. event.actorDeliveryState = slot->level.deliveryState;
  331. event.actorDeliveryKnown = (slot->level.seen & kActorSeenDelivery) == kActorSeenDelivery;
  332. event.actorSuppressed = slot->level.suppressed;
  333. std::array<char, 160> details{};
  334. const int written =
  335. std::snprintf(details.data(),
  336. details.size(),
  337. "registry=%08X slot=%u generation=%d revision=%d "
  338. "state=%d suppressed=%u delivery_revision=%d delivery_state=%d",
  339. slot->registryKey,
  340. static_cast<unsigned>(slot->slotIndex),
  341. slot->level.generation,
  342. slot->level.revision,
  343. slot->level.state,
  344. slot->level.suppressed ? 1U : 0U,
  345. event.actorDeliveryKnown ? slot->level.deliveryRevision : -1,
  346. event.actorDeliveryKnown ? slot->level.deliveryState : -1);
  347. if (written > 0 && static_cast<std::size_t>(written) < details.size()) {
  348. log_line(core::log::Level::debug,
  349. &instance,
  350. "actor_path",
  351. "changed",
  352. {details.data(), static_cast<std::size_t>(written)});
  353. }
  354. push_script_event(instance, event);
  355. }
  356. }
  357. /** Raises one event per damage monitor whose health, shield or revision changed. */
  358. void push_damage_edges(RuntimeInstance& instance,
  359. const host::SenseObservationSnapshot& sense) noexcept {
  360. namespace damage = middleware::bap::activity_message::damage_monitor;
  361. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  362. const host::SenseObservation& observation = sense.observations[index];
  363. if (!observation_of(observation, sense, damage::kSlotType, damage::kSenseSchema)) {
  364. continue;
  365. }
  366. float health = -1.0F;
  367. float shield = -1.0F;
  368. std::int32_t revision = 0;
  369. bool revisionKnown = false;
  370. for (const sense_values::DecodedValue& value : observation_values(observation, sense)) {
  371. if (!value.present || value.schemaRow != observation.key.schemaRow) {
  372. continue;
  373. }
  374. if (value.fieldOrdinal == kDamageHealthOrdinal) {
  375. health = value.realValue;
  376. } else if (value.fieldOrdinal == kDamageShieldOrdinal) {
  377. shield = value.realValue;
  378. } else if (value.fieldOrdinal == kDamageRevisionOrdinal) {
  379. revision = static_cast<std::int32_t>(value.signedValue);
  380. revisionKnown = true;
  381. }
  382. }
  383. if (!revisionKnown || revision <= 0 || !std::isfinite(health) || !std::isfinite(shield)) {
  384. continue;
  385. }
  386. DamageObservation* const row = find_slot_row(instance.damageObservations, observation.key);
  387. if (row == nullptr || revision < row->revision
  388. || (row->revision == revision && row->health == health && row->shield == shield)) {
  389. continue;
  390. }
  391. row->revision = revision;
  392. row->health = health;
  393. row->shield = shield;
  394. host::Event event = sense_edge_event(instance, observation);
  395. event.kind = host::EventKind::damageState;
  396. event.damageHealth = health;
  397. event.damageShield = shield;
  398. event.damageRevision = revision;
  399. std::array<char, 128> details{};
  400. const int written =
  401. std::snprintf(details.data(),
  402. details.size(),
  403. "registry=%08X slot=%u revision=%d health=%.4f shield=%.4f",
  404. observation.key.registryKey,
  405. static_cast<unsigned>(observation.key.slotIndex),
  406. revision,
  407. static_cast<double>(health),
  408. static_cast<double>(shield));
  409. if (written > 0 && static_cast<std::size_t>(written) < details.size()) {
  410. log_line(core::log::Level::debug,
  411. &instance,
  412. "damage",
  413. "changed",
  414. {details.data(), static_cast<std::size_t>(written)});
  415. }
  416. push_script_event(instance, event);
  417. }
  418. }
  419. /** Raises object state and accepted interaction events per interactable object. */
  420. void push_object_interaction_edges(RuntimeInstance& instance,
  421. const host::SenseObservationSnapshot& sense) noexcept {
  422. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  423. const host::SenseObservation& observation = sense.observations[index];
  424. if (!observation_of(
  425. observation, sense, format::kObjectSlotType, format::kObjectSenseSchema)) {
  426. continue;
  427. }
  428. ObjectInteractionObservation* const slot =
  429. find_slot_row(instance.objectInteractionObservations, observation.key);
  430. if (slot == nullptr) {
  431. continue;
  432. }
  433. const ObjectInteractionLevel before = slot->level;
  434. const bool interacted = update_object_interaction(
  435. slot->level, observation_values(observation, sense), observation.key.schemaRow);
  436. const ObjectInteractionLevel& level = slot->level;
  437. host::Event event = sense_edge_event(instance, observation);
  438. event.initialObservation = !before.stateKnown || before.generation != level.generation;
  439. event.objectEntryIndex = level.entryIndex;
  440. event.objectSpawnMask = level.spawnMask;
  441. event.objectGeneration = level.generation;
  442. event.objectPresent = level.present;
  443. // The alive lane carries Sense ordinal 1, which the script reads as interaction_open.
  444. event.objectAlive = level.interactionOpen;
  445. event.objectOwnerKnown = level.ownerKnown;
  446. event.objectHasOwner = level.hasOwner;
  447. event.objectOwnerKey = level.ownerKey;
  448. const bool stateChanged =
  449. level.generationKnown && level.generation > 0 && level.stateKnown
  450. && (!before.stateKnown || before.generation != level.generation
  451. || before.present != level.present
  452. || before.interactionOpen != level.interactionOpen
  453. || before.ownerKnown != level.ownerKnown || before.hasOwner != level.hasOwner
  454. || before.ownerKey != level.ownerKey);
  455. if (stateChanged) {
  456. event.kind = host::EventKind::objectState;
  457. std::array<char, 128> details{};
  458. const int written = std::snprintf(details.data(),
  459. details.size(),
  460. "registry=%08X slot=%u generation=%d present=%u "
  461. "interaction_open=%u owner_known=%u has_owner=%u",
  462. observation.key.registryKey,
  463. static_cast<unsigned>(observation.key.slotIndex),
  464. level.generation,
  465. level.present ? 1U : 0U,
  466. level.interactionOpen ? 1U : 0U,
  467. level.ownerKnown ? 1U : 0U,
  468. level.hasOwner ? 1U : 0U);
  469. if (written > 0 && static_cast<std::size_t>(written) < details.size()) {
  470. log_line(core::log::Level::debug,
  471. &instance,
  472. "object",
  473. "changed",
  474. {details.data(), static_cast<std::size_t>(written)});
  475. }
  476. push_script_event(instance, event);
  477. }
  478. if (interacted) {
  479. event.kind = host::EventKind::objectInteracted;
  480. push_script_event(instance, event);
  481. }
  482. }
  483. }
  484. /** Raises one event per Ghost link whose level changed. */
  485. void push_ghost_edges(RuntimeInstance& instance,
  486. const host::SenseObservationSnapshot& sense) noexcept {
  487. namespace ghost = middleware::bap::activity_message::ghost_link;
  488. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  489. const host::SenseObservation& observation = sense.observations[index];
  490. if (!observation_of(observation, sense, ghost::kSlotType, ghost::kSenseSchema)) {
  491. continue;
  492. }
  493. GhostObservation* const slot = find_slot_row(instance.ghostObservations, observation.key);
  494. if (slot == nullptr
  495. || !update_ghost_level(
  496. slot->level, observation_values(observation, sense), observation.key.schemaRow)) {
  497. continue;
  498. }
  499. host::Event event = sense_edge_event(instance, observation);
  500. event.kind = host::EventKind::ghostLinkState;
  501. event.ghostGeneration = slot->level.generation;
  502. event.ghostProgress = slot->level.progress;
  503. event.ghostActive = slot->level.active;
  504. push_script_event(instance, event);
  505. }
  506. publish_ghost_levels(instance);
  507. }
  508. /** Hands the VM the retained Ghost-link levels, so a callback can read one it did not receive. */
  509. void publish_ghost_levels(RuntimeInstance& instance) noexcept {
  510. std::array<GhostLinkRow, kGhostObservationCapacity> rows{};
  511. std::size_t count = 0;
  512. for (const GhostObservation& observation : instance.ghostObservations) {
  513. if (observation.used) {
  514. rows[count++] = {observation.level,
  515. observation.registryKey,
  516. observation.objectTag,
  517. observation.slotIndex};
  518. }
  519. }
  520. lua_vm::publish_ghost_levels(instance.vm, std::span(rows).first(count));
  521. }
  522. /**
  523. * Raises the squad events derived from one msg 6 body.
  524. * The client publishes levels, so a first sighting only records them. A slot count that rose is the
  525. * only spawn signal, and the alive count falling is the only death signal.
  526. */
  527. void push_squad_edges(RuntimeInstance& instance,
  528. const host::SenseObservationSnapshot& sense) noexcept {
  529. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  530. const host::SenseObservation& observation = sense.observations[index];
  531. if (observation.sourceGeneration != sense.sourceGeneration
  532. || observation.key.slotType != format::kSquadSlotType
  533. || observation.key.senseSchema != format::kSquadSenseSchema
  534. || observation.firstValue + observation.valueCount > sense.valueCount) {
  535. continue;
  536. }
  537. const std::span<const sense_values::DecodedValue> body(
  538. &sense.values[observation.firstValue], observation.valueCount);
  539. const std::uint32_t root = observation.key.schemaRow;
  540. if (!observe_population(instance, observation, body)) {
  541. return;
  542. }
  543. SquadObservation* const squad = find_squad(instance, observation.key);
  544. if (squad == nullptr) {
  545. continue;
  546. }
  547. const bool costsChanged = update_squad_objective_costs(squad->objectiveCosts, body, root);
  548. // Cost-only deltas retain combat counts; they must never manufacture a death.
  549. std::int32_t alive = squad->aliveCount;
  550. const bool hasAlive = read_squad_alive(body, root, alive);
  551. const bool removal = sense_flag(body, root, kSquadRemovalOrdinal);
  552. auto counts = squad->slotCounts;
  553. auto countLength = squad->slotCountLength;
  554. std::array<std::int32_t, host::kSquadSlotCapacity> incomingCounts{};
  555. const auto incomingLength = read_squad_created_counts(body, incomingCounts);
  556. if (incomingLength != 0) {
  557. counts = incomingCounts;
  558. countLength = incomingLength;
  559. }
  560. if (!hasAlive && !costsChanged && incomingLength == 0) {
  561. continue;
  562. }
  563. const bool first = !squad->used;
  564. squad->used = true;
  565. squad->populationKnown = squad->populationKnown || hasAlive;
  566. // An absent spawn generation keeps the last one reported, like the other retained levels.
  567. const sense_values::DecodedValue* const spawn =
  568. sense_value(body, root, kSquadSpawnGenerationOrdinal);
  569. if (spawn != nullptr && spawn->present && spawn->signedValue >= 0
  570. && spawn->signedValue <= kMaximumCounter) {
  571. squad->spawnGeneration = static_cast<std::int32_t>(spawn->signedValue);
  572. squad->hasSpawnGeneration = true;
  573. }
  574. const std::int32_t previousAlive = first ? 0 : squad->aliveCount;
  575. const bool changed =
  576. costsChanged || first || squad->aliveCount != alive || squad->removalFlag != removal
  577. || squad->slotCountLength != countLength || squad->slotCounts != counts;
  578. if (!changed) {
  579. continue;
  580. }
  581. host::Event state = squad_edge_event(instance, observation, *squad);
  582. state.squadObjectiveCosts = squad->objectiveCosts.values;
  583. state.squadObjectiveCostMask = squad->objectiveCosts.known;
  584. state.squadObjectiveRevision = squad->objectiveCosts.revision;
  585. qualify_objective_costs(instance, state);
  586. state.squadAliveCount = alive;
  587. state.squadPreviousAliveCount = previousAlive;
  588. state.squadPopulationAvailable = squad->populationKnown;
  589. state.squadRemovalFlag = removal;
  590. state.squadSlotCounts = counts;
  591. state.squadSlotCountLength = countLength;
  592. state.initialObservation = first;
  593. state.kind = host::EventKind::squadState;
  594. if (instance.programStatus == ProgramStatus::loaded) {
  595. push_script_event(instance, state);
  596. }
  597. for (std::uint8_t slot = 0; slot < countLength; ++slot) {
  598. const std::int32_t previous =
  599. !first && slot < squad->slotCountLength ? squad->slotCounts[slot] : 0;
  600. if (counts[slot] <= previous) {
  601. continue;
  602. }
  603. host::Event spawned = squad_edge_event(instance, observation, *squad);
  604. spawned.squadSlotOrdinal = slot;
  605. spawned.squadSlotValue = counts[slot];
  606. spawned.squadPreviousSlotValue = previous;
  607. spawned.kind = host::EventKind::entitySpawned;
  608. if (instance.programStatus == ProgramStatus::loaded) {
  609. push_script_event(instance, spawned);
  610. }
  611. }
  612. if (!first && alive < squad->aliveCount) {
  613. host::Event died = squad_edge_event(instance, observation, *squad);
  614. died.squadAliveCount = alive;
  615. died.squadPreviousAliveCount = squad->aliveCount;
  616. died.squadRemovalFlag = removal;
  617. died.kind = host::EventKind::entityDied;
  618. if (instance.programStatus == ProgramStatus::loaded) {
  619. push_script_event(instance, died);
  620. }
  621. }
  622. squad->aliveCount = alive;
  623. squad->removalFlag = removal;
  624. squad->slotCounts = counts;
  625. squad->slotCountLength = countLength;
  626. }
  627. }
  628. /** @return The scene's gate rows; empty when its slot does not resolve. */
  629. [[nodiscard]] std::span<const format::AuthoredSceneEventKey>
  630. scene_gate_rows(const RuntimeInstance& instance, const host::SenseObservationKey& key) noexcept {
  631. if (instance.view.catalog == nullptr) {
  632. return {};
  633. }
  634. const slot_index::SenseKey indexKey{.registryKey = key.registryKey,
  635. .objectTag = key.objectTag,
  636. .senseSchema = key.senseSchema,
  637. .slotIndex = key.slotIndex,
  638. .slotType = key.slotType};
  639. slot_index::Found found{};
  640. if (!slot_index::by_sense(instance.view, indexKey, found)) {
  641. return {};
  642. }
  643. return sdk::scene_gates(instance.view.catalog->authored_scene_event_keys(), found.nativeRow);
  644. }
  645. /**
  646. * Records the output keys a scene body lists, raising one event per key new to this run.
  647. * Any root value equal to an output key counts; the keys are name hashes, so no other field
  648. * carries one.
  649. */
  650. void record_scene_outputs(RuntimeInstance& instance,
  651. const host::SenseObservation& observation,
  652. std::span<const sense_values::DecodedValue> body,
  653. std::uint32_t root,
  654. SceneObservation& scene,
  655. bool raise) noexcept {
  656. const auto gates = scene_gate_rows(instance, observation.key);
  657. for (const sense_values::DecodedValue& value : body) {
  658. const auto key = static_cast<std::uint32_t>(value.unsignedValue);
  659. if (value.schemaRow != root || !value.present
  660. || std::find(scene.outputs.begin(), scene.outputs.begin() + scene.outputCount, key)
  661. != scene.outputs.begin() + scene.outputCount
  662. || std::none_of(gates.begin(), gates.end(), [key](const auto& gate) {
  663. return gate.key == key && sdk::scene_trigger_is_output(gate);
  664. })) {
  665. continue;
  666. }
  667. if (scene.outputCount == scene.outputs.size()) {
  668. log_line(core::log::Level::warn, &instance, "scene", "output_capacity");
  669. return;
  670. }
  671. scene.outputs[scene.outputCount++] = key;
  672. if (!raise) {
  673. continue;
  674. }
  675. host::Event event = sense_edge_event(instance, observation);
  676. event.sceneActivationToken = scene.token;
  677. event.sceneOutputKey = key;
  678. event.kind = host::EventKind::sceneOutput;
  679. push_script_event(instance, event);
  680. }
  681. }
  682. /**
  683. * Raises one event per watched authored scene that latched complete, and one per output gate a
  684. * scene newly lists. The completion field is a latch, so only the false to true edge is a finish.
  685. * A scene seen for the first time records its level and outputs and raises nothing.
  686. */
  687. void push_scene_edges(RuntimeInstance& instance,
  688. const host::SenseObservationSnapshot& sense) noexcept {
  689. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  690. const host::SenseObservation& observation = sense.observations[index];
  691. if (observation.key.slotType != format::kAuthoredSceneSlotType
  692. || observation.key.senseSchema != format::kAuthoredSceneSenseSchema
  693. || observation.firstValue + observation.valueCount > sense.valueCount) {
  694. continue;
  695. }
  696. const std::span<const sense_values::DecodedValue> body(
  697. &sense.values[observation.firstValue], observation.valueCount);
  698. const std::uint32_t root = observation.key.schemaRow;
  699. // An unchanged scene sends an empty delta, which must not read as a completion of false.
  700. const bool hasCompletion = sense_value(body, root, kSceneCompletionOrdinal) != nullptr;
  701. const bool completed = sense_flag(body, root, kSceneCompletionOrdinal);
  702. const sense_values::DecodedValue* const token = sense_value(body, root, kSceneTokenOrdinal);
  703. SceneObservation* slot = nullptr;
  704. SceneObservation* spare = nullptr;
  705. for (SceneObservation& retained : instance.sceneObservations) {
  706. if (retained.used && retained.registryKey == observation.key.registryKey
  707. && retained.objectTag == observation.key.objectTag
  708. && retained.slotIndex == observation.key.slotIndex) {
  709. slot = &retained;
  710. break;
  711. }
  712. if (!retained.used && spare == nullptr) {
  713. spare = &retained;
  714. }
  715. }
  716. if (slot == nullptr) {
  717. if (spare == nullptr) {
  718. log_line(core::log::Level::warn, &instance, "scene", "watch_capacity");
  719. continue;
  720. }
  721. *spare = {};
  722. spare->registryKey = observation.key.registryKey;
  723. spare->objectTag = observation.key.objectTag;
  724. spare->slotIndex = observation.key.slotIndex;
  725. spare->completed = completed;
  726. spare->token = sense_number(body, root, kSceneTokenOrdinal);
  727. spare->used = true;
  728. record_scene_outputs(instance, observation, body, root, *spare, false);
  729. continue;
  730. }
  731. // A new activation token is a new run of the scene, whose outputs start empty.
  732. if (token != nullptr && token->present
  733. && static_cast<std::int32_t>(token->signedValue) != slot->token) {
  734. slot->token = static_cast<std::int32_t>(token->signedValue);
  735. slot->outputCount = 0;
  736. }
  737. record_scene_outputs(instance, observation, body, root, *slot, true);
  738. if (!hasCompletion) {
  739. continue;
  740. }
  741. const bool rising = completed && !slot->completed;
  742. slot->completed = completed;
  743. if (!rising) {
  744. continue;
  745. }
  746. host::Event event = sense_edge_event(instance, observation);
  747. event.sceneActivationToken = sense_number(body, root, kSceneTokenOrdinal);
  748. event.kind = host::EventKind::sceneFinished;
  749. push_script_event(instance, event);
  750. }
  751. }
  752. /**
  753. * Raises one event per watched objective task counter that rose.
  754. * The client owns the counter and raises it on an actor teardown, so a rise is the only progress
  755. * signal. A sensor seen for the first time records its counters and raises nothing.
  756. */
  757. void push_objective_edges(RuntimeInstance& instance,
  758. const host::SenseObservationSnapshot& sense) noexcept {
  759. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  760. const host::SenseObservation& observation = sense.observations[index];
  761. if (observation.key.slotType != format::kObjectiveSlotType
  762. || observation.key.senseSchema != format::kObjectiveSenseSchema
  763. || observation.firstValue + observation.valueCount > sense.valueCount) {
  764. continue;
  765. }
  766. const std::span<const sense_values::DecodedValue> body(
  767. &sense.values[observation.firstValue], observation.valueCount);
  768. ObjectiveCounters counters{};
  769. objective_task_counters(body, observation.key.schemaRow, counters);
  770. // An unchanged sensor sends an empty delta, which must not read as every counter at zero.
  771. if (counters.blocks == 0) {
  772. continue;
  773. }
  774. ObjectiveObservation* const watched = find_objective(instance, observation.key);
  775. if (watched == nullptr) {
  776. continue;
  777. }
  778. const bool first = !watched->used;
  779. watched->used = true;
  780. for (std::uint8_t block = 0; block < counters.blocks; ++block) {
  781. for (std::size_t task = 0; task < kObjectiveTaskCapacity; ++task) {
  782. if (!counters.present[block][task]) {
  783. continue;
  784. }
  785. const std::uint8_t value = counters.value[block][task];
  786. const std::uint8_t previous = watched->counters[block][task];
  787. watched->counters[block][task] = value;
  788. if (first || value <= previous) {
  789. continue;
  790. }
  791. host::Event event = sense_edge_event(instance, observation);
  792. event.objectiveOrdinal = block;
  793. event.objectiveTaskOrdinal = static_cast<std::uint8_t>(task);
  794. event.objectiveTaskCount = value;
  795. event.objectivePreviousTaskCount = previous;
  796. event.kind = host::EventKind::objectiveProgress;
  797. push_script_event(instance, event);
  798. }
  799. }
  800. }
  801. }
  802. } // namespace sunrise::server::activity::mission