#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 #include #include #include #include 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::is_iec559, "native float representation"); static_assert(std::is_trivially_copyable::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(values.x); result.y = static_cast(values.y); result.z = static_cast(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