mission_script_runtime_edges.cpp 42 KB

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