| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801 |
- #include <algorithm>
- #include <cstddef>
- #include <cstring>
- #include <limits>
- #include <new>
- #include "mission_script_vm_internal.h"
- namespace sunrise::server::activity::mission::lua_vm {
- namespace detail {
- struct VmAccess final {
- [[nodiscard]] static Impl& get(Vm& vm) noexcept {
- return *std::launder(reinterpret_cast<Impl*>(vm.storage_.data()));
- }
- [[nodiscard]] static const Impl& get(const Vm& vm) noexcept {
- return *std::launder(reinterpret_cast<const Impl*>(vm.storage_.data()));
- }
- };
- } // namespace detail
- namespace {
- using detail::CallFrame;
- using detail::Candidate;
- using detail::Handler;
- using detail::Impl;
- using detail::impl_from_state;
- using detail::raise_lua_error;
- using detail::VmAccess;
- static_assert(sizeof(Impl) <= kVmStorageByteCapacity);
- static_assert(alignof(Impl) <= alignof(std::max_align_t));
- /** Reads only an already-interned Lua string; coercing arbitrary errors can allocate. */
- [[nodiscard]] const char* existing_error(lua_State* state, const char* fallback) noexcept {
- return lua_type(state, -1) == LUA_TSTRING ? lua_tostring(state, -1) : fallback;
- }
- /** Instructions between budget checks. Every lua_sethook call must charge this same count. */
- constexpr std::uint32_t kHookInterval = 100;
- /** Charges the fixed interval against the frame budget and errors once it cannot be paid. */
- void instruction_hook(lua_State* state, lua_Debug*) {
- Impl* const impl = impl_from_state(state);
- if (impl == nullptr || impl->frame == nullptr) {
- raise_lua_error(state, "instruction_budget");
- }
- if (impl->frame->remainingInstructions <= kHookInterval) {
- impl->frame->remainingInstructions = 0;
- impl->frame->instructionExceeded = true;
- luaL_error(state, "instruction_budget");
- }
- impl->frame->remainingInstructions -= kHookInterval;
- }
- /** Takes a registry reference to one named entry; nil is allowed, a non-function is refused. */
- [[nodiscard]] bool capture_handler(lua_State* state, int table, const char* key, int& output) {
- lua_pushstring(state, key);
- lua_rawget(state, table);
- if (lua_isnil(state, -1)) {
- lua_pop(state, 1);
- output = LUA_NOREF;
- return true;
- }
- if (!lua_isfunction(state, -1)) {
- lua_pop(state, 1);
- return false;
- }
- output = luaL_ref(state, LUA_REGISTRYINDEX);
- return output != LUA_NOREF && output != LUA_REFNIL;
- }
- /** Captures the optional generated mission-state declaration without retaining its Lua table. */
- [[nodiscard]] bool capture_initial_state(lua_State* state, int program, Impl& impl) noexcept {
- impl.initialStateRegion = -1;
- impl.hasInitialState = false;
- lua_pushliteral(state, "initial_state");
- lua_rawget(state, program);
- if (lua_isnil(state, -1)) {
- lua_pop(state, 1);
- return true;
- }
- if (!lua_istable(state, -1)) {
- lua_pop(state, 1);
- return false;
- }
- lua_pushliteral(state, "region_index");
- lua_rawget(state, -2);
- if (!lua_isinteger(state, -1)) {
- lua_pop(state, 2);
- return false;
- }
- const lua_Integer region = lua_tointeger(state, -1);
- if (region < 0
- || static_cast<std::uint64_t>(region)
- > static_cast<std::uint64_t>((std::numeric_limits<std::int32_t>::max)())) {
- lua_pop(state, 2);
- return false;
- }
- impl.initialStateRegion = static_cast<std::int32_t>(region);
- impl.hasInitialState = true;
- lua_pop(state, 2);
- return true;
- }
- // One callback name per EventKind, in EventKind order. The index is the kind.
- inline constexpr std::array<const char*, host::kEventKindCount> kEventHandlerNames{{
- "on_event_sensor_sense_updated",
- "on_event_client_state_changed",
- "on_event_client_message_received",
- "on_event_auth_state_committed",
- "on_event_auth_state_transport_staged",
- "on_event_auth_state_canceled",
- "on_event_incident_received",
- "on_event_incident_queued",
- "on_event_incident_transport_staged",
- "on_event_incident_canceled",
- "on_event_incident_refused",
- "on_event_scriptable_override_committed",
- "on_event_scriptable_override_transport_staged",
- "on_event_scriptable_override_canceled",
- "on_event_operator_refused",
- "on_event_timer_elapsed",
- "on_event_effect_result",
- "on_event_phase_entered",
- "on_event_trigger_entered",
- "on_event_trigger_exited",
- "on_event_squad_state",
- "on_event_entity_spawned",
- "on_event_entity_died",
- "on_event_scene_finished",
- "on_event_objective_progress",
- "on_event_entity_slots_requested",
- "on_event_session_joined",
- "on_event_session_left",
- "on_event_player_trigger",
- "on_event_cinematic_started",
- "on_event_cinematic_terminated",
- "on_event_ghost_link_state",
- "on_event_actor_path_state",
- "on_event_object_interacted",
- "on_event_cinematic_skip_requested",
- "on_event_fireteam_state",
- "on_event_object_state",
- "on_event_damage_state",
- "on_event_squad_provoked",
- "on_event_device_state",
- }};
- static_assert([] {
- for (const auto* name : kEventHandlerNames) {
- if (name == nullptr) {
- return false;
- }
- }
- return true;
- }());
- /** Captures immutable callback references while table access remains inside lua_pcall. */
- [[nodiscard]] int capture_program(lua_State* state) {
- Impl* const impl = impl_from_state(state);
- if (impl == nullptr || lua_type(state, 1) != LUA_TTABLE) {
- return luaL_error(state, "mission chunk must return one program table");
- }
- lua_pushliteral(state, "on_event");
- lua_rawget(state, 1);
- const bool genericEventHandler = !lua_isnil(state, -1);
- lua_pop(state, 1);
- if (genericEventHandler) {
- return luaL_error(state, "on_event is unsupported; use one on_event_<type> callback");
- }
- if (!capture_initial_state(state, 1, *impl)) {
- return luaL_error(state,
- "initial_state must be a generated state with an integer region_index");
- }
- if (!capture_handler(state, 1, "on_start", impl->startReference)
- || !capture_handler(state, 1, "on_load", impl->loadReference)) {
- return luaL_error(state, "mission entries must be functions or nil");
- }
- for (std::size_t index = 0; index < kEventHandlerNames.size(); ++index) {
- if (!capture_handler(state, 1, kEventHandlerNames[index], impl->eventReferences[index])) {
- return luaL_error(state, "mission event entries must be functions or nil");
- }
- }
- return 0;
- }
- void preserve_failure_and_close(Impl& impl) noexcept {
- const std::array<char, 256> error = impl.lastError;
- detail::destroy_state(impl);
- impl.lastError = error;
- impl.faulted = true;
- }
- [[nodiscard]] bool contains_error(const Impl& impl, std::string_view marker) noexcept {
- const std::string_view error(impl.lastError.data());
- return error.find(marker) != std::string_view::npos;
- }
- [[nodiscard]] CallStatus
- failed_call_status(const Impl& impl, const CallFrame& frame, int luaStatus) noexcept {
- if (luaStatus == LUA_ERRMEM || frame.memoryExceeded) {
- return CallStatus::outOfMemory;
- }
- return contains_error(impl, "instruction_budget") ? CallStatus::instructionBudget
- : CallStatus::scriptError;
- }
- // Past three quarters of the arena a step cannot keep up, so a full collection runs; below
- // that the step is charged 64 KiB at a time.
- constexpr std::size_t kCollectionPressureBytes = (kArenaByteCapacity * 3U) / 4U;
- constexpr int kCollectionStepKilobytes = 64;
- /** Advances collection only after Lua has left a protected transaction. */
- void collect_after_transaction(Impl& impl) noexcept {
- if (impl.state != nullptr) {
- const int operation =
- impl.arena.used >= kCollectionPressureBytes ? LUA_GCCOLLECT : LUA_GCSTEP;
- const int amount = operation == LUA_GCSTEP ? kCollectionStepKilobytes : 0;
- static_cast<void>(lua_gc(impl.state, operation, amount));
- static_cast<void>(lua_gc(impl.state, LUA_GCSTOP, 0));
- ++impl.collections;
- }
- }
- [[nodiscard]] bool same_key(const StateKey& left, const StateKey& right) noexcept {
- return left.length == right.length && left.bytes == right.bytes;
- }
- [[nodiscard]] bool same_value(const VariableValue& left, const VariableValue& right) noexcept {
- return left.stringValue == right.stringValue && left.integerValue == right.integerValue
- && left.realValue == right.realValue && left.stringLength == right.stringLength
- && left.kind == right.kind && left.booleanValue == right.booleanValue;
- }
- [[nodiscard]] bool same_variable(const ScriptVariable& left, const ScriptVariable& right) noexcept {
- return same_key(left.key, right.key) && same_value(left.value, right.value);
- }
- [[nodiscard]] bool same_timer(const MissionTimer& left, const MissionTimer& right) noexcept {
- return same_key(left.key, right.key) && left.deadlineTick == right.deadlineTick
- && left.sequence == right.sequence;
- }
- /** True when the candidate differs from committed durable state in any field it may change. */
- [[nodiscard]] bool durable_delta(const Impl& impl, const Candidate& candidate) noexcept {
- if ((candidate.phaseChanged && candidate.phase != impl.phase)
- || candidate.variableCount != impl.variableCount || candidate.timerCount != impl.timerCount
- || candidate.nextTimerSequence != impl.nextTimerSequence
- || candidate.nextIntentKey != impl.nextIntentKey) {
- return true;
- }
- for (std::size_t index = 0; index < candidate.variableCount; ++index) {
- if (!same_variable(candidate.variables[index], impl.variables[index])) {
- return true;
- }
- }
- for (std::size_t index = 0; index < candidate.timerCount; ++index) {
- if (!same_timer(candidate.timers[index], impl.timers[index])) {
- return true;
- }
- }
- return false;
- }
- void initialize_candidate(const Impl& impl, Candidate& candidate) noexcept {
- candidate.phase = impl.phase;
- candidate.variables = impl.variables;
- candidate.timers = impl.timers;
- candidate.variableCount = impl.variableCount;
- candidate.timerCount = impl.timerCount;
- candidate.nextTimerSequence = impl.nextTimerSequence;
- candidate.nextIntentKey = impl.nextIntentKey;
- }
- /** Consumes only the exact active timer represented by an internal elapsed event. */
- [[nodiscard]] bool consume_elapsed_timer(Candidate& candidate, const host::Event& event) noexcept {
- if (event.kind != host::EventKind::timerElapsed) {
- return true;
- }
- for (std::size_t index = 0; index < candidate.timerCount; ++index) {
- const MissionTimer& timer = candidate.timers[index];
- if (timer.sequence != event.timerSequence || timer.deadlineTick != event.timerDeadlineTick
- || !same_key(timer.key, event.timerName)) {
- continue;
- }
- for (std::size_t move = index + 1; move < candidate.timerCount; ++move) {
- candidate.timers[move - 1] = candidate.timers[move];
- }
- --candidate.timerCount;
- candidate.timers[candidate.timerCount] = {};
- return true;
- }
- return false;
- }
- /** Publishes one callback candidate without allowing its revision to wrap. */
- [[nodiscard]] CallStatus commit_candidate(Impl& impl, Candidate& candidate) noexcept {
- const bool phaseChanged = candidate.phaseChanged && candidate.phase != impl.phase;
- const bool stateChanged = durable_delta(impl, candidate);
- if ((stateChanged || !candidate.intents.empty())
- && impl.stateRevision == (std::numeric_limits<std::uint64_t>::max)()) {
- detail::set_error(impl, "mission state revision is exhausted");
- ++impl.refusedCallbacks;
- impl.faulted = true;
- return CallStatus::scriptError;
- }
- if (impl.outboxRead != 0) {
- impl.outbox.erase(impl.outbox.begin(),
- impl.outbox.begin() + static_cast<std::ptrdiff_t>(impl.outboxRead));
- impl.outboxRead = 0;
- }
- if (candidate.intents.size() > impl.outbox.max_size() - impl.outbox.size()) {
- detail::set_error(impl, "mission outbox allocation failed");
- ++impl.refusedCallbacks;
- impl.faulted = true;
- return CallStatus::outOfMemory;
- }
- try {
- impl.outbox.reserve(impl.outbox.size() + candidate.intents.size());
- } catch (const std::bad_alloc&) {
- detail::set_error(impl, "mission outbox allocation failed");
- ++impl.refusedCallbacks;
- impl.faulted = true;
- return CallStatus::outOfMemory;
- }
- for (Intent& intent : candidate.intents) {
- impl.outbox.push_back(std::move(intent));
- }
- if (phaseChanged) {
- impl.phase = candidate.phase;
- }
- if (stateChanged) {
- impl.variables = candidate.variables;
- impl.timers = candidate.timers;
- impl.variableCount = candidate.variableCount;
- impl.timerCount = candidate.timerCount;
- impl.nextTimerSequence = candidate.nextTimerSequence;
- impl.nextIntentKey = candidate.nextIntentKey;
- }
- if (stateChanged || !candidate.intents.empty()) {
- ++impl.stateRevision;
- }
- impl.lastError = {};
- ++impl.committedCallbacks;
- return CallStatus::committed;
- }
- /** Runs one callback under the instruction budget, then commits its candidate or faults the vm. */
- [[nodiscard]] CallStatus invoke(Impl& impl,
- Handler handler,
- const host::Event* event,
- const host::ClientMessageSnapshot* clientMessage,
- std::uint64_t now) noexcept {
- if (!impl.active || impl.state == nullptr || impl.faulted) {
- return CallStatus::inactive;
- }
- CallFrame frame{};
- initialize_candidate(impl, frame.candidate);
- if (event != nullptr && !consume_elapsed_timer(frame.candidate, *event)) {
- detail::set_error(impl, "mission timer event no longer matches authoritative state");
- ++impl.refusedCallbacks;
- impl.faulted = true;
- return CallStatus::scriptError;
- }
- frame.handler = handler;
- frame.event = event;
- frame.clientMessage = clientMessage;
- frame.now = now;
- impl.frame = &frame;
- ++impl.callbacks;
- lua_sethook(impl.state, &instruction_hook, LUA_MASKCOUNT, kHookInterval);
- lua_pushcfunction(impl.state, &detail::protected_callback);
- const int result = lua_pcall(impl.state, 0, 1, 0);
- lua_sethook(impl.state, nullptr, 0, 0);
- impl.frame = nullptr;
- if (result != LUA_OK) {
- detail::set_error(
- impl, existing_error(impl.state, "mission callback failed with a non-string error"));
- lua_settop(impl.state, 0);
- collect_after_transaction(impl);
- ++impl.refusedCallbacks;
- impl.faulted = true;
- return failed_call_status(impl, frame, result);
- }
- const bool handled = lua_toboolean(impl.state, -1) != 0;
- lua_settop(impl.state, 0);
- collect_after_transaction(impl);
- if (!handled) {
- if (durable_delta(impl, frame.candidate)) {
- const CallStatus committed = commit_candidate(impl, frame.candidate);
- return committed == CallStatus::committed ? CallStatus::noHandler : committed;
- }
- return CallStatus::noHandler;
- }
- return commit_candidate(impl, frame.candidate);
- }
- } // namespace
- Vm::Vm() noexcept {
- // No arena block yet. An activity slot with no open program holds none.
- static_cast<void>(::new (storage_.data()) detail::Impl{});
- }
- Vm::~Vm() noexcept {
- detail::Impl& impl = detail::VmAccess::get(*this);
- detail::destroy_state(impl);
- impl.~Impl();
- }
- /** Replaces the bound view of a program that is already open and running. */
- bool rebind(Vm& vm, const ProgramIdentity& identity, const DefinitionApi& definitions) noexcept {
- Impl& impl = VmAccess::get(vm);
- if (impl.state == nullptr || !impl.active || impl.faulted) {
- return false;
- }
- impl.identity = identity;
- impl.definitions = definitions;
- return true;
- }
- /** Compiles and validates one build-bound text chunk in a fresh arena. */
- OpenStatus open(Vm& vm,
- const ProgramIdentity& identity,
- const DefinitionApi& definitions,
- std::span<const char> source) noexcept {
- Impl& impl = VmAccess::get(vm);
- detail::destroy_state(impl);
- impl.arenaBytesAfterClose = 0;
- if (source.empty() || source.size() > kSourceByteCapacity) {
- detail::set_error(impl, "mission source is empty or exceeds 128 KiB");
- impl.faulted = true;
- return OpenStatus::sourceTooLarge;
- }
- // The build id is the 7-byte "sha256:" prefix plus 64 hex digits, 71 characters total.
- constexpr std::string_view kSdkBuildIdPrefix = "sha256:";
- constexpr std::size_t kSdkBuildIdLength = kSdkBuildIdPrefix.size() + 64;
- if (identity.sdkBuildId.back() != '\0'
- || std::strlen(identity.sdkBuildId.data()) != kSdkBuildIdLength
- || std::string_view(identity.sdkBuildId.data(), kSdkBuildIdPrefix.size())
- != kSdkBuildIdPrefix
- || identity.activityId.back() != '\0' || identity.activityId.front() == '\0'
- || definitions.resolveSquadRow == nullptr || definitions.resolveSquadId == nullptr
- || definitions.resolveSceneRow == nullptr || definitions.resolveSceneId == nullptr
- || definitions.resolveSlotRow == nullptr || definitions.resolveSlotId == nullptr
- || definitions.resolveSenseSlot == nullptr
- || definitions.resolveActivityMessageRow == nullptr
- || definitions.resolveActivityMessageId == nullptr
- || definitions.resolveActivityMessageName == nullptr
- || definitions.resolveActivityMessageFieldRow == nullptr
- || definitions.resolveActivityMessageFieldIndex == nullptr
- || definitions.resolveActivityBinding == nullptr
- || definitions.activityBindingTagCount == nullptr
- || definitions.resolveActivityBindingTag == nullptr
- || definitions.activityBindingLocatorCount == nullptr
- || definitions.resolveActivityBindingLocator == nullptr || definitions.squadCount == nullptr
- || definitions.sceneCount == nullptr || definitions.slotCount == nullptr
- || definitions.activityMessageCount == nullptr) {
- detail::set_error(impl, "mission identity or SDK definition bridge is invalid");
- impl.faulted = true;
- return OpenStatus::invalidProgram;
- }
- impl.identity = identity;
- impl.definitions = definitions;
- if (!detail::arena_initialize(impl.arena)) {
- detail::set_error(impl, "mission arena block could not be taken from the heap");
- impl.faulted = true;
- return OpenStatus::outOfMemory;
- }
- impl.state = lua_newstate(&detail::arena_allocate, &impl.arena);
- if (impl.state == nullptr) {
- detail::set_error(impl, "Lua state exceeded its fixed arena");
- preserve_failure_and_close(impl);
- return OpenStatus::outOfMemory;
- }
- static_cast<void>(lua_gc(impl.state, LUA_GCSTOP, 0));
- if (!detail::initialize_sandbox(impl)) {
- const OpenStatus status =
- contains_error(impl, "memory") ? OpenStatus::outOfMemory : OpenStatus::runtimeError;
- preserve_failure_and_close(impl);
- return status;
- }
- const int compiled =
- luaL_loadbufferx(impl.state, source.data(), source.size(), identity.activityId.data(), "t");
- if (compiled != LUA_OK) {
- detail::set_error(
- impl, existing_error(impl.state, "mission compile failed with a non-string error"));
- const OpenStatus status =
- compiled == LUA_ERRMEM ? OpenStatus::outOfMemory : OpenStatus::compileError;
- preserve_failure_and_close(impl);
- return status;
- }
- CallFrame loadFrame{};
- loadFrame.remainingInstructions = kInitializationInstructionBudget;
- impl.frame = &loadFrame;
- lua_sethook(impl.state, &instruction_hook, LUA_MASKCOUNT, kHookInterval);
- const int executed = lua_pcall(impl.state, 0, 1, 0);
- lua_sethook(impl.state, nullptr, 0, 0);
- impl.frame = nullptr;
- if (executed != LUA_OK) {
- detail::set_error(
- impl,
- existing_error(impl.state, "mission initialization failed with a non-string error"));
- const OpenStatus status = executed == LUA_ERRMEM || loadFrame.memoryExceeded
- ? OpenStatus::outOfMemory
- : OpenStatus::runtimeError;
- preserve_failure_and_close(impl);
- return status;
- }
- lua_pushcfunction(impl.state, &capture_program);
- lua_insert(impl.state, -2);
- const int captured = lua_pcall(impl.state, 1, 0, 0);
- if (captured != LUA_OK) {
- detail::set_error(
- impl, existing_error(impl.state, "mission declaration failed with a non-string error"));
- const OpenStatus status =
- captured == LUA_ERRMEM ? OpenStatus::outOfMemory : OpenStatus::invalidProgram;
- preserve_failure_and_close(impl);
- return status;
- }
- lua_settop(impl.state, 0);
- impl.active = true;
- impl.faulted = false;
- return OpenStatus::ready;
- }
- /**
- * Publishes the current peer set.
- * It replaces the previous set outright, so a peer that left is gone on the next read.
- */
- void publish_peers(Vm& vm, std::span<const PeerSession> peers) noexcept {
- Impl& impl = VmAccess::get(vm);
- const std::size_t count = (std::min)(peers.size(), impl.peers.size());
- impl.peers = {};
- std::copy_n(peers.begin(), count, impl.peers.begin());
- impl.peerCount = static_cast<std::uint8_t>(count);
- }
- /** Restores state and typed actions only before any callback or action transaction. */
- bool restore_state(Vm& vm,
- std::uint32_t phase,
- std::uint64_t revision,
- std::span<const ScriptVariable> variables,
- std::span<const MissionTimer> timers,
- std::uint64_t nextTimerSequence,
- std::uint64_t nextIntentKey,
- std::span<const Intent> pendingIntents) noexcept {
- Impl& impl = VmAccess::get(vm);
- if (!impl.active || impl.state == nullptr || impl.faulted || impl.frame != nullptr
- || impl.callbacks != 0 || !impl.outbox.empty() || variables.size() > impl.variables.size()
- || timers.size() > impl.timers.size()) {
- return false;
- }
- impl.phase = phase;
- impl.stateRevision = revision;
- std::copy(variables.begin(), variables.end(), impl.variables.begin());
- std::fill(impl.variables.begin() + static_cast<std::ptrdiff_t>(variables.size()),
- impl.variables.end(),
- ScriptVariable{});
- std::copy(timers.begin(), timers.end(), impl.timers.begin());
- std::fill(impl.timers.begin() + static_cast<std::ptrdiff_t>(timers.size()),
- impl.timers.end(),
- MissionTimer{});
- impl.variableCount = variables.size();
- impl.timerCount = timers.size();
- impl.nextTimerSequence = nextTimerSequence;
- impl.nextIntentKey = nextIntentKey;
- try {
- impl.outbox.assign(pendingIntents.begin(), pendingIntents.end());
- } catch (const std::bad_alloc&) {
- return false;
- }
- impl.outboxRead = 0;
- return true;
- }
- /** Restores state when no typed actions are pending. */
- bool restore_state(Vm& vm, std::uint32_t phase, std::uint64_t revision) noexcept {
- return restore_state(vm,
- phase,
- revision,
- {},
- {},
- state::activity::mission::kFirstTimerSequence,
- state::activity::mission::kFirstIntentKey,
- {});
- }
- /** Runs start and reserves one revision for the durable started transition. */
- CallStatus start(Vm& vm, std::uint64_t now) noexcept {
- Impl& impl = VmAccess::get(vm);
- const std::uint64_t before = impl.stateRevision;
- const CallStatus status = invoke(impl, Handler::start, nullptr, nullptr, now);
- if (status != CallStatus::committed && status != CallStatus::noHandler) {
- return status;
- }
- if (impl.stateRevision != before) {
- return status;
- }
- if (impl.stateRevision == (std::numeric_limits<std::uint64_t>::max)()) {
- detail::set_error(impl, "mission start revision is exhausted");
- impl.faulted = true;
- return CallStatus::scriptError;
- }
- ++impl.stateRevision;
- return status;
- }
- /**
- * Runs load for a program whose durable state was restored.
- * It reserves no revision. A restore is not a state transition, so the revision is already correct.
- */
- CallStatus load(Vm& vm, std::uint64_t now) noexcept {
- Impl& impl = VmAccess::get(vm);
- // A program that declares no on_load must cost a reattach nothing at all.
- if (impl.loadReference == LUA_NOREF && impl.active && impl.state != nullptr && !impl.faulted) {
- return CallStatus::noHandler;
- }
- return invoke(impl, Handler::load, nullptr, nullptr, now);
- }
- CallStatus dispatch(Vm& vm,
- const host::Event& event,
- const host::ClientMessageSnapshot* clientMessage,
- std::uint64_t now) noexcept {
- return invoke(VmAccess::get(vm), Handler::event, &event, clientMessage, now);
- }
- bool handles_event(const Vm& vm, host::EventKind kind) noexcept {
- const Impl& impl = VmAccess::get(vm);
- return impl.active && !impl.faulted && detail::event_reference(impl, kind) != LUA_NOREF;
- }
- bool initial_state_region(const Vm& vm, std::int32_t& output) noexcept {
- const Impl& impl = VmAccess::get(vm);
- output = -1;
- if (!impl.active || impl.faulted || !impl.hasInitialState || impl.initialStateRegion < 0) {
- return false;
- }
- output = impl.initialStateRegion;
- return true;
- }
- /** Copies the next unread outbox action without consuming it; false when none is pending. */
- bool pending_intent(const Vm& vm, Intent& output) noexcept {
- const Impl& impl = VmAccess::get(vm);
- output = {};
- if (!impl.active || impl.outboxRead >= impl.outbox.size()) {
- return false;
- }
- try {
- output = impl.outbox[impl.outboxRead];
- } catch (const std::bad_alloc&) {
- output = {};
- return false;
- }
- return true;
- }
- /** Copies the complete ordered outbox without consuming an action. */
- bool snapshot_intents(const Vm& vm, std::vector<Intent>& output) noexcept {
- const Impl& impl = VmAccess::get(vm);
- output.clear();
- if (!impl.active) {
- return false;
- }
- try {
- output.assign(impl.outbox.begin() + static_cast<std::ptrdiff_t>(impl.outboxRead),
- impl.outbox.end());
- } catch (const std::bad_alloc&) {
- output.clear();
- return false;
- }
- return true;
- }
- /** Copies complete durable script state without changing its revision. */
- bool snapshot_durable_state(const Vm& vm,
- std::span<ScriptVariable> variables,
- std::size_t& variableCount,
- std::span<MissionTimer> timers,
- std::size_t& timerCount,
- std::uint64_t& nextTimerSequence,
- std::uint64_t& nextIntentKey) noexcept {
- const Impl& impl = VmAccess::get(vm);
- variableCount = 0;
- timerCount = 0;
- nextTimerSequence = state::activity::mission::kAbsentTimerSequence;
- nextIntentKey = state::activity::mission::kAbsentIntentKey;
- if (!impl.active || variables.size() < impl.variableCount || timers.size() < impl.timerCount) {
- return false;
- }
- std::copy_n(impl.variables.begin(), impl.variableCount, variables.begin());
- std::copy_n(impl.timers.begin(), impl.timerCount, timers.begin());
- variableCount = impl.variableCount;
- timerCount = impl.timerCount;
- nextTimerSequence = impl.nextTimerSequence;
- nextIntentKey = impl.nextIntentKey;
- return true;
- }
- /** Drops the oldest unread action, and empties the outbox once every action has been read. */
- void consume_intent(Vm& vm) noexcept {
- Impl& impl = VmAccess::get(vm);
- if (impl.outboxRead >= impl.outbox.size()) {
- return;
- }
- impl.outbox[impl.outboxRead] = {};
- ++impl.outboxRead;
- if (impl.outboxRead == impl.outbox.size()) {
- impl.outbox.clear();
- impl.outboxRead = 0;
- }
- }
- void discard_intents(Vm& vm) noexcept {
- Impl& impl = VmAccess::get(vm);
- std::vector<Intent>{}.swap(impl.outbox);
- impl.outboxRead = 0;
- }
- void fault(Vm& vm, std::string_view reason) noexcept {
- Impl& impl = VmAccess::get(vm);
- detail::set_error(impl, reason);
- impl.faulted = true;
- }
- /** Copies the counters, durable sizes and arena figures a caller reads for diagnostics. */
- void snapshot(const Vm& vm, Snapshot& output) noexcept {
- const Impl& impl = VmAccess::get(vm);
- output = {};
- output.lastError = impl.lastError;
- output.stateRevision = impl.stateRevision;
- output.callbacks = impl.callbacks;
- output.committedCallbacks = impl.committedCallbacks;
- output.refusedCallbacks = impl.refusedCallbacks;
- output.collections = impl.collections;
- output.phase = impl.phase;
- output.pendingIntents = impl.outbox.size() - impl.outboxRead;
- output.variableCount = impl.variableCount;
- output.timerCount = impl.timerCount;
- output.nextTimerSequence = impl.nextTimerSequence;
- output.nextIntentKey = impl.nextIntentKey;
- output.initialStateRegion = impl.initialStateRegion;
- output.arenaBytes = impl.arena.used;
- output.arenaHighWater = impl.arena.highWater;
- output.arenaBytesAfterClose = impl.arenaBytesAfterClose;
- output.active = impl.active;
- output.faulted = impl.faulted;
- output.hasInitialState = impl.hasInitialState;
- }
- void close(Vm& vm) noexcept {
- detail::destroy_state(VmAccess::get(vm));
- }
- /** Stable lowercase log token for one open status. */
- const char* status_name(OpenStatus status) noexcept {
- switch (status) {
- case OpenStatus::ready:
- return "ready";
- case OpenStatus::sourceTooLarge:
- return "source_too_large";
- case OpenStatus::outOfMemory:
- return "out_of_memory";
- case OpenStatus::compileError:
- return "compile_error";
- case OpenStatus::runtimeError:
- return "runtime_error";
- case OpenStatus::invalidProgram:
- return "invalid_program";
- }
- return "unknown";
- }
- /** Stable lowercase log token for one call status. */
- const char* status_name(CallStatus status) noexcept {
- switch (status) {
- case CallStatus::committed:
- return "committed";
- case CallStatus::noHandler:
- return "no_handler";
- case CallStatus::inactive:
- return "inactive";
- case CallStatus::scriptError:
- return "script_error";
- case CallStatus::instructionBudget:
- return "instruction_budget";
- case CallStatus::outOfMemory:
- return "out_of_memory";
- }
- return "unknown";
- }
- } // namespace sunrise::server::activity::mission::lua_vm
|