#pragma once #include #include #include namespace dnf90_companion_projection { constexpr std::uint16_t kPrivateOpcode = 0x7F41; constexpr std::uint32_t kMagic = 0x314A5250u; // "PRJ1" little-endian constexpr std::uint8_t kVersion = 3; constexpr std::uint8_t kFlagPresent = 0x01; constexpr std::uint8_t kFlagProgression = 0x02; constexpr std::uint8_t kFlagAiPlan = 0x04; constexpr std::size_t kPacketHeaderSize = 16; // network router/class0 dispatch packet constexpr std::size_t kBodyHeaderSize = 72; constexpr std::size_t kMode0ObjectKeyOffset = 0x4C; constexpr std::size_t kMode1ObjectKeyOffset = 0x15; constexpr std::size_t kMaxProjectionBody = 32768; constexpr std::size_t kMaxDynamicAicBytes = 2048; constexpr std::size_t kMaxAicNameUnits = 32; constexpr std::size_t kMaxAicEquipment = 32; constexpr std::size_t kMaxAicSkills = 128; constexpr std::size_t kMaxAicQuickSkills = 16; constexpr std::uint16_t kPlaceholderObjectKey = 0xFFFF; inline std::uint16_t Read16(const std::uint8_t* p) { return static_cast(p[0]) | static_cast(p[1] << 8); } inline std::uint32_t Read32(const std::uint8_t* p) { return static_cast(p[0]) | (static_cast(p[1]) << 8) | (static_cast(p[2]) << 16) | (static_cast(p[3]) << 24); } inline std::uint64_t Read64(const std::uint8_t* p) { return static_cast(Read32(p)) | (static_cast(Read32(p + 4)) << 32); } inline void Write16(std::uint8_t* p, std::uint16_t v) { p[0] = static_cast(v); p[1] = static_cast(v >> 8); } struct View { bool present = false; bool progression = false; std::uint32_t experience = 0; std::uint8_t aiBehavior = 0; std::uint8_t aiComboPresetHash = 0; std::uint16_t aiClass = 0; bool aiComboEnabled = false; std::uint16_t sourceCharacter = 0; std::uint64_t revision = 0; const std::uint8_t* fingerprint = nullptr; std::uint8_t job = 0; std::uint8_t grow = 0; std::uint16_t level = 0; const std::uint8_t* nameUtf16 = nullptr; std::size_t nameUnits = 0; const std::uint8_t* equipmentIds = nullptr; std::size_t equipmentCount = 0; const std::uint8_t* skills = nullptr; std::size_t skillCount = 0; const std::uint8_t* quickSkills = nullptr; std::size_t quickSkillCount = 0; const std::uint8_t* mode0 = nullptr; std::size_t mode0Length = 0; const std::uint8_t* mode1 = nullptr; std::size_t mode1Length = 0; }; inline bool BodiesHavePlaceholder( const std::uint8_t* mode0, std::size_t mode0Length, const std::uint8_t* mode1, std::size_t mode1Length) { if (!mode0 || mode0Length <= kMode0ObjectKeyOffset + 1 || mode0[0] != 0 || Read16(mode0 + 1) != 1 || Read16(mode0 + kMode0ObjectKeyOffset) != kPlaceholderObjectKey) { return false; } if (mode1Length == 0) { return mode1 == nullptr; } return mode1 && mode1Length > kMode1ObjectKeyOffset + 1 && mode1[0] == 1 && Read16(mode1 + 1) == 1 && Read16(mode1 + kMode1ObjectKeyOffset) == kPlaceholderObjectKey; } inline bool DecodeBody( const std::uint8_t* body, std::size_t bodyLength, View* out) { if (!body || !out || bodyLength < kBodyHeaderSize) { return false; } if (Read32(body) != kMagic || body[4] != kVersion || (body[5] & ~(kFlagPresent | kFlagProgression | kFlagAiPlan)) != 0) { return false; } View value{}; value.present = (body[5] & kFlagPresent) != 0; value.progression = (body[5] & kFlagProgression) != 0; value.experience = Read32(body + 68); const bool hasAiPlan = (body[5] & kFlagAiPlan) != 0; if (hasAiPlan && value.progression) return false; if (hasAiPlan) { value.aiBehavior = static_cast(value.experience & 0x03); value.aiComboEnabled = (value.experience & 0x04) != 0; value.aiComboPresetHash = static_cast((value.experience >> 8) & 0xFF); value.aiClass = static_cast((value.experience >> 16) & 0xFFFF); if (value.aiBehavior < 1 || value.aiBehavior > 3) return false; } value.sourceCharacter = Read16(body + 6); value.revision = Read64(body + 8); value.mode0Length = Read32(body + 16); value.mode1Length = Read32(body + 20); value.fingerprint = body + 24; value.job = body[56]; value.grow = body[57]; value.nameUnits = Read16(body + 58); value.equipmentCount = Read16(body + 60); value.level = Read16(body + 62); value.skillCount = Read16(body + 64); value.quickSkillCount = Read16(body + 66); if (value.revision == 0 || (!value.progression && !hasAiPlan && value.experience != 0)) return false; if (!value.present) { if (value.progression || value.sourceCharacter != 0 || value.mode0Length != 0 || value.mode1Length != 0 || value.nameUnits != 0 || value.equipmentCount != 0 || value.level != 0 || value.skillCount != 0 || value.quickSkillCount != 0 || bodyLength != kBodyHeaderSize || hasAiPlan) { return false; } *out = value; return true; } if (value.sourceCharacter == 0 || value.mode0Length == 0 || value.mode0Length > kMaxProjectionBody || value.mode1Length > kMaxProjectionBody || value.level == 0 || value.level > 90 || value.nameUnits == 0 || value.nameUnits > kMaxAicNameUnits || value.equipmentCount > kMaxAicEquipment || value.skillCount > kMaxAicSkills || value.quickSkillCount > kMaxAicQuickSkills) { return false; } const std::size_t nameBytes = value.nameUnits * 2; const std::size_t equipmentBytes = value.equipmentCount * 4; const std::size_t skillBytes = value.skillCount * 8; const std::size_t quickSkillBytes = value.quickSkillCount * 8; const std::size_t dynamicBytes = nameBytes + equipmentBytes + skillBytes + quickSkillBytes; if (dynamicBytes > kMaxDynamicAicBytes) return false; const std::size_t wanted = kBodyHeaderSize + dynamicBytes + value.mode0Length + value.mode1Length; if (wanted != bodyLength) return false; value.nameUtf16 = body + kBodyHeaderSize; value.equipmentIds = value.nameUtf16 + nameBytes; value.skills = value.equipmentIds + equipmentBytes; value.quickSkills = value.skills + skillBytes; value.mode0 = value.quickSkills + quickSkillBytes; value.mode1 = value.mode1Length != 0 ? value.mode0 + value.mode0Length : nullptr; if (!BodiesHavePlaceholder( value.mode0, value.mode0Length, value.mode1, value.mode1Length)) { return false; } // Live progression carries the existing native 92-byte base-stat block, // but never dispatches mode0/mode1 (which can reconstruct actor resources). if (value.progression && (value.mode1Length < 123 || Read32(value.mode1 + 27) != 92)) return false; for (std::size_t i = 0; value.progression && i < value.skillCount; ++i) { if (Read32(value.skills + i * 8) == 0 || Read32(value.skills + i * 8) >= 512 || Read32(value.skills + i * 8 + 4) == 0 || Read32(value.skills + i * 8 + 4) > 255) return false; } *out = value; return true; } inline bool IsPrivateNetworkPacketHeader( const std::uint8_t* packet, std::size_t totalLength) { if (!packet || totalLength < kPacketHeaderSize + kBodyHeaderSize) { return false; } const std::uint32_t packetLength = Read32(packet + 3); return packet[0] == 0 && Read16(packet + 1) == kPrivateOpcode && packetLength == totalLength; } inline bool DecodePacket( const std::uint8_t* packet, std::size_t available, View* out) { if (!IsPrivateNetworkPacketHeader(packet, available) || !out) { return false; } return DecodeBody( packet + kPacketHeaderSize, available - kPacketHeaderSize, out); } inline bool PatchTargetObjectKey( std::uint8_t* mode0, std::size_t mode0Length, std::uint8_t* mode1, std::size_t mode1Length, std::uint16_t objectKey) { if (objectKey == 0 || objectKey == kPlaceholderObjectKey || !BodiesHavePlaceholder(mode0, mode0Length, mode1, mode1Length)) { return false; } Write16(mode0 + kMode0ObjectKeyOffset, objectKey); if (mode1Length != 0) { Write16(mode1 + kMode1ObjectKeyOffset, objectKey); } return true; } } // namespace dnf90_companion_projection