DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,308 @@
|
||||
#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 <thread>
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user