mission_script_runtime.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. /**
  2. * The mission-program instance table, the durable state commit, the timers and the service slice.
  3. * The service slice and every lifecycle entry point take the mission runtime lock.
  4. */
  5. #include "mission_script_runtime.h"
  6. #include <Windows.h>
  7. #include <algorithm>
  8. #include <array>
  9. #include <cstddef>
  10. #include <cstdio>
  11. #include <limits>
  12. #include <new>
  13. #include <string_view>
  14. #include <vector>
  15. #include "../../../core/logging/log.h"
  16. #include "../../../core/settings/settings.h"
  17. #include "../../../state/activity/mission/runtime.h"
  18. #include "../../../state/activity/runtime.h"
  19. #include "../../gameplay/squad_entity_retirement.h"
  20. #include "../host_runtime.h"
  21. #include "mission_script_event_batch.h"
  22. #include "mission_script_runtime_internal.h"
  23. #include "mission_script_vm.h"
  24. namespace sunrise::server::activity::mission {
  25. std::array<RuntimeInstance, host::kInstanceCapacity> g_instances{};
  26. namespace {
  27. SRWLOCK g_lock{SRWLOCK_INIT};
  28. bool g_enabled{};
  29. bool g_pathReady{};
  30. static_assert(mission_state::kSquadMemberCapacity == lua_vm::kSquadMemberCapacity);
  31. /** Copies the complete VM outbox into typed State values in delivery order. */
  32. [[nodiscard]] bool
  33. snapshot_state_intents(const lua_vm::Vm& vm,
  34. std::vector<mission_state::TypedIntent>& output) noexcept {
  35. return lua_vm::snapshot_intents(vm, output);
  36. }
  37. [[nodiscard]] bool earlier_timer(const lua_vm::MissionTimer& left,
  38. const lua_vm::MissionTimer& right) noexcept {
  39. return left.deadlineTick < right.deadlineTick
  40. || (left.deadlineTick == right.deadlineTick && left.sequence < right.sequence);
  41. }
  42. /** @return The life of the instance's own player, from its retained participation levels. */
  43. [[nodiscard]] PlayerLife own_player_life(const RuntimeInstance& instance) noexcept {
  44. if (!instance.occupied
  45. || instance.playerLifeGeneration != instance.view.activityClientGeneration) {
  46. return PlayerLife::unknown;
  47. }
  48. for (const PlayerLifeObservation& life : instance.playerLife) {
  49. if (instance.playerKey != 0 && life.playerKey == instance.playerKey) {
  50. return life.life();
  51. }
  52. }
  53. return PlayerLife::unknown;
  54. }
  55. } // namespace
  56. /**
  57. * Writes one bounded mission-script diagnostic line.
  58. * @param instance Binding the line reports, or null before one is bound.
  59. * @param fields Extra key=value pairs, appended as written.
  60. * @param error Free text; quoted and placed last so it never splits the pairs.
  61. */
  62. void log_line(core::log::Level level,
  63. const RuntimeInstance* instance,
  64. std::string_view stage,
  65. std::string_view result,
  66. std::string_view fields,
  67. std::string_view error) noexcept {
  68. std::array<char, core::log::kLineCapacity> line{};
  69. const unsigned long long session = instance == nullptr ? 0 : instance->view.binding.sessionId;
  70. const unsigned activityRow = instance == nullptr ? 0 : instance->identity.activityRow;
  71. const int written = std::snprintf(line.data(),
  72. line.size(),
  73. "ev=mission_script stage=%.*s result=%.*s session=%llu "
  74. "activity_row=%u",
  75. static_cast<int>(stage.size()),
  76. stage.data(),
  77. static_cast<int>(result.size()),
  78. result.data(),
  79. session,
  80. activityRow);
  81. if (written <= 0) {
  82. return;
  83. }
  84. std::size_t length = (std::min)(static_cast<std::size_t>(written), line.size() - 1);
  85. if (!fields.empty()) {
  86. const int piece = std::snprintf(line.data() + length,
  87. line.size() - length,
  88. " %.*s",
  89. static_cast<int>(fields.size()),
  90. fields.data());
  91. if (piece > 0) {
  92. length = (std::min)(length + static_cast<std::size_t>(piece), line.size() - 1);
  93. }
  94. }
  95. if (!error.empty()) {
  96. const int piece = std::snprintf(line.data() + length,
  97. line.size() - length,
  98. " error=\"%.*s\"",
  99. static_cast<int>(error.size()),
  100. error.data());
  101. if (piece > 0) {
  102. length = (std::min)(length + static_cast<std::size_t>(piece), line.size() - 1);
  103. }
  104. }
  105. core::log::write(core::log::Channel::server, level, {line.data(), length});
  106. }
  107. /**
  108. * Raises a fireteam event on each private instance whose party life counts changed. The party
  109. * is the committed destination peers; a missing or loading member counts as unknown.
  110. */
  111. void publish_fireteam_life(std::uint64_t now) noexcept {
  112. for (RuntimeInstance& instance : g_instances) {
  113. if (!instance.occupied || instance.publicTarget || !instance.sessionRosterObserved
  114. || instance.programStatus != ProgramStatus::loaded) {
  115. continue;
  116. }
  117. FireteamLife counts{};
  118. counts.add(own_player_life(instance));
  119. for (const SessionRosterWatch& peer : instance.sessionRoster) {
  120. if (!peer.used) {
  121. continue;
  122. }
  123. PlayerLife life = PlayerLife::unknown;
  124. for (const RuntimeInstance& candidate : g_instances) {
  125. if (candidate.occupied && !candidate.publicTarget
  126. && candidate.view.binding.sessionId == peer.sessionId
  127. && candidate.view.binding.createdRevision == peer.createdRevision) {
  128. life = own_player_life(candidate);
  129. break;
  130. }
  131. }
  132. counts.add(life);
  133. }
  134. if (instance.fireteamLifePublished && counts == instance.lastFireteamLife) {
  135. continue;
  136. }
  137. instance.lastFireteamLife = counts;
  138. instance.fireteamLifePublished = true;
  139. host::Event event{};
  140. event.kind = host::EventKind::fireteamState;
  141. event.binding = instance.view.binding;
  142. event.sequence = instance.missionStateRevision;
  143. event.sourceGeneration = instance.view.activityClientGeneration;
  144. event.missionSequence = instance.lastMissionSequence;
  145. event.tick = now;
  146. event.fireteamAlive = counts.alive;
  147. event.fireteamDead = counts.dead;
  148. event.fireteamUnknown = counts.unknown;
  149. push_script_event(instance, event);
  150. std::array<char, 96> fields{};
  151. const int length = std::snprintf(fields.data(),
  152. fields.size(),
  153. "alive=%u dead=%u unknown=%u",
  154. static_cast<unsigned>(counts.alive),
  155. static_cast<unsigned>(counts.dead),
  156. static_cast<unsigned>(counts.unknown));
  157. if (length > 0) {
  158. log_line(core::log::Level::debug,
  159. &instance,
  160. "fireteam_life",
  161. "changed",
  162. {fields.data(), static_cast<std::size_t>(length)});
  163. }
  164. }
  165. }
  166. /** Merges the type-13 participation records of one Sense snapshot into the instance. */
  167. void observe_player_life(RuntimeInstance& instance,
  168. const host::SenseObservationSnapshot& sense) noexcept {
  169. if (instance.playerLifeGeneration != sense.sourceGeneration) {
  170. instance.playerLife = {};
  171. instance.playerLifeGeneration = sense.sourceGeneration;
  172. }
  173. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  174. const host::SenseObservation& observation = sense.observations[index];
  175. if (observation.key.slotType != kParticipationSlotType
  176. || observation.key.senseSchema != kParticipationSenseSchema
  177. || observation.key.objectTag != kParticipationObjectTag
  178. || observation.key.slotIndex < kFirstParticipationSlot
  179. || observation.key.slotIndex >= kFirstParticipationSlot + kParticipationSlotCount
  180. || observation.firstValue > sense.valueCount
  181. || observation.valueCount > sense.valueCount - observation.firstValue) {
  182. continue;
  183. }
  184. update_player_life(
  185. instance.playerLife[observation.key.slotIndex - kFirstParticipationSlot],
  186. std::span(sense.values).subspan(observation.firstValue, observation.valueCount));
  187. }
  188. }
  189. /**
  190. * Appends one host-state event for the script.
  191. * Host-state bursts are intentionally dynamic: authored Sense updates can raise more events than
  192. * any fixed bound without making the events invalid.
  193. */
  194. void push_script_event(RuntimeInstance& instance, const host::Event& event) noexcept {
  195. if (instance.programStatus != ProgramStatus::loaded
  196. || !lua_vm::handles_event(instance.vm, event.kind)) {
  197. return;
  198. }
  199. try {
  200. instance.scriptEvents.push_back(event);
  201. } catch (const std::bad_alloc&) {
  202. log_line(core::log::Level::warn, &instance, "script_event", "allocation_failure");
  203. }
  204. }
  205. /** Retains the last VM stage and status shown on the panel. */
  206. void note_vm_status(RuntimeInstance& instance,
  207. std::string_view stage,
  208. std::string_view status) noexcept {
  209. copy_text(instance.lastVmStage, stage);
  210. copy_text(instance.lastVmStatus, status);
  211. }
  212. /** Frees one slot; its queued events are retired unless the caller keeps them for a reattach. */
  213. void clear_instance(RuntimeInstance& instance, bool clearPending) noexcept {
  214. if (instance.occupied && clearPending) {
  215. server::gameplay::squad_entity_retirement::cancel_placed_transition(
  216. instance.view.binding, instance.view.activityClientGeneration);
  217. clear_pending_events(instance.view.binding);
  218. } else if (instance.occupied) {
  219. reset_pending_events_for_reattach(instance.view.binding);
  220. }
  221. lua_vm::close(instance.vm);
  222. instance.worldView = {};
  223. instance.view = {};
  224. instance.identity = {};
  225. instance.programKey = {};
  226. instance.lastVmStage = {};
  227. instance.lastVmStatus = {};
  228. instance.eventsSeen = 0;
  229. instance.eventsCommitted = 0;
  230. instance.intentsTransportStaged = 0;
  231. instance.lastEventSequence = 0;
  232. instance.lastLoggedRevision = 0;
  233. instance.lastMissionSequence = 0;
  234. instance.missionPhase = 0;
  235. instance.missionStateRevision = 0;
  236. instance.activityStateRevision = 0;
  237. instance.durableIntentSequence = 0;
  238. instance.durableHostOutputRevision = 0;
  239. instance.expectedScriptableRevision = 0;
  240. instance.deliveryDeadline = 0;
  241. instance.firstIntentAttempt = 0;
  242. instance.nextIntentAttempt = 0;
  243. instance.firstStartAttempt = 0;
  244. instance.nextStartAttempt = 0;
  245. instance.pendingTimerEvent = {};
  246. instance.firstTimerAttempt = 0;
  247. instance.nextTimerAttempt = 0;
  248. instance.intentAttempts = 0;
  249. instance.startAttempts = 0;
  250. instance.timerAttempts = 0;
  251. instance.durablePendingIntentCount = 0;
  252. instance.lastIntentStatus = (std::numeric_limits<std::uint16_t>::max)();
  253. instance.initialStateRegion = -1;
  254. instance.activeRegion = -1;
  255. instance.programStatus = ProgramStatus::none;
  256. instance.deliveryStage = DeliveryStage::idle;
  257. instance.publicTarget = false;
  258. instance.playerKey = 0;
  259. instance.missionStateBound = false;
  260. instance.missionStarted = false;
  261. instance.missionStateFaulted = false;
  262. instance.initialStateDeclared = false;
  263. instance.initialStateSelected = false;
  264. instance.missionReattached = false;
  265. instance.startPending = false;
  266. instance.timerPending = false;
  267. instance.triggerOccupancy = {};
  268. instance.ghostObservations = {};
  269. instance.actorPathObservations = {};
  270. instance.squadObservations = {};
  271. instance.sceneObservations = {};
  272. instance.objectiveObservations = {};
  273. instance.sessionRoster = {};
  274. instance.sessionRosterObserved = false;
  275. instance.playerLife = {};
  276. instance.playerLifeGeneration = 0;
  277. instance.lastFireteamLife = {};
  278. instance.fireteamLifePublished = false;
  279. std::vector<host::Event>{}.swap(instance.scriptEvents);
  280. instance.firstScriptEventAttempt = 0;
  281. instance.nextScriptEventAttempt = 0;
  282. instance.scriptEventAttempts = 0;
  283. instance.scriptEventRead = 0;
  284. instance.occupied = false;
  285. }
  286. /** @return The open slot for one exact binding, or null. */
  287. RuntimeInstance* find_instance(const state::activity::SessionBinding& binding) noexcept {
  288. for (RuntimeInstance& instance : g_instances) {
  289. if (instance.occupied && same_binding(instance.view.binding, binding)) {
  290. return &instance;
  291. }
  292. }
  293. return nullptr;
  294. }
  295. /** @return One unused slot, or null when every slot is open. */
  296. RuntimeInstance* free_instance() noexcept {
  297. for (RuntimeInstance& instance : g_instances) {
  298. if (!instance.occupied) {
  299. return &instance;
  300. }
  301. }
  302. return nullptr;
  303. }
  304. /** Copies one committed authoritative snapshot into the runtime's exact compare baseline. */
  305. void accept_mission_state(RuntimeInstance& instance,
  306. const mission_state::Snapshot& snapshot) noexcept {
  307. instance.missionStateRevision = snapshot.state.revision;
  308. instance.lastMissionSequence = snapshot.state.inputSequence;
  309. instance.activityStateRevision = snapshot.activityStateRevision;
  310. instance.missionPhase = snapshot.state.phase;
  311. instance.missionStarted = snapshot.state.started;
  312. instance.missionStateFaulted = snapshot.state.faulted;
  313. instance.durablePendingIntentCount = snapshot.state.pendingIntents.size();
  314. instance.durableIntentSequence = snapshot.state.pendingIntents.empty()
  315. ? mission_state::kAbsentIntentSequence
  316. : snapshot.state.pendingIntents.front().sequence;
  317. instance.durableHostOutputRevision =
  318. snapshot.state.pendingIntents.empty()
  319. ? mission_state::kAbsentHostOutputRevision
  320. : snapshot.state.pendingIntents.front().hostOutputRevision;
  321. }
  322. /** Marks the already-faulted VM in durable State when its exact compare still matches. */
  323. void persist_mission_fault(RuntimeInstance& instance) noexcept {
  324. if (!instance.missionStateBound) {
  325. return;
  326. }
  327. mission_state::Snapshot snapshot{};
  328. const mission_state::Status status = mission_state::fault(
  329. instance.view.binding, instance.programKey, instance.missionStateRevision, snapshot);
  330. if (status == mission_state::Status::ready) {
  331. accept_mission_state(instance, snapshot);
  332. return;
  333. }
  334. log_line(core::log::Level::warn, &instance, "state_fault", mission_state::status_name(status));
  335. }
  336. /** Faults both the VM and the exact server-owned mission record. */
  337. void fault_instance(RuntimeInstance& instance, std::string_view reason) noexcept {
  338. server::gameplay::squad_entity_retirement::cancel_placed_transition(
  339. instance.view.binding, instance.view.activityClientGeneration);
  340. lua_vm::fault(instance.vm, reason);
  341. instance.programStatus = ProgramStatus::programError;
  342. persist_mission_fault(instance);
  343. }
  344. /** Commits the VM's phase/revision/start transaction into exact server-owned State. */
  345. bool commit_mission_state(RuntimeInstance& instance,
  346. bool started,
  347. std::uint64_t nextInputSequence) noexcept {
  348. if (!instance.missionStateBound) {
  349. fault_instance(instance, "mission program has no authoritative State binding");
  350. return false;
  351. }
  352. lua_vm::Snapshot vm{};
  353. lua_vm::snapshot(instance.vm, vm);
  354. std::vector<mission_state::TypedIntent> pendingIntents{};
  355. std::array<mission_state::ScriptVariable, mission_state::kVariableCapacity> variables{};
  356. std::array<mission_state::MissionTimer, mission_state::kTimerCapacity> timers{};
  357. std::size_t variableCount = 0;
  358. std::size_t timerCount = 0;
  359. std::uint64_t nextTimerSequence = mission_state::kAbsentTimerSequence;
  360. std::uint64_t nextIntentKey = mission_state::kAbsentIntentKey;
  361. if (!snapshot_state_intents(instance.vm, pendingIntents)) {
  362. fault_instance(instance, "mission VM outbox snapshot was refused");
  363. return false;
  364. }
  365. if (!lua_vm::snapshot_durable_state(instance.vm,
  366. variables,
  367. variableCount,
  368. timers,
  369. timerCount,
  370. nextTimerSequence,
  371. nextIntentKey)) {
  372. fault_instance(instance, "mission VM durable-state snapshot was refused");
  373. return false;
  374. }
  375. const mission_state::CommitCandidate transaction{
  376. .variables = {variables.data(), variableCount},
  377. .timers = {timers.data(), timerCount},
  378. .pendingIntents = pendingIntents,
  379. .nextRevision = vm.stateRevision,
  380. .nextInputSequence = nextInputSequence,
  381. .nextTimerSequence = nextTimerSequence,
  382. .nextIntentKey = nextIntentKey,
  383. .phase = vm.phase,
  384. .started = started,
  385. };
  386. mission_state::Snapshot snapshot{};
  387. const mission_state::Status status = mission_state::commit(instance.view.binding,
  388. instance.programKey,
  389. instance.missionStateRevision,
  390. instance.lastMissionSequence,
  391. transaction,
  392. snapshot);
  393. if (status == mission_state::Status::ready) {
  394. const std::uint32_t previousPhase = instance.missionPhase;
  395. accept_mission_state(instance, snapshot);
  396. if (instance.missionPhase != previousPhase) {
  397. queue_phase_entered(instance, previousPhase);
  398. }
  399. return true;
  400. }
  401. log_line(core::log::Level::warn, &instance, "state_commit", mission_state::status_name(status));
  402. fault_instance(instance, "authoritative mission State commit was refused");
  403. instance.programStatus = ProgramStatus::programError;
  404. return false;
  405. }
  406. namespace {
  407. /**
  408. * Settles the inputs of every instance whose activity has no script. No program will consume
  409. * them, and an unsettled row is retained by the Host feed until its capacity refuses new ones.
  410. */
  411. void retire_scriptless_inputs() noexcept {
  412. for (RuntimeInstance& instance : g_instances) {
  413. if (!instance.occupied || instance.programStatus != ProgramStatus::missing) {
  414. continue;
  415. }
  416. mission_state::InputSequenceSnapshot inputState{};
  417. if (!mission_state::input_sequence_snapshot(instance.view.binding, inputState)
  418. || inputState.issued == inputState.committed) {
  419. continue;
  420. }
  421. const mission_state::Status status =
  422. mission_state::retire_unbound_inputs(instance.view.binding);
  423. if (status != mission_state::Status::ready) {
  424. log_line(core::log::Level::warn,
  425. &instance,
  426. "events",
  427. mission_state::status_name(status),
  428. "reason=scriptless_inputs_retained");
  429. }
  430. }
  431. }
  432. /** Delivers queued derived events in arrival order until one needs output or the batch is spent. */
  433. void service_script_events(std::uint64_t now) noexcept {
  434. for (RuntimeInstance& instance : g_instances) {
  435. const auto ready = [&] {
  436. if (!instance.occupied || instance.scriptEventRead >= instance.scriptEvents.size()
  437. || instance.programStatus != ProgramStatus::loaded || instance.startPending) {
  438. return false;
  439. }
  440. lua_vm::Intent pendingIntent{};
  441. return instance.deliveryStage == DeliveryStage::idle
  442. && !lua_vm::pending_intent(instance.vm, pendingIntent);
  443. };
  444. const auto dispatch = [&] {
  445. const bool firstAttempt = instance.scriptEventAttempts == 0;
  446. if (firstAttempt) {
  447. instance.firstScriptEventAttempt = now;
  448. }
  449. ++instance.scriptEventAttempts;
  450. host::Event& head = instance.scriptEvents[instance.scriptEventRead];
  451. head.tick = now;
  452. static_cast<void>(dispatch_event(instance, head, nullptr, nullptr, firstAttempt, now));
  453. retire_script_event(instance);
  454. };
  455. static_cast<void>(drain_script_event_batch(ready, dispatch));
  456. }
  457. }
  458. /** Selects at most one due timer per instance without adding a second durable event feed. */
  459. void service_timers(std::uint64_t now) noexcept {
  460. for (RuntimeInstance& instance : g_instances) {
  461. if (!instance.occupied || instance.programStatus != ProgramStatus::loaded
  462. || instance.startPending) {
  463. continue;
  464. }
  465. lua_vm::Intent pendingIntent{};
  466. if (instance.deliveryStage != DeliveryStage::idle
  467. || lua_vm::pending_intent(instance.vm, pendingIntent)) {
  468. continue;
  469. }
  470. if (!instance.timerPending) {
  471. mission_state::InputSequenceSnapshot inputState{};
  472. if (has_pending_host_input(instance)
  473. || !mission_state::input_sequence_snapshot(instance.view.binding, inputState)
  474. || inputState.faulted || inputState.issued != inputState.committed) {
  475. continue;
  476. }
  477. std::array<lua_vm::ScriptVariable, lua_vm::kVariableCapacity> variables{};
  478. std::array<lua_vm::MissionTimer, lua_vm::kTimerCapacity> timers{};
  479. std::size_t variableCount = 0;
  480. std::size_t timerCount = 0;
  481. std::uint64_t nextTimerSequence = mission_state::kAbsentTimerSequence;
  482. std::uint64_t nextIntentKey = mission_state::kAbsentIntentKey;
  483. if (!lua_vm::snapshot_durable_state(instance.vm,
  484. variables,
  485. variableCount,
  486. timers,
  487. timerCount,
  488. nextTimerSequence,
  489. nextIntentKey)) {
  490. fault_instance(instance, "mission timer snapshot was refused");
  491. continue;
  492. }
  493. const lua_vm::MissionTimer* selected = nullptr;
  494. for (std::size_t index = 0; index < timerCount; ++index) {
  495. if (timers[index].deadlineTick <= now
  496. && (selected == nullptr || earlier_timer(timers[index], *selected))) {
  497. selected = &timers[index];
  498. }
  499. }
  500. if (selected == nullptr) {
  501. continue;
  502. }
  503. instance.pendingTimerEvent = {};
  504. instance.pendingTimerEvent.binding = instance.view.binding;
  505. instance.pendingTimerEvent.timerName = selected->key;
  506. instance.pendingTimerEvent.sequence = selected->sequence;
  507. instance.pendingTimerEvent.tick = now;
  508. instance.pendingTimerEvent.sourceGeneration = instance.view.activityClientGeneration;
  509. instance.pendingTimerEvent.timerDeadlineTick = selected->deadlineTick;
  510. instance.pendingTimerEvent.timerSequence = selected->sequence;
  511. instance.pendingTimerEvent.missionSequence = instance.lastMissionSequence;
  512. instance.pendingTimerEvent.kind = host::EventKind::timerElapsed;
  513. instance.firstTimerAttempt = now;
  514. instance.nextTimerAttempt = now;
  515. instance.timerAttempts = 0;
  516. instance.timerPending = true;
  517. }
  518. const bool firstAttempt = instance.timerAttempts == 0;
  519. ++instance.timerAttempts;
  520. static_cast<void>(dispatch_event(
  521. instance, instance.pendingTimerEvent, nullptr, nullptr, firstAttempt, now));
  522. instance.pendingTimerEvent = {};
  523. instance.firstTimerAttempt = 0;
  524. instance.nextTimerAttempt = 0;
  525. instance.timerAttempts = 0;
  526. instance.timerPending = false;
  527. }
  528. }
  529. } // namespace
  530. /** Clears all state, reads the enable switch, and resolves the script and SDK Lua paths. */
  531. void initialize() noexcept {
  532. AcquireSRWLockExclusive(&g_lock);
  533. clear_attach_diagnostics();
  534. clear_all_pending_events();
  535. for (RuntimeInstance& instance : g_instances) {
  536. clear_instance(instance);
  537. }
  538. g_enabled = core::settings::get().server.activation.missionScripting;
  539. g_pathReady = false;
  540. reset_feed_cursors();
  541. if (!g_enabled) {
  542. ReleaseSRWLockExclusive(&g_lock);
  543. return;
  544. }
  545. g_pathReady = resolve_script_paths();
  546. if (!g_pathReady) {
  547. ReleaseSRWLockExclusive(&g_lock);
  548. return;
  549. }
  550. log_line(core::log::Level::info, nullptr, "initialize", "enabled");
  551. ReleaseSRWLockExclusive(&g_lock);
  552. }
  553. /** One pass: attach, start, events, timers, then delivery for every open instance. */
  554. void service(std::uint64_t now) noexcept {
  555. AcquireSRWLockExclusive(&g_lock);
  556. if (!g_enabled || !g_pathReady) {
  557. ReleaseSRWLockExclusive(&g_lock);
  558. return;
  559. }
  560. synchronize_instances(now);
  561. service_pending_starts(now);
  562. consume_delivery_events(now);
  563. consume_mission_inputs(now);
  564. retire_scriptless_inputs();
  565. service_timers(now);
  566. service_script_events(now);
  567. for (RuntimeInstance& instance : g_instances) {
  568. if (instance.occupied) {
  569. if (instance.programStatus == ProgramStatus::programError) {
  570. reconcile_terminal_delivery(instance);
  571. } else {
  572. dispatch_intent(instance, now);
  573. }
  574. }
  575. }
  576. ReleaseSRWLockExclusive(&g_lock);
  577. }
  578. /** Copies every open instance row and every retained attach row for the panel. */
  579. void snapshot(DiagnosticsSnapshot& output) noexcept {
  580. output = {};
  581. AcquireSRWLockShared(&g_lock);
  582. output.enabled = g_enabled;
  583. output.pathReady = g_pathReady;
  584. for (const RuntimeInstance& instance : g_instances) {
  585. if (instance.occupied && output.instanceCount < output.instances.size()) {
  586. copy_diagnostics(instance, output.instances[output.instanceCount++]);
  587. }
  588. }
  589. snapshot_attach_diagnostics(output);
  590. ReleaseSRWLockShared(&g_lock);
  591. }
  592. /** Closes every instance and authorizes one program replacement so the next attach recompiles. */
  593. bool reload() noexcept {
  594. AcquireSRWLockExclusive(&g_lock);
  595. if (!g_enabled || !g_pathReady) {
  596. ReleaseSRWLockExclusive(&g_lock);
  597. return false;
  598. }
  599. bool reloaded = true;
  600. for (RuntimeInstance& instance : g_instances) {
  601. if (!instance.occupied) {
  602. continue;
  603. }
  604. log_line(core::log::Level::info, &instance, "reload", "requested");
  605. if (instance.missionStateBound && instance.missionStateFaulted) {
  606. mission_state::Snapshot recovered{};
  607. const mission_state::Status status =
  608. mission_state::recover(instance.view.binding,
  609. instance.programKey,
  610. instance.missionStateRevision,
  611. recovered);
  612. if (status != mission_state::Status::ready) {
  613. log_line(core::log::Level::warn,
  614. &instance,
  615. "reload",
  616. mission_state::status_name(status));
  617. reloaded = false;
  618. continue;
  619. }
  620. accept_mission_state(instance, recovered);
  621. clear_pending_events(instance.view.binding);
  622. }
  623. if (!authorize_reload(instance)) {
  624. log_line(core::log::Level::warn, &instance, "reload", "capacity");
  625. reloaded = false;
  626. continue;
  627. }
  628. clear_instance(instance, false);
  629. }
  630. clear_attach_diagnostics();
  631. ReleaseSRWLockExclusive(&g_lock);
  632. return reloaded;
  633. }
  634. /** Closes every instance and clears every global this unit owns. */
  635. void shutdown() noexcept {
  636. AcquireSRWLockExclusive(&g_lock);
  637. for (RuntimeInstance& instance : g_instances) {
  638. clear_instance(instance);
  639. }
  640. clear_all_pending_events();
  641. clear_attach_diagnostics();
  642. clear_script_paths();
  643. clear_feed_cursors();
  644. g_enabled = false;
  645. g_pathReady = false;
  646. ReleaseSRWLockExclusive(&g_lock);
  647. }
  648. } // namespace sunrise::server::activity::mission