服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
241 lines
8.5 KiB
C++
241 lines
8.5 KiB
C++
#pragma once
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
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<std::uint16_t>(p[0]) |
|
|
static_cast<std::uint16_t>(p[1] << 8);
|
|
}
|
|
inline std::uint32_t Read32(const std::uint8_t* p) {
|
|
return static_cast<std::uint32_t>(p[0]) |
|
|
(static_cast<std::uint32_t>(p[1]) << 8) |
|
|
(static_cast<std::uint32_t>(p[2]) << 16) |
|
|
(static_cast<std::uint32_t>(p[3]) << 24);
|
|
}
|
|
inline std::uint64_t Read64(const std::uint8_t* p) {
|
|
return static_cast<std::uint64_t>(Read32(p)) |
|
|
(static_cast<std::uint64_t>(Read32(p + 4)) << 32);
|
|
}
|
|
inline void Write16(std::uint8_t* p, std::uint16_t v) {
|
|
p[0] = static_cast<std::uint8_t>(v);
|
|
p[1] = static_cast<std::uint8_t>(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<std::uint8_t>(value.experience & 0x03);
|
|
value.aiComboEnabled = (value.experience & 0x04) != 0;
|
|
value.aiComboPresetHash = static_cast<std::uint8_t>((value.experience >> 8) & 0xFF);
|
|
value.aiClass = static_cast<std::uint16_t>((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
|