mission_script_runtime_feed.cpp 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. /**
  2. * The two Host feeds, the queue of accepted rows, and the VM callback each row reaches.
  3. * Every function here runs under the mission runtime lock its caller already holds.
  4. */
  5. #include <Windows.h>
  6. #include <algorithm>
  7. #include <array>
  8. #include <cstddef>
  9. #include <cstdint>
  10. #include <cstdio>
  11. #include <limits>
  12. #include <new>
  13. #include <string_view>
  14. #include <vector>
  15. #include "../../../core/logging/log.h"
  16. #include "../../../state/activity/mission/runtime.h"
  17. #include "../../../state/activity/runtime.h"
  18. #include "../host_runtime.h"
  19. #include "mission_script_region.h"
  20. #include "mission_script_runtime_internal.h"
  21. #include "mission_script_vm.h"
  22. namespace sunrise::server::activity::mission {
  23. namespace {
  24. /** One queued mission event, dispatched once when the drain reaches it. */
  25. struct PendingMissionEvent final {
  26. host::Event event{};
  27. host::SenseObservationSnapshot sense{};
  28. host::ClientMessageSnapshot clientMessage{};
  29. bool missionSequenceObserved{};
  30. bool senseAvailable{};
  31. bool clientMessageAvailable{};
  32. bool occupied{};
  33. };
  34. /** The queue grows with real accepted input and is drained in the tick it arrives. */
  35. std::vector<PendingMissionEvent> g_pendingMissionEvents{};
  36. host::EventCursor g_eventCursor{};
  37. host::MissionInputCursor g_missionInputCursor{};
  38. void clear_pending_event(PendingMissionEvent& pending) noexcept {
  39. SecureZeroMemory(&pending, sizeof(pending));
  40. }
  41. /** @return True for the host events that report an output's progress, not an input. */
  42. [[nodiscard]] bool delivery_lifecycle_event(host::EventKind kind) noexcept {
  43. switch (kind) {
  44. case host::EventKind::authStateCommitted:
  45. case host::EventKind::authStateTransportStaged:
  46. case host::EventKind::authStateCanceled:
  47. case host::EventKind::incidentQueued:
  48. case host::EventKind::incidentTransportStaged:
  49. case host::EventKind::incidentCanceled:
  50. case host::EventKind::incidentRefused:
  51. case host::EventKind::scriptableOverrideCommitted:
  52. case host::EventKind::scriptableOverrideTransportStaged:
  53. case host::EventKind::scriptableOverrideCanceled:
  54. case host::EventKind::operatorRefused:
  55. return true;
  56. default:
  57. return false;
  58. }
  59. }
  60. /**
  61. * @return True for a row that arrives on the ordered mission-input feed and owns a sequence.
  62. * A host-state row must never answer true. It would consume a mission-input sequence it does not
  63. * own, which faults the binding on the next real input.
  64. */
  65. [[nodiscard]] bool host_feed_row(host::EventKind kind) noexcept {
  66. switch (kind) {
  67. case host::EventKind::timerElapsed:
  68. case host::EventKind::effectResult:
  69. case host::EventKind::phaseEntered:
  70. case host::EventKind::triggerEntered:
  71. case host::EventKind::triggerExited:
  72. case host::EventKind::triggerState:
  73. case host::EventKind::squadState:
  74. case host::EventKind::squadProvoked:
  75. case host::EventKind::entitySpawned:
  76. case host::EventKind::entityDied:
  77. case host::EventKind::sceneFinished:
  78. case host::EventKind::sceneOutput:
  79. case host::EventKind::objectiveProgress:
  80. case host::EventKind::sessionJoined:
  81. case host::EventKind::sessionLeft:
  82. case host::EventKind::playerTrigger:
  83. case host::EventKind::cinematicStarted:
  84. case host::EventKind::cinematicSkipRequested:
  85. case host::EventKind::cinematicTerminated:
  86. case host::EventKind::actorPathState:
  87. case host::EventKind::damageState:
  88. case host::EventKind::deviceState:
  89. case host::EventKind::regionChanged:
  90. case host::EventKind::objectState:
  91. case host::EventKind::fireteamState:
  92. case host::EventKind::objectInteracted:
  93. case host::EventKind::ghostLinkState:
  94. return false;
  95. default:
  96. return true;
  97. }
  98. }
  99. /** True when the event may reach a callback for this instance's ActivityClient generation. */
  100. [[nodiscard]] bool eligible_event(const RuntimeInstance& instance,
  101. const host::Event& event) noexcept {
  102. if (event.attemptGeneration != 0 && event.attemptGeneration != instance.attempt.generation) {
  103. return false;
  104. }
  105. if (event.kind == host::EventKind::timerElapsed) {
  106. return true;
  107. }
  108. if (event.sourceGeneration != instance.view.activityClientGeneration) {
  109. return false;
  110. }
  111. return event.kind == host::EventKind::clientStateChanged
  112. || event.kind == host::EventKind::incidentReceived
  113. || event.kind == host::EventKind::clientMessageReceived
  114. || event.kind == host::EventKind::effectResult
  115. || event.kind == host::EventKind::phaseEntered
  116. || event.kind == host::EventKind::triggerEntered
  117. || event.kind == host::EventKind::triggerState
  118. || event.kind == host::EventKind::triggerExited
  119. || event.kind == host::EventKind::squadState
  120. || event.kind == host::EventKind::squadProvoked
  121. || event.kind == host::EventKind::entitySpawned
  122. || event.kind == host::EventKind::entityDied
  123. || event.kind == host::EventKind::sceneFinished
  124. || event.kind == host::EventKind::sceneOutput
  125. || event.kind == host::EventKind::objectiveProgress
  126. || event.kind == host::EventKind::entitySlotsRequested
  127. || event.kind == host::EventKind::sessionJoined
  128. || event.kind == host::EventKind::sessionLeft
  129. || event.kind == host::EventKind::playerTrigger
  130. || event.kind == host::EventKind::cinematicStarted
  131. || event.kind == host::EventKind::actorPathState
  132. || event.kind == host::EventKind::damageState
  133. || event.kind == host::EventKind::deviceState
  134. || event.kind == host::EventKind::regionChanged
  135. || event.kind == host::EventKind::objectState
  136. || event.kind == host::EventKind::fireteamState
  137. || event.kind == host::EventKind::objectInteracted
  138. || event.kind == host::EventKind::ghostLinkState
  139. || event.kind == host::EventKind::cinematicSkipRequested
  140. || event.kind == host::EventKind::cinematicTerminated
  141. || delivery_lifecycle_event(event.kind) || event.has_sense_observations();
  142. }
  143. /** Faults the instance unless the ordered mission input arrives with no gap, starting at one. */
  144. [[nodiscard]] bool validate_mission_sequence(RuntimeInstance& instance,
  145. const host::Event& event) noexcept {
  146. if (event.kind != host::EventKind::senseUpdate
  147. && event.kind != host::EventKind::incidentReceived
  148. && event.kind != host::EventKind::clientStateChanged
  149. && event.kind != host::EventKind::entitySlotsRequested
  150. && event.kind != host::EventKind::clientMessageReceived) {
  151. return true;
  152. }
  153. if (instance.lastMissionSequence == 0) {
  154. if (event.missionSequence == 1) {
  155. return true;
  156. }
  157. fault_instance(instance, "activity mission input did not start at sequence one");
  158. log_line(core::log::Level::warn, &instance, "events", "initial_binding_gap");
  159. return false;
  160. }
  161. const std::uint64_t expected =
  162. instance.lastMissionSequence == (std::numeric_limits<std::uint64_t>::max)()
  163. ? 1
  164. : instance.lastMissionSequence + 1;
  165. if (event.missionSequence == expected) {
  166. return true;
  167. }
  168. fault_instance(instance, "activity mission input sequence has a gap");
  169. log_line(core::log::Level::warn, &instance, "events", "binding_gap");
  170. return false;
  171. }
  172. /** One free queue row, growing the queue by one when none is free; null when it cannot grow. */
  173. [[nodiscard]] PendingMissionEvent* free_pending_event() noexcept {
  174. for (PendingMissionEvent& pending : g_pendingMissionEvents) {
  175. if (!pending.occupied) {
  176. return &pending;
  177. }
  178. }
  179. if (g_pendingMissionEvents.size() == g_pendingMissionEvents.max_size()) {
  180. return nullptr;
  181. }
  182. try {
  183. g_pendingMissionEvents.emplace_back();
  184. } catch (const std::bad_alloc&) {
  185. return nullptr;
  186. }
  187. return &g_pendingMissionEvents.back();
  188. }
  189. /** Copies one accepted input and its values into a queue row; faults the instance when full. */
  190. [[nodiscard]] bool queue_mission_event(RuntimeInstance* instance,
  191. const host::MissionInputEvent& input) noexcept {
  192. PendingMissionEvent* const pending = free_pending_event();
  193. if (pending == nullptr) {
  194. if (instance != nullptr) {
  195. fault_instance(*instance, "mission event queue allocation failed");
  196. clear_pending_events(instance->view.binding);
  197. }
  198. log_line(core::log::Level::warn, instance, "events", "allocation_failed");
  199. return false;
  200. }
  201. clear_pending_event(*pending);
  202. if (input.event.has_sense_observations()) {
  203. pending->senseAvailable =
  204. host::mission_input_sense_snapshot(input.sequence, pending->sense);
  205. }
  206. if (input.event.kind == host::EventKind::clientMessageReceived) {
  207. pending->clientMessageAvailable =
  208. host::mission_input_client_message_snapshot(input.sequence, pending->clientMessage);
  209. }
  210. pending->event = input.event;
  211. pending->occupied = true;
  212. return true;
  213. }
  214. /** @return True when the left sequence comes before the right one across the wrap. */
  215. [[nodiscard]] constexpr bool mission_sequence_precedes(std::uint64_t left,
  216. std::uint64_t right) noexcept {
  217. // Sequences wrap, so ordering holds only inside half the 64-bit range.
  218. constexpr std::uint64_t halfRange = std::uint64_t{1} << 63U;
  219. return left != right && right - left < halfRange;
  220. }
  221. static_assert(mission_sequence_precedes((std::numeric_limits<std::uint64_t>::max)(), 1));
  222. static_assert(!mission_sequence_precedes(1, (std::numeric_limits<std::uint64_t>::max)()));
  223. /** @return True when the durable cursor already committed this retained input row. */
  224. [[nodiscard]] bool mission_sequence_committed(std::uint64_t sequence,
  225. std::uint64_t committed) noexcept {
  226. return committed != 0
  227. && (sequence == committed || mission_sequence_precedes(sequence, committed));
  228. }
  229. /** True when the same binding still holds a queued row with an earlier mission sequence. */
  230. [[nodiscard]] bool has_earlier_pending_event(const PendingMissionEvent& selected) noexcept {
  231. for (const PendingMissionEvent& pending : g_pendingMissionEvents) {
  232. if (pending.occupied && &pending != &selected
  233. && same_binding(pending.event.binding, selected.event.binding)
  234. && mission_sequence_precedes(pending.event.missionSequence,
  235. selected.event.missionSequence)) {
  236. return true;
  237. }
  238. }
  239. return false;
  240. }
  241. /** TODO: no caller. Decide whether the drain gate retires on this or on `binding_matches`. */
  242. [[nodiscard]] bool host_binding_active(const state::activity::SessionBinding& binding) noexcept {
  243. host::InstanceSnapshot snapshot{};
  244. return host::instance_snapshot(binding, snapshot) && snapshot.active;
  245. }
  246. /** Dispatches queued rows in mission-sequence order and retires those no callback can take. */
  247. void drain_pending_mission_events(std::uint64_t now) noexcept {
  248. bool progressed = false;
  249. do {
  250. progressed = false;
  251. for (PendingMissionEvent& pending : g_pendingMissionEvents) {
  252. if (!pending.occupied || has_earlier_pending_event(pending)) {
  253. continue;
  254. }
  255. RuntimeInstance* const instance = find_instance(pending.event.binding);
  256. if (instance == nullptr) {
  257. if (!state::activity::binding_matches(pending.event.binding)) {
  258. clear_pending_event(pending);
  259. progressed = true;
  260. }
  261. continue;
  262. }
  263. if (instance->programStatus != ProgramStatus::loaded) {
  264. clear_pending_event(pending);
  265. progressed = true;
  266. continue;
  267. }
  268. if (instance->startPending) {
  269. continue;
  270. }
  271. if (instance->timerPending) {
  272. continue;
  273. }
  274. if (mission_sequence_committed(pending.event.missionSequence,
  275. instance->lastMissionSequence)) {
  276. clear_pending_event(pending);
  277. progressed = true;
  278. continue;
  279. }
  280. if (!pending.missionSequenceObserved) {
  281. if (!validate_mission_sequence(*instance, pending.event)) {
  282. clear_pending_events(instance->view.binding);
  283. progressed = true;
  284. break;
  285. }
  286. pending.missionSequenceObserved = true;
  287. }
  288. if (!eligible_event(*instance, pending.event)) {
  289. if (!commit_mission_state(
  290. *instance, instance->missionStarted, pending.event.missionSequence)) {
  291. clear_pending_events(instance->view.binding);
  292. progressed = true;
  293. break;
  294. }
  295. clear_pending_event(pending);
  296. progressed = true;
  297. continue;
  298. }
  299. if (pending.event.kind == host::EventKind::senseUpdate && !pending.senseAvailable) {
  300. fault_instance(*instance, "accepted mission Sense values were unavailable");
  301. clear_pending_events(instance->view.binding);
  302. log_line(core::log::Level::warn, instance, "events", "sense_unavailable");
  303. progressed = true;
  304. break;
  305. }
  306. if (pending.event.kind == host::EventKind::clientMessageReceived
  307. && !pending.clientMessageAvailable) {
  308. fault_instance(*instance,
  309. "accepted mission client-message values were unavailable");
  310. clear_pending_events(instance->view.binding);
  311. log_line(core::log::Level::warn, instance, "events", "client_message_unavailable");
  312. progressed = true;
  313. break;
  314. }
  315. lua_vm::Intent intent{};
  316. if (instance->deliveryStage != DeliveryStage::idle
  317. || lua_vm::pending_intent(instance->vm, intent)) {
  318. continue;
  319. }
  320. const host::SenseObservationSnapshot* const sense =
  321. pending.event.kind == host::EventKind::senseUpdate && pending.senseAvailable
  322. ? &pending.sense
  323. : nullptr;
  324. const host::ClientMessageSnapshot* const clientMessage =
  325. pending.event.kind == host::EventKind::clientMessageReceived
  326. && pending.clientMessageAvailable
  327. ? &pending.clientMessage
  328. : nullptr;
  329. static_cast<void>(dispatch_event(*instance, pending.event, sense, clientMessage, now));
  330. clear_pending_event(pending);
  331. progressed = true;
  332. }
  333. } while (progressed);
  334. }
  335. /** @return True when one exact accepted sequence is already retained locally or in this read. */
  336. [[nodiscard]] bool input_sequence_retained(const state::activity::SessionBinding& binding,
  337. std::uint64_t sequence,
  338. const host::MissionInputRead& inputs) noexcept {
  339. for (const PendingMissionEvent& pending : g_pendingMissionEvents) {
  340. if (pending.occupied && same_binding(pending.event.binding, binding)
  341. && pending.event.missionSequence == sequence) {
  342. return true;
  343. }
  344. }
  345. for (std::size_t index = 0; index < inputs.count; ++index) {
  346. if (same_binding(inputs.events[index].event.binding, binding)
  347. && inputs.events[index].event.missionSequence == sequence) {
  348. return true;
  349. }
  350. }
  351. return false;
  352. }
  353. /** @return True when every uncommitted sequence is in this read or the local queue. */
  354. [[nodiscard]] bool
  355. outstanding_input_interval_complete(const state::activity::SessionBinding& binding,
  356. const mission_state::InputSequenceSnapshot& state,
  357. const host::MissionInputRead& inputs) noexcept {
  358. if (state.issued < state.committed) {
  359. return false;
  360. }
  361. const std::uint64_t outstanding = state.issued - state.committed;
  362. if (outstanding > g_pendingMissionEvents.size() + inputs.count) {
  363. return false;
  364. }
  365. std::uint64_t sequence = state.committed;
  366. for (std::uint64_t index = 0; index < outstanding; ++index) {
  367. ++sequence;
  368. if (!input_sequence_retained(binding, sequence, inputs)) {
  369. return false;
  370. }
  371. }
  372. return true;
  373. }
  374. /** Faults only retained bindings whose durable uncommitted interval is provably incomplete. */
  375. void reconcile_input_feed_loss(const host::MissionInputRead& inputs) noexcept {
  376. host::DiagnosticsSnapshot hostState{};
  377. host::snapshot(hostState);
  378. for (std::size_t index = 0; index < hostState.instanceCount; ++index) {
  379. const host::InstanceSnapshot& hostInstance = hostState.instances[index];
  380. if (!state::activity::binding_matches(hostInstance.binding)) {
  381. continue;
  382. }
  383. mission_state::InputSequenceSnapshot inputState{};
  384. if (!mission_state::input_sequence_snapshot(hostInstance.binding, inputState)
  385. || inputState.faulted
  386. || outstanding_input_interval_complete(hostInstance.binding, inputState, inputs)) {
  387. continue;
  388. }
  389. mission_state::Snapshot snapshot{};
  390. const mission_state::Status status =
  391. mission_state::fault_input_feed(hostInstance.binding, snapshot);
  392. RuntimeInstance* const instance = find_instance(hostInstance.binding);
  393. if (status != mission_state::Status::ready) {
  394. log_line(core::log::Level::warn,
  395. instance,
  396. "events",
  397. mission_state::status_name(status),
  398. "reason=feed_gap_fault_refused");
  399. continue;
  400. }
  401. if (instance != nullptr) {
  402. accept_mission_state(*instance, snapshot);
  403. lua_vm::fault(instance->vm, "accepted mission input feed lost a row");
  404. instance->programStatus = ProgramStatus::programError;
  405. }
  406. log_line(core::log::Level::warn, instance, "events", "feed_gap_faulted");
  407. clear_pending_events(hostInstance.binding);
  408. }
  409. }
  410. /** Reads one page of the ordered feed and queues every row not yet committed. */
  411. [[nodiscard]] bool consume_mission_input_page() noexcept {
  412. host::MissionInputRead inputs{};
  413. host::read_mission_inputs_after(g_missionInputCursor, inputs);
  414. if (inputs.reset) {
  415. g_missionInputCursor = {inputs.cursor.generation, 0};
  416. reconcile_input_feed_loss(inputs);
  417. log_line(core::log::Level::warn, nullptr, "events", "mission_feed_reset");
  418. }
  419. if (inputs.gap) {
  420. log_line(core::log::Level::warn, nullptr, "events", "mission_feed_gap");
  421. reconcile_input_feed_loss(inputs);
  422. }
  423. for (std::size_t index = 0; index < inputs.count; ++index) {
  424. const host::MissionInputEvent& input = inputs.events[index];
  425. RuntimeInstance* const instance = find_instance(input.event.binding);
  426. mission_state::InputSequenceSnapshot inputState{};
  427. const bool hasInputState =
  428. mission_state::input_sequence_snapshot(input.event.binding, inputState);
  429. if ((instance == nullptr && !state::activity::binding_matches(input.event.binding))
  430. || (instance != nullptr && instance->programStatus != ProgramStatus::loaded)
  431. || (hasInputState
  432. && (inputState.faulted
  433. || mission_sequence_committed(input.event.missionSequence,
  434. inputState.committed)))) {
  435. g_missionInputCursor.generation = inputs.cursor.generation;
  436. g_missionInputCursor.sequence = input.sequence;
  437. continue;
  438. }
  439. if (instance != nullptr
  440. && mission_sequence_committed(input.event.missionSequence,
  441. instance->lastMissionSequence)) {
  442. g_missionInputCursor.generation = inputs.cursor.generation;
  443. g_missionInputCursor.sequence = input.sequence;
  444. continue;
  445. }
  446. if (instance != nullptr) {
  447. lua_vm::Snapshot diagnostics{};
  448. lua_vm::snapshot(instance->vm, diagnostics);
  449. if (diagnostics.faulted) {
  450. g_missionInputCursor.generation = inputs.cursor.generation;
  451. g_missionInputCursor.sequence = input.sequence;
  452. continue;
  453. }
  454. }
  455. if (!queue_mission_event(instance, input)) {
  456. return false;
  457. }
  458. g_missionInputCursor.generation = inputs.cursor.generation;
  459. g_missionInputCursor.sequence = input.sequence;
  460. }
  461. return inputs.count == host::kMissionInputReadPageSize;
  462. }
  463. } // namespace
  464. /** Retires every queued mission event that belongs to one binding. */
  465. void clear_pending_events(const state::activity::SessionBinding& binding) noexcept {
  466. for (PendingMissionEvent& pending : g_pendingMissionEvents) {
  467. if (pending.occupied && same_binding(pending.event.binding, binding)) {
  468. clear_pending_event(pending);
  469. }
  470. }
  471. }
  472. /** Retires every queued mission event of every binding and frees the queue. */
  473. void clear_all_pending_events() noexcept {
  474. std::vector<PendingMissionEvent>{}.swap(g_pendingMissionEvents);
  475. }
  476. /** Keeps accepted values but removes every VM- and ActivityClient-generation-local decision. */
  477. void reset_pending_events_for_reattach(const state::activity::SessionBinding& binding) noexcept {
  478. for (PendingMissionEvent& pending : g_pendingMissionEvents) {
  479. if (!pending.occupied || !same_binding(pending.event.binding, binding)) {
  480. continue;
  481. }
  482. pending.missionSequenceObserved = false;
  483. }
  484. }
  485. /** Retires rows as soon as authoritative State no longer owns their exact binding. */
  486. void retire_unbound_pending_events() noexcept {
  487. for (PendingMissionEvent& pending : g_pendingMissionEvents) {
  488. if (pending.occupied && !state::activity::binding_matches(pending.event.binding)) {
  489. clear_pending_event(pending);
  490. }
  491. }
  492. }
  493. /** @return Queued rows still owed to one exact binding. */
  494. std::size_t pending_event_count(const state::activity::SessionBinding& binding) noexcept {
  495. std::size_t count = 0;
  496. for (const PendingMissionEvent& pending : g_pendingMissionEvents) {
  497. count += pending.occupied && same_binding(pending.event.binding, binding) ? 1U : 0U;
  498. }
  499. return count;
  500. }
  501. /** @return True when one queued accepted row is still owed to this instance. */
  502. bool has_pending_host_input(const RuntimeInstance& instance) noexcept {
  503. return std::any_of(g_pendingMissionEvents.begin(),
  504. g_pendingMissionEvents.end(),
  505. [&instance](const PendingMissionEvent& pending) noexcept {
  506. return pending.occupied
  507. && same_binding(pending.event.binding, instance.view.binding);
  508. });
  509. }
  510. /** Points both Host cursors at the current feed heads and replays retained accepted inputs. */
  511. void reset_feed_cursors() noexcept {
  512. g_eventCursor = host::current_event_cursor();
  513. g_missionInputCursor = host::current_mission_input_cursor();
  514. // Durable per-binding cursors suppress callbacks that already committed; an evicted
  515. // predecessor still faults as a gap.
  516. g_missionInputCursor.sequence = 0;
  517. }
  518. /** Clears both Host cursors. */
  519. void clear_feed_cursors() noexcept {
  520. g_eventCursor = {};
  521. g_missionInputCursor = {};
  522. }
  523. /**
  524. * Runs one event through the VM, commits what it changed, and faults on a script failure.
  525. * @param sense Values owned by a Sense row, or null.
  526. * @param clientMessage Envelope snapshot owned by a client-message row, or null.
  527. * @return The VM call status; `inactive` when no callback could take the event.
  528. */
  529. lua_vm::CallStatus dispatch_event(RuntimeInstance& instance,
  530. const host::Event& event,
  531. const host::SenseObservationSnapshot* sense,
  532. const host::ClientMessageSnapshot* clientMessage,
  533. std::uint64_t now) noexcept {
  534. if (instance.programStatus == ProgramStatus::missing
  535. && event.kind == host::EventKind::senseUpdate && sense != nullptr) {
  536. push_squad_edges(instance, *sense);
  537. return lua_vm::CallStatus::inactive;
  538. }
  539. if (instance.programStatus != ProgramStatus::loaded || !eligible_event(instance, event)) {
  540. return lua_vm::CallStatus::inactive;
  541. }
  542. ++instance.eventsSeen;
  543. // The three region numbers the script is about to read. A report restates only the leg it
  544. // moved, so `pending` and `current` read -1 on most reports and `held` is the one that says
  545. // where the client is standing.
  546. if (event.kind == host::EventKind::clientStateChanged) {
  547. std::array<char, 64> legs{};
  548. const int written =
  549. std::snprintf(legs.data(),
  550. legs.size(),
  551. "held=%d pending=%d current=%d",
  552. event.heldRegionIndex,
  553. event.clientStateHasRegion ? event.regionIndex : -1,
  554. event.clientStateHasCurrentRegion ? event.currentRegionIndex : -1);
  555. if (written > 0) {
  556. log_line(core::log::Level::debug,
  557. &instance,
  558. "client_state",
  559. "legs",
  560. {legs.data(), static_cast<std::size_t>(written)});
  561. }
  562. }
  563. instance.dispatchAttemptGeneration = event.attemptGeneration;
  564. instance.dispatchInputSequence = event.missionSequence;
  565. const lua_vm::CallStatus status = lua_vm::dispatch(instance.vm, event, clientMessage, now);
  566. if (event.kind == host::EventKind::clientStateChanged) {
  567. // A pending-region report can name the next slice while the player still holds the old
  568. // one, so the held region wins.
  569. if (event.heldRegionIndex >= 0) {
  570. instance.activeRegion = event.heldRegionIndex;
  571. } else if (event.currentRegionIndex >= 0) {
  572. instance.activeRegion = event.currentRegionIndex;
  573. }
  574. host::Event changed{};
  575. if (make_region_changed(event, changed)) {
  576. push_script_event(instance, changed);
  577. }
  578. }
  579. if (event.kind == host::EventKind::incidentReceived) {
  580. push_player_trigger(instance, event);
  581. push_cinematic(instance, event);
  582. }
  583. if (event.kind == host::EventKind::senseUpdate && sense != nullptr) {
  584. observe_player_life(instance, *sense);
  585. publish_fireteam_life(now);
  586. push_trigger_edges(instance, *sense);
  587. push_ghost_edges(instance, *sense);
  588. push_object_interaction_edges(instance, *sense);
  589. push_damage_edges(instance, *sense);
  590. push_actor_path_edges(instance, *sense);
  591. push_squad_edges(instance, *sense);
  592. push_combatant_damage_edges(instance, *sense);
  593. push_device_edges(instance, *sense);
  594. push_scene_edges(instance, *sense);
  595. push_objective_edges(instance, *sense);
  596. }
  597. instance.dispatchAttemptGeneration = 0;
  598. instance.dispatchInputSequence = 0;
  599. note_vm_status(instance, "event", lua_vm::status_name(status));
  600. if (instance.eventsSeen == 1) {
  601. log_line(core::log::Level::info,
  602. &instance,
  603. "dispatch",
  604. lua_vm::status_name(status),
  605. event.kind == host::EventKind::senseUpdate ? "detail=sense"
  606. : event.kind == host::EventKind::clientStateChanged ? "detail=client_state"
  607. : event.kind == host::EventKind::incidentReceived ? "detail=incident"
  608. : event.kind == host::EventKind::entitySlotsRequested
  609. ? "detail=entity_slots_requested"
  610. : event.kind == host::EventKind::timerElapsed ? "detail=timer"
  611. : event.kind == host::EventKind::effectResult ? "detail=effect_result"
  612. : "detail=client_message");
  613. }
  614. const std::uint64_t nextInputSequence =
  615. host_feed_row(event.kind) ? event.missionSequence : instance.lastMissionSequence;
  616. if (status == lua_vm::CallStatus::committed) {
  617. if (!commit_mission_state(instance, true, nextInputSequence)) {
  618. return lua_vm::CallStatus::scriptError;
  619. }
  620. ++instance.eventsCommitted;
  621. lua_vm::Snapshot diagnostics{};
  622. lua_vm::snapshot(instance.vm, diagnostics);
  623. if (event.kind == host::EventKind::clientStateChanged
  624. || diagnostics.stateRevision != instance.lastLoggedRevision) {
  625. instance.lastLoggedRevision = diagnostics.stateRevision;
  626. log_line(core::log::Level::debug,
  627. &instance,
  628. "event",
  629. "committed",
  630. event.kind == host::EventKind::senseUpdate ? "detail=sense"
  631. : event.kind == host::EventKind::clientStateChanged ? "detail=client_state"
  632. : event.kind == host::EventKind::incidentReceived ? "detail=incident"
  633. : event.kind == host::EventKind::entitySlotsRequested
  634. ? "detail=entity_slots_requested"
  635. : event.kind == host::EventKind::timerElapsed ? "detail=timer"
  636. : event.kind == host::EventKind::effectResult ? "detail=effect_result"
  637. : "detail=client_message");
  638. }
  639. return status;
  640. }
  641. if (status == lua_vm::CallStatus::noHandler) {
  642. return commit_mission_state(instance, true, nextInputSequence)
  643. ? status
  644. : lua_vm::CallStatus::scriptError;
  645. }
  646. if (status == lua_vm::CallStatus::inactive) {
  647. return status;
  648. }
  649. lua_vm::Snapshot diagnostics{};
  650. lua_vm::snapshot(instance.vm, diagnostics);
  651. log_line(core::log::Level::warn,
  652. &instance,
  653. "event",
  654. lua_vm::status_name(status),
  655. {},
  656. diagnostics.lastError.data());
  657. persist_mission_fault(instance);
  658. return status;
  659. }
  660. /** Reads new host events, advances delivery, and queues only the lifecycle rows for scripts. */
  661. void consume_delivery_events(std::uint64_t now) noexcept {
  662. host::EventRead events{};
  663. host::read_events_after(g_eventCursor, events);
  664. g_eventCursor = events.cursor;
  665. if (events.reset) {
  666. log_line(core::log::Level::warn, nullptr, "delivery", "event_feed_reset");
  667. return;
  668. }
  669. if (events.gap) {
  670. log_line(core::log::Level::warn, nullptr, "delivery", "event_feed_gap");
  671. }
  672. for (std::size_t index = 0; index < events.count; ++index) {
  673. RuntimeInstance* const instance = find_instance(events.events[index].binding);
  674. if (instance != nullptr) {
  675. observe_delivery_event(*instance, events.events[index], now);
  676. // Sense, client-state, incident and client-message rows reach the script through the
  677. // ordered mission-input feed. Only the delivery lifecycle rows belong in this queue.
  678. if (delivery_lifecycle_event(events.events[index].kind)) {
  679. push_script_event(*instance, events.events[index]);
  680. }
  681. }
  682. }
  683. }
  684. /**
  685. * Reads the ordered mission-input feed and drains the queue. The feed is unbounded and one read
  686. * copies at most a page, so a burst is consumed in the tick it arrives instead of a page a tick.
  687. */
  688. void consume_mission_inputs(std::uint64_t now) noexcept {
  689. drain_pending_mission_events(now);
  690. while (consume_mission_input_page()) {
  691. drain_pending_mission_events(now);
  692. }
  693. drain_pending_mission_events(now);
  694. }
  695. } // namespace sunrise::server::activity::mission