// 90CN / 86JP.dll - current NoPack TCLS launch compatibility plus the // current-EXE upper-body plaintext codec bypass. // // This DLL keeps the six current-EXE TCLS launch helpers needed by the direct // launcher and patches the proven sub_33AAFE0/sub_33AB0A0 upper-body codec // boundaries plus bounded DPROTO and inbound-route transport compatibility. // It also applies bounded current-EXE UI/state compatibility for native views // whose managers and actor records consume separate server projections. It // does not alter send/recv results, chain-load old DLLs, replay server packet // bodies, or own gameplay state. A bounded first-login receive probe observes // only the current game socket until the first recognized client progress. #include "90CN.h" #include "client_assets.h" #include "90CN_expert_job_projection.h" #include "90CN_seria_minimap.h" #include "90CN_premium_clock_policy.h" #include "90CN_first_login_recovery_policy.h" #include "90CN_companion_p0.h" #include "90CN_companion_new_native.h" #include "90CN_ui_iterator_policy.h" #include "90CN_combat_panel_layout.h" #include "90CN_dungeon_assist_presence.h" #include "90CN_digest_trace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" void InstallRaidProbe(); namespace { constexpr uintptr_t kCipherEncodeRva = 0x02FAAFE0; // sub_33AAFE0 constexpr uintptr_t kCipherDecodeRva = 0x02FAB0A0; // sub_33AB0A0 constexpr uintptr_t kCipherEncodeCallRva = 0x030569A0; // upper_pkt_flush_send13 call-site constexpr uintptr_t kCodecSetKey0Rva = 0x03077B30; constexpr uintptr_t kCodecSetKey2Rva = 0x03076E90; constexpr uintptr_t kCodecSetKey3Rva = 0x03077E30; constexpr uintptr_t kCodecSetKey7Rva = 0x03075900; constexpr uintptr_t kCodecSetKey8Rva = 0x03078260; constexpr uintptr_t kTclsParseRva = 0x015AA570; constexpr uintptr_t kTclsFetchLoginRva = 0x01AF7C40; constexpr uintptr_t kTclsFetchTextRva = 0x01AF6970; constexpr uintptr_t kTclsFetchCryptoRva = 0x01AF6A50; constexpr uintptr_t kTclsFetchTailRva = 0x01AF6DE0; constexpr uintptr_t kTclsTailRva = 0x01AF6F50; constexpr uintptr_t kGameWideAssignRva = 0x00038270; constexpr uintptr_t kChecksumRva = 0x030545B0; constexpr uintptr_t kDprotoRoutePredicateRva = 0x030734E0; // sub_34734E0 constexpr uintptr_t kDprotoOutgoingGateRva = 0x030738FA; // JZ after mode-7 predicate constexpr uintptr_t kDprotoOutgoingCloneRva = 0x0307391C; // first mode-7 call constexpr uintptr_t kDprotoOutgoingResumeRva = 0x03073921; // after stolen 5 bytes at clone site constexpr uintptr_t kDprotoOutgoingReturnRva = 0x03073A2E; constexpr uintptr_t kDprotoFalseResumeRva = 0x03073A2F; // native predicate-false epilogue constexpr uintptr_t kQuestAutoCompleteCallContextRva = 0x02C4DBC8; // sub_304DB90 selected quest confirmation constexpr uintptr_t kQuestAutoCompleteCallRva = 0x02C4DBD2; constexpr uintptr_t kQuestAutoCompleteCaptureRva = 0x02C4E12E; // sub_304E070 stores selected quest at callback+0x30 constexpr uintptr_t kQuestAutoCompleteSendRva = 0x015EE300; // sub_19EE300 constexpr uintptr_t kGameAllocatorRva = 0x00669630; constexpr uintptr_t kGameMemcpyRva = 0x037106F0; constexpr uintptr_t kChannelScriptDownloadRva = 0x0189FB00; // sub_1C9FB00 constexpr uintptr_t kChannelScriptLoadRva = 0x014A5360; // sub_18A5360 constexpr uintptr_t kChannelDirectoryApplyRva = 0x018A2440; // sub_1CA2440 constexpr uintptr_t kChannelRuntimeLoadRva = 0x014A2780; // sub_18A2780 constexpr uintptr_t kChannelCategoryInsertRva = 0x014A1FF0; // sub_18A1FF0 constexpr uintptr_t kChannelConnectRva = 0x018A0DA0; // sub_1CA0DA0 constexpr uintptr_t kChannelConnectorRva = 0x0189EAD0; // sub_1C9EAD0 constexpr uintptr_t kSelectedChannelPointerRva = 0x04D88FA8; // dword_5188FA8 constexpr uintptr_t kNativeConnectAddressRva = 0x04D50EB0; // dword_5190EB0 constexpr uintptr_t kNativeConnectPortRva = 0x04D50EB4; // word_5190EB4 constexpr uintptr_t kNativeConnectAux0Rva = 0x04D50EB8; // dword_5190EB8 constexpr uintptr_t kNativeConnectAux1Rva = 0x04D50EBC; // word_5190EBC constexpr uintptr_t kNativeConnectStateObjectRva = 0x04D51358; // unk_5191358 constexpr uintptr_t kNativeConnectStateCallRva = 0x03107F60; // sub_3507F60 constexpr uintptr_t kNativeEndpointStateResetRva = 0x0189E750; // sub_1C9E750 constexpr uintptr_t kResidentConnectionSingletonRva = 0x014A5110; // sub_18A5110 constexpr uintptr_t kResidentConnectionResetRva = 0x014A5180; // sub_18A5180 // Current-EXE ChannelConnect ultimately routes the game TCP transport through // sub_3454640()->sub_3455630(), and sub_3455630 calls sub_35BFD00 with that // same object as this. sub_35BF4A0 is the matching native close/reset method: // it closes [this+0x1C4], writes INVALID_SOCKET, and clears transport state. // Use it only after the first-login resident reset has succeeded. constexpr uintptr_t kGameTransportSingletonRva = 0x03054640; // sub_3454640 constexpr uintptr_t kGameTransportSocketCloseRva = 0x031BF4A0; // sub_35BF4A0 constexpr uintptr_t kGameTransportSocketHandleOffset = 0x1C4; // Current EXE: getter 0225CF3C loads the existing router singleton. Its // 02261E63 gate rejects packets until [router+4] becomes nonzero. Read the // pointer directly: calling the lazy getter would change native startup order. // // RETAIL ADAPTATION (this deployment: DNF.exe TimeDateStamp=0x5B599504, // SizeOfImage=0x05FB2000). The router singleton is not at the DB4 address. // The retail loader instruction at 0x01E5CF3C is `A1 90 12 1B 05`, i.e. // `mov eax,[0x051B1290]`, so the pointer lives at 0x051B1290. Reading the DB4 // address returned four zero bytes, the ready gate never opened, the automatic // channel connection never fired and the client hung at "connecting to channel // server" until it reported "cannot connect". The gate instruction itself // (0x01E61E63 = 80 7E 04 00) is byte-identical in the retail binary, so only // this pointer address had to move. constexpr uintptr_t kPacketProcessorPointerRva = 0x04DB1290; constexpr uintptr_t kPacketProcessorPointerLoadRva = 0x01E5CF3C; constexpr uintptr_t kPacketProcessorReadyGateRva = 0x01E61E63; constexpr uintptr_t kPacketProcessorReadyOffset = 4; // Current-EXE guild creation UI: both branches below gate the final Create // button on manager state +0x2FC == 1 (member authentication approved). // Single-player mode keeps name validation, 300000-gold validation, and the // approved-name equality checks, but removes only this second-member gate. constexpr uintptr_t kGuildCreateClickMemberAuthJneRva = 0x02D53B46; // 75 2F constexpr uintptr_t kGuildCreateEnableMemberAuthJneRva = 0x02D53E7C; // 75 5A constexpr uintptr_t kResidentChannelRva = 0x04D89178; // unk_5189178 constexpr uintptr_t kChannelScriptLookupRva = 0x032B1910; // sub_36B1910 constexpr uintptr_t kChannelScriptStoreRva = 0x04D88FF0; // unk_5188FF0 constexpr uintptr_t kPvpChannelQueryRva = 0x01498C30; // sub_1898C30 // Fresh PK click on 2026-09-12: the category refresh queries several types at // VA 01E9918F, then the default list wrapper queries type 13 at VA 01E954DD. constexpr uintptr_t kPvpCategoryQueryReturnRva = 0x01A9918F; constexpr uintptr_t kPvpDefaultListQueryReturnRva = 0x01A954DD; // After the prerequisite gate, 01E96F50 -> 01E95CA0 -> 01E95500 selects // the same category through this native query wrapper. constexpr uintptr_t kPvpSelectionQueryReturnRva = 0x01A9553F; constexpr unsigned int kPvpDefaultListQueryType = 13; // The native exact-type query must reach the dedicated free-PvP listener. constexpr unsigned int kPvpResidentQueryType = 13; constexpr uintptr_t kHudServerIndexDescriptorRva = 0x04DB0EE4; // dword_51B0EE4 constexpr uintptr_t kHudServerIndexValueRva = 0x04DB0EE8; // dword_51B0EE8 constexpr uintptr_t kHudServerIndexLockRva = 0x04DB0EEC; // dword_51B0EEC constexpr uintptr_t kHudChannelIndexDescriptorRva = 0x04DB0EF0; // dword_51B0EF0 constexpr uintptr_t kHudChannelIndexValueRva = 0x04DB0EF4; // dword_51B0EF4 constexpr uintptr_t kHudChannelIndexLockRva = 0x04DB0EF8; // dword_51B0EF8 constexpr uintptr_t kServerClockDescriptorRva = 0x04DB0F20; // dword_51B0F20 constexpr uintptr_t kServerClockValueRva = 0x04DB0F24; // dword_51B0F24 constexpr uintptr_t kLocalClockDescriptorRva = 0x04DB0F28; // dword_51B0F28 constexpr uintptr_t kLocalClockValueRva = 0x04DB0F2C; // dword_51B0F2C constexpr uintptr_t kServerTickDescriptorRva = 0x04DB0F30; // dword_51B0F30 constexpr uintptr_t kServerTickValueRva = 0x04DB0F34; // dword_51B0F34 constexpr uintptr_t kAtomicSpinLockRva = 0x030DB470; // sub_34DB470 constexpr uintptr_t kAtomicSpinUnlockRva = 0x030DB4A0; // sub_34DB4A0 constexpr uintptr_t kObfuscatedStoreCase0Rva = 0x030DB910; // sub_34DB910 constexpr uintptr_t kObfuscatedStoreCase4Rva = 0x030DB990; // sub_34DB990 constexpr uintptr_t kObfuscatedStoreCase8Rva = 0x030DBA10; // sub_34DBA10 constexpr uintptr_t kSelectedPageApplyRva = 0x013D6BE0; // sub_17D6BE0 constexpr uintptr_t kSelectorCreateTickRva = 0x02FE9640; // sub_33E9640 constexpr uintptr_t kSelectorCreateTransitionRva = 0x02FE8390; // sub_33E8390 constexpr uintptr_t kSelectorRestrictionRva = 0x04DB27D0; // byte_51B27D0 constexpr uintptr_t kUIEventStampRva = 0x04CE0E04; // dword_50E0E04 constexpr uintptr_t kCreateUIClickRva = 0x01AAC2A0; // sub_1EAC2A0 constexpr uintptr_t kCreateUIOpenRva = 0x01AAC3E0; // sub_1EAC3E0 constexpr uintptr_t kUpperCreateSendRva = 0x02FD4990; // sub_33D4990 constexpr uintptr_t kClass0DispatchRva = 0x01E5CA00; // sub_225CA00 // Current EXE class0/op0x63 writes the mailbox unread model at sub_270B330, // but its zero-count branch skips the HUD recompute that hides owner 7. // Reuse the exact existing UI-manager refresh path after native dispatch. constexpr unsigned short kMailboxAlarmOpcode = 0x0063; constexpr uintptr_t kMailboxHudManagerRva = 0x01BB9D30; // sub_1FB9D30 constexpr uintptr_t kMailboxHudReadyRva = 0x01BB3390; // sub_1FB3390 constexpr uintptr_t kMailboxHudRefreshRva = 0x01BC2050; // sub_1FC2050 constexpr uintptr_t kClass1DispatchRva = 0x01E5C8A0; // sub_225C8A0 constexpr uintptr_t kClass0RegistryRva = 0x01E62A80; // sub_2262A80 constexpr uintptr_t kClass1RegistryRva = 0x01E628F0; // sub_22628F0 constexpr uintptr_t kDispatchLookupRva = 0x01E62430; // sub_2262430 // Current-EXE class0/op454 first selects and loads the native ClientOnly // keyboard table, then returns through the shared class0 dispatcher. A server // extension may append the persisted op198 text table after the one-byte key // type. The table contains I/C/J/K row families; movement, attack, skill-bar, // and ordinary UI keyboard bindings are the I rows. Validate every family, // resolve every complete token, then apply the rows through the same native // method used by sub_1FA77B0. constexpr uintptr_t kHotkeyManagerSingletonRva = 0x01BA6EF0; // sub_1FA6EF0 constexpr uintptr_t kHotkeyResolveActionRva = 0x01BA4410; // sub_1FA4410 constexpr uintptr_t kHotkeyApplyBindingRva = 0x01BA75F0; // sub_1FA75F0 constexpr unsigned int kHotkeyActionCount = 109; constexpr unsigned int kHotkeyPersistedTextMaxLength = 8192; // Current NoPack event 2347 globals. These are read only after the native // class0 handlers return, so the trace can distinguish route rejection from a // UI/resource creation failure without mutating client-owned activity state. constexpr uintptr_t kSpendTimeCatalogPointerRva = 0x04D78A30; // dword_5178A30 constexpr uintptr_t kSpendTimeUiSharedRva = 0x04D78A80; // dword_5178A80 // A marked op863 state projection must not reach sub_1D25590: that result // handler only creates the one-shot [11,8] unlock effect. Current NoPack's // actual lock gate is sub_26905A0, which reads bit 3 from the current actor's // state block at resolver-result+0x1AC. Mirror the marked durable state there // before owner 0xC9 is constructed so reopening the avatar panel is silent. constexpr uintptr_t kSceneUiOpenRva = 0x02BDD1A0; // sub_2FDD1A0 constexpr uintptr_t kSceneUiIsOpenRva = 0x02BD4B40; // sub_2FD4B40 // The current EXE calls sub_22645B0 from owner 609's native open gate while // the transplanted NPC conversation is still in a blocked scene sub-state. // That early return happens before op1291 and before the owner is promoted // from its fallback vector. Override only that exact return address; all other // callers retain the native scene-state decision. constexpr uintptr_t kJoustSceneBlockCheckRva = 0x01E645B0; // sub_22645B0 constexpr uintptr_t kJoustOpenGateReturnRva = 0x00B2D0E7; // sub_F2D0A0+0x47 // sub_F2D0A0 checks whether the type-73 state string has a historical // weekend/10:00 schedule before it emits the joust-opening request. The // local server publishes this activity as all-day; retain the native request // and UI creation but skip only that retired client-side early return. constexpr uintptr_t kJoustHistoricTimeGateRva = 0x00B2D126; // jnz loc_F2D15C constexpr uintptr_t kPartyDirectoryOwnerGuardRva = 0x02E6CF2C; constexpr uintptr_t kPartyDirectoryOpenOwnerRva = 0x02E6CF72; constexpr uintptr_t kPartyDirectoryCloseOwnerARva = 0x02E6D435; constexpr uintptr_t kPartyDirectoryCloseOwnerBRva = 0x02E6D4B1; constexpr uintptr_t kPartyDirectoryUiOwner = 9; constexpr unsigned short kPartyDirectoryRefreshOpcode = 98; constexpr unsigned char kPartyDirectoryRefreshModeFull = 0; constexpr unsigned short kAuraSkinSlotOpcode = 863; constexpr uintptr_t kAvatarPanelUiOwner = 0xC9; constexpr uintptr_t kJoustUiOwner = 0x261; // Current-EXE live owner-vector evidence: 0xC9 is the equipment/inventory // panel used by the aura compatibility path, while 0xD9 owns the native // personal-information panel opened by the character-info shortcut. constexpr uintptr_t kPersonalInfoUiOwner = 0xD9; constexpr size_t kSceneUiOwnerVectorOffset = 0x108; constexpr size_t kSceneUiOwnerVectorStride = 20; constexpr size_t kSceneUiOwnerVectorBeginOffset = 4; constexpr size_t kSceneUiOwnerVectorEndOffset = 8; // The personal-info object owns its root D3D widget at +0x30. The current // widget stores the content rectangle as x/y/width/height at +0x64..+0x70. // Its skinned frame extends 8 pixels past the content on the right and 13 // pixels above it; those are the exact sidecar attachment edges. constexpr size_t kPersonalPanelRootWidgetOffset = 0x30; constexpr size_t kPersonalPanelWidgetXOffset = 0x64; constexpr size_t kPersonalPanelWidgetYOffset = 0x68; constexpr size_t kPersonalPanelWidgetWidthOffset = 0x6C; constexpr size_t kPersonalPanelWidgetHeightOffset = 0x70; // Current DNF.exe UI transform owner/function (hash-locked EXE). constexpr uintptr_t kNativeUiTransformPointerRva = 0x04D7DBA0; // dword_517DBA0 constexpr uintptr_t kNativeUiTransformRva = 0x00CDC420; constexpr uintptr_t kNativeUiViewportWidthRva = 0x04CE34B8; // dword_50E34B8 constexpr uintptr_t kNativeUiViewportHeightRva = 0x04CE34BC; // The root widget excludes roughly five pixels of the visible personal-panel // frame. Thirteen therefore leaves the screenshot-matched six-pixel gap // between the two visible borders instead of welding both frames together. constexpr int kPersonalPanelFrameRight = 13; constexpr int kPersonalPanelFrameTop = 13; constexpr unsigned int kAuraSkinSilentRestorePacketLength = 21; constexpr size_t kAuraSkinActorStateOwnerOffset = 0x4C8; constexpr size_t kAuraSkinStateResolverVtableOffset = 0xA98; constexpr size_t kAuraSkinStateFlagsOffset = 0x1AC; constexpr unsigned int kAuraSkinUnlockedMask = 1u << 3; constexpr uintptr_t kGameSingletonPointerRva = 0x04EEA4BC; // dword_52EA4BC constexpr uintptr_t kGameDispatchGateOffset = 0x002BD068; // sub_3454600 constexpr uintptr_t kObjectManagerPointerRva = 0x04DB2728; // dword_51B2728 constexpr uintptr_t kSceneActorManagerPointerRva = 0x04DB2738; // dword_51B2738 constexpr uintptr_t kControlledActorPointerRva = 0x04EB5BB0; // dword_52B5BB0 constexpr uintptr_t kSceneRootPointerRva = 0x04DB2764; // dword_51B2764 constexpr uintptr_t kCurrentActorRva = 0x0229A050; // sub_269A050 constexpr uintptr_t kCompanionP0PayloadEncodeRva = 0x02BA1EF0; // sub_2FA1EF0 internal compact payload encoder constexpr uintptr_t kCompanionP0PeerDeliveryRva = 0x031E9B20; // sub_35E9B20 local peer delivery constexpr uintptr_t kCompanionP0RegistrationRva = 0x021BCC90; // sub_25BCC90 actor registration constexpr uintptr_t kCompanionP1NetworkRouterRva = 0x01E61E30; // sub_2261E30 socket packet router constexpr uintptr_t kCompanionP1AicCopyRva = 0x02099F10; // sub_2499F10 shared AIC -> per-APC AI deep copy constexpr uintptr_t kActorByObjectKeyRva = 0x02299A30; // sub_2699A30 constexpr uintptr_t kActorByContextRva = 0x013B6AA0; // sub_17B6AA0 constexpr size_t kCurrentActorCacheOffset = 0x70; // sub_269A050 object-manager cache constexpr uintptr_t kResolveActorInContextRva = 0x022A2000; // sub_26A2000 constexpr uintptr_t kActorVisualDestroyRva = 0x02294460; // sub_2694460 constexpr uintptr_t kActorVisualCreateRva = 0x02294890; // sub_2694890 constexpr size_t kActorVisualOffset = 0x4C4; constexpr size_t kActorPartyOwnerOffset = 0x498; // Current NoPack class0/op205 updates auxiliary-profession state, not the // actor's personal-info type (+364) or live function type (+380). The native // system-menu action4 reads the latter (00FF1FF0). Mirror both from the exact // server projection after native dispatch; never force a menu gate result. constexpr unsigned short kExpertJobInfoOpcode = 205; // Current live packet evidence identifies dungeon entry as one typed class-0 // op28 dungeon-info packet, the first op29 start-map packet, and then the // exact op3 local-user activation. Later room changes emit op29 without op3. constexpr unsigned short kDungeonUserStateOpcode = 3; constexpr unsigned short kDungeonInfoOpcode = 28; constexpr unsigned short kDungeonStartMapOpcode = 29; // Current NoPack class0/op9 updates the global party slot table first, then // refreshes HUD state 1 through sub_27310B0 only when its owner/object lookup // resolves to the native current actor. The local bridge's established owner // context 0 is remapped at mode-0 creation time, so a later op9 can safely // populate the slots but miss that final pointer-equality branch. Repair only // the op9 immediately following a successful class1/op12 SET_PARTY_INFO ACK, // using the exact native HUD path from sub_1D64CA0. constexpr uintptr_t kSceneHudContextRva = 0x0141E750; // sub_181E750 constexpr uintptr_t kPartyHudRefreshRva = 0x023310B0; // sub_27310B0 constexpr unsigned short kSetPartyInfoOpcode = 12; constexpr unsigned short kPartyActorProjectionOpcode = 9; constexpr unsigned short kPartyActorProjectionScene = 9999; constexpr uintptr_t kLocalActorCreateRva = 0x01C036C0; // sub_20036C0 constexpr uintptr_t kMode0OwnerCompareRva = 0x01C092A5; // sub_2009160 scene/local channel cmp // The seven-byte detour also overwrites MOVZX EDX,AL at +4. Replay it in // the proxy and resume at the first untouched instruction, PUSH EDX. constexpr uintptr_t kMode0OwnerRemoteRva = 0x01C092AC; constexpr uintptr_t kMode0OwnerLocalRva = 0x01C092EB; // sub_2008600 resolves the mode-3 actor with the same owner/context pair as // mode 0. The bridge's established context-0 sentinel must resolve to the // current scene actor here as well; otherwise sub_2002FC0 returns null and the // native mismatch path dereferences it at sub_24A0920. constexpr uintptr_t kMode3OwnerResolveRva = 0x01C08737; constexpr uintptr_t kMode3OwnerRemoteResumeRva = 0x01C08741; constexpr uintptr_t kMode3OwnerLocalResumeRva = 0x01C0877A; // sub_2008600 compares the same local-owner byte with the current scene // channel a second time after resolving the actor. Context 0 must take the // local branch here too; otherwise sub_24A0920 marks the selected actor as a // remote actor even though the first comparison resolved it locally. constexpr uintptr_t kMode3OwnerFinalizeRva = 0x01C0884F; constexpr uintptr_t kMode3OwnerFinalizeRemoteResumeRva = 0x01C0885A; constexpr uintptr_t kMode3OwnerFinalizeLocalResumeRva = 0x01C08863; // sub_1D84FE0's final current-creature record update assumes the equipped // creature key is present in the op105 map. Imported/current-profile state can // legitimately miss that lookup; the native handler otherwise dereferences // null at sub_34DEA20(this=0x14) after it has already updated the actor and // live creature objects. constexpr uintptr_t kCreatureRenameMapNullCheckRva = 0x0198515F; constexpr uintptr_t kCreatureRenameMapUpdateRva = 0x01985165; constexpr uintptr_t kCreatureRenameMapDoneRva = 0x0198516A; // Current NoPack sub_1D73120 parses every class0/op14 raw item row into the // complete native dynamic-state object. Its same-template fast path then // updates only the count/serial and a creature trade flag. For list 7 that // leaves an already-created pet object holding the old enchant-card fields // even though raw+0x0E/+0x12 and the server repository contain the replacement // card. The different-template branch already applies the parsed state with // the item's native vtable +0x158 method. Reuse that exact call on the list-7 // same-template path without replacing the item pointer. constexpr uintptr_t kPetItemUpdateDynamicStateRva = 0x019739B9; constexpr uintptr_t kPetItemUpdateDynamicStateResumeRva = 0x019739C0; // sub_20695F0 scans ordinary equipment first, then the optional auxiliary // equipment container at player+0x646C for slots 3..8. The latter is absent // during a valid dungeon-entry window, but this one call site invokes // sub_2326860 without the null check used by the other current-EXE callers. // An active op903 auto-repair service reaches this window and otherwise // dereferences 0x00000044 in sub_34DBD90. constexpr uintptr_t kAutoRepairAuxLookupRva = 0x01C6977F; constexpr uintptr_t kAutoRepairAuxLookupResumeRva = 0x01C69786; constexpr uintptr_t kAutoRepairAuxLoopDoneRva = 0x01C698EA; constexpr uintptr_t kOp24SceneModeRva = 0x01E64FD0; // sub_2264FD0 constexpr uintptr_t kOp24LoadingGateRva = 0x022C8DA0; // sub_26C8DA0 constexpr uintptr_t kTownEntryStateRva = 0x04D9A290; // unk_519A290 constexpr uintptr_t kLocalOwnerServerRva = 0x04D99C4A; // unk_5199C4A constexpr uintptr_t kLocalOwnerChannelRva = 0x04D99C49; // byte_5199C49 // The current actor stores its native item/equipment container at +0x646C. // sub_249CCE0 creates this exact object with sub_759030(260) followed by // sub_233CFD0(memory, 0, 366). Both audited manual pickup paths refuse to call // sub_232F810 (the native op43 writer) while this field is null. constexpr uintptr_t kPickupContainerGetterRva = 0x020A1BC0; // sub_24A1BC0 constexpr uintptr_t kManualPickupRva = 0x020B2840; // sub_24B2840 constexpr uintptr_t kPickupContainerAllocatorRva = 0x00359030; // sub_759030 constexpr uintptr_t kPickupContainerConstructorRva = 0x01F3CFD0; // sub_233CFD0 constexpr size_t kPickupContainerOffset = 0x646C; constexpr size_t kPickupContainerBytes = 260; constexpr int kPickupContainerKind = 366; constexpr unsigned int kDungeonTownEntryState = 3; constexpr unsigned short kGamePortBase = 10000; constexpr unsigned short kCrackBootstrapPort = 10019; // Passive socket-open diagnostics. These current-NoPack functions are never // used to alter packets or item state: the row parser only caches exact input // bytes, and the panel callback only logs the matching cached row. constexpr uintptr_t kCurrentItemRowParseRva = 0x01D792A0; // sub_21792A0 constexpr uintptr_t kSocketPanelSelectRva = 0x01D12CF0; // sub_2112CF0 constexpr uintptr_t kSocketOpenWriterRva = 0x01D12D50; // sub_2112D50 // Current main-inventory right-click paths that serialize class1/op44. // The fallback hooks these exact current-EXE functions and only activates for // item ids proven by this compatibility unit's premiumlist_new.etc. constexpr uintptr_t kInventoryUseUIARva = 0x011280C0; // sub_15280C0 constexpr uintptr_t kInventoryUseUIBRva = 0x01772FD0; // sub_1B72FD0 // These two current item-action virtual callbacks sit before the panel writer. // sub_275A140 first asks the native 0x7D4 eligibility service and can return // before sub_2759FD0/sub_2331CE0 is reached; sub_275A060 is its sibling route. constexpr uintptr_t kInventoryUseGateARva = 0x0235A060; // sub_275A060 constexpr uintptr_t kInventoryUseGateBRva = 0x0235A140; // sub_275A140 // The active-coupon panel has its own native op44 writer. Unlike the two // generic inventory callbacks above it receives the selected template ID and // main-bag slot directly, then applies a global UI gate before writing op44. constexpr uintptr_t kInventoryUsePanelRva = 0x01F31CE0; // sub_2331CE0 constexpr uintptr_t kInventorySelectionManagerRva = 0x04DB2768; // dword_51B2768 constexpr uintptr_t kInventorySelectionCtorRva = 0x032EFB70; // sub_36EFB70 constexpr uintptr_t kInventorySelectionCollectRva = 0x01F31F70; // sub_2331F70 constexpr uintptr_t kInventorySelectionDtorRva = 0x030D9490; // sub_34D9490 constexpr uintptr_t kInventorySelectionLookupRva = 0x01F26860; // sub_2326860 constexpr uintptr_t kCurrentItemTemplateReadRva = 0x003CA070; // sub_7CA070 constexpr uintptr_t kCurrentItemIdentityReadRva = 0x01C64A20; // sub_2064A20 // The current EXE special-cases rare-avatar ticket 2681594 in both the shop // click handler and its availability check. Each branch looks up Starlight // Unicorn 63092 before allowing the purchase path to continue. Replace only // those two ticket comparisons with an unreachable item id so this ticket // follows the ordinary shop path; price, purchase-limit, capacity, and all // other product checks remain native. constexpr uintptr_t kRareAvatarTicketClickCompareRva = 0x012BAEFD; constexpr uintptr_t kRareAvatarTicketAvailabilityCompareRva = 0x012BD03C; // The current main-inventory tooltip owner keeps the wrapper under the mouse // here. Native right-click eligibility is already true for the reproduced // contracts, but their item class has no current-EXE op44 dispatch route. constexpr uintptr_t kHoveredInventoryItemRva = 0x04EE1294; // dword_52E1294 constexpr uintptr_t kUpperWriterValueRva = 0x03054640; // sub_3454640 constexpr uintptr_t kUpperWriterU16Rva = 0x03054EB0; // sub_3454EB0 constexpr uintptr_t kUpperWriterU8Rva = 0x03054FC0; // sub_3454FC0 constexpr uintptr_t kUpperWriterI16Rva = 0x03054FF0; // sub_3454FF0 constexpr uintptr_t kUpperWriterU32Rva = 0x03055020; // sub_3455020 constexpr uintptr_t kUpperWriterFlushRva = 0x030566F0; // sub_34566F0 constexpr unsigned short kUseStackableOpcode = 44; constexpr int kMainInventorySelectionGroup = 0x39; constexpr unsigned char kMainInventoryListType = 0; constexpr int kCurrentMainInventoryFirstItemSlot = 3; constexpr int kCurrentMainInventoryLastItemSlot = 359; constexpr size_t kCurrentItemIdentityOffset = 0x0E; // sub_229ACD0 gates the two optional post-row blobs in scene op14/list3. // It is traced only to classify the already-selected template; it does not // alter the decision or packet reader state. constexpr uintptr_t kSocketOp14ExtensionGateRva = 0x01E9ACD0; // sub_229ACD0 constexpr size_t kCurrentItemRawBytes = 0x77; constexpr LONG kSocketTraceCacheRows = 2048; constexpr LONG kSocketTraceMaxSelections = 8; constexpr LONG kSocketTraceTemplateMatches = 8; constexpr LONG kSocketTraceMaxWriterHits = 8; constexpr LONG kSocketTraceMaxExtensionGateHits = 8; // The latest confirmed current-client op914 selection is list-3 slot 9. // Keep this exact filter narrow so the scene-reader hook is not a hot-path logger. constexpr uint32_t kSocketTraceSelectedTemplateID = 100270515; // Current-EXE global-minimize branches in sub_2201FE0. Every IAT proxy checks // the exact return address so unrelated UI and COM calls retain native behavior. constexpr uintptr_t kToggleDesktopCoCreateReturnRva = 0x01A0227F; constexpr uintptr_t kForeignShowWindowReturnRva = 0x01A02528; constexpr uintptr_t kMinimizeAllSendMessageReturnRva = 0x01A02629; // Current DNF.exe sub_384BEB0 calls CreateMutexW at RVA 0x0304BEFF, // then compares GetLastError() with ERROR_ALREADY_EXISTS. The return RVA // keeps the compatibility scoped to that audited single-instance branch. constexpr uintptr_t kMultiClientCreateMutexReturnRva = 0x0304BF05; // Current DNF.exe sub_38AD160 calls FindWindowW at RVA 0x030AD1CA, // focuses the existing window and returns false when it succeeds. Returning // no match only at this call lets the additional process create its own // per-process registered window, matching the client's supported dual-launch // path without changing unrelated window searches. constexpr uintptr_t kMultiClientFindWindowReturnRva = 0x030AD1D0; // Lua notices use an owned popup overlay instead of an unproved game UI call. // Read-only current-EXE evidence and live screenshots rejected sub_226F9C0: // its downstream manager hashes the string into a registered sound/resource // table, so an arbitrary UTF-16 string is accepted without becoming visible. // The existing DNF window procedure remains the only cross-thread boundary. constexpr size_t kLuaNoticeMaximumCharacters = 511; constexpr size_t kLuaNoticeQueueCapacity = 32; constexpr size_t kLuaClientEventQueueCapacity = 32; constexpr wchar_t kLuaNoticeWindowMessageName[] = L"DNF90.90CNLua.ClientNotice.90cn-decode-bypass-v1"; constexpr wchar_t kLuaNoticeOverlayClassName[] = L"DNF90.90CNLua.NoticeOverlay.90cn-decode-bypass-v1"; constexpr wchar_t kCombatPanelWindowMessageName[] = L"DNF90.90CNLua.CombatPanelUpdate.90cn-decode-bypass-v1"; constexpr wchar_t kCombatPanelOverlayClassName[] = L"DNF90.90CNLua.CombatPanel.90cn-decode-bypass-v1"; // The DLL-private class0/op413 envelope is kept only for the combat-power // sidecar snapshot. Seria-luck HUD rendering must stay owned by the native EXE // path, so type-0 lookalikes are no longer consumed here. constexpr unsigned short kCombatPowerAffixPrivateOpcode = 413; // v4 shipped an 86-byte class0 packet (16-byte header + 70-byte body); the // current server emits the compact v5 layout (16 + 74 = 90). Both layouts keep // the fields at identical body offsets, so only the length/version gate differs. constexpr unsigned int kCombatPowerAffixPrivatePacketLengthV4 = 86; constexpr unsigned int kCombatPowerAffixPrivatePacketLengthV5 = 90; constexpr unsigned char kCombatPowerAffixPrivateType = 1; constexpr unsigned char kCombatPowerAffixPrivateVersionV4 = 4; constexpr unsigned char kCombatPowerAffixPrivateVersionV5 = 5; constexpr UINT_PTR kLuaNoticeOverlayTimer = 0x90C1; constexpr UINT_PTR kCombatPanelPollTimer = 0x90C2; constexpr UINT kLuaNoticeOverlayDurationMs = 4000; constexpr UINT kCombatPanelPollIntervalMs = 100; constexpr int kCombatPanelWidth = 118; constexpr int kCombatPanelHeight = 386; constexpr int kCombatRankIconSize = 80; constexpr int kCombatRankIconX = 19; constexpr int kCombatRankIconY = 37; constexpr int kCombatPanelRankTooltipWidth = 230; constexpr int kCombatPanelRankTooltipHeight = 260; constexpr int kCombatPanelRankHoverTop = 24; constexpr int kCombatPanelRankHoverBottom = 158; constexpr int kCombatPanelUpgradeButtonLeft = 26; constexpr int kCombatPanelUpgradeButtonTop = 177; constexpr int kCombatPanelUpgradeButtonRight = 93; constexpr int kCombatPanelUpgradeButtonBottom = 204; constexpr int kCombatPanelGuideWidth = 292; constexpr int kCombatPanelGuideHeight = 238; struct CurrentItemRowTrace { LONG sequence; uintptr_t parserObject; unsigned char raw[kCurrentItemRawBytes]; }; struct LuaClientNotice { unsigned int length; wchar_t text[kLuaNoticeMaximumCharacters + 1]; }; struct LuaClientEventContext { unsigned int flags; unsigned int dungeonID; unsigned int roomX; unsigned int roomY; unsigned int roomLayerFlag; unsigned int mapID; unsigned int previousRoomX; unsigned int previousRoomY; unsigned int previousRoomLayerFlag; unsigned int previousMapID; }; enum LuaDungeonEntryStage : unsigned int { kLuaDungeonEntryIdle = 0, kLuaDungeonEntryAwaitStartMap = 1, kLuaDungeonEntryAwaitUserState = 2, kLuaDungeonEntryActive = 3, }; uintptr_t g_dnfBase = 0; uintptr_t g_dnfModuleSize = 0; uintptr_t g_checksumFn = 0; uintptr_t g_gameAllocatorFn = 0; uintptr_t g_gameMemcpyFn = 0; uintptr_t g_dprotoOutgoingResume = 0; uintptr_t g_dprotoOutgoingReturn = 0; uintptr_t g_dprotoFalseResume = 0; uintptr_t g_mode0OwnerRemoteResume = 0; uintptr_t g_mode0OwnerLocalResume = 0; uintptr_t g_mode3OwnerRemoteResume = 0; uintptr_t g_mode3OwnerLocalResume = 0; uintptr_t g_mode3OwnerFinalizeRemoteResume = 0; uintptr_t g_mode3OwnerFinalizeLocalResume = 0; uintptr_t g_mode3LocalOwnerChannelAddress = 0; uintptr_t g_creatureRenameMapUpdateResume = 0; uintptr_t g_creatureRenameMapDoneResume = 0; uintptr_t g_petItemUpdateDynamicStateResume = 0; uintptr_t g_autoRepairAuxLookupFn = 0; uintptr_t g_autoRepairAuxLookupResume = 0; uintptr_t g_autoRepairAuxLoopDone = 0; uintptr_t g_questAutoCompleteSendFn = 0; uintptr_t g_originalCipherEncodeFn = 0; void* g_originalSelectedPageApply = nullptr; void* g_originalSelectorCreateTick = nullptr; void* g_originalSelectorCreateTransition = nullptr; void* g_originalCreateUIClick = nullptr; void* g_originalCreateUIOpen = nullptr; void* g_originalUpperCreateSend = nullptr; void* g_originalClass0Dispatch = nullptr; volatile LONG g_mailboxHudRefreshTargetsVerified = 0; volatile LONG g_mailboxHudZeroRefreshPending = 0; volatile LONG g_mailboxHudRefreshLogCount = 0; void* g_originalClass1Dispatch = nullptr; void* g_originalSceneUiOpen = nullptr; void* g_originalJoustSceneBlockCheck = nullptr; void* g_originalActorByObjectKey = nullptr; void* g_originalLocalActorCreate = nullptr; void* g_originalPickupContainerGetter = nullptr; void* g_originalManualPickup = nullptr; void* g_originalOp24SceneMode = nullptr; void* g_originalOp24LoadingGate = nullptr; void* g_originalInventoryUseUIA = nullptr; void* g_originalInventoryUseUIB = nullptr; void* g_originalInventoryUseGateA = nullptr; void* g_originalInventoryUseGateB = nullptr; void* g_originalInventoryUsePanel = nullptr; void* g_originalInventorySelectionLookup = nullptr; WNDPROC g_originalDnfWindowProc = nullptr; HWND g_dnfWindow = nullptr; PVOID volatile g_luaNoticeBridgeWindow = nullptr; volatile LONG g_luaNoticeWindowMessage = 0; HWND g_luaNoticeOverlayWindow = nullptr; wchar_t g_luaNoticeOverlayText[kLuaNoticeMaximumCharacters + 1] = {}; SRWLOCK g_luaNoticeLock = SRWLOCK_INIT; LuaClientNotice g_luaNoticeQueue[kLuaNoticeQueueCapacity] = {}; size_t g_luaNoticeHead = 0; size_t g_luaNoticeCount = 0; volatile LONG g_luaNoticeQueuedCount = 0; volatile LONG g_luaNoticeDispatchCount = 0; volatile LONG g_luaNoticeRejectedCount = 0; SRWLOCK g_luaClientEventLock = SRWLOCK_INIT; DNF90ClientEvent g_luaClientEventQueue[kLuaClientEventQueueCapacity] = {}; size_t g_luaClientEventHead = 0; size_t g_luaClientEventCount = 0; volatile LONG g_luaClientEventSequence = 0; volatile LONG g_luaClientEventRejectedCount = 0; volatile LONG g_luaUiReadyEventQueued = 0; volatile LONG g_luaUiClosedEventQueued = 0; volatile LONG g_combatPanelWindowMessage = 0; HWND g_combatPanelOverlayWindow = nullptr; HWND g_combatPanelRankTooltipWindow = nullptr; HWND g_combatPanelGuideWindow = nullptr; bool g_combatPanelPersonalInfoOpen = false; bool g_combatPanelRankTooltipVisible = false; bool g_combatPanelGuideVisible = false; SRWLOCK g_characterStatSnapshotLock = SRWLOCK_INIT; DNF90CharacterStatSnapshot g_characterStatSnapshot = {}; volatile LONG g_characterStatGeneration = 0; volatile LONG g_characterStatRejectLogCount = 0; SRWLOCK g_equipmentSnapshotLock = SRWLOCK_INIT; DNF90EquipmentSnapshot g_equipmentSnapshot = {}; volatile LONG g_equipmentSnapshotGeneration = 0; volatile LONG g_equipmentSnapshotRejectLogCount = 0; SRWLOCK g_damageAffixSnapshotLock = SRWLOCK_INIT; DNF90DamageAffixSnapshot g_damageAffixSnapshot = {}; volatile LONG g_damageAffixSnapshotGeneration = 0; volatile LONG g_damageAffixSnapshotUpdateCount = 0; SRWLOCK g_combatPanelStateLock = SRWLOCK_INIT; DNF90CombatPanelState g_combatPanelState = { sizeof(DNF90CombatPanelState), 0, 0, DNF90_COMBAT_PANEL_ENABLED, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; bool g_contractRButtonPending = false; int g_contractRButtonPendingSlot = -1; uint32_t g_contractRButtonPendingTemplateID = 0; uint32_t g_contractRButtonPendingIdentity = 0; LONG g_contractRButtonPendingOp44Count = 0; volatile LONG g_cipherCallCount = 0; volatile LONG g_clientInventoryUseSendCount = 0; volatile LONG g_protocolTraceEnabled = 0; // Native dispatch inspection performs several pointer walks and synchronous // file writes on the game thread. Keep it opt-in for diagnosis so normal // packet dispatch remains free of tracing I/O. volatile LONG g_dispatchTraceEnabled = 0; volatile LONG g_contractUseUIAHitCount = 0; volatile LONG g_contractUseUIBHitCount = 0; volatile LONG g_contractUseGateAHitCount = 0; volatile LONG g_contractUseGateBHitCount = 0; volatile LONG g_contractUsePanelHitCount = 0; volatile LONG g_contractRButtonFallbackCount = 0; volatile LONG g_auraSkinEntitlementState = -1; volatile LONG g_auraSkinSilentRestoreCount = 0; volatile LONG g_auraSkinDeferredApplyCount = 0; volatile LONG g_partyHudRefreshPending = 0; volatile LONG g_partyHudRefreshApplyCount = 0; volatile LONG g_autoRepairAuxNullCount = 0; volatile LONG g_pickupContainerRepairCount = 0; volatile LONG g_questAutoCompleteCallCount = 0; volatile LONG g_socketTraceRowSequence = 0; volatile LONG g_socketTraceSelectionCount = 0; volatile LONG g_socketTraceWriterCount = 0; volatile LONG g_socketTraceExtensionGateCount = 0; volatile LONG g_selectorTraceInitialized = 0; volatile LONG g_selectorTraceMouseDown = 0; uintptr_t g_selectorTraceSelf = 0; uintptr_t g_selectorTraceButton = 0; unsigned int g_selectorTraceReady = 0; unsigned int g_selectorTracePending = 0; unsigned int g_selectorTraceRestriction = 0; unsigned int g_selectorTraceButtonEnabled = 0; unsigned int g_selectorTraceButtonStamp = 0; unsigned int g_selectorTraceGlobalStamp = 0; int g_selectorTraceMode = -1; int g_selectorTraceCount = -1; void* volatile g_currentTclsParser = nullptr; void* volatile g_originalSendMessageW = nullptr; void* volatile g_originalShowWindow = nullptr; void* volatile g_originalCoCreateInstance = nullptr; void* volatile g_originalCreateMutexW = nullptr; void* volatile g_originalFindWindowW = nullptr; PVOID g_exceptionTraceHandle = nullptr; thread_local bool g_traceOp24 = false; thread_local bool g_inExceptionTrace = false; thread_local bool g_contractUseFallbackActive = false; thread_local bool g_pickupContainerRepairActive = false; thread_local bool g_townActorContextLookupActive = false; thread_local unsigned short g_townActorContextLookupKey = 0; thread_local unsigned int g_op24SceneModeCalls = 0; thread_local unsigned int g_op24LoadingGateCalls = 0; thread_local bool g_op24SceneModeEarlyReturn = false; thread_local bool g_op24LoadingGateEarlyReturn = false; // 进图后第一次尝试使用物品时,强制清一次“物品使用等待”字节。 static volatile LONG g_itemUseGateResetArmed = 0; thread_local LuaDungeonEntryStage g_luaDungeonEntryStage = kLuaDungeonEntryIdle; thread_local unsigned int g_luaDungeonID = 0; thread_local unsigned char g_luaDungeonBossX = 0; thread_local unsigned char g_luaDungeonBossY = 0; thread_local bool g_luaDungeonRoomKnown = false; thread_local unsigned char g_luaDungeonRoomX = 0; thread_local unsigned char g_luaDungeonRoomY = 0; thread_local unsigned char g_luaDungeonRoomLayerFlag = 0; thread_local unsigned int g_luaDungeonMapID = 0; SRWLOCK g_logWriteLock = SRWLOCK_INIT; SRWLOCK g_socketTraceLock = SRWLOCK_INIT; CurrentItemRowTrace g_socketTraceRows[kSocketTraceCacheRows] = {}; // Class0/op2 identifies remote town actors by an owner byte plus their stable // uint16 object key, while op22/op23 carry the key alone. The current EXE keeps // remotely-owned actors out of the global key table, so retain only the owner // byte required to resolve the existing context actor for later movement and // user interaction. The entry is cleared before native op6 leave handling. unsigned char g_townActorOwnerByObjectKey[0x10000] = {}; unsigned char g_townActorLookupLoggedByObjectKey[0x10000] = {}; // op205 is the authoritative expert-job projection, but the current EXE's // later local op2/mode0 actor refresh zeroes actor state. Retain only the // validated projection for both type fields on the exact actor pointer/key so a // different character or a remote town actor can never inherit it. volatile LONG g_cachedExpertJobType = -1; void* g_cachedExpertJobActor = nullptr; unsigned short g_cachedExpertJobObjectKey = 0; unsigned short g_currentActorObjectKey = 0; typedef bool (__thiscall* TclsParseFn)(void*, const wchar_t*); typedef bool (__thiscall* TclsFetchLoginFn)(void*, void*, void*, void*); typedef bool (__thiscall* TclsFetchTextFn)(void*, int, void*); typedef bool (__thiscall* TclsFetchOneFn)(void*, void*); typedef bool (__stdcall* TclsTailFn)(void*); typedef void* (__thiscall* GameWideAssignFn)(void*, const wchar_t*, size_t); typedef int (__thiscall* CodecSetKeyFn)(void*, int, int); typedef LRESULT (WINAPI* SendMessageWFn)(HWND, UINT, WPARAM, LPARAM); typedef BOOL (WINAPI* ShowWindowFn)(HWND, int); typedef HANDLE (WINAPI* CreateMutexWFn)(LPSECURITY_ATTRIBUTES, BOOL, LPCWSTR); typedef HWND (WINAPI* FindWindowWFn)(LPCWSTR, LPCWSTR); typedef HRESULT (WINAPI* CoCreateInstanceFn)( const GUID&, LPUNKNOWN, DWORD, const GUID&, LPVOID*); typedef void (__thiscall* SelectedPageApplyFn)(void*); typedef uintptr_t (__thiscall* SelectorCreateTickFn)(void*); typedef uintptr_t (__thiscall* SelectorCreateTransitionFn)(void*); typedef int (__thiscall* CreateUIClickFn)(void*); typedef unsigned char (__thiscall* CreateUIOpenFn)(void*, int*, int); typedef int (__thiscall* UpperCreateSendFn)(void*); typedef int (__stdcall* Class0DispatchFn)(int, int); typedef void* (__cdecl* MailboxHudManagerFn)(); typedef unsigned char (__thiscall* MailboxHudReadyFn)(void*); typedef void (__thiscall* MailboxHudRefreshFn)(void*); typedef int (__stdcall* Class1DispatchFn)(int, int, int); typedef void* (__thiscall* SceneUiOpenFn)(void*, uintptr_t, int, unsigned int); typedef bool (__thiscall* SceneUiIsOpenFn)(void*, uintptr_t); typedef bool (__thiscall* JoustSceneBlockCheckFn)(void*); typedef void* (__thiscall* AuraSkinStateResolverFn)(void*); typedef void* (__cdecl* DispatchRegistryFn)(); typedef void* (__thiscall* DispatchLookupFn)(void*, unsigned int); typedef void* (__thiscall* CurrentActorFn)(void*); typedef void* (__cdecl* ActorByObjectKeyFn)(unsigned short); typedef void* (__cdecl* ActorByContextFn)(unsigned short, int); typedef void* (__thiscall* ResolveActorInContextFn)( void*, unsigned char, unsigned short); typedef void* (__thiscall* ActorVisualDestroyFn)(void*); typedef int (__thiscall* ActorVisualCreateFn)(void*, int); typedef void* (__thiscall* SceneHudContextFn)(void*); typedef void (__thiscall* PartyHudRefreshFn)(void*, int); typedef int (__cdecl* LocalActorCreateFn)(int); typedef void* (__thiscall* PickupContainerGetterFn)(void*); typedef void* (__thiscall* ManualPickupFn)(void*, void*); typedef void* (__cdecl* PickupContainerAllocatorFn)(size_t); typedef void* (__thiscall* PickupContainerConstructorFn)(void*, int, int); typedef int (__thiscall* Op24SceneModeFn)(void*); typedef unsigned char (__cdecl* Op24LoadingGateFn)(); typedef bool (__thiscall* ChannelObjectPredicateFn)(void*); typedef int (__thiscall* ChannelObjectIntFn)(void*); typedef unsigned short (__thiscall* ChannelObjectPortFn)(void*); typedef const wchar_t* (__thiscall* ChannelObjectNameFn)(void*); typedef int (__fastcall* CurrentItemRowParseFn)(int, int, char*); typedef char (__thiscall* SocketPanelSelectFn)(void*, int, int); typedef char (__thiscall* SocketOpenWriterFn)(void*); typedef bool (__cdecl* SocketOp14ExtensionGateFn)(int); typedef int (__thiscall* InventoryUseUIAFn)(void*, int, int, int, int); typedef void (__cdecl* InventoryUseUIBFn)(); typedef uintptr_t (__thiscall* InventoryUseGateFn)(void*, int, int, int); typedef unsigned char (__thiscall* InventoryUsePanelFn)(void*, uint32_t, int); typedef void (__thiscall* InventorySelectionCtorFn)(void*); typedef void (__thiscall* InventorySelectionCollectFn)(void*, void*, int); typedef void (__thiscall* InventorySelectionDtorFn)(void*); typedef void* (__thiscall* InventorySelectionLookupFn)(void*, int); typedef void* (__thiscall* CurrentItemDataFn)(void*); typedef uint32_t (__thiscall* CurrentItemTemplateReadFn)(void*); typedef uint32_t (__cdecl* CurrentItemIdentityReadFn)(uint32_t); typedef void* (__cdecl* UpperWriterValueFn)(); typedef void (__thiscall* UpperWriterScalarFn)(void*, uint32_t); typedef void (__cdecl* UpperWriterFlushFn)(); TclsParseFn g_originalTclsParse = nullptr; TclsFetchLoginFn g_originalTclsFetchLogin = nullptr; TclsFetchTextFn g_originalTclsFetchText = nullptr; TclsFetchOneFn g_originalTclsFetchCrypto = nullptr; TclsFetchOneFn g_originalTclsFetchTail = nullptr; TclsTailFn g_originalTclsTail = nullptr; GameWideAssignFn g_gameWideAssign = nullptr; CodecSetKeyFn g_originalCodecSetKey0 = nullptr; SocketOp14ExtensionGateFn g_originalSocketOp14ExtensionGate = nullptr; CodecSetKeyFn g_originalCodecSetKey2 = nullptr; CodecSetKeyFn g_originalCodecSetKey3 = nullptr; CodecSetKeyFn g_originalCodecSetKey7 = nullptr; CodecSetKeyFn g_originalCodecSetKey8 = nullptr; CurrentItemRowParseFn g_originalCurrentItemRowParse = nullptr; SocketPanelSelectFn g_originalSocketPanelSelect = nullptr; SocketOpenWriterFn g_originalSocketOpenWriter = nullptr; typedef bool (__thiscall* ChannelScriptDownloadFn)(void*); typedef bool (__thiscall* ChannelScriptLoadFn)(void*, const char*); typedef bool (__cdecl* ChannelDirectoryApplyFn)(); typedef void* (__thiscall* ChannelRuntimeLoadFn)(void*, const void*); typedef void* (__thiscall* ChannelScriptLookupFn)(void*, int, int); typedef int (__stdcall* ChannelCategoryInsertFn)(int, void*); typedef bool (__thiscall* ChannelConnectFn)(void*, int, unsigned short); typedef void* (__cdecl* ChannelConnectorFn)(); typedef void (__cdecl* ChannelEndpointStateResetFn)(); typedef void* (__cdecl* ChannelResidentSingletonFn)(); typedef bool (__thiscall* ChannelResidentResetFn)(void*); typedef void* (__cdecl* GameTransportSingletonFn)(); typedef bool (__thiscall* GameTransportSocketCloseFn)(void*); typedef void (__thiscall* ChannelConnectStateFn)(void*, int); typedef unsigned int* (__stdcall* ChannelQueryFn)(unsigned int*, int); typedef void (__thiscall* AtomicSpinFn)(void*); typedef void (__thiscall* ObfuscatedStoreFn)(void*, const int*, int*); typedef UINT_PTR NativeSocket; typedef int (WINAPI* NativeRecvFn)(NativeSocket, char*, int, int); typedef int (WINAPI* NativeIoctlSocketFn)(NativeSocket, long, unsigned long*); typedef int (WINAPI* NativeWSAEventSelectFn)(NativeSocket, HANDLE, long); struct NativeWSANetworkEvents { long networkEvents; int errorCodes[10]; }; typedef int (WINAPI* NativeWSAEnumNetworkEventsFn)( NativeSocket, HANDLE, NativeWSANetworkEvents*); typedef DWORD (WINAPI* NativeWSAWaitForMultipleEventsFn)( DWORD, const HANDLE*, BOOL, DWORD, BOOL); typedef int (WINAPI* NativeWSAGetLastErrorFn)(); typedef void (WINAPI* NativeWSASetLastErrorFn)(int); ChannelScriptDownloadFn g_originalChannelScriptDownload = nullptr; ChannelScriptLoadFn g_originalChannelScriptLoad = nullptr; ChannelDirectoryApplyFn g_originalChannelDirectoryApply = nullptr; ChannelRuntimeLoadFn g_originalChannelRuntimeLoad = nullptr; ChannelCategoryInsertFn g_originalChannelCategoryInsert = nullptr; ChannelConnectFn g_originalChannelConnect = nullptr; ChannelQueryFn g_originalChannelQuery = nullptr; NativeRecvFn g_originalFirstLoginRecv = nullptr; NativeIoctlSocketFn g_originalFirstLoginIoctlSocket = nullptr; NativeWSAEventSelectFn g_originalFirstLoginEventSelect = nullptr; NativeWSAEnumNetworkEventsFn g_originalFirstLoginEnumNetworkEvents = nullptr; NativeWSAWaitForMultipleEventsFn g_originalFirstLoginWaitForEvents = nullptr; NativeWSAGetLastErrorFn g_firstLoginProbeGetLastError = nullptr; NativeWSASetLastErrorFn g_firstLoginProbeSetLastError = nullptr; void* g_autoConnectChannel = nullptr; volatile LONG g_autoConnectAttempted = 0; UINT_PTR g_autoConnectTimer = 0; dnf90_first_login_recovery::AutoConnectReadiness g_autoConnectReadiness; volatile LONG g_autoConnectDirectoryGeneration = 0; UINT_PTR g_firstLoginWatchdogTimer = 0; volatile LONG g_firstLoginWatchdogActive = 0; volatile LONG g_firstLoginWatchdogGeneration = 0; volatile LONG g_firstLoginServerDataSeen = 0; volatile LONG g_firstLoginClientDataSeen = 0; volatile LONG g_firstLoginRecoveryCount = 0; volatile LONG g_firstLoginInitialSuccessGeneration = 0; volatile LONG g_firstLoginServerProgressLogged = 0; volatile LONG g_firstLoginClientProgressLogged = 0; volatile LONG g_firstLoginProbeGeneration = 0; volatile LONG g_firstLoginProbeRecvRows = 0; volatile LONG g_firstLoginProbeIoctlRows = 0; volatile LONG g_firstLoginProbeEventSelectRows = 0; volatile LONG g_firstLoginProbeEnumRows = 0; volatile LONG g_firstLoginProbeWaitRows = 0; volatile LONG g_firstLoginRawProbeInstalled = 0; bool BytesMatch(const unsigned char* address, const unsigned char* expected, size_t count); bool CommitNativeServerClockState(int serverUnix); bool InstallKnownInlineHook(uintptr_t target, const unsigned char* expected, size_t patchLength, void* detour, void** original, const char* label); void LogLine(const char* format, ...); void LogFirstLoginNativeChainSnapshot( const char* phase, LONG generation, void* channel, void* connector, int connectResult); void QueueLuaEnterTownEventIfAccepted( int packet, unsigned int sceneModeCalls, unsigned int loadingGateCalls, bool sceneModeEarlyReturn, bool loadingGateEarlyReturn); void UpdateLuaDungeonEntryStateAfterDispatch( int packet, unsigned int opcode); __declspec(noinline) void LogProtocolPacket( const char* direction, int messageType, const void* body, int bodyLength); __declspec(noinline) void __cdecl LogClientToServerPacket( int messageType, const void* body, int bodyLength); __declspec(noinline) void __cdecl LogServerToClientPacket( int messageType, const void* body, int bodyLength); void TraceDispatchRegistryEntry(unsigned int classID, unsigned int opcode); bool ApplyPersistedHotkeyTableFromOp454Packet(int packet); __declspec(noinline) LRESULT WINAPI ProxySendMessageW( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller == g_dnfBase + kMinimizeAllSendMessageReturnRva && msg == WM_COMMAND && wParam == 0x19F && lParam == 0) { LogLine("[nomin] blocked audited SendMessageW branch caller_rva=0x%08X hwnd=%p", kMinimizeAllSendMessageReturnRva, hWnd); return 0; } SendMessageWFn original = reinterpret_cast(g_originalSendMessageW); if (original) return original(hWnd, msg, wParam, lParam); LogLine("[nomin] SendMessageW original missing caller=%p", _ReturnAddress()); return 0; } __declspec(noinline) BOOL WINAPI ProxyShowWindow(HWND hWnd, int command) { uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller == g_dnfBase + kForeignShowWindowReturnRva && command == SW_MINIMIZE && hWnd) { DWORD ownerProcessID = 0; GetWindowThreadProcessId(hWnd, &ownerProcessID); if (ownerProcessID && ownerProcessID != GetCurrentProcessId()) { LogLine("[nomin] blocked audited ShowWindow branch caller_rva=0x%08X hwnd=%p pid=%lu", kForeignShowWindowReturnRva, hWnd, ownerProcessID); return TRUE; } } ShowWindowFn original = reinterpret_cast(g_originalShowWindow); if (original) return original(hWnd, command); LogLine("[nomin] ShowWindow original missing caller=%p", _ReturnAddress()); return FALSE; } bool IsMultiClientLaunch() { wchar_t enabled[8] = {}; DWORD length = GetEnvironmentVariableW( L"DNF_MULTI_CLIENT", enabled, static_cast(_countof(enabled))); return length > 0 && length < _countof(enabled) && enabled[0] == L'1'; } __declspec(noinline) HANDLE WINAPI ProxyCreateMutexW( LPSECURITY_ATTRIBUTES attributes, BOOL initialOwner, LPCWSTR name) { CreateMutexWFn original = reinterpret_cast(g_originalCreateMutexW); if (!original) { LogLine("[multi] CreateMutexW original missing caller=%p", _ReturnAddress()); SetLastError(ERROR_PROC_NOT_FOUND); return nullptr; } uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller == g_dnfBase + kMultiClientCreateMutexReturnRva && IsMultiClientLaunch() && name) { wchar_t uniqueName[96] = {}; _snwprintf_s( uniqueName, _countof(uniqueName), _TRUNCATE, L"Local\\DNF90.CurrentEXE.MultiClient.%lu", GetCurrentProcessId()); HANDLE result = original(attributes, initialOwner, uniqueName); DWORD lastError = GetLastError(); LogLine( "[multi] isolated audited instance mutex caller_rva=0x%08X pid=%lu result=%p gle=%lu", kMultiClientCreateMutexReturnRva, GetCurrentProcessId(), result, lastError); SetLastError(lastError); return result; } return original(attributes, initialOwner, name); } __declspec(noinline) HWND WINAPI ProxyFindWindowW( LPCWSTR className, LPCWSTR windowName) { FindWindowWFn original = reinterpret_cast(g_originalFindWindowW); if (!original) { LogLine("[multi] FindWindowW original missing caller=%p", _ReturnAddress()); SetLastError(ERROR_PROC_NOT_FOUND); return nullptr; } uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller == g_dnfBase + kMultiClientFindWindowReturnRva && IsMultiClientLaunch()) { LogLine( "[multi] isolated audited existing-window branch caller_rva=0x%08X pid=%lu", kMultiClientFindWindowReturnRva, GetCurrentProcessId()); SetLastError(ERROR_SUCCESS); return nullptr; } return original(className, windowName); } static const GUID kShellApplicationClsid = {0x13709620, 0xC279, 0x11CE, {0xA4, 0x9E, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}}; static const GUID kShellDispatch4Iid = {0xEFD84B2D, 0x4BCF, 0x4298, {0xBE, 0x25, 0xEB, 0x54, 0x2A, 0x59, 0xFB, 0xDA}}; __declspec(noinline) HRESULT WINAPI ProxyCoCreateInstance( const GUID& classID, LPUNKNOWN outer, DWORD context, const GUID& interfaceID, LPVOID* object) { uintptr_t caller = reinterpret_cast(_ReturnAddress()); if (caller == g_dnfBase + kToggleDesktopCoCreateReturnRva && IsEqualGUID(classID, kShellApplicationClsid) && IsEqualGUID(interfaceID, kShellDispatch4Iid)) { if (object) *object = nullptr; LogLine("[nomin] blocked audited Shell.ToggleDesktop branch caller_rva=0x%08X", kToggleDesktopCoCreateReturnRva); return E_FAIL; } CoCreateInstanceFn original = reinterpret_cast(g_originalCoCreateInstance); if (original) return original(classID, outer, context, interfaceID, object); if (object) *object = nullptr; LogLine("[nomin] CoCreateInstance original missing caller=%p", _ReturnAddress()); return E_FAIL; } void __fastcall ProxySelectedPageApply(void* self, void* /*unused*/) { if (!self) { LogLine("selected page apply skipped null this rva=0x%08X", kSelectedPageApplyRva); return; } SelectedPageApplyFn original = reinterpret_cast(g_originalSelectedPageApply); if (original) original(self); } struct SelectorCreateTraceState { uintptr_t self; uintptr_t button; unsigned int ready; unsigned int pending; unsigned int restriction; unsigned int buttonEnabled; unsigned int buttonStamp; unsigned int globalStamp; int mode; int count; bool click; }; bool ReadSelectorCreateTraceState(void* self, SelectorCreateTraceState* state) { if (!state) return false; memset(state, 0, sizeof(*state)); state->self = reinterpret_cast(self); state->mode = -1; state->count = -1; if (!self) return false; __try { unsigned char* bytes = static_cast(self); unsigned char* button = *reinterpret_cast(bytes + 0xD8); state->button = reinterpret_cast(button); state->ready = bytes[0x9C]; state->pending = bytes[0xCC]; state->mode = *reinterpret_cast(bytes + 0x8C); state->restriction = *reinterpret_cast(g_dnfBase + kSelectorRestrictionRva); state->globalStamp = *reinterpret_cast(g_dnfBase + kUIEventStampRva); uintptr_t begin = *reinterpret_cast(bytes + 0xA4); uintptr_t end = *reinterpret_cast(bytes + 0xA8); if (end >= begin && (end - begin) % sizeof(uintptr_t) == 0 && (end - begin) / sizeof(uintptr_t) <= 32) { state->count = static_cast((end - begin) / sizeof(uintptr_t)); } if (button) { state->buttonEnabled = button[0x61]; state->buttonStamp = *reinterpret_cast(button + 0x10C); state->click = state->buttonEnabled != 0 && state->buttonStamp == state->globalStamp; } return true; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("selector-create-state read exception self=%p code=0x%08X", self, GetExceptionCode()); return false; } } uintptr_t __fastcall ProxySelectorCreateTick(void* self, void* /*unused*/) { SelectorCreateTraceState before = {}; bool stateReady = ReadSelectorCreateTraceState(self, &before); LONG mouseDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0 ? 1 : 0; LONG previousMouseDown = InterlockedExchange(&g_selectorTraceMouseDown, mouseDown); bool mousePressed = mouseDown != 0 && previousMouseDown == 0; bool changed = stateReady && (InterlockedCompareExchange(&g_selectorTraceInitialized, 1, 0) == 0 || before.self != g_selectorTraceSelf || before.button != g_selectorTraceButton || before.ready != g_selectorTraceReady || before.pending != g_selectorTracePending || before.restriction != g_selectorTraceRestriction || before.buttonEnabled != g_selectorTraceButtonEnabled || before.buttonStamp != g_selectorTraceButtonStamp || before.globalStamp != g_selectorTraceGlobalStamp || before.mode != g_selectorTraceMode || before.count != g_selectorTraceCount); if (changed || before.click || mousePressed) { LogLine("selector-create-gate phase=before self=%p button=%p ready=%u " "pending=%u restriction=%u button_enabled=%u button_stamp=%u " "global_stamp=%u click=%u mouse_pressed=%u mode=%d count=%d", self, reinterpret_cast(before.button), before.ready, before.pending, before.restriction, before.buttonEnabled, before.buttonStamp, before.globalStamp, before.click ? 1u : 0u, mousePressed ? 1u : 0u, before.mode, before.count); } if (stateReady) { g_selectorTraceSelf = before.self; g_selectorTraceButton = before.button; g_selectorTraceReady = before.ready; g_selectorTracePending = before.pending; g_selectorTraceRestriction = before.restriction; g_selectorTraceButtonEnabled = before.buttonEnabled; g_selectorTraceButtonStamp = before.buttonStamp; g_selectorTraceGlobalStamp = before.globalStamp; g_selectorTraceMode = before.mode; g_selectorTraceCount = before.count; } SelectorCreateTickFn original = reinterpret_cast(g_originalSelectorCreateTick); uintptr_t result = original ? original(self) : 0; SelectorCreateTraceState after = {}; if ((before.click || mousePressed) && ReadSelectorCreateTraceState(self, &after)) { LogLine("selector-create-gate phase=after self=%p ready=%u pending=%u " "restriction=%u mode=%d count=%d result=0x%08X", self, after.ready, after.pending, after.restriction, after.mode, after.count, static_cast(result)); } return result; } uintptr_t __fastcall ProxySelectorCreateTransition(void* self, void* /*unused*/) { SelectorCreateTraceState before = {}; ReadSelectorCreateTraceState(self, &before); unsigned int autoGuard = 0; __try { if (self) { autoGuard = static_cast(self)[0xBD4]; } } __except (EXCEPTION_EXECUTE_HANDLER) { autoGuard = 0xFFFFFFFFu; } LogLine("selector-create-transition phase=enter self=%p ready=%u pending=%u " "restriction=%u mode=%d count=%d auto_guard=%u", self, before.ready, before.pending, before.restriction, before.mode, before.count, autoGuard); SelectorCreateTransitionFn original = reinterpret_cast(g_originalSelectorCreateTransition); uintptr_t result = original ? original(self) : 0; SelectorCreateTraceState after = {}; ReadSelectorCreateTraceState(self, &after); LogLine("selector-create-transition phase=leave self=%p mode_before=%d " "mode_after=%d count=%d result=0x%08X", self, before.mode, after.mode, after.count, static_cast(result)); return result; } void ReadCreateUIState( void* self, void** button, int* mode, int* group, unsigned int* classReady, unsigned int* pending) { if (button) *button = nullptr; if (mode) *mode = -1; if (group) *group = -1; if (classReady) *classReady = 0; if (pending) *pending = 0; if (!self) return; __try { unsigned char* bytes = static_cast(self); if (button) *button = *reinterpret_cast(bytes + 0x17C); if (group) *group = *reinterpret_cast(bytes + 0x1C4); if (classReady) *classReady = bytes[0x1C8]; if (mode) *mode = *reinterpret_cast(bytes + 0x1CC); if (pending) *pending = bytes[0x301]; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("create-ui-state read exception self=%p code=0x%08X", self, GetExceptionCode()); } } int __fastcall ProxyCreateUIClick(void* self, void* /*unused*/) { void* button = nullptr; int mode = -1; int group = -1; unsigned int classReady = 0; unsigned int pending = 0; ReadCreateUIState(self, &button, &mode, &group, &classReady, &pending); uintptr_t caller = reinterpret_cast(_ReturnAddress()); LogLine("create-ui-click phase=enter self=%p button=%p mode=%d group=%d " "class_ready=%u pending=%u caller=0x%08X caller_rva=0x%08X", self, button, mode, group, classReady, pending, caller, caller >= g_dnfBase ? caller - g_dnfBase : 0); CreateUIClickFn original = reinterpret_cast(g_originalCreateUIClick); int result = original ? original(self) : 0; ReadCreateUIState(self, nullptr, &mode, nullptr, nullptr, &pending); LogLine("create-ui-click phase=leave self=%p result=0x%08X mode=%d pending=%u", self, result, mode, pending); return result; } unsigned char __fastcall ProxyCreateUIOpen( void* self, void* /*unused*/, int* requestedMode, int group) { int requested = -1; __try { if (requestedMode) requested = *requestedMode; } __except (EXCEPTION_EXECUTE_HANDLER) { requested = -2; } uintptr_t caller = reinterpret_cast(_ReturnAddress()); LogLine("create-ui-open phase=enter self=%p mode_ptr=%p requested_mode=%d " "group=%d caller=0x%08X caller_rva=0x%08X", self, requestedMode, requested, group, caller, caller >= g_dnfBase ? caller - g_dnfBase : 0); CreateUIOpenFn original = reinterpret_cast(g_originalCreateUIOpen); unsigned char result = original ? original(self, requestedMode, group) : 0; int mode = -1; int currentGroup = -1; ReadCreateUIState(self, nullptr, &mode, ¤tGroup, nullptr, nullptr); LogLine("create-ui-open phase=leave self=%p result=%u mode=%d group=%d", self, static_cast(result), mode, currentGroup); return result; } int __fastcall ProxyUpperCreateSend(void* self, void* /*unused*/) { int mode = -1; int group = -1; unsigned int job = 0; unsigned int pending = 0; ReadCreateUIState(self, nullptr, &mode, &group, &job, &pending); uintptr_t caller = reinterpret_cast(_ReturnAddress()); LogLine("upper-create-send phase=enter self=%p job=%u mode=%d group=%d " "pending=%u caller=0x%08X caller_rva=0x%08X", self, job, mode, group, pending, caller, caller >= g_dnfBase ? caller - g_dnfBase : 0); UpperCreateSendFn original = reinterpret_cast(g_originalUpperCreateSend); int result = original ? original(self) : 0; ReadCreateUIState(self, nullptr, nullptr, nullptr, nullptr, &pending); LogLine("upper-create-send phase=leave self=%p result=0x%08X pending=%u", self, result, pending); return result; } void LogLine(const char* format, ...) { #ifdef DNF90_SILENT_LOG (void)format; return; #endif wchar_t exePath[MAX_PATH] = { 0 }; GetModuleFileNameW(nullptr, exePath, MAX_PATH); wchar_t* slash = wcsrchr(exePath, L'\\'); if (slash) slash[1] = L'\0'; wchar_t logPath[MAX_PATH] = { 0 }; _snwprintf(logPath, MAX_PATH - 1, L"%s90CN_trace.log", exePath); char line[2048] = { 0 }; SYSTEMTIME now; GetLocalTime(&now); int offset = _snprintf(line, sizeof(line) - 2, "[%04d-%02d-%02d %02d:%02d:%02d.%03d] [TRACE] [pid=%u tid=%u] ", now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond, now.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId()); if (offset < 0) offset = 0; va_list args; va_start(args, format); int wrote = _vsnprintf(line + offset, sizeof(line) - 2 - offset, format, args); va_end(args); int length = wrote >= 0 ? offset + wrote : static_cast(sizeof(line) - 2); line[length++] = '\n'; AcquireSRWLockExclusive(&g_logWriteLock); HANDLE file = CreateFileW(logPath, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file != INVALID_HANDLE_VALUE) { DWORD ignored = 0; WriteFile(file, line, static_cast(length), &ignored, nullptr); CloseHandle(file); } ReleaseSRWLockExclusive(&g_logWriteLock); } constexpr NativeSocket kInvalidNativeSocket = static_cast(~0u); constexpr LONG kFirstLoginProbeMaxRecvRows = 64; constexpr LONG kFirstLoginProbeMaxIoctlRows = 16; constexpr LONG kFirstLoginProbeMaxEventSelectRows = 16; constexpr LONG kFirstLoginProbeMaxEnumRows = 64; constexpr LONG kFirstLoginProbeMaxWaitRows = 64; void RefreshFirstLoginProbeGeneration(LONG generation) { if (generation <= 0) return; LONG previous = InterlockedCompareExchange( &g_firstLoginProbeGeneration, generation, 0); if (previous == generation) return; if (previous != 0 && InterlockedCompareExchange( &g_firstLoginProbeGeneration, generation, previous) != previous) { return; } InterlockedExchange(&g_firstLoginProbeRecvRows, 0); InterlockedExchange(&g_firstLoginProbeIoctlRows, 0); InterlockedExchange(&g_firstLoginProbeEventSelectRows, 0); InterlockedExchange(&g_firstLoginProbeEnumRows, 0); InterlockedExchange(&g_firstLoginProbeWaitRows, 0); LogLine("first-login raw-receive probe generation=%ld previous_generation=%ld", generation, previous); } bool ReadFirstLoginProbeGameSocket(NativeSocket* socketHandle, void** transport) { if (socketHandle) *socketHandle = kInvalidNativeSocket; if (transport) *transport = nullptr; if (!g_dnfBase) return false; __try { GameTransportSingletonFn singleton = reinterpret_cast( g_dnfBase + kGameTransportSingletonRva); void* gameTransport = singleton ? singleton() : nullptr; if (!gameTransport) return false; NativeSocket current = *reinterpret_cast( reinterpret_cast(gameTransport) + kGameTransportSocketHandleOffset); if (socketHandle) *socketHandle = current; if (transport) *transport = gameTransport; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool ShouldTraceFirstLoginSocket( NativeSocket socketHandle, LONG* generation, void** transport) { LONG currentGeneration = InterlockedCompareExchange( &g_autoConnectDirectoryGeneration, 0, 0); if (generation) *generation = currentGeneration; if (transport) *transport = nullptr; if (currentGeneration <= 0 || InterlockedCompareExchange(&g_firstLoginClientDataSeen, 0, 0) != 0) { return false; } RefreshFirstLoginProbeGeneration(currentGeneration); NativeSocket currentSocket = kInvalidNativeSocket; void* gameTransport = nullptr; if (!ReadFirstLoginProbeGameSocket(¤tSocket, &gameTransport) || currentSocket == kInvalidNativeSocket || currentSocket != socketHandle) { return false; } if (transport) *transport = gameTransport; return true; } int CaptureFirstLoginProbeSocketError(int result) { if (result != -1 || !g_firstLoginProbeGetLastError) return 0; return g_firstLoginProbeGetLastError(); } void RestoreFirstLoginProbeSocketError(int result, int error) { if (result == -1 && g_firstLoginProbeSetLastError) { g_firstLoginProbeSetLastError(error); } } void FormatFirstLoginProbeBytes( const char* buffer, int length, char* output, size_t outputSize) { if (!output || outputSize == 0) return; output[0] = '\0'; if (!buffer || length <= 0) return; static const char kHex[] = "0123456789abcdef"; int captured = length > 32 ? 32 : length; size_t used = 0; __try { for (int index = 0; index < captured && used + 2 < outputSize; ++index) { unsigned char value = static_cast(buffer[index]); output[used++] = kHex[value >> 4]; output[used++] = kHex[value & 0x0F]; } } __except (EXCEPTION_EXECUTE_HANDLER) { if (used + 1 < outputSize) output[used++] = '!'; } output[used] = '\0'; } int WINAPI ProxyFirstLoginRecv( NativeSocket socketHandle, char* buffer, int length, int flags) { NativeRecvFn original = g_originalFirstLoginRecv; if (!original) return -1; uintptr_t caller = reinterpret_cast(_ReturnAddress()); int result = original(socketHandle, buffer, length, flags); int error = CaptureFirstLoginProbeSocketError(result); LONG generation = 0; void* transport = nullptr; if (ShouldTraceFirstLoginSocket(socketHandle, &generation, &transport)) { LONG ordinal = InterlockedIncrement(&g_firstLoginProbeRecvRows); if (ordinal <= kFirstLoginProbeMaxRecvRows) { char preview[72] = {}; if (result > 0) { FormatFirstLoginProbeBytes(buffer, result, preview, sizeof(preview)); } LogLine( "first-login raw-recv generation=%ld ordinal=%ld socket=0x%08X " "transport=%p requested=%d flags=0x%X result=%d wsa_error=%d " "caller=0x%08X caller_rva=0x%08X bytes=%s", generation, ordinal, static_cast(socketHandle), transport, length, flags, result, error, static_cast(caller), caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0, preview[0] ? preview : "-"); } } RestoreFirstLoginProbeSocketError(result, error); return result; } int WINAPI ProxyFirstLoginIoctlSocket( NativeSocket socketHandle, long command, unsigned long* argument) { NativeIoctlSocketFn original = g_originalFirstLoginIoctlSocket; if (!original) return -1; unsigned long valueBefore = 0; bool valueReadable = false; __try { if (argument) { valueBefore = *argument; valueReadable = true; } } __except (EXCEPTION_EXECUTE_HANDLER) { valueReadable = false; } uintptr_t caller = reinterpret_cast(_ReturnAddress()); int result = original(socketHandle, command, argument); int error = CaptureFirstLoginProbeSocketError(result); LONG generation = 0; void* transport = nullptr; if (ShouldTraceFirstLoginSocket(socketHandle, &generation, &transport)) { LONG ordinal = InterlockedIncrement(&g_firstLoginProbeIoctlRows); if (ordinal <= kFirstLoginProbeMaxIoctlRows) { LogLine( "first-login socket-ioctl generation=%ld ordinal=%ld socket=0x%08X " "transport=%p command=0x%08X value_read=%d value=0x%08X " "result=%d wsa_error=%d caller_rva=0x%08X", generation, ordinal, static_cast(socketHandle), transport, static_cast(command), valueReadable ? 1 : 0, static_cast(valueBefore), result, error, caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0); } } RestoreFirstLoginProbeSocketError(result, error); return result; } int WINAPI ProxyFirstLoginWSAEventSelect( NativeSocket socketHandle, HANDLE eventHandle, long networkEvents) { NativeWSAEventSelectFn original = g_originalFirstLoginEventSelect; if (!original) return -1; uintptr_t caller = reinterpret_cast(_ReturnAddress()); int result = original(socketHandle, eventHandle, networkEvents); int error = CaptureFirstLoginProbeSocketError(result); LONG generation = 0; void* transport = nullptr; if (ShouldTraceFirstLoginSocket(socketHandle, &generation, &transport)) { LONG ordinal = InterlockedIncrement(&g_firstLoginProbeEventSelectRows); if (ordinal <= kFirstLoginProbeMaxEventSelectRows) { LogLine( "first-login event-select generation=%ld ordinal=%ld " "socket=0x%08X transport=%p event=%p mask=0x%08X " "result=%d wsa_error=%d caller_rva=0x%08X", generation, ordinal, static_cast(socketHandle), transport, eventHandle, static_cast(networkEvents), result, error, caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0); } } RestoreFirstLoginProbeSocketError(result, error); return result; } int WINAPI ProxyFirstLoginWSAEnumNetworkEvents( NativeSocket socketHandle, HANDLE eventHandle, NativeWSANetworkEvents* networkEvents) { NativeWSAEnumNetworkEventsFn original = g_originalFirstLoginEnumNetworkEvents; if (!original) return -1; uintptr_t caller = reinterpret_cast(_ReturnAddress()); int result = original(socketHandle, eventHandle, networkEvents); int error = CaptureFirstLoginProbeSocketError(result); LONG generation = 0; void* transport = nullptr; if (ShouldTraceFirstLoginSocket(socketHandle, &generation, &transport)) { LONG ordinal = InterlockedIncrement(&g_firstLoginProbeEnumRows); if (ordinal <= kFirstLoginProbeMaxEnumRows) { long mask = 0; int readError = 0; int connectError = 0; int closeError = 0; bool eventReadable = false; __try { if (result == 0 && networkEvents) { mask = networkEvents->networkEvents; readError = networkEvents->errorCodes[0]; connectError = networkEvents->errorCodes[4]; closeError = networkEvents->errorCodes[5]; eventReadable = true; } } __except (EXCEPTION_EXECUTE_HANDLER) { eventReadable = false; } LogLine( "first-login enum-network-events generation=%ld ordinal=%ld " "socket=0x%08X transport=%p event=%p result=%d wsa_error=%d " "events_read=%d mask=0x%08X read_error=%d connect_error=%d " "close_error=%d caller_rva=0x%08X", generation, ordinal, static_cast(socketHandle), transport, eventHandle, result, error, eventReadable ? 1 : 0, static_cast(mask), readError, connectError, closeError, caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0); } } RestoreFirstLoginProbeSocketError(result, error); return result; } DWORD WINAPI ProxyFirstLoginWSAWaitForMultipleEvents( DWORD eventCount, const HANDLE* events, BOOL waitAll, DWORD timeout, BOOL alertable) { NativeWSAWaitForMultipleEventsFn original = g_originalFirstLoginWaitForEvents; if (!original) return 0xFFFFFFFFu; uintptr_t caller = reinterpret_cast(_ReturnAddress()); DWORD result = original(eventCount, events, waitAll, timeout, alertable); int socketResult = result == 0xFFFFFFFFu ? -1 : 0; int error = CaptureFirstLoginProbeSocketError(socketResult); LONG generation = InterlockedCompareExchange( &g_autoConnectDirectoryGeneration, 0, 0); if (generation > 0 && InterlockedCompareExchange(&g_firstLoginClientDataSeen, 0, 0) == 0) { RefreshFirstLoginProbeGeneration(generation); NativeSocket currentSocket = kInvalidNativeSocket; void* transport = nullptr; if (ReadFirstLoginProbeGameSocket(¤tSocket, &transport) && currentSocket != kInvalidNativeSocket) { LONG ordinal = InterlockedIncrement(&g_firstLoginProbeWaitRows); if (ordinal <= kFirstLoginProbeMaxWaitRows) { LogLine( "first-login wait-network-events generation=%ld ordinal=%ld " "socket=0x%08X transport=%p count=%lu wait_all=%d " "timeout=%lu alertable=%d result=0x%08X wsa_error=%d " "caller_rva=0x%08X", generation, ordinal, static_cast(currentSocket), transport, eventCount, waitAll ? 1 : 0, timeout, alertable ? 1 : 0, static_cast(result), error, caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0); } } } RestoreFirstLoginProbeSocketError(socketResult, error); return result; } __declspec(noinline) void LogProtocolPacket( const char* direction, int messageType, const void* body, int bodyLength) { if (InterlockedCompareExchange(&g_protocolTraceEnabled, 0, 0) == 0) { return; } wchar_t exePath[MAX_PATH] = { 0 }; GetModuleFileNameW(nullptr, exePath, MAX_PATH); wchar_t* slash = wcsrchr(exePath, L'\\'); if (slash) slash[1] = L'\0'; wchar_t logPath[MAX_PATH] = { 0 }; _snwprintf(logPath, MAX_PATH - 1, L"%s90CN_protocol.log", exePath); SYSTEMTIME now; GetLocalTime(&now); char header[384] = { 0 }; int headerLength = _snprintf(header, sizeof(header) - 1, "[%04d-%02d-%02d %02d:%02d:%02d.%03d] [PROTO] [packet] [pid=%u tid=%u] " "direction=%s msg=%d body_len=%d body=", now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond, now.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId(), direction ? direction : "unknown", messageType, bodyLength); if (headerLength < 0) return; // Packet logging is diagnostic and must never turn an invalid native // pointer into a client crash. Current create/roster packets are small; // cap large scene payloads while retaining their true body length. constexpr int kProtocolBodyLogLimit = 4096; int loggedLength = bodyLength; if (loggedLength < 0) loggedLength = 0; if (loggedLength > kProtocolBodyLogLimit) loggedLength = kProtocolBodyLogLimit; AcquireSRWLockExclusive(&g_logWriteLock); HANDLE file = CreateFileW(logPath, FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { ReleaseSRWLockExclusive(&g_logWriteLock); return; } DWORD ignored = 0; WriteFile(file, header, static_cast(headerLength), &ignored, nullptr); bool readFailed = false; const unsigned char* bytes = static_cast(body); static const char kHex[] = "0123456789ABCDEF"; for (int base = 0; base < loggedLength && !readFailed; base += 64) { int count = loggedLength - base; if (count > 64) count = 64; char hex[64 * 3] = { 0 }; int used = 0; __try { for (int i = 0; i < count; ++i) { unsigned char value = bytes[base + i]; if (base != 0 || i != 0) hex[used++] = ' '; hex[used++] = kHex[value >> 4]; hex[used++] = kHex[value & 0x0F]; } } __except (EXCEPTION_EXECUTE_HANDLER) { readFailed = true; } if (!readFailed && used > 0) { WriteFile(file, hex, static_cast(used), &ignored, nullptr); } } if (readFailed) { static const char kReadFailure[] = " "; WriteFile(file, kReadFailure, sizeof(kReadFailure) - 1, &ignored, nullptr); } else if (bodyLength > loggedLength) { char suffix[96] = { 0 }; int suffixLength = _snprintf(suffix, sizeof(suffix) - 1, " ... ", loggedLength, bodyLength); if (suffixLength > 0) { WriteFile(file, suffix, static_cast(suffixLength), &ignored, nullptr); } } static const char kNewline[] = "\r\n"; WriteFile(file, kNewline, sizeof(kNewline) - 1, &ignored, nullptr); CloseHandle(file); ReleaseSRWLockExclusive(&g_logWriteLock); } static void LogOutgoingPacketStack(const char* tag, int messageType) { if (!g_dnfModuleSize && g_dnfBase) { auto* moduleBase = reinterpret_cast(g_dnfBase); auto* dosHeader = reinterpret_cast(moduleBase); auto* ntHeaders = reinterpret_cast(moduleBase + dosHeader->e_lfanew); g_dnfModuleSize = ntHeaders->OptionalHeader.SizeOfImage; } void* frames[24] = {}; const USHORT count = CaptureStackBackTrace(1, 24, frames, nullptr); char line[512] = {}; int length = _snprintf(line, sizeof(line) - 1, "%s msg=%d frames=%u:", tag, messageType, count); if (length < 0) return; for (USHORT i = 0; i < count && length < static_cast(sizeof(line)) - 16; ++i) { const uintptr_t pc = reinterpret_cast(frames[i]); if (pc >= g_dnfBase && g_dnfModuleSize && pc < g_dnfBase + g_dnfModuleSize) { length += _snprintf(line + length, sizeof(line) - 1 - length, " %x", static_cast(pc - g_dnfBase)); } } LogLine("%s", line); } __declspec(noinline) void __cdecl LogClientToServerPacket( int messageType, const void* body, int bodyLength) { LONG initialGeneration = InterlockedCompareExchange(&g_firstLoginInitialSuccessGeneration, 0, 0); LONG activeGeneration = InterlockedCompareExchange(&g_firstLoginWatchdogGeneration, 0, 0); bool firstLoginTracked = initialGeneration != 0 || InterlockedCompareExchange(&g_firstLoginWatchdogActive, 0, 0) != 0; if (firstLoginTracked && dnf90_first_login_recovery::IsFirstLoginClientProgressMessage(messageType)) { LONG recoveryCount = InterlockedCompareExchange(&g_firstLoginRecoveryCount, 0, 0); // Keep progress sticky across a recovery. A healthy replacement // connection must disable any later stale-socket cleanup. InterlockedExchange(&g_firstLoginClientDataSeen, 1); if (InterlockedCompareExchange( &g_firstLoginClientProgressLogged, 1, 0) == 0) { LONG generation = InterlockedCompareExchange( &g_autoConnectDirectoryGeneration, 0, 0); LogLine( "first-login protocol progress direction=C2S generation=%ld " "initial_generation=%ld msg=%d recovery_count=%ld", generation, initialGeneration ? initialGeneration : activeGeneration, messageType, recoveryCount); } } // The inventory-use fallback must distinguish native op44/op30 emitted // during the same right-click cycle from a missing native dispatch. Keep // this counter independent from optional packet-body tracing. if (messageType == kUseStackableOpcode || messageType == 30) { InterlockedIncrement(&g_clientInventoryUseSendCount); } if (messageType == 39) { static volatile LONG captured = 0; if (InterlockedCompareExchange(&captured, 1, 0) == 0) { LogOutgoingPacketStack("op39-stack", messageType); } } LogProtocolPacket("C2S", messageType, body, bodyLength); } __declspec(noinline) void __cdecl LogServerToClientPacket( int messageType, const void* body, int bodyLength) { LONG initialGeneration = InterlockedCompareExchange(&g_firstLoginInitialSuccessGeneration, 0, 0); LONG activeGeneration = InterlockedCompareExchange(&g_firstLoginWatchdogGeneration, 0, 0); bool firstLoginTracked = initialGeneration != 0 || InterlockedCompareExchange(&g_firstLoginWatchdogActive, 0, 0) != 0; if (firstLoginTracked && dnf90_first_login_recovery::IsFirstLoginServerProgressMessage(messageType)) { LONG recoveryCount = InterlockedCompareExchange(&g_firstLoginRecoveryCount, 0, 0); // serverDataSeen remains diagnostic-only and is sticky across the // bounded replacement attempts. InterlockedExchange(&g_firstLoginServerDataSeen, 1); if (InterlockedCompareExchange( &g_firstLoginServerProgressLogged, 1, 0) == 0) { LONG generation = InterlockedCompareExchange( &g_autoConnectDirectoryGeneration, 0, 0); LogLine( "first-login protocol progress direction=S2C generation=%ld " "initial_generation=%ld msg=%d recovery_count=%ld", generation, initialGeneration ? initialGeneration : activeGeneration, messageType, recoveryCount); } } if (messageType == 173 || messageType == 390 || messageType == 391 || messageType == 454 || messageType == 455) { static volatile LONG settingsRegistryProbed = 0; if (InterlockedCompareExchange(&settingsRegistryProbed, 1, 0) == 0) { // Some optional diagnostics can stop the compatibility installer // before its late startup probes run. At first decoded settings // traffic the native registries are fully initialized, so inspect // all candidate handlers here without executing an unknown packet. TraceDispatchRegistryEntry(0, 173); TraceDispatchRegistryEntry(0, 390); TraceDispatchRegistryEntry(0, 391); TraceDispatchRegistryEntry(0, 454); TraceDispatchRegistryEntry(0, 455); TraceDispatchRegistryEntry(1, 29); } unsigned int lead = 0; __try { if (body && bodyLength >= 4) { lead = *static_cast(body); } } __except (EXCEPTION_EXECUTE_HANDLER) { lead = 0xFFFFFFFFu; } LogLine("settings-s2c-decode opcode=%d body_len=%d body_lead=0x%08X", messageType, bodyLength, lead); } LogProtocolPacket("S2C", messageType, body, bodyLength); } bool ApplyPersistedHotkeyTableFromOp454Packet(int packet) { unsigned short bindings[kHotkeyActionCount] = { 0 }; bool present[kHotkeyActionCount] = { false }; unsigned int keyType = 0; unsigned int payloadLength = 0; unsigned int parsedRowCount = 0; __try { if (!packet) return false; const unsigned char* bytes = reinterpret_cast(packet); const unsigned int packetLength = *reinterpret_cast(bytes + 3); if (packetLength < 21) return false; const unsigned char* body = bytes + 16; const unsigned int bodyLength = packetLength - 16; keyType = body[0]; payloadLength = *reinterpret_cast(body + 1); if (keyType > 1 || payloadLength == 0 || payloadLength > kHotkeyPersistedTextMaxLength || bodyLength != 5 + payloadLength) { LogLine("hotkey persisted table rejected key_type=%u body_len=%u payload_len=%u reason=length", keyType, bodyLength, payloadLength); return false; } const unsigned char* text = body + 5; // Current NoPack's x86 std::wstring uses an eight-wchar inline buffer, // followed by size and capacity. All serialized action tokens are the // three-character I01/C00/J00/K01 form and therefore remain inline. struct NativeShortWideString { wchar_t text[8]; unsigned int length; unsigned int capacity; }; typedef int (__thiscall* HotkeyResolveActionFn)(void*); HotkeyResolveActionFn resolveFn = reinterpret_cast( g_dnfBase + kHotkeyResolveActionRva); if (!resolveFn) return false; unsigned int offset = 0; while (offset < payloadLength) { while (offset < payloadLength && (text[offset] == '\r' || text[offset] == '\n')) ++offset; if (offset == payloadLength) break; const unsigned char prefix = text[offset++]; unsigned int prefixIndex = 0; switch (prefix) { case 'I': prefixIndex = 0; break; case 'C': prefixIndex = 1; break; case 'J': prefixIndex = 2; break; case 'K': prefixIndex = 3; break; default: LogLine("hotkey persisted table rejected key_type=%u offset=%u reason=prefix", keyType, offset - 1); return false; } (void)prefixIndex; if (offset + 2 >= payloadLength || text[offset] < '0' || text[offset] > '9' || text[offset + 1] < '0' || text[offset + 1] > '9' || text[offset + 2] != '\t') { LogLine("keyboard persisted table rejected key_type=%u prefix=%c offset=%u reason=action-token", keyType, prefix, offset); return false; } const unsigned char digitA = text[offset++]; const unsigned char digitB = text[offset++]; ++offset; // tab unsigned int keyCode = 0; unsigned int keyDigits = 0; while (offset < payloadLength && text[offset] >= '0' && text[offset] <= '9') { keyCode = keyCode * 10 + static_cast(text[offset] - '0'); ++offset; ++keyDigits; } if (keyDigits == 0 || keyCode > 0xFF || (offset < payloadLength && text[offset] != '\r' && text[offset] != '\n')) { LogLine("keyboard persisted table rejected key_type=%u token=%c%c%c key=%u reason=value", keyType, prefix, digitA, digitB, keyCode); return false; } NativeShortWideString token = {}; token.text[0] = static_cast(prefix); token.text[1] = static_cast(digitA); token.text[2] = static_cast(digitB); token.length = 3; token.capacity = 7; const int resolvedAction = resolveFn(&token); if (resolvedAction < 0 || resolvedAction >= static_cast(kHotkeyActionCount) || present[resolvedAction]) { LogLine("keyboard persisted table rejected key_type=%u token=%c%c%c resolved=%d reason=resolve", keyType, prefix, digitA, digitB, resolvedAction); return false; } bindings[resolvedAction] = static_cast(keyCode); present[resolvedAction] = true; ++parsedRowCount; } if (parsedRowCount == 0) return false; typedef void* (__cdecl* HotkeyManagerSingletonFn)(); typedef bool (__thiscall* HotkeyApplyBindingFn)(void*, int, int); HotkeyManagerSingletonFn managerFn = reinterpret_cast( g_dnfBase + kHotkeyManagerSingletonRva); HotkeyApplyBindingFn applyFn = reinterpret_cast( g_dnfBase + kHotkeyApplyBindingRva); void* manager = managerFn ? managerFn() : nullptr; if (!manager || !applyFn) return false; unsigned int appliedCount = 0; for (unsigned int action = 0; action < kHotkeyActionCount; ++action) { if (!present[action]) continue; if (applyFn(manager, static_cast(action), static_cast(bindings[action]))) { ++appliedCount; } } LogLine("keyboard persisted table applied key_type=%u payload_len=%u rows=%u applied=%u resolver=native-token-map", keyType, payloadLength, parsedRowCount, appliedCount); return appliedCount == parsedRowCount; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("keyboard persisted table exception key_type=%u payload_len=%u rows=%u code=0x%08X", keyType, payloadLength, parsedRowCount, GetExceptionCode()); return false; } } LONG WINAPI TraceUnhandledClientException(PEXCEPTION_POINTERS exceptionInfo) { if (!exceptionInfo || !exceptionInfo->ExceptionRecord || !exceptionInfo->ContextRecord || g_inExceptionTrace) { return EXCEPTION_CONTINUE_SEARCH; } DWORD code = exceptionInfo->ExceptionRecord->ExceptionCode; switch (code) { case EXCEPTION_ACCESS_VIOLATION: case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: case EXCEPTION_ILLEGAL_INSTRUCTION: case EXCEPTION_INT_DIVIDE_BY_ZERO: case EXCEPTION_PRIV_INSTRUCTION: case EXCEPTION_STACK_OVERFLOW: break; default: return EXCEPTION_CONTINUE_SEARCH; } g_inExceptionTrace = true; CONTEXT* context = exceptionInfo->ContextRecord; ULONG_PTR accessKind = 0; ULONG_PTR accessAddress = 0; if (code == EXCEPTION_ACCESS_VIOLATION && exceptionInfo->ExceptionRecord->NumberParameters >= 2) { accessKind = exceptionInfo->ExceptionRecord->ExceptionInformation[0]; accessAddress = exceptionInfo->ExceptionRecord->ExceptionInformation[1]; } LogLine("exception code=0x%08X address=%p access_kind=%lu access_address=0x%08lX " "eip=0x%08lX esp=0x%08lX ebp=0x%08lX eax=0x%08lX ebx=0x%08lX " "ecx=0x%08lX edx=0x%08lX esi=0x%08lX edi=0x%08lX", code, exceptionInfo->ExceptionRecord->ExceptionAddress, static_cast(accessKind), static_cast(accessAddress), context->Eip, context->Esp, context->Ebp, context->Eax, context->Ebx, context->Ecx, context->Edx, context->Esi, context->Edi); __try { const DWORD* stack = reinterpret_cast(context->Esp); LogLine("exception stack=%08lX %08lX %08lX %08lX %08lX %08lX %08lX %08lX " "%08lX %08lX %08lX %08lX %08lX %08lX %08lX %08lX", stack[0], stack[1], stack[2], stack[3], stack[4], stack[5], stack[6], stack[7], stack[8], stack[9], stack[10], stack[11], stack[12], stack[13], stack[14], stack[15]); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("exception stack unreadable esp=0x%08lX", context->Esp); } g_inExceptionTrace = false; return EXCEPTION_CONTINUE_SEARCH; } void FormatHexBytes(const void* data, int length, char* output, size_t outputSize) { if (!output || outputSize == 0) return; output[0] = '\0'; if (!data || length <= 0) return; const unsigned char* bytes = static_cast(data); size_t used = 0; int cappedLength = length > 96 ? 96 : length; static const char kHex[] = "0123456789ABCDEF"; __try { for (int i = 0; i < cappedLength && used + 3 < outputSize; ++i) { unsigned char value = bytes[i]; output[used++] = kHex[value >> 4]; output[used++] = kHex[value & 0x0F]; if (i + 1 < cappedLength && used + 1 < outputSize) output[used++] = ' '; } output[used] = '\0'; if (length > cappedLength && used + 4 < outputSize) { output[used++] = ' '; output[used++] = '.'; output[used++] = '.'; output[used++] = '.'; output[used] = '\0'; } } __except (EXCEPTION_EXECUTE_HANDLER) { _snprintf(output, outputSize - 1, "", GetExceptionCode()); output[outputSize - 1] = '\0'; } } bool IsObservedDispatchOpcode(unsigned int classID, unsigned int opcode) { if (classID == 1) { return opcode == 4 || opcode == 7 || opcode == 10 || opcode == 11 || opcode == 19 || opcode == 29 || opcode == 81; // Grade-adjust ACK/result popup regression evidence. } return opcode == 1 || opcode == 2 || opcode == 5 || opcode == 9 || opcode == 24 || opcode == 108 || opcode == 120 || opcode == 124 || opcode == 173 || opcode == 390 || opcode == 391 || opcode == 454 || opcode == 455 || opcode == 376 || opcode == 689 || opcode == 693 || opcode == 754 || opcode == 800 || opcode == 1206; } void TraceDispatchRegistryEntry(unsigned int classID, unsigned int opcode) { if (InterlockedCompareExchange(&g_dispatchTraceEnabled, 0, 0) == 0) { return; } void* registry = nullptr; void* record = nullptr; uintptr_t callback = 0; uintptr_t context = 0; __try { DispatchRegistryFn registryFn = reinterpret_cast( g_dnfBase + (classID == 0 ? kClass0RegistryRva : kClass1RegistryRva)); DispatchLookupFn lookupFn = reinterpret_cast( g_dnfBase + kDispatchLookupRva); registry = registryFn ? registryFn() : nullptr; record = registry && lookupFn ? lookupFn(registry, opcode) : nullptr; if (record) { callback = *reinterpret_cast(record); context = *(reinterpret_cast(record) + 1); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("dispatch-registry read exception class=%u opcode=%u code=0x%08X", classID, opcode, GetExceptionCode()); return; } LogLine("dispatch-registry class=%u opcode=%u registry=%p record=%p callback=+0x%08X context=%p", classID, opcode, registry, record, callback ? static_cast(callback - g_dnfBase) : 0, reinterpret_cast(context)); } void TraceDispatchState(const char* phase, unsigned int classID, int packet) { if (InterlockedCompareExchange(&g_dispatchTraceEnabled, 0, 0) == 0) { return; } unsigned int opcode = 0; unsigned int bodyLead = 0; uintptr_t game = 0; int gate = -1; void* registry = nullptr; void* record = nullptr; uintptr_t callback = 0; uintptr_t context = 0; uintptr_t objectManager = 0; uintptr_t currentActor = 0; uintptr_t controlledActor = 0; uintptr_t sceneRoot = 0; unsigned int actorObjectKey = 0; uintptr_t actorByObjectKey = 0; int townEntryState = -1; unsigned int localOwnerServer = 0; unsigned int localOwnerChannel = 0; char gateBytes[128] = { 0 }; __try { if (!packet) return; const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); if (!IsObservedDispatchOpcode(classID, opcode)) return; bodyLead = *reinterpret_cast(bytes + 16); game = *reinterpret_cast(g_dnfBase + kGameSingletonPointerRva); if (game) { const unsigned char* gateAddress = reinterpret_cast( game + kGameDispatchGateOffset); gate = *gateAddress; FormatHexBytes(gateAddress - 8, 24, gateBytes, sizeof(gateBytes)); } DispatchRegistryFn registryFn = reinterpret_cast( g_dnfBase + (classID == 0 ? kClass0RegistryRva : kClass1RegistryRva)); DispatchLookupFn lookupFn = reinterpret_cast( g_dnfBase + kDispatchLookupRva); registry = registryFn ? registryFn() : nullptr; record = registry && lookupFn ? lookupFn(registry, opcode) : nullptr; if (record) { callback = *reinterpret_cast(record); context = *(reinterpret_cast(record) + 1); } objectManager = *reinterpret_cast(g_dnfBase + kObjectManagerPointerRva); controlledActor = *reinterpret_cast(g_dnfBase + kControlledActorPointerRva); sceneRoot = *reinterpret_cast(g_dnfBase + kSceneRootPointerRva); townEntryState = *reinterpret_cast(g_dnfBase + kTownEntryStateRva); localOwnerServer = *reinterpret_cast(g_dnfBase + kLocalOwnerServerRva); localOwnerChannel = *reinterpret_cast(g_dnfBase + kLocalOwnerChannelRva); CurrentActorFn currentActorFn = reinterpret_cast(g_dnfBase + kCurrentActorRva); currentActor = objectManager && currentActorFn ? reinterpret_cast(currentActorFn(reinterpret_cast(objectManager))) : 0; // A mode-0 row stores its actor object key after the 0x47-byte // descriptor. Querying the native object table is read-only and lets // us distinguish a fresh create from a stale-key short circuit. if (classID == 0 && opcode == 2 && bytes[16] == 0 && *reinterpret_cast(bytes + 17) != 0) { actorObjectKey = *reinterpret_cast(bytes + 16 + 76); ActorByObjectKeyFn actorByObjectKeyFn = reinterpret_cast( g_dnfBase + kActorByObjectKeyRva); actorByObjectKey = actorByObjectKeyFn ? reinterpret_cast(actorByObjectKeyFn(static_cast(actorObjectKey))) : 0; } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("dispatch-state read exception phase=%s class=%u opcode=%u code=0x%08X", phase, classID, opcode, GetExceptionCode()); return; } LogLine("dispatch-state phase=%s class=%u opcode=%u body_lead=0x%08X game=%p gate=%d town_entry_state=%d local_owner=%u/%u registry=%p record=%p callback=+0x%08X context=%p object_manager=%p current_actor=%p controlled_actor=%p scene_root=%p actor_key=%u actor_by_key=%p gate_bytes=%s", phase, classID, opcode, bodyLead, reinterpret_cast(game), gate, townEntryState, localOwnerServer, localOwnerChannel, registry, record, callback ? static_cast(callback - g_dnfBase) : 0, reinterpret_cast(context), reinterpret_cast(objectManager), reinterpret_cast(currentActor), reinterpret_cast(controlledActor), reinterpret_cast(sceneRoot), actorObjectKey, reinterpret_cast(actorByObjectKey), gateBytes); if (classID == 0 && (opcode == 108 || opcode == 1206)) { __try { uintptr_t catalog = *reinterpret_cast( g_dnfBase + kSpendTimeCatalogPointerRva); uintptr_t uiObject = *reinterpret_cast( g_dnfBase + kSpendTimeUiSharedRva); uintptr_t uiControl = *reinterpret_cast( g_dnfBase + kSpendTimeUiSharedRva + sizeof(uintptr_t)); unsigned int totalSeconds = catalog ? *reinterpret_cast(catalog + 5 * sizeof(uintptr_t)) : 0; unsigned int rewardCount = catalog ? *reinterpret_cast(catalog + 6 * sizeof(uintptr_t)) : 0; unsigned int completedCount = catalog ? *reinterpret_cast(catalog + 7 * sizeof(uintptr_t)) : 0; LogLine("spend-time native phase=%s opcode=%u catalog=%p total=%u rewards=%u completed=%u ui_object=%p ui_control=%p", phase, opcode, reinterpret_cast(catalog), totalSeconds, rewardCount, completedCount, reinterpret_cast(uiObject), reinterpret_cast(uiControl)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("spend-time native read exception phase=%s opcode=%u code=0x%08X", phase, opcode, GetExceptionCode()); } } } // Read-only current-EXE evidence for town co-presence. The server can prove // delivery, but only the native object tables can distinguish a rejected // mode0 create from a later display-registration failure. Keep this bounded to // the four actor-state messages and never mutate an actor or scene manager. void TraceTownCopresenceDispatchState(const char* phase, int packet) { if (InterlockedCompareExchange(&g_dispatchTraceEnabled, 0, 0) == 0) { return; } unsigned int opcode = 0; unsigned int mode = 0xFF; unsigned int ownerContext = 0; unsigned int objectKey = 0; unsigned int recordKind = 0xFF; uintptr_t byObjectKey = 0; uintptr_t byContext = 0; uintptr_t sceneContext = 0; uintptr_t currentActor = 0; __try { if (!packet) return; const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); unsigned int packetLength = *reinterpret_cast(bytes + 3); if (packetLength < 16) return; const unsigned char* body = bytes + 16; const size_t bodyLength = packetLength - 16; if (opcode == 2 && bodyLength >= 5) { mode = body[0]; ownerContext = body[4]; if (mode == 0 && bodyLength >= 0x4E) { objectKey = *reinterpret_cast(body + 0x4C); } else if (mode == 1 && bodyLength >= 0x17) { objectKey = *reinterpret_cast(body + 0x15); } else { return; } } else if (opcode == 9 && bodyLength >= 10 && *reinterpret_cast(body) != 0) { objectKey = *reinterpret_cast(body + 4); ownerContext = body[7]; recordKind = body[9]; } else if (opcode == 23 && bodyLength >= 2) { objectKey = *reinterpret_cast(body); } else if (opcode == 24 && bodyLength >= 4) { // Op24 has no owner context. Log its selected final row only; the // bridge separately records every roster row on the wire. unsigned int count = *reinterpret_cast(body + 2); if (count != 0 && bodyLength >= 4 + count * 8) { objectKey = *reinterpret_cast( body + 4 + (count - 1) * 8); } } else { return; } uintptr_t objectManager = *reinterpret_cast(g_dnfBase + kObjectManagerPointerRva); uintptr_t sceneActorManager = *reinterpret_cast(g_dnfBase + kSceneActorManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); ActorByObjectKeyFn actorByObjectKeyFn = reinterpret_cast( g_dnfBase + kActorByObjectKeyRva); ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); ResolveActorInContextFn resolveActorInContextFn = reinterpret_cast( g_dnfBase + kResolveActorInContextRva); currentActor = objectManager && currentActorFn ? reinterpret_cast( currentActorFn(reinterpret_cast(objectManager))) : 0; byObjectKey = objectKey && actorByObjectKeyFn ? reinterpret_cast( actorByObjectKeyFn(static_cast(objectKey))) : 0; byContext = objectKey && actorByContextFn ? reinterpret_cast(actorByContextFn( static_cast(objectKey), static_cast(ownerContext))) : 0; sceneContext = sceneActorManager && objectKey && resolveActorInContextFn ? reinterpret_cast(resolveActorInContextFn( reinterpret_cast(sceneActorManager), static_cast(ownerContext), static_cast(objectKey))) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("town-copresence-dispatch read exception phase=%s opcode=%u code=0x%08X", phase, opcode, GetExceptionCode()); return; } LogLine("town-copresence-dispatch phase=%s opcode=%u mode=%u owner=%u key=%u kind=%u by_key=%p by_context=%p scene_context=%p current_actor=%p", phase, opcode, mode, ownerContext, objectKey, recordKind, reinterpret_cast(byObjectKey), reinterpret_cast(byContext), reinterpret_cast(sceneContext), reinterpret_cast(currentActor)); } void* __cdecl ProxyTownActorByObjectKey(unsigned short objectKey) { void* projectionActor = dnf90_companion_p0::ProjectionActorOverrideForObjectKey(objectKey); if (projectionActor) { LogLine("companion p1 scoped actor lookup override key=%u actor=%p", objectKey, projectionActor); return projectionActor; } ActorByObjectKeyFn original = reinterpret_cast( g_originalActorByObjectKey); void* actor = original ? original(objectKey) : nullptr; if (actor || objectKey == 0) return actor; unsigned char ownerContext = g_townActorOwnerByObjectKey[objectKey]; if (ownerContext == 0) return nullptr; ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); actor = actorByContextFn ? actorByContextFn(objectKey, static_cast(ownerContext)) : nullptr; bool packetScoped = g_townActorContextLookupActive && objectKey == g_townActorContextLookupKey; if (actor && !packetScoped) { void* visual = *reinterpret_cast( reinterpret_cast(actor) + kActorVisualOffset); if (!visual) return nullptr; } if (actor && !g_townActorLookupLoggedByObjectKey[objectKey]) { g_townActorLookupLoggedByObjectKey[objectKey] = 1; uintptr_t caller = reinterpret_cast(_ReturnAddress()); LogLine("town co-presence actor lookup fallback key=%u owner=%u actor=%p packet_scoped=%d caller=+0x%08X", objectKey, ownerContext, actor, packetScoped ? 1 : 0, caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0); } return actor; } // Current NoPack's remote mode-0 branch stores the actor in the owner-context // map but does not construct its actor+0x4C4 visual component. Live acceptance // proved that sub_26A2980 is not a general town-presence list: adding an // ungrouped peer to its second slot makes the native client render that peer as // a blue-name party member. Call only the exact visual constructor reached by // that function, leaving native party-slot membership untouched. bool EnsureTownRemoteActorVisual( unsigned short objectKey, unsigned char ownerContext, void* actor) { if (!objectKey || !ownerContext || !actor) return false; // 10/27 client compatibility: the 10/28 scene-actor creation path resolves // an actor layout this EXE does not provide and crashes at the visual // constructor. Keep the actor/context registration for P0 and the current // actor, but do not synthesize remote town visuals here. LogLine("town co-presence remote visual disabled key=%u owner=%u actor=%p", objectKey, ownerContext, actor); return false; __try { // Register/retrieve the actor through the scene actor manager. Without // this the mode-0 row only creates the global object+visual and the // dungeon room renderer never sees it. uintptr_t sceneActorManager = *reinterpret_cast(g_dnfBase + kSceneActorManagerPointerRva); ResolveActorInContextFn resolveActorInContextFn = reinterpret_cast( g_dnfBase + kResolveActorInContextRva); void* sceneActor = sceneActorManager && resolveActorInContextFn ? resolveActorInContextFn( reinterpret_cast(sceneActorManager), ownerContext, objectKey) : nullptr; if (sceneActor) { LogLine("town co-presence scene actor resolved key=%u owner=%u global=%p scene=%p", objectKey, ownerContext, actor, sceneActor); actor = sceneActor; } uintptr_t objectManager = *reinterpret_cast(g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); void* currentActor = objectManager && currentActorFn ? currentActorFn(reinterpret_cast(objectManager)) : nullptr; if (actor == currentActor) return false; void** visual = reinterpret_cast( reinterpret_cast(actor) + kActorVisualOffset); if (*visual) { LogLine("town co-presence actor visual already exists key=%u owner=%u actor=%p visual=%p", objectKey, ownerContext, actor, *visual); return true; } ActorVisualCreateFn createFn = reinterpret_cast( g_dnfBase + kActorVisualCreateRva); if (!createFn) return false; createFn(actor, 0); LogLine("town co-presence actor visual create key=%u owner=%u actor=%p visual=%p", objectKey, ownerContext, actor, *visual); return *visual != nullptr; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("town co-presence actor visual create exception key=%u owner=%u actor=%p code=0x%08X", objectKey, ownerContext, actor, GetExceptionCode()); return false; } } void RemoveTownRemoteActorVisual( unsigned short objectKey, unsigned char ownerContext) { if (!objectKey || !ownerContext) return; __try { ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); void* actor = actorByContextFn ? actorByContextFn(objectKey, static_cast(ownerContext)) : nullptr; if (!actor) return; void** visual = reinterpret_cast( reinterpret_cast(actor) + kActorVisualOffset); if (!*visual) return; ActorVisualDestroyFn destroyFn = reinterpret_cast( g_dnfBase + kActorVisualDestroyRva); if (!destroyFn) return; void* visualBefore = *visual; destroyFn(actor); LogLine("town co-presence actor visual remove key=%u owner=%u actor=%p visual_before=%p visual_after=%p", objectKey, ownerContext, actor, visualBefore, *visual); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("town co-presence actor visual remove exception key=%u owner=%u code=0x%08X", objectKey, ownerContext, GetExceptionCode()); } } void RepairTownCurrentActor(unsigned short objectKey) { __try { uintptr_t objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); if (!objectManager) { LogLine("town current actor repair skipped key=%u reason=no-object-manager", objectKey); return; } CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); void* currentBefore = currentActorFn ? currentActorFn(reinterpret_cast(objectManager)) : nullptr; ActorByObjectKeyFn originalActorByObjectKey = reinterpret_cast(g_originalActorByObjectKey); void* actor = originalActorByObjectKey ? originalActorByObjectKey(objectKey) : nullptr; const char* source = "global"; if (!actor) { ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); actor = actorByContextFn ? actorByContextFn(objectKey, 0) : nullptr; source = "context0"; } void** cache = reinterpret_cast( objectManager + kCurrentActorCacheOffset); void* cacheBefore = *cache; bool repaired = false; if (!currentBefore && actor) { // sub_269A050 owns this exact cache slot and lazily fills it with // sub_2699A30's result. The context-0 bridge row can create the // selected actor after that first lazy lookup has cached null, so // complete the same native assignment after op2 has initialized it. *cache = actor; repaired = true; } void* currentAfter = currentActorFn ? currentActorFn(reinterpret_cast(objectManager)) : *cache; LogLine("town current actor repair key=%u source=%s actor=%p cache_before=%p current_before=%p current_after=%p repaired=%d verified=%d", objectKey, source, actor, cacheBefore, currentBefore, currentAfter, repaired ? 1 : 0, actor && currentAfter == actor ? 1 : 0); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("town current actor repair exception key=%u code=0x%08X", objectKey, GetExceptionCode()); } } void SyncCurrentActorExpertJobType(unsigned int expertJobType) { if (!g_dnfBase || expertJobType > 4) return; __try { void* objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); void* actor = objectManager && currentActorFn ? currentActorFn(objectManager) : nullptr; if (!actor) { LogLine("expert-job actor sync skipped type=%u reason=current-actor-missing", expertJobType); return; } dnf90_expert_job::PreviousTypes previous = {}; if (!dnf90_expert_job::MirrorType(actor, expertJobType, previous)) return; g_cachedExpertJobActor = actor; g_cachedExpertJobObjectKey = g_currentActorObjectKey; InterlockedExchange(&g_cachedExpertJobType, static_cast(expertJobType)); LogLine("expert-job actor sync type=%u previous=%u previous_function=%u actor=%p source=class0-op205-native-manager-projection", expertJobType, previous.personalInfo, previous.function, actor); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("expert-job actor sync exception type=%u code=0x%08X", expertJobType, GetExceptionCode()); } } void ReapplyCurrentActorExpertJobTypeAfterMode0( unsigned short objectKey, unsigned char ownerContext) { if (!g_dnfBase || !objectKey) return; __try { void* objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); void* currentActor = objectManager && currentActorFn ? currentActorFn(objectManager) : nullptr; if (!currentActor) return; ActorByObjectKeyFn originalActorByObjectKey = reinterpret_cast(g_originalActorByObjectKey); void* packetActor = originalActorByObjectKey ? originalActorByObjectKey(objectKey) : nullptr; if (!packetActor && ownerContext != 0) { ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); packetActor = actorByContextFn ? actorByContextFn(objectKey, static_cast(ownerContext)) : nullptr; } if (!packetActor || packetActor != currentActor) return; g_currentActorObjectKey = objectKey; LONG cachedType = InterlockedCompareExchange( &g_cachedExpertJobType, -1, -1); if (cachedType < 0 || cachedType > 4 || g_cachedExpertJobActor != currentActor || (g_cachedExpertJobObjectKey != 0 && g_cachedExpertJobObjectKey != objectKey)) { return; } dnf90_expert_job::PreviousTypes previous = {}; if (!dnf90_expert_job::MirrorType(currentActor, static_cast(cachedType), previous)) return; g_cachedExpertJobObjectKey = objectKey; LogLine("expert-job actor reproject type=%u previous=%u previous_function=%u key=%u owner=%u actor=%p source=class0-op2-mode0-current-actor-refresh", static_cast(cachedType), previous.personalInfo, previous.function, objectKey, ownerContext, currentActor); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("expert-job actor reproject exception key=%u owner=%u code=0x%08X", objectKey, ownerContext, GetExceptionCode()); } } void CacheCurrentCharacterStatsFromClass0Packet(int packet); bool TryConsumePrivateCombatPowerAffixes(int packet) { DNF90DamageAffixSnapshot snapshot = {}; bool matched = false; __try { if (packet) { const unsigned char* bytes = reinterpret_cast(packet); const unsigned short opcode = *reinterpret_cast(bytes + 1); const unsigned int packetLength = *reinterpret_cast(bytes + 3); if (opcode == kCombatPowerAffixPrivateOpcode && (packetLength == kCombatPowerAffixPrivatePacketLengthV4 || packetLength == kCombatPowerAffixPrivatePacketLengthV5) && bytes[16] == kCombatPowerAffixPrivateType && (bytes[17] == kCombatPowerAffixPrivateVersionV4 || bytes[17] == kCombatPowerAffixPrivateVersionV5)) { const unsigned char* body = bytes + 16; snapshot.size = sizeof(snapshot); snapshot.generation = static_cast( InterlockedIncrement(&g_damageAffixSnapshotGeneration)); snapshot.validFlags = DNF90_DAMAGE_AFFIX_SNAPSHOT_VALUES_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_IDENTITY_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_THREE_ATTACKS_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_EQUIPMENT_SCORE_VALID; snapshot.version = body[1]; snapshot.whiteDamageTenths = *reinterpret_cast(body + 2); snapshot.yellowDamageTenths = *reinterpret_cast(body + 4); snapshot.criticalDamageTenths = *reinterpret_cast(body + 6); snapshot.yellowAdditionalTenths = *reinterpret_cast(body + 8); snapshot.criticalAdditionalTenths = *reinterpret_cast(body + 10); snapshot.allAttackTenths = *reinterpret_cast(body + 12); snapshot.equippedItemCount = *reinterpret_cast(body + 14); snapshot.activeSetCount = *reinterpret_cast(body + 16); snapshot.job = body[18]; snapshot.growType = body[19]; snapshot.level = body[20]; const unsigned int professionLength = body[21]; snapshot.physicalAttack = *reinterpret_cast(body + 22); snapshot.magicalAttack = *reinterpret_cast(body + 26); snapshot.independentAttack = *reinterpret_cast(body + 30); snapshot.pvfEquipmentScore = *reinterpret_cast(body + 66); if (professionLength > 0 && professionLength < sizeof(snapshot.professionUtf8)) { memcpy(snapshot.professionUtf8, body + 34, professionLength); snapshot.professionUtf8[professionLength] = '\0'; } matched = snapshot.equippedItemCount <= DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS && snapshot.level > 0 && professionLength > 0 && professionLength < sizeof(snapshot.professionUtf8); } } } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } if (!matched) return false; AcquireSRWLockExclusive(&g_damageAffixSnapshotLock); g_damageAffixSnapshot = snapshot; ReleaseSRWLockExclusive(&g_damageAffixSnapshotLock); LONG hit = InterlockedIncrement(&g_damageAffixSnapshotUpdateCount); if (hit <= 8) { LogLine("combat-power affix private projection consumed hit=%ld generation=%u white=%u yellow=%u critical=%u yellow_add=%u critical_add=%u all_attack=%u items=%u sets=%u job=%u grow=%u level=%u physical=%u magical=%u independent=%u pvf_equipment_score=%u profession=%s", hit, snapshot.generation, snapshot.whiteDamageTenths, snapshot.yellowDamageTenths, snapshot.criticalDamageTenths, snapshot.yellowAdditionalTenths, snapshot.criticalAdditionalTenths, snapshot.allAttackTenths, snapshot.equippedItemCount, snapshot.activeSetCount, snapshot.job, snapshot.growType, snapshot.level, snapshot.physicalAttack, snapshot.magicalAttack, snapshot.independentAttack, snapshot.pvfEquipmentScore, snapshot.professionUtf8); } return true; } bool TryRefreshMailboxHudFromNativeModel() { if (InterlockedCompareExchange( &g_mailboxHudRefreshTargetsVerified, 0, 0) == 0) { return false; } __try { MailboxHudManagerFn managerFn = reinterpret_cast( g_dnfBase + kMailboxHudManagerRva); MailboxHudReadyFn readyFn = reinterpret_cast( g_dnfBase + kMailboxHudReadyRva); MailboxHudRefreshFn refreshFn = reinterpret_cast( g_dnfBase + kMailboxHudRefreshRva); void* manager = managerFn ? managerFn() : nullptr; if (!manager || !readyFn || !readyFn(manager) || !refreshFn) { return false; } // sub_1FBD580, reached by sub_1FC2050, owns the real mailbox HUD // decision: vector-size + unread-count == 0 calls sub_1FB4F70(7) // (hide), otherwise it calls sub_1FB7280(7) (show). Do not write the // indicator or the mailbox model directly here. refreshFn(manager); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { LONG row = InterlockedIncrement(&g_mailboxHudRefreshLogCount); if (row <= 8) { LogLine("mailbox HUD native refresh exception code=0x%08X", GetExceptionCode()); } return false; } } void RefreshMailboxHudAfterClass0Dispatch( int packet, unsigned int opcode, unsigned int packetLength) { if (opcode == kMailboxAlarmOpcode) { if (!packet || packetLength < 18) { return; } unsigned short unread = 0; __try { unread = *reinterpret_cast(packet + 16); } __except (EXCEPTION_EXECUTE_HANDLER) { return; } // A positive alarm already runs the native show path. It must cancel a // deferred zero refresh from an earlier character before that refresh // can hide a newly valid indicator. InterlockedExchange(&g_mailboxHudZeroRefreshPending, unread == 0 ? 1 : 0); } if (InterlockedCompareExchange( &g_mailboxHudZeroRefreshPending, 0, 0) == 0) { return; } if (TryRefreshMailboxHudFromNativeModel()) { InterlockedExchange(&g_mailboxHudZeroRefreshPending, 0); LONG row = InterlockedIncrement(&g_mailboxHudRefreshLogCount); if (row <= 8) { LogLine("mailbox HUD recomputed after zero alarm opcode=%u packet_len=%u", opcode, packetLength); } } } bool TryConsumeDungeonAssistPresence(int packet); int __stdcall ProxyTownCopresenceClass0Dispatch(int a1, int packet) { // The production build installs this wrapper as the active class0 // dispatcher. The historical ProxyClass0Dispatch below is retained only // in the disabled research installer, so companion projections must be // captured here before the native dispatcher consumes the packet. // 章节过场日志(只读):这里是**生产环境真正的** class0 入口,op1378/op24 // 都从这里过,所以日志必须挂在这里。 if (packet) { __try { cn90::LogClass0CutscenePacket( reinterpret_cast(packet)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("digest trace exception code=0x%08X", GetExceptionCode()); } } if (packet) { bool projectionCaptured = false; __try { projectionCaptured = dnf90_companion_p0::CaptureProjectionClass0Packet( a1, packet); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("companion projection active-class0 capture exception code=0x%08X", GetExceptionCode()); } if (projectionCaptured) { LogLine("companion projection active-class0 packet captured dispatcher=%d packet=%p", a1, reinterpret_cast(packet)); } } if (TryConsumePrivateCombatPowerAffixes(packet)) return 1; if (TryConsumeDungeonAssistPresence(packet)) return 1; TraceTownCopresenceDispatchState("before", packet); unsigned int opcode = 0; unsigned int mode = 0xFF; unsigned int packetLength = 0; unsigned int expertJobType = 0; bool expertJobTypePresent = false; unsigned short objectKey = 0; unsigned char ownerContext = 0; __try { if (packet) { const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); packetLength = *reinterpret_cast(bytes + 3); if (packetLength >= 16) { const unsigned char* body = bytes + 16; const size_t bodyLength = packetLength - 16; if ((opcode == 22 || opcode == 23 || opcode == 6) && bodyLength >= 2) { objectKey = *reinterpret_cast(body); } else if (opcode == 2 && bodyLength >= 5) { mode = body[0]; ownerContext = body[4]; if (mode == 0 && bodyLength >= 0x4E) { objectKey = *reinterpret_cast(body + 0x4C); } } else if (opcode == kExpertJobInfoOpcode && bodyLength >= 2 && body[1] <= 4) { expertJobType = body[1]; expertJobTypePresent = true; } } } } __except (EXCEPTION_EXECUTE_HANDLER) { opcode = 0; mode = 0xFF; expertJobType = 0; expertJobTypePresent = false; objectKey = 0; ownerContext = 0; } bool previousTraceOp24 = g_traceOp24; unsigned int previousSceneModeCalls = g_op24SceneModeCalls; unsigned int previousLoadingGateCalls = g_op24LoadingGateCalls; bool previousSceneModeEarlyReturn = g_op24SceneModeEarlyReturn; bool previousLoadingGateEarlyReturn = g_op24LoadingGateEarlyReturn; bool currentOp24 = opcode == 24; g_traceOp24 = currentOp24; if (currentOp24) { g_op24SceneModeCalls = 0; g_op24LoadingGateCalls = 0; g_op24SceneModeEarlyReturn = false; g_op24LoadingGateEarlyReturn = false; } bool previousLookupActive = g_townActorContextLookupActive; unsigned short previousLookupKey = g_townActorContextLookupKey; bool enableContextLookup = (opcode == 22 || opcode == 23) && objectKey != 0 && g_townActorOwnerByObjectKey[objectKey] != 0; g_townActorContextLookupActive = enableContextLookup; g_townActorContextLookupKey = enableContextLookup ? objectKey : 0; unsigned char removeOwnerContext = opcode == 6 && objectKey != 0 ? g_townActorOwnerByObjectKey[objectKey] : 0; if (removeOwnerContext != 0) { RemoveTownRemoteActorVisual(objectKey, removeOwnerContext); g_townActorOwnerByObjectKey[objectKey] = 0; g_townActorLookupLoggedByObjectKey[objectKey] = 0; } Class0DispatchFn original = reinterpret_cast( g_originalClass0Dispatch); unsigned seriaPacketTicket = dnf90_seria::BeginPacket(packet); int result = 0; __try { result = original ? original(a1, packet) : 0; } __finally { dnf90_seria::EndPacket(seriaPacketTicket); } // Native class0/op0x63 always commits its count to the mailbox model, but // count==0 skips the HUD recompute. Repair only that post-dispatch gap; // later class0 packets provide a bounded retry if the town UI is not ready // at the exact alarm callback. RefreshMailboxHudAfterClass0Dispatch(packet, opcode, packetLength); if (opcode == 454) { ApplyPersistedHotkeyTableFromOp454Packet(packet); } // This is the active current-profile class0 entrypoint. Cache the already // accepted local mode1/mode3 stat blob here; ProxyClass0Dispatch belongs // to the fallback profile and is not installed alongside this hook. CacheCurrentCharacterStatsFromClass0Packet(packet); unsigned int currentSceneModeCalls = currentOp24 ? g_op24SceneModeCalls : 0; unsigned int currentLoadingGateCalls = currentOp24 ? g_op24LoadingGateCalls : 0; bool currentSceneModeEarlyReturn = currentOp24 && g_op24SceneModeEarlyReturn; bool currentLoadingGateEarlyReturn = currentOp24 && g_op24LoadingGateEarlyReturn; g_traceOp24 = previousTraceOp24; g_op24SceneModeCalls = previousSceneModeCalls; g_op24LoadingGateCalls = previousLoadingGateCalls; g_op24SceneModeEarlyReturn = previousSceneModeEarlyReturn; g_op24LoadingGateEarlyReturn = previousLoadingGateEarlyReturn; g_townActorContextLookupActive = previousLookupActive; g_townActorContextLookupKey = previousLookupKey; if (expertJobTypePresent) { SyncCurrentActorExpertJobType(expertJobType); } if (opcode == 2 && mode == 0 && objectKey != 0 && ownerContext == 0) { // Only the selected actor's own mode0 may repair the current-actor cache. // A volunteer/party actor arriving with an owner-less context must be // registered as a remote actor instead; adopting it as current is the // known M1 crash. if (g_currentActorObjectKey == 0 || objectKey == g_currentActorObjectKey) { RepairTownCurrentActor(objectKey); } else { LogLine("town co-presence current-actor repair skipped key=%u current_key=%u reason=foreign-ownerless-mode0", objectKey, g_currentActorObjectKey); } } if (opcode == 2 && mode == 0 && objectKey != 0) { ReapplyCurrentActorExpertJobTypeAfterMode0(objectKey, ownerContext); } if (opcode == 2 && mode == 0 && objectKey != 0 && ownerContext != 0) { ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); void* actor = actorByContextFn ? actorByContextFn(objectKey, static_cast(ownerContext)) : nullptr; bool globalActor = false; if (!actor) { ActorByObjectKeyFn originalActorByObjectKey = reinterpret_cast(g_originalActorByObjectKey); actor = originalActorByObjectKey ? originalActorByObjectKey(objectKey) : nullptr; globalActor = actor != nullptr; } if (actor) { if (!globalActor) { g_townActorOwnerByObjectKey[objectKey] = ownerContext; g_townActorLookupLoggedByObjectKey[objectKey] = 0; } LogLine("town co-presence actor registered key=%u owner=%u actor=%p global=%d", objectKey, ownerContext, actor, globalActor ? 1 : 0); EnsureTownRemoteActorVisual(objectKey, ownerContext, actor); } } else if (opcode == 6 && objectKey != 0) { g_townActorOwnerByObjectKey[objectKey] = 0; if (g_currentActorObjectKey == objectKey) { g_currentActorObjectKey = 0; g_cachedExpertJobObjectKey = 0; g_cachedExpertJobActor = nullptr; InterlockedExchange(&g_cachedExpertJobType, -1); } LogLine("town co-presence actor context removed key=%u", objectKey); } TraceTownCopresenceDispatchState("after", packet); UpdateLuaDungeonEntryStateAfterDispatch(packet, opcode); if (currentOp24) { QueueLuaEnterTownEventIfAccepted( packet, currentSceneModeCalls, currentLoadingGateCalls, currentSceneModeEarlyReturn, currentLoadingGateEarlyReturn); } if (dnf90_companion_p0::PollFixedApc5405()) { LogLine("companion p0 fixed APC submitted source=class0-retry"); } if (dnf90_companion_p0::ApplyPendingProjection()) { LogLine("companion p1 character projection applied source=class0-safe-point"); } return result; } bool ReadEquipmentWireBytes(const unsigned char** cursor, const unsigned char* end, void* output, size_t size) { if (!cursor || !*cursor || !end || *cursor > end || size > static_cast(end - *cursor)) { return false; } if (output && size != 0) memcpy(output, *cursor, size); *cursor += size; return true; } bool SkipEquipmentWireBytes(const unsigned char** cursor, const unsigned char* end, size_t size) { return ReadEquipmentWireBytes(cursor, end, nullptr, size); } bool SkipEquipmentWireRawBlock(const unsigned char** cursor, const unsigned char* end) { unsigned int length = 0; if (!ReadEquipmentWireBytes(cursor, end, &length, sizeof(length)) || length > 1024) { return false; } return SkipEquipmentWireBytes(cursor, end, length); } bool IsCombatPowerEquipmentSlot(unsigned char actorSlot) { // Current mode1/PVF evidence: 12 is weapon, 13 is title, 14..18 are // armor, 19..21 are accessories, 22/23 are support/magic stone, and 25 // is earring. Slots 0..11 are avatar/aura objects; 26+ are creature, // medal, gems, and other independent systems. return (actorSlot >= 12 && actorSlot <= 23) || actorSlot == 25; } bool ParseCurrentEquipmentSnapshot(const unsigned char* body, size_t bodyLength, size_t statDataOffset, unsigned int sourceStatGeneration, DNF90EquipmentSnapshot* output) { if (!body || !output) return false; constexpr size_t kCurrentActorStatWireBytes = 92; const size_t countOffset = statDataOffset + kCurrentActorStatWireBytes + 1; // extra-equipment-slot bitset if (countOffset >= bodyLength) return false; const unsigned char* cursor = body + countOffset; const unsigned char* end = body + bodyLength; unsigned char rowCount = 0; if (!ReadEquipmentWireBytes(&cursor, end, &rowCount, sizeof(rowCount)) || rowCount > DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS) { return false; } DNF90EquipmentSnapshot snapshot = {}; snapshot.size = sizeof(snapshot); snapshot.sourceStatGeneration = sourceStatGeneration; snapshot.validFlags = DNF90_EQUIPMENT_SNAPSHOT_ROWS_VALID; snapshot.itemCount = rowCount; uint64_t seenSlots = 0; for (unsigned int index = 0; index < rowCount; ++index) { unsigned char actorSlot = 0; unsigned int itemID = 0; unsigned int qualitySeed = 0; unsigned char extData = 0; unsigned short durability = 0; unsigned int linkedItemID = 0; unsigned int auxiliaryValue = 0; unsigned char auxiliaryByte = 0; unsigned char amplifyType = 0; unsigned short amplifyValue = 0; if (!ReadEquipmentWireBytes(&cursor, end, &actorSlot, sizeof(actorSlot)) || !ReadEquipmentWireBytes(&cursor, end, &itemID, sizeof(itemID)) || !ReadEquipmentWireBytes(&cursor, end, &qualitySeed, sizeof(qualitySeed)) || !ReadEquipmentWireBytes(&cursor, end, &extData, sizeof(extData)) || !ReadEquipmentWireBytes(&cursor, end, &durability, sizeof(durability)) || !ReadEquipmentWireBytes(&cursor, end, &linkedItemID, sizeof(linkedItemID)) || actorSlot > 32 || itemID == 0 || (seenSlots & (uint64_t(1) << actorSlot)) != 0) { return false; } seenSlots |= uint64_t(1) << actorSlot; if (linkedItemID != 0 && actorSlot == 9 && !SkipEquipmentWireRawBlock(&cursor, end)) { return false; } if (!ReadEquipmentWireBytes(&cursor, end, &auxiliaryValue, sizeof(auxiliaryValue)) || !ReadEquipmentWireBytes(&cursor, end, &auxiliaryByte, sizeof(auxiliaryByte)) || !ReadEquipmentWireBytes(&cursor, end, &lifyType, sizeof(amplifyType)) || !ReadEquipmentWireBytes(&cursor, end, &lifyValue, sizeof(amplifyValue))) { return false; } // Current item types 0..11 read the two avatar raw blocks; type 26 // additionally reads a durability/instance override dword. if (actorSlot <= 0x0B && (!SkipEquipmentWireRawBlock(&cursor, end) || !SkipEquipmentWireRawBlock(&cursor, end))) { return false; } if (actorSlot == 26 && !SkipEquipmentWireBytes(&cursor, end, sizeof(unsigned int))) { return false; } unsigned char vectorRecordCount = 0; unsigned char dwordVectorCount = 0; unsigned char indexedRecordCount = 0; if (!ReadEquipmentWireBytes(&cursor, end, &vectorRecordCount, sizeof(vectorRecordCount)) || vectorRecordCount > 2 || !SkipEquipmentWireBytes(&cursor, end, static_cast(vectorRecordCount) * 8) || !SkipEquipmentWireBytes(&cursor, end, sizeof(unsigned int)) || !ReadEquipmentWireBytes(&cursor, end, &dwordVectorCount, sizeof(dwordVectorCount)) || dwordVectorCount > 2 || !SkipEquipmentWireBytes(&cursor, end, static_cast(dwordVectorCount) * 4) || !SkipEquipmentWireBytes(&cursor, end, sizeof(unsigned short)) || !ReadEquipmentWireBytes(&cursor, end, &indexedRecordCount, sizeof(indexedRecordCount)) || indexedRecordCount > 3 || !SkipEquipmentWireBytes(&cursor, end, static_cast(indexedRecordCount) * 3)) { return false; } if (indexedRecordCount != 0) { unsigned char activeValue = 0; unsigned char selectedIndex = 0; if (!ReadEquipmentWireBytes(&cursor, end, &activeValue, sizeof(activeValue)) || !ReadEquipmentWireBytes(&cursor, end, &selectedIndex, sizeof(selectedIndex)) || (selectedIndex != 0xFF && !SkipEquipmentWireBytes(&cursor, end, 4))) { return false; } } // state120/state160/state128/state140/state144/resource and // state168..170 are ten fixed bytes before the final raw blocks. if (!SkipEquipmentWireBytes(&cursor, end, 10) || !SkipEquipmentWireRawBlock(&cursor, end) || !SkipEquipmentWireRawBlock(&cursor, end) || !SkipEquipmentWireBytes(&cursor, end, 1) || !SkipEquipmentWireRawBlock(&cursor, end)) { return false; } DNF90EquippedItemSnapshot& item = snapshot.items[index]; item.itemID = itemID; item.qualitySeed = qualitySeed; item.durability = durability; item.amplifyValue = amplifyValue; item.actorSlot = actorSlot; item.upgradeLevel = extData & 0x1F; item.amplifyType = amplifyType; } snapshot.generation = static_cast( InterlockedIncrement(&g_equipmentSnapshotGeneration)); *output = snapshot; return true; } void CacheCurrentCharacterStatsFromClass0Packet(int packet) { if (!packet) return; __try { const unsigned char* bytes = reinterpret_cast(packet); const unsigned int opcode = *reinterpret_cast(bytes + 1); const unsigned int packetLength = *reinterpret_cast(bytes + 3); if (opcode != 2 || packetLength < 16 + 0x17) return; const unsigned char* body = bytes + 16; const unsigned int bodyLength = packetLength - 16; const unsigned char mode = body[0]; size_t statLengthOffset = 0; size_t statDataOffset = 0; size_t objectKeyOffset = 0; if (mode == 1) { statLengthOffset = 0x1B; statDataOffset = 0x1F; objectKeyOffset = 0x15; } else if (mode == 3) { statLengthOffset = 0x13; statDataOffset = 0x17; objectKeyOffset = 0x0D; } else { return; } constexpr unsigned int kCurrentActorStatWireBytes = 92; if (bodyLength < statDataOffset + kCurrentActorStatWireBytes || bodyLength < statLengthOffset + sizeof(unsigned int) || bodyLength < objectKeyOffset + sizeof(unsigned short) || *reinterpret_cast( body + statLengthOffset) != kCurrentActorStatWireBytes) { return; } const unsigned short objectKey = *reinterpret_cast(body + objectKeyOffset); if (objectKey == 0) return; // The current local actor can be installed by the context-aware town // route without populating g_currentActorObjectKey. Verify the exact // native actor pointer instead of rejecting that accepted mode1 row. bool currentActorMatch = objectKey == g_currentActorObjectKey; void* currentActor = nullptr; void* packetActor = nullptr; void* objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); if (objectManager && currentActorFn) { currentActor = currentActorFn(objectManager); } ActorByObjectKeyFn originalActorByObjectKey = reinterpret_cast(g_originalActorByObjectKey); if (originalActorByObjectKey) { packetActor = originalActorByObjectKey(objectKey); } if (!packetActor && bodyLength >= 5) { ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); if (actorByContextFn) { packetActor = actorByContextFn( objectKey, static_cast(body[4])); } } if (currentActor && packetActor == currentActor) { currentActorMatch = true; g_currentActorObjectKey = objectKey; } if (!currentActorMatch) { LONG rejection = InterlockedIncrement( &g_characterStatRejectLogCount); if (rejection <= 8) { LogLine("combat-power base snapshot ignored mode=%u key=%u " "cached_key=%u current_actor=%p packet_actor=%p", static_cast(mode), objectKey, static_cast(g_currentActorObjectKey), currentActor, packetActor); } return; } const unsigned char* stat = body + statDataOffset; DNF90CharacterStatSnapshot snapshot = {}; snapshot.size = sizeof(snapshot); snapshot.generation = static_cast( InterlockedIncrement(&g_characterStatGeneration)); snapshot.validFlags = DNF90_CHARACTER_STATS_BASE_VALID | DNF90_CHARACTER_STATS_SPEED_VALID; snapshot.hpMax = *reinterpret_cast(stat + 0); snapshot.mpMax = *reinterpret_cast(stat + 4); snapshot.strength = *reinterpret_cast(stat + 8) / 10; snapshot.vitality = *reinterpret_cast(stat + 10) / 10; snapshot.intelligence = *reinterpret_cast(stat + 12) / 10; snapshot.spirit = *reinterpret_cast(stat + 14) / 10; snapshot.moveSpeed = *reinterpret_cast(stat + 68); snapshot.attackSpeed = *reinterpret_cast(stat + 72); snapshot.castSpeed = *reinterpret_cast(stat + 74); AcquireSRWLockExclusive(&g_characterStatSnapshotLock); g_characterStatSnapshot = snapshot; ReleaseSRWLockExclusive(&g_characterStatSnapshotLock); LogLine("combat-power base snapshot generation=%u mode=%u key=%u " "hp=%u mp=%u str=%d vit=%d int=%d spi=%d", snapshot.generation, static_cast(mode), objectKey, snapshot.hpMax, snapshot.mpMax, snapshot.strength, snapshot.vitality, snapshot.intelligence, snapshot.spirit); DNF90EquipmentSnapshot equipment = {}; if (!ParseCurrentEquipmentSnapshot(body, bodyLength, statDataOffset, snapshot.generation, &equipment)) { LONG rejection = InterlockedIncrement( &g_equipmentSnapshotRejectLogCount); if (rejection <= 8) { LogLine("combat-power equipment snapshot ignored mode=%u " "key=%u body_len=%u stat_generation=%u", static_cast(mode), objectKey, bodyLength, snapshot.generation); } return; } unsigned int combatItems = 0; unsigned int upgradedItems = 0; unsigned int amplifiedItems = 0; for (unsigned int index = 0; index < equipment.itemCount; ++index) { const DNF90EquippedItemSnapshot& item = equipment.items[index]; if (!IsCombatPowerEquipmentSlot(item.actorSlot)) continue; ++combatItems; if (item.upgradeLevel != 0) ++upgradedItems; if (item.amplifyType >= 1 && item.amplifyType <= 4 && item.amplifyValue != 0) { ++amplifiedItems; } } AcquireSRWLockExclusive(&g_equipmentSnapshotLock); g_equipmentSnapshot = equipment; ReleaseSRWLockExclusive(&g_equipmentSnapshotLock); LogLine("combat-power equipment snapshot generation=%u " "stat_generation=%u mode=%u key=%u rows=%u combat_items=%u " "upgraded_items=%u amplified_items=%u", equipment.generation, equipment.sourceStatGeneration, static_cast(mode), objectKey, equipment.itemCount, combatItems, upgradedItems, amplifiedItems); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-power base snapshot exception code=0x%08X", GetExceptionCode()); } } // M1 diagnostic: dump the class0/msg2 select-init sequence (scene actor mode0, int __stdcall ProxyClass0Dispatch(int a1, int packet) { LogLine("ZZMARKER-12345"); TraceDispatchState("before", 0, packet); bool previousTraceOp24 = g_traceOp24; unsigned int previousSceneModeCalls = g_op24SceneModeCalls; unsigned int previousLoadingGateCalls = g_op24LoadingGateCalls; __try { const unsigned short opcode = packet ? *reinterpret_cast(packet + 1) : 0; g_traceOp24 = opcode == 24; if (g_traceOp24) { g_op24SceneModeCalls = 0; g_op24LoadingGateCalls = 0; } } __except (EXCEPTION_EXECUTE_HANDLER) { g_traceOp24 = false; } // 章节过场日志(只读):记录 op1378(story digest)与 op24(场景重建)的 // 到达时间与载荷,用来判断「开区任务完成当时客户端到底有没有收到 digest」, // 以及它与场景重建的先后顺序。不改行为、不改寄存器现场。 if (packet) { __try { cn90::LogClass0CutscenePacket( reinterpret_cast(packet)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("digest trace exception code=0x%08X", GetExceptionCode()); } } // The network-router hook sees the protected outer packet. At this // class0 boundary the native router has already normalized the packet // body, while the original dispatcher has not yet copied it into the // companion AIC/APC instance. Capture here so the projection is available // during the same native construction transaction. if (packet) { bool projectionCaptured = false; __try { projectionCaptured = dnf90_companion_p0::CaptureProjectionClass0Packet( a1, packet); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("companion projection class0 capture exception code=0x%08X", GetExceptionCode()); } if (projectionCaptured) { LogLine("companion projection class0 packet captured dispatcher=%d packet=%p", a1, reinterpret_cast(packet)); } else { __try { const auto* bytes = reinterpret_cast(packet); if (bytes[0] == 0 && bytes[1] == 0x41 && bytes[2] == 0x7F) { const unsigned int length = *reinterpret_cast(bytes + 3); dnf90_companion_p0::LogProjectionDecodeRejected( bytes, length, "class0-dispatch"); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("companion projection class0 rejection diagnostic exception code=0x%08X", GetExceptionCode()); } } } Class0DispatchFn original = reinterpret_cast(g_originalClass0Dispatch); unsigned seriaPacketTicket = dnf90_seria::BeginPacket(packet); int result = 0; __try { result = original ? original(a1, packet) : 0; } __finally { dnf90_seria::EndPacket(seriaPacketTicket); } g_traceOp24 = previousTraceOp24; g_op24SceneModeCalls = previousSceneModeCalls; g_op24LoadingGateCalls = previousLoadingGateCalls; // The native handler has already accepted and applied this packet. Cache // only the complete local mode1/mode3 stat block for the Lua calculator; // remote town actors and incomplete bodies are deliberately ignored. CacheCurrentCharacterStatsFromClass0Packet(packet); unsigned int opcode = 0; unsigned int packetLength = 0; unsigned int recordCount = 0; unsigned int scene = 0; unsigned int recordKind = 0; unsigned int recordObjectKey = 0; unsigned int recordOwnerContext = 0; unsigned int memberCount = 0; bool recordContainsSelfSlot = false; __try { if (packet) { const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); packetLength = *reinterpret_cast(bytes + 3); if (opcode == kPartyActorProjectionOpcode && packetLength >= 26) { recordCount = *reinterpret_cast(bytes + 16); scene = *reinterpret_cast(bytes + 18); recordObjectKey = *reinterpret_cast(bytes + 20); recordOwnerContext = bytes[23]; recordKind = bytes[25]; // The current compatibility profile's kind-0 row contains an // outer dstr at body+12, 13 fixed bytes, then the party slots. // Every slot carries the cross-channel identity block before // its three state bytes. Walk the bounded row so a native // pointer repair is allowed only when this actor's own object // key is actually present in the received party table. const size_t bodyLength = packetLength - 16; const unsigned char* body = bytes + 16; if (recordKind == 0 && bodyLength >= 33) { size_t offset = 12; unsigned int outerNameLength = *reinterpret_cast(body + offset); offset += 4; if (outerNameLength <= bodyLength - offset) { offset += outerNameLength; if (offset <= bodyLength && bodyLength - offset >= 14) { offset += 13; memberCount = body[offset++]; if (memberCount <= 8) { bool bounded = true; for (unsigned int i = 0; i < memberCount; ++i) { if (bodyLength - offset < 10) { bounded = false; break; } offset += 1; unsigned int memberObjectKey = *reinterpret_cast(body + offset); offset += 2; offset += 4; unsigned int memberNameLength = *reinterpret_cast(body + offset); offset += 4; if (memberNameLength > bodyLength - offset || bodyLength - offset - memberNameLength < 3) { bounded = false; break; } offset += memberNameLength; offset += 3; if (memberObjectKey == recordObjectKey) { recordContainsSelfSlot = true; } } if (!bounded) { memberCount = 0; recordContainsSelfSlot = false; } } } } } } } if (opcode == kPartyActorProjectionOpcode && recordCount != 0 && scene == kPartyActorProjectionScene && recordKind == 0 && InterlockedCompareExchange(&g_partyHudRefreshPending, 0, 1) == 1) { uintptr_t objectManager = *reinterpret_cast(g_dnfBase + kObjectManagerPointerRva); uintptr_t sceneActorManager = *reinterpret_cast( g_dnfBase + kSceneActorManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); ActorByObjectKeyFn actorByObjectKeyFn = reinterpret_cast( g_dnfBase + kActorByObjectKeyRva); ActorByContextFn actorByContextFn = reinterpret_cast( g_dnfBase + kActorByContextRva); ResolveActorInContextFn resolveActorInContextFn = reinterpret_cast( g_dnfBase + kResolveActorInContextRva); uintptr_t currentActor = objectManager && currentActorFn ? reinterpret_cast( currentActorFn(reinterpret_cast(objectManager))) : 0; uintptr_t localMemberActor = recordObjectKey && actorByObjectKeyFn ? reinterpret_cast( actorByObjectKeyFn(static_cast(recordObjectKey))) : 0; uintptr_t contextMemberActor = recordObjectKey && actorByContextFn ? reinterpret_cast( actorByContextFn( static_cast(recordObjectKey), static_cast(recordOwnerContext))) : 0; uintptr_t recordActor = sceneActorManager && recordObjectKey && resolveActorInContextFn ? reinterpret_cast( resolveActorInContextFn( reinterpret_cast(sceneActorManager), static_cast(recordOwnerContext), static_cast(recordObjectKey))) : 0; uintptr_t contextPartyOwnerBefore = contextMemberActor ? *reinterpret_cast( contextMemberActor + kActorPartyOwnerOffset) : 0; uintptr_t hudPartyOwnerBefore = currentActor ? *reinterpret_cast( currentActor + kActorPartyOwnerOffset) : 0; bool repairedContextPartyOwner = false; if (currentActor && localMemberActor == currentActor && contextMemberActor && recordActor && recordContainsSelfSlot && contextPartyOwnerBefore == 0) { // sub_269EE50's official invariant is memberActor+0x498 = // the outer party record resolved by sub_26A2000(ownerContext, // objectKey). With this profile's context-0 scene record and a // nonzero selected channel, the native function resolves that // member through sub_17B6AA0(objectKey, ownerContext), not // sub_2699A30. Restore that exact validated self-member target. *reinterpret_cast( contextMemberActor + kActorPartyOwnerOffset) = recordActor; repairedContextPartyOwner = true; } bool repairedHudPartyOwner = false; hudPartyOwnerBefore = currentActor ? *reinterpret_cast( currentActor + kActorPartyOwnerOffset) : 0; if (currentActor && localMemberActor == currentActor && recordActor && recordContainsSelfSlot && hudPartyOwnerBefore == 0) { // The context-0 compatibility remap keeps the visible local // actor in sub_2699A30 as well. Its HUD state-1 path reads the // same +0x498 field, so preserve that existing compatibility // binding when the exact context member is a separate object. *reinterpret_cast( currentActor + kActorPartyOwnerOffset) = recordActor; repairedHudPartyOwner = true; } uintptr_t contextPartyOwnerAfter = contextMemberActor ? *reinterpret_cast( contextMemberActor + kActorPartyOwnerOffset) : 0; uintptr_t hudPartyOwnerAfter = currentActor ? *reinterpret_cast( currentActor + kActorPartyOwnerOffset) : 0; LogLine("party-hud actor bind opcode=%u packet_len=%u " "record_key=%u owner_context=%u members=%u self_slot=%u " "current_actor=%p local_member_actor=%p " "context_member_actor=%p record_actor=%p " "context_owner_before=%p context_owner_after=%p " "hud_owner_before=%p hud_owner_after=%p " "context_repaired=%u hud_repaired=%u", opcode, packetLength, recordObjectKey, recordOwnerContext, memberCount, recordContainsSelfSlot ? 1u : 0u, reinterpret_cast(currentActor), reinterpret_cast(localMemberActor), reinterpret_cast(contextMemberActor), reinterpret_cast(recordActor), reinterpret_cast(contextPartyOwnerBefore), reinterpret_cast(contextPartyOwnerAfter), reinterpret_cast(hudPartyOwnerBefore), reinterpret_cast(hudPartyOwnerAfter), repairedContextPartyOwner ? 1u : 0u, repairedHudPartyOwner ? 1u : 0u); uintptr_t sceneRoot = *reinterpret_cast(g_dnfBase + kSceneRootPointerRva); SceneHudContextFn contextFn = reinterpret_cast( g_dnfBase + kSceneHudContextRva); PartyHudRefreshFn refreshFn = reinterpret_cast( g_dnfBase + kPartyHudRefreshRva); void* hudContext = sceneRoot && contextFn ? contextFn(reinterpret_cast(sceneRoot)) : nullptr; if (hudContext && refreshFn) { refreshFn(hudContext, 1); LONG hit = InterlockedIncrement(&g_partyHudRefreshApplyCount); LogLine("party-hud native refresh applied hit=%ld opcode=%u " "packet_len=%u records=%u scene=%u kind=%u " "source=successful-class1-op12-next-class0-op9", hit, opcode, packetLength, recordCount, scene, recordKind); } else { InterlockedExchange(&g_partyHudRefreshPending, 1); LogLine("party-hud native refresh deferred opcode=%u " "packet_len=%u scene_root=%p hud_context=%p", opcode, packetLength, reinterpret_cast(sceneRoot), hudContext); } } } __except (EXCEPTION_EXECUTE_HANDLER) { InterlockedExchange(&g_partyHudRefreshPending, 1); LogLine("party-hud native refresh exception opcode=%u packet_len=%u " "records=%u scene=%u kind=%u code=0x%08X pending_restored=1", opcode, packetLength, recordCount, scene, recordKind, GetExceptionCode()); } TraceDispatchState("after", 0, packet); return result; } void CachePartyHudRefreshFromClass1Packet(int packet) { if (!packet || !g_dnfBase) return; unsigned int opcode = 0; unsigned int packetLength = 0; unsigned int success = 0; __try { const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); if (opcode != kSetPartyInfoOpcode) return; packetLength = *reinterpret_cast(bytes + 3); if (packetLength < 17) { LogLine("party-hud refresh ACK rejected opcode=%u packet_len=%u " "reason=short-packet", opcode, packetLength); return; } success = bytes[16]; if (success != 1) { InterlockedExchange(&g_partyHudRefreshPending, 0); LogLine("party-hud refresh ACK rejected opcode=%u packet_len=%u " "success=%u reason=server-rejected-set-party-info", opcode, packetLength, success); return; } InterlockedExchange(&g_partyHudRefreshPending, 1); LogLine("party-hud refresh armed opcode=%u packet_len=%u success=%u " "target=next-class0-op9", opcode, packetLength, success); } __except (EXCEPTION_EXECUTE_HANDLER) { InterlockedExchange(&g_partyHudRefreshPending, 0); LogLine("party-hud refresh ACK cache exception opcode=%u packet_len=%u " "success=%u code=0x%08X", opcode, packetLength, success, GetExceptionCode()); } } bool ApplyPersistentAuraSkinState(const char* source) { LONG entitlement = InterlockedCompareExchange( &g_auraSkinEntitlementState, 0, 0); if (!g_dnfBase || entitlement < 0) return false; uintptr_t objectManager = 0; void* actor = nullptr; void* stateOwner = nullptr; void* stateBlock = nullptr; LONG previous = 0; LONG updated = 0; __try { objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); actor = objectManager && currentActorFn ? currentActorFn(reinterpret_cast(objectManager)) : nullptr; if (!actor) return false; stateOwner = *reinterpret_cast( reinterpret_cast(actor) + kAuraSkinActorStateOwnerOffset); if (!stateOwner) return false; uintptr_t vtable = *reinterpret_cast(stateOwner); if (!vtable) return false; AuraSkinStateResolverFn resolveState = *reinterpret_cast( vtable + kAuraSkinStateResolverVtableOffset); stateBlock = resolveState ? resolveState(stateOwner) : nullptr; if (!stateBlock) return false; volatile LONG* flags = reinterpret_cast( reinterpret_cast(stateBlock) + kAuraSkinStateFlagsOffset); previous = InterlockedCompareExchange(flags, 0, 0); for (;;) { updated = entitlement != 0 ? previous | static_cast(kAuraSkinUnlockedMask) : previous & ~static_cast(kAuraSkinUnlockedMask); LONG observed = InterlockedCompareExchange(flags, updated, previous); if (observed == previous) break; previous = observed; } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("aura-skin persistent state exception state=%ld source=%s " "actor=%p owner=%p block=%p code=0x%08X", entitlement, source ? source : "unknown", actor, stateOwner, stateBlock, GetExceptionCode()); return false; } LONG hit = InterlockedIncrement(&g_auraSkinDeferredApplyCount); LogLine("aura-skin persistent state applied hit=%ld state=%ld changed=%d " "previous=0x%08X updated=0x%08X source=%s actor=%p owner=%p block=%p", hit, entitlement, previous != updated ? 1 : 0, static_cast(previous), static_cast(updated), source ? source : "unknown", actor, stateOwner, stateBlock); return true; } bool CacheAuraSkinPersistentStateFromClass1Packet(int packet) { if (!packet || !g_dnfBase) return false; unsigned int opcode = 0; unsigned int packetLength = 0; unsigned int success = 0; __try { const unsigned char* bytes = reinterpret_cast(packet); opcode = *reinterpret_cast(bytes + 1); if (opcode != kAuraSkinSlotOpcode) return false; packetLength = *reinterpret_cast(bytes + 3); if (packetLength != kAuraSkinSilentRestorePacketLength) return false; success = bytes[16]; if (success > 1 || bytes[17] != 'A' || bytes[18] != 'U' || bytes[19] != 'R' || bytes[20] != 'A') { return false; } InterlockedExchange(&g_auraSkinEntitlementState, static_cast(success)); LONG hit = InterlockedIncrement(&g_auraSkinSilentRestoreCount); bool applied = ApplyPersistentAuraSkinState( "class1-op863-marked-state"); LogLine("aura-skin persistent state cached hit=%ld opcode=%u " "packet_len=%u state=%u applied=%d source=class1-op863-marker", hit, opcode, packetLength, success, applied ? 1 : 0); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("aura-skin persistent state cache exception opcode=%u " "packet_len=%u state=%u code=0x%08X", opcode, packetLength, success, GetExceptionCode()); return false; } } void RestorePremiumServerClockFromClass1Packet(int packet) { if (!packet || !g_dnfBase) return; __try { const unsigned char* bytes = reinterpret_cast(packet); // Reject other packets before examining any extended body bytes. if (bytes[0] != 1 || bytes[1] != 0x87 || bytes[2] != 3 || dnf90_premium_clock::ReadU32(bytes + 3) != dnf90_premium_clock::kPacketSize) return; int serverUnix = 0; if (dnf90_premium_clock::Decode(bytes, dnf90_premium_clock::kPacketSize, &serverUnix)) { bool applied = CommitNativeServerClockState(serverUnix); LogLine("premium server clock packet server_unix=%d applied=%d source=class1-op903-TIME", serverUnix, applied ? 1 : 0); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("premium server clock packet refused code=0x%08X", GetExceptionCode()); } } int __stdcall ProxyClass1Dispatch(int packet, int a2, int a3) { TraceDispatchState("before", 1, packet); if (CacheAuraSkinPersistentStateFromClass1Packet(packet)) { TraceDispatchState("after-persistent-aura-state", 1, packet); return 1; } RestorePremiumServerClockFromClass1Packet(packet); Class1DispatchFn original = reinterpret_cast(g_originalClass1Dispatch); int result = original ? original(packet, a2, a3) : 0; TraceDispatchState("after", 1, packet); if (dnf90_companion_p0::PollFixedApc5405()) { LogLine("companion p0 fixed APC submitted source=class1-retry"); } if (dnf90_companion_p0::ApplyPendingProjection()) { LogLine("companion p1 character projection applied source=class1-safe-point"); } return result; } // Current EXE op38 (VA 0x1D4E680) resolves these same native types before // killing. Query on the resident class0 dispatcher, never a worker thread. bool TryConsumeDungeonAssistPresence(int packet) { using namespace dnf90_dungeon_assist; if (!packet || !g_dnfBase) return false; unsigned char reply[kHeaderSize + 2 + kMaxKeys * 2] = {}; size_t replySize = kHeaderSize + 2; bool matched = false; bool replyReady = false; __try { const unsigned char* bytes = reinterpret_cast(packet); if (U16(bytes + 1) != kOpcode) return false; matched = true; const unsigned int length = U32(bytes + 3); if (length < 16 || length > 16 + kHeaderSize + 1 + kMaxKeys * 2 || !ValidQuery(bytes + 16, length - 16)) return true; const unsigned char* body = bytes + 16; memcpy(reply, body, kHeaderSize); replyReady = true; const unsigned char getterSig[] = {0xE9,0x4B,0xA4,0x56,0x00}; const unsigned char managerSig[] = {0x55,0x8B,0xEC,0x56,0x57,0x8B,0x7D,0x0C,0x83,0xFF,0xFF}; const unsigned char lookupSig[] = {0x55,0x8B,0xEC,0x83,0x7D,0x08,0x00,0x57,0x8B,0xF9}; const unsigned char castSig[] = {0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x85,0xF6}; if (!BytesMatch(reinterpret_cast(g_dnfBase + 0x018F2A70), getterSig, sizeof(getterSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01E645C0), managerSig, sizeof(managerSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x021AD080), lookupSig, sizeof(lookupSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x0178BE90), castSig, sizeof(castSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x0178BE50), castSig, sizeof(castSig))) { reply[kHeaderSize] = 1; } else if (U16(body + 1) != g_currentActorObjectKey || !g_luaDungeonRoomKnown || U32(body + 23) != g_luaDungeonID || U32(body + 27) != g_luaDungeonMapID || body[31] != g_luaDungeonRoomX || body[32] != g_luaDungeonRoomY) { reply[kHeaderSize] = 2; } else { typedef void* (__cdecl* SceneGetter)(); typedef void* (__thiscall* ManagerGetter)(void*, int, int); typedef void* (__thiscall* Lookup)(void*, int, int, int); typedef void* (__cdecl* Cast)(void*); void* scene = reinterpret_cast(g_dnfBase + 0x018F2A70)(); void* manager = scene ? reinterpret_cast(g_dnfBase + 0x01E645C0)(scene, 0, -1) : nullptr; if (!manager) { reply[kHeaderSize] = 2; } else { Lookup lookup = reinterpret_cast(g_dnfBase + 0x021AD080); Cast monster = reinterpret_cast(g_dnfBase + 0x0178BE90); Cast apc = reinterpret_cast(g_dnfBase + 0x0178BE50); for (size_t i = 0; i < body[kHeaderSize]; ++i) { const uint16_t key = U16(body + kHeaderSize + 1 + i * 2); if (!monster(lookup(manager, 0x211, key, 1)) && !apc(lookup(manager, 0x111, key, 1))) continue; reply[replySize++] = static_cast(key); reply[replySize++] = static_cast(key >> 8); ++reply[kHeaderSize + 1]; } if (!reply[kHeaderSize + 1] && body[33] == 1 && U32(body + 34)) { const unsigned char advanceSig[] = {0x8B,0x01,0xFF,0x81,0xC4,0x16,0x00,0x00,0x8B,0x90,0xBC,0x06,0x00,0x00,0xFF,0xE2}; const unsigned char xSig[] = {0x8B,0x89,0xAC,0x00,0x00,0x00,0x6A,0x00}; const unsigned char ySig[] = {0x8B,0x89,0xAC,0x00,0x00,0x00,0x6A,0x01}; if (!BytesMatch(reinterpret_cast(g_dnfBase + 0x024B9F00), advanceSig, sizeof(advanceSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01FB2170), xSig, sizeof(xSig)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01FB2180), ySig, sizeof(ySig))) { reply[kHeaderSize] = 1; } else { // The observed 60083 pillar is this native gate class, // at the exact PVF special-object position (594:301). // Its hit completion owner increments the count and // runs the original threshold/spawn callback. It owns // generation, animation and further Hell Party waves. typedef int (__thiscall* Position)(void*); typedef void (__thiscall* Advance)(void*); uintptr_t begin = *reinterpret_cast(static_cast(manager) + 0x0C); uintptr_t end = *reinterpret_cast(static_cast(manager) + 0x10); void* gate = nullptr; if (end < begin || (end - begin) % 4 != 0 || end - begin > 4096 * 4) { reply[kHeaderSize] = 1; } else { for (uintptr_t at = begin; at < end; at += 4) { void* object = *reinterpret_cast(at); if (!object || *reinterpret_cast(object) != g_dnfBase + 0x042E6254) continue; int x = reinterpret_cast(g_dnfBase + 0x01FB2170)(object); int y = reinterpret_cast(g_dnfBase + 0x01FB2180)(object); if (x != static_cast(U32(body + 38)) || y != static_cast(U32(body + 42))) continue; if (gate) { reply[kHeaderSize] = 1; gate = nullptr; break; } gate = object; } } if (gate && CanAdvanceGate(false, *reinterpret_cast(static_cast(gate) + 0x1781) != 0, *reinterpret_cast(static_cast(gate) + 0x16C4), *reinterpret_cast(static_cast(gate) + 0x16C8))) { reinterpret_cast(g_dnfBase + 0x024B9F00)(gate); LogLine("dungeon assist native gate advanced dungeon=%u map=%u room=%u:%u pillar=%u sequence=%u", U32(body + 23), U32(body + 27), body[31], body[32], U32(body + 34), U32(body + 19)); } } } } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("dungeon assist native presence refused code=0x%08X", GetExceptionCode()); reply[kHeaderSize] = 1; reply[kHeaderSize + 1] = 0; replySize = kHeaderSize + 2; } if (!replyReady) return matched; if (reply[kHeaderSize] || reply[kHeaderSize + 1]) { LogLine("dungeon assist native presence dungeon=%u map=%u room=%u:%u sequence=%u status=%u present=%u", U32(reply + 23), U32(reply + 27), reply[31], reply[32], U32(reply + 19), reply[kHeaderSize], reply[kHeaderSize + 1]); } __try { UpperWriterValueFn value = reinterpret_cast(g_dnfBase + kUpperWriterValueRva); UpperWriterScalarFn u16 = reinterpret_cast(g_dnfBase + kUpperWriterU16Rva); UpperWriterScalarFn u8 = reinterpret_cast(g_dnfBase + kUpperWriterU8Rva); UpperWriterFlushFn flush = reinterpret_cast(g_dnfBase + kUpperWriterFlushRva); u16(value(), kOpcode); for (size_t i = 0; i < replySize; ++i) u8(value(), reply[i]); flush(); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("dungeon assist presence reply failed code=0x%08X", GetExceptionCode()); } return matched; } bool SendCurrentPartyDirectoryRefresh() { if (!g_dnfBase) return false; __try { UpperWriterValueFn value = reinterpret_cast( g_dnfBase + kUpperWriterValueRva); UpperWriterScalarFn writeU16 = reinterpret_cast( g_dnfBase + kUpperWriterU16Rva); UpperWriterScalarFn writeU8 = reinterpret_cast( g_dnfBase + kUpperWriterU8Rva); UpperWriterFlushFn flush = reinterpret_cast( g_dnfBase + kUpperWriterFlushRva); // Exact current-EXE writer sequence at 0x032289E4: // upper_u16(98), upper_u8(0), upper_flush(). writeU16(value(), kPartyDirectoryRefreshOpcode); writeU8(value(), kPartyDirectoryRefreshModeFull); flush(); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("party-directory refresh request exception opcode=%u mode=%u code=0x%08X", kPartyDirectoryRefreshOpcode, static_cast(kPartyDirectoryRefreshModeFull), GetExceptionCode()); return false; } LogLine("party-directory refresh request sent opcode=%u mode=%u source=owner9-open", kPartyDirectoryRefreshOpcode, static_cast(kPartyDirectoryRefreshModeFull)); return true; } bool __fastcall ProxyJoustSceneBlockCheck(void* self, void* /*unused*/) { JoustSceneBlockCheckFn original = reinterpret_cast( g_originalJoustSceneBlockCheck); bool blocked = original ? original(self) : true; uintptr_t caller = reinterpret_cast(_ReturnAddress()); uintptr_t callerRva = caller >= g_dnfBase ? caller - g_dnfBase : 0; if (callerRva != kJoustOpenGateReturnRva) { return blocked; } // The owner-609 gate itself performs the remaining native checks: it // requests op1291, verifies the type-73 activity state, constructs the // native controls, and only then lets sub_2FDD1A0 promote the owner. LogLine("joust scene-state gate caller=+0x%08X native_blocked=%d override=allow owner=%u", static_cast(callerRva), blocked ? 1 : 0, static_cast(kJoustUiOwner)); return false; } void DumpPanelTree(void* panel); void* __fastcall ProxyAuraSkinSceneUiOpen( void* self, void* /*unused*/, uintptr_t uiOwner, int mode, unsigned int group) { if (uiOwner == kAvatarPanelUiOwner) { ApplyPersistentAuraSkinState("avatar-panel-before-create"); } SceneUiOpenFn original = reinterpret_cast(g_originalSceneUiOpen); uintptr_t caller = reinterpret_cast(_ReturnAddress()); void* result = original ? original(self, uiOwner, mode, group) : nullptr; if (uiOwner == kJoustUiOwner) { SceneUiIsOpenFn isOpen = g_dnfBase ? reinterpret_cast(g_dnfBase + kSceneUiIsOpenRva) : nullptr; bool active = false; __try { active = isOpen && isOpen(self, uiOwner); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("joust scene-ui state read exception code=0x%08X", GetExceptionCode()); } LogLine("joust scene-ui open caller=+0x%08X owner=%u mode=%d group=%u result=%p active=%d", caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0, static_cast(uiOwner), mode, group, result, active ? 1 : 0); } if (!result) { return result; } if (uiOwner == kPartyDirectoryUiOwner) { SendCurrentPartyDirectoryRefresh(); } if (uiOwner == kPersonalInfoUiOwner && result) { DumpPanelTree(result); } return result; } void* __fastcall ProxySceneUiOpen( void* self, void* /*unused*/, uintptr_t uiOwner, int mode, unsigned int group) { if (uiOwner == 0x298) { uintptr_t caller = reinterpret_cast(_ReturnAddress()); char preview[192] = { 0 }; unsigned int previewLength = 0; unsigned int previewError = 0; __try { const wchar_t* text = reinterpret_cast( static_cast(static_cast(mode))); wchar_t widePreview[64] = { 0 }; while (text && previewLength + 1 < _countof(widePreview) && text[previewLength] != L'\0') { widePreview[previewLength] = text[previewLength]; ++previewLength; } if (previewLength != 0) { WideCharToMultiByte( CP_UTF8, 0, widePreview, static_cast(previewLength), preview, static_cast(sizeof(preview) - 1), nullptr, nullptr); } } __except (EXCEPTION_EXECUTE_HANDLER) { previewError = GetExceptionCode(); } LogLine("scene-ui announcement open caller=+0x%08X owner=%u " "text_ptr=%p text_len=%u text=%s group=%u read_error=0x%08X", caller >= g_dnfBase ? static_cast(caller - g_dnfBase) : 0, static_cast(uiOwner), reinterpret_cast( static_cast(static_cast(mode))), previewLength, preview, group, previewError); } if (uiOwner == kAvatarPanelUiOwner) { ApplyPersistentAuraSkinState("avatar-panel-before-create-debug"); } SceneUiOpenFn original = reinterpret_cast(g_originalSceneUiOpen); void* result = original ? original(self, uiOwner, mode, group) : nullptr; return result; } int __cdecl ProxyLocalActorCreate(int objectKey) { LogLine("local-actor-create phase=before object_key=%u", static_cast(objectKey) & 0xFFFFu); LocalActorCreateFn original = reinterpret_cast(g_originalLocalActorCreate); int result = original ? original(objectKey) : 0; uintptr_t objectManager = 0; uintptr_t currentActor = 0; uintptr_t controlledActor = 0; __try { objectManager = *reinterpret_cast(g_dnfBase + kObjectManagerPointerRva); controlledActor = *reinterpret_cast(g_dnfBase + kControlledActorPointerRva); CurrentActorFn currentActorFn = reinterpret_cast(g_dnfBase + kCurrentActorRva); currentActor = objectManager && currentActorFn ? reinterpret_cast(currentActorFn(reinterpret_cast(objectManager))) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("local-actor-create state read exception object_key=%u code=0x%08X", static_cast(objectKey) & 0xFFFFu, GetExceptionCode()); } LogLine("local-actor-create phase=after object_key=%u result=%p current_actor=%p controlled_actor=%p", static_cast(objectKey) & 0xFFFFu, reinterpret_cast(result), reinterpret_cast(currentActor), reinterpret_cast(controlledActor)); return result; } bool ShouldForceMode0ProjectionCurrentScene() { return dnf90_companion_p0::ProjectionMode0OwnerContextOverride(); } void __cdecl TraceMode0OwnerCompare(unsigned int sceneChannel, unsigned int localOwnerChannel) { bool currentSceneContextRemap = sceneChannel != 0 && localOwnerChannel == 0; bool projectionOwnerOverride = dnf90_companion_p0::ProjectionMode0OwnerContextOverride(); LogLine("mode0-owner-compare scene_channel=%u local_owner_channel=%u branch=%s", sceneChannel, localOwnerChannel, projectionOwnerOverride ? "projection-current-scene" : (sceneChannel == localOwnerChannel || currentSceneContextRemap ? (currentSceneContextRemap ? "current-scene-context0-remap" : "current-scene-create") : "remote-actor")); } // The local bridge's context-0 actor rows belong to the currently selected // scene. Once the real HUD channel index is restored, translate only that // established context-0 sentinel to the current scene branch. Every nonzero // owner context retains the native CMP/JZ result. __declspec(naked) void ProxyMode0OwnerCompare() { __asm { pushfd pushad push ecx push esi call TraceMode0OwnerCompare add esp, 8 call ShouldForceMode0ProjectionCurrentScene test al, al jnz mode0_owner_saved_local popad popfd cmp esi, ecx je mode0_owner_local test ecx, ecx jnz mode0_owner_remote test esi, esi jnz mode0_owner_local mode0_owner_remote: movzx edx, al jmp dword ptr [g_mode0OwnerRemoteResume] mode0_owner_saved_local: popad popfd mode0_owner_local: jmp dword ptr [g_mode0OwnerLocalResume] } } void __cdecl TraceMode3OwnerResolve(unsigned int currentSceneChannel, unsigned int packetOwnerContext) { bool currentSceneContextRemap = currentSceneChannel != 0 && packetOwnerContext == 0; LogLine("mode3-owner-resolve scene_channel=%u packet_owner=%u branch=%s", currentSceneChannel, packetOwnerContext, currentSceneChannel == packetOwnerContext || currentSceneContextRemap ? (currentSceneContextRemap ? "current-scene-context0-remap" : "current-scene") : "remote-actor"); } // Resolve the actor with the same context-0/current-scene translation as mode // 0. The second comparison below must make the same decision. __declspec(naked) void ProxyMode3OwnerResolve() { __asm { pushfd pushad push ecx push esi call TraceMode3OwnerResolve add esp, 8 popad popfd cmp ecx, esi je mode3_owner_local test ecx, ecx jnz mode3_owner_remote test esi, esi jnz mode3_owner_local mode3_owner_remote: // Complete the displaced remote-path instruction before resuming. mov esi, dword ptr [ebp-4B0h] jmp dword ptr [g_mode3OwnerRemoteResume] mode3_owner_local: jmp dword ptr [g_mode3OwnerLocalResume] } } __declspec(naked) void ProxyMode3OwnerFinalize() { __asm { mov ecx, dword ptr [g_mode3LocalOwnerChannelAddress] movzx eax, byte ptr [ecx] test eax, eax jz mode3_finalize_local cmp eax, ebx je mode3_finalize_local jmp dword ptr [g_mode3OwnerFinalizeRemoteResume] mode3_finalize_local: jmp dword ptr [g_mode3OwnerFinalizeLocalResume] } } void* EnsureDungeonPickupContainer(void* actor, const char* source) { if (!actor || !g_dnfBase || g_pickupContainerRepairActive) return nullptr; void** containerSlot = reinterpret_cast( reinterpret_cast(actor) + kPickupContainerOffset); void* container = nullptr; unsigned int townEntryState = 0; void* controlledActor = nullptr; __try { container = *containerSlot; if (container) return container; controlledActor = *reinterpret_cast( g_dnfBase + kControlledActorPointerRva); townEntryState = *reinterpret_cast( g_dnfBase + kTownEntryStateRva); if (controlledActor != actor || townEntryState != kDungeonTownEntryState) { return nullptr; } g_pickupContainerRepairActive = true; PickupContainerAllocatorFn allocator = reinterpret_cast( g_dnfBase + kPickupContainerAllocatorRva); PickupContainerConstructorFn constructor = reinterpret_cast( g_dnfBase + kPickupContainerConstructorRva); void* memory = allocator ? allocator(kPickupContainerBytes) : nullptr; if (!memory) { g_pickupContainerRepairActive = false; LogLine("pickup-container repair failed source=%s actor=%p reason=allocation", source ? source : "unknown", actor); return nullptr; } container = constructor ? constructor(memory, 0, kPickupContainerKind) : nullptr; if (!container) { g_pickupContainerRepairActive = false; LogLine("pickup-container repair failed source=%s actor=%p reason=constructor", source ? source : "unknown", actor); return nullptr; } // The native actor initializer owns this same field and the matching // teardown paths destroy it. Pickup runs on the game thread, so install // it only after the guarded null check and let native ownership resume. *containerSlot = container; g_pickupContainerRepairActive = false; } __except (EXCEPTION_EXECUTE_HANDLER) { g_pickupContainerRepairActive = false; LogLine("pickup-container repair exception source=%s actor=%p code=0x%08X", source ? source : "unknown", actor, GetExceptionCode()); return nullptr; } LONG repair = InterlockedIncrement(&g_pickupContainerRepairCount); LogLine("pickup-container repaired source=%s actor=%p container=%p state=%u repair=%ld", source ? source : "unknown", actor, container, townEntryState, repair); return container; } void* __fastcall ProxyPickupContainerGetter(void* self, void* /*unused*/) { PickupContainerGetterFn original = reinterpret_cast(g_originalPickupContainerGetter); // sub_24A1BC0 is a general actor-container getter. It is reached while a // town actor is being installed as well as by the dungeon pickup path, and // the current EXE reports town-entry state 3 in both situations. Creating // a replacement container here splits dword_51B2768 from the UI alias at // dword_51B2790 and leaves the crystal/soul warehouse counters reading an // empty manager. Keep this proxy side-effect free; the narrowly scoped // sub_24B2840 hook below owns the missing-container repair immediately // before an actual manual pickup. return original ? original(self) : nullptr; } void* __fastcall ProxyManualPickup(void* self, void* /*unused*/, void* objects) { EnsureDungeonPickupContainer(self, "manual-pickup"); ManualPickupFn original = reinterpret_cast(g_originalManualPickup); return original ? original(self, objects) : nullptr; } // Preserve sub_1D84FE0's native stack contract. The wide-name pointer was // already placed on the stack before the equipped-creature key lookup. The // original add esp,4 removes only sub_25FD8D0's cdecl key argument; on a // successful lookup execution resumes at the native sub_34DEA20 call, while a // missing map entry skips that optional third copy and returns through the // original epilogue. The actor and live creature name copies earlier in the // same handler remain untouched. __declspec(naked) void ProxyCreatureRenameMapNullCheck() { __asm { add esp, 4 test eax, eax jz creature_rename_map_done lea ecx, [eax+14h] jmp dword ptr [g_creatureRenameMapUpdateResume] creature_rename_map_done: jmp dword ptr [g_creatureRenameMapDoneResume] } } // Entry contract at sub_1D73120+0x899 after the native same-template path has // applied the row's serial/count: // esi = existing item object // ebp-0x17D = list type // ebp-0x17C = slot // ebp-0x104 = complete dynamic state parsed from the incoming 0x77 row // Invoke only for list-7 creatures or the equipped creature at list-3/slot-26. // The native dynamic-state setter is already used later in this same handler. // The displaced movsx is restored before the original slot-refresh call, and // the existing item pointer is never destroyed or replaced. __declspec(naked) void ProxyPetItemUpdateDynamicState() { __asm { cmp byte ptr [ebp-17Dh], 7 je pet_item_update_apply cmp byte ptr [ebp-17Dh], 3 jne pet_item_update_resume cmp word ptr [ebp-17Ch], 26 jne pet_item_update_resume pet_item_update_apply: mov edx, [esi] lea eax, [ebp-104h] push eax mov ecx, esi call dword ptr [edx+158h] pet_item_update_resume: movsx eax, word ptr [ebp-17Ch] jmp dword ptr [g_petItemUpdateDynamicStateResume] } } void __cdecl TraceAutoRepairAuxContainerMissing(void* actor) { LONG hit = InterlockedIncrement(&g_autoRepairAuxNullCount); if (hit <= 8) { LogLine("auto-repair auxiliary equipment scan skipped actor=%p reason=null_container hit=%ld", actor, hit); } } // Entry contract at sub_20695F0+0x18F: // eax = sub_24A1BC0(controlled_actor) // [esp] = auxiliary slot index already pushed by the native loop // A present container follows the exact native sub_2326860 call and resumes at // 0x2069786. A missing optional container discards that pending argument and // skips only the auxiliary slots; the ordinary equipment durability pass has // already completed. __declspec(naked) void ProxyAutoRepairAuxContainerLookup() { __asm { test eax, eax jz auto_repair_aux_missing mov ecx, eax call dword ptr [g_autoRepairAuxLookupFn] jmp dword ptr [g_autoRepairAuxLookupResume] auto_repair_aux_missing: pushfd pushad push edi call TraceAutoRepairAuxContainerMissing add esp, 4 popad popfd add esp, 4 jmp dword ptr [g_autoRepairAuxLoopDone] } } int __fastcall ProxyOp24SceneMode(void* self, void* /*unused*/) { Op24SceneModeFn original = reinterpret_cast(g_originalOp24SceneMode); int result = original ? original(self) : 0; if (g_traceOp24) { if (result == 5) g_op24SceneModeEarlyReturn = true; if (g_op24SceneModeCalls++ == 0) { LogLine("op24-native-gate name=scene_mode self=%p result=%d early_return=%d", self, result, result == 5 ? 1 : 0); } } return result; } unsigned char __cdecl ProxyOp24LoadingGate() { Op24LoadingGateFn original = reinterpret_cast(g_originalOp24LoadingGate); unsigned char result = original ? original() : 0; if (g_traceOp24) { if (result != 0) g_op24LoadingGateEarlyReturn = true; if (g_op24LoadingGateCalls++ == 0) { LogLine("op24-native-gate name=loading_gate result=%u early_return=%d", static_cast(result), result ? 1 : 0); } } return result; } bool TryCopyTraceBytes(const void* source, void* destination, size_t length) { if (!source || !destination || length == 0) return false; __try { const unsigned char* from = static_cast(source); unsigned char* to = static_cast(destination); for (size_t i = 0; i < length; ++i) to[i] = from[i]; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } void FormatTraceBytes(const unsigned char* bytes, size_t length, char* output, size_t outputSize) { if (!output || outputSize == 0) return; output[0] = '\0'; if (!bytes || length == 0) return; static const char kHex[] = "0123456789ABCDEF"; size_t used = 0; for (size_t i = 0; i < length && used + 3 < outputSize; ++i) { unsigned char value = bytes[i]; output[used++] = kHex[value >> 4]; output[used++] = kHex[value & 0x0F]; if (i + 1 < length && used + 1 < outputSize) output[used++] = ' '; } output[used] = '\0'; } bool IsSocketTraceEnabled() { wchar_t path[MAX_PATH] = { 0 }; if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) return false; wchar_t* slash = wcsrchr(path, L'\\'); if (!slash) return false; slash[1] = L'\0'; wcsncat_s(path, MAX_PATH, L"90CN_socket_trace.ini", _TRUNCATE); return GetPrivateProfileIntW(L"trace", L"enabled", 0, path) != 0; } void CacheCurrentItemRow(uintptr_t itemObject, const unsigned char* raw) { if (!itemObject || !raw) return; LONG sequence = InterlockedIncrement(&g_socketTraceRowSequence); LONG slot = sequence % kSocketTraceCacheRows; if (slot < 0) slot += kSocketTraceCacheRows; AcquireSRWLockExclusive(&g_socketTraceLock); g_socketTraceRows[slot].sequence = sequence; g_socketTraceRows[slot].parserObject = itemObject; memcpy(g_socketTraceRows[slot].raw, raw, kCurrentItemRawBytes); ReleaseSRWLockExclusive(&g_socketTraceLock); } uint16_t CurrentItemRowSlot(const unsigned char* raw) { if (!raw) return 0; return static_cast(raw[0]) | (static_cast(raw[1]) << 8); } uint32_t CurrentItemRowTemplateID(const unsigned char* raw) { if (!raw) return 0; return static_cast(raw[2]) | (static_cast(raw[3]) << 8) | (static_cast(raw[4]) << 16) | (static_cast(raw[5]) << 24); } uint32_t CurrentItemRowIdentity(const unsigned char* raw) { if (!raw) return 0; return static_cast(raw[kCurrentItemIdentityOffset]) | (static_cast(raw[kCurrentItemIdentityOffset + 1]) << 8) | (static_cast(raw[kCurrentItemIdentityOffset + 2]) << 16) | (static_cast(raw[kCurrentItemIdentityOffset + 3]) << 24); } bool IsCurrentPremiumContractID(uint32_t templateID) { // Generated from the pinned runtime Script.pvf // etc/premiumlist_new.etc [item] rows. Keeping the allow-list in this // current-client compatibility unit prevents an all-stackable bypass. static const uint32_t kContractIDs[] = { 30, 31, 32, 33, 34, 43, 44, 45, 46, 193, 194, 196, 198, 720, 741, 742, 743, 744, 820, 901, 917, 920, 2660012, 2660013, 2660050, 2660051, 2660290, 2660354, 2660358, 2660409, 2660410, 2660411, 2660543, 2660545, 2660703, 2660704, 2660705, 2682763, 2683452, 2683453, 2683454, 2683455, 2683456, 2683457, 2683458, 2683459, 2749321, 10000388, 10000389, 10000390, 10000391, 10012576, 10012577, 10012578, 10012579, 10092497, 10096109, 10096110, 10096111, 10096112, 10096113, 10151653, 10157955, 490700021, 490700022, 490700023, 490700028, 490700029, 490700030, 490700031, 490700032, 490700033, 490700034, 490700035, 490700036 }; for (size_t i = 0; i < sizeof(kContractIDs) / sizeof(kContractIDs[0]); ++i) { if (kContractIDs[i] == templateID) return true; } return false; } // The current TP reset books can return from native use without a request. // Route that silent case through the verified op30 slot writer. The server // resolves the PVF item, owns the TP-only reset and consumes it atomically. bool IsCurrentTPResetBookID(uint32_t templateID) { return templateID == 1206 || templateID == 1253; } // Quest-completion tickets use a PVF [action type] the retail item class has // no inventory dispatch for, so right-click eligibility fails before any op44 // writer runs. The server already owns the whole [lv clear]/[gm quest clear] // contract (PVF resolution, atomic quest+inventory settlement), so the same // verified op44 writer only needs the item admitted here. bool IsCurrentQuestCompletionTicketID(uint32_t templateID) { switch (templateID) { case 10301410: // Lv1~90主线/成就任务完成券, [lv clear] 90 case 10303915: // 1次觉醒和自觉任务完成券 case 10303916: // 2次觉醒和自觉任务完成券 case 490001725: // 辅助装备任务完成券 case 490001726: // 魔法石任务完成券 case 490008275: // 耳环任务完成券 case 490003186: // 外传任务完成券 return true; default: return false; } } // The class-change ticket (stackable/10303001/10303920.stk) keeps the native // "转职状态" gate: the player accepts one [job change quest] 1 chain at the // trainer first, then the ticket completes that chosen branch. It is not in the // fallback list. bool IsCurrentClassChangeTicketID(uint32_t templateID) { // stackable/10303001/10303920.stk: 转职达成券. The native gate rejects use // even while a class-change quest is accepted, so admit it to the verified // op44 fallback; the server enforces the 外传 exclusion and picks the // branch. return templateID == 10303920; } bool IsCurrentInventoryUseFallbackID(uint32_t templateID) { return IsCurrentPremiumContractID(templateID) || IsCurrentTPResetBookID(templateID) || IsCurrentQuestCompletionTicketID(templateID) || IsCurrentClassChangeTicketID(templateID); } struct CurrentInventoryUseSelection { int slot; uint32_t templateID; uint32_t identity; }; bool ResolveCurrentInventoryUseSelection(CurrentInventoryUseSelection* output) { if (!output || !g_dnfBase) return false; output->slot = -1; output->templateID = 0; output->identity = 0; struct NativeIntVector { int* begin; int* end; int* capacity; } selected = {}; bool constructed = false; void* manager = nullptr; int slot = -1; __try { manager = *reinterpret_cast( g_dnfBase + kInventorySelectionManagerRva); if (!manager) return false; InventorySelectionCtorFn ctor = reinterpret_cast( g_dnfBase + kInventorySelectionCtorRva); InventorySelectionCollectFn collect = reinterpret_cast( g_dnfBase + kInventorySelectionCollectRva); InventorySelectionDtorFn dtor = reinterpret_cast( g_dnfBase + kInventorySelectionDtorRva); ctor(&selected); constructed = true; collect(manager, &selected, kMainInventorySelectionGroup); if (selected.begin && selected.end && selected.end > selected.begin) { slot = *selected.begin; } dtor(&selected); constructed = false; } __except (EXCEPTION_EXECUTE_HANDLER) { if (constructed) { __try { InventorySelectionDtorFn dtor = reinterpret_cast( g_dnfBase + kInventorySelectionDtorRva); dtor(&selected); } __except (EXCEPTION_EXECUTE_HANDLER) { } } LogLine("contract-use selection read exception code=0x%08X", GetExceptionCode()); return false; } uint32_t templateID = 0; uint32_t identity = 0; __try { InventorySelectionLookupFn lookup = reinterpret_cast( g_dnfBase + kInventorySelectionLookupRva); void* selectedItem = lookup(manager, slot); if (!selectedItem) return false; void** vtable = *reinterpret_cast(selectedItem); if (!vtable) return false; CurrentItemDataFn itemDataFn = reinterpret_cast(vtable[0x28 / sizeof(void*)]); if (!itemDataFn) return false; void* itemData = itemDataFn(selectedItem); if (!itemData) return false; // This is the exact current-EXE read sequence used immediately before // both native op44 writers. Reading the selected wrapper avoids // confusing same-numbered slots from the equipment, material, avatar, // pet, and consumable lists in the passive parser cache. CurrentItemTemplateReadFn readTemplate = reinterpret_cast( g_dnfBase + kCurrentItemTemplateReadRva); templateID = readTemplate( reinterpret_cast(itemData) + 0x14); if (!IsCurrentInventoryUseFallbackID(templateID)) return false; CurrentItemIdentityReadFn readIdentity = reinterpret_cast( g_dnfBase + kCurrentItemIdentityReadRva); identity = readIdentity(templateID); if (identity == 0 && !IsCurrentTPResetBookID(templateID)) return false; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("contract-use selected-item read exception slot=%d code=0x%08X", slot, GetExceptionCode()); return false; } output->slot = slot; output->templateID = templateID; output->identity = identity; return true; } // sub_2331CE0 writes its own item and slot arguments into op44 and obtains // the same instance identity through sub_2064A20. Resolve the fallback from // those native arguments rather than the generic selection manager: the // active-coupon panel does not need to keep the latter's UI selection alive. bool ResolveCurrentInventoryUsePanelSelection( uint32_t templateID, int slot, CurrentInventoryUseSelection* output) { if (!output) return false; output->slot = -1; output->templateID = 0; output->identity = 0; if (!g_dnfBase || slot < 0 || slot > 0x7FFF || !IsCurrentInventoryUseFallbackID(templateID)) { return false; } uint32_t identity = 0; __try { CurrentItemIdentityReadFn readIdentity = reinterpret_cast( g_dnfBase + kCurrentItemIdentityReadRva); identity = readIdentity(templateID); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("contract-use panel item-identity read exception slot=%d item=%lu code=0x%08X", slot, static_cast(templateID), GetExceptionCode()); return false; } if (identity == 0 && !IsCurrentTPResetBookID(templateID)) return false; output->slot = slot; output->templateID = templateID; output->identity = identity; return true; } bool SendCurrentInventoryUseFallback(const CurrentInventoryUseSelection& selection) { if (selection.slot < 0 || selection.slot > 0x7FFF || !IsCurrentInventoryUseFallbackID(selection.templateID) || (selection.identity == 0 && !IsCurrentTPResetBookID(selection.templateID)) || !g_dnfBase) { return false; } __try { UpperWriterValueFn value = reinterpret_cast( g_dnfBase + kUpperWriterValueRva); UpperWriterScalarFn writeU16 = reinterpret_cast( g_dnfBase + kUpperWriterU16Rva); UpperWriterScalarFn writeU8 = reinterpret_cast( g_dnfBase + kUpperWriterU8Rva); UpperWriterScalarFn writeI16 = reinterpret_cast( g_dnfBase + kUpperWriterI16Rva); UpperWriterScalarFn writeU32 = reinterpret_cast( g_dnfBase + kUpperWriterU32Rva); UpperWriterFlushFn flush = reinterpret_cast( g_dnfBase + kUpperWriterFlushRva); if (IsCurrentTPResetBookID(selection.templateID)) { // A mouse-down snapshot can outlive an inventory update. Confirm // the same book still occupies this MAIN-bag slot before sending. void* manager = *reinterpret_cast(g_dnfBase + kInventorySelectionManagerRva); if (!manager) return false; InventorySelectionLookupFn lookup = reinterpret_cast( g_dnfBase + kInventorySelectionLookupRva); void* item = lookup(manager, selection.slot); if (!item) return false; void** table = *reinterpret_cast(item); if (!table || !table[0x28 / sizeof(void*)]) return false; void* data = reinterpret_cast(table[0x28 / sizeof(void*)])(item); if (!data) return false; CurrentItemTemplateReadFn read = reinterpret_cast( g_dnfBase + kCurrentItemTemplateReadRva); if (read(reinterpret_cast(data) + 0x14) != selection.templateID) return false; writeU16(value(), 30); writeI16(value(), static_cast(selection.slot)); flush(); LogLine("tp-reset-book sent opcode=30 slot=%d item=%lu", selection.slot, static_cast(selection.templateID)); return true; } writeU16(value(), kUseStackableOpcode); writeI16(value(), static_cast(selection.slot)); writeU8(value(), kMainInventoryListType); writeU32(value(), selection.identity); writeU32(value(), selection.templateID); writeU32(value(), 0); flush(); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("inventory-use fallback send exception slot=%d item=%lu identity=%lu code=0x%08X", selection.slot, static_cast(selection.templateID), static_cast(selection.identity), GetExceptionCode()); return false; } LogLine("contract-use fallback sent opcode=44 slot=%d list=0 item=%lu identity=%lu", selection.slot, static_cast(selection.templateID), static_cast(selection.identity)); return true; } LONG FindCurrentItemRowsByTemplate(uint32_t templateID, CurrentItemRowTrace* output, LONG outputCapacity) { if (templateID == 0 || !output || outputCapacity <= 0) return 0; LONG found = 0; AcquireSRWLockShared(&g_socketTraceLock); for (LONG i = 0; i < kSocketTraceCacheRows; ++i) { const CurrentItemRowTrace& row = g_socketTraceRows[i]; if (row.sequence == 0 || CurrentItemRowTemplateID(row.raw) != templateID) continue; memcpy(&output[found], &row, sizeof(row)); ++found; if (found == outputCapacity) break; } ReleaseSRWLockShared(&g_socketTraceLock); return found; } bool ResolveHoveredInventoryUseSelection(CurrentInventoryUseSelection* output) { if (!output || !g_dnfBase) return false; output->slot = -1; output->templateID = 0; output->identity = 0; void* hovered = nullptr; void* hoveredItemData = nullptr; uint32_t templateID = 0; __try { hovered = *reinterpret_cast( g_dnfBase + kHoveredInventoryItemRva); if (!hovered) return false; void** vtable = *reinterpret_cast(hovered); if (!vtable) return false; CurrentItemDataFn itemDataFn = reinterpret_cast( vtable[0x28 / sizeof(void*)]); if (!itemDataFn) return false; void* itemData = itemDataFn(hovered); if (!itemData) return false; hoveredItemData = itemData; CurrentItemTemplateReadFn readTemplate = reinterpret_cast( g_dnfBase + kCurrentItemTemplateReadRva); templateID = readTemplate( reinterpret_cast(itemData) + 0x14); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("contract-use hovered-item read exception code=0x%08X", GetExceptionCode()); return false; } if (!IsCurrentInventoryUseFallbackID(templateID)) return false; // The native selection group is authoritative when the inventory UI has // populated it. Some current item classes reject right-click dispatch // before that group is committed, so the passive op14 row cache provides // the exact slot as the bounded fallback. CurrentInventoryUseSelection selected = {}; if (ResolveCurrentInventoryUseSelection(&selected) && selected.templateID == templateID) { *output = selected; return true; } // The passive row parser also observes warehouse and other container // rows, which can carry the same template and slot number as list 0. // Resolve the hovered template through the current main-inventory manager // instead. Prefer the exact wrapper/item-data object under the mouse; a // single same-template main-bag row is the bounded fallback. int exactSlot = -1; int uniqueSlot = -1; int nativeMatchCount = 0; __try { void* manager = *reinterpret_cast( g_dnfBase + kInventorySelectionManagerRva); if (!manager) return false; InventorySelectionLookupFn lookup = reinterpret_cast( g_dnfBase + kInventorySelectionLookupRva); CurrentItemTemplateReadFn readTemplate = reinterpret_cast( g_dnfBase + kCurrentItemTemplateReadRva); for (int slot = kCurrentMainInventoryFirstItemSlot; slot <= kCurrentMainInventoryLastItemSlot; ++slot) { void* candidate = lookup(manager, slot); if (!candidate) continue; void** vtable = *reinterpret_cast(candidate); if (!vtable) continue; CurrentItemDataFn itemDataFn = reinterpret_cast( vtable[0x28 / sizeof(void*)]); if (!itemDataFn) continue; void* candidateItemData = itemDataFn(candidate); if (!candidateItemData) continue; uint32_t candidateTemplateID = readTemplate( reinterpret_cast(candidateItemData) + 0x14); if (candidateTemplateID != templateID) continue; ++nativeMatchCount; uniqueSlot = slot; if (candidate == hovered || candidateItemData == hoveredItemData) { exactSlot = slot; break; } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("contract-use main-inventory scan exception item=%lu code=0x%08X", static_cast(templateID), GetExceptionCode()); return false; } int slot = exactSlot >= 0 ? exactSlot : (nativeMatchCount == 1 ? uniqueSlot : -1); if (slot < 0) { LogLine("contract-use main-inventory resolve rejected item=%lu native_matches=%d exact_slot=%d", static_cast(templateID), nativeMatchCount, exactSlot); return false; } uint32_t identity = 0; __try { CurrentItemIdentityReadFn readIdentity = reinterpret_cast( g_dnfBase + kCurrentItemIdentityReadRva); identity = readIdentity(templateID); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("contract-use hovered identity read exception item=%lu code=0x%08X", static_cast(templateID), GetExceptionCode()); return false; } if (identity == 0 && !IsCurrentTPResetBookID(templateID)) return false; output->slot = slot; output->templateID = templateID; output->identity = identity; LogLine("contract-use main-inventory resolved slot=%d item=%lu native_matches=%d exact_hover=%d", slot, static_cast(templateID), nativeMatchCount, exactSlot >= 0 ? 1 : 0); return true; } bool RejectLuaClientNotice(const char* reason) { LONG hit = InterlockedIncrement(&g_luaNoticeRejectedCount); if (hit <= 8) { LogLine("lua client notice rejected hit=%ld reason=%s", hit, reason); } return false; } const unsigned char* LoadEmbeddedPixelResource( int resourceID, size_t expectedBytes) { HMODULE module = GetModuleHandleW(L"90CN.dll"); if (!module) return nullptr; HRSRC resource = FindResourceW( module, MAKEINTRESOURCEW(resourceID), RT_RCDATA); if (!resource || SizeofResource(module, resource) != expectedBytes) { return nullptr; } HGLOBAL loaded = LoadResource(module, resource); return loaded ? reinterpret_cast(LockResource(loaded)) : nullptr; } void BlendPremultipliedPixels( unsigned char* destination, int destinationWidth, int destinationHeight, int destinationX, int destinationY, const unsigned char* source, int sourceWidth, int sourceHeight, int copyWidth, int copyHeight) { if (!destination || !source || destinationWidth <= 0 || destinationHeight <= 0 || sourceWidth <= 0 || sourceHeight <= 0 || copyWidth <= 0 || copyHeight <= 0 || destinationX < 0 || destinationY < 0 || destinationX + copyWidth > destinationWidth || destinationY + copyHeight > destinationHeight || copyWidth > sourceWidth || copyHeight > sourceHeight) { return; } for (int y = 0; y < copyHeight; ++y) { unsigned char* destinationRow = destination + ((destinationY + y) * destinationWidth + destinationX) * 4; const unsigned char* sourceRow = source + y * sourceWidth * 4; for (int x = 0; x < copyWidth; ++x) { unsigned char* destinationPixel = destinationRow + x * 4; const unsigned char* sourcePixel = sourceRow + x * 4; const unsigned int sourceAlpha = sourcePixel[3]; const unsigned int inverseAlpha = 255 - sourceAlpha; for (int channel = 0; channel < 3; ++channel) { unsigned int value = sourcePixel[channel] + (destinationPixel[channel] * inverseAlpha + 127) / 255; destinationPixel[channel] = static_cast( value > 255 ? 255 : value); } unsigned int alpha = sourceAlpha + (destinationPixel[3] * inverseAlpha + 127) / 255; destinationPixel[3] = static_cast( alpha > 255 ? 255 : alpha); } } } LRESULT CALLBACK LuaNoticeOverlayWindowProc( HWND window, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_ERASEBKGND: return 1; case WM_NCHITTEST: return HTTRANSPARENT; case WM_MOUSEACTIVATE: return MA_NOACTIVATE; case WM_TIMER: if (wParam == kLuaNoticeOverlayTimer) { KillTimer(window, kLuaNoticeOverlayTimer); ShowWindow(window, SW_HIDE); return 0; } break; case WM_PAINT: { PAINTSTRUCT paint = {}; HDC dc = BeginPaint(window, &paint); RECT client = {}; GetClientRect(window, &client); const COLORREF colorKey = RGB(1, 1, 1); HBRUSH clearBrush = CreateSolidBrush(colorKey); FillRect(dc, &client, clearBrush); DeleteObject(clearBrush); HBRUSH panelBrush = CreateSolidBrush(RGB(24, 28, 36)); HPEN borderPen = CreatePen(PS_SOLID, 2, RGB(230, 184, 74)); HGDIOBJ oldBrush = SelectObject(dc, panelBrush); HGDIOBJ oldPen = SelectObject(dc, borderPen); RoundRect(dc, client.left + 2, client.top + 2, client.right - 2, client.bottom - 2, 18, 18); SelectObject(dc, oldPen); SelectObject(dc, oldBrush); DeleteObject(borderPen); DeleteObject(panelBrush); HFONT font = CreateFontW(-24, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei"); bool ownsFont = font != nullptr; if (!font) { font = reinterpret_cast(GetStockObject(DEFAULT_GUI_FONT)); } HGDIOBJ oldFont = SelectObject(dc, font); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(255, 248, 224)); RECT textRect = client; textRect.left += 18; textRect.right -= 18; DrawTextW(dc, g_luaNoticeOverlayText, -1, &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX); SelectObject(dc, oldFont); if (ownsFont) DeleteObject(font); EndPaint(window, &paint); return 0; } case WM_NCDESTROY: if (g_luaNoticeOverlayWindow == window) { g_luaNoticeOverlayWindow = nullptr; } break; default: break; } return DefWindowProcW(window, message, wParam, lParam); } bool EnsureLuaNoticeOverlayWindow(HWND parent) { if (g_luaNoticeOverlayWindow && IsWindow(g_luaNoticeOverlayWindow)) { return true; } HINSTANCE module = reinterpret_cast( GetModuleHandleW(L"90CN.dll")); WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW; windowClass.lpfnWndProc = &LuaNoticeOverlayWindowProc; windowClass.hInstance = module; windowClass.lpszClassName = kLuaNoticeOverlayClassName; if (!RegisterClassExW(&windowClass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { LogLine("lua client notice overlay class registration failed error=%lu", GetLastError()); return false; } HWND overlay = CreateWindowExW( WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW, kLuaNoticeOverlayClassName, L"", WS_POPUP, 0, 0, 0, 0, parent, nullptr, module, nullptr); if (!overlay) { LogLine("lua client notice overlay creation failed error=%lu", GetLastError()); return false; } if (!SetLayeredWindowAttributes( overlay, RGB(1, 1, 1), 238, LWA_COLORKEY | LWA_ALPHA)) { LogLine("lua client notice overlay alpha setup failed error=%lu", GetLastError()); DestroyWindow(overlay); return false; } g_luaNoticeOverlayWindow = overlay; return true; } bool ShowLuaClientNoticeOverlay( HWND parent, const LuaClientNotice& notice) { if (!EnsureLuaNoticeOverlayWindow(parent)) return false; memcpy_s(g_luaNoticeOverlayText, sizeof(g_luaNoticeOverlayText), notice.text, (notice.length + 1) * sizeof(wchar_t)); RECT parentClient = {}; if (!GetClientRect(parent, &parentClient)) return false; int parentWidth = parentClient.right - parentClient.left; int parentHeight = parentClient.bottom - parentClient.top; int width = parentWidth - 40; if (width > 900) width = 900; if (width < 280) width = 280; int height = 58; int x = (parentWidth - width) / 2; if (x < 0) x = 0; int y = parentHeight >= 400 ? 70 : 20; POINT screenPosition = { x, y }; if (!ClientToScreen(parent, &screenPosition)) return false; InvalidateRect(g_luaNoticeOverlayWindow, nullptr, TRUE); SetWindowPos(g_luaNoticeOverlayWindow, parent, screenPosition.x, screenPosition.y, width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW); UpdateWindow(g_luaNoticeOverlayWindow); SetTimer(g_luaNoticeOverlayWindow, kLuaNoticeOverlayTimer, kLuaNoticeOverlayDurationMs, nullptr); return true; } const wchar_t* CombatPowerRankName(unsigned int score) { if (score < 3100) return L"探索"; if (score < 8100) return L"开拓"; if (score < 23000) return L"无畏"; if (score < 52000) return L"征服"; if (score < 65000) return L"战绝"; if (score < 130000) return L"英杰"; return L"武炼"; } bool TryComputeCombatPowerEquipmentBonusHundredths( const DNF90CombatPanelState& state, unsigned int* output) { if (!output || state.baseAttributeScore == 0 || (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) == 0 || (state.validFlags & DNF90_COMBAT_PANEL_EQUIPMENT_SCORE_VALID) == 0 || (state.validFlags & DNF90_COMBAT_PANEL_DAMAGE_AFFIXES_VALID) == 0) { return false; } // Lua's equipment score contains both the three visible damage-affix // multiplier score and the remaining equipment-derived score. Remove the // white/yellow/critical part first so the "equipment" percentage is an // independent row and never counts the displayed affixes twice. const unsigned int yellowTenths = state.yellowDamageTenths + state.yellowAdditionalTenths; const unsigned int criticalTenths = state.criticalDamageTenths + state.criticalAdditionalTenths; const long double multiplier = (1.0L + static_cast(state.whiteDamageTenths) / 1000.0L) * (1.0L + static_cast(yellowTenths) / 1000.0L) * (1.0L + static_cast(criticalTenths) / 1000.0L); const long double affixValue = static_cast(state.baseAttributeScore) * (multiplier - 1.0L); unsigned int affixScore = 0; if (affixValue >= static_cast(state.equipmentScore)) { affixScore = state.equipmentScore; } else if (affixValue > 0.0L) { affixScore = static_cast(affixValue); } const unsigned int equipmentOnlyScore = state.equipmentScore - affixScore; const unsigned long long scaled = static_cast(equipmentOnlyScore) * 10000ull; const unsigned long long hundredths = scaled / state.baseAttributeScore; if (hundredths > 999999ull) return false; *output = static_cast(hundredths); return true; } int CombatPowerRankIconResource(unsigned int score) { if (score < 3100) return IDR_COMBAT_RANK_EXPLORE_V3; if (score < 8100) return IDR_COMBAT_RANK_PIONEER_V3; if (score < 23000) return IDR_COMBAT_RANK_FEARLESS_V3; if (score < 52000) return IDR_COMBAT_RANK_CONQUER_V3; if (score < 65000) return IDR_COMBAT_RANK_BATTLE_PINNACLE_V3; if (score < 130000) return IDR_COMBAT_RANK_HEROIC_V3; return IDR_COMBAT_RANK_MARTIAL_MASTERY_V3; } HFONT CreateCombatPanelFont(int pixelHeight, int weight) { HFONT font = CreateFontW(-pixelHeight, 0, 0, 0, weight, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Microsoft YaHei"); return font ? font : reinterpret_cast( GetStockObject(DEFAULT_GUI_FONT)); } void DrawCombatPanelText(HDC dc, const wchar_t* text, RECT bounds, int pixelHeight, int weight, COLORREF color, UINT format) { HFONT font = CreateCombatPanelFont(pixelHeight, weight); HGDIOBJ oldFont = SelectObject(dc, font); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, color); DrawTextW(dc, text, -1, &bounds, format | DT_NOPREFIX); SelectObject(dc, oldFont); if (font != GetStockObject(DEFAULT_GUI_FONT)) DeleteObject(font); } void DrawCombatPanelTextShadowed(HDC dc, const wchar_t* text, RECT bounds, int pixelHeight, int weight, COLORREF color, UINT format) { RECT shadow = bounds; OffsetRect(&shadow, 1, 1); DrawCombatPanelText(dc, text, shadow, pixelHeight, weight, RGB(0, 0, 0), format); DrawCombatPanelText(dc, text, bounds, pixelHeight, weight, color, format); } bool DrawEmbeddedBgraResource(HDC dc, int resourceID, int width, int height) { const size_t expectedBytes = static_cast(width) * height * 4; const unsigned char* pixels = LoadEmbeddedPixelResource( resourceID, expectedBytes); if (!pixels) return false; BITMAPINFO bitmapInfo = {}; bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader); bitmapInfo.bmiHeader.biWidth = width; bitmapInfo.bmiHeader.biHeight = -height; bitmapInfo.bmiHeader.biPlanes = 1; bitmapInfo.bmiHeader.biBitCount = 32; bitmapInfo.bmiHeader.biCompression = BI_RGB; return StretchDIBits(dc, 0, 0, width, height, 0, 0, width, height, pixels, &bitmapInfo, DIB_RGB_COLORS, SRCCOPY) != GDI_ERROR; } bool DrawCombatPanelSkinWithRankIcon(HDC dc, unsigned int score) { const size_t panelBytes = static_cast(kCombatPanelWidth) * kCombatPanelHeight * 4; const size_t iconBytes = static_cast(kCombatRankIconSize) * kCombatRankIconSize * 4; const unsigned char* panel = LoadEmbeddedPixelResource( IDR_COMBAT_POWER_PANEL_SKIN_V2, panelBytes); const unsigned char* icon = LoadEmbeddedPixelResource( CombatPowerRankIconResource(score), iconBytes); if (!panel || !icon) return false; unsigned char* composed = static_cast( HeapAlloc(GetProcessHeap(), 0, panelBytes)); if (!composed) return false; memcpy(composed, panel, panelBytes); BlendPremultipliedPixels( composed, kCombatPanelWidth, kCombatPanelHeight, kCombatRankIconX, kCombatRankIconY, icon, kCombatRankIconSize, kCombatRankIconSize, kCombatRankIconSize, kCombatRankIconSize); BITMAPINFO bitmapInfo = {}; bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader); bitmapInfo.bmiHeader.biWidth = kCombatPanelWidth; bitmapInfo.bmiHeader.biHeight = -kCombatPanelHeight; bitmapInfo.bmiHeader.biPlanes = 1; bitmapInfo.bmiHeader.biBitCount = 32; bitmapInfo.bmiHeader.biCompression = BI_RGB; const bool drawn = StretchDIBits(dc, 0, 0, kCombatPanelWidth, kCombatPanelHeight, 0, 0, kCombatPanelWidth, kCombatPanelHeight, composed, &bitmapInfo, DIB_RGB_COLORS, SRCCOPY) != GDI_ERROR; HeapFree(GetProcessHeap(), 0, composed); return drawn; } void DrawCombatPanelProgressBar(HDC dc, int left, int top, int width, unsigned int numerator, unsigned int denominator, COLORREF fillColor) { if (width <= 2) return; HBRUSH track = CreateSolidBrush(RGB(3, 11, 19)); RECT trackRect = { left, top, left + width, top + 6 }; FillRect(dc, &trackRect, track); DeleteObject(track); HPEN border = CreatePen(PS_SOLID, 1, RGB(47, 84, 105)); HGDIOBJ oldPen = SelectObject(dc, border); HGDIOBJ oldBrush = SelectObject(dc, GetStockObject(NULL_BRUSH)); Rectangle(dc, trackRect.left, trackRect.top, trackRect.right, trackRect.bottom); SelectObject(dc, oldBrush); SelectObject(dc, oldPen); DeleteObject(border); if (denominator == 0 || numerator == 0) return; unsigned int fill = static_cast( (static_cast(width - 2) * numerator) / denominator); if (fill > static_cast(width - 2)) fill = width - 2; HBRUSH value = CreateSolidBrush(fillColor); RECT valueRect = { left + 1, top + 1, left + 1 + static_cast(fill), top + 5 }; FillRect(dc, &valueRect, value); DeleteObject(value); } #if 0 void DrawCombatPanelMainLegacy(HDC dc, const DNF90CombatPanelState& state) { const bool baseValid = (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) != 0; const bool affixesValid = (state.validFlags & DNF90_COMBAT_PANEL_DAMAGE_AFFIXES_VALID) != 0; const bool skinDrawn = baseValid ? DrawCombatPanelSkinWithRankIcon(dc, state.totalScore) : DrawEmbeddedBgraResource(dc, IDR_COMBAT_POWER_PANEL_SKIN_V2, kCombatPanelWidth, kCombatPanelHeight); if (!skinDrawn) { RECT fallback = { 0, 0, kCombatPanelWidth, kCombatPanelHeight }; HBRUSH background = CreateSolidBrush(RGB(5, 17, 28)); FillRect(dc, &fallback, background); DeleteObject(background); } wchar_t line[64] = {}; RECT title = { 7, 5, 113, 28 }; DrawCombatPanelTextShadowed(dc, L"我的战斗力值", title, 13, FW_BOLD, RGB(225, 242, 251), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT help = { 105, 25, 122, 42 }; DrawCombatPanelTextShadowed(dc, L"?", help, 11, FW_BOLD, RGB(245, 202, 91), DT_CENTER | DT_VCENTER | DT_SINGLELINE); if (!baseValid) { RECT medalText = { 37, 43, 91, 101 }; DrawCombatPanelTextShadowed(dc, L"战", medalText, 27, FW_BOLD, RGB(232, 247, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } if (baseValid) { swprintf_s(line, L"%u", state.totalScore); } else { wcscpy_s(line, L"--"); } RECT total = { 8, 116, 120, 143 }; DrawCombatPanelTextShadowed(dc, line, total, 19, FW_BOLD, RGB(255, 225, 101), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const wchar_t* rank = baseValid ? CombatPowerRankName(state.totalScore) : L"--"; // The selected TGP badge has a dedicated empty nameplate below the // emblem. Keep the rank inside that plate instead of spending a separate // row below the total score. RECT rankRect = { 34, 96, 94, 114 }; DrawCombatPanelTextShadowed(dc, rank, rankRect, 9, FW_BOLD, RGB(245, 226, 171), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT baseLabel = { 9, 158, 119, 176 }; DrawCombatPanelTextShadowed(dc, L"基础属性加成", baseLabel, 10, FW_BOLD, RGB(184, 224, 242), DT_CENTER | DT_VCENTER | DT_SINGLELINE); if (baseValid) swprintf_s(line, L"%u", state.baseAttributeScore); else wcscpy_s(line, L"--"); RECT baseScore = { 9, 174, 119, 195 }; DrawCombatPanelTextShadowed(dc, line, baseScore, 12, FW_BOLD, RGB(103, 218, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT equipmentLabel = { 9, 198, 119, 225 }; DrawCombatPanelTextShadowed(dc, L"装备加成", equipmentLabel, 10, FW_BOLD, RGB(184, 224, 242), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT affix = { 12, 227, 116, 252 }; if (affixesValid) { swprintf_s(line, L"白字 %u.%u0%%", state.whiteDamageTenths / 10, state.whiteDamageTenths % 10); DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(225, 236, 242), DT_CENTER | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"黄字 %u.%u0%%", state.yellowDamageTenths / 10, state.yellowDamageTenths % 10); affix = { 12, 252, 116, 277 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(255, 216, 95), DT_CENTER | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"爆伤 %u.%u0%%", state.criticalDamageTenths / 10, state.criticalDamageTenths % 10); affix = { 12, 277, 116, 302 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(255, 174, 104), DT_CENTER | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"黄追 %u.%u0%%", state.yellowAdditionalTenths / 10, state.yellowAdditionalTenths % 10); affix = { 12, 302, 116, 327 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(249, 232, 145), DT_CENTER | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"爆追 %u.%u0%%", state.criticalAdditionalTenths / 10, state.criticalAdditionalTenths % 10); affix = { 12, 327, 116, 352 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(246, 190, 132), DT_CENTER | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"全攻 %u.%u0%%", state.allAttackTenths / 10, state.allAttackTenths % 10); affix = { 12, 352, 116, 377 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(161, 216, 241), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else { const wchar_t* pendingRows[] = { L"白字 --", L"黄字 --", L"爆伤 --", L"黄追 --", L"爆追 --", L"全攻 --", }; for (size_t index = 0; index < sizeof(pendingRows) / sizeof(pendingRows[0]); ++index) { affix.top = 227 + static_cast(index) * 25; affix.bottom = affix.top + 25; DrawCombatPanelTextShadowed(dc, pendingRows[index], affix, 10, FW_NORMAL, RGB(143, 193, 215), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } } } #endif void DrawCombatPanelMain(HDC dc, const DNF90CombatPanelState& state) { const bool baseValid = (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) != 0; const bool affixesValid = (state.validFlags & DNF90_COMBAT_PANEL_DAMAGE_AFFIXES_VALID) != 0; const bool skinDrawn = baseValid ? DrawCombatPanelSkinWithRankIcon(dc, state.totalScore) : DrawEmbeddedBgraResource(dc, IDR_COMBAT_POWER_PANEL_SKIN_V2, kCombatPanelWidth, kCombatPanelHeight); if (!skinDrawn) { RECT fallback = { 0, 0, kCombatPanelWidth, kCombatPanelHeight }; HBRUSH background = CreateSolidBrush(RGB(5, 17, 28)); FillRect(dc, &fallback, background); DeleteObject(background); } // 标题、问号、分区标题和金属格子来自原版位图;这里只绘制实时数据。 wchar_t line[64] = {}; if (baseValid) swprintf_s(line, L"%u", state.totalScore); else wcscpy_s(line, L"--"); RECT total = { 5, 128, 113, 153 }; DrawCombatPanelTextShadowed(dc, line, total, 18, FW_BOLD, RGB(255, 225, 101), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const wchar_t* rank = baseValid ? CombatPowerRankName(state.totalScore) : L"--"; HBRUSH rankPlate = CreateSolidBrush(RGB(78, 52, 25)); RECT rankPlateRect = { 35, 102, 83, 117 }; FillRect(dc, &rankPlateRect, rankPlate); DeleteObject(rankPlate); // The copper nameplate is visually one pixel right of the panel's // mathematical center. Center against the plate itself and use the next // font size so two-character ranks fill it like the original TGP panel. RECT rankRect = { 32, 100, 88, 120 }; DrawCombatPanelTextShadowed(dc, rank, rankRect, 10, FW_BOLD, RGB(245, 226, 171), DT_CENTER | DT_VCENTER | DT_SINGLELINE); wchar_t profession[32] = L"--"; const bool identityValid = (state.validFlags & DNF90_COMBAT_PANEL_IDENTITY_VALID) != 0; if (identityValid && state.professionUtf8[0] != '\0') { wchar_t converted[32] = {}; const int convertedCount = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, state.professionUtf8, -1, converted, static_cast(_countof(converted))); if (convertedCount > 0) wcscpy_s(profession, converted); } if (identityValid) swprintf_s(line, L"%s Lv.%u", profession, state.level); else wcscpy_s(line, L"-- Lv.--"); RECT identity = { 5, 153, 113, 177 }; DrawCombatPanelTextShadowed(dc, line, identity, 10, FW_NORMAL, RGB(220, 229, 232), DT_CENTER | DT_VCENTER | DT_SINGLELINE); if (baseValid) swprintf_s(line, L"%u", state.baseAttributeScore); else wcscpy_s(line, L"--"); RECT baseScore = { 5, 224, 113, 251 }; DrawCombatPanelTextShadowed(dc, line, baseScore, 12, FW_BOLD, RGB(103, 218, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT affix = { 7, 274, 111, 302 }; if (affixesValid) { swprintf_s(line, L"白字 %u.%u0%%", state.whiteDamageTenths / 10, state.whiteDamageTenths % 10); DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(225, 236, 242), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const unsigned int yellowTenths = state.yellowDamageTenths + state.yellowAdditionalTenths; swprintf_s(line, L"黄字 %u.%u0%%", yellowTenths / 10, yellowTenths % 10); affix = { 7, 302, 111, 330 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(255, 216, 95), DT_CENTER | DT_VCENTER | DT_SINGLELINE); unsigned int equipmentBonusHundredths = 0; affix = { 7, 330, 111, 358 }; if (TryComputeCombatPowerEquipmentBonusHundredths( state, &equipmentBonusHundredths)) { swprintf_s(line, L"装备 %u.%02u%%", equipmentBonusHundredths / 100, equipmentBonusHundredths % 100); } else { wcscpy_s(line, L"装备 --"); } DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(255, 174, 104), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const unsigned int criticalTenths = state.criticalDamageTenths + state.criticalAdditionalTenths; swprintf_s(line, L"爆伤 %u.%u0%%", criticalTenths / 10, criticalTenths % 10); affix = { 7, 358, 111, 386 }; DrawCombatPanelTextShadowed(dc, line, affix, 10, FW_NORMAL, RGB(139, 218, 244), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } else { const wchar_t* pendingRows[] = { L"白字 --", L"黄字 --", L"装备 --", L"爆伤 --", }; for (size_t index = 0; index < sizeof(pendingRows) / sizeof(pendingRows[0]); ++index) { affix.top = 274 + static_cast(index) * 28; affix.bottom = affix.top + 28; DrawCombatPanelTextShadowed(dc, pendingRows[index], affix, 10, FW_NORMAL, RGB(143, 193, 215), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } } } void DrawCombatPanelRankTooltip(HDC dc, const DNF90CombatPanelState& state) { if (!DrawEmbeddedBgraResource(dc, IDR_COMBAT_POWER_RANK_TOOLTIP_V2, kCombatPanelRankTooltipWidth, kCombatPanelRankTooltipHeight)) { RECT fallback = { 0, 0, kCombatPanelRankTooltipWidth, kCombatPanelRankTooltipHeight }; HBRUSH background = CreateSolidBrush(RGB(5, 17, 28)); FillRect(dc, &fallback, background); DeleteObject(background); } RECT rangeHeader = { 19, 17, 112, 43 }; RECT rankHeader = { 116, 17, 212, 43 }; DrawCombatPanelTextShadowed(dc, L"战斗力区间", rangeHeader, 12, FW_BOLD, RGB(211, 229, 239), DT_CENTER | DT_VCENTER | DT_SINGLELINE); DrawCombatPanelTextShadowed(dc, L"段位等级", rankHeader, 12, FW_BOLD, RGB(211, 229, 239), DT_CENTER | DT_VCENTER | DT_SINGLELINE); struct RankRow { unsigned int lower; unsigned int upper; const wchar_t* range; const wchar_t* name; }; const RankRow rows[] = { { 0, 3099, L"0 - 3100", L"探索" }, { 3100, 8099, L"3100 - 8100", L"开拓" }, { 8100, 22999, L"8100 - 23000", L"无畏" }, { 23000, 51999, L"23000 - 52000", L"征服" }, { 52000, 64999, L"52000 - 65000", L"战绝" }, { 65000, 129999, L"65000 - 130000", L"英杰" }, { 130000, 0xFFFFFFFFu, L"130000 以上", L"武炼" }, }; for (size_t index = 0; index < sizeof(rows) / sizeof(rows[0]); ++index) { const int top = 43 + static_cast(index) * 29; const int bottom = top + 29; if ((state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) != 0 && state.totalScore >= rows[index].lower && state.totalScore <= rows[index].upper) { HPEN highlight = CreatePen(PS_SOLID, 1, RGB(95, 200, 240)); HGDIOBJ oldPen = SelectObject(dc, highlight); HGDIOBJ oldBrush = SelectObject(dc, GetStockObject(NULL_BRUSH)); Rectangle(dc, 17, top + 1, 213, bottom); SelectObject(dc, oldBrush); SelectObject(dc, oldPen); DeleteObject(highlight); } RECT range = { 20, top, 112, bottom }; RECT name = { 116, top, 211, bottom }; DrawCombatPanelTextShadowed(dc, rows[index].range, range, 11, FW_NORMAL, RGB(190, 211, 224), DT_CENTER | DT_VCENTER | DT_SINGLELINE); DrawCombatPanelTextShadowed(dc, rows[index].name, name, 11, FW_BOLD, RGB(236, 218, 170), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } } void DrawCombatPanelGuide(HDC dc, const DNF90CombatPanelState& state) { RECT client = { 0, 0, kCombatPanelGuideWidth, kCombatPanelGuideHeight }; HBRUSH background = CreateSolidBrush(RGB(5, 16, 27)); FillRect(dc, &client, background); DeleteObject(background); HBRUSH inner = CreateSolidBrush(RGB(8, 29, 45)); HPEN outerPen = CreatePen(PS_SOLID, 1, RGB(123, 177, 202)); HGDIOBJ oldBrush = SelectObject(dc, inner); HGDIOBJ oldPen = SelectObject(dc, outerPen); Rectangle(dc, 1, 1, client.right - 1, client.bottom - 1); SelectObject(dc, oldPen); SelectObject(dc, oldBrush); DeleteObject(outerPen); DeleteObject(inner); HBRUSH titleBrush = CreateSolidBrush(RGB(12, 55, 80)); RECT titleBackground = { 4, 4, client.right - 4, 32 }; FillRect(dc, &titleBackground, titleBrush); DeleteObject(titleBrush); RECT title = { 8, 4, client.right - 8, 32 }; DrawCombatPanelTextShadowed(dc, L"战力提升攻略", title, 14, FW_BOLD, RGB(229, 242, 247), DT_CENTER | DT_VCENTER | DT_SINGLELINE); wchar_t summary[96] = {}; const bool baseValid = (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) != 0; unsigned int equipmentBonusHundredths = 0; const bool equipmentBonusValid = TryComputeCombatPowerEquipmentBonusHundredths( state, &equipmentBonusHundredths); if (baseValid && equipmentBonusValid) { swprintf_s(summary, L"当前:%u(%s) 装备:%u.%02u%%", state.totalScore, CombatPowerRankName(state.totalScore), equipmentBonusHundredths / 100, equipmentBonusHundredths % 100); } else if (baseValid) { swprintf_s(summary, L"当前:%u(%s)", state.totalScore, CombatPowerRankName(state.totalScore)); } else { wcscpy_s(summary, L"正在读取当前角色战力..."); } RECT summaryRect = { 12, 38, client.right - 12, 64 }; DrawCombatPanelTextShadowed(dc, summary, summaryRect, 11, FW_BOLD, RGB(255, 219, 105), DT_CENTER | DT_VCENTER | DT_SINGLELINE); HPEN separator = CreatePen(PS_SOLID, 1, RGB(38, 85, 110)); oldPen = SelectObject(dc, separator); MoveToEx(dc, 10, 67, nullptr); LineTo(dc, client.right - 10, 67); SelectObject(dc, oldPen); DeleteObject(separator); const wchar_t* guideLines[] = { L"1. 优先补齐全部装备、称号、时装与光环", L"2. 宠物和所有宠物装备都会计入评分", L"3. 强化、增幅、套装和高品级装备可提分", L"4. 白字可叠加;普通黄字、爆伤只取最高", L"5. 黄追、爆追可叠加;所有攻击归入三攻", }; for (size_t index = 0; index < sizeof(guideLines) / sizeof(guideLines[0]); ++index) { RECT line = { 15, 73 + static_cast(index) * 28, client.right - 12, 99 + static_cast(index) * 28 }; DrawCombatPanelTextShadowed(dc, guideLines[index], line, 11, FW_NORMAL, RGB(205, 226, 236), DT_LEFT | DT_VCENTER | DT_SINGLELINE); } RECT footer = { 10, client.bottom - 27, client.right - 10, client.bottom - 7 }; DrawCombatPanelTextShadowed(dc, L"再次点击“战力提升”关闭", footer, 10, FW_NORMAL, RGB(105, 172, 202), DT_CENTER | DT_VCENTER | DT_SINGLELINE); } DNF90CombatPanelState CopyCombatPanelState() { DNF90CombatPanelState state = {}; AcquireSRWLockShared(&g_combatPanelStateLock); state = g_combatPanelState; ReleaseSRWLockShared(&g_combatPanelStateLock); return state; } void SetCombatPanelGuideVisible(HWND parent, bool visible); void SetCombatPanelRankTooltipVisible(HWND parent, bool visible); bool IsCombatPanelUpgradeButtonPoint(POINT point) { RECT button = { kCombatPanelUpgradeButtonLeft, kCombatPanelUpgradeButtonTop, kCombatPanelUpgradeButtonRight, kCombatPanelUpgradeButtonBottom, }; return PtInRect(&button, point) != FALSE; } LRESULT CALLBACK CombatPanelOverlayWindowProc( HWND window, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_ERASEBKGND: return 1; case WM_NCHITTEST: { if (window == g_combatPanelOverlayWindow) { POINT point = { static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam)), }; if (ScreenToClient(window, &point) && IsCombatPanelUpgradeButtonPoint(point)) { return HTCLIENT; } } return HTTRANSPARENT; } case WM_MOUSEACTIVATE: return MA_NOACTIVATE; case WM_SETCURSOR: if (window == g_combatPanelOverlayWindow) { POINT point = {}; if (GetCursorPos(&point) && ScreenToClient(window, &point) && IsCombatPanelUpgradeButtonPoint(point)) { SetCursor(LoadCursorW(nullptr, IDC_HAND)); return TRUE; } } break; case WM_LBUTTONUP: if (window == g_combatPanelOverlayWindow) { POINT point = { static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam)), }; if (IsCombatPanelUpgradeButtonPoint(point)) { SetCombatPanelGuideVisible( g_dnfWindow, !g_combatPanelGuideVisible); return 0; } } break; case WM_PAINT: { PAINTSTRUCT paint = {}; HDC dc = BeginPaint(window, &paint); RECT client = {}; GetClientRect(window, &client); DNF90CombatPanelState state = CopyCombatPanelState(); if (window == g_combatPanelRankTooltipWindow) { DrawCombatPanelRankTooltip(dc, state); } else if (window == g_combatPanelGuideWindow) { DrawCombatPanelGuide(dc, state); } else { DrawCombatPanelMain(dc, state); } EndPaint(window, &paint); return 0; #if 0 // Preserved only as the V1 fallback layout reference. HBRUSH background = CreateSolidBrush(RGB(7, 18, 29)); FillRect(dc, &client, background); DeleteObject(background); HBRUSH inner = CreateSolidBrush(RGB(11, 32, 49)); HPEN outerPen = CreatePen(PS_SOLID, 2, RGB(48, 154, 204)); HGDIOBJ oldBrush = SelectObject(dc, inner); HGDIOBJ oldPen = SelectObject(dc, outerPen); RoundRect(dc, 1, 1, client.right - 1, client.bottom - 1, 8, 8); SelectObject(dc, oldPen); SelectObject(dc, oldBrush); DeleteObject(outerPen); DeleteObject(inner); HBRUSH titleBrush = CreateSolidBrush(RGB(13, 48, 72)); RECT titleBackground = { 3, 3, client.right - 3, 29 }; FillRect(dc, &titleBackground, titleBrush); DeleteObject(titleBrush); RECT title = { 7, 4, client.right - 7, 28 }; DrawCombatPanelText(dc, L"我的战斗力", title, 14, FW_BOLD, RGB(224, 245, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); // The first implementation deliberately uses an owned GDI emblem. // It is independent from PVF resources and therefore cannot break the // native personal panel when a custom texture is absent. HBRUSH medalOuter = CreateSolidBrush(RGB(181, 119, 46)); HPEN medalPen = CreatePen(PS_SOLID, 2, RGB(248, 207, 112)); oldBrush = SelectObject(dc, medalOuter); oldPen = SelectObject(dc, medalPen); Ellipse(dc, 34, 36, 98, 100); SelectObject(dc, oldPen); SelectObject(dc, oldBrush); DeleteObject(medalPen); DeleteObject(medalOuter); HBRUSH medalInner = CreateSolidBrush(RGB(41, 126, 183)); oldBrush = SelectObject(dc, medalInner); Ellipse(dc, 43, 45, 89, 91); SelectObject(dc, oldBrush); DeleteObject(medalInner); RECT medalText = { 43, 45, 89, 91 }; DrawCombatPanelText(dc, L"战", medalText, 24, FW_BOLD, RGB(238, 251, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const bool baseValid = (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) != 0; wchar_t line[64] = {}; if (baseValid) { swprintf_s(line, L"%u", state.totalScore); } else { wcscpy_s(line, L"读取中..."); } RECT total = { 5, 103, client.right - 5, 132 }; DrawCombatPanelText(dc, line, total, 24, FW_BOLD, RGB(255, 224, 116), DT_CENTER | DT_VCENTER | DT_SINGLELINE); const wchar_t* rank = baseValid ? CombatPowerRankName(state.totalScore) : L"等待角色属性"; RECT rankRect = { 5, 132, client.right - 5, 153 }; DrawCombatPanelText(dc, rank, rankRect, 13, FW_NORMAL, RGB(178, 216, 236), DT_CENTER | DT_VCENTER | DT_SINGLELINE); HPEN separator = CreatePen(PS_SOLID, 1, RGB(30, 84, 116)); oldPen = SelectObject(dc, separator); MoveToEx(dc, 7, 158, nullptr); LineTo(dc, client.right - 7, 158); SelectObject(dc, oldPen); DeleteObject(separator); RECT baseLabel = { 8, 164, client.right - 8, 184 }; DrawCombatPanelText(dc, L"基础属性分", baseLabel, 13, FW_BOLD, RGB(166, 222, 246), DT_LEFT | DT_VCENTER | DT_SINGLELINE); if (baseValid) { swprintf_s(line, L"%u", state.baseAttributeScore); } else { wcscpy_s(line, L"--"); } RECT baseScore = { 8, 184, client.right - 8, 207 }; DrawCombatPanelText(dc, line, baseScore, 17, FW_BOLD, RGB(112, 221, 255), DT_CENTER | DT_VCENTER | DT_SINGLELINE); RECT equipmentLabel = { 8, 211, client.right - 8, 231 }; DrawCombatPanelText(dc, L"装备加成", equipmentLabel, 13, FW_BOLD, RGB(166, 222, 246), DT_LEFT | DT_VCENTER | DT_SINGLELINE); if ((state.validFlags & DNF90_COMBAT_PANEL_EQUIPMENT_SCORE_VALID) != 0) { swprintf_s(line, L"%u", state.equipmentScore); } else { wcscpy_s(line, L"待接入"); } RECT equipmentScore = { 8, 231, client.right - 8, 253 }; DrawCombatPanelText(dc, line, equipmentScore, 14, FW_BOLD, RGB(244, 190, 93), DT_CENTER | DT_VCENTER | DT_SINGLELINE); if (baseValid) { swprintf_s(line, L"力量 %d", state.strength); RECT statLine = { 10, 260, client.right - 8, 279 }; DrawCombatPanelText(dc, line, statLine, 12, FW_NORMAL, RGB(202, 226, 238), DT_LEFT | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"智力 %d", state.intelligence); statLine.top += 20; statLine.bottom += 20; DrawCombatPanelText(dc, line, statLine, 12, FW_NORMAL, RGB(202, 226, 238), DT_LEFT | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"体力 %d", state.vitality); statLine.top += 20; statLine.bottom += 20; DrawCombatPanelText(dc, line, statLine, 12, FW_NORMAL, RGB(202, 226, 238), DT_LEFT | DT_VCENTER | DT_SINGLELINE); swprintf_s(line, L"精神 %d", state.spirit); statLine.top += 20; statLine.bottom += 20; DrawCombatPanelText(dc, line, statLine, 12, FW_NORMAL, RGB(202, 226, 238), DT_LEFT | DT_VCENTER | DT_SINGLELINE); } swprintf_s(line, L"自定义公式 V%u", state.formulaVersion); RECT formula = { 6, client.bottom - 24, client.right - 6, client.bottom - 6 }; DrawCombatPanelText(dc, line, formula, 10, FW_NORMAL, RGB(91, 145, 170), DT_CENTER | DT_VCENTER | DT_SINGLELINE); EndPaint(window, &paint); return 0; #endif } case WM_NCDESTROY: if (g_combatPanelOverlayWindow == window) { g_combatPanelOverlayWindow = nullptr; } if (g_combatPanelRankTooltipWindow == window) { g_combatPanelRankTooltipWindow = nullptr; g_combatPanelRankTooltipVisible = false; } if (g_combatPanelGuideWindow == window) { g_combatPanelGuideWindow = nullptr; g_combatPanelGuideVisible = false; } break; default: break; } return DefWindowProcW(window, message, wParam, lParam); } bool EnsureCombatPanelOverlayWindow(HWND parent) { if (g_combatPanelOverlayWindow && IsWindow(g_combatPanelOverlayWindow) && g_combatPanelRankTooltipWindow && IsWindow(g_combatPanelRankTooltipWindow) && g_combatPanelGuideWindow && IsWindow(g_combatPanelGuideWindow)) { return true; } HINSTANCE module = reinterpret_cast( GetModuleHandleW(L"90CN.dll")); WNDCLASSEXW windowClass = {}; windowClass.cbSize = sizeof(windowClass); windowClass.style = CS_HREDRAW | CS_VREDRAW; windowClass.lpfnWndProc = &CombatPanelOverlayWindowProc; windowClass.hInstance = module; windowClass.lpszClassName = kCombatPanelOverlayClassName; if (!RegisterClassExW(&windowClass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { LogLine("combat-panel overlay class registration failed error=%lu", GetLastError()); return false; } const DWORD interactiveStyle = WS_EX_LAYERED | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; const DWORD passiveStyle = interactiveStyle | WS_EX_TRANSPARENT; if (!g_combatPanelOverlayWindow || !IsWindow(g_combatPanelOverlayWindow)) { HWND overlay = CreateWindowExW( interactiveStyle, kCombatPanelOverlayClassName, L"", WS_POPUP, 0, 0, 0, 0, parent, nullptr, module, nullptr); if (!overlay) { LogLine("combat-panel overlay creation failed error=%lu", GetLastError()); return false; } if (!SetLayeredWindowAttributes(overlay, 0, 250, LWA_ALPHA)) { LogLine("combat-panel overlay alpha setup failed error=%lu", GetLastError()); DestroyWindow(overlay); return false; } g_combatPanelOverlayWindow = overlay; } if (!g_combatPanelRankTooltipWindow || !IsWindow(g_combatPanelRankTooltipWindow)) { HWND tooltip = CreateWindowExW( passiveStyle, kCombatPanelOverlayClassName, L"", WS_POPUP, 0, 0, 0, 0, parent, nullptr, module, nullptr); if (!tooltip) { LogLine("combat-panel rank tooltip creation failed error=%lu", GetLastError()); return false; } if (!SetLayeredWindowAttributes(tooltip, 0, 252, LWA_ALPHA)) { LogLine("combat-panel rank tooltip alpha setup failed error=%lu", GetLastError()); DestroyWindow(tooltip); return false; } g_combatPanelRankTooltipWindow = tooltip; } if (!g_combatPanelGuideWindow || !IsWindow(g_combatPanelGuideWindow)) { HWND guide = CreateWindowExW( passiveStyle, kCombatPanelOverlayClassName, L"", WS_POPUP, 0, 0, 0, 0, parent, nullptr, module, nullptr); if (!guide) { LogLine("combat-panel guide creation failed error=%lu", GetLastError()); return false; } if (!SetLayeredWindowAttributes(guide, 0, 252, LWA_ALPHA)) { LogLine("combat-panel guide alpha setup failed error=%lu", GetLastError()); DestroyWindow(guide); return false; } g_combatPanelGuideWindow = guide; } return true; } bool IsNativePersonalInfoPanelOpen() { if (!g_dnfBase) return false; __try { void* sceneRoot = *reinterpret_cast( g_dnfBase + kSceneRootPointerRva); SceneUiIsOpenFn isOpen = reinterpret_cast( g_dnfBase + kSceneUiIsOpenRva); // Opening equipment while personal information is still registered // hides the latter natively. Suppress the sidecar for that overlap so // it never floats above the inventory window. return sceneRoot && isOpen && isOpen(sceneRoot, kPersonalInfoUiOwner) && !isOpen(sceneRoot, kAvatarPanelUiOwner); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-panel native personal visibility exception code=0x%08X", GetExceptionCode()); return false; } } bool TryGetNativePersonalPanelRect( int parentWidth, int parentHeight, int* panelX, int* panelY, int* panelWidth, int* panelHeight) { if (!g_dnfBase || !panelX || !panelY || !panelWidth || !panelHeight) { return false; } __try { uintptr_t sceneRoot = *reinterpret_cast( g_dnfBase + kSceneRootPointerRva); if (!sceneRoot) return false; uintptr_t vector = sceneRoot + kSceneUiOwnerVectorOffset + kPersonalInfoUiOwner * kSceneUiOwnerVectorStride; uintptr_t begin = *reinterpret_cast( vector + kSceneUiOwnerVectorBeginOffset); uintptr_t end = *reinterpret_cast( vector + kSceneUiOwnerVectorEndOffset); if (!begin || end <= begin || end - begin > 64 || ((end - begin) % sizeof(uintptr_t)) != 0) { return false; } uintptr_t panel = *reinterpret_cast(begin); uintptr_t widget = panel ? *reinterpret_cast( panel + kPersonalPanelRootWidgetOffset) : 0; if (!widget) return false; int x = *reinterpret_cast( widget + kPersonalPanelWidgetXOffset); int y = *reinterpret_cast( widget + kPersonalPanelWidgetYOffset); int width = *reinterpret_cast( widget + kPersonalPanelWidgetWidthOffset); int height = *reinterpret_cast( widget + kPersonalPanelWidgetHeightOffset); if (x < -64 || y < -64 || width < 160 || height < 200 || width > 640 || height > 760 || x > parentWidth || y > parentHeight) { return false; } *panelX = x; *panelY = y; *panelWidth = width; *panelHeight = height; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool TryGetNativeCombatPanelPosition( int parentWidth, int parentHeight, dnf90_combat_panel::Position* position) { if (!g_dnfBase || !position) return false; __try { // Guard the function whose global-owner reads and RET 16 were verified // against the current EXE. No detour or client state is installed here. static const unsigned char expected[] = { 0x55, 0x8b, 0xec, 0xf3, 0x0f, 0x10, 0x45, 0x10, 0xf2, 0x0f, 0x10, 0x0d, 0x00, 0xc7, 0x1d, 0x04, 0xf2, 0x0f, 0x10, 0x15, 0xf8, 0xc6, 0x1d, 0x04 }; const unsigned char* entry = reinterpret_cast( g_dnfBase + kNativeUiTransformRva); if (!BytesMatch(entry, expected, sizeof(expected))) return false; void* transformOwner = *reinterpret_cast( g_dnfBase + kNativeUiTransformPointerRva); const int viewportWidth = *reinterpret_cast( g_dnfBase + kNativeUiViewportWidthRva); const int viewportHeight = *reinterpret_cast( g_dnfBase + kNativeUiViewportHeightRva); int nativeX = 0, nativeY = 0, nativeWidth = 0, nativeHeight = 0; if (!TryGetNativePersonalPanelRect(viewportWidth, viewportHeight, &nativeX, &nativeY, &nativeWidth, &nativeHeight)) return false; return dnf90_combat_panel::PositionSidecar( nativeX, nativeY, nativeWidth, nativeHeight, viewportWidth, viewportHeight, parentWidth, parentHeight, kCombatPanelWidth, kCombatPanelHeight, kPersonalPanelFrameRight, kPersonalPanelFrameTop, transformOwner, reinterpret_cast(entry), position); } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool PositionCombatPanelOverlay(HWND parent) { if (!g_combatPanelOverlayWindow) return false; RECT parentClient = {}; if (!GetClientRect(parent, &parentClient)) return false; const int parentWidth = parentClient.right - parentClient.left; const int parentHeight = parentClient.bottom - parentClient.top; // Follow the native widget through its live pivot/scale transform and the // render-viewport to Win32 client mapping on every existing timer tick. // Keep the bounded fallback if the native owner or layout is unavailable. int x = parentWidth / 2 + 40; int y = parentHeight / 2 - 110; dnf90_combat_panel::Position position = {}; if (TryGetNativeCombatPanelPosition(parentWidth, parentHeight, &position)) { x = position.x; y = position.y; } if (x + kCombatPanelWidth > parentWidth - 4) { x = parentWidth - kCombatPanelWidth - 4; } if (x < 4) x = 4; if (y + kCombatPanelHeight > parentHeight - 4) { y = parentHeight - kCombatPanelHeight - 4; } if (y < 4) y = 4; POINT screenPosition = { x, y }; if (!ClientToScreen(parent, &screenPosition)) return false; // hWndInsertAfter=parent places this popup behind the DirectX owner. Keep // it at the top of the active process's non-topmost band instead. return SetWindowPos(g_combatPanelOverlayWindow, HWND_TOP, screenPosition.x, screenPosition.y, kCombatPanelWidth, kCombatPanelHeight, SWP_NOACTIVATE | SWP_SHOWWINDOW) != FALSE; } void SetCombatPanelGuideVisible(HWND parent, bool visible) { if (!visible || !parent || !g_combatPanelOverlayWindow || !g_combatPanelGuideWindow) { if (g_combatPanelGuideWindow) { ShowWindow(g_combatPanelGuideWindow, SW_HIDE); } g_combatPanelGuideVisible = false; return; } RECT panel = {}; RECT parentRect = {}; if (!GetWindowRect(g_combatPanelOverlayWindow, &panel) || !GetWindowRect(parent, &parentRect)) { return; } int x = panel.right + 4; int y = panel.top + kCombatPanelUpgradeButtonTop - 4; if (x + kCombatPanelGuideWidth > parentRect.right - 4) { x = panel.left - kCombatPanelGuideWidth - 4; } if (x < parentRect.left + 4) x = parentRect.left + 4; if (y + kCombatPanelGuideHeight > parentRect.bottom - 4) { y = parentRect.bottom - kCombatPanelGuideHeight - 4; } if (y < parentRect.top + 4) y = parentRect.top + 4; SetCombatPanelRankTooltipVisible(parent, false); const bool wasVisible = IsWindowVisible(g_combatPanelGuideWindow) != FALSE; SetWindowPos(g_combatPanelGuideWindow, HWND_TOP, x, y, kCombatPanelGuideWidth, kCombatPanelGuideHeight, SWP_NOACTIVATE | SWP_SHOWWINDOW); if (!wasVisible) { InvalidateRect(g_combatPanelGuideWindow, nullptr, FALSE); UpdateWindow(g_combatPanelGuideWindow); } g_combatPanelGuideVisible = true; } void SetCombatPanelRankTooltipVisible(HWND parent, bool visible) { if (!visible || !parent || !g_combatPanelOverlayWindow || !g_combatPanelRankTooltipWindow) { if (g_combatPanelRankTooltipWindow) { ShowWindow(g_combatPanelRankTooltipWindow, SW_HIDE); } g_combatPanelRankTooltipVisible = false; return; } RECT panel = {}; RECT parentRect = {}; if (!GetWindowRect(g_combatPanelOverlayWindow, &panel) || !GetWindowRect(parent, &parentRect)) { return; } int x = panel.right + 4; int y = panel.top + 34; if (x + kCombatPanelRankTooltipWidth > parentRect.right - 4) { x = panel.left - kCombatPanelRankTooltipWidth - 4; } if (x < parentRect.left + 4) x = parentRect.left + 4; if (y + kCombatPanelRankTooltipHeight > parentRect.bottom - 4) { y = parentRect.bottom - kCombatPanelRankTooltipHeight - 4; } if (y < parentRect.top + 4) y = parentRect.top + 4; const bool wasVisible = IsWindowVisible(g_combatPanelRankTooltipWindow) != FALSE; SetWindowPos(g_combatPanelRankTooltipWindow, HWND_TOP, x, y, kCombatPanelRankTooltipWidth, kCombatPanelRankTooltipHeight, SWP_NOACTIVATE | SWP_SHOWWINDOW); if (!wasVisible) { InvalidateRect(g_combatPanelRankTooltipWindow, nullptr, FALSE); UpdateWindow(g_combatPanelRankTooltipWindow); } g_combatPanelRankTooltipVisible = true; } void RefreshCombatPanelRankTooltip(HWND parent) { if (g_combatPanelGuideVisible || !g_combatPanelOverlayWindow || !IsWindowVisible(g_combatPanelOverlayWindow)) { SetCombatPanelRankTooltipVisible(parent, false); return; } RECT hover = {}; POINT cursor = {}; if (!GetWindowRect(g_combatPanelOverlayWindow, &hover) || !GetCursorPos(&cursor)) { SetCombatPanelRankTooltipVisible(parent, false); return; } hover.top += kCombatPanelRankHoverTop; hover.bottom = hover.top + (kCombatPanelRankHoverBottom - kCombatPanelRankHoverTop); SetCombatPanelRankTooltipVisible(parent, PtInRect(&hover, cursor) != FALSE); } void RefreshCombatPanelOverlay(HWND parent) { if (!parent || IsIconic(parent) || GetForegroundWindow() != parent) { if (g_combatPanelOverlayWindow) { ShowWindow(g_combatPanelOverlayWindow, SW_HIDE); } SetCombatPanelRankTooltipVisible(parent, false); SetCombatPanelGuideVisible(parent, false); return; } const bool personalInfoOpen = IsNativePersonalInfoPanelOpen(); DNF90CombatPanelState state = CopyCombatPanelState(); const bool enabled = (state.validFlags & DNF90_COMBAT_PANEL_ENABLED) != 0; if (!personalInfoOpen || !enabled) { if (g_combatPanelOverlayWindow && IsWindowVisible(g_combatPanelOverlayWindow)) { ShowWindow(g_combatPanelOverlayWindow, SW_HIDE); } SetCombatPanelRankTooltipVisible(parent, false); SetCombatPanelGuideVisible(parent, false); if (g_combatPanelPersonalInfoOpen != personalInfoOpen) { LogLine("combat-panel personal visibility open=%d enabled=%d", personalInfoOpen ? 1 : 0, enabled ? 1 : 0); } g_combatPanelPersonalInfoOpen = personalInfoOpen; return; } if (!EnsureCombatPanelOverlayWindow(parent)) return; const bool wasVisible = IsWindowVisible(g_combatPanelOverlayWindow) != FALSE; PositionCombatPanelOverlay(parent); if (g_combatPanelGuideVisible) { SetCombatPanelGuideVisible(parent, true); } RefreshCombatPanelRankTooltip(parent); if (!wasVisible) { InvalidateRect(g_combatPanelOverlayWindow, nullptr, FALSE); } if (!g_combatPanelPersonalInfoOpen) { LogLine("combat-panel shown revision=%u generation=%u score=%u " "base_valid=%d equipment_valid=%d", state.revision, state.sourceGeneration, state.totalScore, (state.validFlags & DNF90_COMBAT_PANEL_BASE_SCORE_VALID) ? 1 : 0, (state.validFlags & DNF90_COMBAT_PANEL_EQUIPMENT_SCORE_VALID) ? 1 : 0); } g_combatPanelPersonalInfoOpen = true; } bool QueueLuaClientNoticeInternal(const wchar_t* text, unsigned int length) { if (!text || length == 0 || length > kLuaNoticeMaximumCharacters) { return RejectLuaClientNotice("invalid-length"); } HWND bridgeWindow = reinterpret_cast( InterlockedCompareExchangePointer( &g_luaNoticeBridgeWindow, nullptr, nullptr)); UINT bridgeMessage = static_cast( InterlockedCompareExchange(&g_luaNoticeWindowMessage, 0, 0)); if (!bridgeWindow || !bridgeMessage) { return RejectLuaClientNotice("main-thread-bridge-not-ready"); } LuaClientNotice pending = {}; pending.length = length; __try { for (unsigned int index = 0; index < length; ++index) { wchar_t value = text[index]; if (value == L'\0') { return RejectLuaClientNotice("embedded-nul"); } pending.text[index] = value; } } __except (EXCEPTION_EXECUTE_HANDLER) { return RejectLuaClientNotice("unreadable-text"); } pending.text[length] = L'\0'; AcquireSRWLockExclusive(&g_luaNoticeLock); if (g_luaNoticeCount >= kLuaNoticeQueueCapacity) { ReleaseSRWLockExclusive(&g_luaNoticeLock); return RejectLuaClientNotice("queue-full"); } size_t tail = (g_luaNoticeHead + g_luaNoticeCount) % kLuaNoticeQueueCapacity; g_luaNoticeQueue[tail] = pending; ++g_luaNoticeCount; ReleaseSRWLockExclusive(&g_luaNoticeLock); LONG queued = InterlockedIncrement(&g_luaNoticeQueuedCount); if (!PostMessageW(bridgeWindow, bridgeMessage, 0, 0)) { LogLine("lua client notice PostMessageW failed queued=%ld error=%lu", queued, GetLastError()); return false; } LogLine("lua client notice queued=%ld chars=%u caller_tid=%lu", queued, length, GetCurrentThreadId()); return true; } bool DequeueLuaClientNotice(LuaClientNotice* output) { if (!output) return false; AcquireSRWLockExclusive(&g_luaNoticeLock); if (g_luaNoticeCount == 0) { ReleaseSRWLockExclusive(&g_luaNoticeLock); return false; } *output = g_luaNoticeQueue[g_luaNoticeHead]; g_luaNoticeQueue[g_luaNoticeHead] = {}; g_luaNoticeHead = (g_luaNoticeHead + 1) % kLuaNoticeQueueCapacity; --g_luaNoticeCount; ReleaseSRWLockExclusive(&g_luaNoticeLock); return true; } LuaClientEventContext BuildLuaDungeonClientEventContext( bool previousRoomKnown = false, unsigned int previousRoomX = 0, unsigned int previousRoomY = 0, unsigned int previousRoomLayerFlag = 0, unsigned int previousMapID = 0) { LuaClientEventContext context = {}; if (g_luaDungeonID != 0) { context.flags |= DNF90_CLIENT_EVENT_CONTEXT_DUNGEON_VALID; context.dungeonID = g_luaDungeonID; } if (g_luaDungeonRoomKnown) { context.flags |= DNF90_CLIENT_EVENT_CONTEXT_ROOM_VALID; context.roomX = g_luaDungeonRoomX; context.roomY = g_luaDungeonRoomY; context.roomLayerFlag = g_luaDungeonRoomLayerFlag; context.mapID = g_luaDungeonMapID; if (g_luaDungeonRoomX == g_luaDungeonBossX && g_luaDungeonRoomY == g_luaDungeonBossY) { context.flags |= DNF90_CLIENT_EVENT_CONTEXT_BOSS_ROOM; } } if (previousRoomKnown) { context.flags |= DNF90_CLIENT_EVENT_CONTEXT_PREVIOUS_ROOM_VALID; context.previousRoomX = previousRoomX; context.previousRoomY = previousRoomY; context.previousRoomLayerFlag = previousRoomLayerFlag; context.previousMapID = previousMapID; } return context; } bool QueueLuaClientEventInternal( DNF90ClientEventType type, DWORD uiThreadID, const LuaClientEventContext* context = nullptr) { DNF90ClientEvent pending = {}; pending.size = sizeof(pending); pending.type = static_cast(type); pending.sequence = static_cast( InterlockedIncrement(&g_luaClientEventSequence)); pending.processID = GetCurrentProcessId(); pending.uiThreadID = uiThreadID; if (context) { pending.contextFlags = context->flags; pending.dungeonID = context->dungeonID; pending.roomX = context->roomX; pending.roomY = context->roomY; pending.roomLayerFlag = context->roomLayerFlag; pending.mapID = context->mapID; pending.previousRoomX = context->previousRoomX; pending.previousRoomY = context->previousRoomY; pending.previousRoomLayerFlag = context->previousRoomLayerFlag; pending.previousMapID = context->previousMapID; } AcquireSRWLockExclusive(&g_luaClientEventLock); if (g_luaClientEventCount >= kLuaClientEventQueueCapacity) { ReleaseSRWLockExclusive(&g_luaClientEventLock); LONG rejected = InterlockedIncrement( &g_luaClientEventRejectedCount); LogLine("lua client event rejected=%ld type=%u sequence=%u reason=queue-full", rejected, pending.type, pending.sequence); return false; } size_t tail = (g_luaClientEventHead + g_luaClientEventCount) % kLuaClientEventQueueCapacity; g_luaClientEventQueue[tail] = pending; ++g_luaClientEventCount; ReleaseSRWLockExclusive(&g_luaClientEventLock); LogLine("lua client event queued type=%u sequence=%u ui_tid=%lu pid=%lu " "context=0x%X dungeon=%u room=%u:%u layer=%u map=%u", pending.type, pending.sequence, static_cast(pending.uiThreadID), static_cast(pending.processID), pending.contextFlags, pending.dungeonID, pending.roomX, pending.roomY, pending.roomLayerFlag, pending.mapID); return true; } bool ParseEvidenceBackedDungeonStartMapBody( const unsigned char* body, size_t bodyLength, bool requireInitialRoom, unsigned char* roomX, unsigned char* roomY, unsigned char* roomLayerFlag, unsigned int* mapID) { // The smallest valid current op29 body is 23 bytes. Offset 13 is the // initial room-state flag, offset 14 the map id, and offset 18 the first // variable-table count. Initial entry requires flag 1; later first visits // use flag 1 and revisits use flag 0. Parse every counted row so a // truncated packet cannot publish a lifecycle event merely by sharing the // same opcode. if (!body || !roomX || !roomY || !roomLayerFlag || !mapID || bodyLength < 23 || body[2] > 1 || body[13] > 1 || (requireInitialRoom && body[13] != 1)) { return false; } unsigned int parsedMapID = *reinterpret_cast(body + 14); if (parsedMapID == 0) return false; size_t offset = 19; size_t actorCount = body[18]; if (actorCount > (bodyLength - offset) / 21) return false; offset += actorCount * 21; if (offset >= bodyLength) return false; size_t extraCount = body[offset++]; if (extraCount > (bodyLength - offset) / 21) return false; offset += extraCount * 21; // Hell-party fog byte followed by the ridable-object group table. if (bodyLength - offset < 2) return false; ++offset; size_t groupCount = body[offset++]; for (size_t group = 0; group < groupCount; ++group) { if (offset >= bodyLength) return false; size_t entryCount = body[offset++]; if (entryCount > (bodyLength - offset) / 20) return false; offset += entryCount * 20; } // Exactly one trailing party-member index completes the typed body. if (bodyLength - offset != 1) return false; *roomX = body[0]; *roomY = body[1]; *roomLayerFlag = body[2]; *mapID = parsedMapID; return true; } void UpdateLuaDungeonEntryStateAfterDispatch( int packet, unsigned int opcode) { if (opcode != kDungeonInfoOpcode && opcode != kDungeonStartMapOpcode && opcode != kDungeonUserStateOpcode) { return; } unsigned int packetLength = 0; size_t bodyLength = 0; unsigned int dungeonID = 0; unsigned int mapID = 0; unsigned char roomX = 0; unsigned char roomY = 0; unsigned char roomLayerFlag = 0; unsigned short objectKey = 0; uintptr_t currentActor = 0; uintptr_t selectedActor = 0; __try { if (!packet) return; const unsigned char* bytes = reinterpret_cast(packet); packetLength = *reinterpret_cast(bytes + 3); if (packetLength < 16) return; const unsigned char* body = bytes + 16; bodyLength = packetLength - 16; if (opcode == kDungeonInfoOpcode) { // The evidence-backed typed body is at least 36 bytes; optional // counted groups may extend it. A nonzero dungeon id is required. if (bodyLength < 36) return; dungeonID = *reinterpret_cast(body); if (dungeonID == 0) return; g_luaDungeonEntryStage = kLuaDungeonEntryAwaitStartMap; g_luaDungeonID = dungeonID; g_luaDungeonBossX = body[8]; g_luaDungeonBossY = body[9]; g_luaDungeonRoomKnown = false; g_luaDungeonRoomX = 0; g_luaDungeonRoomY = 0; g_luaDungeonRoomLayerFlag = 0; g_luaDungeonMapID = 0; LogLine("lua enter_dungeon armed stage=op28 packet_len=%u " "dungeon=%u boss=%u:%u", packetLength, dungeonID, g_luaDungeonBossX, g_luaDungeonBossY); return; } if (opcode == kDungeonStartMapOpcode) { bool initialRoom = g_luaDungeonEntryStage == kLuaDungeonEntryAwaitStartMap; bool changedRoom = g_luaDungeonEntryStage == kLuaDungeonEntryActive; if (!initialRoom && !changedRoom) { return; } if (!ParseEvidenceBackedDungeonStartMapBody( body, bodyLength, initialRoom, &roomX, &roomY, &roomLayerFlag, &mapID)) { LogLine("lua dungeon lifecycle waiting stage=op29-invalid " "packet_len=%u body_len=%u initial=%u", packetLength, static_cast(bodyLength), initialRoom ? 1u : 0u); return; } if (initialRoom) { g_luaDungeonRoomKnown = true; g_luaDungeonRoomX = roomX; g_luaDungeonRoomY = roomY; g_luaDungeonRoomLayerFlag = roomLayerFlag; g_luaDungeonMapID = mapID; g_luaDungeonEntryStage = kLuaDungeonEntryAwaitUserState; g_itemUseGateResetArmed = 1; LogLine("lua enter_dungeon armed stage=op29 packet_len=%u " "room=%u:%u layer=%u map=%u", packetLength, roomX, roomY, roomLayerFlag, mapID); return; } if (g_luaDungeonRoomKnown && g_luaDungeonRoomX == roomX && g_luaDungeonRoomY == roomY && g_luaDungeonRoomLayerFlag == roomLayerFlag && g_luaDungeonMapID == mapID) { LogLine("lua dungeon_room_changed suppressed reason=duplicate " "packet_len=%u room=%u:%u layer=%u map=%u", packetLength, roomX, roomY, roomLayerFlag, mapID); return; } unsigned char previousRoomX = g_luaDungeonRoomX; unsigned char previousRoomY = g_luaDungeonRoomY; unsigned char previousRoomLayerFlag = g_luaDungeonRoomLayerFlag; unsigned int previousMapID = g_luaDungeonMapID; bool previousRoomKnown = g_luaDungeonRoomKnown; g_luaDungeonRoomKnown = true; g_luaDungeonRoomX = roomX; g_luaDungeonRoomY = roomY; g_luaDungeonRoomLayerFlag = roomLayerFlag; g_luaDungeonMapID = mapID; LuaClientEventContext context = BuildLuaDungeonClientEventContext( previousRoomKnown, previousRoomX, previousRoomY, previousRoomLayerFlag, previousMapID); bool queued = QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_DUNGEON_ROOM_CHANGED, GetCurrentThreadId(), &context); LogLine("lua dungeon_room_changed confirmed packet_len=%u " "previous_known=%u previous_room=%u:%u previous_layer=%u " "previous_map=%u room=%u:%u layer=%u map=%u queued=%u", packetLength, previousRoomKnown ? 1u : 0u, previousRoomX, previousRoomY, previousRoomLayerFlag, previousMapID, roomX, roomY, roomLayerFlag, mapID, queued ? 1u : 0u); return; } if (g_luaDungeonEntryStage != kLuaDungeonEntryAwaitUserState || bodyLength != 4 || body[0] != 1 || body[3] != 1) { return; } objectKey = *reinterpret_cast(body + 1); if (objectKey == 0) return; uintptr_t objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); ActorByObjectKeyFn actorByObjectKeyFn = reinterpret_cast( g_dnfBase + kActorByObjectKeyRva); currentActor = objectManager && currentActorFn ? reinterpret_cast( currentActorFn(reinterpret_cast(objectManager))) : 0; selectedActor = actorByObjectKeyFn ? reinterpret_cast(actorByObjectKeyFn(objectKey)) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("lua enter_dungeon waiting reason=read-exception " "opcode=%u packet_len=%u body_len=%u key=%u code=0x%08X", opcode, packetLength, static_cast(bodyLength), objectKey, GetExceptionCode()); return; } if (!currentActor || selectedActor != currentActor) { LogLine("lua enter_dungeon waiting reason=current-actor-mismatch " "packet_len=%u key=%u selected_actor=%p current_actor=%p", packetLength, objectKey, reinterpret_cast(selectedActor), reinterpret_cast(currentActor)); return; } // Mark active even if the bounded queue is full: this exact transition // has already happened and a later room packet must never replay it. g_luaDungeonEntryStage = kLuaDungeonEntryActive; dnf90_companion_p0::ArmFixedApc5405(); if (dnf90_companion_p0::PollFixedApc5405()) { LogLine("companion p0 fixed APC submitted dungeon=%u key=%u actor=%p", g_luaDungeonID, objectKey, reinterpret_cast(currentActor)); } else { LogLine("companion p0 fixed APC pending dungeon=%u key=%u actor=%p", g_luaDungeonID, objectKey, reinterpret_cast(currentActor)); } if (dnf90_companion_p0::ApplyPendingProjection()) { LogLine("companion p1 character projection applied source=enter-dungeon-safe-point"); } LuaClientEventContext context = BuildLuaDungeonClientEventContext(); bool queued = QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_ENTER_DUNGEON, GetCurrentThreadId(), &context); LogLine("lua enter_dungeon confirmed packet_len=%u key=%u actor=%p queued=%u", packetLength, objectKey, reinterpret_cast(currentActor), queued ? 1u : 0u); } void QueueLuaEnterTownEventIfAccepted( int packet, unsigned int sceneModeCalls, unsigned int loadingGateCalls, bool sceneModeEarlyReturn, bool loadingGateEarlyReturn) { if (sceneModeEarlyReturn || loadingGateEarlyReturn) { LogLine("lua enter_town suppressed reason=native-early-return " "scene_calls=%u loading_calls=%u scene_rejected=%u loading_rejected=%u", sceneModeCalls, loadingGateCalls, sceneModeEarlyReturn ? 1u : 0u, loadingGateEarlyReturn ? 1u : 0u); return; } unsigned int packetLength = 0; unsigned int rowCount = 0; unsigned int selectedObjectKey = 0; uintptr_t currentActor = 0; uintptr_t selectedActor = 0; __try { if (!packet) return; const unsigned char* bytes = reinterpret_cast(packet); unsigned int opcode = *reinterpret_cast(bytes + 1); packetLength = *reinterpret_cast(bytes + 3); if (opcode != 24 || packetLength < 20) return; const unsigned char* body = bytes + 16; size_t bodyLength = packetLength - 16; rowCount = *reinterpret_cast(body + 2); if (rowCount == 0 || rowCount > (bodyLength - 4) / 8) return; selectedObjectKey = *reinterpret_cast( body + 4 + (rowCount - 1) * 8); if (selectedObjectKey == 0) return; uintptr_t objectManager = *reinterpret_cast( g_dnfBase + kObjectManagerPointerRva); CurrentActorFn currentActorFn = reinterpret_cast( g_dnfBase + kCurrentActorRva); ActorByObjectKeyFn actorByObjectKeyFn = reinterpret_cast( g_dnfBase + kActorByObjectKeyRva); currentActor = objectManager && currentActorFn ? reinterpret_cast( currentActorFn(reinterpret_cast(objectManager))) : 0; selectedActor = actorByObjectKeyFn ? reinterpret_cast(actorByObjectKeyFn( static_cast(selectedObjectKey))) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("lua enter_town suppressed reason=read-exception " "packet_len=%u rows=%u key=%u code=0x%08X", packetLength, rowCount, selectedObjectKey, GetExceptionCode()); return; } if (!currentActor || selectedActor != currentActor) { LogLine("lua enter_town suppressed reason=current-actor-mismatch " "packet_len=%u rows=%u key=%u selected_actor=%p current_actor=%p", packetLength, rowCount, selectedObjectKey, reinterpret_cast(selectedActor), reinterpret_cast(currentActor)); return; } // An accepted town transition closes any pending or active dungeon-entry // lifecycle. The next event must begin with a fresh typed op28 packet. dnf90_companion_p0::CancelFixedApc5405(); LuaDungeonEntryStage previousDungeonStage = g_luaDungeonEntryStage; LuaClientEventContext leaveContext = BuildLuaDungeonClientEventContext(); g_luaDungeonEntryStage = kLuaDungeonEntryIdle; g_luaDungeonID = 0; g_luaDungeonBossX = 0; g_luaDungeonBossY = 0; g_luaDungeonRoomKnown = false; g_luaDungeonRoomX = 0; g_luaDungeonRoomY = 0; g_luaDungeonRoomLayerFlag = 0; g_luaDungeonMapID = 0; bool leaveQueued = false; if (previousDungeonStage == kLuaDungeonEntryActive) { leaveQueued = QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_LEAVE_DUNGEON, GetCurrentThreadId(), &leaveContext); LogLine("lua leave_dungeon confirmed packet_len=%u key=%u actor=%p queued=%u", packetLength, selectedObjectKey, reinterpret_cast(currentActor), leaveQueued ? 1u : 0u); } bool townQueued = QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_ENTER_TOWN, GetCurrentThreadId()); LogLine("lua enter_town confirmed packet_len=%u rows=%u key=%u actor=%p " "scene_calls=%u loading_calls=%u dungeon_stage=%u " "leave_queued=%u queued=%u", packetLength, rowCount, selectedObjectKey, reinterpret_cast(currentActor), sceneModeCalls, loadingGateCalls, static_cast(previousDungeonStage), leaveQueued ? 1u : 0u, townQueued ? 1u : 0u); } bool DequeueLuaClientEventInternal(DNF90ClientEvent* output) { if (!output) return false; AcquireSRWLockExclusive(&g_luaClientEventLock); if (g_luaClientEventCount == 0) { ReleaseSRWLockExclusive(&g_luaClientEventLock); return false; } *output = g_luaClientEventQueue[g_luaClientEventHead]; g_luaClientEventQueue[g_luaClientEventHead] = {}; g_luaClientEventHead = (g_luaClientEventHead + 1) % kLuaClientEventQueueCapacity; --g_luaClientEventCount; ReleaseSRWLockExclusive(&g_luaClientEventLock); return true; } void QueueLuaUiClosedEventOnce(DWORD uiThreadID) { if (InterlockedCompareExchange( &g_luaUiClosedEventQueued, 1, 0) != 0) { return; } if (!QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_UI_CLOSED, uiThreadID)) { InterlockedExchange(&g_luaUiClosedEventQueued, 0); } } LRESULT CALLBACK ProxyDnfWindowProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { WNDPROC original = g_originalDnfWindowProc; if (!original) return DefWindowProcW(hWnd, message, wParam, lParam); // Do not treat WM_CLOSE as final because the client may cancel a close // request. WM_DESTROY is the first confirmed shutdown boundary and // WM_NCDESTROY is its idempotent fallback. if (message == WM_DESTROY || message == WM_NCDESTROY) { QueueLuaUiClosedEventOnce(GetCurrentThreadId()); if (message == WM_DESTROY) { KillTimer(hWnd, kCombatPanelPollTimer); if (g_combatPanelGuideWindow && IsWindow(g_combatPanelGuideWindow)) { DestroyWindow(g_combatPanelGuideWindow); } if (g_combatPanelRankTooltipWindow && IsWindow(g_combatPanelRankTooltipWindow)) { DestroyWindow(g_combatPanelRankTooltipWindow); } if (g_combatPanelOverlayWindow && IsWindow(g_combatPanelOverlayWindow)) { DestroyWindow(g_combatPanelOverlayWindow); } g_combatPanelGuideWindow = nullptr; g_combatPanelGuideVisible = false; g_combatPanelRankTooltipWindow = nullptr; g_combatPanelRankTooltipVisible = false; g_combatPanelOverlayWindow = nullptr; g_combatPanelPersonalInfoOpen = false; } if (message == WM_NCDESTROY) { InterlockedExchangePointer(&g_luaNoticeBridgeWindow, nullptr); InterlockedExchange(&g_luaNoticeWindowMessage, 0); InterlockedExchange(&g_combatPanelWindowMessage, 0); g_dnfWindow = nullptr; } } if (message == WM_TIMER && wParam == kCombatPanelPollTimer) { if (dnf90_companion_p0::PollFixedApc5405()) { LogLine("companion p0 fixed APC submitted source=ui-timer"); } if (dnf90_companion_p0::ApplyPendingProjection()) { LogLine("companion p1 character projection applied source=ui-timer-safe-point"); } RefreshCombatPanelOverlay(hWnd); return 0; } if ((message == WM_MOVE || message == WM_SIZE) && g_combatPanelOverlayWindow && IsWindowVisible(g_combatPanelOverlayWindow)) { PositionCombatPanelOverlay(hWnd); if (g_combatPanelGuideVisible) { SetCombatPanelGuideVisible(hWnd, true); } } UINT noticeMessage = static_cast( InterlockedCompareExchange(&g_luaNoticeWindowMessage, 0, 0)); if (noticeMessage && message == noticeMessage) { LuaClientNotice notice = {}; if (!DequeueLuaClientNotice(¬ice)) return 0; __try { if (ShowLuaClientNoticeOverlay(hWnd, notice)) { LONG dispatched = InterlockedIncrement( &g_luaNoticeDispatchCount); LogLine("lua client notice dispatched=%ld chars=%u ui_tid=%lu renderer=owned-popup-overlay", dispatched, notice.length, GetCurrentThreadId()); } else { RejectLuaClientNotice("overlay-render-failed"); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("lua client notice dispatch exception code=0x%08X chars=%u ui_tid=%lu", GetExceptionCode(), notice.length, GetCurrentThreadId()); } return 0; } UINT combatPanelMessage = static_cast( InterlockedCompareExchange(&g_combatPanelWindowMessage, 0, 0)); if (combatPanelMessage && message == combatPanelMessage) { RefreshCombatPanelOverlay(hWnd); if (g_combatPanelOverlayWindow) { InvalidateRect(g_combatPanelOverlayWindow, nullptr, FALSE); } if (g_combatPanelRankTooltipWindow && IsWindowVisible(g_combatPanelRankTooltipWindow)) { InvalidateRect(g_combatPanelRankTooltipWindow, nullptr, FALSE); } if (g_combatPanelGuideWindow && IsWindowVisible(g_combatPanelGuideWindow)) { InvalidateRect(g_combatPanelGuideWindow, nullptr, FALSE); } return 0; } if (message == WM_RBUTTONDOWN || message == WM_RBUTTONDBLCLK) { LONG before = InterlockedCompareExchange( &g_clientInventoryUseSendCount, 0, 0); CurrentInventoryUseSelection selection = {}; bool selected = ResolveHoveredInventoryUseSelection(&selection); LRESULT result = CallWindowProcW( original, hWnd, message, wParam, lParam); if (!selected) { selected = ResolveHoveredInventoryUseSelection(&selection); } g_contractRButtonPending = selected; g_contractRButtonPendingSlot = selected ? selection.slot : -1; g_contractRButtonPendingTemplateID = selected ? selection.templateID : 0; g_contractRButtonPendingIdentity = selected ? selection.identity : 0; g_contractRButtonPendingOp44Count = before; if (selected) { LogLine("contract-use right-button down slot=%d item=%lu identity=%lu before=%ld", selection.slot, static_cast(selection.templateID), static_cast(selection.identity), before); } return result; } if (message == WM_RBUTTONUP) { LRESULT result = CallWindowProcW( original, hWnd, message, wParam, lParam); if (!g_contractRButtonPending) return result; CurrentInventoryUseSelection selection = {}; selection.slot = g_contractRButtonPendingSlot; selection.templateID = g_contractRButtonPendingTemplateID; selection.identity = g_contractRButtonPendingIdentity; LONG before = g_contractRButtonPendingOp44Count; g_contractRButtonPending = false; g_contractRButtonPendingSlot = -1; g_contractRButtonPendingTemplateID = 0; g_contractRButtonPendingIdentity = 0; LONG after = InterlockedCompareExchange( &g_clientInventoryUseSendCount, 0, 0); if (after == before && GetForegroundWindow() == hWnd) { g_contractUseFallbackActive = true; bool sent = SendCurrentInventoryUseFallback(selection); g_contractUseFallbackActive = false; if (sent) { LONG hit = InterlockedIncrement( &g_contractRButtonFallbackCount); LogLine("contract-use right-button fallback hit=%ld slot=%d item=%lu identity=%lu", hit, selection.slot, static_cast(selection.templateID), static_cast(selection.identity)); } } return result; } return CallWindowProcW(original, hWnd, message, wParam, lParam); } BOOL CALLBACK FindCurrentProcessMainWindow(HWND hWnd, LPARAM parameter) { DWORD processID = 0; GetWindowThreadProcessId(hWnd, &processID); if (processID != GetCurrentProcessId() || !IsWindowVisible(hWnd) || GetWindow(hWnd, GW_OWNER) != nullptr) { return TRUE; } *reinterpret_cast(parameter) = hWnd; return FALSE; } bool InstallContractUseWindowProc() { constexpr int kWaitLimitMs = 120000; int waitedMs = 0; HWND window = nullptr; while (!window && waitedMs < kWaitLimitMs) { EnumWindows(&FindCurrentProcessMainWindow, reinterpret_cast(&window)); if (window) break; Sleep(250); waitedMs += 250; } if (!window) { LogLine("contract-use main window unavailable after wait_ms=%d", waitedMs); return false; } WNDPROC original = reinterpret_cast( GetWindowLongPtrW(window, GWLP_WNDPROC)); if (!original) { LogLine("contract-use main window has no window procedure hwnd=%p", window); return false; } g_originalDnfWindowProc = original; SetLastError(ERROR_SUCCESS); LONG_PTR previous = SetWindowLongPtrW( window, GWLP_WNDPROC, reinterpret_cast(&ProxyDnfWindowProc)); DWORD error = GetLastError(); if (!previous && error != ERROR_SUCCESS) { g_originalDnfWindowProc = nullptr; LogLine("contract-use window procedure install failed hwnd=%p error=%lu", window, error); return false; } g_dnfWindow = window; g_originalDnfWindowProc = reinterpret_cast(previous); InterlockedExchangePointer(&g_luaNoticeBridgeWindow, window); UINT noticeMessage = RegisterWindowMessageW(kLuaNoticeWindowMessageName); if (noticeMessage != 0) { InterlockedExchange(&g_luaNoticeWindowMessage, static_cast(noticeMessage)); DWORD windowProcessID = 0; DWORD windowThreadID = GetWindowThreadProcessId( window, &windowProcessID); LogLine("lua client notice bridge ready hwnd=%p message=0x%04X ui_tid=%lu pid=%lu renderer=owned-popup-overlay", window, noticeMessage, windowThreadID, windowProcessID); if (InterlockedCompareExchange( &g_luaUiReadyEventQueued, 1, 0) == 0 && !QueueLuaClientEventInternal( DNF90_CLIENT_EVENT_UI_READY, windowThreadID)) { InterlockedExchange(&g_luaUiReadyEventQueued, 0); } } else { LogLine("lua client notice bridge unavailable message=0x%04X error=%lu", noticeMessage, GetLastError()); } UINT combatPanelMessage = RegisterWindowMessageW( kCombatPanelWindowMessageName); if (combatPanelMessage != 0) { InterlockedExchange(&g_combatPanelWindowMessage, static_cast(combatPanelMessage)); UINT_PTR timer = SetTimer(window, kCombatPanelPollTimer, kCombatPanelPollIntervalMs, nullptr); LogLine("combat-panel UI bridge ready hwnd=%p message=0x%04X " "timer=%p interval_ms=%u owner=%u is_open=+0x%08X", window, combatPanelMessage, reinterpret_cast(timer), kCombatPanelPollIntervalMs, static_cast(kPersonalInfoUiOwner), static_cast(kSceneUiIsOpenRva)); } else { LogLine("combat-panel UI bridge unavailable error=%lu", GetLastError()); } LogLine("contract-use window procedure installed hwnd=%p original=%p wait_ms=%d", window, g_originalDnfWindowProc, waitedMs); return g_originalDnfWindowProc != nullptr; } int __fastcall ProxyCurrentItemRowParse(int itemObject, int unused, char* raw) { unsigned char rawCopy[kCurrentItemRawBytes] = { 0 }; bool captured = TryCopyTraceBytes(raw, rawCopy, sizeof(rawCopy)); int result = g_originalCurrentItemRowParse ? g_originalCurrentItemRowParse(itemObject, unused, raw) : 0; if (captured) CacheCurrentItemRow(static_cast(itemObject), rawCopy); return result; } int __fastcall ProxyInventoryUseUIA( void* self, void* /*unused*/, int eventCode, int eventDetail, int arg2, int arg3) { InventoryUseUIAFn original = reinterpret_cast(g_originalInventoryUseUIA); if (!original) return 0; if (g_contractUseFallbackActive || eventCode != 0x12C || eventDetail != 0x0D) { return original(self, eventCode, eventDetail, arg2, arg3); } LONG hit = InterlockedIncrement(&g_contractUseUIAHitCount); CurrentInventoryUseSelection selection = {}; bool contractSelected = ResolveCurrentInventoryUseSelection(&selection); LONG before = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); LogLine("contract-use callback A hit=%ld selected=%d slot=%d item=%lu identity=%lu before=%ld", hit, contractSelected ? 1 : 0, selection.slot, static_cast(selection.templateID), static_cast(selection.identity), before); g_contractUseFallbackActive = true; int result = original(self, eventCode, eventDetail, arg2, arg3); LONG after = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); if (contractSelected && after == before) { SendCurrentInventoryUseFallback(selection); } g_contractUseFallbackActive = false; return result; } void __cdecl ProxyInventoryUseUIB() { InventoryUseUIBFn original = reinterpret_cast(g_originalInventoryUseUIB); if (!original) return; if (g_contractUseFallbackActive) { original(); return; } LONG hit = InterlockedIncrement(&g_contractUseUIBHitCount); CurrentInventoryUseSelection selection = {}; bool contractSelected = ResolveCurrentInventoryUseSelection(&selection); LONG before = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); LogLine("contract-use callback B hit=%ld selected=%d slot=%d item=%lu identity=%lu before=%ld", hit, contractSelected ? 1 : 0, selection.slot, static_cast(selection.templateID), static_cast(selection.identity), before); g_contractUseFallbackActive = true; original(); LONG after = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); if (contractSelected && after == before) { SendCurrentInventoryUseFallback(selection); } g_contractUseFallbackActive = false; } uintptr_t RunInventoryUseGateFallback( InventoryUseGateFn original, volatile LONG* hitCounter, const char* label, void* self, int arg0, int arg1, int arg2) { if (!original) return 0; if (g_contractUseFallbackActive) { return original(self, arg0, arg1, arg2); } LONG hit = InterlockedIncrement(hitCounter); CurrentInventoryUseSelection selection = {}; bool contractSelected = ResolveCurrentInventoryUseSelection(&selection); LONG before = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); LogLine("contract-use %s hit=%ld selected=%d slot=%d item=%lu identity=%lu before=%ld args=%08X/%08X/%08X", label, hit, contractSelected ? 1 : 0, selection.slot, static_cast(selection.templateID), static_cast(selection.identity), before, static_cast(arg0), static_cast(arg1), static_cast(arg2)); // The current client can reject a contract in sub_275A140's native 0x7D4 // eligibility check before any of the three lower op44 callbacks runs. // Keep the whole native callback first; only recover an allow-listed, // selected main-bag contract when that callback emitted no op44. g_contractUseFallbackActive = true; uintptr_t result = original(self, arg0, arg1, arg2); if (!contractSelected) { contractSelected = ResolveCurrentInventoryUseSelection(&selection); } LONG after = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); if (contractSelected && after == before) { SendCurrentInventoryUseFallback(selection); } g_contractUseFallbackActive = false; return result; } uintptr_t __fastcall ProxyInventoryUseGateA( void* self, void* /*unused*/, int arg0, int arg1, int arg2) { return RunInventoryUseGateFallback( reinterpret_cast(g_originalInventoryUseGateA), &g_contractUseGateAHitCount, "gate-A", self, arg0, arg1, arg2); } uintptr_t __fastcall ProxyInventoryUseGateB( void* self, void* /*unused*/, int arg0, int arg1, int arg2) { return RunInventoryUseGateFallback( reinterpret_cast(g_originalInventoryUseGateB), &g_contractUseGateBHitCount, "gate-B", self, arg0, arg1, arg2); } unsigned char __fastcall ProxyInventoryUsePanel( void* self, void* /*unused*/, uint32_t templateID, int slot) { InventoryUsePanelFn original = reinterpret_cast(g_originalInventoryUsePanel); if (!original) return 0; if (g_contractUseFallbackActive) { return original(self, templateID, slot); } LONG hit = InterlockedIncrement(&g_contractUsePanelHitCount); CurrentInventoryUseSelection selection = {}; bool contractSelected = ResolveCurrentInventoryUsePanelSelection( templateID, slot, &selection); LONG before = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); LogLine("contract-use panel hit=%ld selected=%d slot=%d item=%lu identity=%lu before=%ld", hit, contractSelected ? 1 : 0, selection.slot, static_cast(selection.templateID), static_cast(selection.identity), before); // Preserve the native panel behavior first. The fallback only covers its // audited global-gate branch when no C2S op44/op30 reached the packet logger. g_contractUseFallbackActive = true; unsigned char result = original(self, templateID, slot); LONG after = InterlockedCompareExchange(&g_clientInventoryUseSendCount, 0, 0); if (contractSelected && after == before) { SendCurrentInventoryUseFallback(selection); } g_contractUseFallbackActive = false; return result; } // Native sub_2326860 is __thiscall and assumes `self` is non-null. It loads // `self + 0x44` into ecx and calls sub_34DBD90, whose first instruction is // `mov eax, [ecx]`. When the main-inventory selection manager has not been // created yet (new character / title-preview animation / shutdown race), // self == nullptr turns that into a read at address 0x44 and the client exits // with 0xC0000005. Return the same "no selected item" result the native // bounds checks use instead of executing the dereference. A stale manager is // also contained by the SEH boundary. void* __fastcall ProxyInventorySelectionLookup( void* self, void* /*unused*/, int slot) { if (!self || reinterpret_cast(self) < 0x10000) { return nullptr; } InventorySelectionLookupFn original = reinterpret_cast( g_originalInventorySelectionLookup); if (!original) return nullptr; __try { return original(self, slot); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("inventory selection lookup guard exception self=%p slot=%d code=0x%08X", self, slot, GetExceptionCode()); return nullptr; } } void LogSocketPanelSelection(int selectedItem) { LONG selection = InterlockedIncrement(&g_socketTraceSelectionCount); if (selection > kSocketTraceMaxSelections) return; // Current NoPack's selected item wrapper uses vtable+0x28 // (sub_105FB00) to return this+8. Its +0x18 field is the exact u32 // template value consumed by sub_2112D50. sub_21792A0 itself receives // a stack-local parser object, so its pointer cannot be compared to this // persistent selected wrapper after scene-row parsing has returned. uintptr_t itemData = static_cast(selectedItem) + 8; uint32_t templateID = 0; bool templateReadable = TryCopyTraceBytes(reinterpret_cast(itemData + 0x18), &templateID, sizeof(templateID)); CurrentItemRowTrace matches[kSocketTraceTemplateMatches] = {}; LONG matchCount = templateReadable ? FindCurrentItemRowsByTemplate(templateID, matches, kSocketTraceTemplateMatches) : 0; unsigned char objectBytes[0xC4] = { 0 }; bool objectReadable = TryCopyTraceBytes(reinterpret_cast(itemData), objectBytes, sizeof(objectBytes)); char objectHex[640] = { 0 }; if (objectReadable) FormatTraceBytes(objectBytes, sizeof(objectBytes), objectHex, sizeof(objectHex)); // The selected pointer itself is the polymorphic wrapper consumed by // sub_2112F70. Record its vtable and only the two narrow ranges needed to // map the lock renderer back to the current raw row. This is passive // evidence collection: no wrapper, vtable, packet or item byte is changed. uintptr_t selectedVtable = 0; bool selectedVtableReadable = TryCopyTraceBytes(reinterpret_cast(selectedItem), &selectedVtable, sizeof(selectedVtable)); unsigned char wrapperHead[0x40] = { 0 }; bool wrapperHeadReadable = TryCopyTraceBytes(reinterpret_cast(selectedItem), wrapperHead, sizeof(wrapperHead)); char wrapperHeadHex[256] = { 0 }; if (wrapperHeadReadable) { FormatTraceBytes(wrapperHead, sizeof(wrapperHead), wrapperHeadHex, sizeof(wrapperHeadHex)); } unsigned char wrapperState[0x120] = { 0 }; bool wrapperStateReadable = TryCopyTraceBytes( reinterpret_cast(static_cast(selectedItem) + 0x2E0), wrapperState, sizeof(wrapperState)); char wrapperStateHex[896] = { 0 }; if (wrapperStateReadable) { FormatTraceBytes(wrapperState, sizeof(wrapperState), wrapperStateHex, sizeof(wrapperStateHex)); } unsigned char vtableBytes[0xE0] = { 0 }; bool vtableReadable = selectedVtableReadable && TryCopyTraceBytes( reinterpret_cast(selectedVtable), vtableBytes, sizeof(vtableBytes)); char vtableHex[704] = { 0 }; if (vtableReadable) { FormatTraceBytes(vtableBytes, sizeof(vtableBytes), vtableHex, sizeof(vtableHex)); } LogLine("socket-trace panel-select ordinal=%ld item_wrapper=0x%08X item_data=0x%08X template_read=%d template_id=%lu raw_template_matches=%ld cached_rows=%ld", selection, selectedItem, itemData, templateReadable ? 1 : 0, static_cast(templateID), matchCount, g_socketTraceRowSequence); for (LONG i = 0; i < matchCount; ++i) { char rawHex[384] = { 0 }; FormatTraceBytes(matches[i].raw, sizeof(matches[i].raw), rawHex, sizeof(rawHex)); LogLine("socket-trace raw-template-match ordinal=%ld match=%ld parse_sequence=%ld parser_object=0x%08X raw_slot=%u raw_template_id=%lu raw77=%s", selection, i + 1, matches[i].sequence, matches[i].parserObject, CurrentItemRowSlot(matches[i].raw), static_cast(CurrentItemRowTemplateID(matches[i].raw)), rawHex); } LogLine("socket-trace panel-object ordinal=%ld item_data=0x%08X bytes_c4=%s", selection, itemData, objectReadable ? objectHex : ""); LogLine("socket-trace selected-wrapper ordinal=%ld wrapper=0x%08X vtable_read=%d vtable=0x%08X head_40=%s", selection, selectedItem, selectedVtableReadable ? 1 : 0, selectedVtable, wrapperHeadReadable ? wrapperHeadHex : ""); LogLine("socket-trace selected-wrapper-state ordinal=%ld wrapper=0x%08X range=+0x2E0..+0x3FF bytes_120=%s", selection, selectedItem, wrapperStateReadable ? wrapperStateHex : ""); LogLine("socket-trace selected-vtable ordinal=%ld vtable=0x%08X bytes_e0=%s", selection, selectedVtable, vtableReadable ? vtableHex : ""); } char __fastcall ProxySocketPanelSelect(void* self, void* /*unused*/, int selectedItem, int arg) { char result = g_originalSocketPanelSelect ? g_originalSocketPanelSelect(self, selectedItem, arg) : 0; LogSocketPanelSelection(selectedItem); return result; } char __fastcall ProxySocketOpenWriter(void* self, void* /*unused*/) { LONG hit = InterlockedIncrement(&g_socketTraceWriterCount); uintptr_t selectedItem = 0; bool selectedReadable = self && TryCopyTraceBytes( reinterpret_cast(reinterpret_cast(self) + 0x1B4), &selectedItem, sizeof(selectedItem)); if (hit <= kSocketTraceMaxWriterHits) { LogLine("socket-trace writer-enter ordinal=%ld ui=0x%08X selected_read=%d selected_wrapper=0x%08X", hit, self, selectedReadable ? 1 : 0, selectedItem); } char result = g_originalSocketOpenWriter ? g_originalSocketOpenWriter(self) : 0; if (hit <= kSocketTraceMaxWriterHits) { LogLine("socket-trace writer-return ordinal=%ld result=%d", hit, static_cast(static_cast(result))); } return result; } bool __cdecl ProxySocketOp14ExtensionGate(int templateID) { bool accepted = g_originalSocketOp14ExtensionGate ? g_originalSocketOp14ExtensionGate(templateID) : false; // Do not turn a broad scene-reader hook into a logging hot path. The // passive socket trace already established this selected normal-equipment // template; record only its actual current-client gate result. if (static_cast(templateID) == kSocketTraceSelectedTemplateID) { LONG hit = InterlockedIncrement(&g_socketTraceExtensionGateCount); if (hit <= kSocketTraceMaxExtensionGateHits) { LogLine("socket-trace op14-list3-extension-gate ordinal=%ld template_id=%lu accepted=%d", hit, static_cast(static_cast(templateID)), accepted ? 1 : 0); } } return accepted; } void LogCodecSetKey(int index, void* self, int keyPtr, int keyLength, int result, void* returnAddress) { char hex[360] = { 0 }; FormatHexBytes(reinterpret_cast(keyPtr), keyLength, hex, sizeof(hex)); uintptr_t vtable = 0; uintptr_t state = 0; __try { vtable = self ? *reinterpret_cast(self) : 0; state = self ? *(reinterpret_cast(self) + 2) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { vtable = 0; state = 0; } LogLine("codec-setkey idx=%d self=%p vt=0x%08X state=0x%08X key=%p len=%d ret=0x%08X caller=0x%08X bytes=%s", index, self, vtable, state, reinterpret_cast(keyPtr), keyLength, result, reinterpret_cast(returnAddress), hex); } bool IsTclsTrimSpace(wchar_t value) { return value == L' ' || value == L'\t' || value == L'\r' || value == L'\n'; } bool WideSliceEquals(const wchar_t* text, size_t length, const wchar_t* expected) { if (!text || !expected) return false; size_t expectedLength = wcslen(expected); return length == expectedLength && wmemcmp(text, expected, length) == 0; } // The launch parser owns a vector at +0xC4/+0xC8. Its entries are // direct NUL-terminated strings. The fetch output objects are a different // type and are written only through the current EXE's native assign method. bool TryReadTclsSlot(unsigned int slot, const wchar_t** text, size_t* length) { if (!text || !length) return false; *text = nullptr; *length = 0; __try { unsigned char* parser = static_cast(g_currentTclsParser); if (!parser) return false; void** begin = *reinterpret_cast(parser + 0xC4); void** end = *reinterpret_cast(parser + 0xC8); if (!begin || !end || end < begin) return false; size_t count = static_cast(end - begin); if (count == 0 || count > 64 || slot >= count) return false; const wchar_t* valueText = reinterpret_cast(begin[slot]); if (!valueText) return false; size_t valueLength = 0; while (valueLength < 4096 && valueText[valueLength] != L'\0') ++valueLength; if (valueLength == 0 || valueLength == 4096) return false; size_t first = 0; size_t last = valueLength; while (first < last && IsTclsTrimSpace(valueText[first])) ++first; while (last > first && IsTclsTrimSpace(valueText[last - 1])) --last; if (last == first) return false; valueText += first; valueLength = last - first; if (WideSliceEquals(valueText, valueLength, L"") || WideSliceEquals(valueText, valueLength, L"")) { return false; } *text = valueText; *length = valueLength; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool IsDirectTclsLaunch() { const wchar_t* marker = nullptr; size_t markerLength = 0; return TryReadTclsSlot(0, &marker, &markerLength) && WideSliceEquals(marker, markerLength, L"99"); } bool AssignTclsSlot(void* output, unsigned int slot) { if (!output || !g_gameWideAssign || !IsDirectTclsLaunch()) return false; const wchar_t* value = nullptr; size_t valueLength = 0; if (!TryReadTclsSlot(slot, &value, &valueLength)) return false; __try { g_gameWideAssign(output, value, valueLength); LogLine("tcls native fallback slot=%u len=%u", slot, static_cast(valueLength)); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("tcls native fallback exception slot=%u code=0x%08X", slot, GetExceptionCode()); return false; } } size_t ReadGameWideLength(void* value) { if (!value) return 0; __try { return *reinterpret_cast(static_cast(value) + 0x14); } __except (EXCEPTION_EXECUTE_HANDLER) { return 0; } } bool __fastcall ProxyTclsParse(void* self, void* /*unused*/, const wchar_t* text) { g_currentTclsParser = self; bool result = g_originalTclsParse ? g_originalTclsParse(self, text) : false; g_currentTclsParser = nullptr; LogLine("tcls parse result=%d", result ? 1 : 0); return result; } bool __fastcall ProxyTclsFetchLogin(void* self, void* /*unused*/, void* outIp, void* outPort, void* outChannel) { bool result = g_originalTclsFetchLogin ? g_originalTclsFetchLogin(self, outIp, outPort, outChannel) : false; if (!result) { // Match the native compatibility behavior: attempt all three writes // even if an earlier field fails, then report aggregate success. bool ipAssigned = AssignTclsSlot(outIp, 1); bool portAssigned = AssignTclsSlot(outPort, 2); bool channelAssigned = AssignTclsSlot(outChannel, 5); result = ipAssigned && portAssigned && channelAssigned; } LogLine("tcls fetch login result=%d", result ? 1 : 0); return result; } bool __fastcall ProxyTclsFetchText(void* self, void* /*unused*/, int key, void* output) { bool result = g_originalTclsFetchText ? g_originalTclsFetchText(self, key, output) : false; if (!result && key == 2) result = AssignTclsSlot(output, 3); LogLine("tcls fetch text key=%d result=%d", key, result ? 1 : 0); return result; } bool __fastcall ProxyTclsFetchCrypto(void* self, void* /*unused*/, void* output) { bool result = g_originalTclsFetchCrypto ? g_originalTclsFetchCrypto(self, output) : false; if (!result) result = AssignTclsSlot(output, 4); LogLine("tcls fetch crypto result=%d", result ? 1 : 0); return result; } bool __fastcall ProxyTclsFetchTail(void* self, void* /*unused*/, void* output) { bool result = g_originalTclsFetchTail ? g_originalTclsFetchTail(self, output) : false; if (!result) result = AssignTclsSlot(output, 15); LogLine("tcls fetch tail result=%d", result ? 1 : 0); return result; } bool __stdcall ProxyTclsTail(void* output) { bool result = g_originalTclsTail ? g_originalTclsTail(output) : false; if (!result || ReadGameWideLength(output) == 0) { if (AssignTclsSlot(output, 12)) result = true; } LogLine("tcls tail result=%d len=%u", result ? 1 : 0, static_cast(ReadGameWideLength(output))); return result; } int __fastcall ProxyCodecSetKey0(void* self, void* /*unused*/, int keyPtr, int keyLength) { int result = g_originalCodecSetKey0 ? g_originalCodecSetKey0(self, keyPtr, keyLength) : 1879048192; LogCodecSetKey(0, self, keyPtr, keyLength, result, _ReturnAddress()); return result; } int __fastcall ProxyCodecSetKey2(void* self, void* /*unused*/, int keyPtr, int keyLength) { int result = g_originalCodecSetKey2 ? g_originalCodecSetKey2(self, keyPtr, keyLength) : 1879048192; LogCodecSetKey(2, self, keyPtr, keyLength, result, _ReturnAddress()); return result; } int __fastcall ProxyCodecSetKey3(void* self, void* /*unused*/, int keyPtr, int keyLength) { int result = g_originalCodecSetKey3 ? g_originalCodecSetKey3(self, keyPtr, keyLength) : 1879048192; LogCodecSetKey(3, self, keyPtr, keyLength, result, _ReturnAddress()); return result; } int __fastcall ProxyCodecSetKey7(void* self, void* /*unused*/, int keyPtr, int keyLength) { int result = g_originalCodecSetKey7 ? g_originalCodecSetKey7(self, keyPtr, keyLength) : 1879048192; LogCodecSetKey(7, self, keyPtr, keyLength, result, _ReturnAddress()); return result; } int __fastcall ProxyCodecSetKey8(void* self, void* /*unused*/, int keyPtr, int keyLength) { int result = g_originalCodecSetKey8 ? g_originalCodecSetKey8(self, keyPtr, keyLength) : 1879048192; LogCodecSetKey(8, self, keyPtr, keyLength, result, _ReturnAddress()); return result; } bool InstallKnownInlineHook(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 (!BytesMatch(reinterpret_cast(target), expected, patchLength)) { LogLine("%s native 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) { LogLine("%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)) { LogLine("%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; LogLine("%s installed target=0x%08X trampoline=%p", label, target, trampoline); return true; } // Current-EXE iterator guard is scoped to the UI update call-site. #include "90CN_ui_iterator_guard.inl" bool InstallPartyDirectoryFullPageCompatibility() { // sub_326D450 opens owner 0x17B for ordinary channels. That owner is the // six-row regional summary and never sends the full-directory request. // The current EXE's native full party directory is owner 9 and its only // native open site passes owner 9. Keep the controller lifecycle symmetric // by changing its owner guard, open owner, and both owner-0x17B close paths // as one validated patch set. Preserve the controller's existing mode/group // arguments; owner 9's current-EXE open handler does not read either value. static const unsigned char kOwner9[] = { 0x68, 0x09, 0x00, 0x00, 0x00 }; static const unsigned char kGuardContext[] = { 0x55, 0x8B, 0xEC, 0x57, 0x8B, 0xF9, 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x68, 0x7B, 0x01, 0x00, 0x00, 0xE8, 0xDA, 0x7A, 0xD6, 0xFF, 0x84, 0xC0, 0x75, 0x46, 0x83, 0x3D, 0xE4, 0x0A, 0x2E, 0x05, 0x00, 0x75, 0x3D, 0x56, 0x6A, 0x00, 0x6A, 0xFF, 0x6A, 0xFF, 0x6A, 0x00, 0x6A }; static const unsigned char kOpenContext[] = { 0x8B, 0x75, 0x08, 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x83, 0xC4, 0x1C, 0x56, 0x6A, 0x00, 0x68, 0x7B, 0x01, 0x00, 0x00, 0xE8, 0x24, 0x02, 0xD7, 0xFF, 0x89, 0x77, 0x34 }; static const unsigned char kCloseContextA[] = { 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0xFF, 0x68, 0x7C, 0x01, 0x00, 0x00, 0xE8, 0xA5, 0xB1, 0xD6, 0xFF, 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0xFF, 0x68, 0x7B, 0x01, 0x00, 0x00, 0xE8, 0x91, 0xB1, 0xD6, 0xFF, 0x5F }; static const unsigned char kCloseContextB[] = { 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0xFF, 0x68, 0x7C, 0x01, 0x00, 0x00, 0xE8, 0x29, 0xB1, 0xD6, 0xFF, 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0xFF, 0x68, 0x7B, 0x01, 0x00, 0x00, 0xE8, 0x15, 0xB1, 0xD6, 0xFF, 0x5E }; struct Context { uintptr_t target; const unsigned char* expected; size_t length; const char* label; }; Context contexts[] = { { g_dnfBase + 0x02E6CF20, kGuardContext, sizeof(kGuardContext), "owner guard context" }, { g_dnfBase + 0x02E6CF63, kOpenContext, sizeof(kOpenContext), "open context" }, { g_dnfBase + 0x02E6D417, kCloseContextA, sizeof(kCloseContextA), "close context A" }, { g_dnfBase + 0x02E6D493, kCloseContextB, sizeof(kCloseContextB), "close context B" }, }; struct Patch { uintptr_t target; const unsigned char* replacement; size_t length; }; Patch patches[] = { { g_dnfBase + kPartyDirectoryOwnerGuardRva, kOwner9, sizeof(kOwner9) }, { g_dnfBase + kPartyDirectoryOpenOwnerRva, kOwner9, sizeof(kOwner9) }, { g_dnfBase + kPartyDirectoryCloseOwnerARva, kOwner9, sizeof(kOwner9) }, { g_dnfBase + kPartyDirectoryCloseOwnerBRva, kOwner9, sizeof(kOwner9) }, }; for (size_t i = 0; i < _countof(contexts); ++i) { if (!BytesMatch(reinterpret_cast(contexts[i].target), contexts[i].expected, contexts[i].length)) { LogLine("party-directory full-page compatibility rejected: %s " "bytes mismatch at 0x%08X", contexts[i].label, contexts[i].target); return false; } } uintptr_t patchStart = patches[0].target; uintptr_t patchEnd = patches[_countof(patches) - 1].target + patches[_countof(patches) - 1].length; DWORD oldProtection = 0; if (!VirtualProtect(reinterpret_cast(patchStart), patchEnd - patchStart, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("party-directory full-page compatibility VirtualProtect failed: %u", GetLastError()); return false; } for (size_t i = 0; i < _countof(patches); ++i) { memcpy(reinterpret_cast(patches[i].target), patches[i].replacement, patches[i].length); } DWORD ignoredProtection = 0; VirtualProtect(reinterpret_cast(patchStart), patchEnd - patchStart, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(patchStart), patchEnd - patchStart); LogLine("party-directory full-page compatibility installed " "guard=0x%08X open=0x%08X close_a=0x%08X close_b=0x%08X", patches[0].target, patches[1].target, patches[2].target, patches[3].target); return true; } bool __fastcall ProxyChannelScriptDownload(void* self, void* /*unused*/) { LogLine("channel script download enter self=%p", self); bool result = g_originalChannelScriptDownload ? g_originalChannelScriptDownload(self) : false; LogLine("channel script download return result=%d", result ? 1 : 0); return result; } bool __fastcall ProxyChannelScriptLoad(void* self, void* /*unused*/, const char* text) { size_t length = 0; char hex[360] = { 0 }; __try { if (text) { while (length < 0x80000 && text[length] != '\0') ++length; FormatHexBytes(text, static_cast(length), hex, sizeof(hex)); } } __except (EXCEPTION_EXECUTE_HANDLER) { length = 0; _snprintf(hex, sizeof(hex) - 1, "", GetExceptionCode()); } LogLine("channel script load enter self=%p len=%u bytes=%s", self, static_cast(length), hex); bool result = g_originalChannelScriptLoad ? g_originalChannelScriptLoad(self, text) : false; LogLine("channel script load return result=%d", result ? 1 : 0); return result; } void* __fastcall ProxyChannelRuntimeLoad(void* self, void* /*unused*/, const void* key) { unsigned int serverID = 0; unsigned int channelID = 0; __try { if (key) { const unsigned int* values = static_cast(key); serverID = values[0]; channelID = values[1]; } } __except (EXCEPTION_EXECUTE_HANDLER) { serverID = 0xFFFFFFFFu; channelID = 0xFFFFFFFFu; } void* scriptRecord = nullptr; unsigned int scriptType = 0xFFFFFFFFu; __try { if (serverID != 0xFFFFFFFFu && channelID != 0xFFFFFFFFu) { ChannelScriptLookupFn lookup = reinterpret_cast( g_dnfBase + kChannelScriptLookupRva); scriptRecord = lookup(reinterpret_cast(g_dnfBase + kChannelScriptStoreRva), static_cast(serverID), static_cast(channelID)); if (scriptRecord) { scriptType = *reinterpret_cast( static_cast(scriptRecord) + 0x20); } } } __except (EXCEPTION_EXECUTE_HANDLER) { scriptRecord = nullptr; scriptType = 0xFFFFFFFFu; } void* result = g_originalChannelRuntimeLoad ? g_originalChannelRuntimeLoad(self, key) : nullptr; LogLine("channel runtime load server=%u channel=%u script=%p type=%u result=%p", serverID, channelID, scriptRecord, scriptType, result); return result; } int __stdcall ProxyChannelCategoryInsert(int category, void* channel) { unsigned int serverID = 0xFFFFFFFFu; unsigned int channelID = 0xFFFFFFFFu; unsigned int channelType = 0xFFFFFFFFu; __try { if (channel) { const unsigned int* fields = static_cast(channel); serverID = fields[1]; channelType = fields[2]; channelID = fields[0x70 / sizeof(unsigned int)]; } } __except (EXCEPTION_EXECUTE_HANDLER) { } LogLine("channel category insert category=%d server=%u channel=%u type=%u object=%p", category, serverID, channelID, channelType, channel); // RETAIL ADAPTATION - auto-connect channel target. // // The stock code only captured the candidate when DNF_AUTO_CONNECT_CHANNEL_ID // was present AND the server index was 0. On this deployment both failed: // * the variable is frequently absent (it is not part of the original // launcher contract), and // * the server advertises its channels under server index 1 // (channel_server_index=1), not 0. // With no captured candidate the delayed auto-connect never arms, the game // socket is never opened, and the UI sits at "connecting to channel server" // until it reports "cannot connect". The baseline DLL had no auto-connect at // all, which is why the unpatched client looked fine while this one stalled. // // Resolution order (first match wins): // 1. DNF_AUTO_CONNECT_CHANNEL_ID // 2. DNF_RESIDENT_CHANNEL_ID (already used for the port redirect) // 3. 11 (this deployment's advertised channel) // Only ordinary channels (category 0) are eligible, and the channel id must // still equal the resolved target, so we never latch onto a PVP/raid row. unsigned long autoConnectChannelID = 0; const wchar_t* autoConnectSources[2] = { L"DNF_AUTO_CONNECT_CHANNEL_ID", L"DNF_RESIDENT_CHANNEL_ID", }; for (int sourceIndex = 0; sourceIndex < 2; ++sourceIndex) { wchar_t configured[16] = { 0 }; DWORD configuredLength = GetEnvironmentVariableW( autoConnectSources[sourceIndex], configured, 16); if (configuredLength == 0 || configuredLength >= 16) continue; wchar_t* end = nullptr; unsigned long value = wcstoul(configured, &end, 10); if (end && *end == L'\0' && value > 0) { autoConnectChannelID = value; break; } } if (autoConnectChannelID == 0) autoConnectChannelID = 11; if (category == 0 && (serverID == 0 || serverID == 1) && channelID == autoConnectChannelID) { g_autoConnectChannel = channel; LogLine("channel auto-connect candidate captured server=%u channel=%u object=%p target=%lu", serverID, channelID, channel, autoConnectChannelID); } return g_originalChannelCategoryInsert ? g_originalChannelCategoryInsert(category, channel) : 0; } bool StoreNativeHudState(uintptr_t descriptorRva, uintptr_t valueRva, uintptr_t lockRva, uintptr_t storeRva, int value) { AtomicSpinFn lock = reinterpret_cast( g_dnfBase + kAtomicSpinLockRva); AtomicSpinFn unlock = reinterpret_cast( g_dnfBase + kAtomicSpinUnlockRva); ObfuscatedStoreFn store = reinterpret_cast( g_dnfBase + storeRva); void* descriptor = reinterpret_cast(g_dnfBase + descriptorRva); int* encryptedValue = reinterpret_cast(g_dnfBase + valueRva); void* stateLock = reinterpret_cast(g_dnfBase + lockRva); bool stored = false; lock(stateLock); __try { store(descriptor, &value, encryptedValue); stored = true; } __finally { unlock(stateLock); } return stored; } bool CommitNativeHudChannelState(int serverID, int channelID) { if (serverID < 0 || channelID <= 0) { return false; } bool serverStored = StoreNativeHudState( kHudServerIndexDescriptorRva, kHudServerIndexValueRva, kHudServerIndexLockRva, kObfuscatedStoreCase4Rva, serverID); bool channelStored = StoreNativeHudState( kHudChannelIndexDescriptorRva, kHudChannelIndexValueRva, kHudChannelIndexLockRva, kObfuscatedStoreCase0Rva, channelID); LogLine("native channel HUD state committed server=%d channel=%d channel_store=%d server_store=%d", serverID, channelID, channelStored ? 1 : 0, serverStored ? 1 : 0); return channelStored && serverStored; } bool StoreNativeClockValue( uintptr_t descriptorRva, uintptr_t valueRva, uintptr_t storeRva, int value) { ObfuscatedStoreFn store = reinterpret_cast( g_dnfBase + storeRva); void* descriptor = reinterpret_cast(g_dnfBase + descriptorRva); int* encryptedValue = reinterpret_cast(g_dnfBase + valueRva); store(descriptor, &value, encryptedValue); return true; } bool CommitNativeServerClockState(int serverUnix) { __time32_t currentTime = _time32(nullptr); if (!g_dnfBase || currentTime <= 0 || serverUnix < 946684800) { LogLine("native server clock state skipped invalid unix time=%d", static_cast(currentTime)); return false; } typedef DWORD (WINAPI* TimeGetTimeFn)(); HMODULE winmm = GetModuleHandleW(L"winmm.dll"); TimeGetTimeFn timeGetTimeFn = winmm ? reinterpret_cast( GetProcAddress(winmm, "timeGetTime")) : nullptr; if (!timeGetTimeFn) { LogLine("premium server clock refused missing native timeGetTime"); return false; } DWORD tick = timeGetTimeFn(); bool serverStored = false; bool localStored = false; bool tickStored = false; __try { // Disk/current-EXE evidence: both native __thiscall stores have this // complete prologue. Check every relevant unhooked site before ANY // store. A version mismatch must not partially initialize the clock. static const unsigned char kStoreGuard[] = { 0x55,0x8B,0xEC,0xA1,0xB0,0x74,0x2F,0x05,0x40,0x8B,0xD0,0xC1,0xEA,0x08,0x0F,0xB6,0xD2 }; static const unsigned char kClockGuard[] = { 0x55,0x8B,0xEC,0x51,0xB8,0x30,0x0F,0x1B,0x05,0x83,0xE0,0x0F,0x83,0xF8,0x0C,0x77,0x62 }; static const unsigned char kSubtractGuard[] = { 0xE8,0x87,0xE0,0x20,0x00,0x2B,0xF0 }; static const unsigned char kCopyGuard[] = { 0x8D,0x7B,0x17,0xB9,0x12,0,0,0,0xF3,0xA5,0x66,0xA5 }; static const unsigned char kRegistrationGuard[] = { 0x6A,0,0x68,0xD0,0xE1,0xCF,0x01,0x68,0x87,0x03,0,0,0x8B,0xCE,0xE8,0x54,0x95,0x55,0 }; if (!BytesMatch(reinterpret_cast(g_dnfBase + kObfuscatedStoreCase0Rva), kStoreGuard, sizeof(kStoreGuard)) || !BytesMatch(reinterpret_cast(g_dnfBase + kObfuscatedStoreCase8Rva), kStoreGuard, sizeof(kStoreGuard)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01E76FE0), kClockGuard, sizeof(kClockGuard)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01C68F54), kSubtractGuard, sizeof(kSubtractGuard)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01C6944B), kCopyGuard, sizeof(kCopyGuard)) || !BytesMatch(reinterpret_cast(g_dnfBase + 0x01909419), kRegistrationGuard, sizeof(kRegistrationGuard))) { LogLine("premium server clock refused native signature mismatch"); return false; } // Current NoPack sub_1D70130 initializes these three obfuscated // values from CHANNELINFO field 11, _time32, and timeGetTime. The // marked op903 restores exactly those anchors, without replaying // CHANNELINFO or touching native channel/scene/HUD state. Server time // is authoritative; local wall time is only the local anchor. serverStored = StoreNativeClockValue( kServerClockDescriptorRva, kServerClockValueRva, kObfuscatedStoreCase0Rva, serverUnix); localStored = StoreNativeClockValue( kLocalClockDescriptorRva, kLocalClockValueRva, kObfuscatedStoreCase8Rva, static_cast(currentTime)); tickStored = StoreNativeClockValue( kServerTickDescriptorRva, kServerTickValueRva, kObfuscatedStoreCase0Rva, static_cast(tick)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("native server clock state failed with structured exception"); return false; } LogLine("native server clock state committed server_unix=%d local_unix=%d tick=%u server_store=%d local_store=%d tick_store=%d", serverUnix, static_cast(currentTime), static_cast(tick), serverStored ? 1 : 0, localStored ? 1 : 0, tickStored ? 1 : 0); return serverStored && localStored && tickStored; } bool CommitResidentChannelSnapshot(unsigned short connectedPort) { if (!g_gameWideAssign) { LogLine("channel resident snapshot skipped: wide-string assign is unavailable"); return false; } __try { void* selected = *reinterpret_cast(g_dnfBase + kSelectedChannelPointerRva); if (!selected) { LogLine("channel resident snapshot skipped: selected channel is null"); return false; } uintptr_t* vtable = *reinterpret_cast(selected); ChannelObjectPredicateFn predicateA = reinterpret_cast(vtable[20 / sizeof(uintptr_t)]); ChannelObjectPredicateFn predicateB = reinterpret_cast(vtable[24 / sizeof(uintptr_t)]); bool predicateAResult = predicateA(selected); bool predicateBResult = predicateB(selected); ChannelObjectNameFn getName = reinterpret_cast(vtable[68 / sizeof(uintptr_t)]); ChannelObjectIntFn getServerID = reinterpret_cast(vtable[76 / sizeof(uintptr_t)]); ChannelObjectIntFn getAddress = reinterpret_cast(vtable[128 / sizeof(uintptr_t)]); ChannelObjectPortFn getPort = reinterpret_cast(vtable[132 / sizeof(uintptr_t)]); const wchar_t* name = getName(selected); if (!name) { LogLine("channel resident snapshot skipped: selected=%p name is null", selected); return false; } size_t nameLength = wcsnlen(name, 256); if (nameLength == 256) { LogLine("channel resident snapshot skipped: selected=%p name is unterminated", selected); return false; } int serverID = getServerID(selected); int address = getAddress(selected); unsigned short port = getPort(selected); int channelID = *reinterpret_cast( static_cast(selected) + 0x70); unsigned int expectedPort = channelID > 0 ? static_cast(kGamePortBase) + static_cast(channelID) : 0; if (channelID <= 0 || expectedPort > 0xFFFF || port != static_cast(expectedPort) || (connectedPort != 0 && port != connectedPort)) { LogLine("channel resident snapshot skipped: selected=%p channel=%d port=%u expected_port=%u connected_port=%u predicate_a=%d predicate_b=%d", selected, channelID, static_cast(port), expectedPort, static_cast(connectedPort), predicateAResult ? 1 : 0, predicateBResult ? 1 : 0); return false; } void* resident = reinterpret_cast(g_dnfBase + kResidentChannelRva); g_gameWideAssign(resident, name, nameLength); *reinterpret_cast(static_cast(resident) + 32) = address; *reinterpret_cast(static_cast(resident) + 36) = static_cast(port); if (!CommitNativeHudChannelState(serverID, channelID)) { LogLine("channel resident snapshot skipped HUD commit server=%d channel=%d", serverID, channelID); } LogLine("channel resident snapshot committed selected=%p server=%d channel=%d name_length=%u address=0x%08X port=%u predicate_a=%d predicate_b=%d", selected, serverID, channelID, static_cast(nameLength), static_cast(address), static_cast(port), predicateAResult ? 1 : 0, predicateBResult ? 1 : 0); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("channel resident snapshot failed with structured exception"); return false; } } bool __fastcall ProxyChannelConnect(void* self, void* /*unused*/, int address, unsigned short port) { unsigned short targetPort = port; unsigned long channelID = 11; const char* configurationSource = "default"; wchar_t configured[16] = { 0 }; DWORD length = GetEnvironmentVariableW(L"DNF_RESIDENT_CHANNEL_ID", configured, 16); if (length > 0 && length < 16) { wchar_t* end = nullptr; unsigned long configuredChannelID = wcstoul(configured, &end, 10); if (end && *end == L'\0') { channelID = configuredChannelID; configurationSource = channelID > 0 ? "environment" : "disabled"; } else { LogLine("channel bootstrap invalid configuration; using default channel=%lu", channelID); } } if (port == kCrackBootstrapPort && channelID > 0) { unsigned long candidatePort = static_cast(kGamePortBase) + channelID; if (candidatePort <= 0xFFFF) { targetPort = static_cast(candidatePort); LogLine("channel bootstrap redirect source=%s source_port=%u target_channel=%lu target_port=%u", configurationSource, static_cast(port), channelID, static_cast(targetPort)); } } bool result = g_originalChannelConnect ? g_originalChannelConnect(self, address, targetPort) : false; LogLine("channel connect address=0x%08X requested_port=%u effective_port=%u result=%d", static_cast(address), static_cast(port), static_cast(targetPort), result ? 1 : 0); return result; } void LogFirstLoginNativeChainSnapshot( const char* phase, LONG generation, void* channel, void* connector, int connectResult) { void* selectedChannel = nullptr; void* resident = nullptr; void* gameTransport = nullptr; uint32_t socketHandle = 0xFFFFFFFFu; bool residentInitialized = false; bool captureOK = false; __try { selectedChannel = *reinterpret_cast( g_dnfBase + kSelectedChannelPointerRva); GameTransportSingletonFn getGameTransport = reinterpret_cast( g_dnfBase + kGameTransportSingletonRva); // sub_18A5110 lazily constructs the resident object. Diagnostics must // not call that getter before ChannelConnect and accidentally change // the lifecycle being observed. The current-EXE object has a stable // address; its one-time-init flag is at object+0x48. resident = reinterpret_cast(g_dnfBase + kResidentChannelRva); residentInitialized = (*reinterpret_cast( reinterpret_cast(resident) + 0x48) & 1) != 0; gameTransport = getGameTransport(); if (gameTransport) { socketHandle = *reinterpret_cast( reinterpret_cast(gameTransport) + kGameTransportSocketHandleOffset); } captureOK = true; } __except (EXCEPTION_EXECUTE_HANDLER) { captureOK = false; } LogLine( "first-login native chain phase=%s generation=%ld channel=%p " "selected_channel=%p connector=%p resident=%p game_transport=%p " "resident_initialized=%d socket=0x%08X connect_result=%d capture_ok=%d", phase ? phase : "unknown", generation, channel, selectedChannel, connector, resident, gameTransport, residentInitialized ? 1 : 0, static_cast(socketHandle), connectResult, captureOK ? 1 : 0); } bool ResetStaleFirstLoginNativeConnection(LONG generation) { bool reset = false; __try { ChannelEndpointStateResetFn resetEndpoint = reinterpret_cast( g_dnfBase + kNativeEndpointStateResetRva); ChannelResidentSingletonFn getResident = reinterpret_cast( g_dnfBase + kResidentConnectionSingletonRva); ChannelResidentResetFn resetResident = reinterpret_cast( g_dnfBase + kResidentConnectionResetRva); GameTransportSingletonFn getGameTransport = reinterpret_cast( g_dnfBase + kGameTransportSingletonRva); GameTransportSocketCloseFn closeGameTransportSocket = reinterpret_cast( g_dnfBase + kGameTransportSocketCloseRva); // Keep the proven current-EXE logical cleanup first: clear endpoint // state and release the resident selected-connection ownership. Live // evidence on 2026-09-19 proved that sub_18A5180 can return success // while the old 10251 TCP socket remains open for another ~11 seconds, // causing every following ChannelConnect to be rejected before a new // socket is created. resetEndpoint(); void* resident = getResident(); bool residentReset = resident && resetResident(resident); // sub_3454640 is the exact game transport object used by the successful // ChannelConnect path. sub_35BF4A0 is its native idempotent socket // close/reset method; do not call it unless the resident cleanup has // already succeeded and the transport still owns a real socket. void* gameTransport = nullptr; uint32_t socketBefore = 0xFFFFFFFFu; uint32_t socketAfter = 0xFFFFFFFFu; bool socketCloseAttempted = false; bool socketCloseResult = false; if (residentReset) { gameTransport = getGameTransport(); if (gameTransport) { uintptr_t transportBase = reinterpret_cast(gameTransport); socketBefore = *reinterpret_cast( transportBase + kGameTransportSocketHandleOffset); bool socketPresent = socketBefore != 0xFFFFFFFFu; if (dnf90_first_login_recovery::ShouldForceReleaseStaleSocket( residentReset, socketPresent)) { socketCloseAttempted = true; socketCloseResult = closeGameTransportSocket(gameTransport); } socketAfter = *reinterpret_cast( transportBase + kGameTransportSocketHandleOffset); } } bool socketReleased = !socketCloseAttempted || socketCloseResult || socketAfter == 0xFFFFFFFFu; reset = residentReset && socketReleased; LogLine( "first-login stale native reset generation=%ld resident=%p " "resident_reset=%d game_transport=%p socket_before=0x%08X " "socket_close_attempted=%d socket_close_result=%d " "socket_after=0x%08X result=%d", generation, resident, residentReset ? 1 : 0, gameTransport, static_cast(socketBefore), socketCloseAttempted ? 1 : 0, socketCloseResult ? 1 : 0, static_cast(socketAfter), reset ? 1 : 0); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("first-login stale native reset generation=%ld structured exception", generation); } return reset; } bool ClaimFirstLoginRecoveryAttempt(LONG* claimedCount) { LONG current = InterlockedCompareExchange( &g_firstLoginRecoveryCount, 0, 0); while (current < dnf90_first_login_recovery::kMaxFirstLoginRecoveryAttempts) { LONG next = current + 1; LONG observed = InterlockedCompareExchange( &g_firstLoginRecoveryCount, next, current); if (observed == current) { if (claimedCount) *claimedCount = next; return true; } current = observed; } return false; } VOID CALLBACK FirstLoginWatchdogTimerProc(HWND, UINT, UINT_PTR timerID, DWORD) { KillTimer(nullptr, timerID); if (g_firstLoginWatchdogTimer != timerID) { LogLine("first-login watchdog ignored stale timer=%p active_timer=%p", reinterpret_cast(timerID), reinterpret_cast(g_firstLoginWatchdogTimer)); return; } g_firstLoginWatchdogTimer = 0; LONG wasActive = InterlockedExchange(&g_firstLoginWatchdogActive, 0); LONG watchdogGeneration = InterlockedCompareExchange(&g_firstLoginWatchdogGeneration, 0, 0); LONG currentGeneration = InterlockedCompareExchange(&g_autoConnectDirectoryGeneration, 0, 0); bool serverSeen = InterlockedCompareExchange(&g_firstLoginServerDataSeen, 0, 0) != 0; bool clientSeen = InterlockedCompareExchange(&g_firstLoginClientDataSeen, 0, 0) != 0; LONG recoveryCount = InterlockedCompareExchange(&g_firstLoginRecoveryCount, 0, 0); dnf90_first_login_recovery::WatchdogState state = { watchdogGeneration, currentGeneration, wasActive != 0, serverSeen, clientSeen, recoveryCount, }; bool recover = dnf90_first_login_recovery::ShouldRecover(state); LogLine("first-login watchdog complete generation=%ld current_generation=%ld server_seen=%d client_seen=%d recovery_count=%ld recover=%d", watchdogGeneration, currentGeneration, serverSeen ? 1 : 0, clientSeen ? 1 : 0, recoveryCount, recover ? 1 : 0); LONG claimedCount = 0; if (!recover || !ClaimFirstLoginRecoveryAttempt(&claimedCount)) { return; } LogLine("first-login recovery claimed generation=%ld recovery_count=%ld source=watchdog", watchdogGeneration, claimedCount); // Do not issue a second raw connect here. The native reset path reapplies // the directory, and the existing generation-bound auto-connect logic then // retries against the newly-created channel object. ResetStaleFirstLoginNativeConnection(watchdogGeneration); } bool BeginFirstLoginWatchdogCandidate(LONG generation) { if (generation <= 0 || InterlockedCompareExchange(&g_firstLoginInitialSuccessGeneration, 0, 0) != 0 || InterlockedCompareExchange(&g_firstLoginRecoveryCount, 0, 0) != 0) { return false; } InterlockedExchange(&g_firstLoginServerDataSeen, 0); InterlockedExchange(&g_firstLoginClientDataSeen, 0); InterlockedExchange(&g_firstLoginServerProgressLogged, 0); InterlockedExchange(&g_firstLoginClientProgressLogged, 0); InterlockedExchange(&g_firstLoginWatchdogGeneration, generation); InterlockedExchange(&g_firstLoginWatchdogActive, 1); return true; } void FinishFirstLoginWatchdogCandidate(LONG generation, bool candidate, bool connected) { if (!candidate) return; if (!connected) { InterlockedExchange(&g_firstLoginWatchdogActive, 0); return; } if (InterlockedCompareExchange( &g_firstLoginInitialSuccessGeneration, generation, 0) != 0) { InterlockedExchange(&g_firstLoginWatchdogActive, 0); return; } constexpr UINT kWatchdogDelayMs = 5500; UINT_PTR timerID = SetTimer(nullptr, 0, kWatchdogDelayMs, FirstLoginWatchdogTimerProc); if (!timerID) { InterlockedExchange(&g_firstLoginWatchdogActive, 0); LogLine("first-login watchdog timer failed generation=%ld error=%lu", generation, GetLastError()); return; } g_firstLoginWatchdogTimer = timerID; LogLine("first-login watchdog armed generation=%ld delay_ms=%u timer=%p", generation, static_cast(kWatchdogDelayMs), reinterpret_cast(timerID)); } dnf90_first_login_recovery::PacketProcessorReadiness ReadPacketProcessorReadiness( void** processorOut) { using dnf90_first_login_recovery::PacketProcessorReadiness; *processorOut = nullptr; __try { uintptr_t processor = *reinterpret_cast( g_dnfBase + kPacketProcessorPointerRva); *processorOut = reinterpret_cast(processor); if (!processor) return PacketProcessorReadiness::NotReady; return *reinterpret_cast( processor + kPacketProcessorReadyOffset) != 0 ? PacketProcessorReadiness::Ready : PacketProcessorReadiness::NotReady; } __except (EXCEPTION_EXECUTE_HANDLER) { return PacketProcessorReadiness::ReadFailed; } } VOID CALLBACK AutoConnectChannelTimerProc(HWND, UINT, UINT_PTR timerID, DWORD) { KillTimer(nullptr, timerID); if (g_autoConnectTimer != timerID) { LogLine("channel delayed auto-connect ignored stale timer=%p active_timer=%p", reinterpret_cast(timerID), reinterpret_cast(g_autoConnectTimer)); return; } g_autoConnectTimer = 0; LONG generation = InterlockedCompareExchange(&g_autoConnectDirectoryGeneration, 0, 0); void* processor = nullptr; auto readiness = ReadPacketProcessorReadiness(&processor); ULONGLONG now = GetTickCount64(); bool alreadyWaited = g_autoConnectReadiness.waited; auto action = g_autoConnectReadiness.Poll(generation, now, readiness); using dnf90_first_login_recovery::AutoConnectAction; if (action == AutoConnectAction::Ignore) return; if (action == AutoConnectAction::Wait) { if (!alreadyWaited) { LogLine("channel auto-connect waiting for packet processor generation=%ld processor=%p elapsed_ms=%llu", generation, processor, now - g_autoConnectReadiness.startedAt); } g_autoConnectTimer = SetTimer(nullptr, 0, dnf90_first_login_recovery::kPacketProcessorPollMs, AutoConnectChannelTimerProc); if (!g_autoConnectTimer) { g_autoConnectReadiness.Cancel(); InterlockedExchange(&g_autoConnectAttempted, 0); LogLine("channel auto-connect readiness timer failed generation=%ld error=%lu", generation, GetLastError()); } return; } if (action != AutoConnectAction::Connect) { InterlockedExchange(&g_autoConnectAttempted, 0); LogLine("channel auto-connect readiness stopped generation=%ld processor=%p elapsed_ms=%llu read_failed=%d", generation, processor, now - g_autoConnectReadiness.startedAt, action == AutoConnectAction::ReadFailed ? 1 : 0); return; } LogLine("channel auto-connect packet processor ready generation=%ld processor=%p waited=%d elapsed_ms=%llu", generation, processor, alreadyWaited ? 1 : 0, now - g_autoConnectReadiness.startedAt); bool connected = false; __try { void* channel = g_autoConnectChannel; if (!channel || !g_originalChannelConnect) { InterlockedExchange(&g_autoConnectAttempted, 0); return; } uintptr_t* vtable = *reinterpret_cast(channel); ChannelObjectIntFn getAddress = reinterpret_cast( vtable[128 / sizeof(uintptr_t)]); ChannelObjectPortFn getPort = reinterpret_cast( vtable[132 / sizeof(uintptr_t)]); int address = getAddress(channel); unsigned short port = getPort(channel); *reinterpret_cast(g_dnfBase + kSelectedChannelPointerRva) = channel; ChannelConnectorFn getConnector = reinterpret_cast( g_dnfBase + kChannelConnectorRva); void* connector = getConnector(); bool watchdogCandidate = BeginFirstLoginWatchdogCandidate(generation); LogFirstLoginNativeChainSnapshot( "before_connect", generation, channel, connector, -1); connected = connector && ProxyChannelConnect(connector, nullptr, address, port); LogFirstLoginNativeChainSnapshot( "after_connect", generation, channel, connector, connected ? 1 : 0); FinishFirstLoginWatchdogCandidate(generation, watchdogCandidate, connected); LONG initialSuccessGeneration = InterlockedCompareExchange(&g_firstLoginInitialSuccessGeneration, 0, 0); bool clientSeen = InterlockedCompareExchange(&g_firstLoginClientDataSeen, 0, 0) != 0; LONG recoveryCount = InterlockedCompareExchange(&g_firstLoginRecoveryCount, 0, 0); bool recoverRejectedRetry = dnf90_first_login_recovery::ShouldRecoverAfterRejectedRetry( initialSuccessGeneration, generation, clientSeen, recoveryCount, connected); LONG claimedCount = 0; if (recoverRejectedRetry && ClaimFirstLoginRecoveryAttempt(&claimedCount)) { LogLine("first-login rejected retry recovery generation=%ld initial_generation=%ld client_seen=%d recovery_count=%ld", generation, initialSuccessGeneration, clientSeen ? 1 : 0, claimedCount); ResetStaleFirstLoginNativeConnection(generation); } LogLine("channel delayed auto-connect invoked object=%p connector=%p address=0x%08X port=%u generation=%ld result=%d", channel, connector, static_cast(address), static_cast(port), generation, connected ? 1 : 0); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("channel delayed auto-connect failed with structured exception"); } if (!connected) { InterlockedExchange(&g_autoConnectAttempted, 0); } } bool __cdecl ProxyChannelDirectoryApply() { // The native connect call can return success before the login handshake // actually starts. When that handshake stalls, the client downloads and // applies the directory again. Treat each apply as a new retry boundary // and bind the timer to the newly-created channel object. LONG generation = InterlockedIncrement(&g_autoConnectDirectoryGeneration); g_autoConnectReadiness.Cancel(); UINT_PTR pendingTimer = g_autoConnectTimer; if (pendingTimer) { KillTimer(nullptr, pendingTimer); g_autoConnectTimer = 0; LogLine("channel directory apply canceled pending auto-connect generation=%ld timer=%p", generation, reinterpret_cast(pendingTimer)); } g_autoConnectChannel = nullptr; InterlockedExchange(&g_autoConnectAttempted, 0); LogLine("channel directory apply enter generation=%ld", generation); bool result = g_originalChannelDirectoryApply ? g_originalChannelDirectoryApply() : false; LogLine("channel directory apply return generation=%ld result=%d", generation, result ? 1 : 0); if (!result || !g_autoConnectChannel || !g_originalChannelConnect || InterlockedCompareExchange(&g_autoConnectAttempted, 1, 0) != 0) { return result; } g_autoConnectReadiness.Begin(generation, GetTickCount64()); UINT_PTR timerID = SetTimer(nullptr, 0, 1150, AutoConnectChannelTimerProc); if (!timerID) { g_autoConnectReadiness.Cancel(); LogLine("channel delayed auto-connect timer failed error=%lu", GetLastError()); InterlockedExchange(&g_autoConnectAttempted, 0); } else { g_autoConnectTimer = timerID; LogLine("channel delayed auto-connect scheduled generation=%ld delay_ms=1150 timer=%p", generation, reinterpret_cast(timerID)); } return result; } bool IsPvpChannelQueryCall(uintptr_t callerRva, unsigned int requestedType) { return requestedType == kPvpDefaultListQueryType && (callerRva == kPvpCategoryQueryReturnRva || callerRva == kPvpDefaultListQueryReturnRva || callerRva == kPvpSelectionQueryReturnRva); } unsigned int* __stdcall ProxyChannelQuery(unsigned int* output, int type) { uintptr_t callerRva = reinterpret_cast(_ReturnAddress()) - g_dnfBase; unsigned int requestedType = static_cast(type); // Native type 13 is INTEGRATED_FREEPVP. Type 40 is CHANNEL_BEAST and // reconnects into the ordinary town. Preserve the native query identity; // the server now advertises a dedicated type-13 free-duel listener. bool remapped = false; int queryType = type; unsigned int* result = g_originalChannelQuery ? g_originalChannelQuery(output, queryType) : output; if (callerRva != kPvpCategoryQueryReturnRva && callerRva != kPvpDefaultListQueryReturnRva && callerRva != kPvpSelectionQueryReturnRva) { return result; } unsigned int count = 0; __try { if (output && output[2] >= output[1]) { count = (output[2] - output[1]) / sizeof(void*); } } __except (EXCEPTION_EXECUTE_HANDLER) { count = 0xFFFFFFFFu; } LogLine("pvp channel query completed caller_rva=0x%08X requested_type=%u effective_type=%u remapped=%d count=%u output=%p", callerRva, requestedType, queryType, remapped ? 1 : 0, count, output); return result; } bool InstallChannelUiCompatibility() { static const unsigned char kSehPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kCategoryQueryCall[] = { 0xE8, 0xA1, 0xFA, 0x9F, 0xFF }; static const unsigned char kDefaultListQueryCall[] = { 0xE8, 0x53, 0x37, 0xA0, 0xFF }; static const unsigned char kSelectionQueryCall[] = { 0xE8, 0xF1, 0x36, 0xA0, 0xFF }; uintptr_t queryTarget = g_dnfBase + kPvpChannelQueryRva; if (!BytesMatch(reinterpret_cast(queryTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast( g_dnfBase + kPvpCategoryQueryReturnRva - 5), kCategoryQueryCall, sizeof(kCategoryQueryCall)) || !BytesMatch(reinterpret_cast( g_dnfBase + kPvpDefaultListQueryReturnRva - 5), kDefaultListQueryCall, sizeof(kDefaultListQueryCall)) || !BytesMatch(reinterpret_cast( g_dnfBase + kPvpSelectionQueryReturnRva - 5), kSelectionQueryCall, sizeof(kSelectionQueryCall))) { LogLine("channel UI compatibility target verification failed query=0x%08X", queryTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook(queryTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelQuery), &original, "pvp channel query observation")) { return false; } g_originalChannelQuery = reinterpret_cast(original); LogLine("channel UI compatibility installed query=0x%08X pvp_call_sites=3 category_return_rva=0x%08X list_return_rva=0x%08X selection_return_rva=0x%08X requested_type=%u effective_type=%u query_type_width=32", queryTarget, kPvpCategoryQueryReturnRva, kPvpDefaultListQueryReturnRva, kPvpSelectionQueryReturnRva, kPvpDefaultListQueryType, kPvpResidentQueryType); return true; } bool InstallChannelDiagnostic() { unsigned char packetProcessorLoad[] = { 0xA1, 0, 0, 0, 0 }; uintptr_t packetProcessorPointer = g_dnfBase + kPacketProcessorPointerRva; memcpy(packetProcessorLoad + 1, &packetProcessorPointer, sizeof(packetProcessorPointer)); static const unsigned char kPacketProcessorReadyGate[] = { 0x80, 0x7E, 0x04, 0x00 }; if (!BytesMatch(reinterpret_cast(g_dnfBase + kPacketProcessorPointerLoadRva), packetProcessorLoad, sizeof(packetProcessorLoad)) || !BytesMatch(reinterpret_cast(g_dnfBase + kPacketProcessorReadyGateRva), kPacketProcessorReadyGate, sizeof(kPacketProcessorReadyGate))) { LogLine("channel packet processor readiness target verification failed"); return false; } static const unsigned char kSehPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kGameTransportSingletonLoad[] = { 0xA1 }; static const unsigned char kGameTransportSingletonReturn[] = { 0xC3 }; static const unsigned char kGameTransportSocketClosePrologue[] = { 0x56, 0x8B, 0xF1, 0x8B, 0x86, 0xC4, 0x01, 0x00, 0x00, 0xC6, 0x46, 0x08, 0x00, 0x83, 0xF8, 0xFF }; uintptr_t downloadTarget = g_dnfBase + kChannelScriptDownloadRva; uintptr_t loadTarget = g_dnfBase + kChannelScriptLoadRva; uintptr_t directoryApplyTarget = g_dnfBase + kChannelDirectoryApplyRva; uintptr_t runtimeLoadTarget = g_dnfBase + kChannelRuntimeLoadRva; uintptr_t categoryInsertTarget = g_dnfBase + kChannelCategoryInsertRva; uintptr_t connectTarget = g_dnfBase + kChannelConnectRva; uintptr_t gameTransportSingletonTarget = g_dnfBase + kGameTransportSingletonRva; uintptr_t gameTransportSocketCloseTarget = g_dnfBase + kGameTransportSocketCloseRva; if (!BytesMatch(reinterpret_cast(downloadTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(loadTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(directoryApplyTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(runtimeLoadTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(categoryInsertTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(connectTarget), kSehPrologue, sizeof(kSehPrologue)) || !BytesMatch(reinterpret_cast(gameTransportSingletonTarget), kGameTransportSingletonLoad, sizeof(kGameTransportSingletonLoad)) || !BytesMatch(reinterpret_cast(gameTransportSingletonTarget + 5), kGameTransportSingletonReturn, sizeof(kGameTransportSingletonReturn)) || !BytesMatch(reinterpret_cast(gameTransportSocketCloseTarget), kGameTransportSocketClosePrologue, sizeof(kGameTransportSocketClosePrologue))) { LogLine( "channel diagnostic target verification failed download=0x%08X " "load=0x%08X apply=0x%08X runtime=0x%08X category=0x%08X " "connect=0x%08X game_transport=0x%08X game_socket_close=0x%08X", downloadTarget, loadTarget, directoryApplyTarget, runtimeLoadTarget, categoryInsertTarget, connectTarget, gameTransportSingletonTarget, gameTransportSocketCloseTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook(connectTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelConnect), &original, "channel connect")) return false; g_originalChannelConnect = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(runtimeLoadTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelRuntimeLoad), &original, "channel runtime load")) return false; g_originalChannelRuntimeLoad = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(categoryInsertTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelCategoryInsert), &original, "channel category insert")) return false; g_originalChannelCategoryInsert = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(directoryApplyTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelDirectoryApply), &original, "channel directory apply")) return false; g_originalChannelDirectoryApply = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(loadTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelScriptLoad), &original, "channel script load")) return false; g_originalChannelScriptLoad = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(downloadTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyChannelScriptDownload), &original, "channel script download")) return false; g_originalChannelScriptDownload = reinterpret_cast(original); LogLine( "first-login diagnostic telemetry version=3 native_chain=1 " "decoded_progress=1 raw_receive=%ld max_recoveries=%ld", InterlockedCompareExchange(&g_firstLoginRawProbeInstalled, 0, 0), dnf90_first_login_recovery::kMaxFirstLoginRecoveryAttempts); return true; } bool InstallTclsCompatibility() { static const unsigned char kSehPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kFetchTextPrologue[] = { 0x55, 0x8B, 0xEC, 0x81, 0xEC, 0x08, 0x01, 0x00, 0x00 }; static const unsigned char kFetchTailPrologue[] = { 0x55, 0x8B, 0xEC, 0x81, 0xEC, 0x8C, 0x04, 0x00, 0x00 }; uintptr_t parseTarget = g_dnfBase + kTclsParseRva; uintptr_t loginTarget = g_dnfBase + kTclsFetchLoginRva; uintptr_t textTarget = g_dnfBase + kTclsFetchTextRva; uintptr_t cryptoTarget = g_dnfBase + kTclsFetchCryptoRva; uintptr_t fetchTailTarget = g_dnfBase + kTclsFetchTailRva; uintptr_t tailTarget = g_dnfBase + kTclsTailRva; g_gameWideAssign = reinterpret_cast(g_dnfBase + kGameWideAssignRva); // Validate the complete set before changing any entry point. These are // current-EXE local launch helpers, never send/recv or packet parsers. // // This installer is now called at the very start of the worker, before the // self-CRC and the 22 MB movie SHA-256. The packed EXE may not have // restored these prologues yet at that point, so wait briefly instead of // failing once. Waiting here is still far earlier than the old order: // the native TCLS parse runs at roughly +190 ms on the affected machine, // while the previous order did not reach this code until +194 ms. constexpr int kTargetWaitMs = 2000; int waitedMs = 0; for (;;) { const bool ready = BytesMatch(reinterpret_cast(parseTarget), kSehPrologue, sizeof(kSehPrologue)) && BytesMatch(reinterpret_cast(loginTarget), kSehPrologue, sizeof(kSehPrologue)) && BytesMatch(reinterpret_cast(textTarget), kFetchTextPrologue, sizeof(kFetchTextPrologue)) && BytesMatch(reinterpret_cast(cryptoTarget), kSehPrologue, sizeof(kSehPrologue)) && BytesMatch(reinterpret_cast(fetchTailTarget), kFetchTailPrologue, sizeof(kFetchTailPrologue)) && BytesMatch(reinterpret_cast(tailTarget), kSehPrologue, sizeof(kSehPrologue)); if (ready) break; if (waitedMs >= kTargetWaitMs) { LogLine("TCLS current-EXE target verification failed after %d ms; no TCLS hook installed", waitedMs); return false; } Sleep(1); ++waitedMs; } if (waitedMs > 0) { LogLine("TCLS targets ready after %d ms", waitedMs); } void* original = nullptr; if (!InstallKnownInlineHook(loginTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyTclsFetchLogin), &original, "tcls fetch login")) return false; g_originalTclsFetchLogin = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(textTarget, kFetchTextPrologue, sizeof(kFetchTextPrologue), reinterpret_cast(&ProxyTclsFetchText), &original, "tcls fetch text")) return false; g_originalTclsFetchText = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(cryptoTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyTclsFetchCrypto), &original, "tcls fetch crypto")) return false; g_originalTclsFetchCrypto = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(fetchTailTarget, kFetchTailPrologue, sizeof(kFetchTailPrologue), reinterpret_cast(&ProxyTclsFetchTail), &original, "tcls fetch tail")) return false; g_originalTclsFetchTail = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(tailTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyTclsTail), &original, "tcls tail")) return false; g_originalTclsTail = reinterpret_cast(original); // Parse is installed last because all fetches occur recursively inside // the original parse call and must already have valid trampolines. original = nullptr; if (!InstallKnownInlineHook(parseTarget, kSehPrologue, sizeof(kSehPrologue), reinterpret_cast(&ProxyTclsParse), &original, "tcls parse")) return false; g_originalTclsParse = reinterpret_cast(original); LogLine("TCLS local launch compatibility installed (six native helpers)"); return true; } bool InstallCodecSetKeyTrace() { static const unsigned char kSetKey0Prologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x49, 0x08 }; static const unsigned char kSetKey2Prologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x51, 0x08 }; static const unsigned char kSetKey3Prologue[] = { 0x55, 0x8B, 0xEC, 0x56, 0x8B, 0xF1 }; static const unsigned char kSetKey7Prologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kSetKey8Prologue[] = { 0x55, 0x8B, 0xEC, 0x56, 0x8B, 0xF1 }; uintptr_t target0 = g_dnfBase + kCodecSetKey0Rva; uintptr_t target2 = g_dnfBase + kCodecSetKey2Rva; uintptr_t target3 = g_dnfBase + kCodecSetKey3Rva; uintptr_t target7 = g_dnfBase + kCodecSetKey7Rva; uintptr_t target8 = g_dnfBase + kCodecSetKey8Rva; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(target0), kSetKey0Prologue, sizeof(kSetKey0Prologue)) && BytesMatch(reinterpret_cast(target2), kSetKey2Prologue, sizeof(kSetKey2Prologue)) && BytesMatch(reinterpret_cast(target3), kSetKey3Prologue, sizeof(kSetKey3Prologue)) && BytesMatch(reinterpret_cast(target7), kSetKey7Prologue, sizeof(kSetKey7Prologue)) && BytesMatch(reinterpret_cast(target8), kSetKey8Prologue, sizeof(kSetKey8Prologue))) { break; } Sleep(50); waitedMs += 50; } if (waitedMs >= kWaitLimitMs) { LogLine("codec setkey trace targets did not reach verified bytes; no setkey hooks installed"); return false; } void* original = nullptr; if (!InstallKnownInlineHook(target0, kSetKey0Prologue, sizeof(kSetKey0Prologue), reinterpret_cast(&ProxyCodecSetKey0), &original, "codec setkey idx0")) return false; g_originalCodecSetKey0 = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(target2, kSetKey2Prologue, sizeof(kSetKey2Prologue), reinterpret_cast(&ProxyCodecSetKey2), &original, "codec setkey idx2")) return false; g_originalCodecSetKey2 = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(target3, kSetKey3Prologue, sizeof(kSetKey3Prologue), reinterpret_cast(&ProxyCodecSetKey3), &original, "codec setkey idx3")) return false; g_originalCodecSetKey3 = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(target7, kSetKey7Prologue, sizeof(kSetKey7Prologue), reinterpret_cast(&ProxyCodecSetKey7), &original, "codec setkey idx7")) return false; g_originalCodecSetKey7 = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(target8, kSetKey8Prologue, sizeof(kSetKey8Prologue), reinterpret_cast(&ProxyCodecSetKey8), &original, "codec setkey idx8")) return false; g_originalCodecSetKey8 = reinterpret_cast(original); LogLine("codec setkey trace installed wait_ms=%d targets=0x%08X,0x%08X,0x%08X,0x%08X,0x%08X", waitedMs, target0, target2, target3, target7, target8); return true; } int __cdecl TryUpperBodyEncodeBypass(int messageType, char* input, int bodyLength, char* output, int* outLength) { LONG callNumber = InterlockedIncrement(&g_cipherCallCount); int capacity = 0; __try { capacity = outLength ? *outLength : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { capacity = 0; } int emitLength = bodyLength; if (messageType == 4) emitLength = 16; if (messageType != 4 && messageType != 5) { return 0; } if (!input || !output || !outLength || bodyLength <= 0 || bodyLength > 0x200 || capacity < emitLength) { if (callNumber <= 32) { LogLine("upper-body-encode-bypass-skip call=%ld msg=%d len=%d cap=%d emit=%d input=%p output=%p out_len=%p", callNumber, messageType, bodyLength, capacity, emitLength, input, output, outLength); } return 0; } __try { if (messageType == 4) { memset(output, 0, 16); int copyLength = bodyLength < 16 ? bodyLength : 16; memcpy(output, input, static_cast(copyLength)); } else { memcpy(output, input, static_cast(bodyLength)); } *outLength = emitLength; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("upper-body-encode-bypass exception call=%ld msg=%d len=%d code=0x%08X", callNumber, messageType, bodyLength, GetExceptionCode()); return 0; } if (callNumber <= 64) { char hex[360] = { 0 }; FormatHexBytes(output, emitLength, hex, sizeof(hex)); LogLine("upper-body-encode-bypass call=%ld msg=%d len=%d emit_len=%d cap=%d body=%s", callNumber, messageType, bodyLength, emitLength, capacity, hex); } return 1; } unsigned int __cdecl ReadSelectedQuestFromAutoCompleteCallback(void* callback) { unsigned int selector = 0; __try { if (callback) { selector = *reinterpret_cast( static_cast(callback) + 0x30); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("quest-auto-complete selected read exception callback=%p code=0x%08X", callback, GetExceptionCode()); return 0; } LONG callNumber = InterlockedIncrement(&g_questAutoCompleteCallCount); LogLine("quest-auto-complete selected call=%ld callback=%p selector=%u mode=%s", callNumber, callback, selector, selector ? "single" : "native-zero-fallback"); return selector; } // The current task window captures the selected row's exact quest id at // callback+0x30 in sub_304E070, but its confirmation callback discards that // value and calls sub_19EE300(mode=1, selector=-1), which serializes u32(0). // Rewrite only this verified call-site to the native single-row mode. When a // selector cannot be read, preserve the original zero-selector call so the // server's ambiguity guard fails closed without mutating any quest. __declspec(naked) void ProxyQuestAutoCompleteSelected() { __asm { push ecx push edx push edi call ReadSelectedQuestFromAutoCompleteCallback add esp, 4 pop edx pop ecx test eax, eax jz quest_auto_complete_fallback mov dword ptr [esp + 4], 0 mov dword ptr [esp + 8], eax quest_auto_complete_fallback: jmp dword ptr [g_questAutoCompleteSendFn] } } // Function-head proxy for sub_33AAFE0. This matches the current-EXE universal // upper-body codec boundary: the caller has already prepared the packet header // immediately before `input`. Copy the semantic plaintext body to the native // output buffer, repair the total length and checksum, and return success using // the native 5-stack-argument cleanup contract. This covers the whole // opcode%14 codec family without per-opcode filters or socket hooks. __declspec(naked) void ProxyCipherEncodeFunction() { __asm { // Preserve the native register/flags contract while recording the // plaintext semantic body before it is copied into the send buffer. pushfd pushad mov eax, [esp + 28h] // messageType mov ecx, [esp + 2Ch] // input mov edx, [esp + 30h] // bodyLength push edx push ecx push eax call LogClientToServerPacket add esp, 0Ch popad popfd push esi push edi push ebx // Entry stack before preserving registers: // [esp+04]=msg, [esp+08]=input, [esp+0C]=bodyLength, // [esp+10]=output, [esp+14]=outLength* mov ebx, [esp + 14h] // input after three pushes mov ecx, [esp + 18h] // bodyLength test ecx, ecx jle cipher_zero_case mov esi, ebx mov edi, [esp + 1Ch] // output rep movsb // memcpy(output, input, bodyLength) mov eax, [esp + 20h] // outLength* mov ecx, [esp + 18h] mov [eax], ecx lea edx, [ecx + 0Dh] mov [ebx - 0Ah], edx // packet total length = bodyLength + 13 lea eax, [ecx + 02h] push eax // crc length = bodyLength + sequence(2) lea eax, [ebx - 02h] push eax // crc begins at sequence call dword ptr [g_checksumFn] add esp, 08h mov [ebx - 06h], eax mov eax, 1 pop ebx pop edi pop esi ret 14h cipher_zero_case: mov eax, [esp + 20h] mov dword ptr [eax], 0 mov eax, 1 pop ebx pop edi pop esi ret 14h } } // Function-head proxy for sub_33AB0A0, the current-EXE S2C upper-body decode // boundary. The server-side chain is intentionally plaintext while this DLL // is active, so copy the received semantic body into the native output buffer // and report success using the same 5-stack-argument cleanup contract. No // packet headers, socket bytes, opcodes, or routing decisions are touched here. __declspec(naked) void ProxyCipherDecodeFunction() { __asm { // Log the exact plaintext body handed to the native S2C dispatcher. pushfd pushad mov eax, [esp + 28h] // messageType mov ecx, [esp + 2Ch] // input mov edx, [esp + 30h] // bodyLength push edx push ecx push eax call LogServerToClientPacket add esp, 0Ch popad popfd push esi push edi push ebx // Entry stack before preserving registers: // [esp+04]=msg, [esp+08]=input, [esp+0C]=bodyLength, // [esp+10]=output, [esp+14]=outLength* mov ebx, [esp + 14h] // input after three pushes mov ecx, [esp + 18h] // bodyLength test ecx, ecx jle cipher_decode_zero_case mov esi, ebx mov edi, [esp + 1Ch] // output rep movsb // memcpy(output, input, bodyLength) mov eax, [esp + 20h] // outLength* mov ecx, [esp + 18h] mov [eax], ecx mov eax, 1 pop ebx pop edi pop esi ret 14h cipher_decode_zero_case: mov eax, [esp + 20h] mov dword ptr [eax], 0 mov eax, 1 pop ebx pop edi pop esi ret 14h } } // DPROTO compatibility mode, outbound direction. // // This is entered after sub_34738E0 has finalized the ordinary inner packet. // The original function would call the mode-7 client protector and wrap the // result as op1517. We instead clone that finalized packet into the existing // output ownership contract, for every opcode. There is deliberately no // opcode comparison, allow list, network hook, or captured body involved. __declspec(naked) void ProxyDprotoSendDirect() { __asm { // Load the opcode only for log/probe parity with the historical PS1 // trampoline. Do not let it affect the cloned packet pointer. movzx eax, word ptr [esi + 1] // The native CRC call immediately before this entry is cdecl; discard // its two arguments exactly as the original path does at 0x3473924. add esp, 8 // EDI = finalized inner packet length; ESI = source inner packet. push edi call dword ptr [g_gameAllocatorFn] add esp, 4 test eax, eax jz dproto_oom // memcpy(destination=eax, source=esi, length=edi) push edi push esi push eax call dword ptr [g_gameMemcpyFn] add esp, 0Ch // Restore the sequence expected by upper_pkt_flush_send13 and // recompute the copied packet's checksum over sequence + body. mov dx, word ptr [ebp + 10h] mov word ptr [eax + 0Bh], dx push eax lea edx, [eax + 0Bh] mov ecx, edi sub ecx, 0Bh push ecx push edx call dword ptr [g_checksumFn] add esp, 8 pop edx mov dword ptr [edx + 7], eax mov ecx, dword ptr [ebp + 14h] mov dword ptr [ecx], edx jmp dword ptr [g_dprotoOutgoingReturn] dproto_oom: mov edx, dword ptr [ebp + 14h] mov dword ptr [edx], 0 jmp dword ptr [g_dprotoOutgoingReturn] } } // Predicate-false DPROTO compatibility. The native branch at 0x34738FA skips // the TerSafe wrapper for some requests; only commands proven by the PS1 route // are cloned here. Every other opcode resumes the native false epilogue. __declspec(naked) void ProxyDprotoFalseSelective() { __asm { movzx eax, word ptr [esi + 1] cmp ax, 64 je dproto_false_clone cmp ax, 16 je dproto_false_clone cmp ax, 19 je dproto_false_clone cmp ax, 28 je dproto_false_clone cmp ax, 29 je dproto_false_clone cmp ax, 31 je dproto_false_clone cmp ax, 33 je dproto_false_clone cmp ax, 34 je dproto_false_clone cmp ax, 35 je dproto_false_clone cmp ax, 36 je dproto_false_clone cmp ax, 39 je dproto_false_clone cmp ax, 40 je dproto_false_clone cmp ax, 41 je dproto_false_clone cmp ax, 42 je dproto_false_clone cmp ax, 43 je dproto_false_clone cmp ax, 44 je dproto_false_clone cmp ax, 45 je dproto_false_clone cmp ax, 46 je dproto_false_clone cmp ax, 71 je dproto_false_clone cmp ax, 72 je dproto_false_clone cmp ax, 117 je dproto_false_clone cmp ax, 123 je dproto_false_clone cmp ax, 132 je dproto_false_clone cmp ax, 143 je dproto_false_clone cmp ax, 491 je dproto_false_clone cmp ax, 1437 je dproto_false_clone cmp ax, 1443 je dproto_false_clone cmp ax, 999 je dproto_false_clone cmp ax, 1000 je dproto_false_clone jmp dword ptr [g_dprotoFalseResume] dproto_false_clone: push edi mov edi, dword ptr [ebp + 0Ch] push edi call dword ptr [g_gameAllocatorFn] add esp, 4 test eax, eax jz dproto_false_oom // memcpy(destination=eax, source=[ebp+08h], length=edi) push edi mov edx, dword ptr [ebp + 08h] push edx push eax call dword ptr [g_gameMemcpyFn] add esp, 0Ch // False path packets store total length at +3, then sequence/CRC. mov dword ptr [eax + 03h], edi mov dx, word ptr [ebp + 10h] mov word ptr [eax + 0Bh], dx mov esi, eax lea edx, [edi - 0Bh] push edx lea edx, [esi + 0Bh] push edx call dword ptr [g_checksumFn] add esp, 8 mov dword ptr [esi + 07h], eax mov ecx, dword ptr [ebp + 14h] mov dword ptr [ecx], esi pop edi jmp dword ptr [g_dprotoFalseResume] dproto_false_oom: pop edi jmp dword ptr [g_dprotoFalseResume] } } bool __stdcall IsSceneRouteOpcodeAllowed(uint16_t opcode) { if (opcode == 173 || opcode == 390 || opcode == 391 || opcode == 454 || opcode == 455) { LogLine("settings-route-check opcode=%u", static_cast(opcode)); } switch (opcode) { // Login / character / scene bootstrap packets that are built by the Go // server with current-EXE bodies. case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: // Current EXE class1 handlers sub_1D11A80/sub_1D12480 own the town peer // request/response handshake used by the remote-player context menu. case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 18: // Current EXE class0/op19 installs the complete learned-skill trees and // quick slots. The same opcode's class1 handler owns MOVE_ITEMSPACE ACKs // and performs the native equipment/avatar/creature slot relocation. case 19: case 20: case 21: case 22: case 23: case 24: case 26: case 27: case 28: case 29: case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 48: case 53: case 63: case 64: case 66: case 80: case 81: // CmdPacketResetItemAttr - equipment grade-adjust result popup case 94: case 95: case 96: case 99: case 100: case 101: case 102: // Current NoPack sub_1D57AB0 consumes scene class0/op105 as the // creature-status table. Without route admission, slot26 relocation can // succeed through op19 while the creature UI remains null/uninitialized. case 105: // Current NoPack sub_20457F0 owns the activity descriptor and // sub_E713E0 owns event progress. Event 2347 lazily creates the cumulative // online-time UI from this ordered op108 -> op1206 pair. case 108: case 115: case 120: case 124: case 132: case 138: case 143: case 149: case 153: case 160: case 166: case 173: case 174: case 191: case 200: case 201: case 202: case 206: case 208: case 237: case 256: case 260: case 267: case 268: case 269: case 272: case 273: case 295: case 305: case 306: case 307: case 308: case 331: case 332: case 356: case 357: case 358: case 359: case 376: case 390: case 391: case 392: case 400: case 412: case 413: // Current client packet numbering is one earlier than the A21 reference // for character/hotkey option notifications. Opcode 454 is probed before // the bridge sends the persisted text table; opcode 455 remains admitted // for its unrelated native count-table notification. case 454: case 455: case 491: case 518: case 523: case 558: case 559: case 560: case 574: case 635: case 692: case 734: case 800: case 899: case 913: case 914: // CmdPacketAddEquipmentSocket — equipment socket open ACK // Weapon-effect rune success ACK. The native handler applies the selected // rune to the target weapon from the acknowledged request body; blocking // this opcode at the DPROTO route predicate leaves the server mutation // durable but prevents the client from updating its equipped item object. case 951: // CmdPacketAddEquipmentEffect case 985: case 993: case 994: case 997: case 1128: // CmdPacketUseRandomboxItemExpand — random-box ten-open result UI case 1206: case 1220: case 1307: case 1340: case 1346: case 1371: // DB4 story-digest state: class0/op1377, raw u32 last accepted level. // Its native reader 0x01455210 must run before scene finalization. case 1377: case 1378: case 1403: case 1425: case 1429: case 1430: case 1437: case 1443: return true; default: return false; } } // Current-scene inbound route compatibility. This is deliberately narrower // than ProxyDprotoAcceptDirect: proven scene/UI opcodes are admitted to the // direct upper reader, and every other opcode tail-calls the native predicate. // The opcode argument is at [esp+4], matching the live PS1 probe. __declspec(naked) void ProxySceneRouteAllowList() { __asm { movzx eax, word ptr [esp + 4] push ecx push eax call IsSceneRouteOpcodeAllowed pop ecx test al, al jnz scene_route_allow // Native fallback: routeObject = [ecx+0x0c], then tail-call vtable+0x40. mov ecx, dword ptr [ecx + 0Ch] mov eax, dword ptr [ecx] mov eax, dword ptr [eax + 40h] jmp eax scene_route_allow: mov eax, 1 ret 4 } } bool BytesMatch(const unsigned char* address, const unsigned char* expected, size_t count) { __try { for (size_t i = 0; i < count; ++i) { if (address[i] != expected[i]) return false; } return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } bool TryReadByte(const unsigned char* address, unsigned char* value) { __try { *value = *address; return true; } __except (EXCEPTION_EXECUTE_HANDLER) { *value = 0; return false; } } bool WriteCodePatch(uintptr_t target, const unsigned char* patch, size_t count, const char* label) { DWORD oldProtection = 0; if (!VirtualProtect(reinterpret_cast(target), count, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("%s VirtualProtect failed: %u", label, GetLastError()); return false; } memcpy(reinterpret_cast(target), patch, count); DWORD ignoredProtection = 0; VirtualProtect(reinterpret_cast(target), count, oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(target), count); return true; } bool InstallBlackDiamondWebButtonCompatibility() { // Current EXE 8a7af614: shop control +248B0 enters the membership web // flow at 016CD4FE, sets +2748C, then calls 016A2660 (op65 and +24164 // input lock). Skip this obsolete web branch before any state changes. // The separate recharge control +10D8 and op64 purchases are untouched. static const unsigned char expected[] = { 0x8B, 0x8E, 0xB0, 0x48, 0x02, 0x00, 0xE8, 0xB6, 0x48, 0xE4, 0x01, 0x84, 0xC0, 0x74, 0x30, 0x6A, 0x00, 0x6A, 0xFF, 0x6A, 0xFF, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0xFF, 0x68, 0x22, 0x12, 0x01, 0x00, 0xC6, 0x86, 0x8C, 0x74, 0x02, 0x00, 0x01 }; const uintptr_t target = g_dnfBase + 0x012CD4EF; if (!g_dnfBase || !BytesMatch(reinterpret_cast(target), expected, sizeof(expected))) { LogLine("black-diamond web button refused native signature mismatch"); return false; } const unsigned char skip[] = {0xEB, 0x30}; if (!WriteCodePatch(target + 13, skip, sizeof(skip), "black-diamond web button")) { return false; } LogLine("black-diamond web button disabled branch=+0x012CD4FC scope=membership-web-only"); return true; } bool InstallGuildCreateSinglePlayerCompatibility() { if (!g_dnfBase) return false; uintptr_t clickBranch = g_dnfBase + kGuildCreateClickMemberAuthJneRva; uintptr_t enableBranch = g_dnfBase + kGuildCreateEnableMemberAuthJneRva; static const unsigned char kClickExpected[] = {0x75, 0x2F}; static const unsigned char kEnableExpected[] = {0x75, 0x5A}; static const unsigned char kNop2[] = {0x90, 0x90}; if (!BytesMatch( reinterpret_cast(clickBranch), kClickExpected, sizeof(kClickExpected))) { LogLine( "guild-create single-player compatibility skipped reason=click-branch-mismatch " "target=0x%08X", static_cast(clickBranch)); return false; } if (!BytesMatch( reinterpret_cast(enableBranch), kEnableExpected, sizeof(kEnableExpected))) { LogLine( "guild-create single-player compatibility skipped reason=enable-branch-mismatch " "target=0x%08X", static_cast(enableBranch)); return false; } if (!WriteCodePatch( clickBranch, kNop2, sizeof(kNop2), "guild-create click member-auth gate")) { return false; } if (!WriteCodePatch( enableBranch, kNop2, sizeof(kNop2), "guild-create enable member-auth gate")) { WriteCodePatch( clickBranch, kClickExpected, sizeof(kClickExpected), "guild-create click member-auth rollback"); return false; } LogLine( "guild-create single-player compatibility installed " "click=0x%08X enable=0x%08X scope=remove-member-auth-only", static_cast(clickBranch), static_cast(enableBranch)); return true; } bool InstallPvpPrerequisiteQuestCompatibility() { // User-requested local PK policy: skip only the prerequisite-quest gate. // Native 01E96F50 checks level >= 50 first, then this decoded quest flag. // The rejected branch looks up quest 2670 and shows message 0x3AC2. // Reuse its existing success continuation, preserving later PK checks. constexpr uintptr_t kQuestFlagCheckRva = 0x01A9709E; constexpr uintptr_t kQuestPassedRva = 0x01A97141; static const unsigned char kQuestGateContext[] = { 0x80, 0x7D, 0x0B, 0x00, // cmp byte [ebp+0B], 0 0x0F, 0x85, 0x99, 0x00, 0x00, 0x00, // jne 01E97141 0x68, 0x6E, 0x0A, 0x00, 0x00 // push quest 2670 }; static const unsigned char kQuestPassedContext[] = { 0xE8, 0x2A, 0xB9, 0xE5, 0xFF // call 01CF2A70 }; static const unsigned char kSkipQuestGate[] = { 0xE9, 0x9A, 0x00, 0x00, 0x00, 0x90 // jmp 01E97141; nop }; uintptr_t context = g_dnfBase + kQuestFlagCheckRva; if (!BytesMatch(reinterpret_cast(context), kQuestGateContext, sizeof(kQuestGateContext)) || !BytesMatch(reinterpret_cast(g_dnfBase + kQuestPassedRva), kQuestPassedContext, sizeof(kQuestPassedContext))) { LogLine("pvp prerequisite quest compatibility target verification failed"); return false; } if (!WriteCodePatch(context + 4, kSkipQuestGate, sizeof(kSkipQuestGate), "pvp prerequisite quest compatibility")) { return false; } LogLine("pvp prerequisite quest compatibility installed branch_rva=0x%08X continue_rva=0x%08X quest=2670 policy=skip_prerequisite_only", kQuestFlagCheckRva + 4, kQuestPassedRva); return true; } bool HookMainModuleImport(const char* targetDll, const char* targetName, void* detour, void** original) { if (!targetDll || !targetName || !detour || !original) return false; HMODULE module = GetModuleHandleW(nullptr); if (!module) return false; __try { auto* dos = reinterpret_cast(module); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; auto* nt = reinterpret_cast( reinterpret_cast(module) + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; IMAGE_DATA_DIRECTORY importDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; if (!importDirectory.VirtualAddress || !importDirectory.Size) { LogLine("iat hook skipped name=%s reason=no-import-directory", targetName); return false; } auto* descriptor = reinterpret_cast( reinterpret_cast(module) + importDirectory.VirtualAddress); for (; descriptor->Name; ++descriptor) { const char* dllName = reinterpret_cast( reinterpret_cast(module) + descriptor->Name); if (!dllName || _stricmp(dllName, targetDll) != 0) continue; auto* nameThunk = reinterpret_cast( reinterpret_cast(module) + descriptor->OriginalFirstThunk); auto* addressThunk = reinterpret_cast( reinterpret_cast(module) + descriptor->FirstThunk); if (!descriptor->OriginalFirstThunk) nameThunk = addressThunk; for (; nameThunk->u1.AddressOfData && addressThunk->u1.Function; ++nameThunk, ++addressThunk) { if (IMAGE_SNAP_BY_ORDINAL(nameThunk->u1.Ordinal)) continue; auto* importByName = reinterpret_cast( reinterpret_cast(module) + nameThunk->u1.AddressOfData); const char* importName = reinterpret_cast(importByName->Name); if (!importName || strcmp(importName, targetName) != 0) continue; void* current = reinterpret_cast(addressThunk->u1.Function); if (current == detour) { LogLine("iat hook already installed name=%s slot=%p", targetName, addressThunk); return true; } DWORD oldProtection = 0; if (!VirtualProtect(&addressThunk->u1.Function, sizeof(addressThunk->u1.Function), PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("iat hook protect failed name=%s slot=%p gle=%lu", targetName, addressThunk, GetLastError()); return false; } *original = current; addressThunk->u1.Function = reinterpret_cast(detour); DWORD ignoredProtection = 0; VirtualProtect(&addressThunk->u1.Function, sizeof(addressThunk->u1.Function), oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), &addressThunk->u1.Function, sizeof(addressThunk->u1.Function)); LogLine("iat hook name=%s dll=%s slot=%p old=%p detour=%p", targetName, dllName, addressThunk, current, detour); return true; } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("iat hook exception name=%s code=0x%08X", targetName, GetExceptionCode()); return false; } LogLine("iat hook skipped name=%s dll=%s reason=not-imported", targetName, targetDll); return false; } bool HookMainModuleImportOrdinal( const char* targetDll, WORD targetOrdinal, void* detour, void** original) { if (!targetDll || !targetOrdinal || !detour || !original) return false; HMODULE module = GetModuleHandleW(nullptr); if (!module) return false; __try { auto* dos = reinterpret_cast(module); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; auto* nt = reinterpret_cast( reinterpret_cast(module) + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; IMAGE_DATA_DIRECTORY importDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; if (!importDirectory.VirtualAddress || !importDirectory.Size) { LogLine("iat ordinal hook skipped ordinal=%u reason=no-import-directory", static_cast(targetOrdinal)); return false; } auto* descriptor = reinterpret_cast( reinterpret_cast(module) + importDirectory.VirtualAddress); for (; descriptor->Name; ++descriptor) { const char* dllName = reinterpret_cast( reinterpret_cast(module) + descriptor->Name); if (!dllName || _stricmp(dllName, targetDll) != 0) continue; if (!descriptor->OriginalFirstThunk) { LogLine( "iat ordinal hook skipped ordinal=%u dll=%s " "reason=no-original-thunk", static_cast(targetOrdinal), dllName); return false; } auto* nameThunk = reinterpret_cast( reinterpret_cast(module) + descriptor->OriginalFirstThunk); auto* addressThunk = reinterpret_cast( reinterpret_cast(module) + descriptor->FirstThunk); for (; nameThunk->u1.AddressOfData && addressThunk->u1.Function; ++nameThunk, ++addressThunk) { if (!IMAGE_SNAP_BY_ORDINAL(nameThunk->u1.Ordinal)) continue; WORD importedOrdinal = static_cast( IMAGE_ORDINAL(nameThunk->u1.Ordinal)); if (importedOrdinal != targetOrdinal) continue; void* current = reinterpret_cast( addressThunk->u1.Function); if (current == detour) { LogLine( "iat ordinal hook already installed ordinal=%u slot=%p", static_cast(targetOrdinal), addressThunk); return true; } DWORD oldProtection = 0; if (!VirtualProtect( &addressThunk->u1.Function, sizeof(addressThunk->u1.Function), PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine( "iat ordinal hook protect failed ordinal=%u slot=%p gle=%lu", static_cast(targetOrdinal), addressThunk, GetLastError()); return false; } *original = current; addressThunk->u1.Function = reinterpret_cast(detour); DWORD ignoredProtection = 0; VirtualProtect( &addressThunk->u1.Function, sizeof(addressThunk->u1.Function), oldProtection, &ignoredProtection); FlushInstructionCache( GetCurrentProcess(), &addressThunk->u1.Function, sizeof(addressThunk->u1.Function)); LogLine( "iat ordinal hook ordinal=%u dll=%s slot=%p old=%p detour=%p", static_cast(targetOrdinal), dllName, addressThunk, current, detour); return true; } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("iat ordinal hook exception ordinal=%u code=0x%08X", static_cast(targetOrdinal), GetExceptionCode()); return false; } LogLine("iat ordinal hook skipped ordinal=%u dll=%s reason=not-imported", static_cast(targetOrdinal), targetDll); return false; } bool InstallFirstLoginRawReceiveProbe() { HMODULE winsock = GetModuleHandleW(L"WS2_32.dll"); if (!winsock) { LogLine("first-login raw-receive probe unavailable reason=ws2_32-not-loaded"); return false; } g_firstLoginProbeGetLastError = reinterpret_cast( GetProcAddress(winsock, "WSAGetLastError")); g_firstLoginProbeSetLastError = reinterpret_cast( GetProcAddress(winsock, "WSASetLastError")); void* recvOriginal = nullptr; bool recvInstalled = HookMainModuleImportOrdinal( "WS2_32.dll", 16, reinterpret_cast(&ProxyFirstLoginRecv), &recvOriginal); if (recvInstalled && recvOriginal) { g_originalFirstLoginRecv = reinterpret_cast(recvOriginal); } void* ioctlOriginal = nullptr; bool ioctlInstalled = HookMainModuleImportOrdinal( "WS2_32.dll", 10, reinterpret_cast(&ProxyFirstLoginIoctlSocket), &ioctlOriginal); if (ioctlInstalled && ioctlOriginal) { g_originalFirstLoginIoctlSocket = reinterpret_cast(ioctlOriginal); } void* eventSelectOriginal = nullptr; bool eventSelectInstalled = HookMainModuleImport( "WS2_32.dll", "WSAEventSelect", reinterpret_cast(&ProxyFirstLoginWSAEventSelect), &eventSelectOriginal); if (eventSelectInstalled && eventSelectOriginal) { g_originalFirstLoginEventSelect = reinterpret_cast(eventSelectOriginal); } void* enumOriginal = nullptr; bool enumInstalled = HookMainModuleImport( "WS2_32.dll", "WSAEnumNetworkEvents", reinterpret_cast(&ProxyFirstLoginWSAEnumNetworkEvents), &enumOriginal); if (enumInstalled && enumOriginal) { g_originalFirstLoginEnumNetworkEvents = reinterpret_cast(enumOriginal); } void* waitOriginal = nullptr; bool waitInstalled = HookMainModuleImport( "WS2_32.dll", "WSAWaitForMultipleEvents", reinterpret_cast(&ProxyFirstLoginWSAWaitForMultipleEvents), &waitOriginal); if (waitInstalled && waitOriginal) { g_originalFirstLoginWaitForEvents = reinterpret_cast(waitOriginal); } bool requiredInstalled = recvInstalled && eventSelectInstalled && enumInstalled; InterlockedExchange( &g_firstLoginRawProbeInstalled, requiredInstalled ? 1 : 0); LogLine( "first-login raw-receive probe install version=1 raw_recv=%d " "ioctl=%d event_select=%d enum_events=%d wait_events=%d " "wsa_error_api=%d required=%d", recvInstalled ? 1 : 0, ioctlInstalled ? 1 : 0, eventSelectInstalled ? 1 : 0, enumInstalled ? 1 : 0, waitInstalled ? 1 : 0, g_firstLoginProbeGetLastError && g_firstLoginProbeSetLastError ? 1 : 0, requiredInstalled ? 1 : 0); return requiredInstalled; } bool InstallUiCompatibilityHook() { void* sendMessageOriginal = nullptr; bool sendMessageInstalled = HookMainModuleImport("USER32.dll", "SendMessageW", reinterpret_cast(&ProxySendMessageW), &sendMessageOriginal); if (sendMessageInstalled && sendMessageOriginal && !g_originalSendMessageW) { g_originalSendMessageW = sendMessageOriginal; } void* showWindowOriginal = nullptr; bool showWindowInstalled = HookMainModuleImport("USER32.dll", "ShowWindow", reinterpret_cast(&ProxyShowWindow), &showWindowOriginal); if (showWindowInstalled && showWindowOriginal && !g_originalShowWindow) { g_originalShowWindow = showWindowOriginal; } void* coCreateOriginal = nullptr; bool coCreateInstalled = HookMainModuleImport("ole32.dll", "CoCreateInstance", reinterpret_cast(&ProxyCoCreateInstance), &coCreateOriginal); if (coCreateInstalled && coCreateOriginal && !g_originalCoCreateInstance) { g_originalCoCreateInstance = coCreateOriginal; } bool installed = sendMessageInstalled && showWindowInstalled && coCreateInstalled; LogLine("[nomin] audited current-EXE IAT branches send=%d show=%d toggle_desktop=%d", sendMessageInstalled ? 1 : 0, showWindowInstalled ? 1 : 0, coCreateInstalled ? 1 : 0); return installed; } bool InstallMultiClientCompatibilityHook() { if (!IsMultiClientLaunch()) { LogLine("[multi] compatibility disabled for normal client launch"); return true; } void* createMutexOriginal = nullptr; bool installed = HookMainModuleImport( "KERNEL32.dll", "CreateMutexW", reinterpret_cast(&ProxyCreateMutexW), &createMutexOriginal); if (installed && createMutexOriginal && !g_originalCreateMutexW) { g_originalCreateMutexW = createMutexOriginal; } void* findWindowOriginal = nullptr; bool findWindowInstalled = HookMainModuleImport( "USER32.dll", "FindWindowW", reinterpret_cast(&ProxyFindWindowW), &findWindowOriginal); if (findWindowInstalled && findWindowOriginal && !g_originalFindWindowW) { g_originalFindWindowW = findWindowOriginal; } LogLine( "[multi] audited current-EXE hooks mutex=%d window=%d mutex_return_rva=0x%08X window_return_rva=0x%08X", installed ? 1 : 0, findWindowInstalled ? 1 : 0, kMultiClientCreateMutexReturnRva, kMultiClientFindWindowReturnRva); return installed && findWindowInstalled; } bool InstallSocketOpenTrace() { // Both signatures include whole instructions only. A mismatch means this // executable build is not the audited NoPack image, so leave it untouched. static const unsigned char kItemRowParsePrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kPanelSelectPrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08 }; // 53 56 8B F1 is followed by a six-byte mov; keep whole instructions in // the trampoline patch rather than splitting that instruction at byte 5. static const unsigned char kSocketWriterPrologue[] = { 0x53, 0x56, 0x8B, 0xF1, 0x8B, 0x86, 0xB4, 0x01, 0x00, 0x00 }; // push ebp; mov ebp,esp; mov eax,[ebp+8]; mov ecx,[imm32] static const unsigned char kSocketOp14ExtensionGatePrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0x8B, 0x0D, 0xB8, 0x26, 0x1B, 0x05 }; uintptr_t itemRowParseTarget = g_dnfBase + kCurrentItemRowParseRva; uintptr_t panelSelectTarget = g_dnfBase + kSocketPanelSelectRva; uintptr_t socketWriterTarget = g_dnfBase + kSocketOpenWriterRva; uintptr_t socketOp14ExtensionGateTarget = g_dnfBase + kSocketOp14ExtensionGateRva; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(itemRowParseTarget), kItemRowParsePrologue, sizeof(kItemRowParsePrologue)) && BytesMatch(reinterpret_cast(panelSelectTarget), kPanelSelectPrologue, sizeof(kPanelSelectPrologue)) && BytesMatch(reinterpret_cast(socketWriterTarget), kSocketWriterPrologue, sizeof(kSocketWriterPrologue)) && BytesMatch(reinterpret_cast(socketOp14ExtensionGateTarget), kSocketOp14ExtensionGatePrologue, sizeof(kSocketOp14ExtensionGatePrologue))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("socket-trace targets did not reach audited bytes; no trace hook installed row=0x%08X panel=0x%08X writer=0x%08X gate=0x%08X", itemRowParseTarget, panelSelectTarget, socketWriterTarget, socketOp14ExtensionGateTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook(itemRowParseTarget, kItemRowParsePrologue, sizeof(kItemRowParsePrologue), reinterpret_cast(&ProxyCurrentItemRowParse), &original, "socket-trace item-row parser")) { return false; } g_originalCurrentItemRowParse = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(panelSelectTarget, kPanelSelectPrologue, sizeof(kPanelSelectPrologue), reinterpret_cast(&ProxySocketPanelSelect), &original, "socket-trace panel selection")) { LogLine("socket-trace panel hook failed; parser remains passive and silent"); return false; } g_originalSocketPanelSelect = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(socketWriterTarget, kSocketWriterPrologue, sizeof(kSocketWriterPrologue), reinterpret_cast(&ProxySocketOpenWriter), &original, "socket-trace writer")) { LogLine("socket-trace writer hook failed; parser and panel hooks remain passive"); return false; } g_originalSocketOpenWriter = reinterpret_cast(original); original = nullptr; if (!InstallKnownInlineHook(socketOp14ExtensionGateTarget, kSocketOp14ExtensionGatePrologue, sizeof(kSocketOp14ExtensionGatePrologue), reinterpret_cast(&ProxySocketOp14ExtensionGate), &original, "socket-trace op14/list3 extension gate")) { LogLine("socket-trace extension-gate hook failed; existing row/panel/writer hooks remain passive"); return false; } g_originalSocketOp14ExtensionGate = reinterpret_cast(original); LogLine("socket-trace installed row=0x%08X panel=0x%08X writer=0x%08X gate=0x%08X wait_ms=%d max_selections=%ld max_writer_hits=%ld max_gate_hits=%ld", itemRowParseTarget, panelSelectTarget, socketWriterTarget, socketOp14ExtensionGateTarget, waitedMs, kSocketTraceMaxSelections, kSocketTraceMaxWriterHits, kSocketTraceMaxExtensionGateHits); return true; } bool InstallContractUseCompatibility() { static const unsigned char kInventoryUseUIAPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x48, 0xC2, 0xCA, 0x03 }; static const unsigned char kInventoryUseUIBPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0xF9, 0xCD, 0xD3, 0x03 }; // push ebp; mov ebp,esp; mov eax,[ebp+0C]; mov edx,[ebp+10]; // push esi; mov esi,ecx. static const unsigned char kInventoryUseGateAPrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x0C, 0x8B, 0x55, 0x10, 0x56, 0x8B, 0xF1 }; // push ebp; mov ebp,esp; push esi; push 0x7D4; mov esi,ecx. static const unsigned char kInventoryUseGateBPrologue[] = { 0x55, 0x8B, 0xEC, 0x56, 0x68, 0xD4, 0x07, 0x00, 0x00, 0x8B, 0xF1 }; // push ebp; mov ebp,esp; push ecx; push ebx; push edi; mov edi,ecx. // This is the whole-instruction prefix of sub_2331CE0, whose `ret 8` // proves its (template ID, slot) stack arguments. static const unsigned char kInventoryUsePanelPrologue[] = { 0x55, 0x8B, 0xEC, 0x51, 0x53, 0x57, 0x8B, 0xF9 }; static const unsigned char kSelectionCtorPrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x10 }; static const unsigned char kSelectionCollectPrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x14 }; static const unsigned char kSelectionDtorPrologue[] = { 0x56, 0x8B, 0xF1, 0x8B, 0x46, 0x04 }; static const unsigned char kSelectionLookupPrologue[] = { 0x55, 0x8B, 0xEC, 0x51, 0x56 }; static const unsigned char kTemplateReadPrologue[] = { 0x56, 0x6A, 0x01, 0x6A, 0x32 }; static const unsigned char kIdentityReadPrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x1C }; static const unsigned char kWriterValue[] = { 0xA1, 0xBC, 0xA4, 0x2E, 0x05, 0xC3 }; static const unsigned char kWriterU16Prologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kWriterScalarPrologue[] = { 0x55, 0x8B, 0xEC, 0x80, 0x79, 0x08, 0x00 }; static const unsigned char kWriterFlushPrologue[] = { 0x55, 0x8B, 0xEC, 0xB8, 0x50, 0x00, 0x04, 0x00 }; uintptr_t uiATarget = g_dnfBase + kInventoryUseUIARva; uintptr_t uiBTarget = g_dnfBase + kInventoryUseUIBRva; uintptr_t gateATarget = g_dnfBase + kInventoryUseGateARva; uintptr_t gateBTarget = g_dnfBase + kInventoryUseGateBRva; uintptr_t panelTarget = g_dnfBase + kInventoryUsePanelRva; uintptr_t selectionCtorTarget = g_dnfBase + kInventorySelectionCtorRva; uintptr_t selectionCollectTarget = g_dnfBase + kInventorySelectionCollectRva; uintptr_t selectionDtorTarget = g_dnfBase + kInventorySelectionDtorRva; uintptr_t selectionLookupTarget = g_dnfBase + kInventorySelectionLookupRva; uintptr_t templateReadTarget = g_dnfBase + kCurrentItemTemplateReadRva; uintptr_t identityReadTarget = g_dnfBase + kCurrentItemIdentityReadRva; uintptr_t writerValueTarget = g_dnfBase + kUpperWriterValueRva; uintptr_t writerU16Target = g_dnfBase + kUpperWriterU16Rva; uintptr_t writerU8Target = g_dnfBase + kUpperWriterU8Rva; uintptr_t writerI16Target = g_dnfBase + kUpperWriterI16Rva; uintptr_t writerU32Target = g_dnfBase + kUpperWriterU32Rva; uintptr_t writerFlushTarget = g_dnfBase + kUpperWriterFlushRva; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { bool ready = BytesMatch(reinterpret_cast(uiATarget), kInventoryUseUIAPrologue, sizeof(kInventoryUseUIAPrologue)) && BytesMatch(reinterpret_cast(uiBTarget), kInventoryUseUIBPrologue, sizeof(kInventoryUseUIBPrologue)) && BytesMatch(reinterpret_cast(gateATarget), kInventoryUseGateAPrologue, sizeof(kInventoryUseGateAPrologue)) && BytesMatch(reinterpret_cast(gateBTarget), kInventoryUseGateBPrologue, sizeof(kInventoryUseGateBPrologue)) && BytesMatch(reinterpret_cast(panelTarget), kInventoryUsePanelPrologue, sizeof(kInventoryUsePanelPrologue)) && BytesMatch(reinterpret_cast(selectionCtorTarget), kSelectionCtorPrologue, sizeof(kSelectionCtorPrologue)) && BytesMatch(reinterpret_cast(selectionCollectTarget), kSelectionCollectPrologue, sizeof(kSelectionCollectPrologue)) && BytesMatch(reinterpret_cast(selectionDtorTarget), kSelectionDtorPrologue, sizeof(kSelectionDtorPrologue)) && BytesMatch(reinterpret_cast(selectionLookupTarget), kSelectionLookupPrologue, sizeof(kSelectionLookupPrologue)) && BytesMatch(reinterpret_cast(templateReadTarget), kTemplateReadPrologue, sizeof(kTemplateReadPrologue)) && BytesMatch(reinterpret_cast(identityReadTarget), kIdentityReadPrologue, sizeof(kIdentityReadPrologue)) && BytesMatch(reinterpret_cast(writerValueTarget), kWriterValue, sizeof(kWriterValue)) && BytesMatch(reinterpret_cast(writerU16Target), kWriterU16Prologue, sizeof(kWriterU16Prologue)) && BytesMatch(reinterpret_cast(writerU8Target), kWriterScalarPrologue, sizeof(kWriterScalarPrologue)) && BytesMatch(reinterpret_cast(writerI16Target), kWriterScalarPrologue, sizeof(kWriterScalarPrologue)) && BytesMatch(reinterpret_cast(writerU32Target), kWriterScalarPrologue, sizeof(kWriterScalarPrologue)) && BytesMatch(reinterpret_cast(writerFlushTarget), kWriterFlushPrologue, sizeof(kWriterFlushPrologue)); if (ready) break; Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("contract-use compatibility targets did not reach audited bytes; no bypass installed uiA=0x%08X uiB=0x%08X gateA=0x%08X gateB=0x%08X panel=0x%08X", uiATarget, uiBTarget, gateATarget, gateBTarget, panelTarget); return false; } if (!InstallContractUseWindowProc()) { return false; } void* original = nullptr; if (!InstallKnownInlineHook(uiATarget, kInventoryUseUIAPrologue, sizeof(kInventoryUseUIAPrologue), reinterpret_cast(&ProxyInventoryUseUIA), &original, "contract-use main inventory callback A")) { return false; } g_originalInventoryUseUIA = original; original = nullptr; if (!InstallKnownInlineHook(uiBTarget, kInventoryUseUIBPrologue, sizeof(kInventoryUseUIBPrologue), reinterpret_cast(&ProxyInventoryUseUIB), &original, "contract-use main inventory callback B")) { return false; } g_originalInventoryUseUIB = original; original = nullptr; if (!InstallKnownInlineHook(gateATarget, kInventoryUseGateAPrologue, sizeof(kInventoryUseGateAPrologue), reinterpret_cast(&ProxyInventoryUseGateA), &original, "contract-use pre-panel gate A")) { return false; } g_originalInventoryUseGateA = original; original = nullptr; if (!InstallKnownInlineHook(gateBTarget, kInventoryUseGateBPrologue, sizeof(kInventoryUseGateBPrologue), reinterpret_cast(&ProxyInventoryUseGateB), &original, "contract-use pre-panel gate B")) { return false; } g_originalInventoryUseGateB = original; original = nullptr; if (!InstallKnownInlineHook(panelTarget, kInventoryUsePanelPrologue, sizeof(kInventoryUsePanelPrologue), reinterpret_cast(&ProxyInventoryUsePanel), &original, "contract-use active-coupon panel callback")) { return false; } g_originalInventoryUsePanel = original; original = nullptr; if (!InstallKnownInlineHook(selectionLookupTarget, kSelectionLookupPrologue, sizeof(kSelectionLookupPrologue), reinterpret_cast(&ProxyInventorySelectionLookup), &original, "inventory selection lookup null guard")) { return false; } g_originalInventorySelectionLookup = original; LogLine("contract-use compatibility installed uiA=0x%08X uiB=0x%08X gateA=0x%08X gateB=0x%08X panel=0x%08X lookup=0x%08X template=0x%08X identity=0x%08X wait_ms=%d scope=premiumlist-and-tp-reset-books-native-container-scan", uiATarget, uiBTarget, gateATarget, gateBTarget, panelTarget, selectionLookupTarget, templateReadTarget, identityReadTarget, waitedMs); return true; } bool InstallPremiumStateCompatibility() { if (!g_dnfBase) return false; uintptr_t class1DispatchTarget = g_dnfBase + kClass1DispatchRva; uintptr_t sceneUiOpenTarget = g_dnfBase + kSceneUiOpenRva; uintptr_t joustSceneBlockCheckTarget = g_dnfBase + kJoustSceneBlockCheckRva; uintptr_t joustHistoricTimeGateTarget = g_dnfBase + kJoustHistoricTimeGateRva; static const unsigned char kClass1DispatchPrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08 }; static const unsigned char kSceneUiOpenPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kJoustSceneBlockCheckPrologue[] = { 0x8B, 0x81, 0xC0, 0x0F, 0x00, 0x00 }; static const unsigned char kJoustHistoricTimeGate[] = { 0x75, 0x34 }; static const unsigned char kJoustHistoricTimeGatePatch[] = { 0xEB, 0x34 }; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(class1DispatchTarget), kClass1DispatchPrologue, sizeof(kClass1DispatchPrologue)) && BytesMatch(reinterpret_cast(sceneUiOpenTarget), kSceneUiOpenPrologue, sizeof(kSceneUiOpenPrologue)) && BytesMatch(reinterpret_cast(joustSceneBlockCheckTarget), kJoustSceneBlockCheckPrologue, sizeof(kJoustSceneBlockCheckPrologue)) && BytesMatch(reinterpret_cast(joustHistoricTimeGateTarget), kJoustHistoricTimeGate, sizeof(kJoustHistoricTimeGate))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("premium-state compatibility targets unavailable class1=0x%08X scene_ui=0x%08X joust_scene_gate=0x%08X joust_time_gate=0x%08X", class1DispatchTarget, sceneUiOpenTarget, joustSceneBlockCheckTarget, joustHistoricTimeGateTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook(class1DispatchTarget, kClass1DispatchPrologue, sizeof(kClass1DispatchPrologue), reinterpret_cast(&ProxyClass1Dispatch), &original, "marked aura state and premium clock class1 compatibility")) { return false; } g_originalClass1Dispatch = original; original = nullptr; if (!InstallKnownInlineHook(sceneUiOpenTarget, kSceneUiOpenPrologue, sizeof(kSceneUiOpenPrologue), reinterpret_cast(&ProxyAuraSkinSceneUiOpen), &original, "aura state deferred avatar panel apply")) { return false; } g_originalSceneUiOpen = original; original = nullptr; if (!InstallKnownInlineHook(joustSceneBlockCheckTarget, kJoustSceneBlockCheckPrologue, sizeof(kJoustSceneBlockCheckPrologue), reinterpret_cast(&ProxyJoustSceneBlockCheck), &original, "joust owner609 NPC-dialog scene gate")) { return false; } g_originalJoustSceneBlockCheck = original; if (!WriteCodePatch(joustHistoricTimeGateTarget, kJoustHistoricTimeGatePatch, sizeof(kJoustHistoricTimeGatePatch), "joust retired weekend time gate")) { return false; } LogLine("premium-state compatibility installed class1=0x%08X scene_ui=0x%08X joust_scene_gate=0x%08X joust_time_gate=0x%08X joust_caller=+0x%08X wait_ms=%d scope=native-op898-marked-op863-op903-TIME-owner609-dialog-and-all-day-joust-gates", class1DispatchTarget, sceneUiOpenTarget, joustSceneBlockCheckTarget, joustHistoricTimeGateTarget, static_cast(kJoustOpenGateReturnRva), waitedMs); return true; } bool InstallTownCopresenceCompatibility() { uintptr_t actorLookupTarget = g_dnfBase + kActorByObjectKeyRva; uintptr_t op24SceneModeTarget = g_dnfBase + kOp24SceneModeRva; uintptr_t op24LoadingGateTarget = g_dnfBase + kOp24LoadingGateRva; // push ebp; mov ebp,esp; mov eax,[dword_51B2728]. All eight bytes are // complete instructions and the absolute load is safe in the trampoline. static const unsigned char kActorByObjectKeyPrologue[] = { 0x55, 0x8B, 0xEC, 0xA1, 0x28, 0x27, 0x1B, 0x05 }; static const unsigned char kOp24SceneModePrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 }; static const unsigned char kOp24LoadingGatePrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; if (!BytesMatch(reinterpret_cast(op24SceneModeTarget), kOp24SceneModePrologue, sizeof(kOp24SceneModePrologue)) || !BytesMatch(reinterpret_cast(op24LoadingGateTarget), kOp24LoadingGatePrologue, sizeof(kOp24LoadingGatePrologue))) { LogLine("town lifecycle op24 gate signature mismatch scene=0x%08X loading=0x%08X", op24SceneModeTarget, op24LoadingGateTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook( actorLookupTarget, kActorByObjectKeyPrologue, sizeof(kActorByObjectKeyPrologue), reinterpret_cast(&ProxyTownActorByObjectKey), &original, "town co-presence scoped actor-context lookup")) { return false; } g_originalActorByObjectKey = original; original = nullptr; if (!InstallKnownInlineHook( op24SceneModeTarget, kOp24SceneModePrologue, sizeof(kOp24SceneModePrologue), reinterpret_cast(&ProxyOp24SceneMode), &original, "town lifecycle op24 scene-mode gate")) { return false; } g_originalOp24SceneMode = original; original = nullptr; if (!InstallKnownInlineHook( op24LoadingGateTarget, kOp24LoadingGatePrologue, sizeof(kOp24LoadingGatePrologue), reinterpret_cast(&ProxyOp24LoadingGate), &original, "town lifecycle op24 loading gate")) { return false; } g_originalOp24LoadingGate = original; uintptr_t mailboxHudManagerTarget = g_dnfBase + kMailboxHudManagerRva; uintptr_t mailboxHudReadyTarget = g_dnfBase + kMailboxHudReadyRva; uintptr_t mailboxHudRefreshTarget = g_dnfBase + kMailboxHudRefreshRva; static const unsigned char kMailboxHudManagerSignature[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x2E, 0x42, 0xDA, 0x03 }; static const unsigned char kMailboxHudReadySignature[] = { 0x8B, 0x89, 0xB8, 0x00, 0x00, 0x00, 0x85, 0xC9, 0x74, 0x05 }; static const unsigned char kMailboxHudRefreshSignature[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x38, 0x80, 0x3D, 0x70, 0x67, 0x19, 0x05 }; bool mailboxHudTargetsVerified = BytesMatch(reinterpret_cast(mailboxHudManagerTarget), kMailboxHudManagerSignature, sizeof(kMailboxHudManagerSignature)) && BytesMatch(reinterpret_cast(mailboxHudReadyTarget), kMailboxHudReadySignature, sizeof(kMailboxHudReadySignature)) && BytesMatch(reinterpret_cast(mailboxHudRefreshTarget), kMailboxHudRefreshSignature, sizeof(kMailboxHudRefreshSignature)); InterlockedExchange(&g_mailboxHudRefreshTargetsVerified, mailboxHudTargetsVerified ? 1 : 0); if (!mailboxHudTargetsVerified) { LogLine("mailbox HUD zero-alarm compatibility disabled signature mismatch manager=0x%08X ready=0x%08X refresh=0x%08X", mailboxHudManagerTarget, mailboxHudReadyTarget, mailboxHudRefreshTarget); } uintptr_t target = g_dnfBase + kClass0DispatchRva; static const unsigned char kClass0DispatchPrologue[] = { 0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D, 0x0C }; original = nullptr; if (!InstallKnownInlineHook( target, kClass0DispatchPrologue, sizeof(kClass0DispatchPrologue), reinterpret_cast(&ProxyTownCopresenceClass0Dispatch), &original, "town co-presence class0 owner-context bridge")) { return false; } g_originalClass0Dispatch = original; uintptr_t mode0OwnerCompareTarget = g_dnfBase + kMode0OwnerCompareRva; static const unsigned char kMode0OwnerCompare[] = { 0x3B, 0xF1, 0x74, 0x42, 0x0F, 0xB6, 0xD0 }; if (!BytesMatch(reinterpret_cast(mode0OwnerCompareTarget), kMode0OwnerCompare, sizeof(kMode0OwnerCompare))) { LogLine("town co-presence mode0 owner compare signature mismatch target=0x%08X", mode0OwnerCompareTarget); return false; } g_mode0OwnerRemoteResume = g_dnfBase + kMode0OwnerRemoteRva; g_mode0OwnerLocalResume = g_dnfBase + kMode0OwnerLocalRva; unsigned char mode0OwnerComparePatch[7] = { 0xE9 }; intptr_t relative = reinterpret_cast(&ProxyMode0OwnerCompare) - static_cast(mode0OwnerCompareTarget + 5); *reinterpret_cast(mode0OwnerComparePatch + 1) = static_cast(relative); mode0OwnerComparePatch[5] = 0x90; mode0OwnerComparePatch[6] = 0x90; if (!WriteCodePatch(mode0OwnerCompareTarget, mode0OwnerComparePatch, sizeof(mode0OwnerComparePatch), "town mode0 current-scene owner compare")) { return false; } LogLine("town co-presence compatibility installed class0=0x%08X actor_lookup=0x%08X mode0_owner=0x%08X op24_scene_gate=0x%08X op24_loading_gate=0x%08X scope=context-fallback-native-visual-interaction-current-actor-and-lua-enter-town", target, actorLookupTarget, mode0OwnerCompareTarget, op24SceneModeTarget, op24LoadingGateTarget); return true; } bool InstallDungeonPickupCompatibility() { if (!g_dnfBase) return false; uintptr_t manualPickupTarget = g_dnfBase + kManualPickupRva; static const unsigned char kManualPickupPrologue[] = { 0x55, 0x8B, 0xEC, 0x81, 0xEC, 0x24, 0x02, 0x00, 0x00 }; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(manualPickupTarget), kManualPickupPrologue, sizeof(kManualPickupPrologue))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("dungeon pickup compatibility target unavailable manual=0x%08X", manualPickupTarget); return false; } void* original = nullptr; if (!InstallKnownInlineHook(manualPickupTarget, kManualPickupPrologue, sizeof(kManualPickupPrologue), reinterpret_cast(&ProxyManualPickup), &original, "dungeon manual pickup container repair")) { return false; } g_originalManualPickup = original; LogLine("dungeon pickup compatibility installed manual=0x%08X native_ctor=0x%08X wait_ms=%d scope=manual-pickup-controlled-actor-null-container-only", manualPickupTarget, g_dnfBase + kPickupContainerConstructorRva, waitedMs); return true; } bool InstallCreatureRenameCompatibility() { if (!g_dnfBase) return false; uintptr_t target = g_dnfBase + kCreatureRenameMapNullCheckRva; static const unsigned char kExpected[] = { 0x83, 0xC4, 0x04, 0x8D, 0x48, 0x14, 0xE8 }; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(target), kExpected, sizeof(kExpected))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("creature rename compatibility target unavailable null_guard=0x%08X", target); return false; } g_creatureRenameMapUpdateResume = g_dnfBase + kCreatureRenameMapUpdateRva; g_creatureRenameMapDoneResume = g_dnfBase + kCreatureRenameMapDoneRva; unsigned char patch[5] = { 0xE9 }; intptr_t relative = reinterpret_cast(&ProxyCreatureRenameMapNullCheck) - static_cast(target + 5); *reinterpret_cast(patch + 1) = static_cast(relative); if (!WriteCodePatch(target, patch, sizeof(patch), "creature rename map null guard")) { return false; } LogLine("creature rename compatibility installed null_guard=0x%08X native_update=0x%08X native_done=0x%08X wait_ms=%d scope=optional-op105-map-copy-only", target, g_creatureRenameMapUpdateResume, g_creatureRenameMapDoneResume, waitedMs); return true; } bool InstallPetEnchantDisplayCompatibility() { if (!g_dnfBase) return false; uintptr_t target = g_dnfBase + kPetItemUpdateDynamicStateRva; static const unsigned char kExpected[] = { 0x0F, 0xBF, 0x85, 0x84, 0xFE, 0xFF, 0xFF }; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(target), kExpected, sizeof(kExpected))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("pet enchant display compatibility target unavailable same_item=0x%08X", target); return false; } g_petItemUpdateDynamicStateResume = g_dnfBase + kPetItemUpdateDynamicStateResumeRva; unsigned char patch[7] = { 0xE9 }; intptr_t relative = reinterpret_cast(&ProxyPetItemUpdateDynamicState) - static_cast(target + 5); *reinterpret_cast(patch + 1) = static_cast(relative); patch[5] = 0x90; patch[6] = 0x90; if (!WriteCodePatch(target, patch, sizeof(patch), "pet list-7 or equipped slot-26 op14 dynamic-state refresh")) { return false; } LogLine("pet enchant display compatibility installed same_item=0x%08X native_resume=0x%08X wait_ms=%d scope=list7-or-list3-slot26-same-template-op14-native-dynamic-state", target, g_petItemUpdateDynamicStateResume, waitedMs); return true; } bool InstallRareAvatarTicketPurchaseCompatibility() { if (!g_dnfBase) return false; uintptr_t clickTarget = g_dnfBase + kRareAvatarTicketClickCompareRva; uintptr_t availabilityTarget = g_dnfBase + kRareAvatarTicketAvailabilityCompareRva; // cmp dword ptr [ebx+14h], 2681594 / cmp dword ptr [esi+14h], 2681594 static const unsigned char kClickExpected[] = { 0x81, 0x7B, 0x14, 0xFA, 0xEA, 0x28, 0x00 }; static const unsigned char kAvailabilityExpected[] = { 0x81, 0x7E, 0x14, 0xFA, 0xEA, 0x28, 0x00 }; // Preserve the compare instructions and retire only their special-case // selector. 0x7FFFFFFF is outside the current PVF item-id domain. static const unsigned char kClickPatch[] = { 0x81, 0x7B, 0x14, 0xFF, 0xFF, 0xFF, 0x7F }; static const unsigned char kAvailabilityPatch[] = { 0x81, 0x7E, 0x14, 0xFF, 0xFF, 0xFF, 0x7F }; if (BytesMatch(reinterpret_cast(clickTarget), kClickPatch, sizeof(kClickPatch)) && BytesMatch(reinterpret_cast(availabilityTarget), kAvailabilityPatch, sizeof(kAvailabilityPatch))) { LogLine("rare-avatar ticket shop compatibility already installed click=0x%08X availability=0x%08X", clickTarget, availabilityTarget); return true; } constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(clickTarget), kClickExpected, sizeof(kClickExpected)) && BytesMatch(reinterpret_cast(availabilityTarget), kAvailabilityExpected, sizeof(kAvailabilityExpected))) { break; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("rare-avatar ticket shop native comparisons unavailable click_rva=0x%08X availability_rva=0x%08X", static_cast(kRareAvatarTicketClickCompareRva), static_cast(kRareAvatarTicketAvailabilityCompareRva)); return false; } if (!WriteCodePatch(clickTarget, kClickPatch, sizeof(kClickPatch), "rare-avatar ticket ordinary click route")) { return false; } if (!WriteCodePatch(availabilityTarget, kAvailabilityPatch, sizeof(kAvailabilityPatch), "rare-avatar ticket ordinary availability route")) { if (!WriteCodePatch(clickTarget, kClickExpected, sizeof(kClickExpected), "rare-avatar ticket click-route rollback")) { LogLine("rare-avatar ticket click-route rollback failed; fresh DNF process required"); } return false; } LogLine("rare-avatar ticket shop compatibility installed ticket=2681594 retired_requirement=63092 click=0x%08X availability=0x%08X wait_ms=%d", clickTarget, availabilityTarget, waitedMs); return true; } // The current task window captures the selected row's quest id at // callback+0x30 in sub_304E070, but its confirmation callback discards that // value and calls sub_19EE300(mode=1, selector=-1), which serializes u32(0). // Install only this verified call-site in the active transport worker so the // server receives the selected quest id and can complete that row. A failed // signature wait leaves the native zero-selector path untouched. bool InstallQuestAutoCompleteSingleTargetCompatibility() { wchar_t enabled[8] = {0}; DWORD enabledLength = GetEnvironmentVariableW( L"DNF_QUEST_SINGLE_TARGET", enabled, _countof(enabled)); if (enabledLength > 0 && enabled[0] == L'0') { LogLine("quest selected single-target compatibility disabled by environment"); return true; } g_questAutoCompleteSendFn = g_dnfBase + kQuestAutoCompleteSendRva; uintptr_t callContext = g_dnfBase + kQuestAutoCompleteCallContextRva; uintptr_t callTarget = g_dnfBase + kQuestAutoCompleteCallRva; uintptr_t captureTarget = g_dnfBase + kQuestAutoCompleteCaptureRva; static const unsigned char kCallContext[] = { 0x8B, 0x0D, 0x98, 0x27, 0x1B, 0x05, 0x6A, 0xFF, 0x6A, 0x01, 0xE8, 0x29, 0x07, 0x9A, 0xFE }; static const unsigned char kCapture[] = {0x89, 0x5E, 0x30}; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { if (BytesMatch(reinterpret_cast(callContext), kCallContext, sizeof(kCallContext)) && BytesMatch(reinterpret_cast(captureTarget), kCapture, sizeof(kCapture))) { break; } unsigned char firstByte = 0; if (TryReadByte(reinterpret_cast(callTarget), &firstByte) && firstByte == 0xE9) { LogLine("quest selected single-target already hooked; fresh DNF process required"); return false; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("quest selected single-target native targets unavailable call=0x%08X context=0x%08X capture=0x%08X", callTarget, callContext, captureTarget); return false; } unsigned char patch[5] = {0xE8}; intptr_t relative = reinterpret_cast(&ProxyQuestAutoCompleteSelected) - static_cast(callTarget + sizeof(patch)); *reinterpret_cast(patch + 1) = static_cast(relative); if (!WriteCodePatch(callTarget, patch, sizeof(patch), "quest selected single-target confirmation")) { return false; } LogLine("quest selected single-target compatibility installed call=0x%08X capture=0x%08X native_send=0x%08X wait_ms=%d", callTarget, captureTarget, g_questAutoCompleteSendFn, waitedMs); return true; } // Historical research installer retained as non-building evidence only. It // mixed UI, scene, actor, inventory, quest, contract, and debugger behavior // into the transport DLL. Production must never compile or call this path. #if 0 DWORD WINAPI InstallWorkerInner(void*) { g_dnfBase = reinterpret_cast(GetModuleHandleW(nullptr)); if (!g_exceptionTraceHandle) { g_exceptionTraceHandle = AddVectoredExceptionHandler(1, TraceUnhandledClientException); } LogLine("90CN trace enabled exception_handler=%p module_base=0x%08X", g_exceptionTraceHandle, g_dnfBase); InstallEmbeddedDebugCompatibility(); LogProtocolPacket("LOGGER", 0, nullptr, 0); g_checksumFn = g_dnfBase + kChecksumRva; g_gameAllocatorFn = g_dnfBase + kGameAllocatorRva; g_gameMemcpyFn = g_dnfBase + kGameMemcpyRva; g_dprotoOutgoingResume = g_dnfBase + kDprotoOutgoingResumeRva; g_dprotoOutgoingReturn = g_dnfBase + kDprotoOutgoingReturnRva; g_dprotoFalseResume = g_dnfBase + kDprotoFalseResumeRva; g_mode0OwnerRemoteResume = g_dnfBase + kMode0OwnerRemoteRva; g_mode0OwnerLocalResume = g_dnfBase + kMode0OwnerLocalRva; g_mode3OwnerRemoteResume = g_dnfBase + kMode3OwnerRemoteResumeRva; g_mode3OwnerLocalResume = g_dnfBase + kMode3OwnerLocalResumeRva; g_mode3OwnerFinalizeRemoteResume = g_dnfBase + kMode3OwnerFinalizeRemoteResumeRva; g_mode3OwnerFinalizeLocalResume = g_dnfBase + kMode3OwnerFinalizeLocalResumeRva; g_mode3LocalOwnerChannelAddress = g_dnfBase + kLocalOwnerChannelRva; g_creatureRenameMapUpdateResume = g_dnfBase + kCreatureRenameMapUpdateRva; g_creatureRenameMapDoneResume = g_dnfBase + kCreatureRenameMapDoneRva; g_autoRepairAuxLookupFn = g_dnfBase + kInventorySelectionLookupRva; g_autoRepairAuxLookupResume = g_dnfBase + kAutoRepairAuxLookupResumeRva; g_autoRepairAuxLoopDone = g_dnfBase + kAutoRepairAuxLoopDoneRva; g_questAutoCompleteSendFn = g_dnfBase + kQuestAutoCompleteSendRva; uintptr_t cipherTarget = g_dnfBase + kCipherEncodeRva; uintptr_t cipherDecodeTarget = g_dnfBase + kCipherDecodeRva; uintptr_t cipherCallTarget = g_dnfBase + kCipherEncodeCallRva; uintptr_t routePredicateTarget = g_dnfBase + kDprotoRoutePredicateRva; uintptr_t outgoingGateTarget = g_dnfBase + kDprotoOutgoingGateRva; uintptr_t outgoingCloneTarget = g_dnfBase + kDprotoOutgoingCloneRva; uintptr_t selectedPageApplyTarget = g_dnfBase + kSelectedPageApplyRva; uintptr_t selectorCreateTickTarget = g_dnfBase + kSelectorCreateTickRva; uintptr_t selectorCreateTransitionTarget = g_dnfBase + kSelectorCreateTransitionRva; uintptr_t createUIClickTarget = g_dnfBase + kCreateUIClickRva; uintptr_t createUIOpenTarget = g_dnfBase + kCreateUIOpenRva; uintptr_t upperCreateSendTarget = g_dnfBase + kUpperCreateSendRva; uintptr_t class0DispatchTarget = g_dnfBase + kClass0DispatchRva; uintptr_t class1DispatchTarget = g_dnfBase + kClass1DispatchRva; uintptr_t sceneUiOpenTarget = g_dnfBase + kSceneUiOpenRva; uintptr_t localActorCreateTarget = g_dnfBase + kLocalActorCreateRva; uintptr_t mode0OwnerCompareTarget = g_dnfBase + kMode0OwnerCompareRva; uintptr_t mode3OwnerResolveTarget = g_dnfBase + kMode3OwnerResolveRva; uintptr_t mode3OwnerFinalizeTarget = g_dnfBase + kMode3OwnerFinalizeRva; uintptr_t pickupContainerGetterTarget = g_dnfBase + kPickupContainerGetterRva; uintptr_t manualPickupTarget = g_dnfBase + kManualPickupRva; uintptr_t creatureRenameMapNullCheckTarget = g_dnfBase + kCreatureRenameMapNullCheckRva; uintptr_t autoRepairAuxLookupTarget = g_dnfBase + kAutoRepairAuxLookupRva; uintptr_t op24SceneModeTarget = g_dnfBase + kOp24SceneModeRva; uintptr_t op24LoadingGateTarget = g_dnfBase + kOp24LoadingGateRva; uintptr_t questAutoCompleteCallContext = g_dnfBase + kQuestAutoCompleteCallContextRva; uintptr_t questAutoCompleteCallTarget = g_dnfBase + kQuestAutoCompleteCallRva; uintptr_t questAutoCompleteCaptureTarget = g_dnfBase + kQuestAutoCompleteCaptureRva; // The op1128 handler's unconditional trailing toast uses legacy string // ids that this build maps to an unrelated cross-class purchase warning. // The native result window already owns the complete ten-open result. uintptr_t magicBoxFollowupToastTarget = g_dnfBase + 0x019081CF; static const unsigned char kNativePrologue[] = { 0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D }; static const unsigned char kSelectedPageApplyPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kSelectorCreateTickPrologue[] = { 0x56, 0x8B, 0xF1, 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05 }; static const unsigned char kSelectorCreateTransitionPrologue[] = { 0x56, 0x57, 0x8B, 0xF9, 0x8B, 0xB7, 0xA8, 0x00, 0x00, 0x00 }; static const unsigned char kCreateUIClickPrologue[] = { 0x55, 0x8B, 0xEC, 0x51, 0x53, 0x56, 0x57 }; static const unsigned char kCreateUIOpenPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kUpperCreateSendPrologue[] = { 0x55, 0x8B, 0xEC, 0x51, 0x56 }; static const unsigned char kClass0DispatchPrologue[] = { 0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D, 0x0C }; static const unsigned char kClass1DispatchPrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08 }; static const unsigned char kSceneUiOpenPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kLocalActorCreatePrologue[] = { 0x55, 0x8B, 0xEC, 0x51, 0x57 }; static const unsigned char kMode0OwnerCompare[] = { 0x3B, 0xF1, 0x74, 0x42, 0x0F, 0xB6, 0xD0 }; static const unsigned char kMode3OwnerResolve[] = { 0x3B, 0xCE, 0x74, 0x3F, 0x8B, 0xB5, 0x50, 0xFB, 0xFF, 0xFF }; static const unsigned char kMode3OwnerFinalize[] = { 0x0F, 0xB6, 0x05, 0x49, 0x9C, 0x19, 0x05, 0x3B, 0xC3, 0x74, 0x09 }; static const unsigned char kPickupContainerGetterPrologue[] = { 0x8B, 0x81, 0x6C, 0x64, 0x00, 0x00 }; static const unsigned char kManualPickupPrologue[] = { 0x55, 0x8B, 0xEC, 0x81, 0xEC, 0x24, 0x02, 0x00, 0x00 }; static const unsigned char kCreatureRenameMapNullCheck[] = { 0x83, 0xC4, 0x04, 0x8D, 0x48, 0x14, 0xE8 }; static const unsigned char kAutoRepairAuxLookup[] = { 0x8B, 0xC8, 0xE8, 0xDA, 0xD0, 0x2B, 0x00 }; static const unsigned char kOp24SceneModePrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 }; static const unsigned char kOp24LoadingGatePrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; static const unsigned char kCipherEncodeCall[] = { 0xE8, 0x3B, 0x46, 0xF5, 0xFF }; static const unsigned char kRoutePredicatePrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x49, 0x0C }; static const unsigned char kOutgoingGate[] = { 0x0F, 0x84, 0x2F, 0x01, 0x00, 0x00 }; static const unsigned char kOutgoingClonePrologue[] = { 0x8B, 0x4B, 0x0C, 0x8B, 0x01 }; static const unsigned char kQuestAutoCompleteCallContext[] = { 0x8B, 0x0D, 0x98, 0x27, 0x1B, 0x05, 0x6A, 0xFF, 0x6A, 0x01, 0xE8, 0x29, 0x07, 0x9A, 0xFE }; static const unsigned char kQuestAutoCompleteCapture[] = { 0x89, 0x5E, 0x30 }; static const unsigned char kMagicBoxFollowupToastCall[] = { 0xE8, 0xEC, 0x77, 0x56, 0x00 }; LogLine("cipher worker started (module base 0x%08X)", g_dnfBase); InstallUiCompatibilityHook(); InstallMultiClientCompatibilityHook(); // TCLS launch argument fetches run before the first network connection. // Install only these six local helpers immediately. bool tclsInstalled = InstallTclsCompatibility(); LogLine("TCLS local launch compatibility result=%d", tclsInstalled ? 1 : 0); bool channelDiagnosticInstalled = InstallChannelDiagnostic(); LogLine("channel diagnostic result=%d", channelDiagnosticInstalled ? 1 : 0); LogLine("TCLS installed; continuing to install source-built sub_33AAFE0/sub_33AB0A0 upper-body plaintext bypass plus bounded scene route allow-list"); wchar_t enabled[8] = { 0 }; DWORD enabledLength = GetEnvironmentVariableW(L"DNF_CIPHER_PASSTHROUGH", enabled, 8); if (enabledLength > 0 && enabled[0] == L'0') { LogLine("DNF_CIPHER_PASSTHROUGH=0; hook disabled by configuration"); return 0; } wchar_t dprotoEnabled[8] = { 0 }; DWORD dprotoEnabledLength = GetEnvironmentVariableW(L"DNF_DPROTO_COMPAT", dprotoEnabled, 8); // DPROTO is not an ordinary body codec. The bridge must receive the // current EXE's finalized inner packets so dungeon combat, drops, // settlement, inventory, and UI routes can be handled by the server // without the external route/DPROTO PS1 script. This DLL ports the proven // PS1 shape: clone the predicate-true finalized packet, clone only the // measured predicate-false opcodes, and keep inbound route admission on the // bounded scene allow-list. DNF_DPROTO_COMPAT=0 restores the native wrapper. bool enableDprotoCompat = !(dprotoEnabledLength > 0 && dprotoEnabled[0] == L'0'); wchar_t routeEnabled[8] = { 0 }; DWORD routeEnabledLength = GetEnvironmentVariableW(L"DNF_ROUTE_COMPAT", routeEnabled, 8); // Keep the narrow inbound scene/UI route predicate enabled by default. // Set DNF_ROUTE_COMPAT=0 for rollback. This is not a send/recv hook and it // does not modify packet bytes; it only admits proven current-scene opcodes // to the native direct upper readers. bool enableSceneRouteCompat = routeEnabledLength == 0 || routeEnabled[0] != L'0'; wchar_t questSingleTargetEnabled[8] = { 0 }; DWORD questSingleTargetEnabledLength = GetEnvironmentVariableW( L"DNF_QUEST_SINGLE_TARGET", questSingleTargetEnabled, 8); // The bottom task-completion confirmation already captures the selected // row id. Enable the exact-row sender by default; set // DNF_QUEST_SINGLE_TARGET=0 for a clean rollback to the native u32(0). bool enableQuestSingleTarget = questSingleTargetEnabledLength == 0 || questSingleTargetEnabled[0] != L'0'; // Install as soon as the packed EXE has restored the verified native // prologues. A fixed post-TCLS sleep misses the early GET_USERINFO path: // the client has already emitted the role-select handshake before the // plaintext upper-body proxy is active, so the server receives a legacy // packet stream and the selector renders black. The byte-signature wait // below is the safety gate; do not add an unconditional startup delay here. constexpr int kBootstrapDelayMs = 0; if (kBootstrapDelayMs > 0) { Sleep(kBootstrapDelayMs); } // The packed EXE restores .text late. Refuse to patch unknown code. constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { bool cipherReady = BytesMatch(reinterpret_cast(cipherTarget), kNativePrologue, sizeof(kNativePrologue)); bool cipherDecodeReady = BytesMatch(reinterpret_cast(cipherDecodeTarget), kNativePrologue, sizeof(kNativePrologue)); bool selectedPageApplyReady = BytesMatch(reinterpret_cast(selectedPageApplyTarget), kSelectedPageApplyPrologue, sizeof(kSelectedPageApplyPrologue)); bool createUIClickReady = BytesMatch(reinterpret_cast(createUIClickTarget), kCreateUIClickPrologue, sizeof(kCreateUIClickPrologue)); bool createUIOpenReady = BytesMatch(reinterpret_cast(createUIOpenTarget), kCreateUIOpenPrologue, sizeof(kCreateUIOpenPrologue)); bool upperCreateSendReady = BytesMatch(reinterpret_cast(upperCreateSendTarget), kUpperCreateSendPrologue, sizeof(kUpperCreateSendPrologue)); bool localActorCreateReady = BytesMatch(reinterpret_cast(localActorCreateTarget), kLocalActorCreatePrologue, sizeof(kLocalActorCreatePrologue)); bool mode0OwnerCompareReady = BytesMatch(reinterpret_cast(mode0OwnerCompareTarget), kMode0OwnerCompare, sizeof(kMode0OwnerCompare)); bool mode3OwnerResolveReady = BytesMatch(reinterpret_cast(mode3OwnerResolveTarget), kMode3OwnerResolve, sizeof(kMode3OwnerResolve)); bool mode3OwnerFinalizeReady = BytesMatch( reinterpret_cast(mode3OwnerFinalizeTarget), kMode3OwnerFinalize, sizeof(kMode3OwnerFinalize)); bool pickupContainerGetterReady = BytesMatch( reinterpret_cast(pickupContainerGetterTarget), kPickupContainerGetterPrologue, sizeof(kPickupContainerGetterPrologue)); bool manualPickupReady = BytesMatch( reinterpret_cast(manualPickupTarget), kManualPickupPrologue, sizeof(kManualPickupPrologue)); bool creatureRenameMapNullCheckReady = BytesMatch( reinterpret_cast(creatureRenameMapNullCheckTarget), kCreatureRenameMapNullCheck, sizeof(kCreatureRenameMapNullCheck)); bool autoRepairAuxLookupReady = BytesMatch( reinterpret_cast(autoRepairAuxLookupTarget), kAutoRepairAuxLookup, sizeof(kAutoRepairAuxLookup)); bool op24SceneModeReady = BytesMatch(reinterpret_cast(op24SceneModeTarget), kOp24SceneModePrologue, sizeof(kOp24SceneModePrologue)); bool op24LoadingGateReady = BytesMatch(reinterpret_cast(op24LoadingGateTarget), kOp24LoadingGatePrologue, sizeof(kOp24LoadingGatePrologue)); bool routeReady = !enableSceneRouteCompat || BytesMatch(reinterpret_cast(routePredicateTarget), kRoutePredicatePrologue, sizeof(kRoutePredicatePrologue)); bool dprotoReady = !enableDprotoCompat || (BytesMatch(reinterpret_cast(routePredicateTarget), kRoutePredicatePrologue, sizeof(kRoutePredicatePrologue)) && BytesMatch(reinterpret_cast(outgoingGateTarget), kOutgoingGate, sizeof(kOutgoingGate)) && BytesMatch(reinterpret_cast(outgoingCloneTarget), kOutgoingClonePrologue, sizeof(kOutgoingClonePrologue))); bool questSingleTargetReady = !enableQuestSingleTarget || (BytesMatch(reinterpret_cast(questAutoCompleteCallContext), kQuestAutoCompleteCallContext, sizeof(kQuestAutoCompleteCallContext)) && BytesMatch(reinterpret_cast(questAutoCompleteCaptureTarget), kQuestAutoCompleteCapture, sizeof(kQuestAutoCompleteCapture))); bool magicBoxReady = BytesMatch(reinterpret_cast(magicBoxFollowupToastTarget), kMagicBoxFollowupToastCall, sizeof(kMagicBoxFollowupToastCall)); if (cipherReady && cipherDecodeReady && selectedPageApplyReady && createUIClickReady && createUIOpenReady && upperCreateSendReady && localActorCreateReady && mode0OwnerCompareReady && mode3OwnerResolveReady && mode3OwnerFinalizeReady && pickupContainerGetterReady && manualPickupReady && creatureRenameMapNullCheckReady && autoRepairAuxLookupReady && op24SceneModeReady && op24LoadingGateReady && routeReady && dprotoReady && questSingleTargetReady && magicBoxReady) break; unsigned char firstByte = 0; if (TryReadByte(reinterpret_cast(cipherTarget), &firstByte) && firstByte == 0xE9) { LogLine("cipher encode function already hooked; fresh DNF process required for this build"); return 0; } if (TryReadByte(reinterpret_cast(cipherDecodeTarget), &firstByte) && firstByte == 0xE9) { LogLine("cipher decode function already hooked; fresh DNF process required for this build"); return 0; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("native targets did not reach verified bytes; hook not installed route=%d dproto=%d quest_single=%d", enableSceneRouteCompat ? 1 : 0, enableDprotoCompat ? 1 : 0, enableQuestSingleTarget ? 1 : 0); return 0; } unsigned char entryPatch[5] = { 0xE8 }; entryPatch[0] = 0xE9; intptr_t relative = reinterpret_cast(&ProxyCipherEncodeFunction) - static_cast(cipherTarget + 5); *reinterpret_cast(entryPatch + 1) = static_cast(relative); if (!WriteCodePatch(cipherTarget, entryPatch, sizeof(entryPatch), "upper body encode function")) return 0; unsigned char decodePatch[5] = { 0xE9 }; relative = reinterpret_cast(&ProxyCipherDecodeFunction) - static_cast(cipherDecodeTarget + 5); *reinterpret_cast(decodePatch + 1) = static_cast(relative); if (!WriteCodePatch(cipherDecodeTarget, decodePatch, sizeof(decodePatch), "upper body decode function")) return 0; if (enableSceneRouteCompat) { unsigned char routeAllowPatch[6] = { 0xE9 }; intptr_t routeRelative = reinterpret_cast(&ProxySceneRouteAllowList) - static_cast(routePredicateTarget + 5); *reinterpret_cast(routeAllowPatch + 1) = static_cast(routeRelative); routeAllowPatch[5] = 0x90; if (!WriteCodePatch(routePredicateTarget, routeAllowPatch, sizeof(routeAllowPatch), "scene route allow-list")) return 0; } if (enableDprotoCompat) { unsigned char clonePatch[5] = { 0xE9 }; intptr_t cloneRelative = reinterpret_cast(&ProxyDprotoSendDirect) - static_cast(outgoingCloneTarget + 5); *reinterpret_cast(clonePatch + 1) = static_cast(cloneRelative); unsigned char falsePatch[6] = { 0x0F, 0x84 }; intptr_t falseRelative = reinterpret_cast(&ProxyDprotoFalseSelective) - static_cast(outgoingGateTarget + 6); *reinterpret_cast(falsePatch + 2) = static_cast(falseRelative); if (!WriteCodePatch(outgoingCloneTarget, clonePatch, sizeof(clonePatch), "dproto outbound predicate-true clone") || !WriteCodePatch(outgoingGateTarget, falsePatch, sizeof(falsePatch), "dproto outbound predicate-false selective")) { LogLine("DPROTO compatibility patch incomplete; fresh DNF process required"); return 0; } } static const unsigned char kMagicBoxToastNops[] = { 0x90, 0x90, 0x90, 0x90, 0x90 }; if (!WriteCodePatch(magicBoxFollowupToastTarget, kMagicBoxToastNops, sizeof(kMagicBoxToastNops), "seria ten-open stale follow-up toast")) { return 0; } LogLine("seria ten-open compatibility installed toast=0x%08X", magicBoxFollowupToastTarget); if (enableQuestSingleTarget) { unsigned char questSingleTargetPatch[5] = { 0xE8 }; intptr_t questRelative = reinterpret_cast(&ProxyQuestAutoCompleteSelected) - static_cast(questAutoCompleteCallTarget + 5); *reinterpret_cast(questSingleTargetPatch + 1) = static_cast(questRelative); if (!WriteCodePatch(questAutoCompleteCallTarget, questSingleTargetPatch, sizeof(questSingleTargetPatch), "quest selected single-target confirmation")) { LogLine("quest selected single-target patch failed; native zero-selector remains"); return 0; } LogLine("quest selected single-target installed call=0x%08X capture=0x%08X native_send=0x%08X", questAutoCompleteCallTarget, questAutoCompleteCaptureTarget, g_questAutoCompleteSendFn); } unsigned char mode0OwnerComparePatch[7] = { 0xE9 }; relative = reinterpret_cast(&ProxyMode0OwnerCompare) - static_cast(mode0OwnerCompareTarget + 5); *reinterpret_cast(mode0OwnerComparePatch + 1) = static_cast(relative); mode0OwnerComparePatch[5] = 0x90; mode0OwnerComparePatch[6] = 0x90; if (!WriteCodePatch(mode0OwnerCompareTarget, mode0OwnerComparePatch, sizeof(mode0OwnerComparePatch), "mode0 owner compare diagnostic")) { return 0; } unsigned char mode3OwnerResolvePatch[10] = { 0xE9 }; relative = reinterpret_cast(&ProxyMode3OwnerResolve) - static_cast(mode3OwnerResolveTarget + 5); *reinterpret_cast(mode3OwnerResolvePatch + 1) = static_cast(relative); for (size_t i = 5; i < sizeof(mode3OwnerResolvePatch); ++i) { mode3OwnerResolvePatch[i] = 0x90; } if (!WriteCodePatch(mode3OwnerResolveTarget, mode3OwnerResolvePatch, sizeof(mode3OwnerResolvePatch), "mode3 current-scene owner resolve")) { return 0; } unsigned char mode3OwnerFinalizePatch[11] = { 0xE9 }; relative = reinterpret_cast(&ProxyMode3OwnerFinalize) - static_cast(mode3OwnerFinalizeTarget + 5); *reinterpret_cast(mode3OwnerFinalizePatch + 1) = static_cast(relative); for (size_t i = 5; i < sizeof(mode3OwnerFinalizePatch); ++i) { mode3OwnerFinalizePatch[i] = 0x90; } if (!WriteCodePatch(mode3OwnerFinalizeTarget, mode3OwnerFinalizePatch, sizeof(mode3OwnerFinalizePatch), "mode3 current-scene owner finalization")) { return 0; } LogLine("mode3 current-scene compatibility installed resolve=0x%08X finalize=0x%08X local=0x%08X remote=0x%08X", mode3OwnerResolveTarget, mode3OwnerFinalizeTarget, g_mode3OwnerFinalizeLocalResume, g_mode3OwnerFinalizeRemoteResume); void* pickupContainerGetterOriginal = nullptr; if (!InstallKnownInlineHook(pickupContainerGetterTarget, kPickupContainerGetterPrologue, sizeof(kPickupContainerGetterPrologue), reinterpret_cast(&ProxyPickupContainerGetter), &pickupContainerGetterOriginal, "dungeon pickup container getter repair")) { return 0; } g_originalPickupContainerGetter = pickupContainerGetterOriginal; void* manualPickupOriginal = nullptr; if (!InstallKnownInlineHook(manualPickupTarget, kManualPickupPrologue, sizeof(kManualPickupPrologue), reinterpret_cast(&ProxyManualPickup), &manualPickupOriginal, "dungeon manual pickup container repair")) { return 0; } g_originalManualPickup = manualPickupOriginal; LogLine("dungeon pickup compatibility installed getter=0x%08X manual=0x%08X native_ctor=0x%08X", pickupContainerGetterTarget, manualPickupTarget, g_dnfBase + kPickupContainerConstructorRva); unsigned char creatureRenameNullPatch[5] = { 0xE9 }; relative = reinterpret_cast(&ProxyCreatureRenameMapNullCheck) - static_cast(creatureRenameMapNullCheckTarget + 5); *reinterpret_cast(creatureRenameNullPatch + 1) = static_cast(relative); if (!WriteCodePatch(creatureRenameMapNullCheckTarget, creatureRenameNullPatch, sizeof(creatureRenameNullPatch), "creature rename map null guard")) { return 0; } LogLine("creature rename compatibility installed null_guard=0x%08X native_update=0x%08X", creatureRenameMapNullCheckTarget, g_creatureRenameMapUpdateResume); unsigned char autoRepairAuxNullPatch[7] = { 0xE9 }; relative = reinterpret_cast(&ProxyAutoRepairAuxContainerLookup) - static_cast(autoRepairAuxLookupTarget + 5); *reinterpret_cast(autoRepairAuxNullPatch + 1) = static_cast(relative); autoRepairAuxNullPatch[5] = 0x90; autoRepairAuxNullPatch[6] = 0x90; if (!WriteCodePatch(autoRepairAuxLookupTarget, autoRepairAuxNullPatch, sizeof(autoRepairAuxNullPatch), "op903 auto-repair auxiliary equipment null guard")) { return 0; } LogLine("auto-repair auxiliary equipment compatibility installed null_guard=0x%08X native_lookup=0x%08X", autoRepairAuxLookupTarget, g_autoRepairAuxLookupFn); void* selectedPageApplyOriginal = nullptr; if (!InstallKnownInlineHook(selectedPageApplyTarget, kSelectedPageApplyPrologue, sizeof(kSelectedPageApplyPrologue), reinterpret_cast(&ProxySelectedPageApply), &selectedPageApplyOriginal, "selected page null guard")) { return 0; } g_originalSelectedPageApply = selectedPageApplyOriginal; if (BytesMatch(reinterpret_cast(selectorCreateTickTarget), kSelectorCreateTickPrologue, sizeof(kSelectorCreateTickPrologue))) { void* selectorCreateTickOriginal = nullptr; if (InstallKnownInlineHook(selectorCreateTickTarget, kSelectorCreateTickPrologue, sizeof(kSelectorCreateTickPrologue), reinterpret_cast(&ProxySelectorCreateTick), &selectorCreateTickOriginal, "selector create gate trace")) { g_originalSelectorCreateTick = selectorCreateTickOriginal; } else { LogLine("selector create gate trace unavailable; compatibility remains active"); } } else { LogLine("selector create gate trace skipped: prologue mismatch"); } if (BytesMatch(reinterpret_cast(selectorCreateTransitionTarget), kSelectorCreateTransitionPrologue, sizeof(kSelectorCreateTransitionPrologue))) { void* selectorCreateTransitionOriginal = nullptr; if (InstallKnownInlineHook(selectorCreateTransitionTarget, kSelectorCreateTransitionPrologue, sizeof(kSelectorCreateTransitionPrologue), reinterpret_cast(&ProxySelectorCreateTransition), &selectorCreateTransitionOriginal, "selector create transition trace")) { g_originalSelectorCreateTransition = selectorCreateTransitionOriginal; } else { LogLine("selector create transition trace unavailable; compatibility remains active"); } } else { LogLine("selector create transition trace skipped: prologue mismatch"); } void* createUIClickOriginal = nullptr; if (!InstallKnownInlineHook(createUIClickTarget, kCreateUIClickPrologue, sizeof(kCreateUIClickPrologue), reinterpret_cast(&ProxyCreateUIClick), &createUIClickOriginal, "create UI click trace")) { return 0; } g_originalCreateUIClick = createUIClickOriginal; void* createUIOpenOriginal = nullptr; if (!InstallKnownInlineHook(createUIOpenTarget, kCreateUIOpenPrologue, sizeof(kCreateUIOpenPrologue), reinterpret_cast(&ProxyCreateUIOpen), &createUIOpenOriginal, "create UI open trace")) { return 0; } g_originalCreateUIOpen = createUIOpenOriginal; void* upperCreateSendOriginal = nullptr; if (!InstallKnownInlineHook(upperCreateSendTarget, kUpperCreateSendPrologue, sizeof(kUpperCreateSendPrologue), reinterpret_cast(&ProxyUpperCreateSend), &upperCreateSendOriginal, "upper create send trace")) { return 0; } g_originalUpperCreateSend = upperCreateSendOriginal; void* class0DispatchOriginal = nullptr; if (!InstallKnownInlineHook(class0DispatchTarget, kClass0DispatchPrologue, sizeof(kClass0DispatchPrologue), reinterpret_cast(&ProxyClass0Dispatch), &class0DispatchOriginal, "class0 dispatch diagnostic")) { return 0; } g_originalClass0Dispatch = class0DispatchOriginal; void* class1DispatchOriginal = nullptr; if (!InstallKnownInlineHook(class1DispatchTarget, kClass1DispatchPrologue, sizeof(kClass1DispatchPrologue), reinterpret_cast(&ProxyClass1Dispatch), &class1DispatchOriginal, "class1 dispatch diagnostic")) { return 0; } g_originalClass1Dispatch = class1DispatchOriginal; void* sceneUiOpenOriginal = nullptr; if (!InstallKnownInlineHook(sceneUiOpenTarget, kSceneUiOpenPrologue, sizeof(kSceneUiOpenPrologue), reinterpret_cast(&ProxySceneUiOpen), &sceneUiOpenOriginal, "aura skin deferred avatar panel unlock")) { return 0; } g_originalSceneUiOpen = sceneUiOpenOriginal; void* localActorCreateOriginal = nullptr; if (!InstallKnownInlineHook(localActorCreateTarget, kLocalActorCreatePrologue, sizeof(kLocalActorCreatePrologue), reinterpret_cast(&ProxyLocalActorCreate), &localActorCreateOriginal, "local actor create diagnostic")) { return 0; } g_originalLocalActorCreate = localActorCreateOriginal; void* op24SceneModeOriginal = nullptr; if (!InstallKnownInlineHook(op24SceneModeTarget, kOp24SceneModePrologue, sizeof(kOp24SceneModePrologue), reinterpret_cast(&ProxyOp24SceneMode), &op24SceneModeOriginal, "op24 scene mode diagnostic")) { return 0; } g_originalOp24SceneMode = op24SceneModeOriginal; void* op24LoadingGateOriginal = nullptr; if (!InstallKnownInlineHook(op24LoadingGateTarget, kOp24LoadingGatePrologue, sizeof(kOp24LoadingGatePrologue), reinterpret_cast(&ProxyOp24LoadingGate), &op24LoadingGateOriginal, "op24 loading gate diagnostic")) { return 0; } g_originalOp24LoadingGate = op24LoadingGateOriginal; // REQUEST_PEER/RESPONSE_PEER are the current EXE's town-player // interaction handshake. Log both registries before the server admits a // new response body so their concrete callbacks can be audited without // executing an unproved packet. TraceDispatchRegistryEntry(0, 10); TraceDispatchRegistryEntry(1, 10); TraceDispatchRegistryEntry(0, 11); TraceDispatchRegistryEntry(1, 11); // Read the exact current-EXE handlers before sending any experimental // setting notification. Opcode 390 is intentionally probed even though // the current server still replays character options as opcode 391. TraceDispatchRegistryEntry(0, 173); TraceDispatchRegistryEntry(0, 390); TraceDispatchRegistryEntry(0, 391); TraceDispatchRegistryEntry(0, 454); TraceDispatchRegistryEntry(0, 455); LogLine("compatibility DLL installed upper_body_bypass=1 upper_decode_bypass=1 scene_route=%d encode_function=0x%08X decode_function=0x%08X dproto=%d dproto_true=0x%08X dproto_false=0x%08X route=0x%08X quest_single=%d quest_call=0x%08X bootstrap_delay_ms=%d native_wait_ms=%d", enableSceneRouteCompat ? 1 : 0, cipherTarget, cipherDecodeTarget, enableDprotoCompat ? 1 : 0, outgoingCloneTarget, outgoingGateTarget, routePredicateTarget, enableQuestSingleTarget ? 1 : 0, questAutoCompleteCallTarget, kBootstrapDelayMs, waitedMs); if (IsSocketTraceEnabled()) { bool socketTraceInstalled = InstallSocketOpenTrace(); LogLine("socket-trace configuration enabled installed=%d", socketTraceInstalled ? 1 : 0); } else { LogLine("socket-trace disabled (90CN_socket_trace.ini missing or trace.enabled=0)"); } bool contractUseInstalled = InstallContractUseCompatibility(); LogLine("contract-use compatibility result=%d", contractUseInstalled ? 1 : 0); return 0; } #endif bool LoadOptionalLuaPlugin() { wchar_t enabled[8] = {}; DWORD enabledLength = GetEnvironmentVariableW( L"DNF_LUA_PLUGIN", enabled, static_cast(_countof(enabled))); if (enabledLength > 0 && enabled[0] == L'0') { LogLine("optional Lua plugin disabled by DNF_LUA_PLUGIN=0"); return false; } HMODULE selfModule = nullptr; if (!GetModuleHandleExW( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&LoadOptionalLuaPlugin), &selfModule)) { LogLine("optional Lua plugin could not resolve 90CN module error=%u", GetLastError()); return false; } wchar_t pluginPath[32768] = {}; DWORD pathLength = GetModuleFileNameW( selfModule, pluginPath, static_cast(_countof(pluginPath))); if (pathLength == 0 || pathLength >= _countof(pluginPath)) { LogLine("optional Lua plugin could not resolve module path error=%u", GetLastError()); return false; } wchar_t* separator = wcsrchr(pluginPath, L'\\'); if (!separator) { LogLine("optional Lua plugin module path has no directory separator"); return false; } separator[1] = L'\0'; if (wcscat_s(pluginPath, L"90CNLua.dll") != 0) { LogLine("optional Lua plugin path is too long"); return false; } DWORD attributes = GetFileAttributesW(pluginPath); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { if (enabledLength > 0 && enabled[0] == L'1') { LogLine("DNF_LUA_PLUGIN=1 but sibling 90CNLua.dll is missing"); } return false; } HMODULE plugin = LoadLibraryW(pluginPath); if (!plugin) { LogLine("optional Lua plugin LoadLibraryW failed error=%u", GetLastError()); return false; } using InstallLuaPlugin = BOOL(WINAPI*)(); auto install = reinterpret_cast( GetProcAddress(plugin, "Install90CNLua")); if (!install || !install()) { LogLine("optional Lua plugin install export failed error=%u", GetLastError()); return false; } LogLine("optional Lua plugin loaded module=%p", plugin); return true; } // Production boundary: connection bootstrap, packet codec, packet transport, // bounded protocol routing, one validated native party-directory UI/refresh // patch, the retired rare-avatar-ticket Unicorn gate, and narrowly allow-listed // compatibility for dungeon pickup, creature rename, premium-contract op44 // dispatch, and server-owned Crystal/Aura state projection. // ── 影片打开探针(定位客户端"按路径播 avi"的调用点)─────────────────── // // 目的:客户端只有在「角色选择」那条路径上才会播章节动画;要让开区任务当场 // 强制播放,必须先拿到客户端真正的影片加载/播放函数的地址。 // // 做法:hook 主模块对 CreateFileW / CreateFileA 的导入,凡是打开 *.avi 的调用 // 就把**调用点 RVA** 记到 90CN_digest.log。玩家重登触发一次正常过场后,日志里 // 那条 caller_rva 就是影片播放链路的地址,据此再做「主动调用」。 // // 完全被动:只记录,不改变任何返回值与参数。 typedef HANDLE (WINAPI* CreateFileWFn)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE); typedef HANDLE (WINAPI* CreateFileAFn)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE); static CreateFileWFn g_originalCreateFileW = nullptr; static CreateFileAFn g_originalCreateFileA = nullptr; static volatile LONG g_aviProbeCount = 0; static const char* AviProbeCallerRvas(char* buffer, size_t bufferBytes) { if (!buffer || bufferBytes < 16) return ""; buffer[0] = '\0'; void* frames[32] = {}; USHORT captured = CaptureStackBackTrace(0, 32, frames, nullptr); size_t written = 0; int emitted = 0; for (USHORT i = 0; i < captured && emitted < 8; ++i) { uintptr_t address = reinterpret_cast(frames[i]); if (!g_dnfBase || address < g_dnfBase || address >= g_dnfBase + 0x10000000) continue; int added = sprintf_s(buffer + written, bufferBytes - written, "%s0x%08X", emitted ? "," : "", static_cast(address - g_dnfBase)); if (added <= 0) break; written += static_cast(added); ++emitted; } return buffer; } static HANDLE WINAPI ProxyAviCreateFileW(LPCWSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flags, HANDLE templateFile) { __try { char narrow[520] = {}; if (name && WideCharToMultiByte(CP_ACP, 0, name, -1, narrow, sizeof(narrow), nullptr, nullptr)) { if (strstr(narrow, ".avi") || strstr(narrow, ".AVI")) { if (InterlockedIncrement(&g_aviProbeCount) <= 200) { char callerRvas[320] = {}; cn90::LogAviOpen(narrow, AviProbeCallerRvas(callerRvas, sizeof(callerRvas))); } } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("avi probe exception code=0x%08X", GetExceptionCode()); } return g_originalCreateFileW ? g_originalCreateFileW(name, access, share, sa, disposition, flags, templateFile) : INVALID_HANDLE_VALUE; } static HANDLE WINAPI ProxyAviCreateFileA(LPCSTR name, DWORD access, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disposition, DWORD flags, HANDLE templateFile) { __try { if (name && (strstr(name, ".avi") || strstr(name, ".AVI"))) { if (InterlockedIncrement(&g_aviProbeCount) <= 200) { char callerRvas[320] = {}; cn90::LogAviOpen(name, AviProbeCallerRvas(callerRvas, sizeof(callerRvas))); } } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("avi probe exception code=0x%08X", GetExceptionCode()); } return g_originalCreateFileA ? g_originalCreateFileA(name, access, share, sa, disposition, flags, templateFile) : INVALID_HANDLE_VALUE; } static bool InstallAviOpenProbe() { void* originalW = nullptr; bool installedW = HookMainModuleImport("kernel32.dll", "CreateFileW", reinterpret_cast(&ProxyAviCreateFileW), &originalW); if (installedW && originalW) g_originalCreateFileW = reinterpret_cast(originalW); void* originalA = nullptr; bool installedA = HookMainModuleImport("kernel32.dll", "CreateFileA", reinterpret_cast(&ProxyAviCreateFileA), &originalA); if (installedA && originalA) g_originalCreateFileA = reinterpret_cast(originalA); LogLine("avi open probe installed createfilew=%d createfilea=%d", installedW ? 1 : 0, installedA ? 1 : 0); return installedW || installedA; } // ── 影片函数入口探针(确定「按路径播放」函数的调用约定)───────────────── // // 已确认:所有影片(选人背景 / 教学片 / 章节动画)打开时,调用栈里属于主模块的 // 帧都是同一个 RVA —— 0x0314183F(见 CUTSCENE-FORCE-NOTES.md)。 // // 本探针在该调用点所在函数的**入口**挂一个 naked detour,只保存寄存器现场 → // 记一行(返回地址 RVA、ecx、edx、栈上前三个参数、以及其中像路径的字符串)→ // 跳回 trampoline。据此判定是 f(path) 还是 __thiscall f(this, path),下一步才能 // 主动调用它播放指定 avi。 static uintptr_t g_movieFnStart = 0; static void* g_movieTrampoline = nullptr; static volatile LONG g_movieEntryCount = 0; static uintptr_t g_movieSavedEsp = 0; static uintptr_t g_movieSavedEcx = 0; static uintptr_t g_movieSavedEdx = 0; static constexpr uintptr_t kMovieOpenCallerRva = 0x0314183F; // g_movieThis:影片函数入口观察到的 ecx(疑似 this),供主动调用时还原现场。 static void* g_movieThis = nullptr; static void CopyCandidateString(uintptr_t pointer, char* buffer, size_t bytes) { if (!buffer || bytes == 0) return; buffer[0] = '\0'; if (!pointer) return; __try { const char* text = reinterpret_cast(pointer); size_t i = 0; for (; i + 1 < bytes && i < 96; ++i) { const char ch = text[i]; if (ch == '\0') break; if (ch < 0x20 || ch > 0x7E) { buffer[0] = '\0'; return; } buffer[i] = ch; } buffer[i] = '\0'; } __except (EXCEPTION_EXECUTE_HANDLER) { buffer[0] = '\0'; } } extern "C" void __cdecl LogMovieFunctionEntry() { if (InterlockedIncrement(&g_movieEntryCount) > 40) return; uintptr_t ret = 0, arg1 = 0, arg2 = 0, arg3 = 0; __try { const uintptr_t esp = g_movieSavedEsp; ret = *reinterpret_cast(esp); arg1 = *reinterpret_cast(esp + 4); arg2 = *reinterpret_cast(esp + 8); arg3 = *reinterpret_cast(esp + 12); } __except (EXCEPTION_EXECUTE_HANDLER) { } if (g_movieSavedEcx && !g_movieThis) g_movieThis = reinterpret_cast(g_movieSavedEcx); char s1[128] = {}, s2[128] = {}, s3[128] = {}; CopyCandidateString(arg1, s1, sizeof(s1)); CopyCandidateString(arg2, s2, sizeof(s2)); CopyCandidateString(arg3, s3, sizeof(s3)); const unsigned retRva = (g_dnfBase && ret >= g_dnfBase && ret < g_dnfBase + 0x08000000) ? static_cast(ret - g_dnfBase) : 0; // 采集完整调用链(含所属模块),用于定位"开始播放"那段代码在哪个模块里。 { void* chainFrames[16] = {}; const USHORT chainCaptured = CaptureStackBackTrace(0, 16, chainFrames, nullptr); char chain[600] = {}; size_t chainWritten = 0; for (USHORT chainIndex = 0; chainIndex < chainCaptured && chainIndex < 8 && chainWritten + 48 < sizeof(chain); ++chainIndex) { HMODULE chainModule = nullptr; const uintptr_t chainAddress = reinterpret_cast(chainFrames[chainIndex]); if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(chainAddress), &chainModule) || !chainModule) { continue; } char chainModulePath[MAX_PATH] = {}; GetModuleFileNameA(chainModule, chainModulePath, MAX_PATH); const char* chainBaseName = chainModulePath; for (const char* cursor = chainModulePath; *cursor; ++cursor) { if (*cursor == 92) chainBaseName = cursor + 1; // 92 == '\\' } const int chainAdded = sprintf_s(chain + chainWritten, sizeof(chain) - chainWritten, "%s%s!0x%08X", chainWritten ? " <- " : "", chainBaseName, static_cast(chainAddress - reinterpret_cast(chainModule))); if (chainAdded > 0) chainWritten += static_cast(chainAdded); } cn90::LogMovieChain(chain); } // 一次性记录播放对象的 vtable(纯读取,不调用任何方法,零风险)。 // this 很可能是 bdvid32.dll 的播放对象(加载器由它调用),vtable 里就是它的 // open/play/render 等方法。记下来即可定位"开始播放"是哪一槽。 { static bool vtableLogged = false; if (!vtableLogged && g_movieThis) { vtableLogged = true; char vtableText[700] = {}; size_t vtableWritten = 0; const uintptr_t object = reinterpret_cast(g_movieThis); uintptr_t vtable = 0; __try { vtable = *reinterpret_cast(object); } __except (EXCEPTION_EXECUTE_HANDLER) { vtable = 0; } if (vtable) { for (int slot = 0; slot < 12; ++slot) { uintptr_t target = 0; __try { target = *reinterpret_cast(vtable + slot * 4); } __except (EXCEPTION_EXECUTE_HANDLER) { target = 0; } if (!target) continue; HMODULE slotModule = nullptr; const char* slotName = "?"; unsigned slotRva = 0; if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(target), &slotModule) && slotModule) { char slotPath[MAX_PATH] = {}; GetModuleFileNameA(slotModule, slotPath, MAX_PATH); const char* slotBase = slotPath; for (const char* cursor = slotPath; *cursor; ++cursor) { if (*cursor == 92) slotBase = cursor + 1; } slotName = slotBase; slotRva = static_cast(target - reinterpret_cast(slotModule)); } const int added = sprintf_s(vtableText + vtableWritten, sizeof(vtableText) - vtableWritten, "%s[%d]%s!0x%08X", vtableWritten ? " " : "", slot, slotName, slotRva); if (added > 0) vtableWritten += static_cast(added); } } cn90::LogMovieVtable(static_cast(object), static_cast(vtable), vtableText); } } // 原始栈扫描(前 6 次调用各一次):把栈上落在 bdvid32.dll / DNF.exe 里的 // 地址全部记下来,其中「前一条指令是 call」的即为候选返回地址。 // 目的:定位「游戏侧真正发起播放」的那段代码(加载器只是 bdvid 调回来的 // 文件回调,真正的播放 UI 在它下面几层)。见 CUTSCENE-FORCE-NOTES.md 第八节。 { static volatile LONG stackScanCount = 0; if (InterlockedIncrement(&stackScanCount) <= 6) { char stackText[1500] = {}; size_t stackWritten = 0; const uintptr_t stackBase = g_movieSavedEsp; for (int slot = 0; slot < 128; ++slot) { if (stackWritten + 72 >= sizeof(stackText)) break; uintptr_t value = 0; __try { value = *reinterpret_cast(stackBase + slot * 4); } __except (EXCEPTION_EXECUTE_HANDLER) { break; } if (value < 0x00010000) continue; HMODULE valueModule = nullptr; if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(value), &valueModule) || !valueModule) continue; char valuePath[MAX_PATH] = {}; GetModuleFileNameA(valueModule, valuePath, MAX_PATH); const char* valueBase = valuePath; for (const char* cursor = valuePath; *cursor; ++cursor) { if (*cursor == 92) valueBase = cursor + 1; } if (lstrcmpiA(valueBase, "bdvid32.dll") != 0 && lstrcmpiA(valueBase, "DNF.exe") != 0) continue; bool isCall = false; __try { const unsigned char direct = *reinterpret_cast(value - 5); if (direct == 0xE8) { isCall = true; } else { const unsigned char two = *reinterpret_cast(value - 2); const unsigned char one = *reinterpret_cast(value - 1); isCall = (two == 0xFF && (one & 0x38) == 0x10); } } __except (EXCEPTION_EXECUTE_HANDLER) { isCall = false; } const int added = sprintf_s(stackText + stackWritten, sizeof(stackText) - stackWritten, "%s[%d]%s!0x%08X%s", stackWritten ? " " : "", slot, valueBase, static_cast(value - reinterpret_cast(valueModule)), isCall ? "*" : ""); if (added > 0) stackWritten += static_cast(added); } cn90::LogMovieStack(stackText); } } cn90::LogMovieEntry(retRva, static_cast(g_movieSavedEcx), static_cast(g_movieSavedEdx), static_cast(arg1), s1, static_cast(arg2), s2, static_cast(arg3), s3); } extern "C" __declspec(naked) void MovieFunctionDetour() { __asm { mov dword ptr[g_movieSavedEsp], esp mov dword ptr[g_movieSavedEcx], ecx mov dword ptr[g_movieSavedEdx], edx pushad pushfd call LogMovieFunctionEntry popfd popad jmp dword ptr[g_movieTrampoline] } } static bool InstallMovieFunctionProbe() { if (!g_dnfBase) return false; const uintptr_t anchor = g_dnfBase + kMovieOpenCallerRva; uintptr_t start = 0; // 从调用点往前最多扫 0x400 字节,找「CC CC 填充 + 标准序言」的函数头。 for (uintptr_t p = anchor - 1; p > anchor - 0x400 && p > g_dnfBase + 0x1000; --p) { const unsigned char* b = reinterpret_cast(p); const bool padded = (b[-1] == 0xCC && b[-2] == 0xCC); if (!padded) continue; const bool prologue = (b[0] == 0x55 && b[1] == 0x8B && b[2] == 0xEC) || (b[0] == 0x83 && b[1] == 0xEC) || (b[0] == 0x8B && b[1] == 0xFF && b[2] == 0x55); if (prologue) { start = p; break; } } if (!start) { LogLine("movie fn probe: function start not found before anchor_rva=0x%08X", static_cast(kMovieOpenCallerRva)); return false; } unsigned char expected[8] = {}; memcpy(expected, reinterpret_cast(start), sizeof(expected)); void* original = nullptr; if (!InstallKnownInlineHook(start, expected, 5, reinterpret_cast(&MovieFunctionDetour), &original, "movie fn entry probe")) { return false; } g_movieTrampoline = original; g_movieFnStart = start; LogLine("movie fn probe installed fn_rva=0x%08X anchor_rva=0x%08X prologue=%02X %02X %02X %02X %02X", static_cast(start - g_dnfBase), static_cast(kMovieOpenCallerRva), expected[0], expected[1], expected[2], expected[3], expected[4]); return true; } // ── 影片对象 Open/Play 探针(movie-open-probe)──────────────────────── // // 静态分析结论(CUTSCENE-FORCE-NOTES.md 第九节): // 0x03141420 Open(const wchar_t* path) -> bool // 0x031415B0 Play() -> bool // 0x03141270 Render(x, y, w, h, ...) // 0x031416E0 Close() // 对象布局:this[0]=bdvid IVideo、this[8]=D3D、this[0xc]=纹理、this[0x10]/[0x14]=宽/高, // vtable = RVA 0x04527130。 // // 探针做两件事: // 1. 记录每次 Open/Play 的返回地址(= 游戏侧发起播放的代码)、this、路径,以及栈上 // 落在 DNF.exe / bdvid32.dll 里的候选返回地址; // 2. 把第一次见到的 this 存下来,供「文件触发」直接驱动它播放(带 vtable 校验)。 static void* g_movieOpenTrampoline = nullptr; static void* g_moviePlayTrampoline = nullptr; static uintptr_t g_movieOpenSavedEsp = 0; static uintptr_t g_movieOpenSavedEcx = 0; static uintptr_t g_moviePlaySavedEsp = 0; static uintptr_t g_moviePlaySavedEcx = 0; static void* g_movieInstance = nullptr; static volatile LONG g_movieOpenCount = 0; static volatile LONG g_moviePlayCount = 0; static constexpr uintptr_t kMovieOpenRva = 0x03141420; static constexpr uintptr_t kMoviePlayRva = 0x031415B0; static constexpr uintptr_t kMovieVtableRva = 0x04527130; static void AppendStackReturns(char* buffer, size_t bytes, uintptr_t savedEsp) { if (!buffer || bytes < 8) return; buffer[0] = '\0'; size_t written = 0; for (int slot = 0; slot < 96 && written + 40 < bytes; ++slot) { uintptr_t value = 0; __try { value = *reinterpret_cast(savedEsp + slot * 4); } __except (EXCEPTION_EXECUTE_HANDLER) { break; } if (value < 0x00010000) continue; HMODULE module = nullptr; if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(value), &module) || !module) continue; char modulePath[MAX_PATH] = {}; GetModuleFileNameA(module, modulePath, MAX_PATH); const char* baseName = modulePath; for (const char* cursor = modulePath; *cursor; ++cursor) { if (*cursor == 92) baseName = cursor + 1; } if (lstrcmpiA(baseName, "DNF.exe") != 0 && lstrcmpiA(baseName, "bdvid32.dll") != 0) continue; bool isCall = false; __try { const unsigned char direct = *reinterpret_cast(value - 5); if (direct == 0xE8) { isCall = true; } else { const unsigned char two = *reinterpret_cast(value - 2); const unsigned char one = *reinterpret_cast(value - 1); isCall = (two == 0xFF && (one & 0x38) == 0x10); } } __except (EXCEPTION_EXECUTE_HANDLER) { isCall = false; } const int added = sprintf_s(buffer + written, bytes - written, "%s[%d]%s!0x%08X%s", written ? " " : "", slot, baseName, static_cast(value - reinterpret_cast(module)), isCall ? "*" : ""); if (added > 0) written += static_cast(added); } } static void CopyWideText(uintptr_t pointer, char* buffer, size_t bytes) { if (!buffer || bytes == 0) return; buffer[0] = '\0'; if (!pointer) return; __try { const wchar_t* text = reinterpret_cast(pointer); size_t i = 0; for (; i + 1 < bytes && i < 200; ++i) { const wchar_t ch = text[i]; if (ch == 0) break; if (ch < 0x20 || ch > 0x7E) { buffer[0] = '\0'; return; } buffer[i] = static_cast(ch); } if (i == 0) { buffer[0] = '\0'; return; } buffer[i] = '\0'; } __except (EXCEPTION_EXECUTE_HANDLER) { buffer[0] = '\0'; } } extern "C" void __cdecl LogMovieOpenEntry() { if (InterlockedIncrement(&g_movieOpenCount) > 30) return; uintptr_t ret = 0, arg = 0; __try { ret = *reinterpret_cast(g_movieOpenSavedEsp); arg = *reinterpret_cast(g_movieOpenSavedEsp + 4); } __except (EXCEPTION_EXECUTE_HANDLER) { } if (g_movieOpenSavedEcx && !g_movieInstance) g_movieInstance = reinterpret_cast(g_movieOpenSavedEcx); char wideText[220] = {}; CopyWideText(arg, wideText, sizeof(wideText)); const unsigned retRva = (g_dnfBase && ret >= g_dnfBase && ret < g_dnfBase + 0x08000000) ? static_cast(ret - g_dnfBase) : 0; char stackText[900] = {}; AppendStackReturns(stackText, sizeof(stackText), g_movieOpenSavedEsp); cn90::LogMovieOpen(retRva, static_cast(g_movieOpenSavedEcx), static_cast(arg), wideText, stackText); } extern "C" __declspec(naked) void MovieOpenDetour() { __asm { mov dword ptr[g_movieOpenSavedEsp], esp mov dword ptr[g_movieOpenSavedEcx], ecx pushad pushfd call LogMovieOpenEntry popfd popad jmp dword ptr[g_movieOpenTrampoline] } } extern "C" void __cdecl LogMoviePlayEntry() { if (InterlockedIncrement(&g_moviePlayCount) > 40) return; uintptr_t ret = 0; __try { ret = *reinterpret_cast(g_moviePlaySavedEsp); } __except (EXCEPTION_EXECUTE_HANDLER) { } if (g_moviePlaySavedEcx && !g_movieInstance) g_movieInstance = reinterpret_cast(g_moviePlaySavedEcx); const unsigned retRva = (g_dnfBase && ret >= g_dnfBase && ret < g_dnfBase + 0x08000000) ? static_cast(ret - g_dnfBase) : 0; char stackText[900] = {}; AppendStackReturns(stackText, sizeof(stackText), g_moviePlaySavedEsp); cn90::LogMoviePlay(retRva, static_cast(g_moviePlaySavedEcx), stackText); } extern "C" __declspec(naked) void MoviePlayDetour() { __asm { mov dword ptr[g_moviePlaySavedEsp], esp mov dword ptr[g_moviePlaySavedEcx], ecx pushad pushfd call LogMoviePlayEntry popfd popad jmp dword ptr[g_moviePlayTrampoline] } } static bool InstallMovieOpenPlayProbes() { if (!g_dnfBase) return false; unsigned char expected[8] = {}; bool openInstalled = false; bool playInstalled = false; const uintptr_t openAddress = g_dnfBase + kMovieOpenRva; memcpy(expected, reinterpret_cast(openAddress), sizeof(expected)); void* original = nullptr; if (InstallKnownInlineHook(openAddress, expected, 5, reinterpret_cast(&MovieOpenDetour), &original, "movie open probe")) { g_movieOpenTrampoline = original; openInstalled = true; } memset(expected, 0, sizeof(expected)); const uintptr_t playAddress = g_dnfBase + kMoviePlayRva; memcpy(expected, reinterpret_cast(playAddress), sizeof(expected)); original = nullptr; // 注意:0x031415B0 的头 5 字节会切在 `80 7E 18 00`(cmp byte [esi+0x18],0) 指令中间, // trampoline 会执行半条指令直接崩。整条指令共 1+2+4=7 字节,因此这里必须打 7 字节。 if (InstallKnownInlineHook(playAddress, expected, 7, reinterpret_cast(&MoviePlayDetour), &original, "movie play probe")) { g_moviePlayTrampoline = original; playInstalled = true; } LogLine("movie open/play probes open=%d play=%d", openInstalled ? 1 : 0, playInstalled ? 1 : 0); return openInstalled || playInstalled; } // ── Render 探针(movie-render-probe)───────────────────────────────── // 0x03141270 = Render(x, y, w, h, ...),由各场景的渲染循环每帧调用。 // 用来确认「影片在播,但当前场景是否真的在画」。 static void* g_movieRenderTrampoline = nullptr; static uintptr_t g_movieRenderSavedEsp = 0; static uintptr_t g_movieRenderSavedEcx = 0; static volatile LONG g_movieRenderCount = 0; extern "C" void __cdecl LogMovieRenderEntry() { const LONG count = InterlockedIncrement(&g_movieRenderCount); if (count > 5 && (count % 60) != 0) return; uintptr_t ret = 0; __try { ret = *reinterpret_cast(g_movieRenderSavedEsp); } __except (EXCEPTION_EXECUTE_HANDLER) { } const unsigned retRva = (g_dnfBase && ret >= g_dnfBase && ret < g_dnfBase + 0x08000000) ? static_cast(ret - g_dnfBase) : 0; char stackText[700] = {}; AppendStackReturns(stackText, sizeof(stackText), g_movieRenderSavedEsp); cn90::LogMovieRender(count, static_cast(g_movieRenderSavedEcx), retRva, stackText); } extern "C" __declspec(naked) void MovieRenderDetour() { __asm { mov dword ptr[g_movieRenderSavedEsp], esp mov dword ptr[g_movieRenderSavedEcx], ecx pushad pushfd call LogMovieRenderEntry popfd popad jmp dword ptr[g_movieRenderTrampoline] } } static bool InstallMovieRenderProbe() { if (!g_dnfBase) return false; // 整条指令对齐:55 | 8B EC | 81 EC F4 00 00 00 = 9 字节 const uintptr_t renderAddress = g_dnfBase + 0x03141270; unsigned char expected[16] = {}; memcpy(expected, reinterpret_cast(renderAddress), 9); void* original = nullptr; if (!InstallKnownInlineHook(renderAddress, expected, 9, reinterpret_cast(&MovieRenderDetour), &original, "movie render probe")) return false; g_movieRenderTrampoline = original; LogLine("movie render probe installed"); return true; } // 强制播放:直接使用游戏自己的**全局影片播放器**(VA 0x051B2A74 / RVA 0x04DB2A74)。 // // 依据(静态分析 + 实测日志): // - `0x02FE300E mov ecx, 0x51b2a74` → 之后调用 `0x03141420 Open(path)`、`0x031415B0 Play()`; // - `0x02FE2F70` 里在 Open 之前还会先调 `0x03140FC0`(= IVideo::vtable[0x20],Stop/Reset); // - `0x02FE7920` 每帧 `Render(0,0,0,1.0,1.0)` 全屏绘制这个播放器。 // 因此完整序列 = Stop → Open(相对路径) → Play。 static constexpr uintptr_t kMovieGlobalRva = 0x04DB2A74; // 全局播放器实例 // ── 帧边界强制渲染(endscene-forced-render)──────────────────────────── // // 实测:城镇场景根本不会调用 Render(0x03141270)(探针 0 条日志),所以影片虽然在播、 // 纹理也建好了(800x450),却没有画面。这里挂住 D3D9 的 BeginScene/EndScene, // 由补丁自己在帧里调 Render 把影片画上去。 // // this[8] 就是 IDirect3DDevice9*(Render 里用它 [vtable+0x5C] = CreateTexture)。 // D3D9 设备 vtable:BeginScene=21(0x54)、EndScene=22(0x58)。 // 两处都调:BeginScene 时画在最底层,EndScene 时画在最上层,哪一处生效都能出画面。 // 同时每 180 帧写一次心跳,用来区分「钩子没跑」和「画了没效果」。 static void* g_endSceneOriginal = nullptr; static void* g_beginSceneOriginal = nullptr; static void* g_endSceneDevice = nullptr; static void CutsceneMuteBegin(); static void CutsceneMuteEnd(); static volatile LONG g_forceRenderUntil = 0; // GetTickCount() 截止时间 static volatile LONG g_forceMaxDurationMs = 120000; // 本次播放最大绘制时长(按影片时长算) static volatile LONG g_beginSceneCount = 0; static volatile LONG g_endSceneCount = 0; static volatile LONG g_forcedDrawCount = 0; // ── 按窗口等比铺满(fullscreen-cover-draw)────────────────────────────── // Render(this, x, y, scaleX, scaleY, flags):x/y 是 int 位置,scaleX/scaleY 是 float 缩放。 // 场景自己传 (0,0,1.0,1.0) = 原始尺寸画左上角;这里按窗口客户区算 cover 缩放并居中。 static HWND g_gameWindow = nullptr; static DWORD g_forceStartTick = 0; static HWND FindGameWindowForDraw() { struct Context { DWORD pid; HWND best; } context = { GetCurrentProcessId(), nullptr }; EnumWindows([](HWND window, LPARAM parameter) -> BOOL { Context* state = reinterpret_cast(parameter); DWORD pid = 0; GetWindowThreadProcessId(window, &pid); if (pid != state->pid || !IsWindowVisible(window)) return TRUE; RECT client = {}; if (!GetClientRect(window, &client)) return TRUE; if (client.right - client.left < 200 || client.bottom - client.top < 200) return TRUE; state->best = window; return FALSE; }, reinterpret_cast(&context)); return context.best; } static bool ResolveClientRelativePath(wchar_t* output, size_t count, const char* relative); // 定义在后面 // 读影片真实时长(毫秒):Neople 头 32 字节之后找 'avih' 块。 // avih: +0 = dwMicroSecPerFrame,+16 = dwTotalFrames。 static unsigned ReadMovieDurationMs(const wchar_t* relativePath) { if (!relativePath || !relativePath[0]) return 0; char narrow[MAX_PATH] = {}; if (!WideCharToMultiByte(CP_ACP, 0, relativePath, -1, narrow, sizeof(narrow), nullptr, nullptr)) return 0; wchar_t absolute[MAX_PATH] = {}; if (!ResolveClientRelativePath(absolute, MAX_PATH, narrow)) return 0; FILE* file = nullptr; if (_wfopen_s(&file, absolute, L"rb") != 0 || !file) return 0; unsigned char buffer[65536] = {}; const size_t read = fread(buffer, 1, sizeof(buffer), file); fclose(file); if (read < 128) return 0; size_t start = 0; if (memcmp(buffer, "Neople Video Fil", 16) == 0) start = 32; for (size_t i = start; i + 40 < read; ++i) { if (memcmp(buffer + i, "avih", 4) != 0) continue; const unsigned micro = *reinterpret_cast(buffer + i + 8); const unsigned frames = *reinterpret_cast(buffer + i + 8 + 16); if (micro >= 1000 && micro <= 200000 && frames > 0 && frames < 200000) { const unsigned long long durationMs = static_cast(frames) * micro / 1000ULL; if (durationMs > 1000 && durationMs < 1800000ULL) return static_cast(durationMs); } break; } return 0; } static void DrawForcedMovieFrame(const char* phase) { if (!g_movieRenderTrampoline || !g_dnfBase) return; if (static_cast(GetTickCount()) > g_forceRenderUntil) { CutsceneMuteEnd(); return; } const LONG draws = InterlockedIncrement(&g_forcedDrawCount); if (draws <= 2 || (draws % 120) == 0) LogLine("forced movie draw #%ld phase=%s", draws, phase ? phase : "?"); // 退出:播放中按 ESC 立即结束动画(只保留 ESC,不响应空格/鼠标)。 // 起播 0.5 秒内不响应,避免触发它的那次操作把动画直接关掉。 if (g_forceStartTick && GetTickCount() - g_forceStartTick > 500) { const bool exitPressed = (GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0; if (exitPressed) { InterlockedExchange(&g_forceRenderUntil, 0); CutsceneMuteEnd(); typedef bool (__thiscall* StopFn)(void* self); __try { reinterpret_cast(g_dnfBase + 0x03140FC0)( reinterpret_cast(g_dnfBase + kMovieGlobalRva)); } __except (EXCEPTION_EXECUTE_HANDLER) { } LogLine("forced movie draw cancelled by exit key"); return; } } void* instance = reinterpret_cast(g_dnfBase + kMovieGlobalRva); // 影片播完就停:读 bdvid 播放器状态(IVideo vtable[0x38])。 // 实测「播放中」= 3;播完会变成别的值(日志里能看到)。同时留 40 秒硬上限兜底, // 避免状态语义意外时画面一直挂在屏幕上。 const DWORD elapsed = g_forceStartTick ? (GetTickCount() - g_forceStartTick) : 0; int videoState = -1; __try { const uintptr_t video = *reinterpret_cast(instance); void** videoVtable = *reinterpret_cast(video); typedef int (__thiscall* GetVideoStateFn)(void* self, int* state); reinterpret_cast(videoVtable[14])( reinterpret_cast(video), &videoState); } __except (EXCEPTION_EXECUTE_HANDLER) { videoState = -1; } { static volatile LONG stateLogCounter = 0; if (InterlockedIncrement(&stateLogCounter) % 120 == 0) LogLine("forced draw state=%d elapsed=%u", videoState, static_cast(elapsed)); } const DWORD maxDuration = static_cast(g_forceMaxDurationMs); if ((videoState >= 0 && videoState != 3 && elapsed > 2000) || elapsed > maxDuration) { InterlockedExchange(&g_forceRenderUntil, 0); CutsceneMuteEnd(); LogLine("forced movie draw finished state=%d elapsed=%u", videoState, static_cast(elapsed)); return; } // 读影片原始尺寸(this[0x10]/this[0x14]),按窗口客户区等比铺满并居中。 unsigned videoWidth = 800, videoHeight = 450; __try { const uintptr_t base = reinterpret_cast(instance); videoWidth = *reinterpret_cast(base + 0x10); videoHeight = *reinterpret_cast(base + 0x14); } __except (EXCEPTION_EXECUTE_HANDLER) { } if (videoWidth < 16 || videoHeight < 16) { videoWidth = 800; videoHeight = 450; } int drawX = 0, drawY = 0; float scaleX = 1.0f, scaleY = 1.0f; RECT client = {}; if (g_gameWindow && GetClientRect(g_gameWindow, &client) && client.right > client.left && client.bottom > client.top) { const float windowWidth = static_cast(client.right - client.left); const float windowHeight = static_cast(client.bottom - client.top); const float scaleToWidth = windowWidth / static_cast(videoWidth); const float scaleToHeight = windowHeight / static_cast(videoHeight); const float cover = scaleToWidth > scaleToHeight ? scaleToWidth : scaleToHeight; scaleX = cover; scaleY = cover; drawX = static_cast((windowWidth - videoWidth * cover) * 0.5f); drawY = static_cast((windowHeight - videoHeight * cover) * 0.5f); } typedef void (__thiscall* RenderFn)(void* self, int x, int y, float scaleX, float scaleY, int flags); __try { reinterpret_cast(g_movieRenderTrampoline)(instance, drawX, drawY, scaleX, scaleY, 0); } __except (EXCEPTION_EXECUTE_HANDLER) { static volatile LONG exceptions = 0; if (InterlockedIncrement(&exceptions) <= 3) LogLine("forced draw exception 0x%08X phase=%s", GetExceptionCode(), phase ? phase : "?"); } } extern "C" void __cdecl DrawForcedMovieFrameBeginEntry() { const LONG count = InterlockedIncrement(&g_beginSceneCount); if (count == 1 || (count % 180) == 0) LogLine("d3d begin heartbeat n=%ld", count); DrawForcedMovieFrame("begin"); } extern "C" void __cdecl DrawForcedMovieFrameEndEntry() { const LONG count = InterlockedIncrement(&g_endSceneCount); if (count == 1 || (count % 180) == 0) LogLine("d3d end heartbeat n=%ld", count); DrawForcedMovieFrame("end"); } extern "C" __declspec(naked) void BeginSceneDetour() { __asm { pushad pushfd call DrawForcedMovieFrameBeginEntry popfd popad jmp dword ptr[g_beginSceneOriginal] } } extern "C" __declspec(naked) void EndSceneDetour() { __asm { pushad pushfd call DrawForcedMovieFrameEndEntry popfd popad jmp dword ptr[g_endSceneOriginal] } } static bool IsD3d9Address(void* address, const char* label, int slot) { if (!address) return false; HMODULE module = nullptr; if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module) || !module) { LogLine("%s hook: slot%d not in a module (%p)", label, slot, address); return false; } char modulePath[MAX_PATH] = {}; GetModuleFileNameA(module, modulePath, MAX_PATH); const char* baseName = modulePath; for (const char* cursor = modulePath; *cursor; ++cursor) { if (*cursor == 92) baseName = cursor + 1; } if (lstrcmpiA(baseName, "d3d9.dll") != 0) { LogLine("%s hook: slot%d=%p module=%s", label, slot, address, baseName); return false; } return true; } static bool PatchVtableSlot(void** vtable, int slot, void* replacement, void** original, const char* label) { void* current = vtable[slot]; if (!IsD3d9Address(current, label, slot)) return false; DWORD oldProtection = 0; if (!VirtualProtect(&vtable[slot], sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtection)) return false; *original = current; vtable[slot] = replacement; DWORD ignored = 0; VirtualProtect(&vtable[slot], sizeof(void*), oldProtection, &ignored); LogLine("%s hook installed slot=%d device-slot=%p original=%p", label, slot, &vtable[slot], current); return true; } // ── 给 d3d9 的 BeginScene/EndScene 函数本体下钩子(d3d-frame-func-hook)── // // 不扫描、不解析任何未知指针(上一版扫包装对象字段,解引用垃圾指针把客户端崩掉了)。 // 只做三件安全的事: // 1. 从 `this[8]` 的 vtable 读 [21]/[22](包装对象本身是有效对象,读 vtable 安全); // 2. 校验这两个函数地址落在 d3d9.dll; // 3. 仅当函数序言是已知的 5 字节对齐形态时才打补丁,否则跳过并记日志。 // 这两个函数被所有 D3D9 设备共用,因此不需要先拿到设备指针。 static bool QuietIsD3d9(void* address) { if (!address) return false; HMODULE module = nullptr; if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module) || !module) return false; char modulePath[MAX_PATH] = {}; GetModuleFileNameA(module, modulePath, MAX_PATH); const char* baseName = modulePath; for (const char* cursor = modulePath; *cursor; ++cursor) { if (*cursor == 92) baseName = cursor + 1; } return lstrcmpiA(baseName, "d3d9.dll") == 0; } static bool HookD3dFrameFunction(void* function, const char* label, void* detour, void** trampoline, int* patchLengthOut) { if (!function) return false; if (!QuietIsD3d9(function)) { LogLine("%s: %p is not in d3d9.dll", label, function); return false; } unsigned char prologue[8] = {}; __try { memcpy(prologue, function, sizeof(prologue)); } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } int patchLength = 0; if (prologue[0] == 0x8B && prologue[1] == 0xFF && prologue[2] == 0x55 && prologue[3] == 0x8B && prologue[4] == 0xEC) { patchLength = 5; // mov edi,edi; push ebp; mov ebp,esp } else if (prologue[0] == 0x55 && prologue[1] == 0x8B && prologue[2] == 0xEC && prologue[3] == 0x6A && prologue[4] == 0xFF) { patchLength = 5; // push ebp; mov ebp,esp; push -1 } else { LogLine("%s: unknown prologue %02X %02X %02X %02X %02X, skipped", label, prologue[0], prologue[1], prologue[2], prologue[3], prologue[4]); return false; } void* original = nullptr; if (!InstallKnownInlineHook(reinterpret_cast(function), prologue, patchLength, detour, &original, label)) { return false; } *trampoline = original; if (patchLengthOut) *patchLengthOut = patchLength; return true; } // ── Present 强制渲染(present-forced-render)──────────────────────────── // 实测 DNF 从不调用 BeginScene/EndScene(钩子装上后心跳 0 条),只在 Present 交帧。 // 因此改为在 Present 之前调 Render 画影片。 static void* g_presentOriginal = nullptr; static volatile LONG g_presentCount = 0; extern "C" void __cdecl DrawForcedMovieFramePresentEntry() { const LONG count = InterlockedIncrement(&g_presentCount); if (count == 1 || (count % 180) == 0) LogLine("d3d present heartbeat n=%ld", count); DrawForcedMovieFrame("present"); } extern "C" __declspec(naked) void PresentDetour() { __asm { pushad pushfd call DrawForcedMovieFramePresentEntry popfd popad jmp dword ptr[g_presentOriginal] } } static bool InstallD3dFrameHooksFromWrapper(void* wrapper) { if (!wrapper) return false; if (g_beginSceneOriginal && g_endSceneOriginal) return true; void** vtable = nullptr; __try { vtable = *reinterpret_cast(wrapper); } __except (EXCEPTION_EXECUTE_HANDLER) { vtable = nullptr; } if (!vtable) return false; void* beginScene = vtable[21]; void* endScene = vtable[22]; LogLine("d3d frame funcs begin=%p end=%p", beginScene, endScene); if (!g_beginSceneOriginal) { HookD3dFrameFunction(beginScene, "d3d BeginScene", reinterpret_cast(&BeginSceneDetour), &g_beginSceneOriginal, nullptr); } if (!g_endSceneOriginal) { HookD3dFrameFunction(endScene, "d3d EndScene", reinterpret_cast(&EndSceneDetour), &g_endSceneOriginal, nullptr); } if (!g_presentOriginal) { // Present 有 4 个参数(源矩形、目标矩形、窗口、脏区),但我们是 naked detour, // 只保存现场后跳转,不碰参数,所以不需要知道签名。 HookD3dFrameFunction(vtable[17], "d3d Present", reinterpret_cast(&PresentDetour), &g_presentOriginal, nullptr); } g_endSceneDevice = wrapper; return g_beginSceneOriginal != nullptr || g_endSceneOriginal != nullptr; } static bool InstallEndSceneHook(void* device) { if (!device) return false; if (g_endSceneOriginal && g_beginSceneOriginal) return true; void** vtable = nullptr; __try { vtable = *reinterpret_cast(device); } __except (EXCEPTION_EXECUTE_HANDLER) { vtable = nullptr; } if (!vtable) return false; if (!g_beginSceneOriginal) PatchVtableSlot(vtable, 21, reinterpret_cast(&BeginSceneDetour), &g_beginSceneOriginal, "begin-scene"); if (!g_endSceneOriginal) PatchVtableSlot(vtable, 22, reinterpret_cast(&EndSceneDetour), &g_endSceneOriginal, "end-scene"); g_endSceneDevice = device; return g_endSceneOriginal != nullptr; } // ── 章节动画期间静音 BGM/环境音效(FMOD 主通道组)───────────────────── // // 2026-10-21:影片声音由 bdvid32.dll 独立播放,不经过 FMOD;因此动画期间把 // FMOD 的主通道组音量临时设为 0,即可静音 BGM/环境音效而保留影片原声, // 动画结束后恢复原音量。 typedef int(__stdcall* FmodSystemCreateFn)(void** system); typedef int(__stdcall* FmodGetMasterChannelGroupFn)(void* system, void** group); typedef int(__stdcall* FmodChannelGroupSetVolumeFn)(void* group, float volume); typedef int(__stdcall* FmodChannelGroupGetVolumeFn)(void* group, float* volume); static void* g_fmodSystem = nullptr; static FmodSystemCreateFn g_fmodSystemCreateOriginal = nullptr; static FmodGetMasterChannelGroupFn g_fmodGetMasterGroup = nullptr; static FmodChannelGroupSetVolumeFn g_fmodSetGroupVolume = nullptr; static FmodChannelGroupGetVolumeFn g_fmodGetGroupVolume = nullptr; static volatile LONG g_cutsceneMuteActive = 0; static float g_cutsceneSavedVolume = 1.0f; static int __stdcall ProxyFmodSystemCreate(void** system) { int result = g_fmodSystemCreateOriginal ? g_fmodSystemCreateOriginal(system) : -1; if (result == 0 && system && *system) g_fmodSystem = *system; return result; } // ── Winsock send 探针:定位 op39(怪物死亡上报)的发送调用点 ────────────── typedef int(__stdcall* WinsockSendFn)(SOCKET s, const char* buf, int len, int flags); static WinsockSendFn g_originalWinsockSend = nullptr; static volatile LONG g_sendProbeHits = 0; static int __stdcall ProxyWinsockSend(SOCKET s, const char* buf, int len, int flags) { if (buf && len >= 64 && len <= 160) { const LONG hit = InterlockedIncrement(&g_sendProbeHits); if (hit <= 12) { void* frames[24] = {}; const USHORT count = CaptureStackBackTrace(1, 24, frames, nullptr); char line[600] = {}; int length = _snprintf(line, sizeof(line) - 1, "send-probe len=%d lead=", len); for (int i = 0; i < 12 && i < len && length < static_cast(sizeof(line)) - 12; ++i) { length += _snprintf(line + length, sizeof(line) - 1 - length, "%02x", static_cast(buf[i])); } length += _snprintf(line + length, sizeof(line) - 1 - length, " stack:"); if (!g_dnfModuleSize && g_dnfBase) { auto* moduleBase = reinterpret_cast(g_dnfBase); auto* dosHeader = reinterpret_cast(moduleBase); auto* ntHeaders = reinterpret_cast(moduleBase + dosHeader->e_lfanew); g_dnfModuleSize = ntHeaders->OptionalHeader.SizeOfImage; } for (USHORT i = 0; i < count && length < static_cast(sizeof(line)) - 16; ++i) { const uintptr_t pc = reinterpret_cast(frames[i]); if (pc >= g_dnfBase && g_dnfModuleSize && pc < g_dnfBase + g_dnfModuleSize) { length += _snprintf(line + length, sizeof(line) - 1 - length, " %x", static_cast(pc - g_dnfBase)); } } LogLine("%s", line); } } WinsockSendFn original = g_originalWinsockSend; if (original) return original(s, buf, len, flags); return -1; } static void InstallSendProbe() { if (g_originalWinsockSend) return; HookMainModuleImport("ws2_32.dll", "send", reinterpret_cast(&ProxyWinsockSend), reinterpret_cast(&g_originalWinsockSend)); LogLine("send-probe hook=%p", g_originalWinsockSend); } static void InstallFmodMuteHooks() { if (g_fmodSystemCreateOriginal) return; HookMainModuleImport("fmodex.dll", "FMOD_System_Create", reinterpret_cast(&ProxyFmodSystemCreate), reinterpret_cast(&g_fmodSystemCreateOriginal)); HMODULE fmod = GetModuleHandleW(L"fmodex.dll"); if (!fmod) fmod = LoadLibraryW(L"fmodex.dll"); if (fmod) { g_fmodGetMasterGroup = reinterpret_cast( GetProcAddress(fmod, "FMOD_System_GetMasterChannelGroup")); g_fmodSetGroupVolume = reinterpret_cast( GetProcAddress(fmod, "FMOD_ChannelGroup_SetVolume")); g_fmodGetGroupVolume = reinterpret_cast( GetProcAddress(fmod, "FMOD_ChannelGroup_GetVolume")); } LogLine("fmod mute hooks create=%p master=%p set=%p get=%p", g_fmodSystemCreateOriginal, g_fmodGetMasterGroup, g_fmodSetGroupVolume, g_fmodGetGroupVolume); } static void CutsceneMuteBegin() { if (!g_fmodSystem || !g_fmodGetMasterGroup || !g_fmodSetGroupVolume) return; if (InterlockedCompareExchange(&g_cutsceneMuteActive, 1, 0) != 0) return; void* group = nullptr; if (g_fmodGetMasterGroup(g_fmodSystem, &group) == 0 && group) { float volume = 1.0f; if (g_fmodGetGroupVolume && g_fmodGetGroupVolume(group, &volume) == 0 && volume > 0.0f) { g_cutsceneSavedVolume = volume; } g_fmodSetGroupVolume(group, 0.0f); LogLine("cutscene mute: bgm/ambient volume=0 (saved=%.2f)", g_cutsceneSavedVolume); } else { InterlockedExchange(&g_cutsceneMuteActive, 0); } } static void CutsceneMuteEnd() { if (InterlockedCompareExchange(&g_cutsceneMuteActive, 0, 1) != 1) return; if (!g_fmodSystem || !g_fmodGetMasterGroup || !g_fmodSetGroupVolume) return; void* group = nullptr; if (g_fmodGetMasterGroup(g_fmodSystem, &group) == 0 && group) { const float restore = g_cutsceneSavedVolume > 0.0f ? g_cutsceneSavedVolume : 1.0f; g_fmodSetGroupVolume(group, restore); LogLine("cutscene mute: restored volume=%.2f", restore); } } static bool PlayCutsceneWithCapturedPlayer(const wchar_t* path) { if (!path || !g_dnfBase || !g_movieOpenTrampoline || !g_moviePlayTrampoline) return false; void* instance = reinterpret_cast(g_dnfBase + kMovieGlobalRva); uintptr_t video = 0; uintptr_t videoVtable = 0; __try { video = *reinterpret_cast(instance); videoVtable = video ? *reinterpret_cast(video) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("cutscene player unreadable this=0x%08X", static_cast(reinterpret_cast(instance))); return false; } LogLine("cutscene player layout this=0x%08X video=0x%08X vtbl=0x%08X", static_cast(reinterpret_cast(instance)), static_cast(video), static_cast(videoVtable)); if (!video) { LogLine("cutscene player has no video interface yet"); return false; } typedef bool (__thiscall* MovieStopFn)(void* self); typedef bool (__thiscall* MovieOpenFn)(void* self, const wchar_t* path); typedef bool (__thiscall* MoviePlayFn)(void* self); // 1) 先 Stop/Reset(与游戏自己的 EnsureMoviePlaying 顺序一致) const bool stopped = reinterpret_cast(g_dnfBase + 0x03140FC0)(instance); // 2) Open(相对路径,与游戏自己调用一致) const bool opened = reinterpret_cast(g_movieOpenTrampoline)(instance, path); // 3) Play const bool played = opened ? reinterpret_cast(g_moviePlayTrampoline)(instance) : false; LogLine("cutscene player stop=%d open=%d play=%d", stopped ? 1 : 0, opened ? 1 : 0, played ? 1 : 0); if (played) { void* device = nullptr; __try { device = *reinterpret_cast(reinterpret_cast(instance) + 8); } __except (EXCEPTION_EXECUTE_HANDLER) { device = nullptr; } // this[8] 是游戏自己的包装对象,从它的 vtable 取出 d3d9 的 Begin/EndScene 函数本体, // 直接给函数下钩子(所有设备共用,不需要设备指针,也不解引用任何未知指针)。 if (device) InstallD3dFrameHooksFromWrapper(device); // 影片大约 30 秒,这里给 120 秒的强制渲染窗口(想停就重启客户端)。 // 按影片真实时长设窗口:时长 + 8 秒余量;读不到就用 120 秒。 const unsigned movieDurationMs = ReadMovieDurationMs(path); const unsigned windowMs = movieDurationMs ? (movieDurationMs + 8000) : 120000; InterlockedExchange(&g_forceMaxDurationMs, static_cast(windowMs)); InterlockedExchange(&g_forceRenderUntil, static_cast(GetTickCount() + windowMs)); CutsceneMuteBegin(); LogLine("cutscene duration=%ums window=%ums", movieDurationMs, windowMs); g_forceStartTick = GetTickCount(); if (!g_gameWindow) g_gameWindow = FindGameWindowForDraw(); RECT forcedClient = {}; if (g_gameWindow) GetClientRect(g_gameWindow, &forcedClient); LogLine("forced draw target window=%p client=%dx%d", g_gameWindow, static_cast(forcedClient.right - forcedClient.left), static_cast(forcedClient.bottom - forcedClient.top)); LogLine("cutscene force render armed wrapper=%p begin=%p end=%p", device, g_beginSceneOriginal, g_endSceneOriginal); } // 1.5 秒后把播放器内部状态打出来:纹理指针 / 宽高 / 标志 / 状态,用于判断 // 「在播但没画」到底卡在纹理没建好还是没人渲染。 { struct StateDump { void* instance; }; StateDump* dump = new StateDump(); dump->instance = instance; HANDLE thread = CreateThread(nullptr, 0, [](LPVOID parameter) -> DWORD { StateDump* state = static_cast(parameter); Sleep(1500); unsigned values[6] = {}; __try { const uintptr_t base = reinterpret_cast(state->instance); for (int i = 0; i < 6; ++i) values[i] = *reinterpret_cast(base + 8 + i * 4); } __except (EXCEPTION_EXECUTE_HANDLER) { values[0] = 0xDEADBEEF; } LogLine("cutscene player state d3d=0x%08X tex=0x%08X w=%u h=%u flag18=0x%08X pos1c=0x%08X state20=0x%08X", values[0], values[1], values[2], values[3], values[4], values[5], 0u); delete state; return 0; }, dump, 0, nullptr); if (thread) CloseHandle(thread); } cn90::LogMovieInvoke(static_cast(kMovieOpenRva), static_cast(reinterpret_cast(instance)), "captured-player", 1); return opened && played; } // ── 第 2 步:主动调用客户端影片播放器 ───────────────────────────────── // // 第 1 步已确认:入口被挂钩的函数接收 **宽字符路径**(`wchar_t*`),且 `ecx` // 恒为同一个对象指针(很可能是 this)。于是这里做两件事: // 1. 捕获该 this; // 2. 提供 CallClientMoviePlayer(path) —— 用 __fastcall 形态调用: // ecx = this(与观察到的现场一致),路径作为栈参数压入。 // // 触发方式(本轮先做**文件触发**,避开协议风险、便于验证): // 客户端目录下的 `90CN_play_cutscene.txt`,内容为一个数字 N(1..99)→ // 播放 `Video/movie/origin_cinematic_NN.avi`,播放后删除该文件。 // 服务端与客户端同机,开区时只要写这个文件即可,不需要改动任何协议。 static volatile LONG g_movieWatcherStarted = 0; static bool ResolveClientRelativePath(wchar_t* output, size_t count, const char* relative) { if (!output || !count || !relative) return false; output[0] = L'\0'; HMODULE self = nullptr; if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&ResolveClientRelativePath), &self) || !self) return false; wchar_t modulePath[MAX_PATH] = {}; if (!GetModuleFileNameW(self, modulePath, MAX_PATH)) return false; wchar_t* slash = wcsrchr(modulePath, L'\\'); if (!slash) return false; slash[1] = L'\0'; return swprintf_s(output, count, L"%s%hs", modulePath, relative) > 0; } static bool CallClientMoviePlayer(const wchar_t* path) { if (!path || !path[0] || !g_movieTrampoline) { cn90::LogMovieInvoke(0, 0, "(no-trampoline)", 0); return false; } const unsigned fnRva = (g_dnfBase && g_movieFnStart > g_dnfBase) ? static_cast(g_movieFnStart - g_dnfBase) : 0; char narrow[320] = {}; WideCharToMultiByte(CP_ACP, 0, path, -1, narrow, sizeof(narrow), nullptr, nullptr); if (!g_movieThis) { cn90::LogMovieInvoke(fnRva, 0, narrow, 0); return false; } cn90::LogMovieInvoke(fnRva, static_cast(reinterpret_cast(g_movieThis)), narrow, 1); // __fastcall:ecx 拿到 this,路径作为第一段栈参数压入(与观察到的现场一致)。 typedef void (__fastcall* MovieCallFn)(void* self, void* reserved, const wchar_t* path); reinterpret_cast(g_movieTrampoline)(g_movieThis, nullptr, path); return true; } static bool PlayCutsceneBySequence(int sequence) { if (sequence <= 0 || sequence > 99) return false; char relative[128] = {}; sprintf_s(relative, "Video/movie/origin_cinematic_%02d.avi", sequence); // 优先用「游戏自己的影片对象」播放:游戏自己调用时传的是**相对路径** // (如 Video/CreateCharacter/BackGround/xx.avi),这里必须保持一致。 wchar_t relativeWide[128] = {}; MultiByteToWideChar(CP_ACP, 0, relative, -1, relativeWide, 128); if (PlayCutsceneWithCapturedPlayer(relativeWide)) return true; // 回退:旧的加载器调用(只会打开文件、没有画面),用绝对路径。 wchar_t wide[MAX_PATH] = {}; if (!ResolveClientRelativePath(wide, MAX_PATH, relative)) return false; return CallClientMoviePlayer(wide); } // 播放任意影片:relative 是相对客户端目录的路径,例如 // Video/movie/my_cutscene.avi // 触发文件里写的不是纯数字时,就按路径处理。 // ── 内置影片播放器(overlay-player)──────────────────────────────────── // // 完全绕开 bdvid:直接解析我们自己生成的 MJPEG AVI,用 GDI+ 解码, // 在独立的置顶无边框窗口里逐帧绘制;音频写成临时 WAV 用 PlaySound 播放。 // 优点:不受 Neople 容器私有格式限制,出错最多是不显示,不会崩客户端。 #include #pragma comment(lib, "advapi32.lib") #include #include #pragma comment(lib, "gdiplus.lib") #pragma comment(lib, "winmm.lib") #pragma comment(lib, "ole32.lib") static const int kOverlayMaxFrames = 4000; struct OverlayFrameEntry { DWORD offset; DWORD size; }; static OverlayFrameEntry g_overlayFrames[kOverlayMaxFrames]; static int g_overlayFrameCount = 0; static HWND g_overlayWindow = nullptr; static LRESULT CALLBACK OverlayWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { if (message == WM_DESTROY) { PostQuitMessage(0); return 0; } return DefWindowProcW(window, message, wparam, lparam); } static bool OverlayReadFile(const wchar_t* path, unsigned char** data, DWORD* size) { *data = nullptr; *size = 0; FILE* file = nullptr; if (_wfopen_s(&file, path, L"rb") != 0 || !file) return false; fseek(file, 0, SEEK_END); const long length = ftell(file); fseek(file, 0, SEEK_SET); if (length <= 32 || length > (256L << 20)) { fclose(file); return false; } unsigned char* buffer = static_cast(malloc(static_cast(length))); if (!buffer) { fclose(file); return false; } const size_t read = fread(buffer, 1, static_cast(length), file); fclose(file); if (read != static_cast(length)) { free(buffer); return false; } *data = buffer; *size = static_cast(length); return true; } // 解析我们生成的 AVI:32 字节 Neople 头 + RIFF/AVI + movi('00dc' 帧 / '01wb' 音频) static bool OverlayParse(const unsigned char* data, DWORD size, DWORD* moviStart, unsigned* videoCodecOffset, unsigned* audioStride, unsigned* sampleRate, unsigned* channels) { DWORD base = 0; if (size > 32 && memcmp(data, "Neople Video Fil", 16) == 0) base = 32; if (size < base + 12) return false; if (memcmp(data + base, "RIFF", 4) != 0 || memcmp(data + base + 8, "AVI ", 4) != 0) return false; const unsigned char* p = data + base; const unsigned char* end = data + size; *videoCodecOffset = 0; *audioStride = 0; *sampleRate = 0; *channels = 0; // 找视频 strf(BITMAPINFOHEADER,compression 在 +16) for (const unsigned char* scan = p; scan + 24 < end; ++scan) { if (memcmp(scan, "strf", 4) == 0) { const unsigned char* body = scan + 8; if (body + 20 <= end && memcmp(body + 16, "MJPG", 4) == 0) { *videoCodecOffset = 1; } break; } } // 找音频 strf(WAVEFORMATEX) for (const unsigned char* scan = p; scan + 24 < end; ++scan) { if (memcmp(scan, "auds", 4) == 0) { for (const unsigned char* inner = scan; inner + 24 < end && inner < scan + 256; ++inner) { if (memcmp(inner, "strf", 4) == 0) { const unsigned char* wf = inner + 8; if (wf + 16 <= end) { const unsigned* fields = reinterpret_cast(wf); *channels = wf[2] | (wf[3] << 8); *sampleRate = fields[1]; *audioStride = wf[12] | (wf[13] << 8); } break; } } break; } } // movi for (const unsigned char* scan = p; scan + 8 < end; ++scan) { if (memcmp(scan, "movi", 4) == 0) { *moviStart = static_cast(scan - data) + 4; return true; } } return false; } static bool OverlayPlay(const wchar_t* absolutePath) { unsigned char* data = nullptr; DWORD size = 0; if (!OverlayReadFile(absolutePath, &data, &size)) return false; DWORD moviStart = 0; unsigned isMjpeg = 0, audioStride = 0, sampleRate = 0, channels = 0; if (!OverlayParse(data, size, &moviStart, &isMjpeg, &audioStride, &sampleRate, &channels) || !isMjpeg) { free(data); return false; } g_overlayFrameCount = 0; unsigned char* audio = nullptr; size_t audioBytes = 0; size_t audioCapacity = 0; DWORD p = moviStart; while (p + 8 <= size && g_overlayFrameCount < kOverlayMaxFrames) { const unsigned char* id = data + p; DWORD length = 0; memcpy(&length, data + p + 4, 4); const DWORD payload = p + 8; if (payload + length > size) break; if (memcmp(id, "00dc", 4) == 0) { g_overlayFrames[g_overlayFrameCount].offset = payload; g_overlayFrames[g_overlayFrameCount].size = length; ++g_overlayFrameCount; } else if (memcmp(id, "01wb", 4) == 0 && length > 0) { if (audioBytes + length > audioCapacity) { const size_t wanted = (audioBytes + length) * 2 + 65536; unsigned char* grown = static_cast(realloc(audio, wanted)); if (grown) { audio = grown; audioCapacity = wanted; } } if (audio && audioBytes + length <= audioCapacity) { memcpy(audio + audioBytes, data + payload, length); audioBytes += length; } } p = payload + length + (length & 1); } if (g_overlayFrameCount == 0) { free(data); free(audio); return false; } // 音频 → 临时 WAV → 异步播放 wchar_t wavPath[MAX_PATH] = {}; if (audio && audioBytes > 0 && sampleRate > 0 && channels > 0) { wchar_t tempDir[MAX_PATH] = {}; if (GetTempPathW(MAX_PATH, tempDir)) { swprintf_s(wavPath, L"%sCN90_overlay_%u.wav", tempDir, GetCurrentProcessId()); FILE* wav = nullptr; if (_wfopen_s(&wav, wavPath, L"wb") == 0 && wav) { const unsigned dataSize = static_cast(audioBytes); const unsigned byteRate = sampleRate * audioStride; fwrite("RIFF", 1, 4, wav); unsigned riffSize = 36 + dataSize; fwrite(&riffSize, 4, 1, wav); fwrite("WAVEfmt ", 1, 8, wav); unsigned fmtSize = 16; fwrite(&fmtSize, 4, 1, wav); unsigned short format = 1; fwrite(&format, 2, 1, wav); unsigned short ch = static_cast(channels); fwrite(&ch, 2, 1, wav); fwrite(&sampleRate, 4, 1, wav); fwrite(&byteRate, 4, 1, wav); unsigned short align = static_cast(audioStride); fwrite(&align, 2, 1, wav); unsigned short bits = 16; fwrite(&bits, 2, 1, wav); fwrite("data", 1, 4, wav); fwrite(&dataSize, 4, 1, wav); fwrite(audio, 1, audioBytes, wav); fclose(wav); } } } // 覆盖窗口:盖住游戏客户区 HWND gameWindow = FindGameWindowForDraw(); RECT client = {}; if (gameWindow) GetClientRect(gameWindow, &client); POINT origin = { 0, 0 }; if (gameWindow) ClientToScreen(gameWindow, &origin); const int width = (client.right - client.left) > 0 ? (client.right - client.left) : 1280; const int height = (client.bottom - client.top) > 0 ? (client.bottom - client.top) : 720; static bool classRegistered = false; if (!classRegistered) { WNDCLASSEXW wc = { sizeof(wc) }; wc.lpfnWndProc = OverlayWndProc; wc.hInstance = GetModuleHandleW(nullptr); wc.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); wc.lpszClassName = L"CN90OverlayPlayer"; RegisterClassExW(&wc); classRegistered = true; } HWND window = CreateWindowExW( WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT, L"CN90OverlayPlayer", L"", WS_POPUP | WS_VISIBLE, origin.x, origin.y, width, height, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr); if (!window) { free(data); free(audio); return false; } g_overlayWindow = window; ShowWindow(window, SW_SHOW); SetWindowPos(window, HWND_TOPMOST, origin.x, origin.y, width, height, SWP_SHOWWINDOW); LogLine("overlay player window=%p rect=%dx%d frames=%d audio=%u bytes", window, width, height, g_overlayFrameCount, static_cast(audioBytes)); if (wavPath[0]) PlaySoundW(wavPath, nullptr, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); Gdiplus::GdiplusStartupInput startupInput; ULONG_PTR token = 0; Gdiplus::GdiplusStartup(&token, &startupInput, nullptr); const double frameMs = 1001.0 / 30.0; const DWORD startedAt = GetTickCount(); HDC windowDc = GetDC(window); for (int index = 0; index < g_overlayFrameCount; ++index) { if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) { LogLine("overlay player stopped by ESC at frame %d", index); break; } MSG message; while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&message); DispatchMessageW(&message); } const OverlayFrameEntry& entry = g_overlayFrames[index]; HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, entry.size); if (memory) { void* target = GlobalLock(memory); if (target) memcpy(target, data + entry.offset, entry.size); GlobalUnlock(memory); IStream* stream = nullptr; if (SUCCEEDED(CreateStreamOnHGlobal(memory, TRUE, &stream)) && stream) { Gdiplus::Bitmap* bitmap = Gdiplus::Bitmap::FromStream(stream); if (bitmap && bitmap->GetLastStatus() == Gdiplus::Ok && windowDc) { const double scaleX = static_cast(width) / bitmap->GetWidth(); const double scaleY = static_cast(height) / bitmap->GetHeight(); const double scale = scaleX < scaleY ? scaleX : scaleY; const int drawWidth = static_cast(bitmap->GetWidth() * scale); const int drawHeight = static_cast(bitmap->GetHeight() * scale); const int drawX = (width - drawWidth) / 2; const int drawY = (height - drawHeight) / 2; Gdiplus::Graphics graphics(windowDc); graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBilinear); graphics.DrawImage(bitmap, drawX, drawY, drawWidth, drawHeight); } delete bitmap; stream->Release(); } } const DWORD targetTime = startedAt + static_cast(index * frameMs); DWORD now = GetTickCount(); while (now < targetTime) { const DWORD slice = (targetTime - now) < 8 ? (targetTime - now) : 8; Sleep(slice); now = GetTickCount(); if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) break; } } if (windowDc) ReleaseDC(window, windowDc); Gdiplus::GdiplusShutdown(token); if (wavPath[0]) PlaySoundW(nullptr, nullptr, SND_PURGE); DestroyWindow(window); g_overlayWindow = nullptr; if (wavPath[0]) DeleteFileW(wavPath); free(data); free(audio); g_overlayFrameCount = 0; LogLine("overlay player finished"); return true; } static bool PlayCutsceneByPath(const char* relative) { if (!relative || !relative[0]) return false; wchar_t wide[MAX_PATH] = {}; if (!ResolveClientRelativePath(wide, MAX_PATH, relative)) return false; // 优先走补丁内置播放器(支持我们自己生成的 MJPEG AVI,不依赖游戏播放器)。 // 非 MJPEG(例如章节动画)会自动返回 false,继续走游戏播放器。 if (OverlayPlay(wide)) return true; wchar_t relativeWide[MAX_PATH] = {}; if (MultiByteToWideChar(CP_ACP, 0, relative, -1, relativeWide, MAX_PATH) <= 0) return false; if (PlayCutsceneWithCapturedPlayer(relativeWide)) return true; return CallClientMoviePlayer(wide); } static DWORD WINAPI CutsceneFileWatcher(LPVOID) { for (;;) { Sleep(250); wchar_t marker[MAX_PATH] = {}; if (!ResolveClientRelativePath(marker, MAX_PATH, "90CN_play_cutscene.txt")) continue; WIN32_FILE_ATTRIBUTE_DATA attributes = {}; if (!GetFileAttributesExW(marker, GetFileExInfoStandard, &attributes)) continue; char text[260] = {}; FILE* file = nullptr; if (_wfopen_s(&file, marker, L"rb") == 0 && file) { const size_t read = fread(text, 1, sizeof(text) - 1, file); text[read] = '\0'; fclose(file); } DeleteFileW(marker); // 去掉首尾空白与换行(服务端写文件时可能带 \n) char trimmed[260] = {}; size_t begin = 0, end = strlen(text); while (begin < end && (text[begin] == ' ' || text[begin] == '\t' || text[begin] == '\r' || text[begin] == '\n')) ++begin; while (end > begin && (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\r' || text[end - 1] == '\n')) --end; for (size_t i = begin; i < end && (i - begin) < sizeof(trimmed) - 1; ++i) trimmed[i - begin] = text[i]; const int sequence = atoi(trimmed); const bool isPath = trimmed[0] && (sequence <= 0 || sequence > 99 || strchr(trimmed, '.') || strchr(trimmed, '/') || strchr(trimmed, '\\')); const bool played = isPath ? PlayCutsceneByPath(trimmed) : PlayCutsceneBySequence(sequence); LogLine("cutscene file trigger sequence=%d path=%d played=%d", sequence, isPath ? 1 : 0, played ? 1 : 0); } return 0; } static void StartCutsceneFileWatcher() { if (InterlockedCompareExchange(&g_movieWatcherStarted, 1, 0) != 0) return; HANDLE thread = CreateThread(nullptr, 0, CutsceneFileWatcher, nullptr, 0, nullptr); if (thread) CloseHandle(thread); } // ── 影片绑定校验(movie-bind-guard)────────────────────────────────── // // 客户端与 Video/movie/demo_mjpeg.avi 深度绑定:文件缺失/被占位/不是合法 AVI 时 // 直接终止进程,客户端无法启动。 // 校验放在最前面,任何游戏逻辑之前执行。 static const wchar_t* kBoundMovieRelative = L"Video/movie/demo_mjpeg.avi"; static const DWORD kBoundMovieMinBytes = 64 * 1024; // 影片指纹:编译期固化。文件被改动、替换或删除 → 客户端无法启动。 // SHA-256 = D62E3623C39CD9FCED5F6C4C7F5E6E18BEB69EB269ABF5D713EDE08EE8C926A1 // Size = 22846532 字节 static const unsigned char kBoundMovieSha256[32] = { 0xD6, 0x2E, 0x36, 0x23, 0xC3, 0x9C, 0xD9, 0xFC, 0xED, 0x5F, 0x6C, 0x4C, 0x7F, 0x5E, 0x6E, 0x18, 0xBE, 0xB6, 0x9E, 0xB2, 0x69, 0xAB, 0xF5, 0xD7, 0x13, 0xED, 0xE0, 0x8E, 0xE8, 0xC9, 0x26, 0xA1 }; static const unsigned long long kBoundMovieExpectedBytes = 22846532ULL; // 用 CryptoAPI 计算文件 SHA-256。 static bool ComputeFileSha256(const wchar_t* path, unsigned char digest[32]) { FILE* file = nullptr; if (_wfopen_s(&file, path, L"rb") != 0 || !file) return false; HCRYPTPROV provider = 0; HCRYPTHASH hash = 0; bool ok = false; if (CryptAcquireContextW(&provider, nullptr, nullptr, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) { if (CryptCreateHash(provider, CALG_SHA_256, 0, 0, &hash)) { unsigned char buffer[65536]; size_t read = 0; bool readOk = true; while ((read = fread(buffer, 1, sizeof(buffer), file)) > 0) { if (!CryptHashData(hash, buffer, static_cast(read), 0)) { readOk = false; break; } } if (readOk && !ferror(file)) { DWORD digestLength = 32; ok = CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLength, 0) != FALSE && digestLength == 32; } } } if (hash) CryptDestroyHash(hash); if (provider) CryptReleaseContext(provider, 0); fclose(file); return ok; } static bool VerifyBoundMoviePresent() { wchar_t path[MAX_PATH] = {}; char narrow[MAX_PATH] = {}; WideCharToMultiByte(CP_ACP, 0, kBoundMovieRelative, -1, narrow, sizeof(narrow), nullptr, nullptr); if (!ResolveClientRelativePath(path, MAX_PATH, narrow)) return false; WIN32_FILE_ATTRIBUTE_DATA attributes = {}; if (!GetFileAttributesExW(path, GetFileExInfoStandard, &attributes)) { LogLine("movie bind check: missing file"); return false; } if (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { LogLine("movie bind check: path is a directory"); return false; } const unsigned long long size = (static_cast(attributes.nFileSizeHigh) << 32) | attributes.nFileSizeLow; if (size != kBoundMovieExpectedBytes) { LogLine("movie bind check: size mismatch (%llu != %llu)", size, kBoundMovieExpectedBytes); return false; } unsigned char digest[32] = {}; if (!ComputeFileSha256(path, digest)) { LogLine("movie bind check: hash computation failed"); return false; } if (memcmp(digest, kBoundMovieSha256, sizeof(digest)) != 0) { LogLine("movie bind check: hash mismatch"); return false; } LogLine("movie bind check: ok size=%llu sha256=verified", size); return true; } // ── 自校验(self-integrity-crc)──────────────────────────────────────── // // 构建后由 patch_selfcrc.py 把自身 .text 段的 CRC32 写进 g_selfCrcExpected。 // 运行时重新读取磁盘上的本 DLL,算 .text 段 CRC 并比对: // 任何字节被改动(比如 patch 掉校验分支)→ CRC 不符 → 客户端无法启动。 // 占位初值必须是 0xC0DEC0DE,脚本靠它定位写入位置。 // ── 自校验(self-integrity-crc)──────────────────────────────────────── // // 构建后由 patch_dll_crc.py 写入两个字段: // offset = 本结构 expected 字段在文件中的绝对偏移 // expected = 整个文件的 CRC32(计算时跳过 [offset, offset+4),避免自指) // 运行时重新读盘、用同样规则重算并比对;任何字节被改动 → 不符 → 客户端无法启动。 static struct SelfCrcSlot { unsigned int magic; // 固定 0xC0DEC0DE unsigned int offset; // expected 字段的文件偏移(脚本写入) unsigned int expected; // 文件 CRC32(脚本写入) } g_selfCrcSlot = { 0xC0DEC0DEu, 0u, 0u }; static unsigned int Crc32Update(unsigned int crc, const unsigned char* data, size_t length) { for (size_t i = 0; i < length; ++i) { crc ^= data[i]; for (int bit = 0; bit < 8; ++bit) { const unsigned int mask = (crc & 1u) ? 0xEDB88320u : 0u; crc = (crc >> 1) ^ mask; } } return crc; } static bool VerifySelfIntegrity() { wchar_t path[MAX_PATH] = {}; // 用数据变量地址解析自身模块;函数地址在某些构建/加载方式下解析不到(实测 err=126)。 HMODULE selfModule = nullptr; if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(&g_selfCrcSlot), &selfModule) || !selfModule) { LogLine("self crc diag: GetModuleHandleEx failed err=%u", GetLastError()); return false; } if (!GetModuleFileNameW(selfModule, path, MAX_PATH)) { LogLine("self crc diag: GetModuleFileName failed err=%u", GetLastError()); return false; } FILE* file = nullptr; if (_wfopen_s(&file, path, L"rb") != 0 || !file) { LogLine("self crc diag: open failed"); return false; } fseek(file, 0, SEEK_END); const long size = ftell(file); fseek(file, 0, SEEK_SET); if (size < 4096) { LogLine("self crc diag: file too small size=%ld", size); fclose(file); return false; } unsigned char* image = static_cast(malloc(static_cast(size))); if (!image) { fclose(file); return false; } const size_t read = fread(image, 1, static_cast(size), file); fclose(file); if (read != static_cast(size)) { free(image); return false; } const unsigned int slotOffset = g_selfCrcSlot.offset; const unsigned int expected = g_selfCrcSlot.expected; bool ok = false; if (g_selfCrcSlot.magic != 0xC0DEC0DEu || slotOffset < 16 || static_cast(slotOffset) + 4 > static_cast(size)) { LogLine("self crc diag: slot not patched magic=0x%08X offset=0x%X", g_selfCrcSlot.magic, slotOffset); } else { unsigned int crc = 0xFFFFFFFFu; crc = Crc32Update(crc, image, slotOffset); crc = Crc32Update(crc, image + slotOffset + 4, static_cast(size) - slotOffset - 4); crc ^= 0xFFFFFFFFu; ok = (crc == expected); LogLine("self crc check: computed=0x%08X expected=0x%08X offset=0x%X ok=%d", crc, expected, slotOffset, ok ? 1 : 0); } free(image); return ok; } // ── 物品使用等待字节卡死修复 ──────────────────────────────────────────── // // 客户端在 obj+0x8BE 上记录“物品使用等待”:点击药品 → 置 1,动画/响应结束后清 0。 // 用药动画期间强行退出副本会让它永远停在 1,之后所有药品点击都在 0x01F3B0AE 提前返回, // 表现为“完全没反应”,只有小退重登才恢复(实测 2026-10-19,角色 36)。 // // 0x01F3B0A7 的原指令是 `call 0x022906E0`(读该字节的 getter)。这里把它换成 // 我们的实现:字节正常时行为不变;卡住超过 1.2 秒就清零并放行。 static volatile DWORD g_itemUseWaitSinceTick = 0; static unsigned char __fastcall QueryItemUseWaitByte(void* self) { if (!self) return 0; unsigned char* flag = reinterpret_cast(self) + 0x8BE; if (*flag == 0) { g_itemUseWaitSinceTick = 0; return 0; } if (g_itemUseGateResetArmed) { g_itemUseGateResetArmed = 0; *flag = 0; g_itemUseWaitSinceTick = 0; LogLine("item-use wait byte cleared on first use after dungeon entry"); return 0; } const DWORD now = GetTickCount(); if (g_itemUseWaitSinceTick == 0) g_itemUseWaitSinceTick = now; if (now - g_itemUseWaitSinceTick >= 1200) { *flag = 0; g_itemUseWaitSinceTick = 0; LogLine("item-use wait byte stuck >=1200ms, cleared self=%p", self); return 0; } return 1; } // ── op32 重建分支探针(只读,方案一第 1 步)──────────────────────────── // // 目标:拿到“actor 重建分支”里的 actor 指针、vtable,以及分支里调用的 4 个 // 虚方法(vtable +0x418 / +0x420 / +0x63C / +0xC04)的真实地址。下一步从这些 // 方法里定位“安装蛇腹剑一次性被动”的子函数。 // 分支入口 VA 0x1D40C31(RVA 0x1940C31),前 6 字节是 mov edx,[ebp-0x35C]。 // 运行时内存扫描:重建后查找 UTF-16「已复活」,定位字符串来源。 static volatile LONG g_reviveTextScanStarted = 0; static DWORD WINAPI ReviveTextScanThread(void*) { Sleep(1500); static const unsigned char needle[6] = { 0xF2, 0x5D, 0x0D, 0x59, 0x3B, 0x6D }; int hits = 0; unsigned char* cursor = nullptr; MEMORY_BASIC_INFORMATION info; while (VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info)) { unsigned char* regionBase = static_cast(info.BaseAddress); const size_t regionSize = info.RegionSize; const bool readable = info.State == MEM_COMMIT && (info.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0 && (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; if (readable && regionSize != 0) { __try { for (size_t i = 0; i + sizeof(needle) <= regionSize; ++i) { if (regionBase[i] != needle[0]) continue; if (memcmp(regionBase + i, needle, sizeof(needle)) != 0) continue; LogLine("revive-text hit at %p region=%p size=%u", regionBase + i, regionBase, static_cast(regionSize)); if (hits < 3) { const size_t before = (i < 64) ? i : 64; const size_t after = (i + sizeof(needle) + 64 <= regionSize) ? 64 : 0; char hex[3 * 160 + 1]; size_t pos = 0; for (size_t k = i - before; k < i + sizeof(needle) + after && pos + 3 < sizeof(hex); ++k) { static const char digits[] = "0123456789ABCDEF"; hex[pos++] = digits[(regionBase[k] >> 4) & 0xF]; hex[pos++] = digits[regionBase[k] & 0xF]; hex[pos++] = ' '; } hex[pos] = 0; LogLine("revive-text dump %p: %s", regionBase + i - before, hex); } if (++hits >= 8) break; i += sizeof(needle) - 1; } } __except (EXCEPTION_EXECUTE_HANDLER) { } } if (hits >= 8) break; unsigned char* next = regionBase + regionSize; if (next <= cursor) break; cursor = next; } LogLine("revive-text scan done hits=%d", hits); return 0; } // ── 静默“玩家[%s]已复活”这条消息 ───────────────────────────────────── // // 2026-10-20:op32 强制 actor 重建会顺带打印“玩家[%s]已复活”(客户端运行时 // 字符串表里的模板)。这条模板不在 PVF 文本/EXE/DNFBase 明文里,只在运行时 // 解码出来的内存表里;这里在启动后扫描该模板,把结尾“已复活”三个字改成空格, // 消息仍会生成但显示为空。 static const unsigned char kReviveTemplate[] = { 0xA9, 0x73, // 玩 U+73A9 0xB6, 0x5B, // 家 U+5BB6 0x5B, 0x00, // [ 0x25, 0x00, // % 0x73, 0x00, // s 0x5D, 0x00, // ] 0xF2, 0x5D, // 已 U+5DF2 0x0D, 0x59, // 复 U+590D 0x3B, 0x6D, // 活 U+6D3B }; static int NeutralizeReviveTemplate() { int patched = 0; unsigned char* cursor = nullptr; MEMORY_BASIC_INFORMATION info; while (VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info)) { unsigned char* regionBase = static_cast(info.BaseAddress); const size_t regionSize = info.RegionSize; const bool writableish = info.State == MEM_COMMIT && (info.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0 && (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; if (writableish && regionSize >= sizeof(kReviveTemplate)) { __try { for (size_t i = 0; i + sizeof(kReviveTemplate) <= regionSize; ++i) { if (regionBase[i] != kReviveTemplate[0]) continue; if (memcmp(regionBase + i, kReviveTemplate, sizeof(kReviveTemplate)) != 0) continue; unsigned char* tail = regionBase + i + sizeof(kReviveTemplate) - 6; DWORD oldProtection = 0; if (VirtualProtect(tail, 6, PAGE_READWRITE, &oldProtection)) { tail[0] = 0x20; tail[1] = 0x00; tail[2] = 0x20; tail[3] = 0x00; tail[4] = 0x20; tail[5] = 0x00; DWORD ignored = 0; VirtualProtect(tail, 6, oldProtection, &ignored); ++patched; LogLine("revive-template patched at %p", tail); } i += sizeof(kReviveTemplate) - 1; } } __except (EXCEPTION_EXECUTE_HANDLER) { } } unsigned char* next = regionBase + regionSize; if (next <= cursor) break; cursor = next; } LogLine("revive-template neutralize done patched=%d", patched); return patched; } static DWORD WINAPI ReviveTemplateWatcher(void*) { for (int i = 0; i < 24; ++i) { Sleep(5000); if (NeutralizeReviveTemplate() > 0) break; } return 0; } static unsigned char* g_op32RebuildResume = nullptr; static volatile LONG g_op32RebuildCount = 0; static volatile LONG g_op32RebuildWindow = 0; static volatile DWORD g_op32RebuildThread = 0; static void InstallActorWatchpoints(void* actor); static void* g_actorSnapshotTarget = nullptr; static unsigned char g_actorSnapshot[0x8000]; static volatile LONG g_actorSnapshotStarted = 0; static DWORD WINAPI ActorSnapshotDiffThread(void*) { Sleep(1500); void* actor = g_actorSnapshotTarget; if (!actor) return 0; static unsigned char after[0x8000]; SIZE_T read = 0; if (!ReadProcessMemory(GetCurrentProcess(), actor, after, sizeof(after), &read) || read < 4) { LogLine("actor-diff read failed"); return 0; } int logged = 0; for (size_t off = 0; off + 4 <= read; off += 4) { unsigned int before = *reinterpret_cast(g_actorSnapshot + off); unsigned int now = *reinterpret_cast(after + off); if (before == now) continue; if (logged < 300) { LogLine("actor-diff +0x%X old=%08X new=%08X", static_cast(off), before, now); ++logged; } } LogLine("actor-diff done changed=%d", logged); return 0; } extern "C" void Op32RebuildProbeLog(void* actor) { InterlockedExchange(&g_op32RebuildWindow, static_cast(GetTickCount())); InterlockedExchange(reinterpret_cast(&g_op32RebuildThread), static_cast(GetCurrentThreadId())); // 重建后 1.5 秒扫描一次内存里的「已复活」UTF-16 文本,用于定位字符串来源。 if (InterlockedCompareExchange(&g_reviveTextScanStarted, 1, 0) == 0) { HANDLE thread = CreateThread(nullptr, 0, ReviveTextScanThread, nullptr, 0, nullptr); if (thread) CloseHandle(thread); } const LONG count = InterlockedIncrement(&g_op32RebuildCount); if (count > 5) return; if (!actor) { LogLine("op32-rebuild actor=null count#%ld", count); return; } if (InterlockedCompareExchange(&g_actorSnapshotStarted, 1, 0) == 0) { g_actorSnapshotTarget = actor; SIZE_T read = 0; if (ReadProcessMemory(GetCurrentProcess(), actor, g_actorSnapshot, sizeof(g_actorSnapshot), &read)) { LogLine("actor-snapshot taken actor=%p bytes=%u", actor, static_cast(read)); HANDLE thread = CreateThread(nullptr, 0, ActorSnapshotDiffThread, nullptr, 0, nullptr); if (thread) CloseHandle(thread); } else { LogLine("actor-snapshot read failed actor=%p", actor); } } __try { unsigned char* vtable = *reinterpret_cast(actor); void* m418 = nullptr; void* m420 = nullptr; void* m63c = nullptr; void* mc04 = nullptr; if (vtable) { m418 = *reinterpret_cast(vtable + 0x418); m420 = *reinterpret_cast(vtable + 0x420); m63c = *reinterpret_cast(vtable + 0x63C); mc04 = *reinterpret_cast(vtable + 0xC04); } LogLine("op32-rebuild actor=%p vtbl=%p m418=%p m420=%p m63c=%p mc04=%p count#%ld", actor, vtable, m418, m420, m63c, mc04, count); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("op32-rebuild read fault actor=%p count#%ld", actor, count); } } __declspec(naked) static void Op32RebuildStub() { __asm { pushfd pushad mov eax, dword ptr [esp + 4] push eax call Op32RebuildProbeLog add esp, 4 popad popfd mov edx, dword ptr [ebp - 0x35C] jmp dword ptr [g_op32RebuildResume] } } static void InstallOp32RebuildProbe() { if (!g_dnfBase) return; unsigned char* target = reinterpret_cast(g_dnfBase + 0x01940C31); static const unsigned char expected[6] = { 0x8B, 0x95, 0xA4, 0xFC, 0xFF, 0xFF }; if (memcmp(target, expected, sizeof(expected)) != 0) { LogLine("op32-rebuild-probe skip: prologue mismatch at %p", target); return; } g_op32RebuildResume = target + sizeof(expected); DWORD oldProtection = 0; if (!VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("op32-rebuild-probe install failed: VirtualProtect err=%u", GetLastError()); return; } unsigned char* stub = reinterpret_cast(&Op32RebuildStub); const intptr_t delta = stub - (target + 5); target[0] = 0xE9; *reinterpret_cast(target + 1) = static_cast(delta); DWORD ignored = 0; VirtualProtect(target, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), target, 5); LogLine("op32-rebuild-probe installed target=%p stub=%p resume=%p", target, stub, g_op32RebuildResume); } // ── 方案一第 2 步:重建分支 4 个虚方法入口探针(时间窗内记录)──────────── // // 由 0x1D40C31 的重建分支探针打开时间窗;窗内记录这 4 个方法的调用顺序, // 用来确认“安装蛇腹剑被动”发生在哪一层。 static volatile LONG g_op32M418Count = 0; static volatile LONG g_op32M420Count = 0; static volatile LONG g_op32M63cCount = 0; static volatile LONG g_op32Mc04Count = 0; static unsigned char* g_op32M418Resume = nullptr; static unsigned char* g_op32M420Resume = nullptr; static unsigned char* g_op32M63cResume = nullptr; static unsigned char* g_op32Mc04Resume = nullptr; static bool InOp32RebuildWindow() { const LONG start = InterlockedCompareExchange(&g_op32RebuildWindow, 0, 0); if (start == 0) return false; return static_cast(GetTickCount()) - static_cast(start) < 20; } extern "C" void Op32M418Log() { if (!InOp32RebuildWindow()) return; const LONG n = InterlockedIncrement(&g_op32M418Count); if (n <= 40) LogLine("op32-m418 call#%ld", n); } extern "C" void Op32M420Log() { if (!InOp32RebuildWindow()) return; const LONG n = InterlockedIncrement(&g_op32M420Count); if (n <= 40) LogLine("op32-m420 call#%ld", n); } extern "C" void Op32M63cLog() { if (!InOp32RebuildWindow()) return; const LONG n = InterlockedIncrement(&g_op32M63cCount); if (n <= 40) LogLine("op32-m63c call#%ld", n); } extern "C" void Op32Mc04Log() { if (!InOp32RebuildWindow()) return; const LONG n = InterlockedIncrement(&g_op32Mc04Count); if (n <= 40) LogLine("op32-mc04 call#%ld", n); } __declspec(naked) static void Op32M418Stub() { __asm { pushfd pushad call Op32M418Log popad popfd push ebp mov ebp, esp sub esp, 8 jmp dword ptr [g_op32M418Resume] } } __declspec(naked) static void Op32M420Stub() { __asm { pushfd pushad call Op32M420Log popad popfd mov eax, dword ptr [ecx] mov edx, dword ptr [eax + 0x648] jmp dword ptr [g_op32M420Resume] } } __declspec(naked) static void Op32M63cStub() { __asm { pushfd pushad call Op32M63cLog popad popfd push ebp mov ebp, esp mov eax, dword ptr [ebp + 0x10] jmp dword ptr [g_op32M63cResume] } } __declspec(naked) static void Op32Mc04Stub() { __asm { pushfd pushad call Op32Mc04Log popad popfd push ebp mov ebp, esp mov al, byte ptr [ebp + 8] jmp dword ptr [g_op32Mc04Resume] } } static void InstallMethodProbe( uintptr_t rva, const unsigned char* expected, size_t expectedBytes, unsigned char* stub, unsigned char** resumeSlot, const char* name) { unsigned char* target = reinterpret_cast(g_dnfBase + rva); if (memcmp(target, expected, expectedBytes) != 0) { LogLine("%s skip: prologue mismatch at %p", name, target); return; } *resumeSlot = target + expectedBytes; DWORD oldProtection = 0; if (!VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("%s install failed: VirtualProtect err=%u", name, GetLastError()); return; } const intptr_t delta = stub - (target + 5); target[0] = 0xE9; *reinterpret_cast(target + 1) = static_cast(delta); DWORD ignored = 0; VirtualProtect(target, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), target, 5); LogLine("%s installed target=%p resume=%p", name, target, *resumeSlot); } static void InstallOp32MethodProbes() { if (!g_dnfBase) return; static const unsigned char expected418[6] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 }; InstallMethodProbe(0x0208A9D0, expected418, sizeof(expected418), reinterpret_cast(&Op32M418Stub), &g_op32M418Resume, "op32-m418"); static const unsigned char expected420[8] = { 0x8B, 0x01, 0x8B, 0x90, 0x48, 0x06, 0x00, 0x00 }; InstallMethodProbe(0x0200A6E0, expected420, sizeof(expected420), reinterpret_cast(&Op32M420Stub), &g_op32M420Resume, "op32-m420"); static const unsigned char expected63c[6] = { 0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x10 }; InstallMethodProbe(0x01535A70, expected63c, sizeof(expected63c), reinterpret_cast(&Op32M63cStub), &g_op32M63cResume, "op32-m63c"); static const unsigned char expectedc04[6] = { 0x55, 0x8B, 0xEC, 0x8A, 0x45, 0x08 }; InstallMethodProbe(0x0209DDF0, expectedc04, sizeof(expectedc04), reinterpret_cast(&Op32Mc04Stub), &g_op32Mc04Resume, "op32-mc04"); } static void InstallOp32UiSkip() { if (!g_dnfBase) return; // 0x1D40E0F: cmp eax,3 ; jne 0x1D40E4C (5 bytes) // 直接跳到 0x1D40E8B(test ebx,ebx),跳过 0x1D40E14..0x1D40E8A 的 // “复活 UI/消息”段,保留前面的 actor 状态处理。 unsigned char* site = reinterpret_cast(g_dnfBase + 0x01940E0F); static const unsigned char expected[5] = { 0x83, 0xF8, 0x03, 0x75, 0x38 }; if (memcmp(site, expected, sizeof(expected)) != 0) { LogLine("op32-ui-skip skip: mismatch at %p", site); return; } DWORD oldProtection = 0; if (!VirtualProtect(site, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("op32-ui-skip install failed: VirtualProtect err=%u", GetLastError()); return; } const intptr_t delta = 0x01D40E8B - (0x01D40E0F + 5); site[0] = 0xE9; *reinterpret_cast(site + 1) = static_cast(delta); DWORD ignored = 0; VirtualProtect(site, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), site, 5); LogLine("op32-ui-skip installed site=%p target=01D40E8B", site); } // ── 技能对象探针:sub_2666AE0(op19 设置技能等级)────────────────────── // // 记录每次调用的 (对象指针, vtable, 等级值),用于找到技能 31 对应的对象, // 下一步再从该对象的 vtable 上 hook“取等级”方法,抓安装函数的调用者。 static unsigned char* g_skillObjectResume = nullptr; static volatile LONG g_skillObjectCount = 0; static void* g_callDaimusObject = nullptr; static void* g_callDaimusOriginalGetter = nullptr; static volatile LONG g_callDaimusHooked = 0; static volatile LONG g_callDaimusGetterLogs = 0; extern "C" void CallDaimusGetterLog(void* self, void* caller) { if (!self || self != g_callDaimusObject) return; const LONG n = InterlockedIncrement(&g_callDaimusGetterLogs); if (n <= 30) { LogLine("calldaimus-getter self=%p caller=%p count#%ld", self, caller, n); } } __declspec(naked) static void CallDaimusGetterStub() { __asm { pushfd pushad mov eax, dword ptr [esp + 24] mov edx, dword ptr [esp + 36] push edx push eax call CallDaimusGetterLog add esp, 8 popad popfd jmp dword ptr [g_callDaimusOriginalGetter] } } static void TryHookCallDaimusGetter(void* objectPtr, const char* name) { if (!objectPtr || !name) return; if (name[0] != 'C' || strcmp(name, "CallDaimus") != 0) return; if (InterlockedCompareExchange(&g_callDaimusHooked, 1, 0) != 0) return; __try { unsigned char* vtable = *reinterpret_cast(objectPtr); if (!vtable) return; void** slot = reinterpret_cast(vtable + 0xB60); void* original = *slot; const uintptr_t value = reinterpret_cast(original); if (value < 0x00400000 || value > 0x0FFFFFFF) { LogLine("calldaimus-getter skip: slot value %p not code", original); return; } g_callDaimusObject = objectPtr; g_callDaimusOriginalGetter = original; DWORD oldProtection = 0; if (!VirtualProtect(slot, 4, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("calldaimus-getter hook failed: VirtualProtect err=%u", GetLastError()); return; } *slot = reinterpret_cast(&CallDaimusGetterStub); DWORD ignored = 0; VirtualProtect(slot, 4, oldProtection, &ignored); LogLine("calldaimus-getter hooked object=%p vtbl=%p slot=%p original=%p", objectPtr, vtable, slot, original); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("calldaimus-getter hook fault object=%p", objectPtr); } } extern "C" void SkillObjectProbeLog(void* objectPtr, unsigned char* levelPtr) { const LONG count = InterlockedIncrement(&g_skillObjectCount); if (count > 60) return; unsigned int level = 0; void* vtable = nullptr; __try { if (levelPtr) level = *levelPtr; if (objectPtr) vtable = *reinterpret_cast(objectPtr); } __except (EXCEPTION_EXECUTE_HANDLER) { vtable = nullptr; } LogLine("skill-object call#%ld object=%p vtbl=%p level=%u", count, objectPtr, vtable, level); if (objectPtr) { __try { const unsigned char* raw = static_cast(objectPtr); const wchar_t* name = *reinterpret_cast(raw + 0xC); char ascii[64]; int n = 0; if (name) { for (int i = 0; i < 48 && name[i] && n < 60; ++i) { const wchar_t ch = name[i]; ascii[n++] = (ch >= 0x20 && ch < 0x7F) ? static_cast(ch) : '?'; } } ascii[n] = 0; LogLine("skill-object name: %s", ascii); TryHookCallDaimusGetter(objectPtr, ascii); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("skill-object name fault object=%p", objectPtr); } } if (count <= 12 && objectPtr) { char hex[3 * 16 * 4 + 1]; size_t pos = 0; __try { const unsigned int* words = static_cast(objectPtr); for (int i = 0; i < 16 && pos + 16 < sizeof(hex); ++i) { const unsigned int value = words[i]; static const char digits[] = "0123456789ABCDEF"; for (int shift = 28; shift >= 0; shift -= 4) { hex[pos++] = digits[(value >> shift) & 0xF]; } hex[pos++] = ' '; } } __except (EXCEPTION_EXECUTE_HANDLER) { hex[pos++] = '?'; } hex[pos] = 0; LogLine("skill-object dump %p: %s", objectPtr, hex); if (count <= 6) { const unsigned int* words = static_cast(objectPtr); __try { const unsigned int* p1 = reinterpret_cast(words[2]); const unsigned int* p2 = reinterpret_cast(words[3]); char hex2[3 * 16 * 4 + 1]; size_t pos2 = 0; static const char digits2[] = "0123456789ABCDEF"; for (int i = 0; i < 16; ++i) { const unsigned int value = p1[i]; for (int shift = 28; shift >= 0; shift -= 4) { hex2[pos2++] = digits2[(value >> shift) & 0xF]; } hex2[pos2++] = ' '; } hex2[pos2] = 0; LogLine("skill-object p1 %p: %s", p1, hex2); pos2 = 0; for (int i = 0; i < 16; ++i) { const unsigned int value = p2[i]; for (int shift = 28; shift >= 0; shift -= 4) { hex2[pos2++] = digits2[(value >> shift) & 0xF]; } hex2[pos2++] = ' '; } hex2[pos2] = 0; LogLine("skill-object p2 %p: %s", p2, hex2); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("skill-object deref fault object=%p", objectPtr); } } } } __declspec(naked) static void SkillObjectProbeStub() { __asm { pushfd pushad mov eax, dword ptr [esp + 24] mov edx, dword ptr [esp + 40] mov ecx, dword ptr [esp + 44] push ecx push edx push eax call SkillObjectProbeLog add esp, 0Ch popad popfd push ebp mov ebp, esp push ebx push esi mov esi, dword ptr [ebp + 0Ch] movzx ebx, byte ptr [esi] jmp dword ptr [g_skillObjectResume] } } static void InstallSkillObjectProbe() { if (!g_dnfBase) return; unsigned char* target = reinterpret_cast(g_dnfBase + 0x02266AE0); static const unsigned char expected[11] = { 0x55, 0x8B, 0xEC, 0x53, 0x56, 0x8B, 0x75, 0x0C, 0x0F, 0xB6, 0x1E }; if (memcmp(target, expected, sizeof(expected)) != 0) { LogLine("skill-object-probe skip: prologue mismatch at %p", target); return; } g_skillObjectResume = target + sizeof(expected); DWORD oldProtection = 0; if (!VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("skill-object-probe install failed: VirtualProtect err=%u", GetLastError()); return; } unsigned char* stub = reinterpret_cast(&SkillObjectProbeStub); const intptr_t delta = stub - (target + 5); target[0] = 0xE9; *reinterpret_cast(target + 1) = static_cast(delta); DWORD ignored = 0; VirtualProtect(target, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), target, 5); LogLine("skill-object-probe installed target=%p resume=%p", target, g_skillObjectResume); } // ── 分配点追踪:hook tbbmalloc 的 scalable_malloc IAT ────────────────── // // 客户端对象分配走 tbbmalloc(导入表可见 tbbmalloc.dll / scalable_malloc)。 // 在 op32 重建时间窗内记录每次分配的 size 与调用点返回地址,用来定位 // “创建蛇腹剑形态控制器”的那次分配。 typedef void* (__cdecl* ScalableMallocFn)(size_t); extern "C" __declspec(dllimport) USHORT __stdcall RtlCaptureStackBackTrace( ULONG FramesToSkip, ULONG FramesToCapture, PVOID* BackTrace, PULONG BackTraceHash); static ScalableMallocFn g_realScalableMalloc = nullptr; static volatile LONG g_tbbAllocLogs = 0; struct TbbAllocSite { unsigned int size; void* caller; void* frames[4]; unsigned int count; }; static TbbAllocSite g_tbbAllocSites[192]; static volatile LONG g_tbbAllocSiteCount = 0; static CRITICAL_SECTION g_tbbAllocLock; static volatile LONG g_tbbAllocLockReady = 0; extern "C" void* __cdecl ScalableMallocHook(size_t size) { if (size < 0x8000 && InOp32RebuildWindow() && GetCurrentThreadId() == static_cast(g_op32RebuildThread)) { void* caller = _ReturnAddress(); void* frames[6] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; const USHORT captured = RtlCaptureStackBackTrace(1, 6, frames, nullptr); void* f1 = captured > 1 ? frames[1] : nullptr; void* f2 = captured > 2 ? frames[2] : nullptr; void* f3 = captured > 3 ? frames[3] : nullptr; if (InterlockedCompareExchange(&g_tbbAllocLockReady, 1, 1) == 1) { EnterCriticalSection(&g_tbbAllocLock); bool found = false; const LONG used = InterlockedCompareExchange(&g_tbbAllocSiteCount, 0, 0); for (LONG i = 0; i < used; ++i) { if (g_tbbAllocSites[i].size == static_cast(size) && g_tbbAllocSites[i].caller == caller && g_tbbAllocSites[i].frames[0] == f1 && g_tbbAllocSites[i].frames[1] == f2) { ++g_tbbAllocSites[i].count; found = true; break; } } if (!found && used < static_cast(sizeof(g_tbbAllocSites) / sizeof(g_tbbAllocSites[0]))) { g_tbbAllocSites[used].size = static_cast(size); g_tbbAllocSites[used].caller = caller; g_tbbAllocSites[used].frames[0] = f1; g_tbbAllocSites[used].frames[1] = f2; g_tbbAllocSites[used].frames[2] = f3; g_tbbAllocSites[used].count = 1; InterlockedIncrement(&g_tbbAllocSiteCount); LogLine("tbb-alloc new size=%u caller=%p bt=%p,%p,%p", static_cast(size), caller, f1, f2, f3); } LeaveCriticalSection(&g_tbbAllocLock); } } return g_realScalableMalloc(size); } static bool HookIatFunction( HMODULE module, const char* dllName, const char* functionName, void* hook, void** original) { if (!module || !dllName || !functionName || !hook) return false; unsigned char* base = reinterpret_cast(module); __try { IMAGE_DOS_HEADER* dos = reinterpret_cast(base); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; IMAGE_NT_HEADERS* nt = reinterpret_cast(base + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) return false; IMAGE_DATA_DIRECTORY dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; if (dir.VirtualAddress == 0) return false; IMAGE_IMPORT_DESCRIPTOR* desc = reinterpret_cast(base + dir.VirtualAddress); for (; desc->Name != 0; ++desc) { const char* moduleName = reinterpret_cast(base + desc->Name); if (_stricmp(moduleName, dllName) != 0) continue; IMAGE_THUNK_DATA* first = reinterpret_cast(base + desc->FirstThunk); IMAGE_THUNK_DATA* names = desc->OriginalFirstThunk ? reinterpret_cast(base + desc->OriginalFirstThunk) : nullptr; for (DWORD i = 0; first[i].u1.Function != 0; ++i) { if (!names || names[i].u1.AddressOfData == 0) continue; if (IMAGE_SNAP_BY_ORDINAL(names[i].u1.Ordinal)) continue; IMAGE_IMPORT_BY_NAME* entry = reinterpret_cast( base + names[i].u1.AddressOfData); if (strcmp(reinterpret_cast(entry->Name), functionName) != 0) continue; void** slot = reinterpret_cast(&first[i].u1.Function); if (*original == nullptr) *original = *slot; DWORD oldProtection = 0; if (!VirtualProtect(slot, sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtection)) { return false; } *slot = hook; DWORD ignored = 0; VirtualProtect(slot, sizeof(void*), oldProtection, &ignored); LogLine("iat-hook %s!%s slot=%p original=%p hook=%p", dllName, functionName, slot, *original, hook); return true; } } } __except (EXCEPTION_EXECUTE_HANDLER) { } return false; } static void InstallAllocationTracer() { HMODULE exe = reinterpret_cast(g_dnfBase); if (g_realScalableMalloc) return; InitializeCriticalSection(&g_tbbAllocLock); InterlockedExchange(&g_tbbAllocLockReady, 1); if (HookIatFunction(exe, "tbbmalloc.dll", "scalable_malloc", reinterpret_cast(&ScalableMallocHook), reinterpret_cast(&g_realScalableMalloc))) { return; } HMODULE baseDll = GetModuleHandleW(L"DNFBase.dll"); if (baseDll && HookIatFunction(baseDll, "tbbmalloc.dll", "scalable_malloc", reinterpret_cast(&ScalableMallocHook), reinterpret_cast(&g_realScalableMalloc))) { return; } LogLine("allocation-tracer: scalable_malloc import not found"); } // ── 静默 op32 重建分支里的消息调用 ───────────────────────────────────── // // 2026-10-20:指令追踪显示重建分支(0x1D40C31)的第一个调用 // 0x1D40C3D: call 0x026B7230 (ecx=全局 0x51CCFD8, edx=[ebp-0x35C]) // 进入后是大量字符串/UI/系统模块工作,怀疑是“玩家[xxx]已复活”这条消息的 // 打印入口。这里把该调用改成 5 个 NOP 做验证:文字应消失、形态应保留。 static void InstallOp32MessageSkip() { if (!g_dnfBase) return; unsigned char* site = reinterpret_cast(g_dnfBase + 0x01940C3D); if (site[0] != 0xE8) { LogLine("op32-message-skip skip: opcode mismatch at %p", site); return; } DWORD oldProtection = 0; if (!VirtualProtect(site, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("op32-message-skip install failed: VirtualProtect err=%u", GetLastError()); return; } memset(site, 0x90, 5); DWORD ignored = 0; VirtualProtect(site, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), site, 5); LogLine("op32-message-skip installed site=%p", site); } // ── 读取 actor vtable 关键槽位(启动时一次)──────────────────────────── // // 2026-10-20:Ghidra 反编译重建分支后,确认它调用 actor 的 9 个虚方法: // +0x18 +0xB54 +0x63C +0xC04 +0x420 +0x418 +0x428 +0x750 +0xACC // actor vtable 在运行时是 0x0449DD5C(EXE 基址 0x400000)。这里在启动时读取并 // 打印这些槽位的函数地址,之后用 Ghidra 逐个反编译找“安装被动”的那个。 static void InstallActorVtableProbe() { if (!g_dnfBase) return; static const unsigned int offsets[] = { 0x18, 0xB54, 0x63C, 0xC04, 0x420, 0x418, 0x428, 0x750, 0xACC }; __try { unsigned char* vtable = reinterpret_cast( g_dnfBase + (0x0449DD5C - 0x400000)); for (int i = 0; i < 9; ++i) { const unsigned int offset = offsets[i]; void* fn = *reinterpret_cast(vtable + offset); LogLine("actor-vtable +0x%X -> %p", offset, fn); } } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("actor-vtable probe fault"); } } // ── 手动触发的 actor 快照(A/B 差分实验)────────────────────────────── // // 轮询 D:\90server\Client\90CN_snap.flag;文件存在时扫描内存里第一个 // “vtable == 0x0449DD5C” 的对象(本地玩家 actor),把 0x8000 字节 dump 到 // D:\90server\Client\snap_.bin,用于“无形态 vs 有形态”两组对比。 static void* FindActorByVtable(unsigned int vtableVa) { void* found = nullptr; unsigned char* cursor = nullptr; MEMORY_BASIC_INFORMATION info; while (VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info)) { unsigned char* regionBase = static_cast(info.BaseAddress); const size_t regionSize = info.RegionSize; const bool readable = info.State == MEM_COMMIT && (info.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0 && (info.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0; if (readable && regionSize >= 32) { __try { for (size_t i = 0; i + 32 <= regionSize; i += 4) { unsigned int value = *reinterpret_cast(regionBase + i); if (value != vtableVa) continue; unsigned char* candidate = regionBase + i; static unsigned char probe[0x1000]; SIZE_T got = 0; if (!ReadProcessMemory(GetCurrentProcess(), candidate, probe, sizeof(probe), &got) || got < 0x100) { continue; } if (*reinterpret_cast(probe) != vtableVa) continue; bool nonzero = false; for (int k = 4; k < 64; ++k) { if (probe[k] != 0) { nonzero = true; break; } } if (!nonzero) continue; found = candidate; break; } } __except (EXCEPTION_EXECUTE_HANDLER) { } } if (found) break; unsigned char* next = regionBase + regionSize; if (next <= cursor) break; cursor = next; } return found; } static DWORD WINAPI SnapshotWatcherThread(void*) { static int counter = 0; for (;;) { Sleep(500); if (GetFileAttributesW(L"D:\\90server\\Client\\90CN_snap.flag") == INVALID_FILE_ATTRIBUTES) { continue; } DeleteFileW(L"D:\\90server\\Client\\90CN_snap.flag"); void* actor = FindActorByVtable(0x0449DD5C); if (!actor) { LogLine("snap: actor not found"); continue; } ++counter; char path[128]; sprintf_s(path, "D:\\90server\\Client\\snap_%d.bin", counter); HANDLE file = CreateFileA(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { LogLine("snap: create file failed %s", path); continue; } static unsigned char buffer[0x8000]; SIZE_T total = 0; for (SIZE_T off = 0; off < sizeof(buffer); off += 0x1000) { SIZE_T got = 0; if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(actor) + off, buffer + off, 0x1000, &got)) { break; } total += got; } if (total > 0) { DWORD written = 0; WriteFile(file, buffer, static_cast(total), &written, nullptr); LogLine("snap: actor=%p bytes=%u -> %s", actor, static_cast(written), path); } else { LogLine("snap: read failed actor=%p", actor); } CloseHandle(file); } return 0; } static void StartSnapshotWatcher() { HANDLE thread = CreateThread(nullptr, 0, SnapshotWatcherThread, nullptr, 0, nullptr); if (thread) CloseHandle(thread); LogLine("snap: watcher started"); } // ── actor 形态字段硬件写断点(DR0/DR1)──────────────────────────────── // // A/B 快照差分发现:形态出现时 actor+0x7E4 / actor+0x8D8 会从 0 变成指针 // 0x4A636000,actor+0x86C..0x874 会从 0 变成浮点。这里在重建分支拿到 actor // 后给当前线程设硬件写断点,抓“写这两个字段”的指令地址和调用者。 static volatile LONG g_actorWatchArmed = 0; static LONG CALLBACK ActorWatchpointHandler(PEXCEPTION_POINTERS info) { if (!info || !info->ExceptionRecord) return EXCEPTION_CONTINUE_SEARCH; if (info->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) { return EXCEPTION_CONTINUE_SEARCH; } CONTEXT* ctx = info->ContextRecord; if (!ctx || (ctx->Dr6 & 0x3) == 0) return EXCEPTION_CONTINUE_SEARCH; LogLine("actor-watch hit eip=%p dr6=%08X dr0=%08X dr1=%08X", reinterpret_cast(ctx->Eip), ctx->Dr6, ctx->Dr0, ctx->Dr1); __try { const unsigned int* stack = reinterpret_cast(ctx->Esp); LogLine("actor-watch stack %08X %08X %08X %08X", stack[0], stack[1], stack[2], stack[3]); } __except (EXCEPTION_EXECUTE_HANDLER) { } ctx->Dr6 = 0; return EXCEPTION_CONTINUE_EXECUTION; } static void InstallActorWatchpoints(void* actor) { if (!actor) return; if (InterlockedCompareExchange(&g_actorWatchArmed, 1, 0) != 0) return; __try { const DWORD base = reinterpret_cast(actor); HANDLE thread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, GetCurrentThreadId()); if (!thread) return; CONTEXT ctx; ZeroMemory(&ctx, sizeof(ctx)); ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (GetThreadContext(thread, &ctx)) { ctx.Dr0 = base + 0x7E4; ctx.Dr1 = base + 0x8D8; ctx.Dr6 = 0; ctx.Dr7 = (1 << 0) | (1 << 2) | (0x1 << 16) | (0x1 << 20) | (0x3 << 18) | (0x3 << 22); if (SetThreadContext(thread, &ctx)) { LogLine("actor-watch armed actor=%p dr0=%08X dr1=%08X dr7=%08X", actor, ctx.Dr0, ctx.Dr1, ctx.Dr7); } else { LogLine("actor-watch SetThreadContext failed err=%u", GetLastError()); } } CloseHandle(thread); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("actor-watch setup fault"); } } static void StartActorWatchpointHandler() { static PVOID handler = nullptr; if (!handler) { handler = AddVectoredExceptionHandler(1, ActorWatchpointHandler); LogLine("actor-watch handler=%p", handler); } } // ── 0x0254C4A9 形态设置指令的 INT3 软件断点 ───────────────────────────── // // 2026-10-21:x32dbg 已确认该指令是“设置形态字段”的写入点 // (mov [edi+0x7E4], eax;随后 [edi+0x8D8]=esi)。这里用 INT3 + VEH 自动 // 捕获命中时的寄存器、[ebp+8/0xC/0x10] 参数和返回地址,命中后恢复原字节、 // 单步执行、再重新下断,避免硬件断点导致客户端崩溃。 #define ACTOR_SET_INSN_VA 0x0254C4A9u static volatile LONG g_actorSetHookArmed = 0; static volatile LONG g_actorSetStepping = 0; static volatile LONG g_actorSetHits = 0; static unsigned char* g_actorSetInsn = nullptr; static unsigned char g_actorSetOriginal = 0; static void WriteActorSetByte(unsigned char value) { DWORD oldProtect = 0; if (VirtualProtect(g_actorSetInsn, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) { *g_actorSetInsn = value; VirtualProtect(g_actorSetInsn, 1, oldProtect, &oldProtect); FlushInstructionCache(GetCurrentProcess(), g_actorSetInsn, 1); } } static LONG CALLBACK ActorSetInt3Handler(PEXCEPTION_POINTERS info) { if (!info || !info->ExceptionRecord || !info->ContextRecord) { return EXCEPTION_CONTINUE_SEARCH; } CONTEXT* ctx = info->ContextRecord; const DWORD code = info->ExceptionRecord->ExceptionCode; if (code == EXCEPTION_BREAKPOINT) { if (ctx->Eip != ACTOR_SET_INSN_VA + 1) { return EXCEPTION_CONTINUE_SEARCH; } const LONG hit = InterlockedIncrement(&g_actorSetHits); if (hit <= 12) { LogLine("actor-set hit#%ld edi=%08X eax=%08X esi=%08X ebp=%08X esp=%08X eip=%08X", hit, ctx->Edi, ctx->Eax, ctx->Esi, ctx->Ebp, ctx->Esp, ctx->Eip - 1); __try { const unsigned int* ebp = reinterpret_cast(ctx->Ebp); LogLine("actor-set args [ebp+8]=%08X [ebp+C]=%08X [ebp+10]=%08X ret=%08X", ebp[2], ebp[3], ebp[4], *reinterpret_cast(ctx->Esp)); } __except (EXCEPTION_EXECUTE_HANDLER) { } } // 恢复原字节,回到指令起点,单步执行后重新下断。 WriteActorSetByte(g_actorSetOriginal); InterlockedExchange(&g_actorSetStepping, 1); ctx->Eip = ACTOR_SET_INSN_VA; ctx->EFlags |= 0x100; // TF return EXCEPTION_CONTINUE_EXECUTION; } if (code == EXCEPTION_SINGLE_STEP && InterlockedCompareExchange(&g_actorSetStepping, 0, 1) == 1) { WriteActorSetByte(0xCC); ctx->EFlags &= ~0x100u; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } static void InstallActorSetInt3Hook() { if (InterlockedCompareExchange(&g_actorSetHookArmed, 1, 0) != 0) return; if (!g_dnfBase) return; g_actorSetInsn = reinterpret_cast(g_dnfBase + (ACTOR_SET_INSN_VA - 0x400000)); __try { g_actorSetOriginal = *g_actorSetInsn; AddVectoredExceptionHandler(1, ActorSetInt3Handler); WriteActorSetByte(0xCC); LogLine("actor-set int3 armed at %p original=%02X", g_actorSetInsn, g_actorSetOriginal); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("actor-set int3 install fault"); } } static void InstallItemUseWaitRecovery() { if (!g_dnfBase) return; unsigned char* site = reinterpret_cast(g_dnfBase + 0x01F3B0A7); static const unsigned char expected[5] = { 0xE8, 0x34, 0x56, 0x35, 0x00 }; if (memcmp(site, expected, sizeof(expected)) != 0) { LogLine("item-use wait fix: call site mismatch at 0x%08X", static_cast(reinterpret_cast(site))); return; } DWORD oldProtection = 0; if (!VirtualProtect(site, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) { LogLine("item-use wait fix: VirtualProtect failed %u", GetLastError()); return; } unsigned char patch[5] = { 0xE8 }; *reinterpret_cast(patch + 1) = static_cast( reinterpret_cast(&QueryItemUseWaitByte) - reinterpret_cast(site + 5)); memcpy(site, patch, sizeof(patch)); DWORD ignored = 0; VirtualProtect(site, 5, oldProtection, &ignored); FlushInstructionCache(GetCurrentProcess(), site, 5); LogLine("item-use wait fix installed site=0x%08X", static_cast(reinterpret_cast(site))); } static void EnforceBoundMovieOrExit() { if (VerifyBoundMoviePresent()) return; LogLine("movie bind check failed: terminating client"); MessageBoxW(nullptr, L"检测到用于客户端播放的影片文件缺失或已损坏,客户端无法启动。\n\n" L"请恢复文件:\nClient\\Video\\movie\\demo_mjpeg.avi", L"客户端文件校验失败", MB_OK | MB_ICONERROR | MB_TOPMOST); TerminateProcess(GetCurrentProcess(), 1); } // Windows 10 hosts can still be unpacking the client when the companion hooks // are first attempted, so the quickbar condition hook may miss its prologue. // Retrying it independently keeps the feature timing-independent: the hook is // the only thing that lets the companion press a projected quickbar key. static volatile LONG g_companionConditionHookInstalled = 0; DWORD WINAPI CompanionConditionRetryWorker(void*) { const uintptr_t target = g_dnfBase + 0x0222A7F0; static const unsigned char prologue[] = {0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x14}; for (int attempt = 0; attempt < 240; ++attempt) { if (InterlockedCompareExchange(&g_companionConditionHookInstalled, 1, 1) == 1) { return 0; } if (BytesMatch(reinterpret_cast(target), prologue, sizeof(prologue))) { void* original = nullptr; if (InstallKnownInlineHook(target, prologue, sizeof(prologue), reinterpret_cast(&dnf90_companion_p0::ProxyAICondition), &original, "companion owned quickbar support selection (retry)")) { dnf90_companion_p0::SetOriginalAICondition(original); InterlockedExchange(&g_companionConditionHookInstalled, 1); LogLine("companion quickbar condition hook installed by retry attempt=%d", attempt + 1); return 0; } } Sleep(500); } LogLine("companion quickbar condition hook retry exhausted"); return 0; } // sub_17B1DE0 is the native "create attack party" entry. The current build // hard-codes UI owner 0x76 (RaidCreateWindow) there even though owner 0x7A // (RaidSelectSingleModeWindow) and both of its button callbacks are complete. // Patch only the push-immediate byte; the selection window stays entirely // native and its buttons continue to pass mode 1/0 into owner 0x76 locally. // Ported from the vendor client-patch raid_probe.cpp / 90CN.cpp compatibility // (same byte signature, same fail-closed validation). constexpr uintptr_t kAntonRaidModeSelectContextRva = 0x013B1E02; constexpr uintptr_t kAntonRaidModeSelectOwnerRva = 0x013B1E0D; constexpr unsigned char kRaidCreateUiOwner = 0x76; constexpr unsigned char kRaidModeSelectUiOwner = 0x7A; bool InstallAntonRaidModeSelectCompatibility() { static const unsigned char kNativeContext[] = { 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0x00, 0x6A, kRaidCreateUiOwner, 0xE8, 0x8D, 0xB3, 0x82, 0x01, 0xC3 }; static const unsigned char kInstalledContext[] = { 0x8B, 0x0D, 0x64, 0x27, 0x1B, 0x05, 0x6A, 0x00, 0x6A, 0x00, 0x6A, kRaidModeSelectUiOwner, 0xE8, 0x8D, 0xB3, 0x82, 0x01, 0xC3 }; uintptr_t context = g_dnfBase + kAntonRaidModeSelectContextRva; uintptr_t owner = g_dnfBase + kAntonRaidModeSelectOwnerRva; if (BytesMatch(reinterpret_cast(context), kInstalledContext, sizeof(kInstalledContext))) { LogLine("Anton raid native mode-select UI already installed owner=0x%02X target=0x%08X", kRaidModeSelectUiOwner, owner); return true; } if (!BytesMatch(reinterpret_cast(context), kNativeContext, sizeof(kNativeContext))) { LogLine("Anton raid native mode-select UI rejected: context mismatch at 0x%08X", context); return false; } static const unsigned char kModeSelectOwner[] = { kRaidModeSelectUiOwner }; if (!WriteCodePatch(owner, kModeSelectOwner, sizeof(kModeSelectOwner), "Anton raid native mode-select UI owner")) { return false; } LogLine("Anton raid native mode-select UI installed entry=0x%08X owner=0x%02X", context, kRaidModeSelectUiOwner); return true; } // --- temporary named-stat probe (panel field mapping) ---------------------- // Reads 90CN_panel_pairs.txt (name=value lines), scans committed memory for // each value (u32 or s16*10), checks for a printable name at -0x20/-0x18/-0x10 // or +4 relative to the hit, and writes 90CN_panel_named.txt with matches. struct NamedProbeValue { char name[24]; long long value; }; static bool ProbeSafeRead(const void* src, void* dst, size_t size) { __try { memcpy(dst, src, size); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static std::wstring ProbeClientFile(const wchar_t* name) { wchar_t path[MAX_PATH] = { 0 }; DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH); if (length == 0 || length >= MAX_PATH) { return std::wstring(name); } std::wstring dir(path, length); size_t slash = dir.find_last_of(L"\\/"); if (slash != std::wstring::npos) { dir.resize(slash + 1); } else { dir.clear(); } return dir + name; } static bool ProbeReadPairs(const wchar_t* path, std::vector& out) { HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return false; } char buffer[8192] = { 0 }; DWORD read = 0; BOOL ok = ReadFile(file, buffer, sizeof(buffer) - 1, &read, nullptr); CloseHandle(file); if (!ok || read == 0) { return false; } buffer[read] = 0; char* line = buffer; while (line && *line) { char* next = strchr(line, '\n'); if (next) { *next = 0; } char* text = line; while (*text == ' ' || *text == '\t' || *text == '\r') { ++text; } if (*text != '#' && *text != 0) { char* equals = strchr(text, '='); if (equals) { *equals = 0; char* valueText = equals + 1; while (*valueText == ' ' || *valueText == '\t') { ++valueText; } char* end = nullptr; long long value = _strtoi64(valueText, &end, 10); if (end && end != valueText && *text) { NamedProbeValue entry = {}; strncpy_s(entry.name, text, _TRUNCATE); entry.value = value; out.push_back(entry); } } } line = next ? next + 1 : nullptr; } return !out.empty(); } static bool ProbeReadName(const unsigned char* data, size_t size, char* out, size_t cap) { size_t len = 0; bool terminated = false; for (size_t i = 0; i < 8; ++i) { unsigned char c = data[i]; if (c == 0) { for (size_t j = i; j < 8; ++j) { if (data[j] != 0) { return false; } } terminated = true; break; } bool ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; if (!ok || len + 1 >= cap) { return false; } if (len == 0 && !(c >= 'a' && c <= 'z')) { return false; } out[len++] = static_cast(c); } if (!terminated || len < 2) { return false; } out[len] = 0; return true; } static void ProbeWrite(const wchar_t* path, const std::string& text) { HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return; } DWORD written = 0; WriteFile(file, text.data(), static_cast(text.size()), &written, nullptr); CloseHandle(file); } static void ProbeRun(const std::vector& values, const wchar_t* outPath) { std::string out; char header[256] = { 0 }; _snprintf_s(header, sizeof(header), _TRUNCATE, "== named probe: %u values module=0x%08X", static_cast(values.size()), static_cast(g_dnfBase)); out += header; out += "\r\n"; std::unordered_map> u32Map; for (size_t v = 0; v < values.size(); ++v) { if (values[v].value >= 0) { u32Map[static_cast(values[v].value)].push_back(v); } } SYSTEM_INFO info = {}; GetSystemInfo(&info); uintptr_t cursor = reinterpret_cast(info.lpMinimumApplicationAddress); uintptr_t limit = reinterpret_cast(info.lpMaximumApplicationAddress); const size_t kChunk = 0x400000; std::vector chunk(kChunk); size_t matches = 0; while (cursor < limit && matches < 200) { MEMORY_BASIC_INFORMATION mbi = {}; if (VirtualQuery(reinterpret_cast(cursor), &mbi, sizeof(mbi)) != sizeof(mbi)) { break; } uintptr_t base = reinterpret_cast(mbi.BaseAddress); size_t size = mbi.RegionSize; bool readable = mbi.State == MEM_COMMIT && (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD)); if (readable) { for (size_t offset = 0; offset < size && matches < 200; offset += kChunk) { size_t take = size - offset; if (take > kChunk) { take = kChunk; } if (!ProbeSafeRead(reinterpret_cast(base + offset), chunk.data(), take)) { break; } for (size_t i = 0x20; i + 4 <= take && matches < 200; i += 4) { uint32_t raw = 0; memcpy(&raw, chunk.data() + i, 4); auto it = u32Map.find(raw); if (it == u32Map.end()) { continue; } uint32_t padding = 0; memcpy(&padding, chunk.data() + i + 4, 4); if (padding != 0) { continue; } if (i >= 0x21) { unsigned char prev = chunk.data()[i - 0x21]; if ((prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == '_') { continue; } } char name[16] = { 0 }; if (!ProbeReadName(chunk.data() + i - 0x20, 8, name, sizeof(name))) { continue; } for (size_t v : it->second) { if (matches >= 200) { break; } char line[512] = { 0 }; _snprintf_s(line, sizeof(line), _TRUNCATE, "match value=%lld(%s) name=%s at=0x%08X", values[v].value, values[v].name, name, static_cast(base + offset + i)); out += line; out += "\r\n"; ++matches; } } } } cursor = base + size; } char footer[64] = { 0 }; _snprintf_s(footer, sizeof(footer), _TRUNCATE, "== matches=%u ==", static_cast(matches)); out += footer; out += "\r\n"; ProbeWrite(outPath, out); LogLine("named probe finished matches=%u", static_cast(matches)); } DWORD WINAPI NamedProbeThread(void*) { std::wstring pairsPath = ProbeClientFile(L"90CN_panel_pairs.txt"); std::wstring outPath = ProbeClientFile(L"90CN_panel_named.txt"); FILETIME lastWrite = {}; LogLine("named probe watcher started pairs=%ls out=%ls", pairsPath.c_str(), outPath.c_str()); for (;;) { WIN32_FILE_ATTRIBUTE_DATA attributes = {}; if (GetFileAttributesExW(pairsPath.c_str(), GetFileExInfoStandard, &attributes)) { if (CompareFileTime(&attributes.ftLastWriteTime, &lastWrite) != 0) { lastWrite = attributes.ftLastWriteTime; std::vector values; if (ProbeReadPairs(pairsPath.c_str(), values)) { ProbeRun(values, outPath.c_str()); } } } Sleep(1500); } return 0; } // --- temporary panel widget-tree dump probe (B2) --------------------------- static std::wstring PanelTreePath(const wchar_t* name) { wchar_t path[MAX_PATH] = { 0 }; DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH); if (length == 0 || length >= MAX_PATH) { return std::wstring(name); } std::wstring dir(path, length); size_t slash = dir.find_last_of(L"\\/"); if (slash != std::wstring::npos) { dir.resize(slash + 1); } else { dir.clear(); } return dir + name; } static bool PanelTreeRead(const void* source, void* destination, size_t size) { __try { memcpy(destination, source, size); return true; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } static void PanelTreeWrite(const wchar_t* name, const void* data, size_t size) { std::wstring path = PanelTreePath(name); HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (file == INVALID_HANDLE_VALUE) { return; } DWORD written = 0; WriteFile(file, data, static_cast(size), &written, nullptr); CloseHandle(file); } static void DumpPanelTree(void* panel) { static bool dumped = false; if (dumped || !panel) { return; } dumped = true; const size_t kObjectSize = 0x4000; std::vector objectBuffer(kObjectSize); if (!PanelTreeRead(panel, objectBuffer.data(), kObjectSize)) { LogLine("panel tree dump: panel unreadable"); return; } PanelTreeWrite(L"90CN_panel_obj.bin", objectBuffer.data(), kObjectSize); uintptr_t root = 0; memcpy(&root, objectBuffer.data() + 0x30, 4); std::vector rootBuffer(kObjectSize); if (root) { if (PanelTreeRead(reinterpret_cast(root), rootBuffer.data(), kObjectSize)) { PanelTreeWrite(L"90CN_panel_root.bin", rootBuffer.data(), kObjectSize); } } std::string index; char line[256] = { 0 }; _snprintf_s(line, sizeof(line), _TRUNCATE, "panel=0x%08X root=0x%08X\r\n", static_cast(reinterpret_cast(panel)), static_cast(root)); index += line; std::vector pointers; auto collect = [&](const unsigned char* buffer, size_t size, const char* label) { for (size_t i = 0; i + 4 <= size; i += 4) { uint32_t value = 0; memcpy(&value, buffer + i, 4); if (value < 0x00010000 || value > 0x7FFFFFF0) { continue; } bool seen = false; for (size_t k = 0; k < pointers.size(); ++k) { if (pointers[k] == value) { seen = true; break; } } if (seen) { continue; } pointers.push_back(value); char entry[128] = { 0 }; _snprintf_s(entry, sizeof(entry), _TRUNCATE, "%s+0x%X=0x%08X\r\n", label, static_cast(i), value); index += entry; if (pointers.size() >= 96) { return; } } }; collect(objectBuffer.data(), kObjectSize, "panel"); if (root) { collect(rootBuffer.data(), kObjectSize, "root"); } const size_t kPointerSize = 0x1000; std::vector pointerBuffer(kPointerSize); for (size_t i = 0; i < pointers.size(); ++i) { if (!PanelTreeRead(reinterpret_cast(pointers[i]), pointerBuffer.data(), kPointerSize)) { continue; } wchar_t name[64] = { 0 }; _snwprintf_s(name, _countof(name), _TRUNCATE, L"90CN_panel_ptr_%02u.bin", static_cast(i)); PanelTreeWrite(name, pointerBuffer.data(), kPointerSize); } PanelTreeWrite(L"90CN_panel_index.txt", index.data(), index.size()); LogLine("panel tree dumped panel=0x%08X root=0x%08X pointers=%u", static_cast(reinterpret_cast(panel)), static_cast(root), static_cast(pointers.size())); } static bool InstallApcRegistrationNullGuard() { constexpr uintptr_t kEntryRva = 0x03108290; constexpr uintptr_t kBackRva = 0x03108296; constexpr size_t kStolen = 6; unsigned char* entry = reinterpret_cast(g_dnfBase + kEntryRva); const unsigned char expected[kStolen] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 }; if (memcmp(entry, expected, kStolen) != 0) { LogLine("APC registration null guard skipped: entry signature mismatch target=0x%08X", static_cast(reinterpret_cast(entry))); return false; } unsigned char* cave = static_cast(VirtualAlloc( nullptr, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!cave) return false; size_t cursor = 0; // if (ecx < 0x10000) return 0; cave[cursor++] = 0x81; cave[cursor++] = 0xF9; *reinterpret_cast(cave + cursor) = 0x10000; cursor += 4; cave[cursor++] = 0x72; cave[cursor++] = 0x0B; // Replay the stolen prologue and continue at push esi. for (size_t i = 0; i < kStolen; ++i) cave[cursor++] = expected[i]; size_t jumpPos = cursor; cave[cursor++] = 0xE9; cursor += 4; size_t failPos = cursor; cave[cursor++] = 0x31; cave[cursor++] = 0xC0; cave[cursor++] = 0xC3; const uintptr_t caveAddress = reinterpret_cast(cave); *reinterpret_cast(cave + jumpPos + 1) = static_cast( (g_dnfBase + kBackRva) - (caveAddress + jumpPos + 5)); cave[7] = static_cast(failPos - 8); unsigned char patch[6] = { 0xE9, 0, 0, 0, 0, 0x90 }; *reinterpret_cast(patch + 1) = static_cast( caveAddress - (reinterpret_cast(entry) + 5)); if (!WriteCodePatch(reinterpret_cast(entry), patch, sizeof(patch), "APC registration null guard")) { return false; } LogLine("APC registration null guard installed entry=0x%08X cave=0x%08X", static_cast(reinterpret_cast(entry)), static_cast(caveAddress)); return true; } static bool InstallNativeQuickbarNullGuard() { constexpr uintptr_t kEntryRva = 0x030DBE00; constexpr uintptr_t kBackRva = 0x030DBE06; constexpr size_t kStolen = 6; unsigned char* entry = reinterpret_cast(g_dnfBase + kEntryRva); const unsigned char expected[kStolen] = { 0x55, 0x8B, 0xEC, 0x8B, 0x01, 0x56 }; if (memcmp(entry, expected, kStolen) != 0) { LogLine("native quickbar null guard skipped: entry signature mismatch target=0x%08X", static_cast(reinterpret_cast(entry))); return false; } unsigned char* cave = static_cast(VirtualAlloc( nullptr, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!cave) return false; size_t cursor = 0; cave[cursor++] = 0x81; cave[cursor++] = 0xF9; *reinterpret_cast(cave + cursor) = 0x10000; cursor += 4; cave[cursor++] = 0x72; cave[cursor++] = 0x0B; for (size_t i = 0; i < kStolen; ++i) cave[cursor++] = expected[i]; size_t jumpPos = cursor; cave[cursor++] = 0xE9; cursor += 4; size_t failPos = cursor; cave[cursor++] = 0x31; cave[cursor++] = 0xC0; cave[cursor++] = 0xC2; cave[cursor++] = 0x08; cave[cursor++] = 0x00; const uintptr_t caveAddress = reinterpret_cast(cave); *reinterpret_cast(cave + jumpPos + 1) = static_cast( (g_dnfBase + kBackRva) - (caveAddress + jumpPos + 5)); cave[7] = static_cast(failPos - 8); unsigned char patch[6] = { 0xE9, 0, 0, 0, 0, 0x90 }; *reinterpret_cast(patch + 1) = static_cast( caveAddress - (reinterpret_cast(entry) + 5)); if (!WriteCodePatch(reinterpret_cast(entry), patch, sizeof(patch), "native quickbar null guard")) { return false; } LogLine("native quickbar null guard installed entry=0x%08X cave=0x%08X", static_cast(reinterpret_cast(entry)), static_cast(caveAddress)); return true; } static bool InstallQuestAutoNextNullGuard() { constexpr uintptr_t kCallRva = 0x01D8A5CE; constexpr uintptr_t kGetterRva = 0x017C4F80; constexpr uintptr_t kBackRva = 0x01D8A5D3; unsigned char* callSite = reinterpret_cast(g_dnfBase + kCallRva); if (callSite[0] != 0xE8) { LogLine("quest auto-next null guard skipped: call-site signature mismatch target=0x%08X", static_cast(reinterpret_cast(callSite))); return false; } unsigned char* cave = static_cast(VirtualAlloc( nullptr, 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!cave) return false; size_t cursor = 0; cave[cursor++] = 0x85; cave[cursor++] = 0xF6; cave[cursor++] = 0x74; cave[cursor++] = 0x0A; size_t callPos = cursor; cave[cursor++] = 0xE8; cursor += 4; size_t jumpPos = cursor; cave[cursor++] = 0xE9; cursor += 4; cave[cursor++] = 0x31; cave[cursor++] = 0xC0; size_t zeroJumpPos = cursor; cave[cursor++] = 0xE9; cursor += 4; const uintptr_t caveAddress = reinterpret_cast(cave); *reinterpret_cast(cave + callPos + 1) = static_cast( (g_dnfBase + kGetterRva) - (caveAddress + callPos + 5)); *reinterpret_cast(cave + jumpPos + 1) = static_cast( (g_dnfBase + kBackRva) - (caveAddress + jumpPos + 5)); *reinterpret_cast(cave + zeroJumpPos + 1) = static_cast( (g_dnfBase + kBackRva) - (caveAddress + zeroJumpPos + 5)); unsigned char patch[5] = { 0xE9 }; *reinterpret_cast(patch + 1) = static_cast( caveAddress - (reinterpret_cast(callSite) + 5)); if (!WriteCodePatch(reinterpret_cast(callSite), patch, sizeof(patch), "quest auto-next null guard")) { return false; } LogLine("quest auto-next null guard installed call=0x%08X cave=0x%08X", static_cast(reinterpret_cast(callSite)), static_cast(caveAddress)); return true; } DWORD WINAPI InstallTransportWorkerInner(void*) { g_dnfBase = reinterpret_cast(GetModuleHandleW(nullptr)); // TCLS launch argument fetches run before the main window and before the // self-CRC / 22 MB movie hash below. Install the six direct-launch helpers // first: on slower machines those hashes take >100 ms and the native TCLS // parse otherwise wins the race and shows "tcls Parsing Failed". bool tclsInstalled = InstallTclsCompatibility(); LogLine("TCLS early bootstrap result=%d", tclsInstalled); // 自校验:本 DLL 的 .text 段 CRC 必须与构建时写入的值一致。 if (!VerifySelfIntegrity()) { LogLine("self crc check failed: terminating client"); MessageBoxW(nullptr, L"客户端补丁文件已被修改,无法启动。\n\n请使用原始 90CN.dll。", L"客户端文件校验失败", MB_OK | MB_ICONERROR | MB_TOPMOST); TerminateProcess(GetCurrentProcess(), 1); } // 影片绑定校验:文件缺失/损坏直接终止,客户端无法启动。 EnforceBoundMovieOrExit(); // 攻坚队创建入口默认写死 owner 0x76(RaidCreateWindow),把该立即数改成 // 0x7A 后打开原生 RaidSelectSingleModeWindow(团队/单人选择),单人按钮 // 再把 mode 0 传回 0x76 的创建窗口。窗口与按钮回调全部是客户端原生实现。 // Native mode-select owner patch: open RaidSelectSingleModeWindow so both // the 20-player normal button and the single-mode button are reachable. // raid_probe.cpp rescues the 20-player button, whose native branch is dead. bool raidModeSelectInstalled = InstallAntonRaidModeSelectCompatibility(); LogLine("Anton raid native mode-select UI result=%d", raidModeSelectInstalled ? 1 : 0); CreateThread(nullptr, 0, NamedProbeThread, nullptr, 0, nullptr); // op32 重建分支探针:记录 actor 指针并做前后内存快照对比。 // 手动触发快照(A/B 差分实验)。 // 形态设置指令的 INT3 探针。 InstallItemUseWaitRecovery(); // 章节动画/影片播放依赖这些钩子(捕获播放对象、Open/Play、渲染驱动)。 InstallAviOpenProbe(); InstallMovieFunctionProbe(); InstallMovieOpenPlayProbes(); InstallMovieRenderProbe(); if (!g_exceptionTraceHandle) { g_exceptionTraceHandle = AddVectoredExceptionHandler(1, TraceUnhandledClientException); } LogLine("90CN transport worker started exception_handler=%p module_base=0x%08X", g_exceptionTraceHandle, g_dnfBase); // 影片打开探针:被动记录 *.avi 的打开调用点 RVA(见函数上方的说明)。 // 影片函数入口探针:确定 0x0314183F 所在函数的调用约定(第 1 步)。 // 影片对象 Open/Play 探针:抓游戏侧调用者,并允许直接驱动捕获到的实例(第 3 步)。 // 章节动画文件触发(第 2 步):轮询客户端目录下的 90CN_play_cutscene.txt。 StartCutsceneFileWatcher(); // Temporary, passive first-login probe. It hooks only the current EXE's // WinSock imports, preserves every native return/error value, filters to // the game transport socket, and stops logging after recognized C2S login // progress. Keep the existing watchdog/recovery policy unchanged. bool firstLoginRawProbeInstalled = false; // 发布版:移除 WinSock 接收探针 LogLine("first-login raw-receive probe startup result=%d", firstLoginRawProbeInstalled ? 1 : 0); bool luaPluginLoaded = LoadOptionalLuaPlugin(); LogLine("optional Lua plugin result=%d", luaPluginLoaded ? 1 : 0); wchar_t protocolTrace[8] = { 0 }; DWORD protocolTraceLength = GetEnvironmentVariableW(L"DNF_PROTOCOL_TRACE", protocolTrace, 8); InterlockedExchange( &g_protocolTraceEnabled, protocolTraceLength > 0 && protocolTrace[0] == L'1' ? 1 : 0); LogLine("protocol transport trace enabled=%d", InterlockedCompareExchange(&g_protocolTraceEnabled, 0, 0) != 0 ? 1 : 0); wchar_t dispatchTrace[8] = { 0 }; DWORD dispatchTraceLength = GetEnvironmentVariableW( L"DNF_DISPATCH_TRACE", dispatchTrace, 8); InterlockedExchange( &g_dispatchTraceEnabled, dispatchTraceLength > 0 && dispatchTrace[0] == L'1' ? 1 : 0); LogLine("native dispatch trace enabled=%d", InterlockedCompareExchange(&g_dispatchTraceEnabled, 0, 0) != 0 ? 1 : 0); g_checksumFn = g_dnfBase + kChecksumRva; g_gameAllocatorFn = g_dnfBase + kGameAllocatorRva; g_gameMemcpyFn = g_dnfBase + kGameMemcpyRva; g_dprotoOutgoingResume = g_dnfBase + kDprotoOutgoingResumeRva; g_dprotoOutgoingReturn = g_dnfBase + kDprotoOutgoingReturnRva; g_dprotoFalseResume = g_dnfBase + kDprotoFalseResumeRva; // The launcher opts in with DNF_MULTI_CLIENT=1. Keep this limited to the // two audited current-EXE single-instance checks; normal launches are no-op. InstallFmodMuteHooks(); InstallSendProbe(); bool multiClientInstalled = InstallMultiClientCompatibilityHook(); LogLine("multi-client startup compatibility result=%d", multiClientInstalled ? 1 : 0); // Current runtime enters this transport worker, not the legacy installer // path above. Install the mutation-safe UI iterator guard here so the // evidenced update loop is actually patched before scene UI work begins. bool uiIteratorInstalled = InstallUiIteratorAdvanceCompatibility(); LogLine("ui-update iterator startup compatibility result=%d", uiIteratorInstalled ? 1 : 0); bool guildCreateSinglePlayerInstalled = InstallGuildCreateSinglePlayerCompatibility(); LogLine("guild-create single-player compatibility result=%d", guildCreateSinglePlayerInstalled ? 1 : 0); if (!tclsInstalled) { tclsInstalled = InstallTclsCompatibility(); LogLine("TCLS transport bootstrap retry result=%d", tclsInstalled ? 1 : 0); } // The channel hooks only observe directory loading and adapt the bootstrap // connection port. They do not write HUD, clock, scene, or actor state. bool channelInstalled = InstallChannelDiagnostic(); LogLine("channel transport compatibility result=%d", channelInstalled ? 1 : 0); // Install from the active transport worker. The old InstallWorkerInner // implementation above is inside #if 0 and never runs in production. bool channelUiCompatibilityInstalled = InstallChannelUiCompatibility(); LogLine("channel UI compatibility result=%d", channelUiCompatibilityInstalled ? 1 : 0); bool pvpPrerequisiteInstalled = InstallPvpPrerequisiteQuestCompatibility(); LogLine("pvp prerequisite quest compatibility result=%d", pvpPrerequisiteInstalled ? 1 : 0); bool partyDirectoryUiInstalled = InstallPartyDirectoryFullPageCompatibility(); LogLine("party-directory full-page UI compatibility result=%d", partyDirectoryUiInstalled ? 1 : 0); bool townCopresenceInstalled = InstallTownCopresenceCompatibility(); bool seriaMinimapInstalled = dnf90_seria::Install(reinterpret_cast(g_dnfBase), &LogLine); LogLine("Seria room minimap production module installed=%d", seriaMinimapInstalled ? 1 : 0); LogLine("town co-presence actor-context compatibility result=%d", townCopresenceInstalled ? 1 : 0); bool dungeonPickupInstalled = InstallDungeonPickupCompatibility(); LogLine("dungeon pickup compatibility result=%d", dungeonPickupInstalled ? 1 : 0); bool creatureRenameInstalled = InstallCreatureRenameCompatibility(); LogLine("creature rename compatibility result=%d", creatureRenameInstalled ? 1 : 0); bool petEnchantDisplayInstalled = InstallPetEnchantDisplayCompatibility(); LogLine("pet enchant display compatibility result=%d", petEnchantDisplayInstalled ? 1 : 0); bool rareAvatarTicketInstalled = InstallRareAvatarTicketPurchaseCompatibility(); LogLine("rare-avatar ticket shop compatibility result=%d", rareAvatarTicketInstalled ? 1 : 0); bool questAutoCompleteInstalled = InstallQuestAutoCompleteSingleTargetCompatibility(); LogLine("quest selected single-target startup compatibility result=%d", questAutoCompleteInstalled ? 1 : 0); bool questAutoNextGuardInstalled = InstallQuestAutoNextNullGuard(); LogLine("quest auto-next null guard result=%d", questAutoNextGuardInstalled ? 1 : 0); bool apcRegistrationGuardInstalled = InstallApcRegistrationNullGuard(); LogLine("APC registration null guard result=%d", apcRegistrationGuardInstalled ? 1 : 0); bool nativeQuickbarGuardInstalled = InstallNativeQuickbarNullGuard(); LogLine("native quickbar null guard result=%d", nativeQuickbarGuardInstalled ? 1 : 0); wchar_t enabled[8] = { 0 }; DWORD enabledLength = GetEnvironmentVariableW(L"DNF_CIPHER_PASSTHROUGH", enabled, 8); if (enabledLength > 0 && enabled[0] == L'0') { LogLine("DNF_CIPHER_PASSTHROUGH=0; packet codec hook disabled"); return 0; } wchar_t dprotoEnabled[8] = { 0 }; DWORD dprotoEnabledLength = GetEnvironmentVariableW(L"DNF_DPROTO_COMPAT", dprotoEnabled, 8); bool enableDprotoCompat = !(dprotoEnabledLength > 0 && dprotoEnabled[0] == L'0'); wchar_t routeEnabled[8] = { 0 }; DWORD routeEnabledLength = GetEnvironmentVariableW(L"DNF_ROUTE_COMPAT", routeEnabled, 8); bool enableSceneRouteCompat = routeEnabledLength == 0 || routeEnabled[0] != L'0'; uintptr_t cipherTarget = g_dnfBase + kCipherEncodeRva; uintptr_t cipherDecodeTarget = g_dnfBase + kCipherDecodeRva; uintptr_t routePredicateTarget = g_dnfBase + kDprotoRoutePredicateRva; uintptr_t outgoingGateTarget = g_dnfBase + kDprotoOutgoingGateRva; uintptr_t outgoingCloneTarget = g_dnfBase + kDprotoOutgoingCloneRva; static const unsigned char kNativePrologue[] = { 0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D }; static const unsigned char kRoutePredicatePrologue[] = { 0x55, 0x8B, 0xEC, 0x8B, 0x49, 0x0C }; static const unsigned char kOutgoingGate[] = { 0x0F, 0x84, 0x2F, 0x01, 0x00, 0x00 }; static const unsigned char kOutgoingClonePrologue[] = { 0x8B, 0x4B, 0x0C, 0x8B, 0x01 }; constexpr int kWaitLimitMs = 120000; int waitedMs = 0; while (waitedMs < kWaitLimitMs) { bool cipherReady = BytesMatch( reinterpret_cast(cipherTarget), kNativePrologue, sizeof(kNativePrologue)); bool cipherDecodeReady = BytesMatch( reinterpret_cast(cipherDecodeTarget), kNativePrologue, sizeof(kNativePrologue)); bool routeReady = !enableSceneRouteCompat || BytesMatch(reinterpret_cast(routePredicateTarget), kRoutePredicatePrologue, sizeof(kRoutePredicatePrologue)); bool dprotoReady = !enableDprotoCompat || (BytesMatch(reinterpret_cast(routePredicateTarget), kRoutePredicatePrologue, sizeof(kRoutePredicatePrologue)) && BytesMatch(reinterpret_cast(outgoingGateTarget), kOutgoingGate, sizeof(kOutgoingGate)) && BytesMatch(reinterpret_cast(outgoingCloneTarget), kOutgoingClonePrologue, sizeof(kOutgoingClonePrologue))); if (cipherReady && cipherDecodeReady && routeReady && dprotoReady) { break; } unsigned char firstByte = 0; if (TryReadByte(reinterpret_cast(cipherTarget), &firstByte) && firstByte == 0xE9) { LogLine("cipher encode already hooked; fresh DNF process required"); return 0; } if (TryReadByte(reinterpret_cast(cipherDecodeTarget), &firstByte) && firstByte == 0xE9) { LogLine("cipher decode already hooked; fresh DNF process required"); return 0; } Sleep(250); waitedMs += 250; } if (waitedMs >= kWaitLimitMs) { LogLine("transport targets unavailable route=%d dproto=%d", enableSceneRouteCompat ? 1 : 0, enableDprotoCompat ? 1 : 0); return 0; } unsigned char encodePatch[5] = { 0xE9 }; intptr_t relative = reinterpret_cast(&ProxyCipherEncodeFunction) - static_cast(cipherTarget + 5); *reinterpret_cast(encodePatch + 1) = static_cast(relative); if (!WriteCodePatch(cipherTarget, encodePatch, sizeof(encodePatch), "upper body encode transport")) { return 0; } unsigned char decodePatch[5] = { 0xE9 }; relative = reinterpret_cast(&ProxyCipherDecodeFunction) - static_cast(cipherDecodeTarget + 5); *reinterpret_cast(decodePatch + 1) = static_cast(relative); if (!WriteCodePatch(cipherDecodeTarget, decodePatch, sizeof(decodePatch), "upper body decode transport")) { return 0; } if (enableSceneRouteCompat) { unsigned char routePatch[6] = { 0xE9 }; relative = reinterpret_cast(&ProxySceneRouteAllowList) - static_cast(routePredicateTarget + 5); *reinterpret_cast(routePatch + 1) = static_cast(relative); routePatch[5] = 0x90; if (!WriteCodePatch(routePredicateTarget, routePatch, sizeof(routePatch), "inbound protocol route")) { return 0; } } if (enableDprotoCompat) { unsigned char clonePatch[5] = { 0xE9 }; relative = reinterpret_cast(&ProxyDprotoSendDirect) - static_cast(outgoingCloneTarget + 5); *reinterpret_cast(clonePatch + 1) = static_cast(relative); unsigned char falsePatch[6] = { 0x0F, 0x84 }; relative = reinterpret_cast(&ProxyDprotoFalseSelective) - static_cast(outgoingGateTarget + 6); *reinterpret_cast(falsePatch + 2) = static_cast(relative); if (!WriteCodePatch(outgoingCloneTarget, clonePatch, sizeof(clonePatch), "outbound DPROTO direct transport") || !WriteCodePatch(outgoingGateTarget, falsePatch, sizeof(falsePatch), "outbound DPROTO selective transport")) { LogLine("DPROTO transport patch incomplete; fresh DNF process required"); return 0; } } bool companionRegistrationInstalled = false; bool companionAicCopyInstalled = false; bool companionReceiveInstalled = false; bool companionPeerDeliveryInstalled = false; bool companionConditionInstalled = false; bool companionPayloadEncodeInstalled = false; dnf90_companion_p0::SetProjectionClass0Dispatch( g_originalClass0Dispatch); if (dnf90_companion_p0::DirectOp40Enabled()) { uintptr_t companionReceiveTarget = g_dnfBase + kCompanionP1NetworkRouterRva; static const unsigned char kCompanionReceivePrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; void* companionReceiveOriginal = nullptr; if (BytesMatch( reinterpret_cast( companionReceiveTarget), kCompanionReceivePrologue, sizeof(kCompanionReceivePrologue)) && InstallKnownInlineHook( companionReceiveTarget, kCompanionReceivePrologue, sizeof(kCompanionReceivePrologue), reinterpret_cast( &dnf90_companion_p0::ProxyNetworkRouter), &companionReceiveOriginal, "companion P1 network packet router capture")) { dnf90_companion_p0::SetOriginalNetworkRouter( companionReceiveOriginal); companionReceiveInstalled = true; } else { LogLine("companion P1 network-router hook unavailable target=0x%08X; projection fail-closed", companionReceiveTarget); } uintptr_t companionAicCopyTarget = g_dnfBase + kCompanionP1AicCopyRva; static const unsigned char kCompanionAicCopyPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; void* companionAicCopyOriginal = nullptr; if (BytesMatch( reinterpret_cast( companionAicCopyTarget), kCompanionAicCopyPrologue, sizeof(kCompanionAicCopyPrologue)) && InstallKnownInlineHook( companionAicCopyTarget, kCompanionAicCopyPrologue, sizeof(kCompanionAicCopyPrologue), reinterpret_cast( &dnf90_companion_p0::ProxyAicCopy), &companionAicCopyOriginal, "companion P1 dynamic AIC instance projection")) { dnf90_companion_p0::SetOriginalAicCopy( companionAicCopyOriginal); companionAicCopyInstalled = true; } else { LogLine("companion P1 AIC copy hook unavailable target=0x%08X; projection fail-closed", companionAicCopyTarget); } uintptr_t companionRegistrationTarget = g_dnfBase + kCompanionP0RegistrationRva; static const unsigned char kCompanionRegistrationPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; void* companionRegistrationOriginal = nullptr; if (BytesMatch( reinterpret_cast( companionRegistrationTarget), kCompanionRegistrationPrologue, sizeof(kCompanionRegistrationPrologue)) && InstallKnownInlineHook( companionRegistrationTarget, kCompanionRegistrationPrologue, sizeof(kCompanionRegistrationPrologue), reinterpret_cast( &dnf90_companion_p0::ProxyApcRegistration), &companionRegistrationOriginal, "companion P1 APC registration key capture")) { dnf90_companion_p0::SetOriginalApcRegistration( companionRegistrationOriginal); companionRegistrationInstalled = true; } else { LogLine("companion P1 registration hook unavailable target=0x%08X; projection fail-closed", companionRegistrationTarget); } uintptr_t companionPeerDeliveryTarget = g_dnfBase + kCompanionP0PeerDeliveryRva; const uintptr_t companionConditionTarget = g_dnfBase + 0x0222A7F0; static const unsigned char kCompanionConditionPrologue[] = {0x55,0x8B,0xEC,0x83,0xEC,0x14}; void* companionConditionOriginal = nullptr; if (InstallKnownInlineHook(companionConditionTarget, kCompanionConditionPrologue, sizeof(kCompanionConditionPrologue), reinterpret_cast(&dnf90_companion_p0::ProxyAICondition), &companionConditionOriginal, "companion owned quickbar support selection")) { dnf90_companion_p0::SetOriginalAICondition(companionConditionOriginal); companionConditionInstalled = true; InterlockedExchange(&g_companionConditionHookInstalled, 1); } else { // The packed client restores prologues at a machine-dependent pace; // on slower Windows 10 hosts this hook can miss the first window and // the companion then never dispatches a quickbar key. Arm a worker // that keeps retrying instead of failing closed for the whole run. LogLine("companion quickbar condition hook deferred target=0x%08X; retry worker armed", companionConditionTarget); CloseHandle(CreateThread(nullptr, 0, CompanionConditionRetryWorker, nullptr, 0, nullptr)); } static const unsigned char kCompanionPeerDeliveryPrologue[] = { 0x55, 0x8B, 0xEC, 0x6A, 0xFF }; void* companionPeerDeliveryOriginal = nullptr; if (BytesMatch( reinterpret_cast(companionPeerDeliveryTarget), kCompanionPeerDeliveryPrologue, sizeof(kCompanionPeerDeliveryPrologue)) && InstallKnownInlineHook( companionPeerDeliveryTarget, kCompanionPeerDeliveryPrologue, sizeof(kCompanionPeerDeliveryPrologue), reinterpret_cast( &dnf90_companion_p0::ProxyPeerDelivery), &companionPeerDeliveryOriginal, "companion P0 duplicate peer-delivery suppression")) { dnf90_companion_p0::SetOriginalPeerDelivery( companionPeerDeliveryOriginal); companionPeerDeliveryInstalled = true; } else { LogLine("companion P0 peer-delivery hook unavailable target=0x%08X; feature fail-closed", companionPeerDeliveryTarget); } uintptr_t companionPayloadEncodeTarget = g_dnfBase + kCompanionP0PayloadEncodeRva; static const unsigned char kCompanionPayloadEncodePrologue[] = { 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08 }; void* companionPayloadEncodeOriginal = nullptr; if (companionPeerDeliveryInstalled && BytesMatch( reinterpret_cast(companionPayloadEncodeTarget), kCompanionPayloadEncodePrologue, sizeof(kCompanionPayloadEncodePrologue)) && InstallKnownInlineHook( companionPayloadEncodeTarget, kCompanionPayloadEncodePrologue, sizeof(kCompanionPayloadEncodePrologue), reinterpret_cast( &dnf90_companion_p0::ProxyPayloadEncode), &companionPayloadEncodeOriginal, "companion P0 encoded op40 loopback")) { dnf90_companion_p0::SetOriginalPayloadEncode( companionPayloadEncodeOriginal); companionPayloadEncodeInstalled = true; } else { LogLine("companion P0 encoded-op40 hook unavailable target=0x%08X peer_ready=%d; feature fail-closed", companionPayloadEncodeTarget, companionPeerDeliveryInstalled ? 1 : 0); } } bool companionDirectOp40Installed = companionPeerDeliveryInstalled && companionPayloadEncodeInstalled; bool companionProjectionInstalled = companionDirectOp40Installed && companionRegistrationInstalled && companionReceiveInstalled && companionAicCopyInstalled && g_originalClass0Dispatch != nullptr; LogLine("companion P0/P1 compatibility enabled=%d p0_installed=%d projection_installed=%d receive=%d aic_copy=%d registration=%d peer_delivery=%d payload_encode=%d quickbar_condition=%d", dnf90_companion_p0::DirectOp40Enabled() ? 1 : 0, companionDirectOp40Installed ? 1 : 0, companionProjectionInstalled ? 1 : 0, companionReceiveInstalled ? 1 : 0, companionAicCopyInstalled ? 1 : 0, companionRegistrationInstalled ? 1 : 0, companionPeerDeliveryInstalled ? 1 : 0, companionPayloadEncodeInstalled ? 1 : 0, (companionConditionInstalled || InterlockedCompareExchange(&g_companionConditionHookInstalled, 0, 0) == 1) ? 1 : 0); bool blackDiamondWebButtonInstalled = InstallBlackDiamondWebButtonCompatibility(); LogLine("black-diamond web button compatibility result=%d", blackDiamondWebButtonInstalled ? 1 : 0); bool premiumStateInstalled = InstallPremiumStateCompatibility(); LogLine("premium-state compatibility result=%d", premiumStateInstalled ? 1 : 0); bool contractUseInstalled = InstallContractUseCompatibility(); LogLine("contract-use compatibility result=%d", contractUseInstalled ? 1 : 0); // Must run after all 90CN installs: raid_probe chains onto the SceneUiOpen // E9 hook and rescues the dead 20-player button of the mode-select window. InstallRaidProbe(); LogLine("raid probe integrated normal/single mode compatibility"); LogLine("transport DLL installed codec=1 decode=1 route=%d dproto=%d party_ui=%d town_copresence=%d dungeon_pickup=%d creature_rename=%d rare_avatar_ticket=%d premium_state=%d contract_use=%d wait_ms=%d", enableSceneRouteCompat ? 1 : 0, enableDprotoCompat ? 1 : 0, partyDirectoryUiInstalled ? 1 : 0, townCopresenceInstalled ? 1 : 0, dungeonPickupInstalled ? 1 : 0, creatureRenameInstalled ? 1 : 0, rareAvatarTicketInstalled ? 1 : 0, premiumStateInstalled ? 1 : 0, contractUseInstalled ? 1 : 0, waitedMs); return 0; } DWORD WINAPI InstallWorker(void*) { __try { return InstallTransportWorkerInner(nullptr); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("worker exception code=0x%08X", GetExceptionCode()); return 0; } } } // namespace #pragma comment(linker, "/EXPORT:Queue90CNClientNotice=_Queue90CNClientNotice@8") #pragma comment(linker, "/EXPORT:Dequeue90CNClientEvent=_Dequeue90CNClientEvent@8") #pragma comment(linker, "/EXPORT:Query90CNCharacterStatSnapshot=_Query90CNCharacterStatSnapshot@8") #pragma comment(linker, "/EXPORT:Query90CNEquipmentSnapshot=_Query90CNEquipmentSnapshot@8") #pragma comment(linker, "/EXPORT:Query90CNDamageAffixSnapshot=_Query90CNDamageAffixSnapshot@8") #pragma comment(linker, "/EXPORT:Update90CNCombatPanel=_Update90CNCombatPanel@8") EXTERN_C BOOL WINAPI Queue90CNClientNotice( const wchar_t* text, unsigned int length) { return QueueLuaClientNoticeInternal(text, length) ? TRUE : FALSE; } EXTERN_C BOOL WINAPI Dequeue90CNClientEvent( DNF90ClientEvent* output, unsigned int outputSize) { if (!output || outputSize < DNF90_CLIENT_EVENT_V1_SIZE) return FALSE; DNF90ClientEvent event = {}; if (!DequeueLuaClientEventInternal(&event)) return FALSE; const unsigned int copySize = outputSize >= sizeof(event) ? static_cast(sizeof(event)) : DNF90_CLIENT_EVENT_V1_SIZE; event.size = copySize; __try { memcpy(output, &event, copySize); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("lua client event output write exception code=0x%08X", GetExceptionCode()); return FALSE; } return TRUE; } EXTERN_C BOOL WINAPI Query90CNCharacterStatSnapshot( DNF90CharacterStatSnapshot* output, unsigned int outputSize) { if (!output || outputSize < sizeof(DNF90CharacterStatSnapshot)) { return FALSE; } DNF90CharacterStatSnapshot snapshot = {}; AcquireSRWLockShared(&g_characterStatSnapshotLock); snapshot = g_characterStatSnapshot; ReleaseSRWLockShared(&g_characterStatSnapshotLock); if (snapshot.size != sizeof(snapshot) || snapshot.generation == 0 || (snapshot.validFlags & DNF90_CHARACTER_STATS_BASE_VALID) == 0) { return FALSE; } __try { *output = snapshot; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-power snapshot output write exception code=0x%08X", GetExceptionCode()); return FALSE; } return TRUE; } EXTERN_C BOOL WINAPI Query90CNEquipmentSnapshot( DNF90EquipmentSnapshot* output, unsigned int outputSize) { if (!output || outputSize < sizeof(DNF90EquipmentSnapshot)) { return FALSE; } DNF90EquipmentSnapshot snapshot = {}; AcquireSRWLockShared(&g_equipmentSnapshotLock); snapshot = g_equipmentSnapshot; ReleaseSRWLockShared(&g_equipmentSnapshotLock); if (snapshot.size != sizeof(snapshot) || snapshot.generation == 0 || snapshot.itemCount > DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS || (snapshot.validFlags & DNF90_EQUIPMENT_SNAPSHOT_ROWS_VALID) == 0) { return FALSE; } __try { *output = snapshot; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-power equipment snapshot output write exception " "code=0x%08X", GetExceptionCode()); return FALSE; } return TRUE; } EXTERN_C BOOL WINAPI Query90CNDamageAffixSnapshot( DNF90DamageAffixSnapshot* output, unsigned int outputSize) { if (!output || outputSize < sizeof(DNF90DamageAffixSnapshot)) { return FALSE; } DNF90DamageAffixSnapshot snapshot = {}; AcquireSRWLockShared(&g_damageAffixSnapshotLock); snapshot = g_damageAffixSnapshot; ReleaseSRWLockShared(&g_damageAffixSnapshotLock); if (snapshot.size != sizeof(snapshot) || snapshot.generation == 0 || (snapshot.version != kCombatPowerAffixPrivateVersionV4 && snapshot.version != kCombatPowerAffixPrivateVersionV5) || snapshot.equippedItemCount > DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS || (snapshot.validFlags & (DNF90_DAMAGE_AFFIX_SNAPSHOT_VALUES_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_IDENTITY_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_THREE_ATTACKS_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_EQUIPMENT_SCORE_VALID)) != (DNF90_DAMAGE_AFFIX_SNAPSHOT_VALUES_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_IDENTITY_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_THREE_ATTACKS_VALID | DNF90_DAMAGE_AFFIX_SNAPSHOT_EQUIPMENT_SCORE_VALID)) { return FALSE; } __try { *output = snapshot; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-power damage-affix snapshot output write exception code=0x%08X", GetExceptionCode()); return FALSE; } return TRUE; } EXTERN_C BOOL WINAPI Update90CNCombatPanel( const DNF90CombatPanelState* state, unsigned int stateSize) { if (!state || stateSize != sizeof(DNF90CombatPanelState)) return FALSE; DNF90CombatPanelState pending = {}; __try { pending = *state; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("combat-panel state input read exception code=0x%08X", GetExceptionCode()); return FALSE; } constexpr unsigned int kKnownFlags = DNF90_COMBAT_PANEL_ENABLED | DNF90_COMBAT_PANEL_BASE_SCORE_VALID | DNF90_COMBAT_PANEL_EQUIPMENT_SCORE_VALID | DNF90_COMBAT_PANEL_DAMAGE_AFFIXES_VALID | DNF90_COMBAT_PANEL_IDENTITY_VALID | DNF90_COMBAT_PANEL_THREE_ATTACKS_VALID; pending.professionUtf8[_countof(pending.professionUtf8) - 1] = '\0'; const bool identityValid = (pending.validFlags & DNF90_COMBAT_PANEL_IDENTITY_VALID) != 0; if (pending.size != sizeof(pending) || (pending.validFlags & ~kKnownFlags) != 0 || pending.formulaVersion == 0 || pending.totalScore > 2000000000u || pending.baseAttributeScore > 2000000000u || pending.equipmentScore > 2000000000u || pending.whiteDamageTenths > 65535u || pending.yellowDamageTenths > 65535u || pending.criticalDamageTenths > 65535u || pending.yellowAdditionalTenths > 65535u || pending.criticalAdditionalTenths > 65535u || pending.allAttackTenths > 65535u || pending.equippedItemCount > DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS || (identityValid && (pending.level == 0 || pending.professionUtf8[0] == '\0' || MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, pending.professionUtf8, -1, nullptr, 0) <= 0))) { return FALSE; } AcquireSRWLockExclusive(&g_combatPanelStateLock); g_combatPanelState = pending; ReleaseSRWLockExclusive(&g_combatPanelStateLock); HWND bridgeWindow = reinterpret_cast( InterlockedCompareExchangePointer( &g_luaNoticeBridgeWindow, nullptr, nullptr)); UINT bridgeMessage = static_cast( InterlockedCompareExchange(&g_combatPanelWindowMessage, 0, 0)); if (!bridgeWindow || !bridgeMessage || !PostMessageW(bridgeWindow, bridgeMessage, 0, 0)) { return FALSE; } return TRUE; } void Start90CNPatch() { // Do not chain-load the previous client hook. This DLL owns only the // common codec and bounded current-scene route predicate boundary. It does // not hook send/recv or replay packet bodies. HANDLE worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr); if (worker) CloseHandle(worker); else LogLine("CreateThread failed: %u", GetLastError()); } namespace { // RVAs are derived from the new client's fixed image base 0x00400000 and // were verified against its directory object and successful 10011 connect. constexpr uintptr_t kNewChannelFindRva = 0x01739D80; constexpr uintptr_t kNewChannelSelectRva = 0x01739CC0; constexpr uintptr_t kNewChannelConnectorGetterRva = 0x01B4CF70; constexpr uintptr_t kNewChannelEndpointHandlerRva = 0x01B4F1E0; // The native channel-start path marks the shared connection state immediately // after the endpoint handler returns. Without this follow-up the socket is // open, but the packet manager stays in its constructor state and rejects the // first class-0 notice before parsing it. // These are IDA virtual addresses. The new executable is based at 0x00400000, // so subtract that image base before adding them to GetModuleHandleW(nullptr). constexpr uintptr_t kNewConnectionStateGetterRva = 0x00FF4060; constexpr uintptr_t kNewConnectionStateSetterRva = 0x00C4C930; constexpr uintptr_t kNewPacketManagerGetterRva = 0x0211E810; constexpr uintptr_t kNewClientImageSize = 0x06A92000; // Current DB4 packet-parser evidence (DNF.exe SHA-256 // DB4ABD499BFBA88A606A566CCA43F4BF1B9C6FF1292D9BA59A3E8946E04700B2): // sub_376E6C0 jumps to sub_6E83610, whose bitset at 06A83558 admits the // native inbound opcodes. The live parser receives class1/op47, but bit 47 // is the only missing bit in the otherwise current scene set. That makes the // parser return before the registered class1/op47 DROP_ITEM handler can clear // the client's discard-pending state. Admit this one opcode only after the // exact current bitset word is verified; never apply this to the legacy image. constexpr uintptr_t kNewInboundOpcodeBitsetRva = 0x06A8355C; constexpr unsigned int kNewDropItemAckOpcode = 47; constexpr uint32_t kNewInboundOpcodeBitsetWordBefore = 0x80217FFF; constexpr uint32_t kNewInboundOpcodeBitsetWordAfter = 0x8021FFFF; // DB4A task-manual completion uses class1/op1427, whose native parser // admission bit is clear. VA 02523C96 rejects the packet before the class1 // reader at 02523CF6, even though the quest callback is registered. Enable // only bit 19 in this verified word; keep the legacy op1429 bit unchanged. constexpr uintptr_t kNewQuestAutoCompleteAckBitsetRva = 0x06A83608; constexpr unsigned int kNewQuestAutoCompleteAckOpcode = 1427; constexpr uint32_t kNewQuestAutoCompleteAckWordBefore = 0x20620000; constexpr uint32_t kNewQuestAutoCompleteAckWordAfter = 0x206A0000; static_assert((kNewQuestAutoCompleteAckWordBefore ^ kNewQuestAutoCompleteAckWordAfter) == (1u << (kNewQuestAutoCompleteAckOpcode % 32)), "Quest completion ACK compatibility changes only its admission bit"); // DB4 registers the complete mailbox family, but its inbound parser rejects // these six opcodes before dispatch. op97 carries both the letter body and // attachment rows; accepting only the op96 ACK therefore opens an empty UI. // Evidence: current DB4 class0 readers 02043B90/02033020/02032F40 and class1 // readers 01FD6DA0/01FBC8C0/01FBC790. Preserve every non-mailbox admission bit. // op789 is a C2S request, not a mailbox response, and is deliberately absent. struct NewMailboxAdmissionWord { uintptr_t rva; uint32_t before; uint32_t after; unsigned int firstOpcode; const char* operation; }; constexpr NewMailboxAdmissionWord kNewMailboxAdmissionWords[] = { {0x06A83564, 0x11481279, 0x1148127F, 97, "MAILBOX_LIST_REMOVE"}, {0x06A83568, 0x02208410, 0x02208450, 134, "MAILBOX_STATE"}, {0x06A8357C, 0x001E0080, 0x081E0080, 315, "MAILBOX_MULTI_SEND"}, {0x06A83580, 0x00001800, 0x00001810, 324, "MAILBOX_QUERY"}, {0x06A835B0, 0x40000000, 0x40004000, 718, "MAILBOX_RECIPIENTS"}, }; // Current 111MB client: opcode 0x0595 (1429, // ENUM_CMDPACKET_CLEAR_QUEST_TICKET) is constructed by this native helper. // The old 82MB task-window RVAs above do not exist in this image. Keep this // probe current-client-only and fail closed when the prologue changes. constexpr uintptr_t kNewQuestAutoCompleteRequestBuilderRva = 0x0076FFA0; constexpr uintptr_t kNewQuestAutoCompleteUiCallbackRva = 0x0050B390; void* g_originalNewQuestAutoCompleteRequest = nullptr; void* g_originalNewQuestAutoCompleteUiCallback = nullptr; volatile LONG g_newQuestAutoCompleteRequestCount = 0; volatile LONG g_newQuestAutoCompleteUiCallbackCount = 0; void __cdecl LogNewQuestAutoCompleteRequest( void* self, unsigned int selector, uintptr_t caller) { LONG callNumber = InterlockedIncrement( &g_newQuestAutoCompleteRequestCount); const uintptr_t base = reinterpret_cast( GetModuleHandleW(nullptr)); const unsigned int callerRva = (base && caller >= base) ? static_cast(caller - base) : 0; LogLine("new-client quest request builder call=%ld self=%p selector=%u " "caller_rva=0x%08X", callNumber, self, selector, callerRva); } // Preserve the native registers, return value, stack and cleanup contract. // The probe only logs entry and then tail-jumps to the stolen instructions. __declspec(naked) void ProxyNewQuestAutoCompleteRequest() { __asm { pushfd pushad mov eax, [esp + 24h] // native return address mov edx, [esp + 28h] // first native stack argument mov ecx, [esp + 18h] // saved native ECX push eax push edx push ecx call LogNewQuestAutoCompleteRequest add esp, 0Ch popad popfd jmp dword ptr [g_originalNewQuestAutoCompleteRequest] } } bool InstallNewClientQuestAutoCompleteTrace(uintptr_t base) { static const unsigned char kPrologue[] = { 0x55, 0x8B, 0xEC, 0x68, 0x95, 0x05, 0x00, 0x00 }; const uintptr_t target = base + kNewQuestAutoCompleteRequestBuilderRva; if (!BytesMatch(reinterpret_cast(target), kPrologue, sizeof(kPrologue))) { LogLine("new-client quest request builder trace skipped: prologue " "mismatch target=0x%08X", target); return false; } void* original = nullptr; if (!InstallKnownInlineHook(target, kPrologue, sizeof(kPrologue), reinterpret_cast(&ProxyNewQuestAutoCompleteRequest), &original, "new-client quest request builder trace")) { return false; } g_originalNewQuestAutoCompleteRequest = original; LogLine("new-client quest request builder trace installed target=0x%08X " "rva=0x%08X", target, static_cast(kNewQuestAutoCompleteRequestBuilderRva)); return true; } void __cdecl LogNewQuestAutoCompleteUiCallback( unsigned int argument, uintptr_t caller) { LONG callNumber = InterlockedIncrement( &g_newQuestAutoCompleteUiCallbackCount); const uintptr_t base = reinterpret_cast( GetModuleHandleW(nullptr)); const unsigned int callerRva = (base && caller >= base) ? static_cast(caller - base) : 0; LogLine("new-client quest UI callback call=%ld argument=%u " "caller_rva=0x%08X", callNumber, argument, callerRva); } __declspec(naked) void ProxyNewQuestAutoCompleteUiCallback() { __asm { pushfd pushad mov eax, [esp + 24h] // native return address mov edx, [esp + 28h] // first native stack argument push eax push edx call LogNewQuestAutoCompleteUiCallback add esp, 8 popad popfd jmp dword ptr [g_originalNewQuestAutoCompleteUiCallback] } } bool InstallNewClientQuestAutoCompleteUiTrace(uintptr_t base) { static const unsigned char kPrologue[] = { // push ebp; mov ebp,esp; sub esp,44h; mov eax,[055B3BDCh]. // Include all five bytes of A1 so the trampoline resumes on a // complete instruction boundary. 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x44, 0xA1, 0xDC, 0x3B, 0x5B, 0x05 }; const uintptr_t target = base + kNewQuestAutoCompleteUiCallbackRva; if (!BytesMatch(reinterpret_cast(target), kPrologue, sizeof(kPrologue))) { LogLine("new-client quest UI callback trace skipped: prologue " "mismatch target=0x%08X", target); return false; } void* original = nullptr; if (!InstallKnownInlineHook(target, kPrologue, sizeof(kPrologue), reinterpret_cast(&ProxyNewQuestAutoCompleteUiCallback), &original, "new-client quest UI callback trace")) { return false; } g_originalNewQuestAutoCompleteUiCallback = original; LogLine("new-client quest UI callback trace installed target=0x%08X " "rva=0x%08X", target, static_cast(kNewQuestAutoCompleteUiCallbackRva)); return true; } // Both directory helpers end in `ret 8`; they are stdcall even though the // native callers do not carry a C++ declaration in the shipped client. typedef void* (__stdcall* NewChannelFindFn)(unsigned int, unsigned int); typedef void* (__stdcall* NewChannelSelectFn)(unsigned int, unsigned int); typedef void* (__cdecl* NewChannelConnectorGetterFn)(); typedef unsigned char (__thiscall* NewChannelEndpointHandlerFn)( void*, unsigned int, unsigned int); typedef void* (__cdecl* NewConnectionStateGetterFn)(); typedef void (__thiscall* NewConnectionStateSetterFn)(void*, unsigned char); typedef void* (__cdecl* NewPacketManagerGetterFn)(); unsigned int NewClientChannelID() { wchar_t value[16] = {}; DWORD length = GetEnvironmentVariableW( L"DNF_AUTO_CONNECT_CHANNEL_ID", value, _countof(value)); if (!length || length >= _countof(value)) return 11; wchar_t* end = nullptr; unsigned long parsed = wcstoul(value, &end, 10); return end && *end == L'\0' && parsed > 0 && parsed < 1000 ? static_cast(parsed) : 11; } unsigned int NewClientGamePortBase() { // The controller owns the selected game-port base. Keep the old native // default for launches outside the controller, but reject signs, // whitespace, overflow, and bases that cannot represent every supported // channel ID. A fixed 10000 base would leave this worker polling a dead // socket after the runtime moves out of a Windows excluded-port range. constexpr unsigned int kDefaultBase = 10000; constexpr unsigned int kMaxChannelID = 999; wchar_t value[16] = {}; DWORD length = GetEnvironmentVariableW( L"DNF_GAME_PORT_BASE", value, _countof(value)); if (!length || length >= _countof(value)) return kDefaultBase; unsigned int parsed = 0; for (DWORD i = 0; i < length; ++i) { if (value[i] < L'0' || value[i] > L'9') return kDefaultBase; unsigned int digit = static_cast(value[i] - L'0'); if (parsed > (65535u - digit) / 10u) return kDefaultBase; parsed = parsed * 10u + digit; } if (parsed == 0 || parsed > 65535u - kMaxChannelID) { return kDefaultBase; } return parsed; } bool InstallNewClientInboundAckCompatibility(uintptr_t base, const wchar_t* environmentName, const char* operation, uintptr_t wordRva, unsigned int opcode, uint32_t before, uint32_t after) { wchar_t enabled[8] = {}; DWORD length = GetEnvironmentVariableW( environmentName, enabled, _countof(enabled)); if (length > 0 && enabled[0] == L'0') { LogLine("new-client %s ACK compatibility disabled by configuration", operation); return false; } unsigned char* target = reinterpret_cast( base + wordRva); uint32_t current = 0; __try { memcpy(¤t, target, sizeof(current)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("new-client %s ACK compatibility skipped: bitset read exception target=0x%08X", operation, reinterpret_cast(target)); return false; } if (current == after) { LogLine("new-client %s ACK compatibility already installed target=0x%08X opcode=%u word=0x%08X", operation, reinterpret_cast(target), opcode, static_cast(current)); return true; } if (current != before) { LogLine("new-client %s ACK compatibility refused bitset mismatch target=0x%08X expected=0x%08X actual=0x%08X", operation, reinterpret_cast(target), static_cast(before), static_cast(current)); return false; } DWORD oldProtection = 0; if (!VirtualProtect(target, sizeof(after), PAGE_READWRITE, &oldProtection)) { LogLine("new-client %s ACK compatibility VirtualProtect failed target=0x%08X error=%lu", operation, reinterpret_cast(target), GetLastError()); return false; } bool written = false; __try { memcpy(target, &after, sizeof(after)); uint32_t verified = 0; memcpy(&verified, target, sizeof(verified)); written = verified == after; } __except (EXCEPTION_EXECUTE_HANDLER) { written = false; } DWORD ignoredProtection = 0; VirtualProtect(target, sizeof(after), oldProtection, &ignoredProtection); FlushInstructionCache(GetCurrentProcess(), target, sizeof(after)); if (!written) { LogLine("new-client %s ACK compatibility write verification failed target=0x%08X", operation, reinterpret_cast(target)); return false; } LogLine("new-client %s ACK compatibility installed target=0x%08X rva=0x%08X opcode=%u before=0x%08X after=0x%08X scope=current-db4-parser", operation, reinterpret_cast(target), static_cast(wordRva), opcode, static_cast(before), static_cast(after)); return true; } bool InstallNewClientDropItemAckCompatibility(uintptr_t base) { return InstallNewClientInboundAckCompatibility(base, L"DNF_DROP_ITEM_ACK_COMPAT", "DROP_ITEM", kNewInboundOpcodeBitsetRva, kNewDropItemAckOpcode, kNewInboundOpcodeBitsetWordBefore, kNewInboundOpcodeBitsetWordAfter); } bool InstallNewClientQuestAutoCompleteAckCompatibility(uintptr_t base) { return InstallNewClientInboundAckCompatibility(base, L"DNF_QUEST_AUTO_COMPLETE_ACK_COMPAT", "QUEST_AUTO_COMPLETE", kNewQuestAutoCompleteAckBitsetRva, kNewQuestAutoCompleteAckOpcode, kNewQuestAutoCompleteAckWordBefore, kNewQuestAutoCompleteAckWordAfter); } bool InstallNewClientMailboxCompatibility(uintptr_t base) { // Check the whole family before changing any word. A different image or // unexpected third-party patch must not leave a partially enabled mailbox. for (const auto& word : kNewMailboxAdmissionWords) { uint32_t current = 0; __try { memcpy(¤t, reinterpret_cast(base + word.rva), sizeof(current)); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("new-client MAILBOX compatibility refused: bitset read exception rva=0x%08X", static_cast(word.rva)); return false; } if (current != word.before && current != word.after) { LogLine("new-client MAILBOX compatibility refused: bitset mismatch rva=0x%08X expected=0x%08X actual=0x%08X", static_cast(word.rva), word.before, current); return false; } } for (const auto& word : kNewMailboxAdmissionWords) { if (!InstallNewClientInboundAckCompatibility(base, L"DNF_MAILBOX_COMPAT", word.operation, word.rva, word.firstOpcode, word.before, word.after)) { return false; } } return true; } using NewCompanionActorLookupFn = void* (__cdecl*)(unsigned short); NewCompanionActorLookupFn g_newCompanionActorLookup = nullptr; void* __cdecl ProxyNewCompanionActorLookup(unsigned short key) { void* actor = dnf90_companion_p0::ProjectionActorOverrideForObjectKey(key); return actor ? actor : (g_newCompanionActorLookup ? g_newCompanionActorLookup(key) : nullptr); } bool InstallNewClientCompanionCompatibility(uintptr_t base) { using namespace dnf90_companion_p0; using namespace dnf90_companion_new_native; if (!base || !ConfigureNewNativeBindings()) return false; // Complete instruction boundaries. The protected peer entry starts with // a relative jump, whose displacement must be relocated in its trampoline. const unsigned char seh[] = {0x55,0x8B,0xEC,0x6A,0xFF}; const unsigned char condition[] = {0x55,0x8B,0xEC,0x83,0xEC,0x14}; const unsigned char encoder[] = {0x55,0x8B,0xEC,0x83,0xEC,0x08}; const unsigned char peer[] = {0xE9,0x00,0x11,0x52,0x03}; const unsigned char lookup[] = {0x55,0x8B,0xEC,0xA1,0x58,0x37,0x6A,0x05}; const unsigned char dispatch[] = {0x55,0x8B,0xEC,0x53,0x8B,0x5D,0x0C}; const unsigned char capture[] = {0x8A,0x45,0xA3,0xC7,0x45,0xAC,0x01,0x00,0x00,0x00}; void* original = nullptr; if (!InstallKnownInlineHook(base + 0x02364160, seh, sizeof(seh), reinterpret_cast(&ProxyAicCopy), &original, "new-native companion AIC copy")) return false; SetOriginalAicCopy(original); if (!InstallKnownInlineHook(base + 0x0248A260, seh, sizeof(seh), reinterpret_cast(&ProxyApcRegistration), &original, "new-native companion registration")) return false; SetOriginalApcRegistration(original); if (!InstallKnownInlineHook(base + 0x024F98B0, condition, sizeof(condition), reinterpret_cast(&ProxyAICondition), &original, "new-native companion AI")) return false; SetOriginalAICondition(original); if (!InstallKnownInlineHook(base + 0x034FB0E0, peer, sizeof(peer), reinterpret_cast(&ProxyPeerDelivery), &original, "new-native companion peer delivery")) return false; const uintptr_t peerTarget = base + 0x034FB0E0 + 5 + 0x03521100; *reinterpret_cast(static_cast(original) + 1) = static_cast(peerTarget - reinterpret_cast(original) - 5); FlushInstructionCache(GetCurrentProcess(), original, sizeof(peer)); SetOriginalPeerDelivery(original); if (!InstallKnownInlineHook(base + 0x02E83610, encoder, sizeof(encoder), reinterpret_cast(&ProxyPayloadEncode), &original, "new-native companion op40 loopback")) return false; SetOriginalPayloadEncode(original); if (!InstallKnownInlineHook(base + 0x0256D730, lookup, sizeof(lookup), reinterpret_cast(&ProxyNewCompanionActorLookup), &original, "new-native companion actor lookup")) return false; g_newCompanionActorLookup = reinterpret_cast(original); if (!InstallKnownInlineHook(base + 0x0211E340, dispatch, sizeof(dispatch), reinterpret_cast(&ProxyNewNativeClass0Dispatch), &original, "new-native companion class0 safe point")) return false; SetOriginalNewNativeClass0Dispatch(original); SetProjectionClass0Dispatch(original); ConfigureNewNativeCaptureResume(base, &LogLine); if (!InstallKnownInlineHook(base + 0x02123C8A, capture, sizeof(capture), reinterpret_cast(&ProxyNewNativeProjectionCapture), &original, "new-native companion decoded capture")) return false; SetNewNativeHooksReady(true); return true; } DWORD WINAPI NewClientChannelWorker(void*) { // The proxy is loaded while the executable is still inside its startup // path. The directory manager and connector singleton are not valid at // that point; calling their fixed-address helpers immediately can make // the native client take its invalid-parameter termination path. Give // the executable time to finish its own initialization and open the // directory connection before touching those objects. Sleep(5000); const uintptr_t base = reinterpret_cast( GetModuleHandleW(nullptr)); if (!base) return 0; bool dropItemAckInstalled = InstallNewClientDropItemAckCompatibility(base); LogLine("new-client DROP_ITEM ACK compatibility result=%d", dropItemAckInstalled ? 1 : 0); bool questAutoCompleteAckInstalled = InstallNewClientQuestAutoCompleteAckCompatibility(base); LogLine("new-client QUEST_AUTO_COMPLETE ACK compatibility result=%d", questAutoCompleteAckInstalled ? 1 : 0); const bool mailboxInstalled = InstallNewClientMailboxCompatibility(base); LogLine("new-client MAILBOX compatibility result=%d", mailboxInstalled ? 1 : 0); const bool companionInstalled = InstallNewClientCompanionCompatibility(base); LogLine("new-client companion compatibility result=%d", companionInstalled ? 1 : 0); InstallNewClientQuestAutoCompleteTrace(base); InstallNewClientQuestAutoCompleteUiTrace(base); const unsigned int channelID = NewClientChannelID(); const unsigned int address = 0x0100007Fu; // 127.0.0.1 in native order const unsigned int port = NewClientGamePortBase() + channelID; NewChannelFindFn findChannel = reinterpret_cast( base + kNewChannelFindRva); NewChannelSelectFn selectChannel = reinterpret_cast( base + kNewChannelSelectRva); NewChannelConnectorGetterFn getConnector = reinterpret_cast( base + kNewChannelConnectorGetterRva); NewChannelEndpointHandlerFn connectEndpoint = reinterpret_cast( base + kNewChannelEndpointHandlerRva); NewConnectionStateGetterFn getConnectionState = reinterpret_cast( base + kNewConnectionStateGetterRva); NewConnectionStateSetterFn setConnectionState = reinterpret_cast( base + kNewConnectionStateSetterRva); NewPacketManagerGetterFn getPacketManager = reinterpret_cast( base + kNewPacketManagerGetterRva); LogLine("new-client channel worker started base=0x%08X channel=%u " "address=0x%08X port=%u", base, channelID, address, port); // The native startup path initializes this manager asynchronously. The // channel socket must not be opened while its +4 gate is still zero: the // native consumer will reject the first class-0 notice and the client // will send its timeout exit packet. Wait for the manager's own startup // call to finish instead of invoking that call from this worker thread; // the native routine requires the startup thread's TLS and is not safe to // run concurrently with the executable's initialization. void* packetManager = nullptr; unsigned int packetManagerReady = 0; for (unsigned int waited = 0; waited < 60000; waited += 100) { __try { packetManager = getPacketManager ? getPacketManager() : nullptr; packetManagerReady = packetManager ? *reinterpret_cast( reinterpret_cast(packetManager) + 4) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { packetManagerReady = 0; } if (packetManagerReady) break; Sleep(100); } LogLine("new-client packet manager ready manager=%p ready=%u", packetManager, packetManagerReady); if (!packetManagerReady) { LogLine("new-client packet manager readiness timed out"); return 0; } // Directory application is asynchronous. Bound the wait so a malformed // directory cannot leave a permanent worker in a process that never // reaches the channel screen. for (unsigned int waited = 0; waited < 120000; waited += 100) { void* channel = nullptr; __try { channel = findChannel(address, port); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("new-client channel lookup exception waited_ms=%u", waited); Sleep(100); continue; } if (!channel) { Sleep(100); continue; } void* connector = nullptr; unsigned char result = 0; __try { // The native selector updates the same global used by the UI // selection path. The endpoint handler then performs the full // connector state transition and socket connect. // channel_select performs the native wrapper allocation and // publishes the selected-channel global itself. Do not replace // that wrapper with the raw directory object: later native code // resolves the selected channel through the wrapper's vtable. selectChannel(address, port); connector = getConnector(); result = connector ? connectEndpoint( connector, address, port) : 0; } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("new-client channel connect exception channel=%p " "connector=%p", channel, connector); return 0; } LogLine("new-client channel connect channel=%p connector=%p " "address=0x%08X port=%u result=%u", channel, connector, address, port, static_cast(result)); if (result) { // This is the same post-endpoint transition used by the native // channel selection handler (call 0x13F4060, then 0x104C930(0)). // It changes only the client's own connection-state object; the // server still owns the packet and login semantics. void* connectionState = nullptr; __try { connectionState = getConnectionState(); if (connectionState) setConnectionState(connectionState, 0); } __except (EXCEPTION_EXECUTE_HANDLER) { LogLine("new-client connection state transition exception " "state=%p", connectionState); return 0; } LogLine("new-client connection state transitioned state=%p", connectionState); return 0; } // A rejected attempt can leave the directory object valid while the // native manager is still rebuilding. Retry at a low rate against // the same endpoint; no packet or client state is fabricated here. Sleep(1000); } LogLine("new-client channel worker timed out channel=%u port=%u", channelID, port); return 0; } } bool IsNewClientCompatibilityMode() { wchar_t enabled[8] = {}; DWORD length = GetEnvironmentVariableW( L"DNF_NEW_CLIENT_COMPAT", enabled, _countof(enabled)); if (length > 0) return enabled[0] == L'1'; HMODULE executable = GetModuleHandleW(nullptr); if (!executable) return false; __try { const auto* dos = reinterpret_cast( executable); if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; const auto* nt = reinterpret_cast( reinterpret_cast(executable) + dos->e_lfanew); return nt->Signature == IMAGE_NT_SIGNATURE && nt->OptionalHeader.SizeOfImage == kNewClientImageSize; } __except (EXCEPTION_EXECUTE_HANDLER) { return false; } } void StartNewClientChannelCompatibility() { HANDLE worker = CreateThread(nullptr, 0, NewClientChannelWorker, nullptr, 0, nullptr); if (worker) CloseHandle(worker); else LogLine("new-client channel worker CreateThread failed error=%lu", GetLastError()); }