#pragma once // Client-side execution coordinator. Not installed into 90CN.dll. // NativeDriver is deliberately an interface: no current native implementation // has passed creation/pause/retirement verification. Offline tests inject a fake. #include "90CN_companion_native_contract.h" #include namespace dnf90_companion { struct ActorHandle { std::uint32_t address32 = 0, object_key = 0, generation = 0; }; inline bool SameActor(const ActorHandle& a, const ActorHandle& b) { return a.address32 == b.address32 && a.object_key == b.object_key && a.generation == b.generation; } inline bool ValidActor(const ActorHandle& h) { return h.address32 && h.object_key && h.object_key < 65535 && h.generation; } struct ClientPlan { SpawnIdentity identity{}; std::uint16_t source_character = 0; RequestValues values{}; }; enum class ActorPresence { Unavailable, Present, Gone, IdentityMismatch }; struct ActorObservation { ActorPresence presence = ActorPresence::Unavailable; bool registered = false, paused = false, alive = false; std::uint64_t room_generation = 0; }; enum class Submission { NotSubmitted, Pending }; // Every method executes on the verified game thread and MUST NOT wait for IO. // Implementations resolve handles afresh before touching native memory, and // validate provenance against the authenticated server/session/run and profile. // BeginPaused must guarantee no AI attack before the creation observer; a later // best-effort pause is insufficient. Pending also covers uncertain submission. // Gone means proven engine retirement, not lookup error or missing room manager. class NativeDriver { public: virtual ~NativeDriver() = default; virtual bool VerifiedFor(const ClientPlan&) const noexcept = 0; virtual bool RoomReady(const SpawnIdentity&, std::uint64_t) const noexcept = 0; virtual Submission BeginPaused(const ClientPlan&, std::uint64_t) noexcept = 0; virtual bool BelongsToRequest(const ClientPlan&, const ActorHandle&) const noexcept = 0; virtual ActorObservation Inspect(const SpawnIdentity&, const ActorHandle&) const noexcept = 0; virtual bool SetPaused(const SpawnIdentity&, const ActorHandle&, bool) noexcept = 0; virtual bool RequestRetirement(const SpawnIdentity&, const ActorHandle&) noexcept = 0; // true means the pending operation AND its callback queue are drained, // with no undisclosed live object. false does not authorize another spawn. virtual bool CancelAndDrain(const SpawnIdentity&) noexcept = 0; virtual bool CreationDrained(const SpawnIdentity&) const noexcept = 0; }; enum class ClientPhase { Idle, Prepared, Creating, AwaitOwner, Bound, Active, Transitioning, Dead, Retiring, CleanupBlocked, Closed }; enum class ClientResult { Ok, WrongThread, InvalidPlan, StaleRun, Busy, Unsupported, RoomNotReady, WrongPhase, WrongActor, ObservationUnavailable, AINotPaused, NativeFailure, Timeout, CleanupPending, ClockRegression }; struct ClientStatus { ClientPhase phase = ClientPhase::Idle; ClientResult reason = ClientResult::Ok; SpawnIdentity identity{}; ActorHandle actor{}; std::uint64_t room_generation = 0; bool owner_bound = false, creation_pending = false; }; struct ClientTimeouts { std::uint64_t preparation_ms = 10000, creation_ms = 5000, binding_ms = 5000, transition_ms = 10000, retirement_ms = 5000; }; // All mutating methods, including observations, are game-thread only. The // future channel must queue immutable messages there, never call this object // from its reader thread. The coordinator creates no thread, hook or timer. class ClientRuntime { public: ClientRuntime(NativeDriver& driver, std::uint64_t session_generation, std::uint16_t owner_character, std::uint16_t player_object_key, ClientTimeouts timeouts = {}) : driver_(driver), thread_(std::this_thread::get_id()), session_(session_generation), owner_(owner_character), player_key_(player_object_key), timeouts_(timeouts) {} ClientRuntime(const ClientRuntime&) = delete; ClientRuntime& operator=(const ClientRuntime&) = delete; bool TryStatus(ClientStatus& out) const { if (std::this_thread::get_id() != thread_) return false; out = state_; return true; } ClientResult Prepare(const ClientPlan& plan, std::uint64_t now) { auto guard = TimeGuard(now); if (guard != ClientResult::Ok) return guard; if (state_.phase != ClientPhase::Idle && state_.phase != ClientPhase::Closed) return ClientResult::Busy; const auto& id = plan.identity; NativeApcRequest32 request{}; if (!session_ || !owner_ || owner_ == 65535 || !player_key_ || player_key_ == 65535 || id.session_generation != session_ || id.owner_character != owner_ || !id.run_token || !id.request_sequence || !plan.source_character || plan.source_character == owner_ || plan.source_character == 65535 || BuildRequest(plan.values, &request) != RequestError::None) return ClientResult::InvalidPlan; if (id.request_sequence <= last_request_) return ClientResult::StaleRun; if (!driver_.VerifiedFor(plan)) return ClientResult::Unsupported; plan_ = plan; state_ = {}; state_.identity = id; state_.phase = ClientPhase::Prepared; since_ = now; last_request_ = id.request_sequence; retire_sent_ = cancel_sent_ = false; return ClientResult::Ok; } ClientResult RoomReady(const SpawnIdentity& id, std::uint64_t room, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (!room) return ClientResult::RoomNotReady; if (room == state_.room_generation && (state_.phase == ClientPhase::Creating || state_.phase == ClientPhase::AwaitOwner || state_.phase == ClientPhase::Bound || state_.phase == ClientPhase::Active)) return ClientResult::Ok; // repeated load callback never resubmits if (room <= state_.room_generation || !driver_.RoomReady(id, room)) return ClientResult::RoomNotReady; if (state_.phase == ClientPhase::Transitioning) { const auto o = driver_.Inspect(id, state_.actor); if (o.presence != ActorPresence::Present) return ClientResult::ObservationUnavailable; if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; } if (!o.registered || !o.paused || o.room_generation != room || !state_.owner_bound) return ClientResult::RoomNotReady; state_.room_generation = room; return Resume(now); } if (state_.phase != ClientPhase::Prepared) return ClientResult::WrongPhase; if (!driver_.VerifiedFor(plan_)) return Close(ClientResult::Unsupported); state_.room_generation = room; state_.phase = ClientPhase::Creating; state_.creation_pending = true; since_ = now; // State is installed before submitting. The driver must enqueue its // observer callbacks until this call has returned (no re-entrancy). if (driver_.BeginPaused(plan_, room) == Submission::NotSubmitted) { state_.creation_pending = false; return Close(ClientResult::NativeFailure); } return ClientResult::Ok; } ClientResult NativeCreated(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (!ValidActor(h) || h.object_key == player_key_ || !driver_.BelongsToRequest(plan_, h)) return ClientResult::WrongActor; if (ValidActor(state_.actor)) { if (!SameActor(state_.actor, h)) return Block(ClientResult::WrongActor); // A duplicate cannot reset a terminal/dead/active lifecycle. return ClientResult::Ok; } if (!state_.creation_pending || (state_.phase != ClientPhase::Creating && state_.phase != ClientPhase::Retiring && state_.phase != ClientPhase::CleanupBlocked)) return ClientResult::WrongPhase; const auto o = driver_.Inspect(id, h); if (o.presence != ActorPresence::Present || !o.registered) return ClientResult::ObservationUnavailable; state_.actor = h; state_.creation_pending = false; if (state_.phase == ClientPhase::Retiring || state_.phase == ClientPhase::CleanupBlocked) { state_.phase = ClientPhase::Retiring; since_ = now; return SendRetirement(); // late result after cancel: never bind or activate } if (!o.paused || !o.alive || o.room_generation != state_.room_generation) { return StartRetirement(now, ClientResult::AINotPaused); } state_.phase = ClientPhase::AwaitOwner; since_ = now; return ClientResult::Ok; } ClientResult OwnerBound(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (!SameActor(h, state_.actor) || !ValidActor(h)) return ClientResult::WrongActor; if ((state_.phase == ClientPhase::Bound || state_.phase == ClientPhase::Active) && state_.owner_bound) return ClientResult::Ok; if (state_.phase != ClientPhase::AwaitOwner) return ClientResult::WrongPhase; const auto o = driver_.Inspect(id, h); if (o.presence != ActorPresence::Present || !o.registered) return ClientResult::ObservationUnavailable; if (!o.paused || !o.alive || o.room_generation != state_.room_generation) return StartRetirement(now, ClientResult::AINotPaused); state_.owner_bound = true; state_.phase = ClientPhase::Bound; return ClientResult::Ok; } ClientResult Activate(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (!SameActor(h, state_.actor) || !ValidActor(h)) return ClientResult::WrongActor; if (state_.phase == ClientPhase::Active) return ClientResult::Ok; if (state_.phase != ClientPhase::Bound || !state_.owner_bound) return ClientResult::WrongPhase; return Resume(now); } ClientResult RoomLeaving(const SpawnIdentity& id, std::uint64_t room, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (room != state_.room_generation) return ClientResult::RoomNotReady; if (state_.phase == ClientPhase::Transitioning || state_.phase == ClientPhase::Dead) return ClientResult::Ok; if (state_.phase != ClientPhase::Active) return ClientResult::WrongPhase; auto o = driver_.Inspect(id, state_.actor); if (o.presence != ActorPresence::Present) return Block(ClientResult::ObservationUnavailable); if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; } if (!driver_.SetPaused(id, state_.actor, true)) return StartRetirement(now, ClientResult::NativeFailure); o = driver_.Inspect(id, state_.actor); if (o.presence != ActorPresence::Present || !o.paused) return StartRetirement(now, ClientResult::AINotPaused); state_.phase = ClientPhase::Transitioning; since_ = now; return ClientResult::Ok; } ClientResult Retire(const SpawnIdentity& id, std::uint64_t now) { auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard; if (state_.phase == ClientPhase::Closed) return ClientResult::Ok; if (state_.phase == ClientPhase::Retiring || state_.phase == ClientPhase::CleanupBlocked) return ClientResult::CleanupPending; return StartRetirement(now, ClientResult::Ok); } ClientResult Poll(std::uint64_t now) { auto guard = TimeGuard(now); if (guard != ClientResult::Ok) return guard; const auto phase = state_.phase; if (phase == ClientPhase::Idle || phase == ClientPhase::Closed) return ClientResult::Ok; if (phase == ClientPhase::Retiring || phase == ClientPhase::CleanupBlocked) { if (state_.creation_pending && driver_.CreationDrained(state_.identity)) state_.creation_pending = false; if (!ValidActor(state_.actor)) { if (!state_.creation_pending) return Close(state_.reason); } else { const auto o = driver_.Inspect(state_.identity, state_.actor); if (o.presence == ActorPresence::Gone && !state_.creation_pending) return Close(state_.reason); if (o.presence == ActorPresence::IdentityMismatch) return Block(ClientResult::WrongActor); } if (Elapsed(now, timeouts_.retirement_ms)) return Block(ClientResult::Timeout); return ClientResult::CleanupPending; } if (phase == ClientPhase::Active || phase == ClientPhase::Transitioning || phase == ClientPhase::Dead) { const auto o = driver_.Inspect(state_.identity, state_.actor); if (o.presence == ActorPresence::IdentityMismatch) return Block(ClientResult::WrongActor); if (o.presence == ActorPresence::Gone) return Close(ClientResult::NativeFailure); if (o.presence == ActorPresence::Unavailable) return ClientResult::ObservationUnavailable; if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; } } std::uint64_t timeout = 0; if (phase == ClientPhase::Prepared) timeout = timeouts_.preparation_ms; if (phase == ClientPhase::Creating) timeout = timeouts_.creation_ms; if (phase == ClientPhase::AwaitOwner || phase == ClientPhase::Bound) timeout = timeouts_.binding_ms; if (phase == ClientPhase::Transitioning) timeout = timeouts_.transition_ms; if (timeout && Elapsed(now, timeout)) return StartRetirement(now, ClientResult::Timeout); return ClientResult::Ok; } private: ClientResult TimeGuard(std::uint64_t now) { if (std::this_thread::get_id() != thread_) return ClientResult::WrongThread; if (now < last_now_) return ClientResult::ClockRegression; last_now_ = now; return ClientResult::Ok; } ClientResult Guard(const SpawnIdentity& id, std::uint64_t now) { auto result = TimeGuard(now); if (result != ClientResult::Ok) return result; return SameSpawn(id, state_.identity) ? ClientResult::Ok : ClientResult::StaleRun; } bool Elapsed(std::uint64_t now, std::uint64_t limit) const { return now - since_ >= limit; } ClientResult Block(ClientResult reason) { state_.phase = ClientPhase::CleanupBlocked; state_.reason = reason; return reason; } ClientResult Close(ClientResult reason) { state_.phase = ClientPhase::Closed; state_.reason = reason; state_.owner_bound = false; state_.creation_pending = false; return reason; } ClientResult SendRetirement() { if (retire_sent_) return ClientResult::CleanupPending; const auto o = driver_.Inspect(state_.identity, state_.actor); if (o.presence == ActorPresence::Gone) return Close(state_.reason); if (o.presence != ActorPresence::Present) return Block(ClientResult::ObservationUnavailable); retire_sent_ = true; // no uncertain native operation is blindly retried if (!driver_.RequestRetirement(state_.identity, state_.actor)) return Block(ClientResult::NativeFailure); return ClientResult::CleanupPending; } ClientResult StartRetirement(std::uint64_t now, ClientResult reason) { state_.phase = ClientPhase::Retiring; state_.reason = reason; since_ = now; if (ValidActor(state_.actor)) return SendRetirement(); if (state_.creation_pending && !cancel_sent_) { cancel_sent_ = true; if (driver_.CancelAndDrain(state_.identity)) state_.creation_pending = false; } if (!state_.creation_pending) return Close(reason); return ClientResult::CleanupPending; } ClientResult Resume(std::uint64_t now) { const auto o = driver_.Inspect(state_.identity, state_.actor); if (o.presence != ActorPresence::Present || !o.registered || !o.paused || !o.alive || o.room_generation != state_.room_generation || !state_.owner_bound) return StartRetirement(now, ClientResult::AINotPaused); if (!driver_.SetPaused(state_.identity, state_.actor, false)) return StartRetirement(now, ClientResult::NativeFailure); const auto after = driver_.Inspect(state_.identity, state_.actor); if (after.presence != ActorPresence::Present || !after.registered || after.paused || !after.alive || after.room_generation != state_.room_generation) return StartRetirement(now, ClientResult::NativeFailure); state_.phase = ClientPhase::Active; since_ = now; return ClientResult::Ok; } NativeDriver& driver_; const std::thread::id thread_; const std::uint64_t session_; const std::uint16_t owner_, player_key_; const ClientTimeouts timeouts_; ClientPlan plan_{}; ClientStatus state_{}; std::uint64_t since_ = 0, last_now_ = 0, last_request_ = 0; bool retire_sent_ = false, cancel_sent_ = false; }; } // namespace dnf90_companion