服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
190 lines
8.7 KiB
C++
190 lines
8.7 KiB
C++
#pragma once
|
|
// Offline-tested 32-bit layouts from current 90CN EXE 8a7af614... .
|
|
// NOT a network packet builder, callable native adapter, or deployment switch.
|
|
// See docs/evidence/companion-v1-native-create-20260923.md.
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <type_traits>
|
|
|
|
namespace dnf90_companion {
|
|
constexpr std::size_t kNativeRequestSize = 0x54;
|
|
constexpr std::size_t kNativeTailSize = 0x29;
|
|
constexpr std::uint32_t kAllocateNativeObjectKey = 0xffff;
|
|
constexpr std::uint32_t kProtocolVersion = 1;
|
|
|
|
// Keep target addresses 32-bit even when tests run in a 64-bit host process.
|
|
// Unresolved fields are named by offset, not assigned speculative semantics.
|
|
#pragma pack(push, 4)
|
|
struct NativeApcRequest32 {
|
|
std::uint32_t template_id; // 00
|
|
std::uint32_t source_value04; // 04: owner vtable+370 result
|
|
std::uint32_t level; // 08: narrowed to u8 by serializer
|
|
std::uint32_t x, y, z, direction; // 0c..18
|
|
std::uint32_t mode1c; // 1c: mode 2 rejected by sender
|
|
std::uint32_t extra_address32, extra_size; // 20..24
|
|
std::uint8_t flag28, padding29[3];
|
|
std::uint32_t object_key; // 2c: ffff asks native allocator
|
|
std::uint8_t flag30, flag31, padding32[2];
|
|
std::uint32_t tail_address32; // 34: optional 29-byte native tail
|
|
std::uint8_t flag38, padding39[3];
|
|
std::int32_t auxiliary3c;
|
|
std::uint8_t flag40, padding41[3];
|
|
std::uint32_t byte_list_address32; // 44: native container, NOT byte[]
|
|
std::uint8_t flag48, padding49[3];
|
|
std::uint32_t auxiliary4c;
|
|
std::uint8_t flag50, padding51[3];
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
// Native initializer 037E6B30 has unaligned fields. Do not use a host ABI
|
|
// structure without packing, or memcpy these bytes into an upper game packet.
|
|
#pragma pack(push, 1)
|
|
struct NativeSummonTail32 {
|
|
std::uint8_t flag00, flag01, flag02;
|
|
std::uint32_t duration_value; // 03: item description's fourth int
|
|
std::uint16_t owner07, owner09;
|
|
std::uint8_t flag0b, reserved0c;
|
|
float rate0d;
|
|
std::uint8_t flag11;
|
|
std::uint16_t value12;
|
|
std::uint32_t value14, value18, value1c, value20;
|
|
std::uint8_t flag24;
|
|
std::int32_t value25;
|
|
};
|
|
#pragma pack(pop)
|
|
|
|
static_assert(sizeof(NativeApcRequest32) == kNativeRequestSize, "request ABI");
|
|
static_assert(alignof(NativeApcRequest32) == 4, "request alignment");
|
|
static_assert(offsetof(NativeApcRequest32, source_value04) == 0x04, "field04");
|
|
static_assert(offsetof(NativeApcRequest32, mode1c) == 0x1c, "field1c");
|
|
static_assert(offsetof(NativeApcRequest32, flag28) == 0x28, "field28");
|
|
static_assert(offsetof(NativeApcRequest32, object_key) == 0x2c, "object key");
|
|
static_assert(offsetof(NativeApcRequest32, flag31) == 0x31, "field31");
|
|
static_assert(offsetof(NativeApcRequest32, tail_address32) == 0x34, "tail pointer");
|
|
static_assert(offsetof(NativeApcRequest32, auxiliary3c) == 0x3c, "field3c");
|
|
static_assert(offsetof(NativeApcRequest32, byte_list_address32) == 0x44, "field44");
|
|
static_assert(offsetof(NativeApcRequest32, auxiliary4c) == 0x4c, "field4c");
|
|
static_assert(offsetof(NativeApcRequest32, flag50) == 0x50, "field50");
|
|
static_assert(sizeof(NativeSummonTail32) == kNativeTailSize, "tail ABI");
|
|
static_assert(offsetof(NativeSummonTail32, duration_value) == 3, "tail duration");
|
|
static_assert(offsetof(NativeSummonTail32, rate0d) == 0x0d, "tail rate");
|
|
static_assert(offsetof(NativeSummonTail32, value25) == 0x25, "tail ending");
|
|
static_assert(sizeof(float) == 4 && std::numeric_limits<float>::is_iec559,
|
|
"native float representation");
|
|
static_assert(std::is_trivially_copyable<NativeApcRequest32>::value, "request POD");
|
|
|
|
inline NativeApcRequest32 DefaultRequest() {
|
|
NativeApcRequest32 value;
|
|
// Native padding is not fully initialized; zero it to avoid leaked stack data.
|
|
std::memset(&value, 0, sizeof(value));
|
|
value.direction = 4;
|
|
value.object_key = kAllocateNativeObjectKey;
|
|
value.auxiliary3c = -1;
|
|
value.auxiliary4c = 5;
|
|
return value;
|
|
}
|
|
inline NativeSummonTail32 DefaultTail() {
|
|
NativeSummonTail32 value;
|
|
std::memset(&value, 0, sizeof(value));
|
|
value.owner07 = value.owner09 = 0xffff;
|
|
value.flag0b = 1;
|
|
value.rate0d = 1.0f; // actual constant at preferred VA 04B99848
|
|
value.value25 = -1;
|
|
return value;
|
|
}
|
|
|
|
struct RequestValues {
|
|
std::uint32_t template_id;
|
|
std::uint32_t source_value04;
|
|
std::uint32_t level;
|
|
std::int64_t x, y, z;
|
|
std::uint32_t direction;
|
|
};
|
|
enum class RequestError {
|
|
None, MissingOutput, TemplateRange, SourceValueRange, LevelRange,
|
|
CoordinateRange, DirectionRange
|
|
};
|
|
inline bool FitsCoordinate(std::int64_t value) { return value >= 0 && value <= 65535; }
|
|
inline RequestError BuildRequest(const RequestValues& values, NativeApcRequest32* out) {
|
|
if (!out) return RequestError::MissingOutput;
|
|
// Factory 02DE5CA0 supports positive ids <= 80000, but catalog existence
|
|
// and job/profile compatibility MUST still be proved separately.
|
|
if (values.template_id == 0 || values.template_id > 80000)
|
|
return RequestError::TemplateRange;
|
|
if (values.source_value04 > 65535) return RequestError::SourceValueRange;
|
|
if (values.level == 0 || values.level > 255) return RequestError::LevelRange;
|
|
if (!FitsCoordinate(values.x) || !FitsCoordinate(values.y) || !FitsCoordinate(values.z))
|
|
return RequestError::CoordinateRange;
|
|
// Native initializer uses 4; the item route uses the actor facing result.
|
|
// Only the scalar width is proved here, not a complete direction enum.
|
|
if (values.direction > 255) return RequestError::DirectionRange;
|
|
auto result = DefaultRequest();
|
|
result.template_id = values.template_id;
|
|
result.source_value04 = values.source_value04;
|
|
result.level = values.level;
|
|
result.x = static_cast<std::uint32_t>(values.x);
|
|
result.y = static_cast<std::uint32_t>(values.y);
|
|
result.z = static_cast<std::uint32_t>(values.z);
|
|
result.direction = values.direction;
|
|
*out = result; // errors leave output unchanged; used on the game thread
|
|
return RequestError::None;
|
|
}
|
|
|
|
// Pure receipt matcher for the future game-thread adapter. A sender returning
|
|
// does not prove creation. The actual receiver must independently observe
|
|
// registration, per-instance state and the engine's safe AI pause boundary.
|
|
struct SpawnIdentity {
|
|
std::uint64_t session_generation, run_token, request_sequence;
|
|
std::uint16_t owner_character;
|
|
};
|
|
inline bool SameSpawn(const SpawnIdentity& a, const SpawnIdentity& b) {
|
|
return a.session_generation == b.session_generation && a.run_token == b.run_token &&
|
|
a.request_sequence == b.request_sequence && a.owner_character == b.owner_character;
|
|
}
|
|
struct NativeReceipt {
|
|
SpawnIdentity identity;
|
|
std::uint32_t template_id;
|
|
std::uint32_t actor_address32;
|
|
std::uint32_t object_key;
|
|
std::uint32_t actor_generation;
|
|
bool registered;
|
|
bool inactive_confirmed;
|
|
};
|
|
enum class ReceiptError {
|
|
None, StaleSpawn, InvalidIdentity, WrongTemplate, InvalidActor,
|
|
InvalidKey, KeyCollision, NotRegistered, AINotPaused, ConflictingDuplicate
|
|
};
|
|
class PendingSpawn {
|
|
public:
|
|
PendingSpawn(SpawnIdentity identity, std::uint32_t template_id)
|
|
: identity_(identity), template_id_(template_id) {}
|
|
ReceiptError Observe(const NativeReceipt& receipt, bool key_collision) {
|
|
if (!SameSpawn(identity_, receipt.identity)) return ReceiptError::StaleSpawn;
|
|
if (!identity_.session_generation || !identity_.run_token ||
|
|
!identity_.request_sequence || !identity_.owner_character ||
|
|
!template_id_ || template_id_ > 80000) return ReceiptError::InvalidIdentity;
|
|
if (receipt.template_id != template_id_) return ReceiptError::WrongTemplate;
|
|
if (!receipt.actor_address32 || !receipt.actor_generation) return ReceiptError::InvalidActor;
|
|
if (!receipt.object_key || receipt.object_key >= 65535) return ReceiptError::InvalidKey;
|
|
if (key_collision) return ReceiptError::KeyCollision;
|
|
if (!receipt.registered) return ReceiptError::NotRegistered;
|
|
if (!receipt.inactive_confirmed) return ReceiptError::AINotPaused;
|
|
if (has_receipt_ && (receipt_.actor_address32 != receipt.actor_address32 ||
|
|
receipt_.object_key != receipt.object_key ||
|
|
receipt_.actor_generation != receipt.actor_generation))
|
|
return ReceiptError::ConflictingDuplicate;
|
|
receipt_ = receipt;
|
|
has_receipt_ = true;
|
|
return ReceiptError::None;
|
|
}
|
|
bool CanBindOwner() const { return has_receipt_; }
|
|
private:
|
|
SpawnIdentity identity_;
|
|
std::uint32_t template_id_;
|
|
bool has_receipt_ = false;
|
|
NativeReceipt receipt_{};
|
|
};
|
|
} // namespace dnf90_companion
|