Files
90CNServer/client-patch/90CN_companion_projection.h
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

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