Files
90CNServer/client-patch/90CN_companion_p0.cpp
T

3887 lines
169 KiB
C++

#include "90CN_companion_p0.h"
#include "90CN_companion_projection.h"
#include "90CN_companion_quickbar_policy.h"
#include "90CN_companion_native_bindings.h"
#include <windows.h>
#include <cstdint>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <cwchar>
#include <deque>
#include <vector>
#include <utility>
#include <intrin.h>
// Defined in 90CN.cpp. Kept at global scope so the declaration has external
// linkage (the anonymous namespace below would give it internal linkage).
// Local copy of the prologue-checked inline hook installer.
// (90CN.cpp's helper has internal linkage in this build, so duplicate the small
// trampoline routine here instead of exposing it across translation units.)
// The naked detour below references this by its plain C name.
extern "C" void* g_companionEquipmentTrampoline = nullptr;
namespace dnf90_companion_p0 {
namespace {
CompanionNativeBindings g_native;
bool g_newNativeHooksReady = false;
// Read-only stat probe state. g_lastCompanionStatBlob is the exact 92-byte
// value set the client was told to use for the current companion actor; the
// probe scans the actor window for those values to locate the stat fields
// without hardcoding any build-specific offset.
alignas(4) unsigned char g_lastCompanionStatBlob[92] = {};
bool g_hasLastCompanionStatBlob = false;
void* g_companionStatProbedActor = nullptr;
constexpr int kDungeonSceneMode = 3;
constexpr unsigned char kNativeSummonApcSignature[] = {
0x55, 0x8B, 0xEC, 0x81, 0xEC, 0x80, 0x00, 0x00,
0x00, 0x56, 0x8B, 0x75, 0x08, 0x85, 0xF6, 0x0F,
};
constexpr unsigned char kNativeReadySignature[] = {
0x56, 0x8B, 0xF1, 0xE8, 0xD8, 0x54, 0x39, 0x00,
0x85, 0xC0, 0x74, 0x14, 0xE8, 0xCF, 0x54, 0x39,
};
constexpr unsigned char kSceneContextGetterSignature[] = {
0xE9, 0x4B, 0xA4, 0x56, 0x00
};
constexpr unsigned char kSceneModeSignature[] = {
0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08
};
using NativeSummonApcFn = void (__thiscall*)(
void* ignoredThis,
void* actor,
const FixedApcDescriptor* descriptor);
using NativeReadyFn = unsigned char (__thiscall*)(void* actor);
using SceneContextGetterFn = void* (__cdecl*)();
using SceneModeFn = int (__thiscall*)(void* sceneContext);
using PayloadEncodeFn = void (__cdecl*)(
unsigned char* payload,
std::int32_t length,
void* keyState);
using InternalReceiveFn = void (__thiscall*)(
void* self,
std::int32_t sourceSlot,
unsigned char* packet,
std::int32_t totalLength);
using NetworkRouterFn = int (__thiscall*)(
void* self,
std::int32_t sourceContext,
unsigned char* packet,
std::int32_t totalLength);
using PeerDeliveryFn = void (__thiscall*)(
void* self,
unsigned char* packet,
std::int32_t length,
std::int32_t slot,
std::uint32_t routeKind,
std::uint32_t mode);
using AicCopyFn = void* (__thiscall*)(
void* destination,
const void* source);
using NativeRefStringAllocFn = void* (__cdecl*)(std::size_t bytes);
using NativeRefStringFreeFn = void (__cdecl*)(void* header);
using NativeRefStringRefFn = long (__stdcall*)(void* header);
using NativeVectorFreeFn = void (__cdecl*)(void* data);
using NativeVectorAllocateFn = unsigned char (__thiscall*)(
void* vectorObject,
std::uint32_t elementCount);
using ApcRegistrationFn = unsigned char (__thiscall*)(
void* self,
void* ownerActor,
std::uint32_t flags,
std::uint32_t objectKey,
void* actor,
std::uint32_t value5,
std::uint32_t value6,
std::uint32_t value7,
std::uint32_t value8);
using Class0ProjectionDispatchFn = int (__stdcall*)(
int dispatcherArg,
int packet);
thread_local FixedApcRetryPolicy g_retry;
thread_local void* g_armedActor = nullptr;
thread_local DWORD g_armedThread = 0;
thread_local unsigned int g_waitLogCount = 0;
thread_local bool g_directLoopbackArmed = false;
thread_local bool g_directLoopbackInReceive = false;
thread_local bool g_directLoopbackHandled = false;
thread_local bool g_directLoopbackFailed = false;
thread_local unsigned int g_suppressedPeerDeliveries = 0;
PayloadEncodeFn g_originalPayloadEncode = nullptr;
PeerDeliveryFn g_originalPeerDelivery = nullptr;
NetworkRouterFn g_originalNetworkRouter = nullptr;
AicCopyFn g_originalAicCopy = nullptr;
ApcRegistrationFn g_originalApcRegistration = nullptr;
using AIConditionFn = unsigned char (__thiscall*)(void*);
using AIParameterFn = float* (__thiscall*)(void*, const int*);
using KeyStepReadyFn = unsigned char (__thiscall*)(void*, int);
AIConditionFn g_originalAICondition = nullptr;
Class0ProjectionDispatchFn g_projectionClass0Dispatch = nullptr;
constexpr std::size_t kProjectionPacketCapacity =
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kBodyHeaderSize +
dnf90_companion_projection::kMaxDynamicAicBytes +
2 * dnf90_companion_projection::kMaxProjectionBody;
constexpr std::size_t kProjectionNativePacketCapacity =
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kMaxProjectionBody;
thread_local std::uint16_t g_projectionApcObjectKey = 0;
thread_local void* g_projectionApcActor = nullptr;
thread_local unsigned char g_projectionPacket[kProjectionPacketCapacity] = {};
thread_local std::size_t g_projectionPacketLength = 0;
thread_local int g_projectionDispatcherArg = 0;
thread_local bool g_projectionPending = false;
thread_local bool g_projectionApplyActive = false;
thread_local bool g_projectionAicApplied = false;
thread_local bool g_projectionMode1Failed = false;
thread_local unsigned int g_projectionMode0RetryCount = 0;
thread_local unsigned char g_projectionMode0Packet[kProjectionNativePacketCapacity] = {};
thread_local unsigned char g_projectionMode1Packet[kProjectionNativePacketCapacity] = {};
constexpr unsigned int kProjectionMode0RetryLimit = 3;
constexpr std::size_t kProjectionQueueMaxEntries = 8;
constexpr std::size_t kProjectionQueueMaxBytes = 512 * 1024;
// __except filters are the only place GetExceptionInformation may be read, so
// the crash site is copied out there and reported by the handler.
// Runtime counters: they make it possible to tell "hook never called" apart
// from "hook called but nothing selected" on a machine that misbehaves.
volatile LONG g_conditionEvaluations = 0;
volatile LONG g_supportSelections = 0;
std::uintptr_t g_lastProjectionExceptionAddress = 0;
std::uintptr_t g_lastProjectionFaultAddress = 0;
// Return addresses captured from the faulting EBP chain. Printed as module
// RVAs so a crash inside the native dispatcher can be located without an
// external debugger.
std::uintptr_t g_lastProjectionStack[8] = {};
int g_lastProjectionStackCount = 0;
int CaptureProjectionException(PEXCEPTION_POINTERS info)
{
if (info && info->ExceptionRecord) {
g_lastProjectionExceptionAddress =
reinterpret_cast<std::uintptr_t>(info->ExceptionRecord->ExceptionAddress);
if (info->ExceptionRecord->NumberParameters > 1) {
g_lastProjectionFaultAddress = static_cast<std::uintptr_t>(
info->ExceptionRecord->ExceptionInformation[1]);
}
}
g_lastProjectionStackCount = 0;
if (info && info->ContextRecord) {
std::uintptr_t frame = info->ContextRecord->Ebp;
for (int depth = 0; depth < 8 && frame; ++depth) {
__try {
const std::uintptr_t next = *reinterpret_cast<std::uintptr_t*>(frame);
const std::uintptr_t ret = *reinterpret_cast<std::uintptr_t*>(frame + 4);
if (!ret) break;
g_lastProjectionStack[g_lastProjectionStackCount++] = ret;
if (next <= frame) break;
frame = next;
} __except (EXCEPTION_EXECUTE_HANDLER) {
break;
}
}
}
return EXCEPTION_EXECUTE_HANDLER;
}
struct QueuedProjection {
std::vector<unsigned char> packet;
int dispatcherArg = 0;
};
thread_local std::deque<QueuedProjection> g_projectionQueue;
thread_local std::size_t g_projectionQueueBytes = 0;
thread_local std::size_t g_projectionQueueHighWater = 0;
thread_local unsigned int g_projectionQueueDropCount = 0;
void Log(const char* format, ...);
// ── Companion equipment-instance experiment ───────────────────────────────
// The private 0x7F41 mode1 body already carries the source character's real
// equipment create rows (item id + instance/quality/sockets/forging), but the
// native userinfo dispatcher faults on APC actors, so the patch currently
// applies only the 92-byte stat blob. This block replays the native equipment
// row reader (sub_1D77560 / RVA 0x01977560) against the APC actor, gated by an
// explicit environment flag. The probe hook is read-only; the apply path
// changes only the opt-in run.
//
// DNF90_COMPANION_EQUIP_PROBE=1 install the logging detour
// DNF90_COMPANION_EQUIP_APPLY=1 also replay the mode1 equipment rows
struct ProjectionNativeReaderState {
unsigned char* cursor;
int remaining;
unsigned char* packet;
int packetLength;
};
static_assert(sizeof(ProjectionNativeReaderState) == 16, "native x86 reader ABI");
constexpr uintptr_t kEquipmentApplyRva = 0x01977560;
using EquipmentApplyFn = int (__cdecl*)(void* actor, int mode, int arg3, int arg4);
EquipmentApplyFn g_equipmentApplyOriginal = nullptr;
volatile LONG g_equipmentProbeCalls = 0;
bool g_equipmentProbeInstalled = false;
bool CompanionEquipmentEnvFlag(const char* name) {
char value[8] = {};
DWORD length = GetEnvironmentVariableA(name, value, sizeof(value));
return length > 0 && value[0] == '1';
}
bool CompanionFlagFilePresent(const wchar_t* name) {
wchar_t path[MAX_PATH] = {};
const DWORD length = GetModuleFileNameW(nullptr, path, _MAX_PATH);
if (!length || length >= _MAX_PATH || !name) return false;
wchar_t* slash = wcsrchr(path, L'\\');
if (!slash) return false;
if (wcscpy_s(slash + 1, _MAX_PATH - (slash + 1 - path), name) != 0) {
return false;
}
const DWORD attributes = GetFileAttributesW(path);
return attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
bool CompanionEquipmentProbeEnabled() {
static const bool enabled =
CompanionEquipmentEnvFlag("DNF90_COMPANION_EQUIP_PROBE") ||
CompanionFlagFilePresent(L"90CN_equip_probe.enabled");
return enabled;
}
// 支援兵普攻开关:启动器在客户端目录放置 90CN_basic_attack.disabled。
// 只影响 CompanionBasicAttack 回退程序,12 格技能程序不变。
bool CompanionBasicAttackEnabled() {
static const bool disabled =
CompanionFlagFilePresent(L"90CN_basic_attack.disabled");
return !disabled;
}
bool CompanionEquipmentApplyEnabled() {
// Production default: the mode1 equipment rows are replayed for every
// companion APC. A disable flag exists for a one-restart rollback.
static const bool disabled =
CompanionEquipmentEnvFlag("DNF90_COMPANION_EQUIP_APPLY_DISABLE") ||
CompanionFlagFilePresent(L"90CN_equip_apply.disabled");
return !disabled;
}
bool CompanionEquipmentVerifyEnabled() {
static const bool enabled =
CompanionEquipmentEnvFlag("DNF90_COMPANION_EQUIP_VERIFY") ||
CompanionFlagFilePresent(L"90CN_equip_verify.enabled");
return enabled;
}
bool CompanionModuleContains(const void* pointer) {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!base || !pointer) return false;
__try {
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) {
return false;
}
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
base + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
const uintptr_t value = reinterpret_cast<uintptr_t>(pointer);
return value >= base && value < base + nt->OptionalHeader.SizeOfImage;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
extern "C" void __cdecl CompanionEquipmentProbeLog(
unsigned actor, unsigned a2, unsigned a3, unsigned a4, unsigned ret)
{
if (!CompanionEquipmentProbeEnabled()) return;
const LONG calls = InterlockedIncrement(&g_equipmentProbeCalls);
if (calls > 24) return;
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
auto* reader = reinterpret_cast<ProjectionNativeReaderState*>(
base + g_native.kProjectionReaderStateRva);
const unsigned char* cursor = reader ? reader->cursor : nullptr;
unsigned vtable = 0;
unsigned getter = 0;
__try {
if (actor) {
vtable = *reinterpret_cast<const unsigned*>(actor);
if (vtable) {
getter = *reinterpret_cast<const unsigned*>(vtable + 0xCF4);
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
}
Log("equipment-probe call=%ld actor=%08X a2=%u a3=%u a4=%u ret=%08X vtable=%08X getter=%08X cursor=%p remaining=%d bytes=%02X %02X %02X %02X",
calls, actor, a2, a3, a4, ret, vtable, getter,
cursor, reader ? reader->remaining : -1,
cursor ? cursor[0] : 0, cursor ? cursor[1] : 0,
cursor ? cursor[2] : 0, cursor ? cursor[3] : 0);
}
extern "C" void __declspec(naked) CompanionEquipmentProbeDetour()
{
__asm {
push ebp
mov ebp, esp
pushad
pushfd
push dword ptr [ebp + 4]
push dword ptr [ebp + 0x14]
push dword ptr [ebp + 0x10]
push dword ptr [ebp + 0x0C]
push dword ptr [ebp + 0x08]
call CompanionEquipmentProbeLog
add esp, 20
popfd
popad
pop ebp
jmp dword ptr [g_companionEquipmentTrampoline]
}
}
bool InstallCompanionInlineHook(uintptr_t target, const unsigned char* expected,
size_t patchLength, void* detour, void** original, const char* label)
{
if (!target || !expected || patchLength < 5 || !detour || !original) {
return false;
}
if (memcmp(reinterpret_cast<const void*>(target), expected, patchLength) != 0) {
Log("%s prologue mismatch at 0x%08X", label, target);
return false;
}
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
nullptr, patchLength + 5, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!trampoline) {
Log("%s trampoline allocation failed: %u", label, GetLastError());
return false;
}
memcpy(trampoline, reinterpret_cast<void*>(target), patchLength);
trampoline[patchLength] = 0xE9;
*reinterpret_cast<int32_t*>(trampoline + patchLength + 1) =
static_cast<int32_t>(
static_cast<intptr_t>(target + patchLength) -
reinterpret_cast<intptr_t>(trampoline + patchLength + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(reinterpret_cast<void*>(target), patchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
Log("%s VirtualProtect failed: %u", label, GetLastError());
VirtualFree(trampoline, 0, MEM_RELEASE);
return false;
}
unsigned char jump[5] = {0xE9};
*reinterpret_cast<int32_t*>(jump + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(detour) -
static_cast<intptr_t>(target + 5));
memcpy(reinterpret_cast<void*>(target), jump, sizeof(jump));
for (size_t i = sizeof(jump); i < patchLength; ++i) {
*reinterpret_cast<unsigned char*>(target + i) = 0x90;
}
DWORD ignoredProtection = 0;
VirtualProtect(reinterpret_cast<void*>(target), patchLength,
oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(),
reinterpret_cast<void*>(target), patchLength);
FlushInstructionCache(GetCurrentProcess(), trampoline, patchLength + 5);
*original = trampoline;
Log("%s installed target=0x%08X trampoline=%p", label, target, trampoline);
return true;
}
bool InstallCompanionEquipmentProbe() {
Log("equipment-hook flags probe=%u apply=%u installed=%u",
CompanionEquipmentProbeEnabled() ? 1u : 0u,
CompanionEquipmentApplyEnabled() ? 1u : 0u,
g_equipmentProbeInstalled ? 1u : 0u);
if (g_equipmentProbeInstalled) {
return g_equipmentApplyOriginal != nullptr;
}
if (!CompanionEquipmentProbeEnabled() &&
!CompanionEquipmentApplyEnabled()) {
return false;
}
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
const uintptr_t target = base + kEquipmentApplyRva;
static const unsigned char expected[] = {0x55, 0x8B, 0xEC, 0x6A, 0xFF};
void* trampoline = nullptr;
if (!InstallCompanionInlineHook(target, expected, sizeof(expected),
reinterpret_cast<void*>(&CompanionEquipmentProbeDetour),
&trampoline, "companion-equipment-probe")) {
Log("equipment-probe hook install failed target=0x%08X", target);
return false;
}
g_companionEquipmentTrampoline = trampoline;
g_equipmentApplyOriginal = reinterpret_cast<EquipmentApplyFn>(trampoline);
g_equipmentProbeInstalled = true;
Log("equipment-probe hook installed target=0x%08X trampoline=%p probe=%u apply=%u",
target, trampoline, CompanionEquipmentProbeEnabled() ? 1u : 0u,
CompanionEquipmentApplyEnabled() ? 1u : 0u);
return true;
}
void LogCompanionEquipmentVerify(
const char* phase, void* actor, void* info, void* aic)
{
if (!CompanionEquipmentVerifyEnabled()) return;
unsigned aicRows = 0;
const unsigned weaponID = 101010856u;
unsigned weaponRow[3] = {};
if (aic) {
auto* begin = *reinterpret_cast<unsigned char**>(
reinterpret_cast<unsigned char*>(aic) +
g_native.kAicEquipmentVectorOffset);
auto* end = *reinterpret_cast<unsigned char**>(
reinterpret_cast<unsigned char*>(aic) +
g_native.kAicEquipmentVectorOffset + 4);
if (begin && end >= begin && ((end - begin) % 12) == 0) {
aicRows = static_cast<unsigned>((end - begin) / 12);
for (unsigned i = 0; i < aicRows; ++i) {
auto* row = reinterpret_cast<unsigned*>(begin + i * 12);
if (row[0] == weaponID) {
weaponRow[0] = row[0];
weaponRow[1] = row[1];
weaponRow[2] = row[2];
}
}
}
}
int qualityHits = 0;
int weaponHits = 0;
int qualityOff = -1;
int weaponOff = -1;
const unsigned quality = 999999998u;
if (info) {
__try {
auto* p = reinterpret_cast<unsigned char*>(info);
for (int off = 0; off + 4 <= 0x4000; off += 4) {
const unsigned value = *reinterpret_cast<unsigned*>(p + off);
if (value == quality) {
++qualityHits;
if (qualityOff < 0) qualityOff = off;
}
if (value == weaponID) {
++weaponHits;
if (weaponOff < 0) weaponOff = off;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
}
}
Log("equipment-verify phase=%s actor=%p info=%p aic=%p aic_rows=%u aic_weapon=%u/%u/%u quality_hits=%d@%d weapon_hits=%d@%d",
phase, actor, info, aic, aicRows,
weaponRow[0], weaponRow[1], weaponRow[2],
qualityHits, qualityOff, weaponHits, weaponOff);
}
bool TryApplyCompanionEquipment(
void* actor,
unsigned char* mode1Packet,
std::size_t mode1PacketLength,
unsigned char* mode1,
std::size_t mode1Length,
void* aic)
{
if (mode1 && mode1Length >= 125 && CompanionEquipmentProbeEnabled()) {
Log("equipment-rows source body=%zu count=%u",
mode1Length, static_cast<unsigned>(mode1[124]));
}
if (!CompanionEquipmentApplyEnabled() || !g_equipmentApplyOriginal ||
!actor || !mode1Packet || !mode1) {
return false;
}
if (mode1Length < 125 || mode1PacketLength < 16 + mode1Length) {
return false;
}
const unsigned count = mode1[124];
if (count == 0 || count > 32) {
Log("equipment-apply skipped rows=%u body=%zu", count, mode1Length);
return false;
}
auto** table = *reinterpret_cast<void***>(actor);
if (!table) return false;
const void* getter = table[0xCF4 / 4];
if (!CompanionModuleContains(getter)) {
Log("equipment-apply skipped: actor userinfo getter=%p outside module",
getter);
return false;
}
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
void* info = nullptr;
using UserInfoFn = void* (__thiscall*)(void*);
__try {
info = reinterpret_cast<UserInfoFn>(
const_cast<void*>(getter))(actor);
} __except (EXCEPTION_EXECUTE_HANDLER) {
info = nullptr;
}
LogCompanionEquipmentVerify("before", actor, info, aic);
auto* reader = reinterpret_cast<ProjectionNativeReaderState*>(
base + g_native.kProjectionReaderStateRva);
const ProjectionNativeReaderState saved = *reader;
int result = -1;
unsigned long code = 0;
__try {
reader->cursor = mode1 + 124;
reader->remaining = static_cast<int>(mode1Length - 124);
reader->packet = mode1Packet;
reader->packetLength = static_cast<int>(mode1PacketLength);
result = g_equipmentApplyOriginal(actor, 1, 0, 0);
} __except (EXCEPTION_EXECUTE_HANDLER) {
code = GetExceptionCode();
}
*reader = saved;
LogCompanionEquipmentVerify("after", actor, info, aic);
Log("equipment-apply actor=%p rows=%u body=%zu result=%d exception=0x%08X",
actor, count, mode1Length, result, code);
return code == 0;
}
// Registration, not combat packets, owns the source -> APC identity. Entries
// are discarded on the accepted town boundary / a new dungeon arm.
struct LiveProjectionActor {
std::uint16_t source = 0, key = 0, level = 0;
std::uint64_t revision = 0;
void* actor = nullptr;
void* owner = nullptr;
void* aic = nullptr;
std::uint32_t skills[512] = {};
std::uint32_t quick[12] = {};
std::uint8_t aiBehavior = 0;
std::uint8_t aiComboPresetHash = 0;
std::uint16_t aiClass = 0;
bool aiComboEnabled = false;
bool keysReady = false;
std::uint8_t job = 0, grow = 0;
void* aiVm = nullptr;
void* buffCondition = nullptr;
void* buffCooldown = nullptr;
void* healCondition = nullptr;
void* actionStateCondition = nullptr;
float* buffSkillParameter = nullptr;
float* buffSlotParameter = nullptr;
float* healThresholdParameter = nullptr;
void* modeConditions[8] = {};
std::size_t modeConditionCount = 0;
void* areaCooldowns[128] = {};
std::size_t areaCooldownCount = 0;
bool supportActionPending = true;
int selfHpPercent = -1;
std::uint32_t observedClock = 0, selfHurtAt = 0;
bool selfHurtRecorded = false, recentlyHurt = false;
bool quickbarPolicyReady = false;
unsigned int lastChosenSkill = 0;
};
thread_local std::vector<LiveProjectionActor> g_liveProjectionActors;
thread_local bool g_quickbarSelectionActive = false;
void ResetCurrentProjectionActorState() {
g_projectionApcObjectKey = 0;
g_projectionApcActor = nullptr;
g_projectionApplyActive = false;
g_projectionAicApplied = false;
g_projectionMode1Failed = false;
g_projectionMode0RetryCount = 0;
}
void ClearCurrentProjectionPacket() {
ResetCurrentProjectionActorState();
g_projectionPacketLength = 0;
g_projectionDispatcherArg = 0;
g_projectionPending = false;
}
void ClearQueuedProjections(const char* reason) {
if (!g_projectionQueue.empty()) {
Log("projection queue cleared reason=%s entries=%zu bytes=%zu high_water=%zu drops=%u",
reason ? reason : "unspecified", g_projectionQueue.size(),
g_projectionQueueBytes, g_projectionQueueHighWater,
g_projectionQueueDropCount);
}
g_projectionQueue.clear();
g_projectionQueueBytes = 0;
}
bool LoadCurrentProjectionPacket(
const unsigned char* bytes,
std::size_t packetLength,
int dispatcherArg)
{
if (!bytes || packetLength == 0 ||
packetLength > sizeof(g_projectionPacket)) {
return false;
}
ResetCurrentProjectionActorState();
memcpy(g_projectionPacket, bytes, packetLength);
g_projectionPacketLength = packetLength;
g_projectionDispatcherArg = dispatcherArg;
g_projectionPending = true;
return true;
}
bool LoadNextQueuedProjection() {
if (g_projectionQueue.empty()) {
ClearCurrentProjectionPacket();
return false;
}
QueuedProjection next = std::move(g_projectionQueue.front());
g_projectionQueueBytes -= next.packet.size();
g_projectionQueue.pop_front();
return LoadCurrentProjectionPacket(
next.packet.data(), next.packet.size(), next.dispatcherArg);
}
void QueueProjectionPacket(
const unsigned char* bytes,
std::size_t packetLength,
int dispatcherArg)
{
if (!bytes || packetLength == 0 || packetLength > kProjectionQueueMaxBytes) {
++g_projectionQueueDropCount;
Log("projection queue drop reason=packet-too-large length=%zu drops=%u",
packetLength, g_projectionQueueDropCount);
return;
}
dnf90_companion_projection::View incoming{};
const bool decoded = dnf90_companion_projection::DecodePacket(
bytes, packetLength, &incoming);
if (decoded) {
for (auto& existing : g_projectionQueue) {
dnf90_companion_projection::View queuedView{};
if (!dnf90_companion_projection::DecodePacket(
existing.packet.data(), existing.packet.size(), &queuedView) ||
queuedView.sourceCharacter != incoming.sourceCharacter ||
queuedView.revision != incoming.revision) {
continue;
}
g_projectionQueueBytes -= existing.packet.size();
std::vector<unsigned char> replacement(bytes, bytes + packetLength);
existing.packet.swap(replacement);
existing.dispatcherArg = dispatcherArg;
g_projectionQueueBytes += existing.packet.size();
Log("projection queue coalesced source=%u revision=%llu depth=%zu bytes=%zu",
static_cast<unsigned int>(incoming.sourceCharacter),
static_cast<unsigned long long>(incoming.revision),
g_projectionQueue.size(), g_projectionQueueBytes);
return;
}
}
while (g_projectionQueue.size() >= kProjectionQueueMaxEntries ||
g_projectionQueueBytes + packetLength > kProjectionQueueMaxBytes) {
if (g_projectionQueue.empty()) break;
g_projectionQueueBytes -= g_projectionQueue.front().packet.size();
g_projectionQueue.pop_front();
++g_projectionQueueDropCount;
}
if (g_projectionQueue.size() >= kProjectionQueueMaxEntries ||
g_projectionQueueBytes + packetLength > kProjectionQueueMaxBytes) {
++g_projectionQueueDropCount;
Log("projection queue drop reason=bounded-capacity length=%zu drops=%u",
packetLength, g_projectionQueueDropCount);
return;
}
QueuedProjection queued;
queued.packet.assign(bytes, bytes + packetLength);
queued.dispatcherArg = dispatcherArg;
g_projectionQueue.push_back(std::move(queued));
g_projectionQueueBytes += packetLength;
if (g_projectionQueue.size() > g_projectionQueueHighWater) {
g_projectionQueueHighWater = g_projectionQueue.size();
}
}
bool Enabled() {
return DirectOp40Enabled();
}
bool ModuleImageRange(
HMODULE module,
uintptr_t* baseOut,
size_t* sizeOut)
{
if (baseOut) *baseOut = 0;
if (sizeOut) *sizeOut = 0;
if (!module) return false;
__try {
const uintptr_t base =
reinterpret_cast<uintptr_t>(module);
const IMAGE_DOS_HEADER* dos =
reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
const IMAGE_NT_HEADERS* nt =
reinterpret_cast<const IMAGE_NT_HEADERS*>(
base + static_cast<uintptr_t>(dos->e_lfanew));
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
const size_t imageSize =
static_cast<size_t>(nt->OptionalHeader.SizeOfImage);
if (imageSize == 0) return false;
if (baseOut) *baseOut = base;
if (sizeOut) *sizeOut = imageSize;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool SceneModeEntryIsExpected(
const unsigned char* entry)
{
if (!entry) return false;
if (memcmp(entry, kSceneModeSignature,
sizeof(kSceneModeSignature)) == 0) {
return true;
}
if (!SceneModeEntryContract::IsRelativeJump(entry[0])) {
return false;
}
std::int32_t relative = 0;
memcpy(&relative, entry + 1, sizeof(relative));
HMODULE selfModule = GetModuleHandleW(L"90CN.dll");
uintptr_t selfBase = 0;
size_t selfSize = 0;
if (!ModuleImageRange(selfModule, &selfBase, &selfSize)) {
return false;
}
return SceneModeEntryContract::JumpTargetInsideModule(
reinterpret_cast<uintptr_t>(entry),
relative,
selfBase,
selfSize);
}
void Log(const char* format, ...) {
char line[512] = {};
va_list args;
va_start(args, format);
_vsnprintf_s(line, _countof(line), _TRUNCATE, format, args);
va_end(args);
wchar_t path[MAX_PATH] = {};
DWORD length = GetModuleFileNameW(nullptr, path, _countof(path));
if (!length || length >= _countof(path)) return;
wchar_t* slash = wcsrchr(path, L'\\');
if (!slash) return;
if (wcscpy_s(slash + 1, _countof(path) - (slash + 1 - path),
L"90CN_companion_p0.log") != 0) return;
HANDLE file = CreateFileW(
path, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return;
SYSTEMTIME now = {};
GetLocalTime(&now);
char prefix[80] = {};
_snprintf_s(prefix, _countof(prefix), _TRUNCATE,
"%04u-%02u-%02u %02u:%02u:%02u.%03u tid=%lu ",
now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute,
now.wSecond, now.wMilliseconds, GetCurrentThreadId());
DWORD written = 0;
WriteFile(file, prefix, static_cast<DWORD>(strlen(prefix)), &written, nullptr);
WriteFile(file, line, static_cast<DWORD>(strlen(line)), &written, nullptr);
static const char newline[] = "\r\n";
WriteFile(file, newline, 2, &written, nullptr);
CloseHandle(file);
}
bool ReadControlledActor(uintptr_t base, void** actor) {
if (!base || !actor) return false;
*actor = nullptr;
__try {
*actor = g_native.kControlledActorGetterRva
? reinterpret_cast<void* (__cdecl*)()>(base + g_native.kControlledActorGetterRva)()
: *reinterpret_cast<void**>(base + g_native.kControlledActorPointerRva);
return *actor != nullptr;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("blocked reason=controlled-actor-read-exception code=0x%08X",
GetExceptionCode());
return false;
}
}
bool ValidateNativeRoute(uintptr_t base, void* actor) {
if (!base || !actor) return false;
__try {
const unsigned char* summonEntry =
reinterpret_cast<const unsigned char*>(
base + g_native.kNativeSummonApcRva);
if (!g_native.newNative && memcmp(summonEntry, kNativeSummonApcSignature,
sizeof(kNativeSummonApcSignature)) != 0) {
Log("blocked reason=summon-signature-mismatch actor=%p", actor);
return false;
}
const unsigned char* readyEntry =
reinterpret_cast<const unsigned char*>(base + g_native.kNativeReadyRva);
if (!g_native.newNative && memcmp(readyEntry, kNativeReadySignature,
sizeof(kNativeReadySignature)) != 0) {
Log("blocked reason=ready-signature-mismatch actor=%p", actor);
return false;
}
const unsigned char* sceneGetterEntry =
reinterpret_cast<const unsigned char*>(
base + g_native.kSceneContextGetterRva);
if (!g_native.newNative && memcmp(sceneGetterEntry, kSceneContextGetterSignature,
sizeof(kSceneContextGetterSignature)) != 0) {
Log("blocked reason=scene-getter-signature-mismatch actor=%p",
actor);
return false;
}
const unsigned char* sceneModeEntry =
reinterpret_cast<const unsigned char*>(base + g_native.kSceneModeRva);
if (!SceneModeEntryIsExpected(sceneModeEntry)) {
Log("blocked reason=scene-mode-entry-mismatch actor=%p entry=%02X %02X %02X %02X %02X",
actor,
sceneModeEntry[0], sceneModeEntry[1],
sceneModeEntry[2], sceneModeEntry[3],
sceneModeEntry[4]);
return false;
}
const uintptr_t vtable =
*reinterpret_cast<const uintptr_t*>(actor);
if (!vtable) {
Log("blocked reason=actor-vtable-null actor=%p", actor);
return false;
}
const uintptr_t serializer =
*reinterpret_cast<const uintptr_t*>(
vtable + g_native.kSerializerVtableOffset);
if (serializer != base + g_native.kExpectedSerializerRva) {
Log("blocked reason=serializer-mismatch actor=%p actual=%p expected=%p",
actor, reinterpret_cast<void*>(serializer),
reinterpret_cast<void*>(base + g_native.kExpectedSerializerRva));
return false;
}
void* owner = *reinterpret_cast<void**>(
reinterpret_cast<uintptr_t>(actor) + g_native.kActorOwnerContextOffset);
if (!owner) {
return false;
}
void* runtime = *reinterpret_cast<void**>(
reinterpret_cast<uintptr_t>(owner) + g_native.kOwnerRuntimeOffset);
return runtime != nullptr;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("blocked reason=validation-exception actor=%p code=0x%08X",
actor, GetExceptionCode());
return false;
}
}
bool NativeReady(uintptr_t base, void* actor) {
NativeReadyFn ready =
reinterpret_cast<NativeReadyFn>(base + g_native.kNativeReadyRva);
__try {
return ready(actor) != 0;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("blocked reason=ready-exception actor=%p code=0x%08X",
actor, GetExceptionCode());
return false;
}
}
bool DungeonSceneReady(uintptr_t base, int* modeOut) {
if (modeOut) *modeOut = -1;
if (!base) return false;
__try {
SceneContextGetterFn getSceneContext =
reinterpret_cast<SceneContextGetterFn>(
base + g_native.kSceneContextGetterRva);
SceneModeFn getSceneMode =
reinterpret_cast<SceneModeFn>(base + g_native.kSceneModeRva);
void* sceneContext = getSceneContext();
if (!sceneContext) {
return false;
}
const int mode = getSceneMode(sceneContext);
if (modeOut) *modeOut = mode;
return mode == kDungeonSceneMode;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("blocked reason=scene-mode-exception code=0x%08X",
GetExceptionCode());
return false;
}
}
bool Submit(uintptr_t base, void* actor) {
if (!g_originalPayloadEncode) {
Log("blocked reason=direct-op40-encode-hook-not-ready actor=%p", actor);
return false;
}
FixedApcDescriptor descriptor = FixedApc5405Descriptor();
if (g_projectionPending) {
dnf90_companion_projection::View view{};
if (!dnf90_companion_projection::DecodePacket(
g_projectionPacket,
g_projectionPacketLength,
&view)) {
Log("blocked reason=baseline-projection-decode actor=%p", actor);
return false;
}
ProjectionBaseline baseline{};
if (!ProjectionBaselineContract::TrySelect(
view.job, view.grow, &baseline)) {
Log("blocked reason=unsupported-projection-baseline job=%u grow=%u source=%u actor=%p",
static_cast<unsigned int>(view.job),
static_cast<unsigned int>(view.grow),
static_cast<unsigned int>(view.sourceCharacter),
actor);
return false;
}
descriptor = baseline.descriptor;
const std::int32_t baselineLevel = descriptor.minimumLevel;
descriptor.minimumLevel = static_cast<std::int32_t>(view.level);
descriptor.maximumLevel = static_cast<std::int32_t>(view.level);
Log("projection baseline selected source=%u source_job=%u source_grow=0x%02X first_grow=%u awakening=%u apc=%d aic_job=%u aic_grow=%u bridged=%u baseline_level=%d source_level=%u",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(view.job),
static_cast<unsigned int>(view.grow),
static_cast<unsigned int>(baseline.sourceGrow),
static_cast<unsigned int>((view.grow >> 4) & 0x0Fu),
descriptor.apcCode,
static_cast<unsigned int>(baseline.aicJob),
static_cast<unsigned int>(baseline.aicGrow),
baseline.bridged ? 1u : 0u,
baselineLevel,
static_cast<unsigned int>(view.level));
}
NativeSummonApcFn summon =
reinterpret_cast<NativeSummonApcFn>(
base + g_native.kNativeSummonApcRva);
g_directLoopbackHandled = false;
g_directLoopbackFailed = false;
g_suppressedPeerDeliveries = 0;
g_directLoopbackArmed = true;
__try {
summon(actor, actor, &descriptor);
} __except (EXCEPTION_EXECUTE_HANDLER) {
g_directLoopbackArmed = false;
g_directLoopbackFailed = true;
Log("blocked reason=summon-exception actor=%p code=0x%08X",
actor, GetExceptionCode());
return false;
}
g_directLoopbackArmed = false;
if (!g_directLoopbackHandled || g_directLoopbackFailed) {
Log("blocked reason=direct-op40-loopback-not-completed actor=%p handled=%u failed=%u",
actor,
g_directLoopbackHandled ? 1u : 0u,
g_directLoopbackFailed ? 1u : 0u);
return false;
}
Log("submitted direct-op40 apc=%d level=%d lifetime=%d actor=%p suppressed_peer_deliveries=%u",
descriptor.apcCode, descriptor.minimumLevel,
descriptor.lifetimeValue, actor, g_suppressedPeerDeliveries);
return true;
}
} // namespace
void LogProjectionDecodeRejected(
const unsigned char* bytes,
std::size_t packetLength,
const char* stage);
bool DirectOp40Enabled() {
// Formal support-soldier runtime: hooks stay installed for every client.
// Actual APC creation is still fail-closed because FixedApcRetryPolicy
// requires a valid server-owned 0x7F41 projection packet for this run.
return !g_native.newNative || g_newNativeHooksReady;
}
bool ConfigureNewNativeBindings() {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
g_newNativeHooksReady = false;
g_native = NewNativeCompanionBindings();
__try {
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS32*>(base + dos->e_lfanew);
if (dos->e_magic != IMAGE_DOS_SIGNATURE || nt->Signature != IMAGE_NT_SIGNATURE ||
nt->FileHeader.Machine != IMAGE_FILE_MACHINE_I386 ||
nt->FileHeader.TimeDateStamp != 0x5C24898C ||
nt->OptionalHeader.SizeOfImage != 0x06A92000) return false;
for (const auto& anchor : kNewNativeCompanionAnchors) {
if (memcmp(reinterpret_cast<const void*>(base + anchor.rva),
anchor.bytes, sizeof(anchor.bytes))) {
Log("new-native binding rejected rva=0x%08X", anchor.rva);
return false;
}
}
Log("new-native ABI verified anchors=%zu", _countof(kNewNativeCompanionAnchors));
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("new-native ABI verification exception=0x%08X", GetExceptionCode());
return false;
}
}
void SetNewNativeHooksReady(bool ready) {
g_newNativeHooksReady = ready;
if (ready) {
InstallCompanionEquipmentProbe();
}
Log("new-native companion hooks ready=%u", ready ? 1u : 0u);
}
void SetOriginalPayloadEncode(void* original) {
g_originalPayloadEncode = reinterpret_cast<PayloadEncodeFn>(original);
Log("direct-op40 encode hook original=%p enabled=%u",
original, DirectOp40Enabled() ? 1u : 0u);
}
void __cdecl ProxyPayloadEncode(
std::uint8_t* payload,
std::int32_t length,
void* keyState)
{
PayloadEncodeFn original = g_originalPayloadEncode;
if (!original) {
return;
}
// Always preserve the native encoder first. The P0 path observes the
// exact bytes normal send produced; all unrelated calls are unchanged.
original(payload, length, keyState);
if (!g_directLoopbackArmed ||
g_directLoopbackInReceive ||
GetCurrentThreadId() != g_armedThread) {
return;
}
uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!base || !payload || length < 0) {
g_directLoopbackFailed = true;
return;
}
const uintptr_t caller =
reinterpret_cast<uintptr_t>(_ReturnAddress());
if (caller != base + g_native.kPayloadEncodeReturnRva) {
return;
}
__try {
void* writer = *reinterpret_cast<void**>(base + g_native.kWriterPointerRva);
void* dispatcher =
*reinterpret_cast<void**>(base + g_native.kDispatcherPointerRva);
if (!writer || writer != dispatcher) {
Log("blocked reason=direct-op40-owner-mismatch writer=%p dispatcher=%p",
writer, dispatcher);
g_directLoopbackFailed = true;
return;
}
const uintptr_t vtable = *reinterpret_cast<uintptr_t*>(writer);
if (!vtable) {
g_directLoopbackFailed = true;
return;
}
const uintptr_t receiveAddress =
*reinterpret_cast<uintptr_t*>(vtable + 0x04);
if (receiveAddress != base + g_native.kExpectedReceiveRva) {
Log("blocked reason=direct-op40-receive-mismatch actual=%p expected=%p",
reinterpret_cast<void*>(receiveAddress),
reinterpret_cast<void*>(base + g_native.kExpectedReceiveRva));
g_directLoopbackFailed = true;
return;
}
unsigned char* packet =
reinterpret_cast<unsigned char*>(
reinterpret_cast<uintptr_t>(writer) + g_native.kPacketBufferOffset);
unsigned char* cursor =
*reinterpret_cast<unsigned char**>(
reinterpret_cast<uintptr_t>(writer) + g_native.kWriterCursorOffset);
if (!cursor || cursor < packet) {
g_directLoopbackFailed = true;
return;
}
const std::size_t totalLength =
static_cast<std::size_t>(cursor - packet);
if (totalLength < DirectOp40PacketContract::kHeaderSize ||
totalLength > 4096 ||
payload != packet + DirectOp40PacketContract::kHeaderSize ||
static_cast<std::size_t>(length) !=
totalLength - DirectOp40PacketContract::kHeaderSize ||
!DirectOp40PacketContract::Matches(packet, totalLength)) {
// A different encoded message can occur in nested native work.
// It has already been encoded by the original function, so simply
// preserve normal send.
return;
}
unsigned char backup[4096] = {};
memcpy(backup, packet, totalLength);
InternalReceiveFn receive =
reinterpret_cast<InternalReceiveFn>(receiveAddress);
g_directLoopbackInReceive = true;
receive(writer, 0, packet, static_cast<std::int32_t>(totalLength));
g_directLoopbackInReceive = false;
// Native receive decodes payload in-place. Restore the exact encoded
// packet so the remainder of normal send sees byte-for-byte the state
// it would have seen without P0.
memcpy(packet, backup, totalLength);
g_directLoopbackHandled = true;
Log("direct-op40 encoded loopback consumed length=%zu actor=%p",
totalLength, g_armedActor);
} __except (EXCEPTION_EXECUTE_HANDLER) {
g_directLoopbackInReceive = false;
g_directLoopbackFailed = true;
Log("blocked reason=direct-op40-encoded-loopback-exception code=0x%08X actor=%p",
GetExceptionCode(), g_armedActor);
}
}
void SetOriginalPeerDelivery(void* original) {
g_originalPeerDelivery = reinterpret_cast<PeerDeliveryFn>(original);
Log("direct-op40 peer-delivery hook original=%p enabled=%u",
original, DirectOp40Enabled() ? 1u : 0u);
}
void __fastcall ProxyPeerDelivery(
void* self,
void* /*unusedEdx*/,
std::uint8_t* packet,
std::int32_t length,
std::int32_t slot,
std::uint32_t routeKind,
std::uint32_t mode)
{
PeerDeliveryFn original = g_originalPeerDelivery;
if (!original) {
return;
}
const std::size_t packetLength =
length > 0 ? static_cast<std::size_t>(length) : 0;
const bool contractMatch =
DirectOp40PeerDeliveryContract::Matches(
g_directLoopbackArmed,
g_directLoopbackHandled,
g_directLoopbackInReceive,
static_cast<std::uint32_t>(GetCurrentThreadId()),
static_cast<std::uint32_t>(g_armedThread),
packet,
packetLength,
routeKind,
mode);
if (contractMatch) {
uintptr_t base =
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
__try {
void* writer =
*reinterpret_cast<void**>(base + g_native.kWriterPointerRva);
if (writer) {
unsigned char* expectedPacket =
reinterpret_cast<unsigned char*>(
reinterpret_cast<uintptr_t>(writer) +
g_native.kPacketBufferOffset);
unsigned char* cursor =
*reinterpret_cast<unsigned char**>(
reinterpret_cast<uintptr_t>(writer) +
g_native.kWriterCursorOffset);
const std::size_t expectedLength =
cursor && cursor >= expectedPacket ?
static_cast<std::size_t>(cursor - expectedPacket) : 0;
if (packet == expectedPacket &&
packetLength == expectedLength) {
++g_suppressedPeerDeliveries;
if (g_suppressedPeerDeliveries <= 2) {
Log("suppressed duplicate direct-op40 peer delivery slot=%d length=%d route=%u mode=%u actor=%p",
slot, length, routeKind, mode, g_armedActor);
}
return;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("peer-delivery suppression validation exception code=0x%08X",
GetExceptionCode());
}
}
original(self, packet, length, slot, routeKind, mode);
}
std::uint32_t ProjectionTemplateFingerprint(
const void* source)
{
if (!source) return 0;
const std::uint8_t* bytes =
reinterpret_cast<const std::uint8_t*>(source);
std::uint32_t hash = 2166136261u;
auto mix = [&](const void* data, std::size_t length) {
const std::uint8_t* p =
reinterpret_cast<const std::uint8_t*>(data);
for (std::size_t i = 0; i < length; ++i) {
hash ^= p[i];
hash *= 16777619u;
}
};
__try {
mix(bytes + 8, 12);
const std::uintptr_t begin =
*reinterpret_cast<const std::uintptr_t*>(bytes + g_native.kAicEquipmentVectorOffset);
const std::uintptr_t end =
*reinterpret_cast<const std::uintptr_t*>(bytes + g_native.kAicEquipmentVectorOffset + 4);
if (begin && end >= begin && end - begin <= 12u * 32u) {
mix(reinterpret_cast<const void*>(begin), end - begin);
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
return 0;
}
return hash;
}
bool AssignNativeRefWideString(
void* stringField,
const wchar_t* text)
{
if (!stringField || !text) return false;
const std::size_t units = std::wcslen(text);
if (units == 0 ||
units > dnf90_companion_projection::kMaxAicNameUnits) {
return false;
}
uintptr_t base =
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!base) return false;
NativeRefStringAllocFn alloc =
reinterpret_cast<NativeRefStringAllocFn>(
base + g_native.kNativeRefStringAllocRva);
NativeRefStringFreeFn freeBlock =
reinterpret_cast<NativeRefStringFreeFn>(
base + g_native.kNativeRefStringFreeRva);
NativeRefStringRefFn increment =
*reinterpret_cast<NativeRefStringRefFn*>(
base + g_native.kNativeRefStringIncIatRva);
NativeRefStringRefFn decrement =
*reinterpret_cast<NativeRefStringRefFn*>(
base + g_native.kNativeRefStringDecIatRva);
if (!alloc || !freeBlock || !increment || !decrement) {
return false;
}
__try {
wchar_t* oldData =
*reinterpret_cast<wchar_t**>(stringField);
if (!oldData) return false;
std::uint8_t* oldHeader =
reinterpret_cast<std::uint8_t*>(oldData) - 8;
const std::uint32_t oldBytes =
*reinterpret_cast<std::uint32_t*>(oldHeader + 4);
if (oldBytes < 2 || oldBytes > 512 ||
(oldBytes % 2u) != 0) {
return false;
}
const std::size_t newBytes =
(units + 1u) * sizeof(wchar_t);
if (oldBytes == newBytes &&
memcmp(oldData, text, newBytes) == 0) {
return true;
}
std::uint8_t* newHeader =
reinterpret_cast<std::uint8_t*>(
alloc(8u + newBytes));
if (!newHeader) return false;
*reinterpret_cast<long*>(newHeader) = 0;
*reinterpret_cast<std::uint32_t*>(newHeader + 4) =
static_cast<std::uint32_t>(newBytes);
memcpy(newHeader + 8, text, newBytes);
increment(newHeader);
*reinterpret_cast<wchar_t**>(stringField) =
reinterpret_cast<wchar_t*>(newHeader + 8);
const long remaining = decrement(oldHeader);
if (remaining <= 0) {
freeBlock(oldHeader);
}
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("native-ref-string assign exception field=%p code=0x%08X",
stringField, GetExceptionCode());
return false;
}
}
struct NativeVectorStorage {
std::uint32_t opaque = 0;
std::uintptr_t begin = 0;
std::uintptr_t end = 0;
std::uintptr_t capacityEnd = 0;
};
bool EnsureNativeVectorCapacity(
std::uint8_t* destination,
std::size_t objectOffset,
std::size_t elementSize,
std::size_t requestedCount,
std::uintptr_t allocateRva,
std::uintptr_t* beginOut,
std::uintptr_t* endOut,
std::uintptr_t* capacityOut,
bool* grewOut)
{
if (!destination || elementSize == 0 ||
!beginOut || !endOut || !capacityOut || !grewOut ||
requestedCount > static_cast<std::size_t>(0x7FFFFFFFu)) {
return false;
}
*grewOut = false;
__try {
std::uint8_t* object = destination + objectOffset;
std::uintptr_t begin =
*reinterpret_cast<std::uintptr_t*>(object + 4);
std::uintptr_t end =
*reinterpret_cast<std::uintptr_t*>(object + 8);
std::uintptr_t capacityEnd =
*reinterpret_cast<std::uintptr_t*>(object + 12);
if (begin == 0 || end == 0 || capacityEnd == 0) {
if (begin != 0 || end != 0 || capacityEnd != 0) {
return false;
}
} else {
if (end < begin || capacityEnd < end ||
((end - begin) % elementSize) != 0 ||
((capacityEnd - begin) % elementSize) != 0) {
return false;
}
const std::size_t capacity =
(capacityEnd - begin) / elementSize;
if (requestedCount <= capacity) {
*beginOut = begin;
*endOut = end;
*capacityOut = capacityEnd;
return true;
}
}
if (requestedCount == 0) {
*beginOut = begin;
*endOut = end;
*capacityOut = capacityEnd;
return true;
}
uintptr_t base =
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
NativeVectorAllocateFn allocate =
reinterpret_cast<NativeVectorAllocateFn>(
base + allocateRva);
NativeVectorFreeFn freeData =
reinterpret_cast<NativeVectorFreeFn>(
base + g_native.kNativeVectorFreeRva);
if (!allocate || !freeData) return false;
NativeVectorStorage temporary{};
if (!allocate(
&temporary,
static_cast<std::uint32_t>(requestedCount)) ||
temporary.begin == 0 ||
temporary.end != temporary.begin ||
temporary.capacityEnd < temporary.begin ||
((temporary.capacityEnd - temporary.begin) %
elementSize) != 0 ||
(temporary.capacityEnd - temporary.begin) /
elementSize <
requestedCount) {
if (temporary.begin != 0) {
freeData(reinterpret_cast<void*>(temporary.begin));
}
return false;
}
const std::uintptr_t oldBegin = begin;
*reinterpret_cast<std::uintptr_t*>(object + 4) =
temporary.begin;
*reinterpret_cast<std::uintptr_t*>(object + 8) =
temporary.begin;
*reinterpret_cast<std::uintptr_t*>(object + 12) =
temporary.capacityEnd;
if (oldBegin != 0) {
freeData(reinterpret_cast<void*>(oldBegin));
}
*beginOut = temporary.begin;
*endOut = temporary.begin;
*capacityOut = temporary.capacityEnd;
*grewOut = true;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("native-vector ensure exception object_offset=0x%zX count=%zu code=0x%08X",
objectOffset, requestedCount, GetExceptionCode());
return false;
}
}
bool ApplyDynamicAicProjection(
void* destination,
const void* source,
const dnf90_companion_projection::View& view)
{
if (!destination || !source || !view.present ||
!view.nameUtf16 || view.level == 0 || view.level > 90 ||
view.nameUnits == 0 ||
view.nameUnits > dnf90_companion_projection::kMaxAicNameUnits ||
view.equipmentCount > dnf90_companion_projection::kMaxAicEquipment ||
view.skillCount > dnf90_companion_projection::kMaxAicSkills ||
view.quickSkillCount > dnf90_companion_projection::kMaxAicQuickSkills) {
return false;
}
__try {
const std::uint32_t templateBefore =
ProjectionTemplateFingerprint(source);
if (templateBefore == 0) return false;
wchar_t name[
dnf90_companion_projection::kMaxAicNameUnits + 1] = {};
for (std::size_t i = 0; i < view.nameUnits; ++i) {
const std::uint16_t unit =
dnf90_companion_projection::Read16(
view.nameUtf16 + i * 2);
if (unit == 0) return false;
name[i] = static_cast<wchar_t>(unit);
}
name[view.nameUnits] = L'\0';
std::uint8_t* dest =
reinterpret_cast<std::uint8_t*>(destination);
ProjectionBaseline baseline{};
if (!ProjectionBaselineContract::TrySelect(
view.job, view.grow, &baseline)) {
Log("dynamic-aic blocked reason=baseline-map-miss source=%u job=%u grow=0x%02X",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(view.job),
static_cast<unsigned int>(view.grow));
return false;
}
const std::uint32_t baselineJob =
*reinterpret_cast<std::uint32_t*>(dest + 0x0C);
const std::uint32_t baselineGrow =
*reinterpret_cast<std::uint32_t*>(dest + 0x10);
if (baselineJob !=
static_cast<std::uint32_t>(baseline.aicJob) ||
baselineGrow !=
static_cast<std::uint32_t>(baseline.aicGrow)) {
Log("dynamic-aic blocked reason=baseline-job-grow-mismatch source=%u source_job=%u source_grow=0x%02X expected_aic=%u/%u actual_aic=%u/%u apc=%d",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(view.job),
static_cast<unsigned int>(view.grow),
static_cast<unsigned int>(baseline.aicJob),
static_cast<unsigned int>(baseline.aicGrow),
static_cast<unsigned int>(baselineJob),
static_cast<unsigned int>(baselineGrow),
baseline.descriptor.apcCode);
return false;
}
if (ProjectionGrowBridgeContract::ShouldApplyFirstGrow(
baseline)) {
*reinterpret_cast<std::uint32_t*>(
dest + 0x10) =
static_cast<std::uint32_t>(
baseline.sourceGrow);
Log("dynamic-aic grow bridge source=%u job=%u aic_grow=%u source_grow=%u apc=%d",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(baseline.sourceJob),
static_cast<unsigned int>(baseline.aicGrow),
static_cast<unsigned int>(baseline.sourceGrow),
baseline.descriptor.apcCode);
}
if (ProjectionGrowBridgeContract::ShouldApplyAwakening(
baseline, view.grow)) {
const std::uint8_t awakening =
ProjectionGrowBridgeContract::AwakeningStage(
view.grow);
*reinterpret_cast<std::uint32_t*>(
dest + 0x14) =
static_cast<std::uint32_t>(awakening);
Log("dynamic-aic awakening bridge source=%u job=%u packed_grow=0x%02X awakening=%u apc=%d",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(baseline.sourceJob),
static_cast<unsigned int>(view.grow),
static_cast<unsigned int>(awakening),
baseline.descriptor.apcCode);
}
for (std::size_t i = 0; i < view.equipmentCount; ++i) {
if (dnf90_companion_projection::Read32(
view.equipmentIds + i * 4) == 0) {
return false;
}
}
for (std::size_t i = 0; i < view.quickSkillCount; ++i) {
const std::uint32_t id =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8);
const std::uint32_t level =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8 + 4);
// Quickbar projection is positional. Empty A/S/D/F/G/H or
// Q/W/E/R/T/Y slots are encoded as 0/0 so later slots do not
// shift left. A half-empty pair is invalid.
if (!ProjectionQuickSkillContract::PairValid(
id, level)) {
return false;
}
}
for (std::size_t i = 0; i < view.skillCount; ++i) {
if (dnf90_companion_projection::Read32(
view.skills + i * 8) == 0 ||
dnf90_companion_projection::Read32(
view.skills + i * 8 + 4) == 0) {
return false;
}
}
std::uintptr_t equipBegin = 0, equipEnd = 0, equipCapacity = 0;
std::uintptr_t quickBegin = 0, quickEnd = 0, quickCapacity = 0;
std::uintptr_t skillBegin = 0, skillEnd = 0, skillCapacity = 0;
bool equipGrew = false;
bool quickGrew = false;
bool skillGrew = false;
if (!EnsureNativeVectorCapacity(
dest,
g_native.kAicEquipmentVectorObjectOffset,
12u,
view.equipmentCount,
g_native.kNativeEquipmentVectorAllocateRva,
&equipBegin, &equipEnd, &equipCapacity,
&equipGrew) ||
!EnsureNativeVectorCapacity(
dest,
g_native.kAicQuickSkillVectorObjectOffset,
8u,
view.quickSkillCount,
g_native.kNativeSkillVectorAllocateRva,
&quickBegin, &quickEnd, &quickCapacity,
&quickGrew) ||
!EnsureNativeVectorCapacity(
dest,
g_native.kAicSkillVectorObjectOffset,
8u,
view.skillCount,
g_native.kNativeSkillVectorAllocateRva,
&skillBegin, &skillEnd, &skillCapacity,
&skillGrew)) {
Log("dynamic-aic blocked reason=vector-resize source=%u equipment=%zu quick=%zu skills=%zu",
static_cast<unsigned int>(view.sourceCharacter),
view.equipmentCount,
view.quickSkillCount,
view.skillCount);
return false;
}
if (!AssignNativeRefWideString(dest + 8, name)) {
Log("dynamic-aic blocked reason=name-assign source=%u dest=%p",
static_cast<unsigned int>(view.sourceCharacter),
destination);
return false;
}
const std::size_t oldEquipmentCount =
(equipEnd - equipBegin) / 12u;
std::uint8_t* equipmentRows =
reinterpret_cast<std::uint8_t*>(equipBegin);
for (std::size_t i = 0; i < view.equipmentCount; ++i) {
const std::uint32_t itemID =
dnf90_companion_projection::Read32(
view.equipmentIds + i * 4);
if (itemID == 0) return false;
std::uint32_t* row =
reinterpret_cast<std::uint32_t*>(
equipmentRows + i * 12u);
row[0] = itemID;
row[1] = 0;
row[2] = 0;
}
if (oldEquipmentCount > view.equipmentCount) {
memset(equipmentRows + view.equipmentCount * 12u, 0,
(oldEquipmentCount - view.equipmentCount) * 12u);
}
*reinterpret_cast<std::uintptr_t*>(
dest + g_native.kAicEquipmentVectorOffset + 4) =
equipBegin + view.equipmentCount * 12u;
const std::size_t oldQuickCount =
(quickEnd - quickBegin) / 8u;
std::uint8_t* quickRows =
reinterpret_cast<std::uint8_t*>(quickBegin);
for (std::size_t i = 0; i < view.quickSkillCount; ++i) {
const std::uint32_t id =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8);
const std::uint32_t level =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8 + 4);
if (!ProjectionQuickSkillContract::PairValid(
id, level)) return false;
std::uint32_t* row =
reinterpret_cast<std::uint32_t*>(
quickRows + i * 8u);
row[0] = id;
row[1] = level;
}
if (oldQuickCount > view.quickSkillCount) {
memset(quickRows + view.quickSkillCount * 8u, 0,
(oldQuickCount - view.quickSkillCount) * 8u);
}
*reinterpret_cast<std::uintptr_t*>(
dest + g_native.kAicQuickSkillVectorOffset + 4) =
quickBegin + view.quickSkillCount * 8u;
const std::size_t oldSkillCount =
(skillEnd - skillBegin) / 8u;
std::uint8_t* skillRows =
reinterpret_cast<std::uint8_t*>(skillBegin);
for (std::size_t i = 0; i < view.skillCount; ++i) {
const std::uint32_t id =
dnf90_companion_projection::Read32(
view.skills + i * 8);
const std::uint32_t level =
dnf90_companion_projection::Read32(
view.skills + i * 8 + 4);
if (id == 0 || level == 0) return false;
std::uint32_t* row =
reinterpret_cast<std::uint32_t*>(
skillRows + i * 8u);
row[0] = id;
row[1] = level;
}
if (oldSkillCount > view.skillCount) {
memset(skillRows + view.skillCount * 8u, 0,
(oldSkillCount - view.skillCount) * 8u);
}
*reinterpret_cast<std::uintptr_t*>(
dest + g_native.kAicSkillVectorOffset + 4) =
skillBegin + view.skillCount * 8u;
const std::uint32_t templateAfter =
ProjectionTemplateFingerprint(source);
if (templateAfter == 0 || templateAfter != templateBefore) {
Log("dynamic-aic blocked reason=template-mutated before=%08X after=%08X",
templateBefore, templateAfter);
return false;
}
Log("dynamic-aic applied source=%u level=%u source_job=%u source_grow=0x%02X aic_job=%u aic_grow=%u bridged=%u equipment=%zu quick=%zu skills=%zu capacities=%zu/%zu/%zu grew=%u/%u/%u dest=%p source_template=%p template_mutation=0",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(view.level),
static_cast<unsigned int>(view.job),
static_cast<unsigned int>(view.grow),
static_cast<unsigned int>(baseline.aicJob),
static_cast<unsigned int>(baseline.aicGrow),
baseline.bridged ? 1u : 0u,
view.equipmentCount,
view.quickSkillCount,
view.skillCount,
equipBegin && equipCapacity >= equipBegin
? (equipCapacity - equipBegin) / 12u : 0u,
quickBegin && quickCapacity >= quickBegin
? (quickCapacity - quickBegin) / 8u : 0u,
skillBegin && skillCapacity >= skillBegin
? (skillCapacity - skillBegin) / 8u : 0u,
equipGrew ? 1u : 0u,
quickGrew ? 1u : 0u,
skillGrew ? 1u : 0u,
destination,
source);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("dynamic-aic apply exception code=0x%08X",
GetExceptionCode());
return false;
}
}
void SetOriginalAicCopy(void* original) {
g_originalAicCopy =
reinterpret_cast<AicCopyFn>(original);
Log("projection AIC copy hook original=%p enabled=%u",
original, DirectOp40Enabled() ? 1u : 0u);
}
void* __fastcall ProxyAicCopy(
void* destination,
void* /*unusedEdx*/,
const void* source)
{
AicCopyFn original = g_originalAicCopy;
if (!original) return destination;
void* result = original(destination, source);
if (!DynamicAicApplyContract::Matches(
g_projectionPending,
g_directLoopbackInReceive,
g_projectionAicApplied,
GetCurrentThreadId(),
g_armedThread,
destination,
source)) {
return result;
}
dnf90_companion_projection::View view{};
if (!dnf90_companion_projection::DecodePacket(
g_projectionPacket,
g_projectionPacketLength,
&view)) {
Log("dynamic-aic blocked reason=projection-decode");
return result;
}
if (ApplyDynamicAicProjection(
destination, source, view)) {
// Stage 1 is complete, but keep the cached projection pending.
// The registered APC object key is only known later in the same native
// op40 transaction; Stage 2 uses mode0 at the post-registration safe
// point to update scene/name state for that exact actor.
g_projectionAicApplied = true;
}
return result;
}
void SetOriginalNetworkRouter(void* original) {
g_originalNetworkRouter =
reinterpret_cast<NetworkRouterFn>(original);
Log("projection network-router hook original=%p enabled=%u",
original, DirectOp40Enabled() ? 1u : 0u);
}
int __fastcall ProxyNetworkRouter(
void* self,
void* /*unusedEdx*/,
std::int32_t sourceContext,
std::uint8_t* packet,
std::int32_t totalLength)
{
NetworkRouterFn original = g_originalNetworkRouter;
if (!original) {
return static_cast<int>(0x80000001u);
}
const std::size_t available =
totalLength > 0 ? static_cast<std::size_t>(totalLength) : 0;
bool privateProjection = false;
__try {
privateProjection =
Enabled() &&
available <= kProjectionPacketCapacity &&
dnf90_companion_projection::IsPrivateNetworkPacketHeader(
packet, available);
} __except (EXCEPTION_EXECUTE_HANDLER) {
privateProjection = false;
}
// The socket router receives the protected outer packet. The native
// router decodes/normalizes it before entering the class0 dispatcher;
// projection capture is therefore performed by ProxyClass0Dispatch,
// where the body is already plaintext and the same call stack still owns
// the AIC/APC construction transaction.
const int result = original(self, sourceContext, packet, totalLength);
if (!privateProjection) {
return result;
}
Log("projection private packet routed to class0 source_context=%d length=%d native_result=0x%08X",
sourceContext, totalLength, static_cast<unsigned int>(result));
return result;
}
// Direct-key programs are compiled per client build: the retail EXE emits
// 0x180-byte bodies with the skill code at +0x100, DB4 emits 0x1BC at +0x128,
// and the currently deployed client emits 0x200-byte bodies whose skill-code
// slot matches neither constant. Treat the body length as a range and locate
// the code slot by content, so no PVF-side key rewrite is required.
constexpr std::size_t kMinDirectKeyBytecodeSize = 0x80;
// Program bodies are not uniform: the shell AIC ships 512-byte key programs
// next to 1152-byte combo/command bodies, so only a generous sanity bound is
// enforced here. The skill-code field position is what the rebinding relies on.
constexpr std::size_t kMaxDirectKeyBytecodeSize = 0x4000;
std::size_t FindDirectKeySkillCodeOffset(
std::uint8_t* bytecode,
std::size_t size,
std::uint32_t expected)
{
if (bytecode && size >= 4 && expected != 0) {
for (std::size_t offset = 0; offset + 4 <= size; offset += 4) {
if (*reinterpret_cast<std::uint32_t*>(bytecode + offset) == expected) {
return offset;
}
}
}
return g_native.kRuntimeKeySkillCodeOffset;
}
// Bind the source quickbar to the native key programs. Mode selection belongs
// to the owned VM predicates below, separately from the key identities.
bool ApplyDynamicRuntimeKeyPrograms(
void* actor,
const dnf90_companion_projection::View& view,
const std::uint32_t* previousSkills = nullptr)
{
using Contract = ProjectionRuntimeKeyContract;
if (!actor ||
view.quickSkillCount != Contract::kQuickSlotCount) {
return false;
}
__try {
std::uint8_t* actorBytes =
reinterpret_cast<std::uint8_t*>(actor);
std::uint8_t* aic =
*reinterpret_cast<std::uint8_t**>(
actorBytes + g_native.kActorAicOffset);
if (!aic) {
Log("runtime-key projection blocked reason=aic-null actor=%p",
actor);
return false;
}
std::uintptr_t begin =
*reinterpret_cast<std::uintptr_t*>(
aic + g_native.kAicRuntimeKeyProgramVectorOffset);
std::uintptr_t end =
*reinterpret_cast<std::uintptr_t*>(
aic + g_native.kAicRuntimeKeyProgramVectorOffset + 4);
std::uintptr_t capacity =
*reinterpret_cast<std::uintptr_t*>(
aic + g_native.kAicRuntimeKeyProgramVectorOffset + 8);
if (!begin || end < begin || capacity < end ||
((end - begin) % sizeof(void*)) != 0 ||
((capacity - begin) % sizeof(void*)) != 0) {
Log("runtime-key projection blocked reason=program-vector-shape aic=%p begin=%p end=%p cap=%p",
aic,
reinterpret_cast<void*>(begin),
reinterpret_cast<void*>(end),
reinterpret_cast<void*>(capacity));
return false;
}
const std::size_t programCount =
(end - begin) / sizeof(void*);
void** programs =
reinterpret_cast<void**>(begin);
if (!Contract::ProgramCountValid(programCount)) {
Log("runtime-key projection skipped reason=program-count count=%zu expected=%u..%u aic=%p",
programCount,
Contract::kRequiredProgramCount,
Contract::kMaxCompatibleProgramCount,
aic);
// Shape probe: the runtime synthesizer needs the AIC's real program
// object layout when a shell ships fewer than 25 key programs.
for (std::size_t probe = 0; probe < programCount && probe < 3; ++probe) {
std::uint8_t* probeProgram =
reinterpret_cast<std::uint8_t*>(programs[probe]);
if (!probeProgram) continue;
const std::uintptr_t probeBegin =
*reinterpret_cast<std::uintptr_t*>(
probeProgram + g_native.kRuntimeKeyProgramBytecodeBeginOffset);
const std::uintptr_t probeEnd =
*reinterpret_cast<std::uintptr_t*>(
probeProgram + g_native.kRuntimeKeyProgramBytecodeEndOffset);
const std::size_t probeSize =
probeEnd >= probeBegin ? probeEnd - probeBegin : 0;
Log("runtime-key probe index=%zu program=%p next=%p size=%zu code100=0x%08X code128=0x%08X",
probe,
probeProgram,
probe + 1 < programCount ? programs[probe + 1] : nullptr,
probeSize,
probeSize > 0x104 ? *reinterpret_cast<std::uint32_t*>(probeBegin + 0x100) : 0,
probeSize > 0x12C ? *reinterpret_cast<std::uint32_t*>(probeBegin + 0x128) : 0);
}
return false;
}
// 关闭普攻时把 CompanionBasicAttack 的技能码改成空占位码,
// 只禁用技能冷却后的普攻回退,不触碰 0..11 的 12 格技能程序。
if (!CompanionBasicAttackEnabled()) {
void* basicProgram =
programs[Contract::kBasicAttackProgramIndex];
std::uint8_t* basic =
reinterpret_cast<std::uint8_t*>(basicProgram);
if (basic) {
std::uintptr_t basicBegin =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeBeginOffset);
std::uintptr_t basicEnd =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeEndOffset);
std::uintptr_t basicCapacity =
*reinterpret_cast<std::uintptr_t*>(
basic +
g_native.kRuntimeKeyProgramBytecodeCapacityOffset);
if (basicBegin && basicEnd >= basicBegin &&
basicCapacity >= basicEnd &&
basicEnd - basicBegin >=
g_native.kRuntimeKeySkillCodeOffset + 4) {
*reinterpret_cast<std::uint32_t*>(
basicBegin +
g_native.kRuntimeKeySkillCodeOffset) =
Contract::kSkillCodeBias +
Contract::kPlaceholderSkillId;
Log("companion basic attack disabled actor=%p program=%p",
actor,
basicProgram);
}
}
}
std::uint8_t* directBytecode[
Contract::kQuickSlotCount] = {};
std::size_t directBytecodeSize[
Contract::kQuickSlotCount] = {};
std::size_t directSkillCodeOffset[
Contract::kQuickSlotCount] = {};
std::uint8_t* directPrograms[Contract::kQuickSlotCount] = {};
std::uint32_t directCooldown[Contract::kQuickSlotCount] = {};
std::uint32_t encodedSkills[
Contract::kQuickSlotCount] = {};
const std::uint32_t placeholderCode =
Contract::kSkillCodeBias +
Contract::kPlaceholderSkillId;
// Validate the entire direct pool before mutating anything.
for (std::size_t i = 0;
i < Contract::kQuickSlotCount;
++i) {
if (!programs[i] ||
!programs[Contract::kDirectPoolBase + i] ||
programs[i] ==
programs[Contract::kDirectPoolBase + i]) {
Log("runtime-key projection blocked reason=program-null-or-alias slot=%zu front=%p direct=%p",
i,
programs[i],
programs[Contract::kDirectPoolBase + i]);
return false;
}
const bool currentlyBound =
previousSkills && previousSkills[i] != 0;
const std::size_t directIndex = currentlyBound
? i : Contract::kDirectPoolBase + i;
std::uint8_t* program =
reinterpret_cast<std::uint8_t*>(
programs[directIndex]);
std::uintptr_t codeBegin =
*reinterpret_cast<std::uintptr_t*>(
program +
g_native.kRuntimeKeyProgramBytecodeBeginOffset);
std::uintptr_t codeEnd =
*reinterpret_cast<std::uintptr_t*>(
program +
g_native.kRuntimeKeyProgramBytecodeEndOffset);
std::uintptr_t codeCapacity =
*reinterpret_cast<std::uintptr_t*>(
program +
g_native.kRuntimeKeyProgramBytecodeCapacityOffset);
const std::size_t codeSize =
codeEnd >= codeBegin
? static_cast<std::size_t>(codeEnd - codeBegin) : 0;
if (!codeBegin ||
codeEnd < codeBegin ||
codeCapacity < codeEnd ||
codeSize < kMinDirectKeyBytecodeSize ||
codeSize > kMaxDirectKeyBytecodeSize) {
Log("runtime-key projection blocked reason=direct-bytecode-shape slot=%zu program=%p begin=%p end=%p cap=%p size=%zu",
i,
program,
reinterpret_cast<void*>(codeBegin),
reinterpret_cast<void*>(codeEnd),
reinterpret_cast<void*>(codeCapacity),
codeSize);
return false;
}
std::uint8_t* code =
reinterpret_cast<std::uint8_t*>(
codeBegin);
if (i == 0) {
// One-shot layout dump: the shell's original key programs do not
// follow the placeholder contract, so record the real field
// layout next to the quickbar the projection wants to bind.
Log("runtime-key dump slot=0 size=%zu quick=%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u,%u",
codeSize,
dnf90_companion_projection::Read32(view.quickSkills + 0),
dnf90_companion_projection::Read32(view.quickSkills + 8),
dnf90_companion_projection::Read32(view.quickSkills + 16),
dnf90_companion_projection::Read32(view.quickSkills + 24),
dnf90_companion_projection::Read32(view.quickSkills + 32),
dnf90_companion_projection::Read32(view.quickSkills + 40),
dnf90_companion_projection::Read32(view.quickSkills + 48),
dnf90_companion_projection::Read32(view.quickSkills + 56),
dnf90_companion_projection::Read32(view.quickSkills + 64),
dnf90_companion_projection::Read32(view.quickSkills + 72),
dnf90_companion_projection::Read32(view.quickSkills + 80),
dnf90_companion_projection::Read32(view.quickSkills + 88));
for (std::size_t row = 0; row < 8; ++row) {
const std::size_t base = row * 0x28;
if (base + 0x28 > codeSize) break;
Log("runtime-key dump +0x%02zX: %08X %08X %08X %08X %08X %08X %08X %08X %08X %08X",
base,
*reinterpret_cast<std::uint32_t*>(code + base + 0x00),
*reinterpret_cast<std::uint32_t*>(code + base + 0x04),
*reinterpret_cast<std::uint32_t*>(code + base + 0x08),
*reinterpret_cast<std::uint32_t*>(code + base + 0x0C),
*reinterpret_cast<std::uint32_t*>(code + base + 0x10),
*reinterpret_cast<std::uint32_t*>(code + base + 0x14),
*reinterpret_cast<std::uint32_t*>(code + base + 0x18),
*reinterpret_cast<std::uint32_t*>(code + base + 0x1C),
*reinterpret_cast<std::uint32_t*>(code + base + 0x20),
*reinterpret_cast<std::uint32_t*>(code + base + 0x24));
}
}
const std::uint32_t expectedCode = currentlyBound
? Contract::kSkillCodeBias + previousSkills[i] : placeholderCode;
const std::size_t codeOffset =
g_native.kRuntimeKeySkillCodeOffset;
// The shell AIC's original key programs do not carry the projection
// placeholder: the slot holds either an encoded skill (0x100 + id,
// e.g. 0x1A9 = skill 169) or an unrelated value on command-style
// programs (e.g. 0x3E). The field position is what matters, so the
// observed value is recorded and overwritten instead of gating.
const std::uint32_t observedPlaceholder =
*reinterpret_cast<std::uint32_t*>(
code + codeOffset);
if (observedPlaceholder != expectedCode && i == 0) {
Log("runtime-key rebinding slot=%zu observed=0x%08X expected=0x%08X offset=0x%zX size=%zu",
i,
observedPlaceholder,
expectedCode,
codeOffset,
codeSize);
}
directBytecode[i] = code;
directBytecodeSize[i] = codeSize;
directSkillCodeOffset[i] = codeOffset;
directPrograms[i] = program;
const std::uint32_t skillId =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8);
const std::uint32_t skillLevel =
dnf90_companion_projection::Read32(
view.quickSkills + i * 8 + 4);
if (!ProjectionQuickSkillContract::PairValid(
skillId, skillLevel)) {
Log("runtime-key projection blocked reason=quick-pair slot=%zu id=%u level=%u",
i,
skillId,
skillLevel);
return false;
}
if (skillId != 0 &&
!Contract::EncodeSkill(
skillId,
&encodedSkills[i])) {
Log("runtime-key projection blocked reason=skill-encode slot=%zu id=%u",
i,
skillId);
return false;
}
directCooldown[i] = dnf90_companion_quickbar::CooldownMS(view.job, skillId);
if (!directCooldown[i]) {
auto* front = reinterpret_cast<std::uint8_t*>(programs[currentlyBound
? Contract::kDirectPoolBase + i : i]);
directCooldown[i] = *reinterpret_cast<std::uint32_t*>(front + 0x64);
}
}
std::size_t applied = 0;
for (std::size_t i = 0;
i < Contract::kQuickSlotCount;
++i) {
const bool previouslyBound = previousSkills && previousSkills[i] != 0;
const bool shouldBind = encodedSkills[i] != 0;
const std::uint32_t targetCode =
shouldBind ? encodedSkills[i] : placeholderCode;
*reinterpret_cast<std::uint32_t*>(
directBytecode[i] +
directSkillCodeOffset[i]) =
targetCode;
// Native 262B040 reads duration at +0x64 and compares it with
// +0x68, which 262B1A0 updates when the game accepts an action.
// Swapping the direct program must carry its cooldown as well.
*reinterpret_cast<std::uint32_t*>(directPrograms[i] + 0x64) = directCooldown[i];
if (previouslyBound != shouldBind) {
void* originalFront = programs[i];
programs[i] = programs[Contract::kDirectPoolBase + i];
programs[Contract::kDirectPoolBase + i] = originalFront;
}
++applied;
}
Log("runtime-key direct projection applied actor=%p aic=%p slots=%zu programs=%zu placeholder=%u size=%zu code_offset=0x%zX",
actor,
aic,
applied,
programCount,
Contract::kPlaceholderSkillId,
directBytecodeSize[0],
directSkillCodeOffset[0]);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("runtime-key projection exception actor=%p code=0x%08X",
actor,
GetExceptionCode());
return false;
}
}
bool BindAreaCooldownChain(LiveProjectionActor& entry, std::uint8_t* node, AIParameterFn parameter) {
// Reverse 11..0 chain is reserved for crowd selection in the generated
// archive. Validate all nodes before allowing purpose filtering there.
std::uint8_t* chain[12] = {};
for (int slot = 11; slot >= 0; --slot) {
if (!node || *reinterpret_cast<void**>(node + 8) != entry.aiVm ||
*reinterpret_cast<unsigned*>(node + 0x1C) != 57 ||
*reinterpret_cast<unsigned*>(node + 0x58) != 1 ||
*reinterpret_cast<int*>(node + 0x0C) != slot ||
*reinterpret_cast<void**>(node + 0x14)) return false;
int zero = 0;
const auto* index = parameter(node + 0x4C, &zero);
if (!index || *index != static_cast<float>(slot)) return false;
chain[slot] = node;
node = *reinterpret_cast<std::uint8_t**>(node + 0x18);
}
for (auto* cooldown : chain) {
bool duplicate = false;
for (std::size_t i = 0; i < entry.areaCooldownCount; ++i)
if (entry.areaCooldowns[i] == cooldown) duplicate = true;
if (!duplicate) {
if (entry.areaCooldownCount >= 128) return false;
entry.areaCooldowns[entry.areaCooldownCount++] = cooldown;
}
}
return true;
}
bool BindModeConditions(LiveProjectionActor& entry, std::uint8_t* node,
AIParameterFn parameter, unsigned int depth, unsigned int& nodes) {
if (!node) return true;
if (depth > 64 || ++nodes > 512 || *reinterpret_cast<void**>(node + 8) != entry.aiVm) return false;
if (*reinterpret_cast<unsigned*>(node + 0x1C) == 57 && *reinterpret_cast<int*>(node + 0x0C) == 11) {
auto* next = *reinterpret_cast<std::uint8_t**>(node + 0x18);
if (next && *reinterpret_cast<unsigned*>(next + 0x1C) == 57 && *reinterpret_cast<int*>(next + 0x0C) == 10 &&
!BindAreaCooldownChain(entry, node, parameter)) return false;
}
if (*reinterpret_cast<unsigned*>(node + 0x1C) == 4 && *reinterpret_cast<unsigned*>(node + 0x58) == 1) {
int zero = 0;
float* state = parameter(node + 0x4C, &zero);
if (!state) return false;
if (*state == 0) {
bool duplicate = false;
for (std::size_t i = 0; i < entry.modeConditionCount; ++i)
if (entry.modeConditions[i] == node) duplicate = true;
if (!duplicate) {
if (entry.modeConditionCount >= 8) return false;
entry.modeConditions[entry.modeConditionCount++] = node;
}
}
}
return BindModeConditions(entry, *reinterpret_cast<std::uint8_t**>(node + 0x14), parameter, depth + 1, nodes) &&
BindModeConditions(entry, *reinterpret_cast<std::uint8_t**>(node + 0x18), parameter, depth + 1, nodes);
}
bool IsConditionTreeVm(const void* vm, uintptr_t base) {
if (!vm) return false;
if (!g_native.newNative) return true;
const auto* bytes = static_cast<const unsigned char*>(vm);
return *reinterpret_cast<const uintptr_t*>(bytes) ==
base + g_native.kAIControlledVmVtableRva &&
*reinterpret_cast<const unsigned*>(bytes + 0x664) == 0;
}
bool BindQuickbarPolicy(LiveProjectionActor& entry) {
if (!g_originalAICondition || !entry.keysReady || !dnf90_companion_quickbar::SupportsQuickbarPolicy(entry.job)) return false;
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
static const unsigned char parameterSignature[] = {0x55,0x8B,0xEC,0x6A,0xFF,0x68,0xD8,0x3E,0xCD,0x03};
if (!g_native.newNative && memcmp(reinterpret_cast<void*>(base + g_native.kAIParameterRva), parameterSignature, sizeof(parameterSignature))) return false;
const auto parameter = reinterpret_cast<AIParameterFn>(base + g_native.kAIParameterRva);
auto* vm = *reinterpret_cast<std::uint8_t**>(reinterpret_cast<std::uint8_t*>(entry.actor) + g_native.kActorVmOffset);
// The alternate native VM stores unrelated data at the roots offsets.
if (!IsConditionTreeVm(vm, base)) return false;
const auto begin = *reinterpret_cast<uintptr_t*>(vm + 0x6CC);
const auto end = *reinterpret_cast<uintptr_t*>(vm + 0x6D0);
if (!begin || end < begin || end - begin != 4 * sizeof(void*)) return false;
auto* roots = reinterpret_cast<std::uint8_t**>(begin);
auto* state = roots[2];
if (!state || *reinterpret_cast<unsigned*>(state + 0x1C) != 4) return false;
auto* party = *reinterpret_cast<std::uint8_t**>(state + 0x14);
if (!party || *reinterpret_cast<unsigned*>(party + 0x1C) != 47) return false;
auto* buff = *reinterpret_cast<std::uint8_t**>(party + 0x18);
// The current EXE uses a checked STL map at +0x4C: its count is
// map+0x0C (+0x58 in the node), not the unchecked map layout at +0x50.
// Verified against the live parsed BUFF/cooldown/HP nodes and getter.
constexpr std::size_t floatMapCountOffset = 0x58;
if (!buff || *reinterpret_cast<unsigned*>(buff + 0x1C) != 52 ||
*reinterpret_cast<void**>(buff + 8) != vm || *reinterpret_cast<unsigned*>(buff + floatMapCountOffset) != 2) return false;
auto* cooldown = *reinterpret_cast<std::uint8_t**>(buff + 0x14);
if (!cooldown || *reinterpret_cast<unsigned*>(cooldown + 0x1C) != 57 ||
*reinterpret_cast<unsigned*>(cooldown + floatMapCountOffset) != 1 || *reinterpret_cast<int*>(cooldown + 0x0C) != 1) return false;
int zero = 0, one = 1;
float* family = parameter(buff + 0x4C, &zero);
float* skill = parameter(buff + 0x4C, &one);
float* slot = parameter(cooldown + 0x4C, &zero);
// All current stable APCs share this verified placeholder grammar. The
// owned selector supplies actual profession and learned quickbar roles.
if (!family || !skill || !slot || *family != 4 || *skill != 7 || *slot != 1) return false;
auto* heal = roots[0];
if (!heal || *reinterpret_cast<void**>(heal + 8) != vm ||
*reinterpret_cast<unsigned*>(heal + 0x1C) != 12 ||
*reinterpret_cast<std::uint16_t*>(heal + 0x20) != 0x31 ||
*reinterpret_cast<unsigned*>(heal + floatMapCountOffset) != 1 ||
*reinterpret_cast<int*>(heal + 0x0C) != -1 || *reinterpret_cast<int*>(heal + 0x10) != -1 ||
*reinterpret_cast<void**>(heal + 0x14) || *reinterpret_cast<void**>(heal + 0x18)) return false;
float* threshold = parameter(heal + 0x4C, &zero);
if (!threshold || *threshold <= 0 || *threshold > 100 || *reinterpret_cast<unsigned*>(heal + 0x1C) == 4) return false;
entry.aiVm = vm;
entry.buffCondition = buff;
entry.buffCooldown = cooldown;
entry.healCondition = heal;
entry.actionStateCondition = state;
entry.buffSkillParameter = skill;
entry.buffSlotParameter = slot;
entry.healThresholdParameter = threshold;
// Destination, movement and combat use parsed state-0 mode predicates.
// Bind only this APC's VM nodes; keep native state-4 cast protection.
if (!roots[1] || !roots[3] || *reinterpret_cast<unsigned*>(roots[1] + 0x1C) != 4 ||
*reinterpret_cast<unsigned*>(roots[3] + 0x1C) != 4) return false;
unsigned int nodes = 0;
for (const auto lane : {1, 2, 3})
if (!BindModeConditions(entry, roots[lane], parameter, 0, nodes)) return false;
if (entry.modeConditionCount < 4 || entry.areaCooldownCount < 12) return false;
entry.quickbarPolicyReady = true;
Log("quickbar policy bound source=%u actor=%p vm=%p job=%u grow=%u heal_threshold=%.0f behavior=%u mode_nodes=%zu area_nodes=%zu",
entry.source, entry.actor, vm, entry.job, entry.grow, *threshold, entry.aiBehavior, entry.modeConditionCount, entry.areaCooldownCount);
return true;
}
void RememberLiveProjectionActor(void* owner, void* actor,
std::uint16_t key, const dnf90_companion_projection::View& view, bool keysReady) {
LiveProjectionActor entry{};
entry.source = view.sourceCharacter;
entry.key = key;
entry.level = view.level;
entry.revision = view.revision;
entry.actor = actor;
entry.owner = owner;
entry.aic = *reinterpret_cast<void**>(reinterpret_cast<std::uint8_t*>(actor) + g_native.kActorAicOffset);
// Keep the owning condition VM even when the optional, resource-specific
// support selector cannot bind. The generic cooldown guard below still
// has enough information to reject empty/unlearned quickbar slots and
// prevents a stock AIC from falling back to its PVF quick-skill list.
entry.aiVm = *reinterpret_cast<void**>(reinterpret_cast<std::uint8_t*>(actor) + g_native.kActorVmOffset);
entry.aiBehavior = view.aiBehavior;
entry.aiComboPresetHash = view.aiComboPresetHash;
entry.aiClass = view.aiClass;
entry.aiComboEnabled = view.aiComboEnabled;
entry.keysReady = keysReady;
entry.job = view.job;
entry.grow = view.grow;
for (std::size_t i = 0; i < view.skillCount; ++i) {
const auto id = dnf90_companion_projection::Read32(view.skills + i * 8);
if (id < 512) entry.skills[id] = dnf90_companion_projection::Read32(view.skills + i * 8 + 4);
}
for (std::size_t i = 0; i < view.quickSkillCount && i < 12; ++i)
entry.quick[i] = dnf90_companion_projection::Read32(view.quickSkills + i * 8);
__try { BindQuickbarPolicy(entry); }
__except (EXCEPTION_EXECUTE_HANDLER) {
entry.quickbarPolicyReady = false;
Log("quickbar policy binding failed source=%u code=0x%08X", entry.source, GetExceptionCode());
}
for (auto& previous : g_liveProjectionActors) {
if (previous.source == entry.source) { previous = entry; return; }
}
if (g_liveProjectionActors.size() < 64) g_liveProjectionActors.push_back(entry);
}
// Room transitions migrate the same APC into the owner's new manager. Resolve
// membership from the current owner, never the manager saved at registration.
// sub_25BCC90 inserts actors into manager+8 (begin +0x0C, end +0x10); +0xBC
// is updated by vfunc +0x70, and +0x50 returns the registered composite key.
bool LiveProjectionActorPresent(const LiveProjectionActor& entry) {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
void* owner = nullptr;
int scene = -1;
if (GetCurrentThreadId() != g_armedThread ||
!ReadControlledActor(base, &owner) || owner != entry.owner || owner != g_armedActor ||
!DungeonSceneReady(base, &scene)) return false;
auto* ownerBytes = reinterpret_cast<std::uint8_t*>(owner);
auto* manager = *reinterpret_cast<std::uint8_t**>(ownerBytes + g_native.kActorOwnerContextOffset);
if (!manager) return false;
const auto begin = *reinterpret_cast<uintptr_t*>(manager + 0x0C);
const auto end = *reinterpret_cast<uintptr_t*>(manager + 0x10);
if (!begin || end < begin || (end - begin) % sizeof(void*) ||
(end - begin) / sizeof(void*) > 4096) return false;
bool found = false;
for (auto cursor = begin; cursor < end; cursor += sizeof(void*)) {
if (*reinterpret_cast<void**>(cursor) == entry.actor) { found = true; break; }
}
if (!found || entry.actor == owner) return false;
auto* actor = reinterpret_cast<std::uint8_t*>(entry.actor);
if (*reinterpret_cast<void**>(actor + g_native.kActorOwnerContextOffset) != manager) return false;
if (*reinterpret_cast<void**>(actor + g_native.kActorAicOffset) != entry.aic) return false;
auto** table = *reinterpret_cast<void***>(actor);
using KeyFn = std::uint32_t (__thiscall*)(void*);
return static_cast<std::uint16_t>(reinterpret_cast<KeyFn>(table[g_native.kActorKeyVtableOffset / 4])(actor)) == entry.key;
}
bool QuickbarProgramReady(const LiveProjectionActor& entry, std::size_t slot) {
if (slot >= 12 || !entry.aic) return false;
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
static const unsigned char coolSignature[] = {0x56,0x8B,0xF1,0x80,0x7E,0x6D,0x00};
static const unsigned char readySignature[] = {0x55,0x8B,0xEC,0x56,0x8B,0xF1,0x83,0x7E,0x4C,0x00};
if (!g_native.newNative && (memcmp(reinterpret_cast<void*>(base + g_native.kAICooldownRva), coolSignature, sizeof(coolSignature)) ||
memcmp(reinterpret_cast<void*>(base + g_native.kAIKeyReadyRva), readySignature, sizeof(readySignature)))) return false;
auto* aic = reinterpret_cast<std::uint8_t*>(entry.aic);
const auto begin = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset);
const auto end = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset + 4);
if (!begin || end < begin || (end - begin) % sizeof(void*) ||
!ProjectionRuntimeKeyContract::ProgramCountValid((end - begin) / sizeof(void*))) return false;
auto* program = reinterpret_cast<std::uint8_t**>(begin)[slot];
if (!program) return false;
const auto code = *reinterpret_cast<uintptr_t*>(program + 8);
const auto codeEnd = *reinterpret_cast<uintptr_t*>(program + 12);
if (!code || codeEnd < code) return false;
const std::size_t codeSize = codeEnd - code;
if (codeSize < kMinDirectKeyBytecodeSize || codeSize > kMaxDirectKeyBytecodeSize) return false;
const std::uint32_t expectedSkillCode = 0x100u + entry.quick[slot];
const std::size_t codeOffset = g_native.kRuntimeKeySkillCodeOffset;
if (*reinterpret_cast<std::uint32_t*>(code + codeOffset) != expectedSkillCode) return false;
const auto cooling = reinterpret_cast<AIConditionFn>(base + g_native.kAICooldownRva);
const auto ready = reinterpret_cast<KeyStepReadyFn>(base + g_native.kAIKeyReadyRva);
return cooling(program) == 0 && ready(program, 2) != 0;
}
void SetOriginalAICondition(void* original) {
g_originalAICondition = reinterpret_cast<AIConditionFn>(original);
Log("quickbar condition hook original=%p", original);
}
std::uint32_t NativeSupportActionTimestamp(const LiveProjectionActor& entry, std::uint32_t id) {
auto* aic = reinterpret_cast<std::uint8_t*>(entry.aic);
const auto begin = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset);
const auto end = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset + 4);
if (!begin || end < begin || (end - begin) % sizeof(void*) ||
!ProjectionRuntimeKeyContract::ProgramCountValid((end - begin) / sizeof(void*))) return 0;
auto** programs = reinterpret_cast<std::uint8_t**>(begin);
std::uint32_t latest = 0;
for (std::size_t slot = 0; slot < 12; ++slot) {
if (entry.quick[slot] == id && programs[slot]) {
const auto started = *reinterpret_cast<std::uint32_t*>(programs[slot] + 0x68);
if (started > latest) latest = started;
}
}
return latest;
}
bool NativeSupportBuffHandled(const LiveProjectionActor& entry, std::uint32_t id) {
if (!dnf90_companion_quickbar::HasNativeBuffCheck(entry.job, id)) {
return dnf90_companion_quickbar::SupportDispatchHandled(entry.job, id, entry.observedClock,
NativeSupportActionTimestamp(entry, id));
}
const float saved = *entry.buffSkillParameter;
auto** recipient = reinterpret_cast<void**>(reinterpret_cast<std::uint8_t*>(entry.aiVm) + 0x4D4);
void* const savedRecipient = *recipient;
bool present = false;
__try {
// Native 262EB80 reads VM+0x4D4. The PVF party selection currently
// resolves to the APC itself; support policy must inspect its owner.
*recipient = entry.owner;
*entry.buffSkillParameter = static_cast<float>(id);
present = g_originalAICondition(entry.buffCondition) != 0;
} __finally {
*entry.buffSkillParameter = saved;
*recipient = savedRecipient;
}
return present;
}
bool NativeLeaderNeedsHealing(const LiveProjectionActor& entry) {
auto** recipient = reinterpret_cast<void**>(reinterpret_cast<std::uint8_t*>(entry.aiVm) + 0x4D4);
void* const savedRecipient = *recipient;
bool needsHealing = false;
__try {
*recipient = entry.owner;
needsHealing = g_originalAICondition(entry.healCondition) != 0;
} __finally {
*recipient = savedRecipient;
}
return needsHealing;
}
bool ReadNativeActorClock(const LiveProjectionActor& entry, std::uint32_t& now) {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
static const unsigned char clockAddress[] = {0x8D,0x41,0x5C,0xC3};
static const unsigned char clockRead[] = {0x55,0x8B,0xEC,0x83,0xEC,0x10,0x56,0x8B,0xF1};
if (!g_native.newNative && (memcmp(reinterpret_cast<void*>(base + g_native.kActorClockRva), clockAddress, sizeof(clockAddress)) ||
memcmp(reinterpret_cast<void*>(base + g_native.kClockReadRva), clockRead, sizeof(clockRead)))) return false;
// Same getter pair used by native key cooldown 262B040. Never decode
// the encrypted timer or maintain an unrelated wall-clock substitute.
using ClockAddressFn = void* (__thiscall*)(void*);
using ClockReadFn = std::uint32_t (__thiscall*)(void*);
auto* clock = reinterpret_cast<ClockAddressFn>(base + g_native.kActorClockRva)(entry.actor);
if (!clock) return false;
now = reinterpret_cast<ClockReadFn>(base + g_native.kClockReadRva)(clock);
return true;
}
int ReadNativeSelfHpPercent(const LiveProjectionActor& entry) {
auto** recipient = reinterpret_cast<void**>(reinterpret_cast<std::uint8_t*>(entry.aiVm) + 0x4D4);
void* const savedRecipient = *recipient;
const float savedThreshold = *entry.healThresholdParameter;
int low = 0, high = 100;
__try {
*recipient = entry.actor;
// Reuse the verified native percentage predicate rather than guessing
// encrypted HP offsets. At most seven comparisons, once per think.
while (low < high) {
const int middle = (low + high + 1) / 2;
*entry.healThresholdParameter = static_cast<float>(middle);
if (g_originalAICondition(entry.healCondition)) high = middle - 1;
else low = middle;
}
} __finally {
*entry.healThresholdParameter = savedThreshold;
*recipient = savedRecipient;
}
return low;
}
bool CompanionStatProbeEnabled() {
// Not cached: the flag may appear after the client started, and this is
// checked at most once per companion actor spawn.
return CompanionFlagFilePresent(L"90CN_stat_probe.enabled");
}
void LogCompanionStatHex(const unsigned char* base, std::size_t bytes) {
char line[256] = {};
std::size_t used = 0;
for (std::size_t i = 0; i < bytes && used + 4 < sizeof(line); ++i) {
used += static_cast<std::size_t>(snprintf(
line + used, sizeof(line) - used, "%02X ", base[i]));
}
Log("stat-probe hex %s", line);
}
struct CompanionStatHit {
char kind[12];
std::uint32_t value;
long long address;
};
struct CompanionStatTarget {
const char* field;
std::uint32_t value;
};
// The blob carries the four dimensions as s16*10, but the client divides them
// by 10 before storing, so both forms are searched; HP/MP are also tried as
// float. Values above 0xFFFF are never matched as u16 (a truncated match is
// pure noise).
int BuildCompanionStatTargets(
std::uint32_t hp, std::uint32_t mp,
std::int16_t str, std::int16_t vit, std::int16_t intel, std::int16_t spr,
CompanionStatTarget* out)
{
int n = 0;
const auto add = [&](const char* field, std::uint32_t value) {
if (value != 0 && n < 24) { out[n].field = field; out[n].value = value; ++n; }
};
add("hp", hp);
add("mp", mp);
add("str", static_cast<std::uint32_t>(str));
add("str", static_cast<std::uint32_t>(str) / 10u);
add("vit", static_cast<std::uint32_t>(vit));
add("vit", static_cast<std::uint32_t>(vit) / 10u);
add("int", static_cast<std::uint32_t>(intel));
add("int", static_cast<std::uint32_t>(intel) / 10u);
add("spr", static_cast<std::uint32_t>(spr));
add("spr", static_cast<std::uint32_t>(spr) / 10u);
return n;
}
bool ClassifyCompanionStatWord(
const unsigned char* p, const CompanionStatTarget* targets, int targetCount,
const char** field, std::uint32_t* value)
{
const std::uint32_t word = *reinterpret_cast<const std::uint32_t*>(p);
const std::uint16_t half = *reinterpret_cast<const std::uint16_t*>(p);
const float single = *reinterpret_cast<const float*>(p);
for (int i = 0; i < targetCount; ++i) {
const std::uint32_t candidate = targets[i].value;
if (word == candidate) {
*field = targets[i].field;
*value = word;
return true;
}
if (candidate <= 0xFFFFu && half == static_cast<std::uint16_t>(candidate)) {
*field = targets[i].field;
*value = half;
return true;
}
if (single == static_cast<float>(candidate)) {
*field = targets[i].field;
*value = candidate;
return true;
}
}
return false;
}
void DumpCompanionStatWindow(std::uintptr_t address) {
__try {
LogCompanionStatHex(reinterpret_cast<const unsigned char*>(address), 0x80);
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("stat-probe hex skipped (unreadable window)");
}
}
// Scan one region for the blob values. A hit is only reported when a second,
// different value sits within +/-0x100 of it: a lone match in an unrelated
// sequential table is almost always a coincidence.
void ScanCompanionStatRegion(
const char* label, std::uintptr_t base, std::uintptr_t back, std::uintptr_t forward,
const CompanionStatTarget* targets, int targetCount)
{
const std::uintptr_t start = base - back;
const std::uintptr_t end = base + forward;
CompanionStatHit hits[96] = {};
int n = 0;
for (std::uintptr_t cursor = start; cursor < end;) {
MEMORY_BASIC_INFORMATION mbi = {};
if (VirtualQuery(reinterpret_cast<LPCVOID>(cursor), &mbi, sizeof(mbi)) != sizeof(mbi)) break;
const std::uintptr_t regionEnd =
reinterpret_cast<std::uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
const bool readable = mbi.State == MEM_COMMIT &&
(mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0 &&
(mbi.Protect & PAGE_GUARD) == 0;
if (readable) {
const std::uintptr_t scanBegin = cursor > start ? cursor : start;
const std::uintptr_t scanEnd = regionEnd < end ? regionEnd : end;
__try {
for (std::uintptr_t addr = scanBegin; addr + 4 <= scanEnd; addr += 2) {
const char* field = nullptr;
std::uint32_t value = 0;
if (ClassifyCompanionStatWord(
reinterpret_cast<const unsigned char*>(addr),
targets, targetCount, &field, &value) && n < 96) {
hits[n].value = value;
hits[n].address = static_cast<long long>(addr);
strncpy_s(hits[n].kind, field, _TRUNCATE);
++n;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
}
}
if (regionEnd <= cursor) break;
cursor = regionEnd;
}
int confirmed = 0;
std::uintptr_t dumpAt = 0;
for (int i = 0; i < n && confirmed < 10; ++i) {
bool ok = false;
for (int j = 0; j < n && !ok; ++j) {
if (j == i || strcmp(hits[i].kind, hits[j].kind) == 0) continue;
const long long d = hits[j].address - hits[i].address;
if (d > -0x100 && d < 0x100) ok = true;
}
if (!ok) continue;
if (dumpAt == 0) dumpAt = static_cast<std::uintptr_t>(hits[i].address);
++confirmed;
Log("stat-probe %s off=%+lld addr=0x%08llX kind=%s value=%u",
label,
static_cast<long long>(hits[i].address) - static_cast<long long>(base),
hits[i].address, hits[i].kind, hits[i].value);
}
Log("stat-probe %s total=%d confirmed=%d", label, n, confirmed);
if (dumpAt) DumpCompanionStatWindow(dumpAt - 0x20);
}
// Scan the actor, its UserInfo, its AIC and its ability object for the values
// that were just written through the 92-byte stat blob. Read-only:
// VirtualQuery validates pages and every read is SEH-guarded.
void ProbeCompanionStatWindow(void* actor) {
if (!actor || !g_hasLastCompanionStatBlob) return;
const auto* blob = g_lastCompanionStatBlob;
const std::uint32_t hp = *reinterpret_cast<const std::uint32_t*>(blob);
const std::uint32_t mp = *reinterpret_cast<const std::uint32_t*>(blob + 4);
const std::int16_t str = *reinterpret_cast<const std::int16_t*>(blob + 8);
const std::int16_t vit = *reinterpret_cast<const std::int16_t*>(blob + 10);
const std::int16_t intel = *reinterpret_cast<const std::int16_t*>(blob + 12);
const std::int16_t spr = *reinterpret_cast<const std::int16_t*>(blob + 14);
Log("stat-probe scan actor=%p hp=%u mp=%u str=%d vit=%d int=%d spr=%d",
actor, hp, mp, str, vit, intel, spr);
CompanionStatTarget targets[24] = {};
const int targetCount =
BuildCompanionStatTargets(hp, mp, str, vit, intel, spr, targets);
ScanCompanionStatRegion("actor", reinterpret_cast<std::uintptr_t>(actor),
0x4000, 0x200000, targets, targetCount);
// Legacy 2018 base-table slots: +0xB9C = 装备属性, +0x646C = 物品栏,
// +0x6F54 = actor VM. Dump the bytes at each slot verbatim and, when the
// slot holds a plausible pointer, scan around the pointed object too.
{
struct LegacySlot {
const char* name;
std::size_t offset;
};
const LegacySlot legacy[] = {
{"equipAttrs", 0xB9C},
{"inventory", 0x646C},
{"vm", 0x6F54},
};
for (const auto& slot : legacy) {
std::uintptr_t target = 0;
__try {
const auto* at =
reinterpret_cast<const unsigned char*>(actor) + slot.offset;
target = *reinterpret_cast<const std::uint32_t*>(at);
LogCompanionStatHex(at, 0x40);
} __except (EXCEPTION_EXECUTE_HANDLER) {
target = 0;
}
Log("stat-probe legacy %s +0x%X target=0x%08X",
slot.name, static_cast<unsigned>(slot.offset),
static_cast<unsigned>(target));
if (target >= 0x00010000u && target < 0x7FFF0000u) {
ScanCompanionStatRegion(slot.name, target,
0x4000, 0x8000, targets, targetCount);
}
}
}
void* info = nullptr;
void* aic = nullptr;
void* ability = nullptr;
__try {
auto** table = *reinterpret_cast<void***>(actor);
if (table) {
using UserInfoFn = void* (__thiscall*)(void*);
using AbilityFn = void* (__thiscall*)(void*, int);
const void* getter = table[g_native.kActorUserInfoVtableOffset / 4];
info = reinterpret_cast<UserInfoFn>(const_cast<void*>(getter))(actor);
const void* abilityGetter = table[g_native.kActorAbilityVtableOffset / 4];
ability = reinterpret_cast<AbilityFn>(const_cast<void*>(abilityGetter))(actor, 0);
aic = *reinterpret_cast<void**>(
reinterpret_cast<unsigned char*>(actor) + g_native.kActorAicOffset);
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
info = nullptr;
aic = nullptr;
ability = nullptr;
}
Log("stat-probe info=%p aic=%p ability=%p", info, aic, ability);
if (info) {
ScanCompanionStatRegion("info", reinterpret_cast<std::uintptr_t>(info),
0x1000, 0x8000, targets, targetCount);
}
if (aic) {
ScanCompanionStatRegion("aic", reinterpret_cast<std::uintptr_t>(aic),
0x1000, 0x8000, targets, targetCount);
}
if (ability) {
ScanCompanionStatRegion("ability", reinterpret_cast<std::uintptr_t>(ability),
0x2000, 0x10000, targets, targetCount);
}
}
void ObserveNativeCombatState(LiveProjectionActor& entry) {
if (entry.actor && entry.actor != g_companionStatProbedActor &&
CompanionStatProbeEnabled()) {
g_companionStatProbedActor = entry.actor;
ProbeCompanionStatWindow(entry.actor);
}
std::uint32_t now = 0;
if (!ReadNativeActorClock(entry, now)) return;
const int hp = ReadNativeSelfHpPercent(entry);
if (entry.selfHpPercent < 0) {
Log("quickbar self vitals source=%u actor=%p hp_percent=%d native_time=%u",
entry.source, entry.actor, hp, now);
} else if (hp < entry.selfHpPercent) {
entry.selfHurtAt = now;
entry.selfHurtRecorded = true;
Log("quickbar self damage source=%u actor=%p hp_before=%d hp_now=%d native_time=%u",
entry.source, entry.actor, entry.selfHpPercent, hp, now);
}
entry.selfHpPercent = hp;
entry.observedClock = now;
entry.recentlyHurt = entry.selfHurtRecorded &&
dnf90_companion_quickbar::NativeActionRecent(now, entry.selfHurtAt, 1600);
}
bool NativeCombatRecentlyStarted(const LiveProjectionActor& entry) {
auto* aic = reinterpret_cast<std::uint8_t*>(entry.aic);
const auto begin = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset);
const auto end = *reinterpret_cast<uintptr_t*>(aic + g_native.kAicRuntimeKeyProgramVectorOffset + 4);
if (!begin || end < begin || (end - begin) % sizeof(void*) ||
!ProjectionRuntimeKeyContract::ProgramCountValid((end - begin) / sizeof(void*))) return false;
auto** programs = reinterpret_cast<std::uint8_t**>(begin);
constexpr std::size_t basicIndex = ProjectionRuntimeKeyContract::kQuickSlotCount * 2;
for (std::size_t i = 0; i <= basicIndex; ++i) {
if (i >= 12 && i != basicIndex) continue; // Direct pool is not an action slot.
if (i < 12 && dnf90_companion_quickbar::Classify(entry.job, entry.quick[i], entry.grow) !=
dnf90_companion_quickbar::Purpose::Combat) continue;
if (!programs[i]) continue;
const auto started = *reinterpret_cast<std::uint32_t*>(programs[i] + 0x68);
if (started && dnf90_companion_quickbar::NativeActionRecent(entry.observedClock, started, 3000)) return true;
}
return false;
}
unsigned char EvaluateQuickbarCondition(void* condition) {
const auto original = g_originalAICondition;
if (!original || !condition) return 0;
const LONG evaluations = InterlockedIncrement(&g_conditionEvaluations);
if (evaluations % 600 == 0) {
Log("companion stats condition_evals=%ld support_selections=%ld",
evaluations,
static_cast<long>(g_supportSelections));
}
__try {
auto* node = reinterpret_cast<std::uint8_t*>(condition);
const unsigned int type = *reinterpret_cast<unsigned int*>(node + 0x1C);
if (type != 4 && type != 52 && type != 57) return original(condition);
for (auto& entry : g_liveProjectionActors) {
if (!entry.aiVm || *reinterpret_cast<void**>(node + 8) != entry.aiVm ||
!LiveProjectionActorPresent(entry)) continue;
if (!entry.quickbarPolicyReady) {
// BindQuickbarPolicy is intentionally fail-closed because its
// buff/mode tree is PVF-specific. A missing binding must not
// re-enable the native AIC's unrelated [quick skill] list.
// Block the support selector itself as well; otherwise its
// fixed PVF placeholder can still return a slot before the
// twelve-slot policy gets a chance to validate it.
if (type == 52) return 1;
// Type-57 nodes carry the generated CompanionSlot index; only
// allow a slot whose ID is present in the projected twelve
// positions and in the projected learned-skill vector.
if (type == 57) {
const int slot = *reinterpret_cast<int*>(node + 0x0C);
if (slot >= 0 && slot < 12) {
const auto id = entry.quick[slot];
if (id == 0 || id >= 512 || !entry.skills[id]) return 1;
}
}
continue;
}
if (type == 4) {
if (condition == entry.actionStateCondition) ObserveNativeCombatState(entry);
// Do not force the native mode gate closed. In aggressive mode
// this used to answer 0 on every evaluation, which shows up as
// the companion taking one step and stopping, so the native
// decision is kept untouched.
return original(condition);
}
// Native combat cooldown chains must skip empty slots and support
// skills. They remain available through the leader support lane.
if (type == 57) {
if (condition == entry.buffCooldown) return original(condition);
const int slot = *reinterpret_cast<int*>(node + 0x0C);
if (slot >= 0 && slot < 12) {
const auto id = entry.quick[slot];
const auto purpose = dnf90_companion_quickbar::Classify(entry.job, id, entry.grow);
if (id >= 512 || !entry.skills[id] ||
!dnf90_companion_quickbar::CombatPurposeAllowed(entry.aiBehavior, purpose)) return 1;
for (std::size_t i = 0; i < entry.areaCooldownCount; ++i)
if (condition == entry.areaCooldowns[i] && !dnf90_companion_quickbar::IsAreaCombat(entry.job, id)) return 1;
}
return original(condition);
}
// r12 contains repeated fixed buff nodes in fallback branches.
// One owned node selects the current quickbar; the remaining
// fixed checks must not cast whichever skill moved into slot 5.
if (condition != entry.buffCondition) return 1;
const bool supportAllowed = dnf90_companion_quickbar::OrdinarySupportAllowed(entry.aiBehavior,
NativeCombatRecentlyStarted(entry), entry.recentlyHurt);
const int selected = supportAllowed ? dnf90_companion_quickbar::SelectSupportBuff(entry.job, entry.quick, entry.skills,
[&](std::uint32_t id) {
return NativeSupportBuffHandled(entry, id);
}, [&](std::size_t slot) { return QuickbarProgramReady(entry, slot); }, entry.grow) : -1;
int chosen = selected;
if (chosen < 0 && !entry.recentlyHurt && NativeLeaderNeedsHealing(entry)) {
for (std::size_t slot = 0; slot < 12; ++slot) {
const auto id = entry.quick[slot];
const auto purpose = dnf90_companion_quickbar::Classify(entry.job, id, entry.grow);
if (id < 512 && entry.skills[id] &&
(purpose == dnf90_companion_quickbar::Purpose::Heal ||
purpose == dnf90_companion_quickbar::Purpose::DefensiveSupport) &&
QuickbarProgramReady(entry, slot)) { chosen = static_cast<int>(slot); break; }
}
}
if (chosen < 0 && !entry.recentlyHurt && entry.selfHpPercent >= 0 &&
entry.selfHpPercent < *entry.healThresholdParameter) {
for (std::size_t slot = 0; slot < 12; ++slot) {
const auto id = entry.quick[slot];
if (id < 512 && entry.skills[id] &&
dnf90_companion_quickbar::Classify(entry.job, id, entry.grow) == dnf90_companion_quickbar::Purpose::SelfHeal &&
QuickbarProgramReady(entry, slot)) { chosen = static_cast<int>(slot); break; }
}
}
if (chosen < 0 && supportAllowed) {
chosen = dnf90_companion_quickbar::SelectSupportAwakening(entry.job, entry.quick, entry.skills,
[&](std::size_t slot) { return QuickbarProgramReady(entry, slot); }, entry.grow);
}
entry.supportActionPending = chosen >= 0;
if (chosen >= 0) InterlockedIncrement(&g_supportSelections);
if (chosen < 0) { entry.lastChosenSkill = 0; return 1; }
*entry.buffSlotParameter = static_cast<float>(chosen);
*reinterpret_cast<int*>(reinterpret_cast<std::uint8_t*>(entry.buffCooldown) + 0x0C) = chosen;
const auto id = entry.quick[chosen];
if (entry.lastChosenSkill != id) {
Log("quickbar support selected source=%u actor=%p slot=%d skill=%u purpose=%u", entry.source,
entry.actor, chosen, id, static_cast<unsigned>(dnf90_companion_quickbar::Classify(entry.job, id, entry.grow)));
entry.lastChosenSkill = id;
}
return 0;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("quickbar condition selection failed condition=%p code=0x%08X; preserving native decision", condition, GetExceptionCode());
}
return original(condition);
}
unsigned char __fastcall ProxyAICondition(void* condition, void* /*unusedEdx*/) {
const auto original = g_originalAICondition;
if (!original || !condition) return 0;
// A town-return/retry can leave the quickbar condition pointer at a stale
// low address (the observed 0x03508299 crash has ecx/esi=0x38). Never pass
// that object into the native evaluator.
if (reinterpret_cast<std::uintptr_t>(condition) < 0x10000) {
Log("quickbar condition ignored stale pointer=%p", condition);
return 0;
}
// Native skill readiness can evaluate another condition. Let that nested
// call use the original evaluator instead of starting a second selection.
if (g_quickbarSelectionActive) return original(condition);
unsigned char result = 0;
g_quickbarSelectionActive = true;
__try {
result = EvaluateQuickbarCondition(condition);
} __finally {
g_quickbarSelectionActive = false;
}
return result;
}
// Same encrypted ability-level assignment as sub_24DC280, using the native
// alignment-specific setters. Resolve all writers before changing any level.
using NativeLevelWriteFn = void (__thiscall*)(void*, const std::uint32_t*, void*);
NativeLevelWriteFn ResolveNativeLevelWriter(void* target) {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
const uintptr_t alignment = reinterpret_cast<uintptr_t>(target) & 15;
const uintptr_t rvas[] = {0x030DB910, 0x030DB990, 0x030DBA10, 0x030DBA90};
const uintptr_t newRvas[] = {0x033DF130, 0x033DF1B0, 0x033DF230, 0x033DF2B0};
const uintptr_t rva = alignment <= 12 && alignment % 4 == 0
? (g_native.newNative ? newRvas : rvas)[alignment / 4]
: (g_native.newNative ? 0x033DF010 : 0x030DB7F0);
const unsigned char signature[] = {0x55,0x8B,0xEC,0xA1,0xB0,0x74,0x2F,0x05,0x40,0x8B,0xD0,0xC1,0xEA,0x08};
if (!g_native.newNative && memcmp(reinterpret_cast<void*>(base + rva), signature, sizeof(signature))) return nullptr;
return reinterpret_cast<NativeLevelWriteFn>(base + rva);
}
bool SetLiveProjectionLevel(const LiveProjectionActor& entry, std::uint16_t level) {
auto* actor = reinterpret_cast<std::uint8_t*>(entry.actor);
auto* aic = reinterpret_cast<std::uint8_t*>(entry.aic);
auto** table = *reinterpret_cast<void***>(actor);
using AbilityFn = void* (__thiscall*)(void*, int);
using UserInfoFn = void* (__thiscall*)(void*);
void* ability = reinterpret_cast<AbilityFn>(table[g_native.kActorAbilityVtableOffset / 4])(actor, 0);
auto* info = reinterpret_cast<std::uint8_t*>(
reinterpret_cast<UserInfoFn>(table[g_native.kActorUserInfoVtableOffset / 4])(actor));
// APC +CF4 (sub_24814B0) returns its AIC+0x100C UserInfo. Verify the
// exact instance and its registered key (sub_1BF6D00), never a roster row.
if (!ability || !info || info != *reinterpret_cast<void**>(aic + g_native.kAicUserInfoOffset) ||
*reinterpret_cast<std::uint16_t*>(info + g_native.kUserInfoKeyOffset) != entry.key) return false;
// sub_23EB0D0 draws Lv from UserInfo+0x18+0x14, independently of the
// combat ability. Native APC refresh sub_34922A0 synchronizes the AIC
// and UserInfo from actor+0x6F1C; keep all three current without rerunning
// its spawn/stat/skill initialization or replacing this live actor.
void* targets[] = {ability, aic + 0x1C, info + g_native.kUserInfoLevelOffset};
NativeLevelWriteFn writers[3]{};
for (std::size_t i = 0; i < 3; ++i) {
writers[i] = ResolveNativeLevelWriter(targets[i]);
if (!writers[i]) return false;
}
std::uint32_t value = level;
for (std::size_t i = 0; i < 3; ++i)
writers[i](targets[i], &value, reinterpret_cast<std::uint8_t*>(targets[i]) + 4);
// Same plain assignment as the native APC level setter sub_2482890.
*reinterpret_cast<std::uint32_t*>(actor + g_native.kActorSpawnLevelOffset) = value;
return true;
}
// Apply the PVF stat blob carried by a userinfo mode1 body directly through the
// actor's own stats vtable. The native userinfo dispatcher cannot be used for
// projected APC actors (it faults while binding their UserInfo), while this
// path is the one the progression packet already exercises successfully.
bool ApplyProjectionStatVector(
void* actor,
const unsigned char* mode1,
std::size_t mode1Length)
{
constexpr std::size_t kMode1StatBlobOffset = 31;
constexpr std::size_t kMode1StatBlobSize = 92;
if (!actor || !mode1 ||
mode1Length < kMode1StatBlobOffset + kMode1StatBlobSize) {
return false;
}
auto** table = *reinterpret_cast<void***>(actor);
using StatsFn = void (__thiscall*)(void*, const void*);
using RecalculateFn = void (__thiscall*)(void*, int);
alignas(16) unsigned char stats[kMode1StatBlobSize];
memcpy(stats, mode1 + kMode1StatBlobOffset, sizeof(stats));
memcpy(g_lastCompanionStatBlob, stats, sizeof(stats));
g_hasLastCompanionStatBlob = true;
{
const auto* words = reinterpret_cast<const std::uint32_t*>(stats);
Log("projection stat vector a=%u b=%u c=%u d=%u e=%u f=%u g=%u h=%u i=%u j=%u k=%u l=%u m=%u n=%u o=%u p=%u",
words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7],
words[8], words[9], words[10], words[11], words[12], words[13], words[14], words[15]);
}
reinterpret_cast<StatsFn>(table[g_native.kActorStatsVtableOffset / 4])(actor, stats);
reinterpret_cast<RecalculateFn>(table[g_native.kActorRecalculateVtableOffset / 4])(actor, 1);
return true;
}
bool ApplyLiveProjectionProgression(const dnf90_companion_projection::View& view) {
if (!view.progression || !view.present) return false;
for (auto& entry : g_liveProjectionActors) {
if (entry.source != view.sourceCharacter || entry.revision != view.revision) continue;
__try {
if (!LiveProjectionActorPresent(entry) || view.level < entry.level) {
Log("progression skipped source=%u reason=stale-or-retired-actor", view.sourceCharacter);
return false;
}
if (!entry.keysReady || view.quickSkillCount != 12) return false;
// The AIC is an instance copy created by ProxyAicCopy. Keep its
// learned/quick vectors current for native room lifecycle reads,
// while leaving template, name and all worn equipment untouched.
auto* aic = reinterpret_cast<std::uint8_t*>(entry.aic);
uintptr_t skillBegin = 0, skillEnd = 0, skillCapacity = 0;
uintptr_t quickBegin = 0, quickEnd = 0, quickCapacity = 0;
bool grew = false;
if (!EnsureNativeVectorCapacity(aic, g_native.kAicSkillVectorObjectOffset, 8, view.skillCount,
g_native.kNativeSkillVectorAllocateRva, &skillBegin, &skillEnd, &skillCapacity, &grew) ||
!EnsureNativeVectorCapacity(aic, g_native.kAicQuickSkillVectorObjectOffset, 8, view.quickSkillCount,
g_native.kNativeSkillVectorAllocateRva, &quickBegin, &quickEnd, &quickCapacity, &grew)) return false;
// Validate/switch only our per-instance direct-key programs. Existing
// skills keep their native objects/cooldowns; only changed ranks learn.
if (!ApplyDynamicRuntimeKeyPrograms(entry.actor, view, entry.quick)) return false;
for (std::size_t i = 0; i < 12; ++i)
entry.quick[i] = dnf90_companion_projection::Read32(view.quickSkills + i * 8);
if (!SetLiveProjectionLevel(entry, view.level)) {
Log("progression skipped source=%u reason=level-userinfo-unavailable", view.sourceCharacter);
return false;
}
auto** table = *reinterpret_cast<void***>(entry.actor);
using ExpFn = void (__thiscall*)(void*, std::uint64_t, int);
reinterpret_cast<ExpFn>(table[g_native.kActorExpVtableOffset / 4])(entry.actor, view.experience, view.level);
using LearnFn = void (__thiscall*)(void*, std::uint32_t, std::uint32_t);
unsigned learned = 0;
for (std::size_t i = 0; i < view.skillCount; ++i) {
const auto id = dnf90_companion_projection::Read32(view.skills + i * 8);
const auto rank = dnf90_companion_projection::Read32(view.skills + i * 8 + 4);
if (entry.skills[id] == rank) continue;
reinterpret_cast<LearnFn>(table[g_native.kActorLearnVtableOffset / 4])(entry.actor, id, rank);
entry.skills[id] = rank;
++learned;
}
using StatsFn = void (__thiscall*)(void*, const void*);
using RecalculateFn = void (__thiscall*)(void*, int);
alignas(16) unsigned char stats[92];
memcpy(stats, view.mode1 + 31, sizeof(stats));
reinterpret_cast<StatsFn>(table[g_native.kActorStatsVtableOffset / 4])(entry.actor, stats);
reinterpret_cast<RecalculateFn>(table[g_native.kActorRecalculateVtableOffset / 4])(entry.actor, 1);
if (view.skillCount) memcpy(reinterpret_cast<void*>(skillBegin), view.skills, view.skillCount * 8);
memcpy(reinterpret_cast<void*>(quickBegin), view.quickSkills, view.quickSkillCount * 8);
*reinterpret_cast<uintptr_t*>(aic + g_native.kAicSkillVectorOffset + 4) = skillBegin + view.skillCount * 8;
*reinterpret_cast<uintptr_t*>(aic + g_native.kAicQuickSkillVectorOffset + 4) = quickBegin + view.quickSkillCount * 8;
entry.lastChosenSkill = 0;
Log("progression applied source=%u revision=%llu key=%u actor=%p level=%u->%u exp=%u learned=%u same_actor=1 display_level_synced=1",
entry.source, entry.revision, entry.key, entry.actor, entry.level, view.level, view.experience, learned);
entry.level = view.level;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("progression failed source=%u key=%u code=0x%08X", entry.source, entry.key, GetExceptionCode());
return false;
}
}
Log("progression skipped source=%u reason=spawn-identity-unavailable", view.sourceCharacter);
return false;
}
void SetOriginalApcRegistration(void* original) {
g_originalApcRegistration =
reinterpret_cast<ApcRegistrationFn>(original);
Log("projection registration hook original=%p enabled=%u",
original, DirectOp40Enabled() ? 1u : 0u);
}
unsigned char __fastcall ProxyApcRegistration(
void* self,
void* /*unusedEdx*/,
void* ownerActor,
std::uint32_t flags,
std::uint32_t objectKey,
void* actor,
std::uint32_t value5,
std::uint32_t value6,
std::uint32_t value7,
std::uint32_t value8)
{
ApcRegistrationFn original = g_originalApcRegistration;
if (!original) {
return 0;
}
// The 0x03508299 crash is the native APC registration reading
// [self+0x18] with self=0x38 after a town return. Reject the stale object
// before entering the native __thiscall function.
if (reinterpret_cast<std::uintptr_t>(self) < 0x10000) {
Log("APC registration ignored stale self=%p owner=%p actor=%p key=%u",
self, ownerActor, actor, objectKey);
return 0;
}
const unsigned char result = original(
self, ownerActor, flags, objectKey, actor,
value5, value6, value7, value8);
if (ProjectionRegistrationContract::Matches(
g_directLoopbackArmed,
g_directLoopbackInReceive,
static_cast<std::uint32_t>(GetCurrentThreadId()),
static_cast<std::uint32_t>(g_armedThread),
objectKey,
actor,
result)) {
if (g_projectionApcObjectKey == 0 ||
(g_projectionApcObjectKey == objectKey &&
g_projectionApcActor == actor)) {
g_projectionApcObjectKey =
static_cast<std::uint16_t>(objectKey);
g_projectionApcActor = actor;
bool actorNameApplied = false;
bool runtimeKeysApplied = false;
dnf90_companion_projection::View view{};
if (g_projectionAicApplied &&
g_projectionPacketLength >=
dnf90_companion_projection::kPacketHeaderSize) {
if (dnf90_companion_projection::DecodePacket(
g_projectionPacket,
g_projectionPacketLength,
&view) &&
view.present) {
runtimeKeysApplied =
ApplyDynamicRuntimeKeyPrograms(
actor, view);
RememberLiveProjectionActor(ownerActor, actor,
static_cast<std::uint16_t>(objectKey), view, runtimeKeysApplied);
if (view.nameUtf16 &&
view.nameUnits != 0 &&
view.nameUnits <=
dnf90_companion_projection::kMaxAicNameUnits) {
wchar_t actorName[
dnf90_companion_projection::kMaxAicNameUnits + 1] = {};
bool validName = true;
for (std::size_t i = 0; i < view.nameUnits; ++i) {
const std::uint16_t unit =
dnf90_companion_projection::Read16(
view.nameUtf16 + i * 2);
if (unit == 0) {
validName = false;
break;
}
actorName[i] =
static_cast<wchar_t>(unit);
}
if (validName) {
actorName[view.nameUnits] = L'\0';
actorNameApplied =
AssignNativeRefWideString(
reinterpret_cast<std::uint8_t*>(actor) +
g_native.kActorDisplayNameOffset,
actorName);
}
}
}
}
Log("projection APC registered key=%u actor=%p owner=%p flags=0x%08X actor_name_applied=%u runtime_keys_applied=%u ai_behavior=%u ai_class=%u ai_combo=%u ai_preset_hash=%u",
objectKey, actor, ownerActor, flags,
actorNameApplied ? 1u : 0u,
runtimeKeysApplied ? 1u : 0u,
static_cast<unsigned int>(view.aiBehavior),
static_cast<unsigned int>(view.aiClass),
view.aiComboEnabled ? 1u : 0u,
static_cast<unsigned int>(view.aiComboPresetHash));
} else {
Log("projection APC registration conflict previous_key=%u previous_actor=%p key=%u actor=%p",
g_projectionApcObjectKey, g_projectionApcActor,
objectKey, actor);
}
}
return result;
}
// Class0 dispatch invokes callbacks that read global native reader state;
// passing a packet pointer alone does not bind its body. The real router
// (02261E30) binds packet diagnostics, cursor and remaining length separately.
using ProjectionReaderSetCursorFn = void (__cdecl*)(unsigned char*);
using ProjectionReaderSetLengthFn = void (__cdecl*)(int);
using ProjectionReaderSetPacketFn = void (__cdecl*)(unsigned char*, int);
int DispatchProjectionClass0(unsigned char* packet, std::size_t length) {
if (!packet || length < dnf90_companion_projection::kPacketHeaderSize ||
length > kProjectionNativePacketCapacity || !g_projectionClass0Dispatch) {
return -1;
}
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
auto* reader = reinterpret_cast<ProjectionNativeReaderState*>(base + g_native.kProjectionReaderStateRva);
auto** errorCopy = reinterpret_cast<void**>(base + g_native.kProjectionReaderErrorCopyRva);
auto setCursor = reinterpret_cast<ProjectionReaderSetCursorFn>(base + g_native.kProjectionReaderSetCursorRva);
auto setLength = reinterpret_cast<ProjectionReaderSetLengthFn>(base + g_native.kProjectionReaderSetLengthRva);
auto setPacket = reinterpret_cast<ProjectionReaderSetPacketFn>(base + g_native.kProjectionReaderSetPacketRva);
const ProjectionNativeReaderState saved = *reader;
void* const savedErrorCopy = *errorCopy;
// The native packet setter owns/frees the error copy. Detach the outer
// copy before binding, then let it free only this dispatch's copy.
*errorCopy = nullptr;
int result = -1;
__try {
setPacket(packet, static_cast<int>(length));
setCursor(packet + dnf90_companion_projection::kPacketHeaderSize);
setLength(static_cast<int>(length - dnf90_companion_projection::kPacketHeaderSize));
result = g_projectionClass0Dispatch(g_projectionDispatcherArg, reinterpret_cast<int>(packet));
Log("projection reader dispatch mode=%u key=%u body_length=%zu remaining=%d result=%d",
packet[dnf90_companion_projection::kPacketHeaderSize], g_projectionApcObjectKey,
length - dnf90_companion_projection::kPacketHeaderSize, reader->remaining, result);
} __finally {
setPacket(saved.packet, saved.packetLength);
setCursor(saved.cursor);
setLength(saved.remaining);
*errorCopy = savedErrorCopy;
}
return result;
}
void SetProjectionClass0Dispatch(void* original) {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
static const unsigned char cursorSignature[] = {
0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0xA3, 0x00, 0x85, 0x2F, 0x05, 0x5D, 0xC3
};
static const unsigned char lengthSignature[] = {
0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0xA3, 0x04, 0x85, 0x2F, 0x05, 0x5D, 0xC3
};
static const unsigned char packetSignature[] = {
0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0x8B, 0x4D, 0x0C,
0xA3, 0x08, 0x85, 0x2F, 0x05, 0xA1, 0x28, 0x85, 0x2F, 0x05,
0x89, 0x0D, 0x0C, 0x85, 0x2F, 0x05
};
if (!base || (!g_native.newNative && (memcmp(reinterpret_cast<void*>(base + g_native.kProjectionReaderSetCursorRva), cursorSignature, sizeof(cursorSignature)) ||
memcmp(reinterpret_cast<void*>(base + g_native.kProjectionReaderSetLengthRva), lengthSignature, sizeof(lengthSignature)) ||
memcmp(reinterpret_cast<void*>(base + g_native.kProjectionReaderSetPacketRva), packetSignature, sizeof(packetSignature))))) {
g_projectionClass0Dispatch = nullptr;
Log("projection class0 dispatcher disabled: native reader signature mismatch");
return;
}
g_projectionClass0Dispatch =
reinterpret_cast<Class0ProjectionDispatchFn>(original);
Log("projection class0 dispatcher original=%p", original);
}
bool ReplacePendingProgression(const unsigned char* bytes, std::size_t length,
const dnf90_companion_projection::View& update) {
if (!bytes || length == 0 || length > kProjectionQueueMaxBytes) {
return false;
}
dnf90_companion_projection::View pending{};
unsigned char* target = nullptr;
if (!g_projectionAicApplied && dnf90_companion_projection::DecodePacket(
g_projectionPacket, g_projectionPacketLength, &pending) &&
pending.sourceCharacter == update.sourceCharacter && pending.revision == update.revision) {
memcpy(g_projectionPacket, bytes, length);
g_projectionPacketLength = length;
target = g_projectionPacket;
} else {
for (auto& queued : g_projectionQueue) {
if (dnf90_companion_projection::DecodePacket(queued.packet.data(), queued.packet.size(), &pending) &&
pending.sourceCharacter == update.sourceCharacter && pending.revision == update.revision) {
g_projectionQueueBytes -= queued.packet.size();
std::vector<unsigned char> replacement(bytes, bytes + length);
queued.packet.swap(replacement);
g_projectionQueueBytes += queued.packet.size();
target = queued.packet.data();
break;
}
}
}
if (!target) return false;
target[dnf90_companion_projection::kPacketHeaderSize + 5] = dnf90_companion_projection::kFlagPresent;
auto* plan = target + dnf90_companion_projection::kPacketHeaderSize + 68;
std::uint32_t planWord = 0;
if (pending.aiBehavior != 0) {
target[dnf90_companion_projection::kPacketHeaderSize + 5] |= dnf90_companion_projection::kFlagAiPlan;
planWord = pending.aiBehavior | (pending.aiComboEnabled ? 4u : 0u) |
(static_cast<std::uint32_t>(pending.aiComboPresetHash) << 8) |
(static_cast<std::uint32_t>(pending.aiClass) << 16);
}
memcpy(plan, &planWord, sizeof(planWord));
Log("progression updated pending spawn source=%u level=%u", update.sourceCharacter, update.level);
return true;
}
bool CaptureProjectionClass0Packet(
int dispatcherArg,
int packet)
{
if (!Enabled() || !packet) return false;
__try {
const unsigned char* bytes =
reinterpret_cast<const unsigned char*>(packet);
const std::uint32_t packetLength =
dnf90_companion_projection::Read32(bytes + 3);
if (packetLength < dnf90_companion_projection::kPacketHeaderSize ||
packetLength > sizeof(g_projectionPacket)) {
return false;
}
dnf90_companion_projection::View view{};
if (!dnf90_companion_projection::DecodePacket(
bytes, packetLength, &view)) {
return false;
}
if (view.progression) {
if (!ApplyLiveProjectionProgression(view)) ReplacePendingProgression(bytes, packetLength, view);
return true; // handled even when stale: never queue or summon
}
if (!view.present) {
ClearQueuedProjections("private-clear");
g_liveProjectionActors.clear();
ClearCurrentProjectionPacket();
g_retry.Cancel();
Log("projection private packets cleared revision=%llu",
static_cast<unsigned long long>(view.revision));
return true;
}
if (g_projectionPacketLength != 0) {
QueueProjectionPacket(bytes, packetLength, dispatcherArg);
Log("projection private packet queued source=%u revision=%llu queue_depth=%zu queue_bytes=%zu high_water=%zu drops=%u",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
g_projectionQueue.size(), g_projectionQueueBytes,
g_projectionQueueHighWater, g_projectionQueueDropCount);
return true;
}
if (!LoadCurrentProjectionPacket(bytes, packetLength, dispatcherArg)) {
return false;
}
if (g_armedActor != nullptr &&
g_armedThread != 0 &&
GetCurrentThreadId() == g_armedThread) {
g_retry.Arm();
}
Log("projection private packet cached source=%u revision=%llu mode0=%zu mode1=%zu queue_depth=%zu queue_bytes=%zu high_water=%zu drops=%u retry_pending=%u",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
view.mode0Length,
view.mode1Length,
g_projectionQueue.size(), g_projectionQueueBytes,
g_projectionQueueHighWater, g_projectionQueueDropCount,
g_retry.Pending() ? 1u : 0u);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("projection private packet capture exception code=0x%08X",
GetExceptionCode());
return false;
}
}
void LogProjectionDecodeRejected(
const unsigned char* bytes,
std::size_t packetLength,
const char* stage)
{
if (!bytes) {
Log("projection decode rejected stage=%s reason=null-packet",
stage ? stage : "unknown");
return;
}
__try {
const std::uint32_t headerLength =
dnf90_companion_projection::Read32(bytes + 3);
const std::uint16_t opcode =
dnf90_companion_projection::Read16(bytes + 1);
if (packetLength < dnf90_companion_projection::kPacketHeaderSize) {
Log("projection decode rejected stage=%s reason=short-header length=%zu header_length=%u opcode=%u",
stage ? stage : "unknown", packetLength,
static_cast<unsigned int>(headerLength),
static_cast<unsigned int>(opcode));
return;
}
const unsigned char* body =
bytes + dnf90_companion_projection::kPacketHeaderSize;
const std::size_t bodyLength =
packetLength - dnf90_companion_projection::kPacketHeaderSize;
if (bodyLength < dnf90_companion_projection::kBodyHeaderSize) {
Log("projection decode rejected stage=%s reason=short-body length=%zu body_length=%zu header_length=%u opcode=%u magic=%08X",
stage ? stage : "unknown", packetLength, bodyLength,
static_cast<unsigned int>(headerLength),
static_cast<unsigned int>(opcode),
static_cast<unsigned int>(dnf90_companion_projection::Read32(body)));
return;
}
const std::uint32_t mode0Length =
dnf90_companion_projection::Read32(body + 16);
const std::uint32_t mode1Length =
dnf90_companion_projection::Read32(body + 20);
const std::size_t nameUnits =
dnf90_companion_projection::Read16(body + 58);
const std::size_t equipmentCount =
dnf90_companion_projection::Read16(body + 60);
const std::size_t skillCount =
dnf90_companion_projection::Read16(body + 64);
const std::size_t quickSkillCount =
dnf90_companion_projection::Read16(body + 66);
const std::size_t dynamicBytes =
nameUnits * 2u + equipmentCount * 4u +
skillCount * 8u + quickSkillCount * 8u;
const std::size_t mode0Offset =
dnf90_companion_projection::kBodyHeaderSize + dynamicBytes;
unsigned int mode0Key = 0;
unsigned int mode0Byte0 = 0;
unsigned int mode0Version = 0;
if (mode0Offset < bodyLength) {
mode0Byte0 = body[mode0Offset];
if (bodyLength - mode0Offset > 2) {
mode0Version = dnf90_companion_projection::Read16(
body + mode0Offset + 1);
}
if (bodyLength - mode0Offset >
dnf90_companion_projection::kMode0ObjectKeyOffset + 1) {
mode0Key = dnf90_companion_projection::Read16(
body + mode0Offset +
dnf90_companion_projection::kMode0ObjectKeyOffset);
}
}
Log("projection decode rejected stage=%s length=%zu header_length=%u opcode=%u body_length=%zu magic=%08X version=%u flags=%u source=%u revision=%llu mode0=%u mode1=%u name=%zu equip=%zu level=%u skills=%zu quick=%zu dynamic=%zu mode0_offset=%zu mode0_byte0=%u mode0_version=%u mode0_key=%u wanted=%zu",
stage ? stage : "unknown", packetLength,
static_cast<unsigned int>(headerLength),
static_cast<unsigned int>(opcode), bodyLength,
static_cast<unsigned int>(dnf90_companion_projection::Read32(body)),
static_cast<unsigned int>(body[4]),
static_cast<unsigned int>(body[5]),
static_cast<unsigned int>(dnf90_companion_projection::Read16(body + 6)),
static_cast<unsigned long long>(dnf90_companion_projection::Read64(body + 8)),
static_cast<unsigned int>(mode0Length),
static_cast<unsigned int>(mode1Length), nameUnits,
equipmentCount,
static_cast<unsigned int>(dnf90_companion_projection::Read16(body + 62)),
skillCount, quickSkillCount, dynamicBytes, mode0Offset,
mode0Byte0, mode0Version, mode0Key,
dnf90_companion_projection::kBodyHeaderSize + dynamicBytes +
static_cast<std::size_t>(mode0Length) +
static_cast<std::size_t>(mode1Length));
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("projection decode rejected stage=%s reason=diagnostic-exception code=0x%08X",
stage ? stage : "unknown", GetExceptionCode());
}
}
void* ProjectionActorOverrideForObjectKey(std::uint16_t objectKey) {
if (!Enabled() ||
!ProjectionLookupContract::Matches(
g_projectionApplyActive,
objectKey,
g_projectionApcObjectKey,
g_projectionApcActor)) {
return nullptr;
}
return g_projectionApcActor;
}
bool ProjectionMode0OwnerContextOverride() {
// The native local constructor is not safe to force from the projection
// hook: when the owner/context fields are not fully initialized it enters
// a memcpy path with an invalid destination/length and terminates DNF.
// Keep the owner hook diagnostic-only until the native context contract is
// proven. Returning false preserves the stock comparison and fails closed.
return false;
}
bool ApplyPendingProjection() {
// Stage 2 is only valid after the per-instance AIC projection succeeded.
// Before that point the APC is still the baseline template and must not
// receive a scene-object projection.
if (!g_projectionAicApplied) {
return false;
}
if (!Enabled() ||
!g_projectionPending ||
!g_projectionClass0Dispatch ||
g_projectionApcObjectKey == 0 ||
g_projectionApcObjectKey == 0xFFFF ||
g_projectionPacketLength <
dnf90_companion_projection::kPacketHeaderSize ||
g_projectionPacketLength > sizeof(g_projectionPacket)) {
return false;
}
__try {
dnf90_companion_projection::View view{};
if (!dnf90_companion_projection::DecodePacket(
g_projectionPacket,
g_projectionPacketLength,
&view) ||
!view.present ||
view.mode0Length >
dnf90_companion_projection::kMaxProjectionBody ||
view.mode1Length >
dnf90_companion_projection::kMaxProjectionBody) {
Log("projection cached private packet invalid length=%zu",
g_projectionPacketLength);
g_projectionPending = false;
return false;
}
const std::size_t mode0PacketLength =
dnf90_companion_projection::kPacketHeaderSize +
view.mode0Length;
const std::size_t mode1PacketLength =
view.mode1Length != 0
? dnf90_companion_projection::kPacketHeaderSize +
view.mode1Length
: 0;
if (mode0PacketLength > sizeof(g_projectionMode0Packet) ||
mode1PacketLength > sizeof(g_projectionMode1Packet)) {
g_projectionPending = false;
return false;
}
memset(g_projectionMode0Packet, 0, mode0PacketLength);
memcpy(
g_projectionMode0Packet,
g_projectionPacket,
dnf90_companion_projection::kPacketHeaderSize);
g_projectionMode0Packet[0] = 0;
dnf90_companion_projection::Write16(
g_projectionMode0Packet + 1, 2);
if (mode1PacketLength != 0) {
memset(g_projectionMode1Packet, 0, mode1PacketLength);
memcpy(
g_projectionMode1Packet,
g_projectionPacket,
dnf90_companion_projection::kPacketHeaderSize);
g_projectionMode1Packet[0] = 0;
dnf90_companion_projection::Write16(
g_projectionMode1Packet + 1, 2);
}
const std::uint32_t mode0PacketLength32 =
static_cast<std::uint32_t>(mode0PacketLength);
const std::uint32_t mode1PacketLength32 =
static_cast<std::uint32_t>(mode1PacketLength);
memcpy(g_projectionMode0Packet + 3,
&mode0PacketLength32, sizeof(mode0PacketLength32));
if (mode1PacketLength != 0) {
memcpy(g_projectionMode1Packet + 3,
&mode1PacketLength32, sizeof(mode1PacketLength32));
}
unsigned char* mode0 =
g_projectionMode0Packet +
dnf90_companion_projection::kPacketHeaderSize;
unsigned char* mode1 =
mode1PacketLength != 0
? g_projectionMode1Packet +
dnf90_companion_projection::kPacketHeaderSize
: nullptr;
memcpy(mode0, view.mode0, view.mode0Length);
if (view.mode1Length != 0) {
memcpy(mode1, view.mode1, view.mode1Length);
}
if (!dnf90_companion_projection::PatchTargetObjectKey(
mode0, view.mode0Length,
mode1, view.mode1Length,
g_projectionApcObjectKey)) {
Log("projection target-key patch rejected key=%u source=%u revision=%llu",
g_projectionApcObjectKey,
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision));
g_projectionPending = false;
return false;
}
// Consume before native apply so any callback cannot replay the same
// projection. The original APC5405 AIC/template is never written.
g_projectionPending = false;
int mode0Result = 0;
int mode1Result = 0;
bool mode1Exception = false;
bool mode1Direct = false;
bool mode0Exception = false;
bool mode0Skipped = false;
unsigned long mode1ExceptionCode = 0;
unsigned long mode0ExceptionCode = 0;
std::uintptr_t mode0ExceptionAddress = 0;
std::uintptr_t mode0FaultAddress = 0;
std::uintptr_t mode1ExceptionAddress = 0;
std::uintptr_t mode1FaultAddress = 0;
std::uintptr_t mode1Stack[8] = {};
int mode1StackCount = 0;
g_projectionApplyActive = true;
__try {
// Preserve the established stat-then-appearance order. Each call
// binds its own native reader, so neither mode consumes the previous
// network packet or the preceding projection's exhausted cursor.
// Original mode has no mode1 body and applies appearance only.
if (mode1PacketLength != 0 &&
!g_projectionMode1Failed) {
// The native userinfo dispatcher faults on APC actors in this
// client build (fault 0x17 while binding the actor's UserInfo),
// so the stat vector is applied through the actor's own stats
// vtable instead — the same path the progression packet already
// uses successfully. mode1's first 31 bytes are the header; the
// 92-byte PVF stat blob follows it.
__try {
if (ApplyProjectionStatVector(
g_projectionApcActor, view.mode1, view.mode1Length)) {
mode1Result = 0;
mode1Direct = true;
Log("projection stats applied directly key=%u actor=%p source=%u level=%u",
g_projectionApcObjectKey,
g_projectionApcActor,
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned int>(view.level));
} else {
mode1Result = DispatchProjectionClass0(
g_projectionMode1Packet, mode1PacketLength);
}
} __except (CaptureProjectionException(GetExceptionInformation())) {
mode1Exception = true;
mode1ExceptionCode = GetExceptionCode();
mode1ExceptionAddress = g_lastProjectionExceptionAddress;
mode1FaultAddress = g_lastProjectionFaultAddress;
mode1StackCount = g_lastProjectionStackCount;
for (int slot = 0; slot < mode1StackCount; ++slot) {
mode1Stack[slot] = g_lastProjectionStack[slot];
}
mode1Result = -1;
g_projectionMode1Failed = true;
}
} else if (mode1PacketLength != 0) {
mode1Result = -1;
}
// Opt-in experiment: replay the mode1 equipment create rows against
// this APC actor. The native reader is positioned at the row count
// (body+124) and restored afterwards. No-op unless the env flag is
// set; the active projection path is unchanged.
if (mode1PacketLength != 0 && mode1 && g_projectionApcActor) {
void* companionAic = nullptr;
for (auto& live : g_liveProjectionActors) {
if (live.actor == g_projectionApcActor) {
companionAic = live.aic;
break;
}
}
TryApplyCompanionEquipment(
g_projectionApcActor,
g_projectionMode1Packet,
mode1PacketLength,
mode1,
view.mode1Length,
companionAic);
}
__try {
// This client build faults inside the native class0 dispatcher
// with pc == fault == 0x18181818 (a data field used as a call
// target), so mode0 never completes and each attempt leaves the
// actor half-applied. Appearance was already absent while the
// twelve key programs worked, therefore skip the dispatch
// explicitly instead of crashing three times per projection.
mode0Result = -2;
mode0Skipped = true;
} __except (CaptureProjectionException(GetExceptionInformation())) {
mode0Exception = true;
mode0ExceptionCode = GetExceptionCode();
mode0ExceptionAddress = g_lastProjectionExceptionAddress;
mode0FaultAddress = g_lastProjectionFaultAddress;
mode0Result = -1;
}
} __finally {
g_projectionApplyActive = false;
}
if (mode1Exception) {
Log("projection mode1 dispatch exception source=%u revision=%llu key=%u code=0x%08X pc=%p fault=%p rva=0x%zX stack=%p,%p,%p,%p,%p,%p; continuing mode0",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
g_projectionApcObjectKey,
mode1ExceptionCode,
reinterpret_cast<void*>(mode1ExceptionAddress),
reinterpret_cast<void*>(mode1FaultAddress),
mode1ExceptionAddress > reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr))
? mode1ExceptionAddress - reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr))
: 0,
reinterpret_cast<void*>(mode1Stack[0]),
reinterpret_cast<void*>(mode1Stack[1]),
reinterpret_cast<void*>(mode1Stack[2]),
reinterpret_cast<void*>(mode1Stack[3]),
reinterpret_cast<void*>(mode1Stack[4]),
reinterpret_cast<void*>(mode1Stack[5]));
}
if (mode0Exception) {
++g_projectionMode0RetryCount;
if (g_projectionMode0RetryCount <= kProjectionMode0RetryLimit) {
// The APC/AIC transaction already created this actor. Keep
// its packet and retry only the safe-point projection instead
// of arming another native summon for the same source.
g_projectionPending = true;
g_retry.Cancel();
g_waitLogCount = 0;
Log("projection mode0 dispatch exception retry source=%u revision=%llu key=%u code=0x%08X attempt=%u/%u actor=%p pc=%p fault=%p rva=0x%zX",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
g_projectionApcObjectKey,
mode0ExceptionCode,
g_projectionMode0RetryCount,
kProjectionMode0RetryLimit,
g_projectionApcActor,
reinterpret_cast<void*>(mode0ExceptionAddress),
reinterpret_cast<void*>(mode0FaultAddress),
mode0ExceptionAddress > reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr))
? mode0ExceptionAddress - reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr))
: 0);
return false;
}
const std::uint16_t failedKey = g_projectionApcObjectKey;
const bool hasNext = LoadNextQueuedProjection();
g_waitLogCount = 0;
if (hasNext) {
g_retry.Arm();
Log("projection mode0 dispatch exception drained source=%u revision=%llu key=%u code=0x%08X; advancing remaining=%zu actor=%p pc=%p fault=%p",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
failedKey,
mode0ExceptionCode,
g_projectionQueue.size(),
g_armedActor,
reinterpret_cast<void*>(mode0ExceptionAddress),
reinterpret_cast<void*>(mode0FaultAddress));
} else {
g_retry.Cancel();
Log("projection mode0 dispatch exception drained source=%u revision=%llu key=%u code=0x%08X actor=%p pc=%p fault=%p",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
failedKey,
mode0ExceptionCode,
g_armedActor,
reinterpret_cast<void*>(mode0ExceptionAddress),
reinterpret_cast<void*>(mode0FaultAddress));
}
return false;
}
Log("projection applied source=%u revision=%llu key=%u actor=%p mode0_result=%d mode0_skipped=%u mode1_result=%d mode1_present=%u mode1_direct=%u apply_order=%s template_mutation=0",
static_cast<unsigned int>(view.sourceCharacter),
static_cast<unsigned long long>(view.revision),
g_projectionApcObjectKey,
g_projectionApcActor,
mode0Result,
mode0Skipped ? 1u : 0u,
mode1Result,
mode1PacketLength != 0 ? 1u : 0u,
mode1Direct ? 1u : 0u,
mode1PacketLength != 0 ? "mode1_then_mode0" : "mode0_only");
const bool hasNext = LoadNextQueuedProjection();
g_waitLogCount = 0;
if (hasNext) {
g_retry.Arm();
Log("projection queue advanced remaining=%zu actor=%p",
g_projectionQueue.size(), g_armedActor);
} else {
g_retry.Cancel();
}
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
const std::uint16_t failedKey = g_projectionApcObjectKey;
const unsigned long exceptionCode = GetExceptionCode();
const bool hasNext = LoadNextQueuedProjection();
g_waitLogCount = 0;
if (hasNext) {
g_retry.Arm();
Log("projection apply exception recovered key=%u code=0x%08X remaining=%zu actor=%p",
failedKey,
exceptionCode,
g_projectionQueue.size(),
g_armedActor);
} else {
g_retry.Cancel();
Log("projection apply exception drained key=%u code=0x%08X",
failedKey,
exceptionCode);
}
return false;
}
}
void ArmFixedApc5405() {
if (!Enabled()) {
g_retry.Cancel();
g_armedActor = nullptr;
g_armedThread = 0;
return;
}
// The new-native ready callback is not reached on every client build; arm
// is the first point that is guaranteed to run on dungeon entry. The
// installer is idempotent and a no-op unless the probe/apply flag is set.
InstallCompanionEquipmentProbe();
uintptr_t base =
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
void* actor = nullptr;
if (!ReadControlledActor(base, &actor)) {
g_retry.Cancel();
return;
}
g_armedActor = actor;
g_liveProjectionActors.clear();
g_armedThread = GetCurrentThreadId();
g_waitLogCount = 0;
ResetCurrentProjectionActorState();
if (g_projectionPacketLength == 0 && !g_projectionQueue.empty()) {
LoadNextQueuedProjection();
}
g_retry.Arm();
Log("armed apc=5405 actor=%p projection_ready=%u queue_depth=%zu queue_bytes=%zu high_water=%zu drops=%u",
actor, g_projectionPending ? 1u : 0u, g_projectionQueue.size(),
g_projectionQueueBytes, g_projectionQueueHighWater,
g_projectionQueueDropCount);
}
bool PollFixedApc5405() {
if (!Enabled() || !g_retry.Pending()) return false;
if (GetCurrentThreadId() != g_armedThread) return false;
uintptr_t base =
reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
void* actor = nullptr;
if (!ReadControlledActor(base, &actor) ||
actor != g_armedActor) {
CancelFixedApc5405();
Log("cancelled reason=controlled-actor-changed armed=%p current=%p",
g_armedActor, actor);
return false;
}
bool routeReady = ValidateNativeRoute(base, actor);
bool nativeReady = routeReady && NativeReady(base, actor);
int sceneMode = -1;
bool sceneReady =
nativeReady && DungeonSceneReady(base, &sceneMode);
if (!g_retry.ShouldSubmit(routeReady, nativeReady, sceneReady, g_projectionPending)) {
if (g_waitLogCount < 8) {
Log("waiting route_ready=%u native_ready=%u scene_ready=%u projection_ready=%u scene_mode=%d actor=%p",
routeReady ? 1u : 0u,
nativeReady ? 1u : 0u,
sceneReady ? 1u : 0u,
g_projectionPending ? 1u : 0u,
sceneMode,
actor);
++g_waitLogCount;
}
return false;
}
if (!Submit(base, actor)) {
// Native exception is terminal for this run; do not duplicate-create.
return false;
}
return true;
}
void NewNativeSafePoint(int opcode) {
if (!g_native.newNative || !g_newNativeHooksReady) return;
static thread_local bool active = false;
if (active || g_projectionApplyActive || g_directLoopbackInReceive) return;
active = true;
__try {
const uintptr_t base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
void* actor = nullptr;
const bool hasActor = ReadControlledActor(base, &actor);
int mode = -1;
const bool dungeon = DungeonSceneReady(base, &mode);
if (g_armedActor && (!hasActor || actor != g_armedActor)) {
CancelFixedApc5405();
Log("new-native lifecycle cancelled reason=owner-changed opcode=%d mode=%d", opcode, mode);
}
// A room transition can temporarily leave mode 3. Keep that run's
// identities and pause native work until the same owner is ready again.
if (dungeon && hasActor) {
if (!g_armedActor) ArmFixedApc5405();
PollFixedApc5405();
ApplyPendingProjection();
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
Log("new-native safe-point exception opcode=%d code=0x%08X", opcode, GetExceptionCode());
}
active = false;
}
void CancelFixedApc5405() {
g_retry.Cancel();
g_armedActor = nullptr;
g_armedThread = 0;
g_waitLogCount = 0;
ClearQueuedProjections("cancel");
g_liveProjectionActors.clear();
ClearCurrentProjectionPacket();
}
} // namespace dnf90_companion_p0