mission_script_runtime_edges.cpp 44 KB

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