#include "90CN_companion_p0.h" #include "90CN_companion_projection.h" #include "90CN_companion_quickbar_policy.h" #include "90CN_companion_native_bindings.h" #include #include #include #include #include #include #include #include #include #include #include // 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(info->ExceptionRecord->ExceptionAddress); if (info->ExceptionRecord->NumberParameters > 1) { g_lastProjectionFaultAddress = static_cast( 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(frame); const std::uintptr_t ret = *reinterpret_cast(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 packet; int dispatcherArg = 0; }; thread_local std::deque 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(GetModuleHandleW(nullptr)); if (!base || !pointer) return false; __try { const auto* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) { return false; } const auto* nt = reinterpret_cast( base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; const uintptr_t value = reinterpret_cast(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(GetModuleHandleW(nullptr)); auto* reader = reinterpret_cast( base + g_native.kProjectionReaderStateRva); const unsigned char* cursor = reader ? reader->cursor : nullptr; unsigned vtable = 0; unsigned getter = 0; __try { if (actor) { vtable = *reinterpret_cast(actor); if (vtable) { getter = *reinterpret_cast(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(target), expected, patchLength) != 0) { Log("%s prologue mismatch at 0x%08X", label, target); return false; } unsigned char* trampoline = static_cast(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(target), patchLength); trampoline[patchLength] = 0xE9; *reinterpret_cast(trampoline + patchLength + 1) = static_cast( static_cast(target + patchLength) - reinterpret_cast(trampoline + patchLength + 5)); DWORD oldProtection = 0; if (!VirtualProtect(reinterpret_cast(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(jump + 1) = static_cast( reinterpret_cast(detour) - static_cast(target + 5)); memcpy(reinterpret_cast(target), jump, sizeof(jump)); for (size_t i = sizeof(jump); i < patchLength; ++i) { *reinterpret_cast(target + i) = 0x90; } DWORD ignoredProtection = 0; VirtualProtect(reinterpret_cast(target), patchLength, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(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(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(&CompanionEquipmentProbeDetour), &trampoline, "companion-equipment-probe")) { Log("equipment-probe hook install failed target=0x%08X", target); return false; } g_companionEquipmentTrampoline = trampoline; g_equipmentApplyOriginal = reinterpret_cast(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( reinterpret_cast(aic) + g_native.kAicEquipmentVectorOffset); auto* end = *reinterpret_cast( reinterpret_cast(aic) + g_native.kAicEquipmentVectorOffset + 4); if (begin && end >= begin && ((end - begin) % 12) == 0) { aicRows = static_cast((end - begin) / 12); for (unsigned i = 0; i < aicRows; ++i) { auto* row = reinterpret_cast(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(info); for (int off = 0; off + 4 <= 0x4000; off += 4) { const unsigned value = *reinterpret_cast(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(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(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(GetModuleHandleW(nullptr)); void* info = nullptr; using UserInfoFn = void* (__thiscall*)(void*); __try { info = reinterpret_cast( const_cast(getter))(actor); } __except (EXCEPTION_EXECUTE_HANDLER) { info = nullptr; } LogCompanionEquipmentVerify("before", actor, info, aic); auto* reader = reinterpret_cast( base + g_native.kProjectionReaderStateRva); const ProjectionNativeReaderState saved = *reader; int result = -1; unsigned long code = 0; __try { reader->cursor = mode1 + 124; reader->remaining = static_cast(mode1Length - 124); reader->packet = mode1Packet; reader->packetLength = static_cast(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 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 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(incoming.sourceCharacter), static_cast(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(module); const IMAGE_DOS_HEADER* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; const IMAGE_NT_HEADERS* nt = reinterpret_cast( base + static_cast(dos->e_lfanew)); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; const size_t imageSize = static_cast(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(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(strlen(prefix)), &written, nullptr); WriteFile(file, line, static_cast(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(base + g_native.kControlledActorGetterRva)() : *reinterpret_cast(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( 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(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( 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(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(actor); if (!vtable) { Log("blocked reason=actor-vtable-null actor=%p", actor); return false; } const uintptr_t serializer = *reinterpret_cast( vtable + g_native.kSerializerVtableOffset); if (serializer != base + g_native.kExpectedSerializerRva) { Log("blocked reason=serializer-mismatch actor=%p actual=%p expected=%p", actor, reinterpret_cast(serializer), reinterpret_cast(base + g_native.kExpectedSerializerRva)); return false; } void* owner = *reinterpret_cast( reinterpret_cast(actor) + g_native.kActorOwnerContextOffset); if (!owner) { return false; } void* runtime = *reinterpret_cast( reinterpret_cast(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(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( base + g_native.kSceneContextGetterRva); SceneModeFn getSceneMode = reinterpret_cast(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(view.job), static_cast(view.grow), static_cast(view.sourceCharacter), actor); return false; } descriptor = baseline.descriptor; const std::int32_t baselineLevel = descriptor.minimumLevel; descriptor.minimumLevel = static_cast(view.level); descriptor.maximumLevel = static_cast(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(view.sourceCharacter), static_cast(view.job), static_cast(view.grow), static_cast(baseline.sourceGrow), static_cast((view.grow >> 4) & 0x0Fu), descriptor.apcCode, static_cast(baseline.aicJob), static_cast(baseline.aicGrow), baseline.bridged ? 1u : 0u, baselineLevel, static_cast(view.level)); } NativeSummonApcFn summon = reinterpret_cast( 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(GetModuleHandleW(nullptr)); g_newNativeHooksReady = false; g_native = NewNativeCompanionBindings(); __try { const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(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(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(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(GetModuleHandleW(nullptr)); if (!base || !payload || length < 0) { g_directLoopbackFailed = true; return; } const uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller != base + g_native.kPayloadEncodeReturnRva) { return; } __try { void* writer = *reinterpret_cast(base + g_native.kWriterPointerRva); void* dispatcher = *reinterpret_cast(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(writer); if (!vtable) { g_directLoopbackFailed = true; return; } const uintptr_t receiveAddress = *reinterpret_cast(vtable + 0x04); if (receiveAddress != base + g_native.kExpectedReceiveRva) { Log("blocked reason=direct-op40-receive-mismatch actual=%p expected=%p", reinterpret_cast(receiveAddress), reinterpret_cast(base + g_native.kExpectedReceiveRva)); g_directLoopbackFailed = true; return; } unsigned char* packet = reinterpret_cast( reinterpret_cast(writer) + g_native.kPacketBufferOffset); unsigned char* cursor = *reinterpret_cast( reinterpret_cast(writer) + g_native.kWriterCursorOffset); if (!cursor || cursor < packet) { g_directLoopbackFailed = true; return; } const std::size_t totalLength = static_cast(cursor - packet); if (totalLength < DirectOp40PacketContract::kHeaderSize || totalLength > 4096 || payload != packet + DirectOp40PacketContract::kHeaderSize || static_cast(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(receiveAddress); g_directLoopbackInReceive = true; receive(writer, 0, packet, static_cast(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(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(length) : 0; const bool contractMatch = DirectOp40PeerDeliveryContract::Matches( g_directLoopbackArmed, g_directLoopbackHandled, g_directLoopbackInReceive, static_cast(GetCurrentThreadId()), static_cast(g_armedThread), packet, packetLength, routeKind, mode); if (contractMatch) { uintptr_t base = reinterpret_cast(GetModuleHandleW(nullptr)); __try { void* writer = *reinterpret_cast(base + g_native.kWriterPointerRva); if (writer) { unsigned char* expectedPacket = reinterpret_cast( reinterpret_cast(writer) + g_native.kPacketBufferOffset); unsigned char* cursor = *reinterpret_cast( reinterpret_cast(writer) + g_native.kWriterCursorOffset); const std::size_t expectedLength = cursor && cursor >= expectedPacket ? static_cast(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(source); std::uint32_t hash = 2166136261u; auto mix = [&](const void* data, std::size_t length) { const std::uint8_t* p = reinterpret_cast(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(bytes + g_native.kAicEquipmentVectorOffset); const std::uintptr_t end = *reinterpret_cast(bytes + g_native.kAicEquipmentVectorOffset + 4); if (begin && end >= begin && end - begin <= 12u * 32u) { mix(reinterpret_cast(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(GetModuleHandleW(nullptr)); if (!base) return false; NativeRefStringAllocFn alloc = reinterpret_cast( base + g_native.kNativeRefStringAllocRva); NativeRefStringFreeFn freeBlock = reinterpret_cast( base + g_native.kNativeRefStringFreeRva); NativeRefStringRefFn increment = *reinterpret_cast( base + g_native.kNativeRefStringIncIatRva); NativeRefStringRefFn decrement = *reinterpret_cast( base + g_native.kNativeRefStringDecIatRva); if (!alloc || !freeBlock || !increment || !decrement) { return false; } __try { wchar_t* oldData = *reinterpret_cast(stringField); if (!oldData) return false; std::uint8_t* oldHeader = reinterpret_cast(oldData) - 8; const std::uint32_t oldBytes = *reinterpret_cast(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( alloc(8u + newBytes)); if (!newHeader) return false; *reinterpret_cast(newHeader) = 0; *reinterpret_cast(newHeader + 4) = static_cast(newBytes); memcpy(newHeader + 8, text, newBytes); increment(newHeader); *reinterpret_cast(stringField) = reinterpret_cast(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(0x7FFFFFFFu)) { return false; } *grewOut = false; __try { std::uint8_t* object = destination + objectOffset; std::uintptr_t begin = *reinterpret_cast(object + 4); std::uintptr_t end = *reinterpret_cast(object + 8); std::uintptr_t capacityEnd = *reinterpret_cast(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(GetModuleHandleW(nullptr)); NativeVectorAllocateFn allocate = reinterpret_cast( base + allocateRva); NativeVectorFreeFn freeData = reinterpret_cast( base + g_native.kNativeVectorFreeRva); if (!allocate || !freeData) return false; NativeVectorStorage temporary{}; if (!allocate( &temporary, static_cast(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(temporary.begin)); } return false; } const std::uintptr_t oldBegin = begin; *reinterpret_cast(object + 4) = temporary.begin; *reinterpret_cast(object + 8) = temporary.begin; *reinterpret_cast(object + 12) = temporary.capacityEnd; if (oldBegin != 0) { freeData(reinterpret_cast(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(unit); } name[view.nameUnits] = L'\0'; std::uint8_t* dest = reinterpret_cast(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(view.sourceCharacter), static_cast(view.job), static_cast(view.grow)); return false; } const std::uint32_t baselineJob = *reinterpret_cast(dest + 0x0C); const std::uint32_t baselineGrow = *reinterpret_cast(dest + 0x10); if (baselineJob != static_cast(baseline.aicJob) || baselineGrow != static_cast(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(view.sourceCharacter), static_cast(view.job), static_cast(view.grow), static_cast(baseline.aicJob), static_cast(baseline.aicGrow), static_cast(baselineJob), static_cast(baselineGrow), baseline.descriptor.apcCode); return false; } if (ProjectionGrowBridgeContract::ShouldApplyFirstGrow( baseline)) { *reinterpret_cast( dest + 0x10) = static_cast( baseline.sourceGrow); Log("dynamic-aic grow bridge source=%u job=%u aic_grow=%u source_grow=%u apc=%d", static_cast(view.sourceCharacter), static_cast(baseline.sourceJob), static_cast(baseline.aicGrow), static_cast(baseline.sourceGrow), baseline.descriptor.apcCode); } if (ProjectionGrowBridgeContract::ShouldApplyAwakening( baseline, view.grow)) { const std::uint8_t awakening = ProjectionGrowBridgeContract::AwakeningStage( view.grow); *reinterpret_cast( dest + 0x14) = static_cast(awakening); Log("dynamic-aic awakening bridge source=%u job=%u packed_grow=0x%02X awakening=%u apc=%d", static_cast(view.sourceCharacter), static_cast(baseline.sourceJob), static_cast(view.grow), static_cast(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(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(view.sourceCharacter), destination); return false; } const std::size_t oldEquipmentCount = (equipEnd - equipBegin) / 12u; std::uint8_t* equipmentRows = reinterpret_cast(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( 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( dest + g_native.kAicEquipmentVectorOffset + 4) = equipBegin + view.equipmentCount * 12u; const std::size_t oldQuickCount = (quickEnd - quickBegin) / 8u; std::uint8_t* quickRows = reinterpret_cast(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( 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( dest + g_native.kAicQuickSkillVectorOffset + 4) = quickBegin + view.quickSkillCount * 8u; const std::size_t oldSkillCount = (skillEnd - skillBegin) / 8u; std::uint8_t* skillRows = reinterpret_cast(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( 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( 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(view.sourceCharacter), static_cast(view.level), static_cast(view.job), static_cast(view.grow), static_cast(baseline.aicJob), static_cast(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(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(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(0x80000001u); } const std::size_t available = totalLength > 0 ? static_cast(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(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(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(actor); std::uint8_t* aic = *reinterpret_cast( 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( aic + g_native.kAicRuntimeKeyProgramVectorOffset); std::uintptr_t end = *reinterpret_cast( aic + g_native.kAicRuntimeKeyProgramVectorOffset + 4); std::uintptr_t capacity = *reinterpret_cast( 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(begin), reinterpret_cast(end), reinterpret_cast(capacity)); return false; } const std::size_t programCount = (end - begin) / sizeof(void*); void** programs = reinterpret_cast(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(programs[probe]); if (!probeProgram) continue; const std::uintptr_t probeBegin = *reinterpret_cast( probeProgram + g_native.kRuntimeKeyProgramBytecodeBeginOffset); const std::uintptr_t probeEnd = *reinterpret_cast( 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(probeBegin + 0x100) : 0, probeSize > 0x12C ? *reinterpret_cast(probeBegin + 0x128) : 0); } return false; } // 关闭普攻时把 CompanionBasicAttack 的技能码改成空占位码, // 只禁用技能冷却后的普攻回退,不触碰 0..11 的 12 格技能程序。 if (!CompanionBasicAttackEnabled()) { void* basicProgram = programs[Contract::kBasicAttackProgramIndex]; std::uint8_t* basic = reinterpret_cast(basicProgram); if (basic) { std::uintptr_t basicBegin = *reinterpret_cast( basic + g_native.kRuntimeKeyProgramBytecodeBeginOffset); std::uintptr_t basicEnd = *reinterpret_cast( basic + g_native.kRuntimeKeyProgramBytecodeEndOffset); std::uintptr_t basicCapacity = *reinterpret_cast( basic + g_native.kRuntimeKeyProgramBytecodeCapacityOffset); if (basicBegin && basicEnd >= basicBegin && basicCapacity >= basicEnd && basicEnd - basicBegin >= g_native.kRuntimeKeySkillCodeOffset + 4) { *reinterpret_cast( 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( programs[directIndex]); std::uintptr_t codeBegin = *reinterpret_cast( program + g_native.kRuntimeKeyProgramBytecodeBeginOffset); std::uintptr_t codeEnd = *reinterpret_cast( program + g_native.kRuntimeKeyProgramBytecodeEndOffset); std::uintptr_t codeCapacity = *reinterpret_cast( program + g_native.kRuntimeKeyProgramBytecodeCapacityOffset); const std::size_t codeSize = codeEnd >= codeBegin ? static_cast(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(codeBegin), reinterpret_cast(codeEnd), reinterpret_cast(codeCapacity), codeSize); return false; } std::uint8_t* code = reinterpret_cast( 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(code + base + 0x00), *reinterpret_cast(code + base + 0x04), *reinterpret_cast(code + base + 0x08), *reinterpret_cast(code + base + 0x0C), *reinterpret_cast(code + base + 0x10), *reinterpret_cast(code + base + 0x14), *reinterpret_cast(code + base + 0x18), *reinterpret_cast(code + base + 0x1C), *reinterpret_cast(code + base + 0x20), *reinterpret_cast(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( 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(programs[currentlyBound ? Contract::kDirectPoolBase + i : i]); directCooldown[i] = *reinterpret_cast(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( 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(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(node + 8) != entry.aiVm || *reinterpret_cast(node + 0x1C) != 57 || *reinterpret_cast(node + 0x58) != 1 || *reinterpret_cast(node + 0x0C) != slot || *reinterpret_cast(node + 0x14)) return false; int zero = 0; const auto* index = parameter(node + 0x4C, &zero); if (!index || *index != static_cast(slot)) return false; chain[slot] = node; node = *reinterpret_cast(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(node + 8) != entry.aiVm) return false; if (*reinterpret_cast(node + 0x1C) == 57 && *reinterpret_cast(node + 0x0C) == 11) { auto* next = *reinterpret_cast(node + 0x18); if (next && *reinterpret_cast(next + 0x1C) == 57 && *reinterpret_cast(next + 0x0C) == 10 && !BindAreaCooldownChain(entry, node, parameter)) return false; } if (*reinterpret_cast(node + 0x1C) == 4 && *reinterpret_cast(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(node + 0x14), parameter, depth + 1, nodes) && BindModeConditions(entry, *reinterpret_cast(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(vm); return *reinterpret_cast(bytes) == base + g_native.kAIControlledVmVtableRva && *reinterpret_cast(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(GetModuleHandleW(nullptr)); static const unsigned char parameterSignature[] = {0x55,0x8B,0xEC,0x6A,0xFF,0x68,0xD8,0x3E,0xCD,0x03}; if (!g_native.newNative && memcmp(reinterpret_cast(base + g_native.kAIParameterRva), parameterSignature, sizeof(parameterSignature))) return false; const auto parameter = reinterpret_cast(base + g_native.kAIParameterRva); auto* vm = *reinterpret_cast(reinterpret_cast(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(vm + 0x6CC); const auto end = *reinterpret_cast(vm + 0x6D0); if (!begin || end < begin || end - begin != 4 * sizeof(void*)) return false; auto* roots = reinterpret_cast(begin); auto* state = roots[2]; if (!state || *reinterpret_cast(state + 0x1C) != 4) return false; auto* party = *reinterpret_cast(state + 0x14); if (!party || *reinterpret_cast(party + 0x1C) != 47) return false; auto* buff = *reinterpret_cast(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(buff + 0x1C) != 52 || *reinterpret_cast(buff + 8) != vm || *reinterpret_cast(buff + floatMapCountOffset) != 2) return false; auto* cooldown = *reinterpret_cast(buff + 0x14); if (!cooldown || *reinterpret_cast(cooldown + 0x1C) != 57 || *reinterpret_cast(cooldown + floatMapCountOffset) != 1 || *reinterpret_cast(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(heal + 8) != vm || *reinterpret_cast(heal + 0x1C) != 12 || *reinterpret_cast(heal + 0x20) != 0x31 || *reinterpret_cast(heal + floatMapCountOffset) != 1 || *reinterpret_cast(heal + 0x0C) != -1 || *reinterpret_cast(heal + 0x10) != -1 || *reinterpret_cast(heal + 0x14) || *reinterpret_cast(heal + 0x18)) return false; float* threshold = parameter(heal + 0x4C, &zero); if (!threshold || *threshold <= 0 || *threshold > 100 || *reinterpret_cast(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(roots[1] + 0x1C) != 4 || *reinterpret_cast(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(reinterpret_cast(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(reinterpret_cast(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(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(owner); auto* manager = *reinterpret_cast(ownerBytes + g_native.kActorOwnerContextOffset); if (!manager) return false; const auto begin = *reinterpret_cast(manager + 0x0C); const auto end = *reinterpret_cast(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(cursor) == entry.actor) { found = true; break; } } if (!found || entry.actor == owner) return false; auto* actor = reinterpret_cast(entry.actor); if (*reinterpret_cast(actor + g_native.kActorOwnerContextOffset) != manager) return false; if (*reinterpret_cast(actor + g_native.kActorAicOffset) != entry.aic) return false; auto** table = *reinterpret_cast(actor); using KeyFn = std::uint32_t (__thiscall*)(void*); return static_cast(reinterpret_cast(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(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(base + g_native.kAICooldownRva), coolSignature, sizeof(coolSignature)) || memcmp(reinterpret_cast(base + g_native.kAIKeyReadyRva), readySignature, sizeof(readySignature)))) return false; auto* aic = reinterpret_cast(entry.aic); const auto begin = *reinterpret_cast(aic + g_native.kAicRuntimeKeyProgramVectorOffset); const auto end = *reinterpret_cast(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(begin)[slot]; if (!program) return false; const auto code = *reinterpret_cast(program + 8); const auto codeEnd = *reinterpret_cast(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(code + codeOffset) != expectedSkillCode) return false; const auto cooling = reinterpret_cast(base + g_native.kAICooldownRva); const auto ready = reinterpret_cast(base + g_native.kAIKeyReadyRva); return cooling(program) == 0 && ready(program, 2) != 0; } void SetOriginalAICondition(void* original) { g_originalAICondition = reinterpret_cast(original); Log("quickbar condition hook original=%p", original); } std::uint32_t NativeSupportActionTimestamp(const LiveProjectionActor& entry, std::uint32_t id) { auto* aic = reinterpret_cast(entry.aic); const auto begin = *reinterpret_cast(aic + g_native.kAicRuntimeKeyProgramVectorOffset); const auto end = *reinterpret_cast(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(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(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(reinterpret_cast(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(id); present = g_originalAICondition(entry.buffCondition) != 0; } __finally { *entry.buffSkillParameter = saved; *recipient = savedRecipient; } return present; } bool NativeLeaderNeedsHealing(const LiveProjectionActor& entry) { auto** recipient = reinterpret_cast(reinterpret_cast(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(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(base + g_native.kActorClockRva), clockAddress, sizeof(clockAddress)) || memcmp(reinterpret_cast(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(base + g_native.kActorClockRva)(entry.actor); if (!clock) return false; now = reinterpret_cast(base + g_native.kClockReadRva)(clock); return true; } int ReadNativeSelfHpPercent(const LiveProjectionActor& entry) { auto** recipient = reinterpret_cast(reinterpret_cast(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(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(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(str)); add("str", static_cast(str) / 10u); add("vit", static_cast(vit)); add("vit", static_cast(vit) / 10u); add("int", static_cast(intel)); add("int", static_cast(intel) / 10u); add("spr", static_cast(spr)); add("spr", static_cast(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(p); const std::uint16_t half = *reinterpret_cast(p); const float single = *reinterpret_cast(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(candidate)) { *field = targets[i].field; *value = half; return true; } if (single == static_cast(candidate)) { *field = targets[i].field; *value = candidate; return true; } } return false; } void DumpCompanionStatWindow(std::uintptr_t address) { __try { LogCompanionStatHex(reinterpret_cast(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(cursor), &mbi, sizeof(mbi)) != sizeof(mbi)) break; const std::uintptr_t regionEnd = reinterpret_cast(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(addr), targets, targetCount, &field, &value) && n < 96) { hits[n].value = value; hits[n].address = static_cast(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(hits[i].address); ++confirmed; Log("stat-probe %s off=%+lld addr=0x%08llX kind=%s value=%u", label, static_cast(hits[i].address) - static_cast(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(blob); const std::uint32_t mp = *reinterpret_cast(blob + 4); const std::int16_t str = *reinterpret_cast(blob + 8); const std::int16_t vit = *reinterpret_cast(blob + 10); const std::int16_t intel = *reinterpret_cast(blob + 12); const std::int16_t spr = *reinterpret_cast(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(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(actor) + slot.offset; target = *reinterpret_cast(at); LogCompanionStatHex(at, 0x40); } __except (EXCEPTION_EXECUTE_HANDLER) { target = 0; } Log("stat-probe legacy %s +0x%X target=0x%08X", slot.name, static_cast(slot.offset), static_cast(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(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(const_cast(getter))(actor); const void* abilityGetter = table[g_native.kActorAbilityVtableOffset / 4]; ability = reinterpret_cast(const_cast(abilityGetter))(actor, 0); aic = *reinterpret_cast( reinterpret_cast(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(info), 0x1000, 0x8000, targets, targetCount); } if (aic) { ScanCompanionStatRegion("aic", reinterpret_cast(aic), 0x1000, 0x8000, targets, targetCount); } if (ability) { ScanCompanionStatRegion("ability", reinterpret_cast(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(entry.aic); const auto begin = *reinterpret_cast(aic + g_native.kAicRuntimeKeyProgramVectorOffset); const auto end = *reinterpret_cast(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(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(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(g_supportSelections)); } __try { auto* node = reinterpret_cast(condition); const unsigned int type = *reinterpret_cast(node + 0x1C); if (type != 4 && type != 52 && type != 57) return original(condition); for (auto& entry : g_liveProjectionActors) { if (!entry.aiVm || *reinterpret_cast(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(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(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(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(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(chosen); *reinterpret_cast(reinterpret_cast(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(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(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(GetModuleHandleW(nullptr)); const uintptr_t alignment = reinterpret_cast(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(base + rva), signature, sizeof(signature))) return nullptr; return reinterpret_cast(base + rva); } bool SetLiveProjectionLevel(const LiveProjectionActor& entry, std::uint16_t level) { auto* actor = reinterpret_cast(entry.actor); auto* aic = reinterpret_cast(entry.aic); auto** table = *reinterpret_cast(actor); using AbilityFn = void* (__thiscall*)(void*, int); using UserInfoFn = void* (__thiscall*)(void*); void* ability = reinterpret_cast(table[g_native.kActorAbilityVtableOffset / 4])(actor, 0); auto* info = reinterpret_cast( reinterpret_cast(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(aic + g_native.kAicUserInfoOffset) || *reinterpret_cast(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(targets[i]) + 4); // Same plain assignment as the native APC level setter sub_2482890. *reinterpret_cast(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(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(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(table[g_native.kActorStatsVtableOffset / 4])(actor, stats); reinterpret_cast(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(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(entry.actor); using ExpFn = void (__thiscall*)(void*, std::uint64_t, int); reinterpret_cast(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(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(table[g_native.kActorStatsVtableOffset / 4])(entry.actor, stats); reinterpret_cast(table[g_native.kActorRecalculateVtableOffset / 4])(entry.actor, 1); if (view.skillCount) memcpy(reinterpret_cast(skillBegin), view.skills, view.skillCount * 8); memcpy(reinterpret_cast(quickBegin), view.quickSkills, view.quickSkillCount * 8); *reinterpret_cast(aic + g_native.kAicSkillVectorOffset + 4) = skillBegin + view.skillCount * 8; *reinterpret_cast(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(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(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(GetCurrentThreadId()), static_cast(g_armedThread), objectKey, actor, result)) { if (g_projectionApcObjectKey == 0 || (g_projectionApcObjectKey == objectKey && g_projectionApcActor == actor)) { g_projectionApcObjectKey = static_cast(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(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(unit); } if (validName) { actorName[view.nameUnits] = L'\0'; actorNameApplied = AssignNativeRefWideString( reinterpret_cast(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(view.aiBehavior), static_cast(view.aiClass), view.aiComboEnabled ? 1u : 0u, static_cast(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(GetModuleHandleW(nullptr)); auto* reader = reinterpret_cast(base + g_native.kProjectionReaderStateRva); auto** errorCopy = reinterpret_cast(base + g_native.kProjectionReaderErrorCopyRva); auto setCursor = reinterpret_cast(base + g_native.kProjectionReaderSetCursorRva); auto setLength = reinterpret_cast(base + g_native.kProjectionReaderSetLengthRva); auto setPacket = reinterpret_cast(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(length)); setCursor(packet + dnf90_companion_projection::kPacketHeaderSize); setLength(static_cast(length - dnf90_companion_projection::kPacketHeaderSize)); result = g_projectionClass0Dispatch(g_projectionDispatcherArg, reinterpret_cast(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(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(base + g_native.kProjectionReaderSetCursorRva), cursorSignature, sizeof(cursorSignature)) || memcmp(reinterpret_cast(base + g_native.kProjectionReaderSetLengthRva), lengthSignature, sizeof(lengthSignature)) || memcmp(reinterpret_cast(base + g_native.kProjectionReaderSetPacketRva), packetSignature, sizeof(packetSignature))))) { g_projectionClass0Dispatch = nullptr; Log("projection class0 dispatcher disabled: native reader signature mismatch"); return; } g_projectionClass0Dispatch = reinterpret_cast(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 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(pending.aiComboPresetHash) << 8) | (static_cast(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(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(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(view.sourceCharacter), static_cast(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(view.sourceCharacter), static_cast(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(headerLength), static_cast(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(headerLength), static_cast(opcode), static_cast(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(headerLength), static_cast(opcode), bodyLength, static_cast(dnf90_companion_projection::Read32(body)), static_cast(body[4]), static_cast(body[5]), static_cast(dnf90_companion_projection::Read16(body + 6)), static_cast(dnf90_companion_projection::Read64(body + 8)), static_cast(mode0Length), static_cast(mode1Length), nameUnits, equipmentCount, static_cast(dnf90_companion_projection::Read16(body + 62)), skillCount, quickSkillCount, dynamicBytes, mode0Offset, mode0Byte0, mode0Version, mode0Key, dnf90_companion_projection::kBodyHeaderSize + dynamicBytes + static_cast(mode0Length) + static_cast(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(mode0PacketLength); const std::uint32_t mode1PacketLength32 = static_cast(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(view.sourceCharacter), static_cast(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(view.sourceCharacter), static_cast(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(view.sourceCharacter), static_cast(view.revision), g_projectionApcObjectKey, mode1ExceptionCode, reinterpret_cast(mode1ExceptionAddress), reinterpret_cast(mode1FaultAddress), mode1ExceptionAddress > reinterpret_cast(GetModuleHandleW(nullptr)) ? mode1ExceptionAddress - reinterpret_cast(GetModuleHandleW(nullptr)) : 0, reinterpret_cast(mode1Stack[0]), reinterpret_cast(mode1Stack[1]), reinterpret_cast(mode1Stack[2]), reinterpret_cast(mode1Stack[3]), reinterpret_cast(mode1Stack[4]), reinterpret_cast(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(view.sourceCharacter), static_cast(view.revision), g_projectionApcObjectKey, mode0ExceptionCode, g_projectionMode0RetryCount, kProjectionMode0RetryLimit, g_projectionApcActor, reinterpret_cast(mode0ExceptionAddress), reinterpret_cast(mode0FaultAddress), mode0ExceptionAddress > reinterpret_cast(GetModuleHandleW(nullptr)) ? mode0ExceptionAddress - reinterpret_cast(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(view.sourceCharacter), static_cast(view.revision), failedKey, mode0ExceptionCode, g_projectionQueue.size(), g_armedActor, reinterpret_cast(mode0ExceptionAddress), reinterpret_cast(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(view.sourceCharacter), static_cast(view.revision), failedKey, mode0ExceptionCode, g_armedActor, reinterpret_cast(mode0ExceptionAddress), reinterpret_cast(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(view.sourceCharacter), static_cast(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(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(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(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