mission_script_runtime_edges.cpp 42 KB

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