3887 lines
169 KiB
C++
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
|