mission_script_runtime.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  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 "../../../middleware/gameplay/external/external_entity_codec.h"
  18. #include "../../../state/activity/mission/runtime.h"
  19. #include "../../../state/activity/runtime.h"
  20. #include "../../gameplay/squad_entity_retirement.h"
  21. #include "../host_runtime.h"
  22. #include "mission_script_event_batch.h"
  23. #include "mission_script_runtime_internal.h"
  24. #include "mission_script_vm.h"
  25. namespace sunrise::server::activity::mission {
  26. std::array<RuntimeInstance, host::kInstanceCapacity> g_instances{};
  27. namespace {
  28. SRWLOCK g_lock{SRWLOCK_INIT};
  29. bool g_enabled{};
  30. bool g_pathReady{};
  31. [[nodiscard]] bool earlier_timer(const lua_vm::MissionTimer& left,
  32. const lua_vm::MissionTimer& right) noexcept {
  33. return left.deadlineTick < right.deadlineTick
  34. || (left.deadlineTick == right.deadlineTick && left.sequence < right.sequence);
  35. }
  36. /** @return The life of the instance's own player, from its retained participation levels. */
  37. [[nodiscard]] PlayerLife own_player_life(const RuntimeInstance& instance) noexcept {
  38. if (!instance.occupied
  39. || instance.playerLifeGeneration != instance.view.activityClientGeneration) {
  40. return PlayerLife::unknown;
  41. }
  42. for (const PlayerLifeObservation& life : instance.playerLife) {
  43. if (instance.playerKey != 0 && life.playerKey == instance.playerKey) {
  44. return life.life();
  45. }
  46. }
  47. return PlayerLife::unknown;
  48. }
  49. } // namespace
  50. /**
  51. * Writes one bounded mission-script diagnostic line.
  52. * @param instance Binding the line reports, or null before one is bound.
  53. * @param fields Extra key=value pairs, appended as written.
  54. * @param error Free text; quoted and placed last so it never splits the pairs.
  55. */
  56. void log_line(core::log::Level level,
  57. const RuntimeInstance* instance,
  58. std::string_view stage,
  59. std::string_view result,
  60. std::string_view fields,
  61. std::string_view error) noexcept {
  62. std::array<char, core::log::kLineCapacity> line{};
  63. const unsigned long long session = instance == nullptr ? 0 : instance->view.binding.sessionId;
  64. const unsigned activityRow = instance == nullptr ? 0 : instance->identity.activityRow;
  65. const int written = std::snprintf(line.data(),
  66. line.size(),
  67. "ev=mission_script stage=%.*s result=%.*s session=%llu "
  68. "activity_row=%u",
  69. static_cast<int>(stage.size()),
  70. stage.data(),
  71. static_cast<int>(result.size()),
  72. result.data(),
  73. session,
  74. activityRow);
  75. if (written <= 0) {
  76. return;
  77. }
  78. std::size_t length = (std::min)(static_cast<std::size_t>(written), line.size() - 1);
  79. if (!fields.empty()) {
  80. const int piece = std::snprintf(line.data() + length,
  81. line.size() - length,
  82. " %.*s",
  83. static_cast<int>(fields.size()),
  84. fields.data());
  85. if (piece > 0) {
  86. length = (std::min)(length + static_cast<std::size_t>(piece), line.size() - 1);
  87. }
  88. }
  89. if (!error.empty()) {
  90. const int piece = std::snprintf(line.data() + length,
  91. line.size() - length,
  92. " error=\"%.*s\"",
  93. static_cast<int>(error.size()),
  94. error.data());
  95. if (piece > 0) {
  96. length = (std::min)(length + static_cast<std::size_t>(piece), line.size() - 1);
  97. }
  98. }
  99. core::log::write(core::log::Channel::server, level, {line.data(), length});
  100. }
  101. /**
  102. * Raises a fireteam event on each private instance whose party life counts changed. The party
  103. * is the committed destination peers; a missing or loading member counts as unknown.
  104. */
  105. void publish_fireteam_life(std::uint64_t now) noexcept {
  106. for (RuntimeInstance& instance : g_instances) {
  107. if (!instance.occupied || instance.publicTarget || !instance.sessionRosterObserved
  108. || instance.programStatus != ProgramStatus::loaded) {
  109. continue;
  110. }
  111. FireteamLife counts{};
  112. counts.add(own_player_life(instance));
  113. for (const SessionRosterWatch& peer : instance.sessionRoster) {
  114. if (!peer.used) {
  115. continue;
  116. }
  117. PlayerLife life = PlayerLife::unknown;
  118. for (const RuntimeInstance& candidate : g_instances) {
  119. if (candidate.occupied && !candidate.publicTarget
  120. && candidate.view.binding.sessionId == peer.sessionId
  121. && candidate.view.binding.createdRevision == peer.createdRevision) {
  122. life = own_player_life(candidate);
  123. break;
  124. }
  125. }
  126. counts.add(life);
  127. }
  128. if (instance.fireteamLifePublished && counts == instance.lastFireteamLife) {
  129. continue;
  130. }
  131. instance.lastFireteamLife = counts;
  132. instance.fireteamLifePublished = true;
  133. host::Event event = state_edge_event(instance);
  134. event.kind = host::EventKind::fireteamState;
  135. event.tick = now;
  136. event.fireteamAlive = counts.alive;
  137. event.fireteamDead = counts.dead;
  138. event.fireteamUnknown = counts.unknown;
  139. push_script_event(instance, event);
  140. std::array<char, 96> fields{};
  141. const int length = std::snprintf(fields.data(),
  142. fields.size(),
  143. "alive=%u dead=%u unknown=%u",
  144. static_cast<unsigned>(counts.alive),
  145. static_cast<unsigned>(counts.dead),
  146. static_cast<unsigned>(counts.unknown));
  147. if (length > 0) {
  148. log_line(core::log::Level::debug,
  149. &instance,
  150. "fireteam_life",
  151. "changed",
  152. {fields.data(), static_cast<std::size_t>(length)});
  153. }
  154. }
  155. }
  156. /** Merges the type-13 participation records of one Sense snapshot into the instance. */
  157. void observe_player_life(RuntimeInstance& instance,
  158. const host::SenseObservationSnapshot& sense) noexcept {
  159. if (instance.playerLifeGeneration != sense.sourceGeneration) {
  160. instance.playerLife = {};
  161. instance.playerLifeGeneration = sense.sourceGeneration;
  162. }
  163. for (std::size_t index = 0; index < sense.observationCount; ++index) {
  164. const host::SenseObservation& observation = sense.observations[index];
  165. if (observation.key.slotType != kParticipationSlotType
  166. || observation.key.senseSchema != kParticipationSenseSchema
  167. || observation.key.objectTag != kParticipationObjectTag
  168. || observation.key.slotIndex < kFirstParticipationSlot
  169. || observation.key.slotIndex >= kFirstParticipationSlot + kParticipationSlotCount
  170. || observation.firstValue > sense.valueCount
  171. || observation.valueCount > sense.valueCount - observation.firstValue) {
  172. continue;
  173. }
  174. PlayerLifeObservation& level =
  175. instance.playerLife[observation.key.slotIndex - kFirstParticipationSlot];
  176. update_player_life(
  177. level, std::span(sense.values).subspan(observation.firstValue, observation.valueCount));
  178. // The host writes its own player key on every participation record of this link, so a
  179. // record that reports state is that player's.
  180. if ((level.seen & kLifeSeenKey) == 0 && (level.seen & kLifeSeenState) != 0
  181. && instance.playerKey != 0) {
  182. level.playerKey = instance.playerKey;
  183. level.seen |= kLifeSeenKey;
  184. }
  185. }
  186. }
  187. /**
  188. * Appends one host-state event for the script.
  189. * Host-state bursts are intentionally dynamic: authored Sense updates can raise more events than
  190. * any fixed bound without making the events invalid.
  191. */
  192. void push_script_event(RuntimeInstance& instance, const host::Event& event) noexcept {
  193. if (instance.programStatus != ProgramStatus::loaded
  194. || !lua_vm::handles_event(instance.vm, event.kind)) {
  195. return;
  196. }
  197. try {
  198. host::Event admitted = event;
  199. if (admitted.attemptGeneration == 0) {
  200. admitted.attemptGeneration = instance.dispatchAttemptGeneration != 0
  201. ? instance.dispatchAttemptGeneration
  202. : instance.attempt.generation;
  203. }
  204. instance.scriptEvents.push_back(admitted);
  205. } catch (const std::bad_alloc&) {
  206. fault_instance(instance, "mission event allocation failed");
  207. }
  208. }
  209. /**
  210. * Queues an exact host-policy transition without consuming a client mission-input sequence.
  211. * @param binding Activity generation that accepted the damage.
  212. * @param sourceGeneration ActivityClient generation that owns the policy.
  213. * @param registryKey Authored squad registry key.
  214. * @param slotIndex Native squad slot index.
  215. */
  216. void report_squad_provoked(const state::activity::SessionBinding& binding,
  217. std::uint64_t sourceGeneration,
  218. std::uint32_t registryKey,
  219. std::uint16_t slotIndex) noexcept {
  220. if (sourceGeneration == 0 || registryKey == 0
  221. || slotIndex > static_cast<std::uint16_t>((std::numeric_limits<std::int16_t>::max)())) {
  222. return;
  223. }
  224. AcquireSRWLockExclusive(&g_lock);
  225. auto* instance = find_instance(binding);
  226. if (instance != nullptr && instance->view.activityClientGeneration == sourceGeneration) {
  227. host::Event event{};
  228. event.kind = host::EventKind::squadProvoked;
  229. event.binding = binding;
  230. event.sequence = instance->missionStateRevision;
  231. event.missionSequence = instance->lastMissionSequence;
  232. event.sourceGeneration = sourceGeneration;
  233. event.tick = GetTickCount64();
  234. event.firstRegistryKey = registryKey;
  235. event.firstSlotType = sdk::format::kSquadSlotType;
  236. event.firstSlotIndex = slotIndex;
  237. push_script_event(*instance, event);
  238. }
  239. ReleaseSRWLockExclusive(&g_lock);
  240. }
  241. /**
  242. * Queues one replicated damage level for a squad slot, once per change.
  243. * @param binding Activity generation that accepted the entity update.
  244. * @param sourceGeneration ActivityClient generation that owns the policy.
  245. * @param registryKey Authored registry key.
  246. * @param slotType Authored slot type: the squad's, or the combatant's own.
  247. * @param slotIndex Authored slot index.
  248. * @param health Pool 0 of the damage component, 0 to kDamageLevelFull.
  249. * @param shield Pool 1 of the damage component, 0 to kDamageLevelFull.
  250. */
  251. void report_squad_damage(const state::activity::SessionBinding& binding,
  252. std::uint64_t sourceGeneration,
  253. std::uint32_t registryKey,
  254. std::uint8_t slotType,
  255. std::uint16_t slotIndex,
  256. std::uint16_t health,
  257. std::uint16_t shield) noexcept {
  258. constexpr std::uint16_t full = middleware::gameplay::external::kDamageLevelFull;
  259. if (sourceGeneration == 0 || registryKey == 0 || health > full || shield > full
  260. || slotIndex > static_cast<std::uint16_t>((std::numeric_limits<std::int16_t>::max)())) {
  261. return;
  262. }
  263. AcquireSRWLockExclusive(&g_lock);
  264. auto* instance = find_instance(binding);
  265. if (instance != nullptr && instance->view.activityClientGeneration == sourceGeneration) {
  266. push_entity_damage(*instance,
  267. registryKey,
  268. slotType,
  269. slotIndex,
  270. static_cast<float>(health) / static_cast<float>(full),
  271. static_cast<float>(shield) / static_cast<float>(full),
  272. GetTickCount64());
  273. } else {
  274. // Nothing else records a level that finds no mission.
  275. log_line(core::log::Level::debug,
  276. instance,
  277. "combatant_damage",
  278. instance == nullptr ? "no_instance" : "stale_generation");
  279. }
  280. ReleaseSRWLockExclusive(&g_lock);
  281. }
  282. /** Retains the last VM stage and status shown on the panel. */
  283. void note_vm_status(RuntimeInstance& instance,
  284. std::string_view stage,
  285. std::string_view status) noexcept {
  286. copy_text(instance.lastVmStage, stage);
  287. copy_text(instance.lastVmStatus, status);
  288. }
  289. /** Native Sense levels cannot cross an attempt or ActivityClient generation boundary. */
  290. void clear_sense_levels(RuntimeInstance& instance) noexcept {
  291. instance.triggerOccupancy = {};
  292. instance.squadObservations = {};
  293. instance.ghostObservations = {};
  294. instance.damageObservations = {};
  295. instance.combatantDamageObservations = {};
  296. instance.entityDamageObservations = {};
  297. instance.deviceObservations = {};
  298. instance.objectInteractionObservations = {};
  299. instance.actorPathObservations = {};
  300. instance.sceneObservations = {};
  301. instance.objectiveObservations = {};
  302. }
  303. /** Frees one slot; its queued events are retired unless the caller keeps them for a reattach. */
  304. void clear_instance(RuntimeInstance& instance, bool clearPending) noexcept {
  305. if (instance.occupied && clearPending) {
  306. server::gameplay::squad_entity_retirement::cancel_placed_transition(
  307. instance.view.binding, instance.view.activityClientGeneration);
  308. clear_pending_events(instance.view.binding);
  309. } else if (instance.occupied) {
  310. reset_pending_events_for_reattach(instance.view.binding);
  311. }
  312. lua_vm::close(instance.vm);
  313. instance.worldView = {};
  314. instance.view = {};
  315. instance.identity = {};
  316. instance.programKey = {};
  317. instance.lastVmStage = {};
  318. instance.lastVmStatus = {};
  319. instance.attempt = {};
  320. instance.dispatchAttemptGeneration = 0;
  321. instance.dispatchInputSequence = 0;
  322. instance.eventsSeen = 0;
  323. instance.eventsCommitted = 0;
  324. instance.intentsTransportStaged = 0;
  325. instance.lastLoggedRevision = 0;
  326. instance.lastMissionSequence = 0;
  327. instance.missionPhase = 0;
  328. instance.missionStateRevision = 0;
  329. instance.activityStateRevision = 0;
  330. instance.durableIntentSequence = 0;
  331. instance.durableHostOutputRevision = 0;
  332. instance.expectedScriptableRevision = 0;
  333. instance.deliveryDeadline = 0;
  334. instance.firstIntentAttempt = 0;
  335. instance.pendingTimerEvent = {};
  336. instance.intentAttempts = 0;
  337. instance.startAttempts = 0;
  338. instance.durablePendingIntentCount = 0;
  339. instance.lastIntentStatus = (std::numeric_limits<std::uint16_t>::max)();
  340. instance.initialStateRegion = -1;
  341. instance.activeRegion = -1;
  342. instance.programStatus = ProgramStatus::none;
  343. instance.deliveryStage = DeliveryStage::idle;
  344. instance.publicTarget = false;
  345. instance.playerKey = 0;
  346. instance.missionStateBound = false;
  347. instance.missionStarted = false;
  348. instance.missionStateFaulted = false;
  349. instance.initialStateDeclared = false;
  350. instance.initialStateSelected = false;
  351. instance.missionReattached = false;
  352. instance.startPending = false;
  353. instance.timerPending = false;
  354. clear_sense_levels(instance);
  355. instance.sessionRoster = {};
  356. instance.sessionRosterObserved = false;
  357. instance.playerLife = {};
  358. instance.playerLifeGeneration = 0;
  359. instance.lastFireteamLife = {};
  360. instance.fireteamLifePublished = false;
  361. std::vector<host::Event>{}.swap(instance.scriptEvents);
  362. instance.scriptEventRead = 0;
  363. instance.occupied = false;
  364. }
  365. /** @return The open slot for one exact binding, or null. */
  366. RuntimeInstance* find_instance(const state::activity::SessionBinding& binding) noexcept {
  367. for (RuntimeInstance& instance : g_instances) {
  368. if (instance.occupied && same_binding(instance.view.binding, binding)) {
  369. return &instance;
  370. }
  371. }
  372. return nullptr;
  373. }
  374. /** @return One unused slot, or null when every slot is open. */
  375. RuntimeInstance* free_instance() noexcept {
  376. for (RuntimeInstance& instance : g_instances) {
  377. if (!instance.occupied) {
  378. return &instance;
  379. }
  380. }
  381. return nullptr;
  382. }
  383. /** Copies one committed authoritative snapshot into the runtime's exact compare baseline. */
  384. void accept_mission_state(RuntimeInstance& instance,
  385. const mission_state::Snapshot& snapshot) noexcept {
  386. if (instance.attempt.generation != snapshot.state.attempt.generation) {
  387. clear_sense_levels(instance);
  388. // Player life is kept: the client sends only changed fields and never resends the region.
  389. instance.fireteamLifePublished = false;
  390. instance.timerPending = false;
  391. instance.pendingTimerEvent = {};
  392. }
  393. lua_vm::publish_attempt(instance.vm, snapshot.state.attempt);
  394. if (!lua_vm::publish_device_requests(
  395. instance.vm, snapshot.state.deviceRequests, instance.view.activityClientGeneration)) {
  396. lua_vm::fault(instance.vm, "device request snapshot allocation failed");
  397. instance.programStatus = ProgramStatus::programError;
  398. }
  399. publish_ghost_levels(instance);
  400. if (!lua_vm::publish_populations(instance.vm, snapshot.state.squadPopulations)) {
  401. lua_vm::fault(instance.vm, "population snapshot allocation failed");
  402. instance.programStatus = ProgramStatus::programError;
  403. }
  404. accept_compare_fields(instance, snapshot);
  405. }
  406. /** Copies the committed attempt, revision, cursors, phase and intent head into the instance. */
  407. void accept_compare_fields(RuntimeInstance& instance,
  408. const mission_state::Snapshot& snapshot) noexcept {
  409. instance.attempt = snapshot.state.attempt;
  410. instance.missionStateRevision = snapshot.state.revision;
  411. instance.lastMissionSequence = snapshot.state.inputSequence;
  412. instance.activityStateRevision = snapshot.activityStateRevision;
  413. instance.missionPhase = snapshot.state.phase;
  414. instance.missionStarted = snapshot.state.started;
  415. instance.missionStateFaulted = snapshot.state.faulted;
  416. instance.durablePendingIntentCount = snapshot.state.pendingIntents.size();
  417. instance.durableIntentSequence = snapshot.state.pendingIntents.empty()
  418. ? mission_state::kAbsentIntentSequence
  419. : snapshot.state.pendingIntents.front().sequence;
  420. instance.durableHostOutputRevision =
  421. snapshot.state.pendingIntents.empty()
  422. ? mission_state::kAbsentHostOutputRevision
  423. : snapshot.state.pendingIntents.front().hostOutputRevision;
  424. }
  425. /** Marks the already-faulted VM in durable State when its exact compare still matches. */
  426. void persist_mission_fault(RuntimeInstance& instance) noexcept {
  427. if (!instance.missionStateBound) {
  428. return;
  429. }
  430. mission_state::Snapshot snapshot{};
  431. const mission_state::Status status = mission_state::fault(
  432. instance.view.binding, instance.programKey, instance.missionStateRevision, snapshot);
  433. if (status == mission_state::Status::ready) {
  434. accept_mission_state(instance, snapshot);
  435. return;
  436. }
  437. log_line(core::log::Level::warn, &instance, "state_fault", mission_state::status_name(status));
  438. }
  439. /** Faults both the VM and the exact server-owned mission record. */
  440. void fault_instance(RuntimeInstance& instance, std::string_view reason) noexcept {
  441. server::gameplay::squad_entity_retirement::cancel_placed_transition(
  442. instance.view.binding, instance.view.activityClientGeneration);
  443. lua_vm::fault(instance.vm, reason);
  444. instance.programStatus = ProgramStatus::programError;
  445. persist_mission_fault(instance);
  446. }
  447. /** Commits the VM's phase/revision/start transaction into exact server-owned State. */
  448. bool commit_mission_state(RuntimeInstance& instance,
  449. bool started,
  450. std::uint64_t nextInputSequence) noexcept {
  451. if (!instance.missionStateBound) {
  452. fault_instance(instance, "mission program has no authoritative State binding");
  453. return false;
  454. }
  455. lua_vm::Snapshot vm{};
  456. lua_vm::snapshot(instance.vm, vm);
  457. std::vector<mission_state::TypedIntent> pendingIntents{};
  458. std::array<mission_state::ScriptVariable, mission_state::kVariableCapacity> variables{};
  459. std::array<mission_state::MissionTimer, mission_state::kTimerCapacity> timers{};
  460. std::size_t variableCount = 0;
  461. std::size_t timerCount = 0;
  462. std::uint64_t nextTimerSequence = mission_state::kAbsentTimerSequence;
  463. std::uint64_t nextIntentKey = mission_state::kAbsentIntentKey;
  464. if (!lua_vm::snapshot_intents(instance.vm, pendingIntents)) {
  465. fault_instance(instance, "mission VM outbox snapshot was refused");
  466. return false;
  467. }
  468. if (!lua_vm::snapshot_durable_state(instance.vm,
  469. variables,
  470. variableCount,
  471. timers,
  472. timerCount,
  473. nextTimerSequence,
  474. nextIntentKey)) {
  475. fault_instance(instance, "mission VM durable-state snapshot was refused");
  476. return false;
  477. }
  478. const mission_state::CommitCandidate transaction{
  479. .variables = {variables.data(), variableCount},
  480. .timers = {timers.data(), timerCount},
  481. .pendingIntents = pendingIntents,
  482. .nextRevision = vm.stateRevision,
  483. .nextInputSequence = nextInputSequence,
  484. .nextTimerSequence = nextTimerSequence,
  485. .nextIntentKey = nextIntentKey,
  486. .phase = vm.phase,
  487. .started = started,
  488. };
  489. mission_state::Snapshot snapshot{};
  490. const mission_state::Status status = mission_state::commit(instance.view.binding,
  491. instance.programKey,
  492. instance.missionStateRevision,
  493. instance.lastMissionSequence,
  494. transaction,
  495. snapshot);
  496. if (status == mission_state::Status::ready) {
  497. const std::uint32_t previousPhase = instance.missionPhase;
  498. accept_mission_state(instance, snapshot);
  499. if (instance.missionPhase != previousPhase) {
  500. queue_phase_entered(instance, previousPhase);
  501. }
  502. return true;
  503. }
  504. log_line(core::log::Level::warn, &instance, "state_commit", mission_state::status_name(status));
  505. fault_instance(instance, "authoritative mission State commit was refused");
  506. instance.programStatus = ProgramStatus::programError;
  507. return false;
  508. }
  509. namespace {
  510. /**
  511. * Settles the inputs of every instance whose activity has no script. No program will consume
  512. * them, and the unbounded Host feed retains an unsettled row until it is settled.
  513. */
  514. void retire_scriptless_inputs() noexcept {
  515. for (RuntimeInstance& instance : g_instances) {
  516. if (!instance.occupied || instance.programStatus != ProgramStatus::missing) {
  517. continue;
  518. }
  519. mission_state::InputSequenceSnapshot inputState{};
  520. if (!mission_state::input_sequence_snapshot(instance.view.binding, inputState)
  521. || inputState.issued == inputState.committed) {
  522. continue;
  523. }
  524. const mission_state::Status status =
  525. mission_state::retire_unbound_inputs(instance.view.binding);
  526. if (status != mission_state::Status::ready) {
  527. log_line(core::log::Level::warn,
  528. &instance,
  529. "events",
  530. mission_state::status_name(status),
  531. "reason=scriptless_inputs_retained");
  532. }
  533. }
  534. }
  535. /** Delivers queued derived events in arrival order until one needs output or the batch is spent. */
  536. void service_script_events(std::uint64_t now) noexcept {
  537. for (RuntimeInstance& instance : g_instances) {
  538. const auto ready = [&] {
  539. if (!instance.occupied || instance.scriptEventRead >= instance.scriptEvents.size()
  540. || instance.programStatus != ProgramStatus::loaded || instance.startPending) {
  541. return false;
  542. }
  543. lua_vm::Intent pendingIntent{};
  544. return instance.deliveryStage == DeliveryStage::idle
  545. && !lua_vm::pending_intent(instance.vm, pendingIntent);
  546. };
  547. const auto dispatch = [&] {
  548. host::Event& head = instance.scriptEvents[instance.scriptEventRead];
  549. head.tick = now;
  550. static_cast<void>(dispatch_event(instance, head, nullptr, nullptr, now));
  551. retire_script_event(instance);
  552. };
  553. static_cast<void>(drain_script_event_batch(ready, dispatch));
  554. }
  555. }
  556. /** Selects at most one due timer per instance without adding a second durable event feed. */
  557. void service_timers(std::uint64_t now) noexcept {
  558. for (RuntimeInstance& instance : g_instances) {
  559. if (!instance.occupied || instance.programStatus != ProgramStatus::loaded
  560. || instance.startPending) {
  561. continue;
  562. }
  563. lua_vm::Intent pendingIntent{};
  564. if (instance.deliveryStage != DeliveryStage::idle
  565. || lua_vm::pending_intent(instance.vm, pendingIntent)) {
  566. continue;
  567. }
  568. if (!instance.timerPending) {
  569. mission_state::InputSequenceSnapshot inputState{};
  570. if (has_pending_host_input(instance)
  571. || !mission_state::input_sequence_snapshot(instance.view.binding, inputState)
  572. || inputState.faulted || inputState.issued != inputState.committed) {
  573. continue;
  574. }
  575. std::array<lua_vm::ScriptVariable, lua_vm::kVariableCapacity> variables{};
  576. std::array<lua_vm::MissionTimer, lua_vm::kTimerCapacity> timers{};
  577. std::size_t variableCount = 0;
  578. std::size_t timerCount = 0;
  579. std::uint64_t nextTimerSequence = mission_state::kAbsentTimerSequence;
  580. std::uint64_t nextIntentKey = mission_state::kAbsentIntentKey;
  581. if (!lua_vm::snapshot_durable_state(instance.vm,
  582. variables,
  583. variableCount,
  584. timers,
  585. timerCount,
  586. nextTimerSequence,
  587. nextIntentKey)) {
  588. fault_instance(instance, "mission timer snapshot was refused");
  589. continue;
  590. }
  591. const lua_vm::MissionTimer* selected = nullptr;
  592. for (std::size_t index = 0; index < timerCount; ++index) {
  593. if (timers[index].deadlineTick <= now
  594. && (selected == nullptr || earlier_timer(timers[index], *selected))) {
  595. selected = &timers[index];
  596. }
  597. }
  598. if (selected == nullptr) {
  599. continue;
  600. }
  601. instance.pendingTimerEvent = {};
  602. instance.pendingTimerEvent.attemptGeneration = instance.attempt.generation;
  603. instance.pendingTimerEvent.binding = instance.view.binding;
  604. instance.pendingTimerEvent.timerName = selected->key;
  605. instance.pendingTimerEvent.sequence = selected->sequence;
  606. instance.pendingTimerEvent.tick = now;
  607. instance.pendingTimerEvent.sourceGeneration = instance.view.activityClientGeneration;
  608. instance.pendingTimerEvent.timerDeadlineTick = selected->deadlineTick;
  609. instance.pendingTimerEvent.timerSequence = selected->sequence;
  610. instance.pendingTimerEvent.missionSequence = instance.lastMissionSequence;
  611. instance.pendingTimerEvent.kind = host::EventKind::timerElapsed;
  612. instance.timerPending = true;
  613. }
  614. static_cast<void>(
  615. dispatch_event(instance, instance.pendingTimerEvent, nullptr, nullptr, now));
  616. instance.pendingTimerEvent = {};
  617. instance.timerPending = false;
  618. }
  619. }
  620. } // namespace
  621. /** Clears all state, reads the enable switch, and resolves the script and SDK Lua paths. */
  622. void initialize() noexcept {
  623. AcquireSRWLockExclusive(&g_lock);
  624. clear_attach_diagnostics();
  625. clear_all_pending_events();
  626. for (RuntimeInstance& instance : g_instances) {
  627. clear_instance(instance);
  628. }
  629. g_enabled = core::settings::get().server.activation.missionScripting;
  630. g_pathReady = false;
  631. reset_feed_cursors();
  632. if (!g_enabled) {
  633. ReleaseSRWLockExclusive(&g_lock);
  634. return;
  635. }
  636. g_pathReady = resolve_script_paths();
  637. if (!g_pathReady) {
  638. ReleaseSRWLockExclusive(&g_lock);
  639. return;
  640. }
  641. log_line(core::log::Level::info, nullptr, "initialize", "enabled");
  642. ReleaseSRWLockExclusive(&g_lock);
  643. }
  644. namespace {
  645. // A step this long blocks the game's loopback sends; the step that took it is named.
  646. constexpr std::uint64_t kSlowStepMilliseconds = 40;
  647. /** Runs one step of the mission tick and logs it when it runs past the slow limit. */
  648. template <typename Step> void timed_step(const char* name, Step&& step) noexcept {
  649. const std::uint64_t started = GetTickCount64();
  650. step();
  651. const std::uint64_t elapsed = GetTickCount64() - started;
  652. if (elapsed < kSlowStepMilliseconds) {
  653. return;
  654. }
  655. std::array<char, core::log::kLineCapacity> line{};
  656. const int written = std::snprintf(line.data(),
  657. line.size(),
  658. "ev=core stage=service result=slow step=mission.%s ms=%llu",
  659. name,
  660. static_cast<unsigned long long>(elapsed));
  661. if (written > 0) {
  662. core::log::write(core::log::Channel::server,
  663. core::log::Level::warn,
  664. {line.data(), static_cast<std::size_t>(written)});
  665. }
  666. }
  667. } // namespace
  668. /**
  669. * Runs one mission tick under the runtime lock, timing each step.
  670. * @param now Service tick every step measures its deadlines from.
  671. */
  672. void service(std::uint64_t now) noexcept {
  673. AcquireSRWLockExclusive(&g_lock);
  674. if (!g_enabled || !g_pathReady) {
  675. ReleaseSRWLockExclusive(&g_lock);
  676. return;
  677. }
  678. timed_step("synchronize", [now] { synchronize_instances(now); });
  679. timed_step("starts", [now] { service_pending_starts(now); });
  680. timed_step("delivery_events", [now] { consume_delivery_events(now); });
  681. timed_step("inputs", [now] { consume_mission_inputs(now); });
  682. timed_step("scriptless", [] { retire_scriptless_inputs(); });
  683. timed_step("timers", [now] { service_timers(now); });
  684. timed_step("script_events", [now] { service_script_events(now); });
  685. timed_step("dispatch", [now] {
  686. for (RuntimeInstance& instance : g_instances) {
  687. if (instance.occupied) {
  688. if (instance.programStatus == ProgramStatus::programError) {
  689. reconcile_terminal_delivery(instance);
  690. } else {
  691. dispatch_intent(instance, now);
  692. }
  693. }
  694. }
  695. });
  696. timed_step("reload", [now] { service_reload(now); });
  697. ReleaseSRWLockExclusive(&g_lock);
  698. }
  699. /** Copies every open instance row and every retained attach row for the panel. */
  700. void snapshot(DiagnosticsSnapshot& output) noexcept {
  701. output = {};
  702. AcquireSRWLockShared(&g_lock);
  703. output.enabled = g_enabled;
  704. output.pathReady = g_pathReady;
  705. for (const RuntimeInstance& instance : g_instances) {
  706. if (instance.occupied && output.instanceCount < output.instances.size()) {
  707. copy_diagnostics(instance, output.instances[output.instanceCount++]);
  708. }
  709. }
  710. snapshot_attach_diagnostics(output);
  711. ReleaseSRWLockShared(&g_lock);
  712. }
  713. /** @return True when mission scripting is on and its paths resolved. */
  714. bool runtime_ready() noexcept {
  715. AcquireSRWLockShared(&g_lock);
  716. const bool ready = g_enabled && g_pathReady;
  717. ReleaseSRWLockShared(&g_lock);
  718. return ready;
  719. }
  720. /** Closes every instance and clears every global this unit owns. */
  721. void shutdown() noexcept {
  722. AcquireSRWLockExclusive(&g_lock);
  723. for (RuntimeInstance& instance : g_instances) {
  724. clear_instance(instance);
  725. }
  726. clear_all_pending_events();
  727. clear_attach_diagnostics();
  728. clear_script_sources();
  729. clear_feed_cursors();
  730. clear_reload_mailbox();
  731. g_enabled = false;
  732. g_pathReady = false;
  733. ReleaseSRWLockExclusive(&g_lock);
  734. }
  735. } // namespace sunrise::server::activity::mission