mission_script_vm.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. #include <algorithm>
  2. #include <cstddef>
  3. #include <cstring>
  4. #include <limits>
  5. #include <new>
  6. #include "mission_script_vm_internal.h"
  7. namespace sunrise::server::activity::mission::lua_vm {
  8. namespace detail {
  9. struct VmAccess final {
  10. [[nodiscard]] static Impl& get(Vm& vm) noexcept {
  11. return *std::launder(reinterpret_cast<Impl*>(vm.storage_.data()));
  12. }
  13. [[nodiscard]] static const Impl& get(const Vm& vm) noexcept {
  14. return *std::launder(reinterpret_cast<const Impl*>(vm.storage_.data()));
  15. }
  16. };
  17. } // namespace detail
  18. namespace {
  19. using detail::CallFrame;
  20. using detail::Candidate;
  21. using detail::Handler;
  22. using detail::Impl;
  23. using detail::impl_from_state;
  24. using detail::raise_lua_error;
  25. using detail::VmAccess;
  26. static_assert(sizeof(Impl) <= kVmStorageByteCapacity);
  27. static_assert(alignof(Impl) <= alignof(std::max_align_t));
  28. /** Reads only an already-interned Lua string; coercing arbitrary errors can allocate. */
  29. [[nodiscard]] const char* existing_error(lua_State* state, const char* fallback) noexcept {
  30. return lua_type(state, -1) == LUA_TSTRING ? lua_tostring(state, -1) : fallback;
  31. }
  32. /** Instructions between budget checks. Every lua_sethook call must charge this same count. */
  33. constexpr std::uint32_t kHookInterval = 100;
  34. /** Charges the fixed interval against the frame budget and errors once it cannot be paid. */
  35. void instruction_hook(lua_State* state, lua_Debug*) {
  36. Impl* const impl = impl_from_state(state);
  37. if (impl == nullptr || impl->frame == nullptr) {
  38. raise_lua_error(state, "instruction_budget");
  39. }
  40. if (impl->frame->remainingInstructions <= kHookInterval) {
  41. impl->frame->remainingInstructions = 0;
  42. impl->frame->instructionExceeded = true;
  43. luaL_error(state, "instruction_budget");
  44. }
  45. impl->frame->remainingInstructions -= kHookInterval;
  46. }
  47. /** Takes a registry reference to one named entry; nil is allowed, a non-function is refused. */
  48. [[nodiscard]] bool capture_handler(lua_State* state, int table, const char* key, int& output) {
  49. lua_pushstring(state, key);
  50. lua_rawget(state, table);
  51. if (lua_isnil(state, -1)) {
  52. lua_pop(state, 1);
  53. output = LUA_NOREF;
  54. return true;
  55. }
  56. if (!lua_isfunction(state, -1)) {
  57. lua_pop(state, 1);
  58. return false;
  59. }
  60. output = luaL_ref(state, LUA_REGISTRYINDEX);
  61. return output != LUA_NOREF && output != LUA_REFNIL;
  62. }
  63. /** Captures the optional generated mission-state declaration without retaining its Lua table. */
  64. [[nodiscard]] bool capture_initial_state(lua_State* state, int program, Impl& impl) noexcept {
  65. impl.initialStateRegion = -1;
  66. impl.hasInitialState = false;
  67. lua_pushliteral(state, "initial_state");
  68. lua_rawget(state, program);
  69. if (lua_isnil(state, -1)) {
  70. lua_pop(state, 1);
  71. return true;
  72. }
  73. if (!lua_istable(state, -1)) {
  74. lua_pop(state, 1);
  75. return false;
  76. }
  77. lua_pushliteral(state, "region_index");
  78. lua_rawget(state, -2);
  79. if (!lua_isinteger(state, -1)) {
  80. lua_pop(state, 2);
  81. return false;
  82. }
  83. const lua_Integer region = lua_tointeger(state, -1);
  84. if (region < 0
  85. || static_cast<std::uint64_t>(region)
  86. > static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)())) {
  87. lua_pop(state, 2);
  88. return false;
  89. }
  90. impl.initialStateRegion = static_cast<std::int32_t>(region);
  91. impl.hasInitialState = true;
  92. lua_pop(state, 2);
  93. return true;
  94. }
  95. // One callback name per EventKind, in EventKind order. The index is the kind.
  96. inline constexpr std::array<const char*, host::kEventKindCount> kEventHandlerNames{{
  97. "on_event_sensor_sense_updated",
  98. "on_event_client_state_changed",
  99. "on_event_client_message_received",
  100. "on_event_auth_state_committed",
  101. "on_event_auth_state_transport_staged",
  102. "on_event_auth_state_canceled",
  103. "on_event_incident_received",
  104. "on_event_incident_queued",
  105. "on_event_incident_transport_staged",
  106. "on_event_incident_canceled",
  107. "on_event_incident_refused",
  108. "on_event_scriptable_override_committed",
  109. "on_event_scriptable_override_transport_staged",
  110. "on_event_scriptable_override_canceled",
  111. "on_event_operator_refused",
  112. "on_event_timer_elapsed",
  113. "on_event_effect_result",
  114. "on_event_phase_entered",
  115. "on_event_trigger_entered",
  116. "on_event_trigger_exited",
  117. "on_event_squad_state",
  118. "on_event_entity_spawned",
  119. "on_event_entity_died",
  120. "on_event_scene_finished",
  121. "on_event_objective_progress",
  122. "on_event_entity_slots_requested",
  123. "on_event_session_joined",
  124. "on_event_session_left",
  125. "on_event_player_trigger",
  126. "on_event_cinematic_started",
  127. "on_event_cinematic_terminated",
  128. "on_event_ghost_link_state",
  129. "on_event_actor_path_state",
  130. "on_event_object_interacted",
  131. "on_event_cinematic_skip_requested",
  132. "on_event_fireteam_state",
  133. "on_event_object_state",
  134. "on_event_damage_state",
  135. "on_event_squad_provoked",
  136. "on_event_device_state",
  137. }};
  138. static_assert([] {
  139. for (const auto* name : kEventHandlerNames) {
  140. if (name == nullptr) {
  141. return false;
  142. }
  143. }
  144. return true;
  145. }());
  146. /** Captures immutable callback references while table access remains inside lua_pcall. */
  147. [[nodiscard]] int capture_program(lua_State* state) {
  148. Impl* const impl = impl_from_state(state);
  149. if (impl == nullptr || lua_type(state, 1) != LUA_TTABLE) {
  150. return luaL_error(state, "mission chunk must return one program table");
  151. }
  152. lua_pushliteral(state, "on_event");
  153. lua_rawget(state, 1);
  154. const bool genericEventHandler = !lua_isnil(state, -1);
  155. lua_pop(state, 1);
  156. if (genericEventHandler) {
  157. return luaL_error(state, "on_event is unsupported; use one on_event_<type> callback");
  158. }
  159. if (!capture_initial_state(state, 1, *impl)) {
  160. return luaL_error(state,
  161. "initial_state must be a generated state with an integer region_index");
  162. }
  163. if (!capture_handler(state, 1, "on_start", impl->startReference)
  164. || !capture_handler(state, 1, "on_load", impl->loadReference)) {
  165. return luaL_error(state, "mission entries must be functions or nil");
  166. }
  167. for (std::size_t index = 0; index < kEventHandlerNames.size(); ++index) {
  168. if (!capture_handler(state, 1, kEventHandlerNames[index], impl->eventReferences[index])) {
  169. return luaL_error(state, "mission event entries must be functions or nil");
  170. }
  171. }
  172. return 0;
  173. }
  174. void preserve_failure_and_close(Impl& impl) noexcept {
  175. const std::array<char, 256> error = impl.lastError;
  176. detail::destroy_state(impl);
  177. impl.lastError = error;
  178. impl.faulted = true;
  179. }
  180. [[nodiscard]] bool contains_error(const Impl& impl, std::string_view marker) noexcept {
  181. const std::string_view error(impl.lastError.data());
  182. return error.find(marker) != std::string_view::npos;
  183. }
  184. [[nodiscard]] CallStatus
  185. failed_call_status(const Impl& impl, const CallFrame& frame, int luaStatus) noexcept {
  186. if (luaStatus == LUA_ERRMEM || frame.memoryExceeded) {
  187. return CallStatus::outOfMemory;
  188. }
  189. return contains_error(impl, "instruction_budget") ? CallStatus::instructionBudget
  190. : CallStatus::scriptError;
  191. }
  192. // Past three quarters of the arena a step cannot keep up, so a full collection runs; below
  193. // that the step is charged 64 KiB at a time.
  194. constexpr std::size_t kCollectionPressureBytes = (kArenaByteCapacity * 3U) / 4U;
  195. constexpr int kCollectionStepKilobytes = 64;
  196. /** Advances collection only after Lua has left a protected transaction. */
  197. void collect_after_transaction(Impl& impl) noexcept {
  198. if (impl.state != nullptr) {
  199. const int operation =
  200. impl.arena.used >= kCollectionPressureBytes ? LUA_GCCOLLECT : LUA_GCSTEP;
  201. const int amount = operation == LUA_GCSTEP ? kCollectionStepKilobytes : 0;
  202. static_cast<void>(lua_gc(impl.state, operation, amount));
  203. static_cast<void>(lua_gc(impl.state, LUA_GCSTOP, 0));
  204. ++impl.collections;
  205. }
  206. }
  207. [[nodiscard]] bool same_key(const StateKey& left, const StateKey& right) noexcept {
  208. return left.length == right.length && left.bytes == right.bytes;
  209. }
  210. [[nodiscard]] bool same_value(const VariableValue& left, const VariableValue& right) noexcept {
  211. return left.stringValue == right.stringValue && left.integerValue == right.integerValue
  212. && left.realValue == right.realValue && left.stringLength == right.stringLength
  213. && left.kind == right.kind && left.booleanValue == right.booleanValue;
  214. }
  215. [[nodiscard]] bool same_variable(const ScriptVariable& left, const ScriptVariable& right) noexcept {
  216. return same_key(left.key, right.key) && same_value(left.value, right.value);
  217. }
  218. [[nodiscard]] bool same_timer(const MissionTimer& left, const MissionTimer& right) noexcept {
  219. return same_key(left.key, right.key) && left.deadlineTick == right.deadlineTick
  220. && left.sequence == right.sequence;
  221. }
  222. /** True when the candidate differs from committed durable state in any field it may change. */
  223. [[nodiscard]] bool durable_delta(const Impl& impl, const Candidate& candidate) noexcept {
  224. if ((candidate.phaseChanged && candidate.phase != impl.phase)
  225. || candidate.variableCount != impl.variableCount || candidate.timerCount != impl.timerCount
  226. || candidate.nextTimerSequence != impl.nextTimerSequence
  227. || candidate.nextIntentKey != impl.nextIntentKey) {
  228. return true;
  229. }
  230. for (std::size_t index = 0; index < candidate.variableCount; ++index) {
  231. if (!same_variable(candidate.variables[index], impl.variables[index])) {
  232. return true;
  233. }
  234. }
  235. for (std::size_t index = 0; index < candidate.timerCount; ++index) {
  236. if (!same_timer(candidate.timers[index], impl.timers[index])) {
  237. return true;
  238. }
  239. }
  240. return false;
  241. }
  242. void initialize_candidate(const Impl& impl, Candidate& candidate) noexcept {
  243. candidate.phase = impl.phase;
  244. candidate.variables = impl.variables;
  245. candidate.timers = impl.timers;
  246. candidate.variableCount = impl.variableCount;
  247. candidate.timerCount = impl.timerCount;
  248. candidate.nextTimerSequence = impl.nextTimerSequence;
  249. candidate.nextIntentKey = impl.nextIntentKey;
  250. }
  251. /** Consumes only the exact active timer represented by an internal elapsed event. */
  252. [[nodiscard]] bool consume_elapsed_timer(Candidate& candidate, const host::Event& event) noexcept {
  253. if (event.kind != host::EventKind::timerElapsed) {
  254. return true;
  255. }
  256. for (std::size_t index = 0; index < candidate.timerCount; ++index) {
  257. const MissionTimer& timer = candidate.timers[index];
  258. if (timer.sequence != event.timerSequence || timer.deadlineTick != event.timerDeadlineTick
  259. || !same_key(timer.key, event.timerName)) {
  260. continue;
  261. }
  262. for (std::size_t move = index + 1; move < candidate.timerCount; ++move) {
  263. candidate.timers[move - 1] = candidate.timers[move];
  264. }
  265. --candidate.timerCount;
  266. candidate.timers[candidate.timerCount] = {};
  267. return true;
  268. }
  269. return false;
  270. }
  271. /** Publishes one callback candidate without allowing its revision to wrap. */
  272. [[nodiscard]] CallStatus commit_candidate(Impl& impl, Candidate& candidate) noexcept {
  273. const bool phaseChanged = candidate.phaseChanged && candidate.phase != impl.phase;
  274. const bool stateChanged = durable_delta(impl, candidate);
  275. if ((stateChanged || !candidate.intents.empty())
  276. && impl.stateRevision == (std::numeric_limits<std::uint64_t>::max)()) {
  277. detail::set_error(impl, "mission state revision is exhausted");
  278. ++impl.refusedCallbacks;
  279. impl.faulted = true;
  280. return CallStatus::scriptError;
  281. }
  282. if (impl.outboxRead != 0) {
  283. impl.outbox.erase(impl.outbox.begin(),
  284. impl.outbox.begin() + static_cast<std::ptrdiff_t>(impl.outboxRead));
  285. impl.outboxRead = 0;
  286. }
  287. if (candidate.intents.size() > impl.outbox.max_size() - impl.outbox.size()) {
  288. detail::set_error(impl, "mission outbox allocation failed");
  289. ++impl.refusedCallbacks;
  290. impl.faulted = true;
  291. return CallStatus::outOfMemory;
  292. }
  293. try {
  294. impl.outbox.reserve(impl.outbox.size() + candidate.intents.size());
  295. } catch (const std::bad_alloc&) {
  296. detail::set_error(impl, "mission outbox allocation failed");
  297. ++impl.refusedCallbacks;
  298. impl.faulted = true;
  299. return CallStatus::outOfMemory;
  300. }
  301. for (Intent& intent : candidate.intents) {
  302. impl.outbox.push_back(std::move(intent));
  303. }
  304. if (phaseChanged) {
  305. impl.phase = candidate.phase;
  306. }
  307. if (stateChanged) {
  308. impl.variables = candidate.variables;
  309. impl.timers = candidate.timers;
  310. impl.variableCount = candidate.variableCount;
  311. impl.timerCount = candidate.timerCount;
  312. impl.nextTimerSequence = candidate.nextTimerSequence;
  313. impl.nextIntentKey = candidate.nextIntentKey;
  314. }
  315. if (stateChanged || !candidate.intents.empty()) {
  316. ++impl.stateRevision;
  317. }
  318. impl.lastError = {};
  319. ++impl.committedCallbacks;
  320. return CallStatus::committed;
  321. }
  322. /** Runs one callback under the instruction budget, then commits its candidate or faults the vm. */
  323. [[nodiscard]] CallStatus invoke(Impl& impl,
  324. Handler handler,
  325. const host::Event* event,
  326. const host::ClientMessageSnapshot* clientMessage,
  327. std::uint64_t now) noexcept {
  328. if (!impl.active || impl.state == nullptr || impl.faulted) {
  329. return CallStatus::inactive;
  330. }
  331. CallFrame frame{};
  332. initialize_candidate(impl, frame.candidate);
  333. if (event != nullptr && !consume_elapsed_timer(frame.candidate, *event)) {
  334. detail::set_error(impl, "mission timer event no longer matches authoritative state");
  335. ++impl.refusedCallbacks;
  336. impl.faulted = true;
  337. return CallStatus::scriptError;
  338. }
  339. frame.handler = handler;
  340. frame.event = event;
  341. frame.clientMessage = clientMessage;
  342. frame.now = now;
  343. impl.frame = &frame;
  344. ++impl.callbacks;
  345. lua_sethook(impl.state, &instruction_hook, LUA_MASKCOUNT, kHookInterval);
  346. lua_pushcfunction(impl.state, &detail::protected_callback);
  347. const int result = lua_pcall(impl.state, 0, 1, 0);
  348. lua_sethook(impl.state, nullptr, 0, 0);
  349. impl.frame = nullptr;
  350. if (result != LUA_OK) {
  351. detail::set_error(
  352. impl, existing_error(impl.state, "mission callback failed with a non-string error"));
  353. lua_settop(impl.state, 0);
  354. collect_after_transaction(impl);
  355. ++impl.refusedCallbacks;
  356. impl.faulted = true;
  357. return failed_call_status(impl, frame, result);
  358. }
  359. const bool handled = lua_toboolean(impl.state, -1) != 0;
  360. lua_settop(impl.state, 0);
  361. collect_after_transaction(impl);
  362. if (!handled) {
  363. if (durable_delta(impl, frame.candidate)) {
  364. const CallStatus committed = commit_candidate(impl, frame.candidate);
  365. return committed == CallStatus::committed ? CallStatus::noHandler : committed;
  366. }
  367. return CallStatus::noHandler;
  368. }
  369. return commit_candidate(impl, frame.candidate);
  370. }
  371. } // namespace
  372. Vm::Vm() noexcept {
  373. // No arena block yet. An activity slot with no open program holds none.
  374. static_cast<void>(::new (storage_.data()) detail::Impl{});
  375. }
  376. Vm::~Vm() noexcept {
  377. detail::Impl& impl = detail::VmAccess::get(*this);
  378. detail::destroy_state(impl);
  379. impl.~Impl();
  380. }
  381. /** Replaces the bound view of a program that is already open and running. */
  382. bool rebind(Vm& vm, const ProgramIdentity& identity, const DefinitionApi& definitions) noexcept {
  383. Impl& impl = VmAccess::get(vm);
  384. if (impl.state == nullptr || !impl.active || impl.faulted) {
  385. return false;
  386. }
  387. impl.identity = identity;
  388. impl.definitions = definitions;
  389. return true;
  390. }
  391. /** Compiles and validates one build-bound text chunk in a fresh arena. */
  392. OpenStatus open(Vm& vm,
  393. const ProgramIdentity& identity,
  394. const DefinitionApi& definitions,
  395. std::span<const char> source) noexcept {
  396. Impl& impl = VmAccess::get(vm);
  397. detail::destroy_state(impl);
  398. impl.arenaBytesAfterClose = 0;
  399. if (source.empty() || source.size() > kSourceByteCapacity) {
  400. detail::set_error(impl, "mission source is empty or exceeds 128 KiB");
  401. impl.faulted = true;
  402. return OpenStatus::sourceTooLarge;
  403. }
  404. // The build id is the 7-byte "sha256:" prefix plus 64 hex digits, 71 characters total.
  405. constexpr std::string_view kSdkBuildIdPrefix = "sha256:";
  406. constexpr std::size_t kSdkBuildIdLength = kSdkBuildIdPrefix.size() + 64;
  407. if (identity.sdkBuildId.back() != '\0'
  408. || std::strlen(identity.sdkBuildId.data()) != kSdkBuildIdLength
  409. || std::string_view(identity.sdkBuildId.data(), kSdkBuildIdPrefix.size())
  410. != kSdkBuildIdPrefix
  411. || identity.activityId.back() != '\0' || identity.activityId.front() == '\0'
  412. || definitions.resolveSquadRow == nullptr || definitions.resolveSquadId == nullptr
  413. || definitions.resolveSceneRow == nullptr || definitions.resolveSceneId == nullptr
  414. || definitions.resolveSlotRow == nullptr || definitions.resolveSlotId == nullptr
  415. || definitions.resolveSenseSlot == nullptr
  416. || definitions.resolveActivityMessageRow == nullptr
  417. || definitions.resolveActivityMessageId == nullptr
  418. || definitions.resolveActivityMessageName == nullptr
  419. || definitions.resolveActivityMessageFieldRow == nullptr
  420. || definitions.resolveActivityMessageFieldIndex == nullptr
  421. || definitions.resolveActivityBinding == nullptr
  422. || definitions.activityBindingTagCount == nullptr
  423. || definitions.resolveActivityBindingTag == nullptr
  424. || definitions.activityBindingLocatorCount == nullptr
  425. || definitions.resolveActivityBindingLocator == nullptr || definitions.squadCount == nullptr
  426. || definitions.sceneCount == nullptr || definitions.slotCount == nullptr
  427. || definitions.activityMessageCount == nullptr) {
  428. detail::set_error(impl, "mission identity or SDK definition bridge is invalid");
  429. impl.faulted = true;
  430. return OpenStatus::invalidProgram;
  431. }
  432. impl.identity = identity;
  433. impl.definitions = definitions;
  434. if (!detail::arena_initialize(impl.arena)) {
  435. detail::set_error(impl, "mission arena block could not be taken from the heap");
  436. impl.faulted = true;
  437. return OpenStatus::outOfMemory;
  438. }
  439. impl.state = lua_newstate(&detail::arena_allocate, &impl.arena);
  440. if (impl.state == nullptr) {
  441. detail::set_error(impl, "Lua state exceeded its fixed arena");
  442. preserve_failure_and_close(impl);
  443. return OpenStatus::outOfMemory;
  444. }
  445. static_cast<void>(lua_gc(impl.state, LUA_GCSTOP, 0));
  446. if (!detail::initialize_sandbox(impl)) {
  447. const OpenStatus status =
  448. contains_error(impl, "memory") ? OpenStatus::outOfMemory : OpenStatus::runtimeError;
  449. preserve_failure_and_close(impl);
  450. return status;
  451. }
  452. const int compiled =
  453. luaL_loadbufferx(impl.state, source.data(), source.size(), identity.activityId.data(), "t");
  454. if (compiled != LUA_OK) {
  455. detail::set_error(
  456. impl, existing_error(impl.state, "mission compile failed with a non-string error"));
  457. const OpenStatus status =
  458. compiled == LUA_ERRMEM ? OpenStatus::outOfMemory : OpenStatus::compileError;
  459. preserve_failure_and_close(impl);
  460. return status;
  461. }
  462. CallFrame loadFrame{};
  463. loadFrame.remainingInstructions = kInitializationInstructionBudget;
  464. impl.frame = &loadFrame;
  465. lua_sethook(impl.state, &instruction_hook, LUA_MASKCOUNT, kHookInterval);
  466. const int executed = lua_pcall(impl.state, 0, 1, 0);
  467. lua_sethook(impl.state, nullptr, 0, 0);
  468. impl.frame = nullptr;
  469. if (executed != LUA_OK) {
  470. detail::set_error(
  471. impl,
  472. existing_error(impl.state, "mission initialization failed with a non-string error"));
  473. const OpenStatus status = executed == LUA_ERRMEM || loadFrame.memoryExceeded
  474. ? OpenStatus::outOfMemory
  475. : OpenStatus::runtimeError;
  476. preserve_failure_and_close(impl);
  477. return status;
  478. }
  479. lua_pushcfunction(impl.state, &capture_program);
  480. lua_insert(impl.state, -2);
  481. const int captured = lua_pcall(impl.state, 1, 0, 0);
  482. if (captured != LUA_OK) {
  483. detail::set_error(
  484. impl, existing_error(impl.state, "mission declaration failed with a non-string error"));
  485. const OpenStatus status =
  486. captured == LUA_ERRMEM ? OpenStatus::outOfMemory : OpenStatus::invalidProgram;
  487. preserve_failure_and_close(impl);
  488. return status;
  489. }
  490. lua_settop(impl.state, 0);
  491. impl.active = true;
  492. impl.faulted = false;
  493. return OpenStatus::ready;
  494. }
  495. /**
  496. * Publishes the current peer set.
  497. * It replaces the previous set outright, so a peer that left is gone on the next read.
  498. */
  499. void publish_peers(Vm& vm, std::span<const PeerSession> peers) noexcept {
  500. Impl& impl = VmAccess::get(vm);
  501. const std::size_t count = (std::min)(peers.size(), impl.peers.size());
  502. impl.peers = {};
  503. std::copy_n(peers.begin(), count, impl.peers.begin());
  504. impl.peerCount = static_cast<std::uint8_t>(count);
  505. }
  506. /** Restores state and typed actions only before any callback or action transaction. */
  507. bool restore_state(Vm& vm,
  508. std::uint32_t phase,
  509. std::uint64_t revision,
  510. std::span<const ScriptVariable> variables,
  511. std::span<const MissionTimer> timers,
  512. std::uint64_t nextTimerSequence,
  513. std::uint64_t nextIntentKey,
  514. std::span<const Intent> pendingIntents) noexcept {
  515. Impl& impl = VmAccess::get(vm);
  516. if (!impl.active || impl.state == nullptr || impl.faulted || impl.frame != nullptr
  517. || impl.callbacks != 0 || !impl.outbox.empty() || variables.size() > impl.variables.size()
  518. || timers.size() > impl.timers.size()) {
  519. return false;
  520. }
  521. impl.phase = phase;
  522. impl.stateRevision = revision;
  523. std::copy(variables.begin(), variables.end(), impl.variables.begin());
  524. std::fill(impl.variables.begin() + static_cast<std::ptrdiff_t>(variables.size()),
  525. impl.variables.end(),
  526. ScriptVariable{});
  527. std::copy(timers.begin(), timers.end(), impl.timers.begin());
  528. std::fill(impl.timers.begin() + static_cast<std::ptrdiff_t>(timers.size()),
  529. impl.timers.end(),
  530. MissionTimer{});
  531. impl.variableCount = variables.size();
  532. impl.timerCount = timers.size();
  533. impl.nextTimerSequence = nextTimerSequence;
  534. impl.nextIntentKey = nextIntentKey;
  535. try {
  536. impl.outbox.assign(pendingIntents.begin(), pendingIntents.end());
  537. } catch (const std::bad_alloc&) {
  538. return false;
  539. }
  540. impl.outboxRead = 0;
  541. return true;
  542. }
  543. /** Restores state when no typed actions are pending. */
  544. bool restore_state(Vm& vm, std::uint32_t phase, std::uint64_t revision) noexcept {
  545. return restore_state(vm,
  546. phase,
  547. revision,
  548. {},
  549. {},
  550. state::activity::mission::kFirstTimerSequence,
  551. state::activity::mission::kFirstIntentKey,
  552. {});
  553. }
  554. /** Runs start and reserves one revision for the durable started transition. */
  555. CallStatus start(Vm& vm, std::uint64_t now) noexcept {
  556. Impl& impl = VmAccess::get(vm);
  557. const std::uint64_t before = impl.stateRevision;
  558. const CallStatus status = invoke(impl, Handler::start, nullptr, nullptr, now);
  559. if (status != CallStatus::committed && status != CallStatus::noHandler) {
  560. return status;
  561. }
  562. if (impl.stateRevision != before) {
  563. return status;
  564. }
  565. if (impl.stateRevision == (std::numeric_limits<std::uint64_t>::max)()) {
  566. detail::set_error(impl, "mission start revision is exhausted");
  567. impl.faulted = true;
  568. return CallStatus::scriptError;
  569. }
  570. ++impl.stateRevision;
  571. return status;
  572. }
  573. /**
  574. * Runs load for a program whose durable state was restored.
  575. * It reserves no revision. A restore is not a state transition, so the revision is already correct.
  576. */
  577. CallStatus load(Vm& vm, std::uint64_t now) noexcept {
  578. Impl& impl = VmAccess::get(vm);
  579. // A program that declares no on_load must cost a reattach nothing at all.
  580. if (impl.loadReference == LUA_NOREF && impl.active && impl.state != nullptr && !impl.faulted) {
  581. return CallStatus::noHandler;
  582. }
  583. return invoke(impl, Handler::load, nullptr, nullptr, now);
  584. }
  585. CallStatus dispatch(Vm& vm,
  586. const host::Event& event,
  587. const host::ClientMessageSnapshot* clientMessage,
  588. std::uint64_t now) noexcept {
  589. return invoke(VmAccess::get(vm), Handler::event, &event, clientMessage, now);
  590. }
  591. bool handles_event(const Vm& vm, host::EventKind kind) noexcept {
  592. const Impl& impl = VmAccess::get(vm);
  593. return impl.active && !impl.faulted && detail::event_reference(impl, kind) != LUA_NOREF;
  594. }
  595. bool initial_state_region(const Vm& vm, std::int32_t& output) noexcept {
  596. const Impl& impl = VmAccess::get(vm);
  597. output = -1;
  598. if (!impl.active || impl.faulted || !impl.hasInitialState || impl.initialStateRegion < 0) {
  599. return false;
  600. }
  601. output = impl.initialStateRegion;
  602. return true;
  603. }
  604. /** Copies the next unread outbox action without consuming it; false when none is pending. */
  605. bool pending_intent(const Vm& vm, Intent& output) noexcept {
  606. const Impl& impl = VmAccess::get(vm);
  607. output = {};
  608. if (!impl.active || impl.outboxRead >= impl.outbox.size()) {
  609. return false;
  610. }
  611. try {
  612. output = impl.outbox[impl.outboxRead];
  613. } catch (const std::bad_alloc&) {
  614. output = {};
  615. return false;
  616. }
  617. return true;
  618. }
  619. /** Copies the complete ordered outbox without consuming an action. */
  620. bool snapshot_intents(const Vm& vm, std::vector<Intent>& output) noexcept {
  621. const Impl& impl = VmAccess::get(vm);
  622. output.clear();
  623. if (!impl.active) {
  624. return false;
  625. }
  626. try {
  627. output.assign(impl.outbox.begin() + static_cast<std::ptrdiff_t>(impl.outboxRead),
  628. impl.outbox.end());
  629. } catch (const std::bad_alloc&) {
  630. output.clear();
  631. return false;
  632. }
  633. return true;
  634. }
  635. /** Copies complete durable script state without changing its revision. */
  636. bool snapshot_durable_state(const Vm& vm,
  637. std::span<ScriptVariable> variables,
  638. std::size_t& variableCount,
  639. std::span<MissionTimer> timers,
  640. std::size_t& timerCount,
  641. std::uint64_t& nextTimerSequence,
  642. std::uint64_t& nextIntentKey) noexcept {
  643. const Impl& impl = VmAccess::get(vm);
  644. variableCount = 0;
  645. timerCount = 0;
  646. nextTimerSequence = state::activity::mission::kAbsentTimerSequence;
  647. nextIntentKey = state::activity::mission::kAbsentIntentKey;
  648. if (!impl.active || variables.size() < impl.variableCount || timers.size() < impl.timerCount) {
  649. return false;
  650. }
  651. std::copy_n(impl.variables.begin(), impl.variableCount, variables.begin());
  652. std::copy_n(impl.timers.begin(), impl.timerCount, timers.begin());
  653. variableCount = impl.variableCount;
  654. timerCount = impl.timerCount;
  655. nextTimerSequence = impl.nextTimerSequence;
  656. nextIntentKey = impl.nextIntentKey;
  657. return true;
  658. }
  659. /** Drops the oldest unread action, and empties the outbox once every action has been read. */
  660. void consume_intent(Vm& vm) noexcept {
  661. Impl& impl = VmAccess::get(vm);
  662. if (impl.outboxRead >= impl.outbox.size()) {
  663. return;
  664. }
  665. impl.outbox[impl.outboxRead] = {};
  666. ++impl.outboxRead;
  667. if (impl.outboxRead == impl.outbox.size()) {
  668. impl.outbox.clear();
  669. impl.outboxRead = 0;
  670. }
  671. }
  672. void discard_intents(Vm& vm) noexcept {
  673. Impl& impl = VmAccess::get(vm);
  674. std::vector<Intent>{}.swap(impl.outbox);
  675. impl.outboxRead = 0;
  676. }
  677. void fault(Vm& vm, std::string_view reason) noexcept {
  678. Impl& impl = VmAccess::get(vm);
  679. detail::set_error(impl, reason);
  680. impl.faulted = true;
  681. }
  682. /** Copies the counters, durable sizes and arena figures a caller reads for diagnostics. */
  683. void snapshot(const Vm& vm, Snapshot& output) noexcept {
  684. const Impl& impl = VmAccess::get(vm);
  685. output = {};
  686. output.lastError = impl.lastError;
  687. output.stateRevision = impl.stateRevision;
  688. output.callbacks = impl.callbacks;
  689. output.committedCallbacks = impl.committedCallbacks;
  690. output.refusedCallbacks = impl.refusedCallbacks;
  691. output.collections = impl.collections;
  692. output.phase = impl.phase;
  693. output.pendingIntents = impl.outbox.size() - impl.outboxRead;
  694. output.variableCount = impl.variableCount;
  695. output.timerCount = impl.timerCount;
  696. output.nextTimerSequence = impl.nextTimerSequence;
  697. output.nextIntentKey = impl.nextIntentKey;
  698. output.initialStateRegion = impl.initialStateRegion;
  699. output.arenaBytes = impl.arena.used;
  700. output.arenaHighWater = impl.arena.highWater;
  701. output.arenaBytesAfterClose = impl.arenaBytesAfterClose;
  702. output.active = impl.active;
  703. output.faulted = impl.faulted;
  704. output.hasInitialState = impl.hasInitialState;
  705. }
  706. void close(Vm& vm) noexcept {
  707. detail::destroy_state(VmAccess::get(vm));
  708. }
  709. /** Stable lowercase log token for one open status. */
  710. const char* status_name(OpenStatus status) noexcept {
  711. switch (status) {
  712. case OpenStatus::ready:
  713. return "ready";
  714. case OpenStatus::sourceTooLarge:
  715. return "source_too_large";
  716. case OpenStatus::outOfMemory:
  717. return "out_of_memory";
  718. case OpenStatus::compileError:
  719. return "compile_error";
  720. case OpenStatus::runtimeError:
  721. return "runtime_error";
  722. case OpenStatus::invalidProgram:
  723. return "invalid_program";
  724. }
  725. return "unknown";
  726. }
  727. /** Stable lowercase log token for one call status. */
  728. const char* status_name(CallStatus status) noexcept {
  729. switch (status) {
  730. case CallStatus::committed:
  731. return "committed";
  732. case CallStatus::noHandler:
  733. return "no_handler";
  734. case CallStatus::inactive:
  735. return "inactive";
  736. case CallStatus::scriptError:
  737. return "script_error";
  738. case CallStatus::instructionBudget:
  739. return "instruction_budget";
  740. case CallStatus::outOfMemory:
  741. return "out_of_memory";
  742. }
  743. return "unknown";
  744. }
  745. } // namespace sunrise::server::activity::mission::lua_vm