DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{A6828B55-5DD9-42F6-8B67-E1B036BE0727}</ProjectGuid>
<RootNamespace>DebugCompat90CNSmoke</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<TargetName>90CN-debug-smoke</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(ProjectDir)$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)$(Configuration)\90CN-debug-smoke\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="debug-compat\smoke_test.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{24B08AC0-5F6E-4B0C-A3E2-11DCE7554C8A}</ProjectGuid>
<RootNamespace>DebugCompat90CN</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<TargetName>90CN-debug</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<TargetName>90CN-debug</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(ProjectDir)$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)$(Configuration)\90CN-debug\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(ProjectDir)$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)$(Configuration)\90CN-debug\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>debug-compat\90CN-debug.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>debug-compat\90CN-debug.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="debug-compat\dllmain.cpp" />
<ClCompile Include="debug-compat\debug_compat.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
+16316
View File
File diff suppressed because it is too large. Load diff
+306
View File
@@ -0,0 +1,306 @@
#pragma once
#include <windows.h>
// Starts the one-time current-EXE cipher proxy after 86JP.dll is loaded by
// the normal ijl15 plugin chain.
void Start90CNPatch();
// Returns true when the loaded executable is the separately supported
// 2026-10 new client. That executable has a different native layout and
// must enter the narrow channel bootstrap worker instead of the production
// hook set built for the current NoPack client.
bool IsNewClientCompatibilityMode();
// Starts the new-client channel selector/connector worker. It waits for the
// native directory object, selects the configured local channel, and invokes
// the client's own endpoint handler; it does not synthesize packets.
void StartNewClientChannelCompatibility();
// 接口名称:Queue90CNClientNotice
// 中文说明:把一条 UTF-16 游戏提示复制到 90CN 主线程队列,并唤醒游戏窗口线程。
// 参数:text 为不包含内嵌 NUL 的文本;length 为字符数,不包含结尾 NUL,最大 511。
// 返回值:TRUE 表示已经安全入队;FALSE 表示接口未就绪、参数无效或队列已满。
// 线程约束:调用者不会直接执行游戏 UI;实际提示只在 DNF 主窗口线程中显示。
EXTERN_C BOOL WINAPI Queue90CNClientNotice(
const wchar_t* text, unsigned int length);
// 客户端事件类型:只表示已经由当前 EXE 的窗口或场景切换边界可靠确认的状态。
enum DNF90ClientEventType : unsigned int
{
DNF90_CLIENT_EVENT_UI_READY = 1,
DNF90_CLIENT_EVENT_UI_CLOSED = 2,
DNF90_CLIENT_EVENT_ENTER_TOWN = 3,
// 事件名称:enter_dungeon
// 中文说明:当前 EXE 完成副本信息、首张地图和当前角色激活后产生一次。
DNF90_CLIENT_EVENT_ENTER_DUNGEON = 4,
// 事件名称:dungeon_room_changed
// 中文说明:副本活动态接受一个与上次不同的完整 op29 房间后产生一次。
DNF90_CLIENT_EVENT_DUNGEON_ROOM_CHANGED = 5,
// 事件名称:leave_dungeon
// 中文说明:副本活动态接受最终回城 op24 时,在 enter_town 前产生一次。
DNF90_CLIENT_EVENT_LEAVE_DUNGEON = 6,
};
// 上下文标志名称:DNF90ClientEventContextFlag
// 中文说明:说明追加字段中哪些值已经由当前 EXE 的完整 typed 包可靠确认。
enum DNF90ClientEventContextFlag : unsigned int
{
DNF90_CLIENT_EVENT_CONTEXT_DUNGEON_VALID = 1u << 0,
DNF90_CLIENT_EVENT_CONTEXT_ROOM_VALID = 1u << 1,
DNF90_CLIENT_EVENT_CONTEXT_PREVIOUS_ROOM_VALID = 1u << 2,
DNF90_CLIENT_EVENT_CONTEXT_BOSS_ROOM = 1u << 3,
};
// 兼容常量名称:DNF90_CLIENT_EVENT_V1_SIZE
// 中文说明:旧版事件只有前五个字段;新旧 DLL 通过该固定前缀双向兼容。
constexpr unsigned int DNF90_CLIENT_EVENT_V1_SIZE =
5u * sizeof(unsigned int);
// 结构名称:DNF90ClientEvent
// 中文说明:90CN.dll 复制到固定容量队列中的客户端生命周期事件。
// 字段:size 为结构大小;type 为 DNF90ClientEventType;sequence 为进程内递增编号;
// processID 为客户端进程;uiThreadID 为确认该事件的 DNF UI 线程;
// contextFlags 决定后续副本字段是否有效;previousRoom* 只用于换房事件。
struct DNF90ClientEvent
{
unsigned int size;
unsigned int type;
unsigned int sequence;
unsigned int processID;
unsigned int uiThreadID;
unsigned int contextFlags;
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;
};
static_assert(sizeof(DNF90ClientEvent) == 60,
"DNF90ClientEvent append-only ABI size changed unexpectedly");
// 接口名称:Dequeue90CNClientEvent
// 中文说明:从 90CN.dll 的固定容量队列中取出一条客户端生命周期事件。
// 参数:output 指向调用者提供的可写结构;outputSize 至少为 DNF90_CLIENT_EVENT_V1_SIZE。
// 旧调用者只取得 20 字节稳定前缀;当前调用者取得完整追加字段。
// 返回值:TRUE 表示取出一条完整事件;FALSE 表示参数无效或当前队列为空。
// 线程约束:本接口只复制事件数据,不调用 Lua,也不执行游戏 UI。
EXTERN_C BOOL WINAPI Dequeue90CNClientEvent(
DNF90ClientEvent* output, unsigned int outputSize);
// 有效标志名称:DNF90CharacterStatSnapshotFlag
// 中文说明:标记角色属性快照中已经由当前 EXE 的完整 class0/op2 包可靠取得的数据。
enum DNF90CharacterStatSnapshotFlag : unsigned int
{
DNF90_CHARACTER_STATS_BASE_VALID = 1u << 0,
DNF90_CHARACTER_STATS_SPEED_VALID = 1u << 1,
};
// 结构名称:DNF90CharacterStatSnapshot
// 中文说明:从当前 EXE 已成功处理的 92 字节角色状态块中复制出的只读快照。
// 字段:generation 每次收到有效 mode1/mode3 状态块时递增;四维已经还原为游戏数值,
// 不再保留线上协议使用的十倍缩放;速度字段保持当前协议中的原始整数单位。
struct DNF90CharacterStatSnapshot
{
unsigned int size;
unsigned int generation;
unsigned int validFlags;
unsigned int hpMax;
unsigned int mpMax;
int strength;
int vitality;
int intelligence;
int spirit;
unsigned int moveSpeed;
unsigned int attackSpeed;
unsigned int castSpeed;
};
static_assert(sizeof(DNF90CharacterStatSnapshot) == 48,
"DNF90CharacterStatSnapshot append-only ABI size changed unexpectedly");
// 接口名称:Query90CNCharacterStatSnapshot
// 中文说明:读取最近一次可靠角色属性快照,供独立 Lua DLL 计算战斗力。
// 参数:output 指向调用者提供的可写结构;outputSize 必须能容纳当前完整结构。
// 返回值:TRUE 表示已复制有效快照;FALSE 表示尚未收到角色数据或参数无效。
// 线程约束:只在 SRW 锁内复制普通数据,不读取游戏对象,也不执行游戏 UI。
EXTERN_C BOOL WINAPI Query90CNCharacterStatSnapshot(
DNF90CharacterStatSnapshot* output, unsigned int outputSize);
// 装备快照常量名称:DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS
// 中文说明:当前 EXE 的穿戴对象位图覆盖槽位 0~32;快照按已出现对象保存,
// 因而最多需要 33 条,不会读取或虚构这个范围以外的槽位。
enum : unsigned int
{
DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS = 33,
};
// 有效标志名称:DNF90EquipmentSnapshotFlag
// 中文说明:标记装备快照已经由当前 EXE 完整接受的 class0/op2 mode1/mode3
// 穿戴对象创建列表可靠取得。
enum DNF90EquipmentSnapshotFlag : unsigned int
{
DNF90_EQUIPMENT_SNAPSHOT_ROWS_VALID = 1u << 0,
};
// 结构名称:DNF90EquippedItemSnapshot
// 中文说明:一件已穿戴对象的只读线协议摘要。
// 字段:actorSlot 是当前 EXE 的运行时穿戴槽;upgradeLevel 来自 extData 低五位;
// amplifyType 与 amplifyValue 来自同一条已验证 0x77 状态投影。
// 边界:itemID/qualitySeed 只作为身份与换装变化证据;PVF 词条由服务端权威投影提供,
// Lua 不凭线协议 ID 猜稀有度或词条。
struct DNF90EquippedItemSnapshot
{
unsigned int itemID;
unsigned int qualitySeed;
unsigned short durability;
unsigned short amplifyValue;
unsigned char actorSlot;
unsigned char upgradeLevel;
unsigned char amplifyType;
unsigned char reserved;
};
static_assert(sizeof(DNF90EquippedItemSnapshot) == 16,
"DNF90EquippedItemSnapshot append-only ABI size changed unexpectedly");
// 结构名称:DNF90EquipmentSnapshot
// 中文说明:最近一次本地角色完整穿戴对象创建列表的固定容量只读副本。
// 字段:generation 每次接受完整列表时递增;sourceStatGeneration 对应同包属性版本;
// itemCount 仅说明 items 前多少条有效,允许为零以表示确实没有穿戴对象。
struct DNF90EquipmentSnapshot
{
unsigned int size;
unsigned int generation;
unsigned int sourceStatGeneration;
unsigned int validFlags;
unsigned int itemCount;
DNF90EquippedItemSnapshot items[DNF90_EQUIPMENT_SNAPSHOT_MAX_ITEMS];
};
static_assert(sizeof(DNF90EquipmentSnapshot) == 548,
"DNF90EquipmentSnapshot append-only ABI size changed unexpectedly");
// 接口名称:Query90CNEquipmentSnapshot
// 中文说明:读取最近一次可靠装备快照,供独立 Lua DLL 计算装备加成。
// 参数:output 指向调用者提供的可写结构;outputSize 必须能容纳当前完整结构。
// 返回值:TRUE 表示已复制有效快照;FALSE 表示尚未收到完整列表或参数无效。
// 线程约束:只在 SRW 锁内复制普通数据,不读取游戏对象,也不执行游戏 UI。
EXTERN_C BOOL WINAPI Query90CNEquipmentSnapshot(
DNF90EquipmentSnapshot* output, unsigned int outputSize);
// 有效标志名称:DNF90DamageAffixSnapshotFlag
// 中文说明:标记伤害词条快照已由服务端按当前 Script.pvf 与完整穿戴位图计算完成。
enum DNF90DamageAffixSnapshotFlag : unsigned int
{
DNF90_DAMAGE_AFFIX_SNAPSHOT_VALUES_VALID = 1u << 0,
DNF90_DAMAGE_AFFIX_SNAPSHOT_IDENTITY_VALID = 1u << 1,
DNF90_DAMAGE_AFFIX_SNAPSHOT_THREE_ATTACKS_VALID = 1u << 2,
DNF90_DAMAGE_AFFIX_SNAPSHOT_EQUIPMENT_SCORE_VALID = 1u << 3,
};
// 结构名称:DNF90DamageAffixSnapshot
// 中文说明:服务端从当前 Script.pvf 汇总的穿戴词条只读快照;所有百分比以 0.1% 为单位。
// 字段说明:whiteDamageTenths=白字(附加伤害),yellowDamageTenths=黄字(对敌人伤害增加),
// criticalDamageTenths=爆伤(暴击伤害);黄追和爆追始终单独保存,不能冒充前三项;
// allAttackTenths=所有攻击力/最终伤害,只并入三攻加成;
// pvfEquipmentScore=服务端按 PVF 等级、品级和稀有度计算的装备基础分。
struct DNF90DamageAffixSnapshot
{
unsigned int size;
unsigned int generation;
unsigned int validFlags;
unsigned int version;
unsigned int whiteDamageTenths;
unsigned int yellowDamageTenths;
unsigned int criticalDamageTenths;
unsigned int yellowAdditionalTenths;
unsigned int criticalAdditionalTenths;
unsigned int allAttackTenths;
unsigned int equippedItemCount;
unsigned int activeSetCount;
unsigned int job;
unsigned int growType;
unsigned int level;
unsigned int physicalAttack;
unsigned int magicalAttack;
unsigned int independentAttack;
char professionUtf8[32];
unsigned int pvfEquipmentScore;
};
static_assert(sizeof(DNF90DamageAffixSnapshot) == 108,
"DNF90DamageAffixSnapshot append-only ABI size changed unexpectedly");
// 接口名称:Query90CNDamageAffixSnapshot
// 中文说明:读取服务端按 PVF 汇总的完整穿戴词条,供独立 Lua DLL 计算并显示战斗力。
// 参数:output 为调用方可写结构;outputSize 必须容纳完整结构。
// 返回值:TRUE 表示已有有效快照;FALSE 表示尚未收到投影或参数无效。
EXTERN_C BOOL WINAPI Query90CNDamageAffixSnapshot(
DNF90DamageAffixSnapshot* output, unsigned int outputSize);
// 有效标志名称:DNF90CombatPanelStateFlag
// 中文说明:说明 Lua 提交的战斗力面板中哪些分项已经有真实数据来源。
enum DNF90CombatPanelStateFlag : unsigned int
{
DNF90_COMBAT_PANEL_ENABLED = 1u << 0,
DNF90_COMBAT_PANEL_BASE_SCORE_VALID = 1u << 1,
DNF90_COMBAT_PANEL_EQUIPMENT_SCORE_VALID = 1u << 2,
DNF90_COMBAT_PANEL_DAMAGE_AFFIXES_VALID = 1u << 3,
DNF90_COMBAT_PANEL_IDENTITY_VALID = 1u << 4,
DNF90_COMBAT_PANEL_THREE_ATTACKS_VALID = 1u << 5,
};
// 结构名称:DNF90CombatPanelState
// 中文说明:Lua 完成公式计算后交给主 DLL 绘制的纯数据状态。
// 字段:sourceGeneration 对应属性快照版本;formulaVersion 用于区分将来的公式升级;
// equipmentScore 只有在对应有效标志存在时才允许显示为数值。
struct DNF90CombatPanelState
{
unsigned int size;
unsigned int revision;
unsigned int sourceGeneration;
unsigned int validFlags;
unsigned int formulaVersion;
unsigned int totalScore;
unsigned int baseAttributeScore;
unsigned int equipmentScore;
unsigned int hpMax;
unsigned int mpMax;
int strength;
int vitality;
int intelligence;
int spirit;
unsigned int affixGeneration;
unsigned int whiteDamageTenths;
unsigned int yellowDamageTenths;
unsigned int criticalDamageTenths;
unsigned int yellowAdditionalTenths;
unsigned int criticalAdditionalTenths;
unsigned int allAttackTenths;
unsigned int equippedItemCount;
unsigned int activeSetCount;
unsigned int job;
unsigned int growType;
unsigned int level;
unsigned int physicalAttack;
unsigned int magicalAttack;
unsigned int independentAttack;
char professionUtf8[32];
};
static_assert(sizeof(DNF90CombatPanelState) == 148,
"DNF90CombatPanelState append-only ABI size changed unexpectedly");
// 接口名称:Update90CNCombatPanel
// 中文说明:复制 Lua 已计算好的面板状态,并唤醒 DNF UI 线程刷新侧栏。
// 参数:state 指向只读状态;stateSize 必须等于当前完整结构大小。
// 返回值:TRUE 表示已安全接收;FALSE 表示接口未就绪、参数无效或数值越界。
// 线程约束:调用者不直接绘制;所有窗口显示、隐藏和重绘均在 DNF UI 线程执行。
EXTERN_C BOOL WINAPI Update90CNCombatPanel(
const DNF90CombatPanelState* state, unsigned int stateSize);
+117
View File
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{bb1b2c5d-c76c-46e2-8ac6-1336cab9c90b}</ProjectGuid>
<RootNamespace>My90CN</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<TargetName>90CN</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(ProjectDir)$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)$(Configuration)\90CN\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(ProjectDir)$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)$(Configuration)\90CN\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<BufferSecurityCheck>false</BufferSecurityCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="90CN.cpp" />
<ClCompile Include="90CN_digest_trace.cpp" />
<ClCompile Include="90CN_companion_p0.cpp" />
<ClCompile Include="90CN_seria_minimap.cpp" />
<ClCompile Include="90CN_op3_parent_fix.cpp" />
<ClCompile Include="90CN_selected_page_fix.cpp" />
<ClCompile Include="90CN_dungeon_skill_gate_fix.cpp" />
<ClCompile Include="90CN_weapon_skill_relogin_fix.cpp" />
<ClCompile Include="90CN_3149_writer_resync_fix.cpp" />
<ClCompile Include="raid_probe.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="client_assets.rc" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="90CN.h" />
<ClInclude Include="90CN_digest_trace.h" />
<ClInclude Include="90CN_companion_p0.h" />
<ClInclude Include="90CN_companion_native_bindings.h" />
<ClInclude Include="90CN_companion_new_native.h" />
<ClInclude Include="90CN_expert_job_projection.h" />
<ClInclude Include="90CN_premium_clock_policy.h" />
<ClInclude Include="90CN_first_login_recovery_policy.h" />
<ClInclude Include="90CN_seria_minimap.h" />
<ClInclude Include="90CN_seria_room_catalog.h" />
<ClInclude Include="90CN_seria_room_policy.h" />
<ClInclude Include="90CN_op3_parent_fix.h" />
<ClInclude Include="90CN_selected_page_fix.h" />
<ClInclude Include="90CN_dungeon_skill_gate_fix.h" />
<ClInclude Include="90CN_weapon_skill_relogin_fix.h" />
<ClInclude Include="90CN_3149_writer_resync_fix.h" />
<ClInclude Include="client_assets.h" />
<ClInclude Include="XLog.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>
@@ -0,0 +1,282 @@
#include "90CN_3149_writer_resync_fix.h"
#include <stdint.h>
#include <string.h>
namespace
{
// Runtime-verified BAD path:
// 01B947C0 self-removal branch
// 01B947D6 push 0
// 01B947D8 call [vtable+6AC]
// return 01B947DA
// On the affected retained transition, that virtual call resolves to
// 01B9A1A0(0), which directly clears both tier1 +3D1 bytes. The last tier1
// +0x28 update has already happened by then, so manager mode remains 0.
constexpr uintptr_t kSelfRemovalWriterCallsiteRva = 0x017947D6; // VA 01B947D6
constexpr uintptr_t kSelfRemovalWriterContinueRva = 0x017947DC; // VA 01B947DC
// 01B9A1A0 direct +3D1 writes for slot1 and slot0.
constexpr uintptr_t kWriterBSlot1WriteRva = 0x0179A275; // VA 01B9A275
constexpr uintptr_t kWriterBSlot0WriteRva = 0x0179A35D; // VA 01B9A35D
constexpr uintptr_t kWriterBSlot1ContinueRva = 0x0179A27B;
constexpr uintptr_t kWriterBSlot0ContinueRva = 0x0179A363;
constexpr uintptr_t kSceneRootPointerRva = 0x04DB2764;
constexpr uintptr_t kSceneUiManagerRva = 0x01490EE0;
constexpr size_t kD1Offset = 0x3D1;
constexpr size_t kModeOffset = 0x719;
uintptr_t g_exeBase = 0;
uintptr_t g_callsiteContinue = 0;
uintptr_t g_slot1Continue = 0;
uintptr_t g_slot0Continue = 0;
volatile LONG g_captureActive = 0;
volatile LONG g_captureThreadId = 0;
uintptr_t g_captureFirst = 0;
uintptr_t g_captureSecond = 0;
typedef void* (__thiscall* SceneUiManagerFn)(void*);
typedef void (__thiscall* Tier1UpdateFn)(void*);
unsigned int ReadManagerMode(bool* fault)
{
if (fault) *fault = false;
unsigned int mode = 0xFFFFFFFFu;
__try {
void* scene = *reinterpret_cast<void**>(g_exeBase + kSceneRootPointerRva);
if (!scene) return mode;
SceneUiManagerFn getManager = reinterpret_cast<SceneUiManagerFn>(
g_exeBase + kSceneUiManagerRva);
void* manager = getManager ? getManager(scene) : nullptr;
if (!manager) return mode;
mode = *reinterpret_cast<unsigned char*>(
reinterpret_cast<uintptr_t>(manager) + kModeOffset);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
if (fault) *fault = true;
mode = 0xFFFFFFFFu;
}
return mode;
}
unsigned int ReadD1(uintptr_t object, bool* fault)
{
if (fault) *fault = false;
if (!object) return 0xFFFFFFFFu;
unsigned int value = 0xFFFFFFFFu;
__try {
value = *reinterpret_cast<unsigned char*>(object + kD1Offset);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
if (fault) *fault = true;
}
return value;
}
bool PatchJumpExact(uintptr_t target, const unsigned char* expected, size_t patchLength,
void* detour, uintptr_t continuation)
{
if (!target || !expected || patchLength < 5 || !detour || !continuation) return false;
__try {
if (memcmp(reinterpret_cast<const void*>(target), expected, patchLength) != 0) {
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
DWORD oldProtection = 0;
if (!VirtualProtect(reinterpret_cast<void*>(target), patchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
return false;
}
unsigned char* site = reinterpret_cast<unsigned char*>(target);
site[0] = 0xE9;
*reinterpret_cast<LONG*>(site + 1) = static_cast<LONG>(
reinterpret_cast<uintptr_t>(detour) - (target + 5));
for (size_t i = 5; i < patchLength; ++i) site[i] = 0x90;
DWORD ignoredProtection = 0;
VirtualProtect(site, patchLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), site, patchLength);
return true;
}
void __stdcall BeginTier1Capture()
{
g_captureFirst = 0;
g_captureSecond = 0;
InterlockedExchange(&g_captureThreadId, static_cast<LONG>(GetCurrentThreadId()));
InterlockedExchange(&g_captureActive, 1);
}
void __stdcall CaptureTier1Object(void* object)
{
if (!object) return;
if (InterlockedCompareExchange(&g_captureActive, 1, 1) != 1) return;
if (static_cast<DWORD>(InterlockedCompareExchange(&g_captureThreadId, 0, 0)) !=
GetCurrentThreadId()) {
return;
}
uintptr_t address = reinterpret_cast<uintptr_t>(object);
if (!g_captureFirst) {
g_captureFirst = address;
}
else if (g_captureFirst != address && !g_captureSecond) {
g_captureSecond = address;
}
}
bool CallTier1Virtual28(uintptr_t object)
{
if (!object) return false;
bool ok = false;
__try {
uintptr_t* vtable = *reinterpret_cast<uintptr_t**>(object);
if (!vtable) return false;
uintptr_t target = vtable[0x28 / sizeof(uintptr_t)];
if (!target) return false;
Tier1UpdateFn update = reinterpret_cast<Tier1UpdateFn>(target);
update(reinterpret_cast<void*>(object));
ok = true;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
ok = false;
}
return ok;
}
void __stdcall FinishTier1CaptureAndResync()
{
if (static_cast<DWORD>(InterlockedCompareExchange(&g_captureThreadId, 0, 0)) !=
GetCurrentThreadId()) {
return;
}
if (InterlockedExchange(&g_captureActive, 0) != 1) return;
uintptr_t first = g_captureFirst;
uintptr_t second = g_captureSecond;
g_captureFirst = 0;
g_captureSecond = 0;
bool modeFault = false;
unsigned int mode = ReadManagerMode(&modeFault);
bool d1FaultFirst = false;
bool d1FaultSecond = false;
unsigned int firstD1 = ReadD1(first, &d1FaultFirst);
unsigned int secondD1 = ReadD1(second, &d1FaultSecond);
// Scope the behavior change to the exact stale-HUD state observed live:
// manager mode is 0 after the writer has cleared at least one captured
// tier1 object to +3D1 == 0. If mode is already 1, do nothing.
if (!modeFault && mode == 0) {
if (first && !d1FaultFirst && firstD1 == 0) {
(void)CallTier1Virtual28(first);
}
if (second && second != first && !d1FaultSecond && secondD1 == 0) {
(void)CallTier1Virtual28(second);
}
}
}
__declspec(naked) void ProxyWriterBSlot1Write()
{
__asm {
mov byte ptr [esi + 3D1h], bl
pushfd
pushad
push esi
call CaptureTier1Object
popad
popfd
jmp dword ptr [g_slot1Continue]
}
}
__declspec(naked) void ProxyWriterBSlot0Write()
{
__asm {
mov byte ptr [esi + 3D1h], bl
pushfd
pushad
push esi
call CaptureTier1Object
popad
popfd
jmp dword ptr [g_slot0Continue]
}
}
__declspec(naked) void ProxySelfRemovalWriterZero()
{
__asm {
// Reproduce 01B947D6..01B947DA exactly around the native virtual call:
// push 0; call edx; [post-call resync]; push 1; continue at 01B947DC.
pushfd
pushad
call BeginTier1Capture
popad
popfd
push 0
call edx
pushfd
pushad
call FinishTier1CaptureAndResync
popad
popfd
push 1
jmp dword ptr [g_callsiteContinue]
}
}
DWORD WINAPI Install3149WriterResyncFix(LPVOID)
{
g_exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!g_exeBase) return 0;
g_callsiteContinue = g_exeBase + kSelfRemovalWriterContinueRva;
g_slot1Continue = g_exeBase + kWriterBSlot1ContinueRva;
g_slot0Continue = g_exeBase + kWriterBSlot0ContinueRva;
static const unsigned char kWriterWriteExpected[] = {
0x88, 0x9E, 0xD1, 0x03, 0x00, 0x00
};
static const unsigned char kSelfRemovalExpected[] = {
0x6A, 0x00, 0xFF, 0xD2, 0x6A, 0x01
};
bool slot1Hook = PatchJumpExact(
g_exeBase + kWriterBSlot1WriteRva,
kWriterWriteExpected, sizeof(kWriterWriteExpected),
reinterpret_cast<void*>(&ProxyWriterBSlot1Write), g_slot1Continue);
bool slot0Hook = PatchJumpExact(
g_exeBase + kWriterBSlot0WriteRva,
kWriterWriteExpected, sizeof(kWriterWriteExpected),
reinterpret_cast<void*>(&ProxyWriterBSlot0Write), g_slot0Continue);
// Only enable the behavior-changing callsite hook if both read/capture
// hooks are present. Partial capture must never drive a resync.
if (slot1Hook && slot0Hook) {
(void)PatchJumpExact(
g_exeBase + kSelfRemovalWriterCallsiteRva,
kSelfRemovalExpected, sizeof(kSelfRemovalExpected),
reinterpret_cast<void*>(&ProxySelfRemovalWriterZero), g_callsiteContinue);
}
return 0;
}
}
void Start3149WriterResyncFix(HMODULE module)
{
(void)module;
HANDLE worker = CreateThread(nullptr, 0, Install3149WriterResyncFix, nullptr, 0, nullptr);
if (worker) CloseHandle(worker);
}
@@ -0,0 +1,5 @@
#pragma once
#include <windows.h>
void Start3149WriterResyncFix(HMODULE module);
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <windows.h>
namespace dnf90_combat_panel
{
// Current EXE sub_10DC420 transforms a point around the live UI pivot.
using NativeTransformPoint = void (__thiscall*)(void*, float*, float*, float, float);
struct Position
{
int x;
int y;
};
inline bool PositionSidecar(
int nativeX, int nativeY, int nativeWidth, int nativeHeight,
int viewportWidth, int viewportHeight, int clientWidth, int clientHeight,
int sidecarWidth, int sidecarHeight, int frameRight, int frameTop,
void* transformOwner, NativeTransformPoint transform, Position* result)
{
if (!result || !transformOwner || !transform ||
viewportWidth < 320 || viewportWidth > 16384 ||
viewportHeight < 200 || viewportHeight > 16384 ||
clientWidth < 8 || clientWidth > 16384 ||
clientHeight < 8 || clientHeight > 16384 ||
nativeX < -64 || nativeY < -64 ||
nativeX > viewportWidth || nativeY > viewportHeight ||
nativeWidth < 160 || nativeWidth > 640 ||
nativeHeight < 200 || nativeHeight > 760 ||
sidecarWidth <= 0 || sidecarHeight <= 0) {
return false;
}
float right = static_cast<float>(nativeX + nativeWidth + frameRight);
float top = static_cast<float>(nativeY - frameTop);
float left = static_cast<float>(nativeX);
float unusedY = static_cast<float>(nativeY);
transform(transformOwner, &right, &top, 0.0f, 0.0f);
transform(transformOwner, &left, &unusedY, 0.0f, 0.0f);
// Comparisons also reject NaN, before any float-to-int conversion.
if (!(right >= -65536.0f && right <= 65536.0f &&
top >= -65536.0f && top <= 65536.0f &&
left >= -65536.0f && left <= 65536.0f)) {
return false;
}
int x = static_cast<int>(right * clientWidth / viewportWidth);
int y = static_cast<int>(top * clientHeight / viewportHeight);
if (x + sidecarWidth > clientWidth - 4) {
x = static_cast<int>(left * clientWidth / viewportWidth) - sidecarWidth;
}
if (x + sidecarWidth > clientWidth - 4) x = clientWidth - sidecarWidth - 4;
if (x < 4) x = 4;
if (y + sidecarHeight > clientHeight - 4) y = clientHeight - sidecarHeight - 4;
if (y < 4) y = 4;
result->x = x;
result->y = y;
return true;
}
}
@@ -0,0 +1,308 @@
#pragma once
// Client-side execution coordinator. Not installed into 90CN.dll.
// NativeDriver is deliberately an interface: no current native implementation
// has passed creation/pause/retirement verification. Offline tests inject a fake.
#include "90CN_companion_native_contract.h"
#include <thread>
namespace dnf90_companion {
struct ActorHandle {
std::uint32_t address32 = 0, object_key = 0, generation = 0;
};
inline bool SameActor(const ActorHandle& a, const ActorHandle& b) {
return a.address32 == b.address32 && a.object_key == b.object_key &&
a.generation == b.generation;
}
inline bool ValidActor(const ActorHandle& h) {
return h.address32 && h.object_key && h.object_key < 65535 && h.generation;
}
struct ClientPlan {
SpawnIdentity identity{};
std::uint16_t source_character = 0;
RequestValues values{};
};
enum class ActorPresence { Unavailable, Present, Gone, IdentityMismatch };
struct ActorObservation {
ActorPresence presence = ActorPresence::Unavailable;
bool registered = false, paused = false, alive = false;
std::uint64_t room_generation = 0;
};
enum class Submission { NotSubmitted, Pending };
// Every method executes on the verified game thread and MUST NOT wait for IO.
// Implementations resolve handles afresh before touching native memory, and
// validate provenance against the authenticated server/session/run and profile.
// BeginPaused must guarantee no AI attack before the creation observer; a later
// best-effort pause is insufficient. Pending also covers uncertain submission.
// Gone means proven engine retirement, not lookup error or missing room manager.
class NativeDriver {
public:
virtual ~NativeDriver() = default;
virtual bool VerifiedFor(const ClientPlan&) const noexcept = 0;
virtual bool RoomReady(const SpawnIdentity&, std::uint64_t) const noexcept = 0;
virtual Submission BeginPaused(const ClientPlan&, std::uint64_t) noexcept = 0;
virtual bool BelongsToRequest(const ClientPlan&, const ActorHandle&) const noexcept = 0;
virtual ActorObservation Inspect(const SpawnIdentity&, const ActorHandle&) const noexcept = 0;
virtual bool SetPaused(const SpawnIdentity&, const ActorHandle&, bool) noexcept = 0;
virtual bool RequestRetirement(const SpawnIdentity&, const ActorHandle&) noexcept = 0;
// true means the pending operation AND its callback queue are drained,
// with no undisclosed live object. false does not authorize another spawn.
virtual bool CancelAndDrain(const SpawnIdentity&) noexcept = 0;
virtual bool CreationDrained(const SpawnIdentity&) const noexcept = 0;
};
enum class ClientPhase {
Idle, Prepared, Creating, AwaitOwner, Bound, Active, Transitioning, Dead,
Retiring, CleanupBlocked, Closed
};
enum class ClientResult {
Ok, WrongThread, InvalidPlan, StaleRun, Busy, Unsupported, RoomNotReady,
WrongPhase, WrongActor, ObservationUnavailable, AINotPaused, NativeFailure,
Timeout, CleanupPending, ClockRegression
};
struct ClientStatus {
ClientPhase phase = ClientPhase::Idle;
ClientResult reason = ClientResult::Ok;
SpawnIdentity identity{};
ActorHandle actor{};
std::uint64_t room_generation = 0;
bool owner_bound = false, creation_pending = false;
};
struct ClientTimeouts {
std::uint64_t preparation_ms = 10000, creation_ms = 5000,
binding_ms = 5000, transition_ms = 10000, retirement_ms = 5000;
};
// All mutating methods, including observations, are game-thread only. The
// future channel must queue immutable messages there, never call this object
// from its reader thread. The coordinator creates no thread, hook or timer.
class ClientRuntime {
public:
ClientRuntime(NativeDriver& driver, std::uint64_t session_generation,
std::uint16_t owner_character, std::uint16_t player_object_key,
ClientTimeouts timeouts = {})
: driver_(driver), thread_(std::this_thread::get_id()),
session_(session_generation), owner_(owner_character),
player_key_(player_object_key), timeouts_(timeouts) {}
ClientRuntime(const ClientRuntime&) = delete;
ClientRuntime& operator=(const ClientRuntime&) = delete;
bool TryStatus(ClientStatus& out) const {
if (std::this_thread::get_id() != thread_) return false;
out = state_; return true;
}
ClientResult Prepare(const ClientPlan& plan, std::uint64_t now) {
auto guard = TimeGuard(now); if (guard != ClientResult::Ok) return guard;
if (state_.phase != ClientPhase::Idle && state_.phase != ClientPhase::Closed)
return ClientResult::Busy;
const auto& id = plan.identity;
NativeApcRequest32 request{};
if (!session_ || !owner_ || owner_ == 65535 || !player_key_ || player_key_ == 65535 ||
id.session_generation != session_ || id.owner_character != owner_ ||
!id.run_token || !id.request_sequence || !plan.source_character ||
plan.source_character == owner_ || plan.source_character == 65535 ||
BuildRequest(plan.values, &request) != RequestError::None)
return ClientResult::InvalidPlan;
if (id.request_sequence <= last_request_) return ClientResult::StaleRun;
if (!driver_.VerifiedFor(plan)) return ClientResult::Unsupported;
plan_ = plan; state_ = {}; state_.identity = id;
state_.phase = ClientPhase::Prepared; since_ = now;
last_request_ = id.request_sequence;
retire_sent_ = cancel_sent_ = false;
return ClientResult::Ok;
}
ClientResult RoomReady(const SpawnIdentity& id, std::uint64_t room, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (!room) return ClientResult::RoomNotReady;
if (room == state_.room_generation &&
(state_.phase == ClientPhase::Creating || state_.phase == ClientPhase::AwaitOwner ||
state_.phase == ClientPhase::Bound || state_.phase == ClientPhase::Active))
return ClientResult::Ok; // repeated load callback never resubmits
if (room <= state_.room_generation || !driver_.RoomReady(id, room))
return ClientResult::RoomNotReady;
if (state_.phase == ClientPhase::Transitioning) {
const auto o = driver_.Inspect(id, state_.actor);
if (o.presence != ActorPresence::Present) return ClientResult::ObservationUnavailable;
if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; }
if (!o.registered || !o.paused || o.room_generation != room || !state_.owner_bound)
return ClientResult::RoomNotReady;
state_.room_generation = room;
return Resume(now);
}
if (state_.phase != ClientPhase::Prepared) return ClientResult::WrongPhase;
if (!driver_.VerifiedFor(plan_)) return Close(ClientResult::Unsupported);
state_.room_generation = room; state_.phase = ClientPhase::Creating;
state_.creation_pending = true; since_ = now;
// State is installed before submitting. The driver must enqueue its
// observer callbacks until this call has returned (no re-entrancy).
if (driver_.BeginPaused(plan_, room) == Submission::NotSubmitted) {
state_.creation_pending = false;
return Close(ClientResult::NativeFailure);
}
return ClientResult::Ok;
}
ClientResult NativeCreated(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (!ValidActor(h) || h.object_key == player_key_ || !driver_.BelongsToRequest(plan_, h))
return ClientResult::WrongActor;
if (ValidActor(state_.actor)) {
if (!SameActor(state_.actor, h)) return Block(ClientResult::WrongActor);
// A duplicate cannot reset a terminal/dead/active lifecycle.
return ClientResult::Ok;
}
if (!state_.creation_pending ||
(state_.phase != ClientPhase::Creating && state_.phase != ClientPhase::Retiring &&
state_.phase != ClientPhase::CleanupBlocked)) return ClientResult::WrongPhase;
const auto o = driver_.Inspect(id, h);
if (o.presence != ActorPresence::Present || !o.registered)
return ClientResult::ObservationUnavailable;
state_.actor = h; state_.creation_pending = false;
if (state_.phase == ClientPhase::Retiring || state_.phase == ClientPhase::CleanupBlocked) {
state_.phase = ClientPhase::Retiring; since_ = now;
return SendRetirement(); // late result after cancel: never bind or activate
}
if (!o.paused || !o.alive || o.room_generation != state_.room_generation) {
return StartRetirement(now, ClientResult::AINotPaused);
}
state_.phase = ClientPhase::AwaitOwner; since_ = now;
return ClientResult::Ok;
}
ClientResult OwnerBound(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (!SameActor(h, state_.actor) || !ValidActor(h)) return ClientResult::WrongActor;
if ((state_.phase == ClientPhase::Bound || state_.phase == ClientPhase::Active) &&
state_.owner_bound) return ClientResult::Ok;
if (state_.phase != ClientPhase::AwaitOwner) return ClientResult::WrongPhase;
const auto o = driver_.Inspect(id, h);
if (o.presence != ActorPresence::Present || !o.registered)
return ClientResult::ObservationUnavailable;
if (!o.paused || !o.alive || o.room_generation != state_.room_generation)
return StartRetirement(now, ClientResult::AINotPaused);
state_.owner_bound = true; state_.phase = ClientPhase::Bound;
return ClientResult::Ok;
}
ClientResult Activate(const SpawnIdentity& id, ActorHandle h, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (!SameActor(h, state_.actor) || !ValidActor(h)) return ClientResult::WrongActor;
if (state_.phase == ClientPhase::Active) return ClientResult::Ok;
if (state_.phase != ClientPhase::Bound || !state_.owner_bound) return ClientResult::WrongPhase;
return Resume(now);
}
ClientResult RoomLeaving(const SpawnIdentity& id, std::uint64_t room, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (room != state_.room_generation) return ClientResult::RoomNotReady;
if (state_.phase == ClientPhase::Transitioning || state_.phase == ClientPhase::Dead)
return ClientResult::Ok;
if (state_.phase != ClientPhase::Active) return ClientResult::WrongPhase;
auto o = driver_.Inspect(id, state_.actor);
if (o.presence != ActorPresence::Present) return Block(ClientResult::ObservationUnavailable);
if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; }
if (!driver_.SetPaused(id, state_.actor, true)) return StartRetirement(now, ClientResult::NativeFailure);
o = driver_.Inspect(id, state_.actor);
if (o.presence != ActorPresence::Present || !o.paused)
return StartRetirement(now, ClientResult::AINotPaused);
state_.phase = ClientPhase::Transitioning; since_ = now;
return ClientResult::Ok;
}
ClientResult Retire(const SpawnIdentity& id, std::uint64_t now) {
auto guard = Guard(id, now); if (guard != ClientResult::Ok) return guard;
if (state_.phase == ClientPhase::Closed) return ClientResult::Ok;
if (state_.phase == ClientPhase::Retiring || state_.phase == ClientPhase::CleanupBlocked)
return ClientResult::CleanupPending;
return StartRetirement(now, ClientResult::Ok);
}
ClientResult Poll(std::uint64_t now) {
auto guard = TimeGuard(now); if (guard != ClientResult::Ok) return guard;
const auto phase = state_.phase;
if (phase == ClientPhase::Idle || phase == ClientPhase::Closed) return ClientResult::Ok;
if (phase == ClientPhase::Retiring || phase == ClientPhase::CleanupBlocked) {
if (state_.creation_pending && driver_.CreationDrained(state_.identity))
state_.creation_pending = false;
if (!ValidActor(state_.actor)) {
if (!state_.creation_pending) return Close(state_.reason);
} else {
const auto o = driver_.Inspect(state_.identity, state_.actor);
if (o.presence == ActorPresence::Gone && !state_.creation_pending)
return Close(state_.reason);
if (o.presence == ActorPresence::IdentityMismatch) return Block(ClientResult::WrongActor);
}
if (Elapsed(now, timeouts_.retirement_ms)) return Block(ClientResult::Timeout);
return ClientResult::CleanupPending;
}
if (phase == ClientPhase::Active || phase == ClientPhase::Transitioning || phase == ClientPhase::Dead) {
const auto o = driver_.Inspect(state_.identity, state_.actor);
if (o.presence == ActorPresence::IdentityMismatch) return Block(ClientResult::WrongActor);
if (o.presence == ActorPresence::Gone) return Close(ClientResult::NativeFailure);
if (o.presence == ActorPresence::Unavailable) return ClientResult::ObservationUnavailable;
if (!o.alive) { state_.phase = ClientPhase::Dead; return ClientResult::Ok; }
}
std::uint64_t timeout = 0;
if (phase == ClientPhase::Prepared) timeout = timeouts_.preparation_ms;
if (phase == ClientPhase::Creating) timeout = timeouts_.creation_ms;
if (phase == ClientPhase::AwaitOwner || phase == ClientPhase::Bound) timeout = timeouts_.binding_ms;
if (phase == ClientPhase::Transitioning) timeout = timeouts_.transition_ms;
if (timeout && Elapsed(now, timeout)) return StartRetirement(now, ClientResult::Timeout);
return ClientResult::Ok;
}
private:
ClientResult TimeGuard(std::uint64_t now) {
if (std::this_thread::get_id() != thread_) return ClientResult::WrongThread;
if (now < last_now_) return ClientResult::ClockRegression;
last_now_ = now; return ClientResult::Ok;
}
ClientResult Guard(const SpawnIdentity& id, std::uint64_t now) {
auto result = TimeGuard(now); if (result != ClientResult::Ok) return result;
return SameSpawn(id, state_.identity) ? ClientResult::Ok : ClientResult::StaleRun;
}
bool Elapsed(std::uint64_t now, std::uint64_t limit) const { return now - since_ >= limit; }
ClientResult Block(ClientResult reason) {
state_.phase = ClientPhase::CleanupBlocked; state_.reason = reason; return reason;
}
ClientResult Close(ClientResult reason) {
state_.phase = ClientPhase::Closed; state_.reason = reason;
state_.owner_bound = false; state_.creation_pending = false;
return reason;
}
ClientResult SendRetirement() {
if (retire_sent_) return ClientResult::CleanupPending;
const auto o = driver_.Inspect(state_.identity, state_.actor);
if (o.presence == ActorPresence::Gone) return Close(state_.reason);
if (o.presence != ActorPresence::Present) return Block(ClientResult::ObservationUnavailable);
retire_sent_ = true; // no uncertain native operation is blindly retried
if (!driver_.RequestRetirement(state_.identity, state_.actor)) return Block(ClientResult::NativeFailure);
return ClientResult::CleanupPending;
}
ClientResult StartRetirement(std::uint64_t now, ClientResult reason) {
state_.phase = ClientPhase::Retiring; state_.reason = reason; since_ = now;
if (ValidActor(state_.actor)) return SendRetirement();
if (state_.creation_pending && !cancel_sent_) {
cancel_sent_ = true;
if (driver_.CancelAndDrain(state_.identity)) state_.creation_pending = false;
}
if (!state_.creation_pending) return Close(reason);
return ClientResult::CleanupPending;
}
ClientResult Resume(std::uint64_t now) {
const auto o = driver_.Inspect(state_.identity, state_.actor);
if (o.presence != ActorPresence::Present || !o.registered || !o.paused || !o.alive ||
o.room_generation != state_.room_generation || !state_.owner_bound)
return StartRetirement(now, ClientResult::AINotPaused);
if (!driver_.SetPaused(state_.identity, state_.actor, false))
return StartRetirement(now, ClientResult::NativeFailure);
const auto after = driver_.Inspect(state_.identity, state_.actor);
if (after.presence != ActorPresence::Present || !after.registered || after.paused ||
!after.alive || after.room_generation != state_.room_generation)
return StartRetirement(now, ClientResult::NativeFailure);
state_.phase = ClientPhase::Active; since_ = now;
return ClientResult::Ok;
}
NativeDriver& driver_;
const std::thread::id thread_;
const std::uint64_t session_;
const std::uint16_t owner_, player_key_;
const ClientTimeouts timeouts_;
ClientPlan plan_{};
ClientStatus state_{};
std::uint64_t since_ = 0, last_now_ = 0, last_request_ = 0;
bool retire_sent_ = false, cancel_sent_ = false;
};
} // namespace dnf90_companion
@@ -0,0 +1,169 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace dnf90_companion_p0 {
// EXE-specific native ABI. Projection policy, queue, AIC ownership and AI
// remain shared. These bindings are selected once, before installing hooks.
struct CompanionNativeBindings {
size_t kUserInfoKeyOffset = 0xC;
size_t kUserInfoLevelOffset = 0x2C;
uintptr_t kAIControlledVmVtableRva = 0x0;
bool newNative = false;
uintptr_t kExpectedReceiveRva = 0x02BBB690;
uintptr_t kPayloadEncodeReturnRva = 0x02BB76E0;
uintptr_t kWriterPointerRva = 0x04DB275C;
uintptr_t kDispatcherPointerRva = 0x04EC12B8;
size_t kPacketBufferOffset = 0x20;
size_t kWriterCursorOffset = 0x100020;
uintptr_t kNativeSummonApcRva = 0x01F134A0;
uintptr_t kNativeReadyRva = 0x01FB0BF0;
uintptr_t kSceneContextGetterRva = 0x018F2A70;
uintptr_t kSceneModeRva = 0x01E64FD0;
uintptr_t kNativeRefStringAllocRva = 0x03714DA7;
uintptr_t kNativeRefStringFreeRva = 0x037146A3;
uintptr_t kNativeRefStringIncIatRva = 0x03DA2348;
uintptr_t kNativeRefStringDecIatRva = 0x03DA2344;
uintptr_t kNativeVectorFreeRva = 0x0370DEB4;
uintptr_t kNativeEquipmentVectorAllocateRva = 0x01CF1340;
uintptr_t kNativeSkillVectorAllocateRva = 0x020B2420;
size_t kActorDisplayNameOffset = 0x400;
size_t kAicEquipmentVectorObjectOffset = 0x49C;
size_t kAicQuickSkillVectorObjectOffset = 0x4C4;
size_t kAicSkillVectorObjectOffset = 0x4D8;
size_t kAicEquipmentVectorOffset = 0x4A0;
size_t kAicQuickSkillVectorOffset = 0x4C8;
size_t kAicSkillVectorOffset = 0x4DC;
size_t kActorAicOffset = 0x6C28;
size_t kAicRuntimeKeyProgramVectorOffset = 0xFE0;
size_t kRuntimeKeyProgramBytecodeBeginOffset = 0x08;
size_t kRuntimeKeyProgramBytecodeEndOffset = 0x0C;
size_t kRuntimeKeyProgramBytecodeCapacityOffset = 0x10;
size_t kRuntimeKeySkillCodeOffset = 0x100;
size_t kRuntimeDirectKeyBytecodeSize = 0x180;
uintptr_t kExpectedSerializerRva = 0x02136B60;
uintptr_t kControlledActorPointerRva = 0x04EB5BB0;
size_t kSerializerVtableOffset = 0x4F0;
size_t kActorOwnerContextOffset = 0xBC;
size_t kOwnerRuntimeOffset = 0x110;
uintptr_t kProjectionReaderStateRva = 0x04EF8500;
uintptr_t kProjectionReaderErrorCopyRva = 0x04EF8528;
uintptr_t kProjectionReaderSetCursorRva = 0x030E2280;
uintptr_t kProjectionReaderSetLengthRva = 0x030E2290;
uintptr_t kProjectionReaderSetPacketRva = 0x030E3430;
uintptr_t kControlledActorGetterRva = 0;
uintptr_t kAIParameterRva = 0x0222A2A0;
uintptr_t kAICooldownRva = 0x0222B040;
uintptr_t kAIKeyReadyRva = 0x0222B330;
uintptr_t kActorClockRva = 0x0222EB60;
uintptr_t kClockReadRva = 0x03108170;
size_t kActorVmOffset = 0x6F54;
size_t kActorKeyVtableOffset = 0x50;
size_t kActorAbilityVtableOffset = 0x648;
size_t kActorUserInfoVtableOffset = 0xCF4;
size_t kAicUserInfoOffset = 0x100C;
size_t kActorSpawnLevelOffset = 0x6F1C;
size_t kActorExpVtableOffset = 0xA9C;
size_t kActorLearnVtableOffset = 0xB60;
size_t kActorStatsVtableOffset = 0xAC4;
size_t kActorRecalculateVtableOffset = 0xACC;
};
inline CompanionNativeBindings NewNativeCompanionBindings() {
CompanionNativeBindings b;
b.newNative = true;
b.kExpectedReceiveRva = 0x2E9BDC0;
b.kPayloadEncodeReturnRva = 0x2E97D90;
b.kNativeSummonApcRva = 0x21D7580;
b.kNativeReadyRva = 0x2276330;
b.kSceneContextGetterRva = 0x1BA2790;
b.kSceneModeRva = 0x2126B50;
b.kNativeRefStringAllocRva = 0x3A77C68;
b.kNativeRefStringFreeRva = 0x3A777CB;
b.kNativeRefStringIncIatRva = 0x6A85BA8;
b.kNativeRefStringDecIatRva = 0x6A85A3C;
b.kNativeVectorFreeRva = 0x3A70888;
b.kNativeEquipmentVectorAllocateRva = 0x1094240;
b.kNativeSkillVectorAllocateRva = 0x10E8E10;
b.kActorDisplayNameOffset = 0x404;
b.kAicEquipmentVectorObjectOffset = 0x4B4;
b.kAicQuickSkillVectorObjectOffset = 0x4DC;
b.kAicSkillVectorObjectOffset = 0x4F0;
b.kAicEquipmentVectorOffset = 0x4B8;
b.kAicQuickSkillVectorOffset = 0x4E0;
b.kAicSkillVectorOffset = 0x4F4;
b.kActorAicOffset = 0x6CA8;
b.kAicRuntimeKeyProgramVectorOffset = 0xFF8;
b.kExpectedSerializerRva = 0x2402950;
b.kControlledActorGetterRva = 0x25610E0;
b.kSerializerVtableOffset = 0x510;
b.kActorOwnerContextOffset = 0xC8;
b.kProjectionReaderStateRva = 0x53F9AD0;
b.kProjectionReaderErrorCopyRva = 0x53F9AF8;
b.kProjectionReaderSetCursorRva = 0x33E6970;
b.kProjectionReaderSetLengthRva = 0x33E6980;
b.kProjectionReaderSetPacketRva = 0x33E7DA0;
b.kAIParameterRva = 0x24EDD90;
b.kAICooldownRva = 0x24FA120;
b.kAIKeyReadyRva = 0x24FA520;
b.kActorClockRva = 0x351BA30;
b.kClockReadRva = 0x3416310;
b.kActorVmOffset = 0x6FE0;
b.kActorKeyVtableOffset = 0x58;
b.kAicUserInfoOffset = 0x1024;
b.kWriterPointerRva = 0x52A3790;
b.kDispatcherPointerRva = 0x53C2DDC;
b.kActorAbilityVtableOffset = 0x670;
b.kActorUserInfoVtableOffset = 0xD20;
b.kActorSpawnLevelOffset = 0x6FA8;
b.kActorExpVtableOffset = 0xAC8;
b.kActorLearnVtableOffset = 0xB8C;
b.kActorStatsVtableOffset = 0xAF0;
b.kActorRecalculateVtableOffset = 0xAF8;
b.kUserInfoKeyOffset = 0x20;
b.kUserInfoLevelOffset = 0x44;
b.kRuntimeKeySkillCodeOffset = 0x128;
b.kRuntimeDirectKeyBytecodeSize = 0x1BC;
b.kAIControlledVmVtableRva = 0x42CFA14;
return b;
}
// Locked to DB4ABD49...D2. The controller validates the complete EXE hash;
// the DLL also checks PE identity and every entry below before installation.
struct CompanionNativeAnchor { uintptr_t rva; unsigned char bytes[16]; };
constexpr CompanionNativeAnchor kNewNativeCompanionAnchors[] = {
{0x02E9BDC0, {0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x12, 0x85, 0x25, 0x04, 0x64, 0xA1, 0x00, 0x00, 0x00, 0x00}}, // kExpectedReceiveRva
{0x021D7580, {0xE9, 0x21, 0x52, 0x06, 0x00, 0xE4, 0xCC, 0x9A, 0xA9, 0x09, 0x2E, 0x91, 0x44, 0xCC, 0x03, 0xB9}}, // kNativeSummonApcRva
{0x02276330, {0x56, 0x8B, 0xF1, 0xE8, 0xA8, 0x8E, 0x3A, 0x00, 0x85, 0xC0, 0x74, 0x14, 0xE8, 0x9F, 0x8E, 0x3A}}, // kNativeReadyRva
{0x01BA2790, {0xE9, 0x7B, 0xC0, 0x57, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC}}, // kSceneContextGetterRva
{0x02126B50, {0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08, 0x56, 0x8B, 0xF1, 0x8B, 0x0D, 0x88, 0x30, 0x6A, 0x05, 0x85}}, // kSceneModeRva
{0x03A77C68, {0x8B, 0xFF, 0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D, 0x08, 0x83, 0xFB, 0xE0, 0x77, 0x6F, 0x56, 0x57}}, // kNativeRefStringAllocRva
{0x03A777CB, {0x8B, 0xFF, 0x55, 0x8B, 0xEC, 0x83, 0x7D, 0x08, 0x00, 0x74, 0x2D, 0xFF, 0x75, 0x08, 0x6A, 0x00}}, // kNativeRefStringFreeRva
{0x03A70888, {0xFF, 0x25, 0xC0, 0x88, 0x58, 0x04, 0xFF, 0x25, 0xBC, 0x88, 0x58, 0x04, 0x8B, 0xFF, 0x56, 0x8B}}, // kNativeVectorFreeRva
{0x01094240, {0x55, 0x8B, 0xEC, 0x56, 0x33, 0xC0, 0x57, 0x8B, 0x7D, 0x08, 0x8B, 0xF1, 0x89, 0x46, 0x04, 0x89}}, // kNativeEquipmentVectorAllocateRva
{0x010E8E10, {0x55, 0x8B, 0xEC, 0x56, 0x33, 0xC0, 0x57, 0x8B, 0x7D, 0x08, 0x8B, 0xF1, 0x89, 0x46, 0x04, 0x89}}, // kNativeSkillVectorAllocateRva
{0x02402950, {0x55, 0x8B, 0xEC, 0x57, 0x8B, 0xF9, 0x8B, 0x87, 0xC8, 0x00, 0x00, 0x00, 0x85, 0xC0, 0x0F, 0x84}}, // kExpectedSerializerRva
{0x025610E0, {0xA1, 0xB4, 0x6F, 0x7B, 0x05, 0x85, 0xC0, 0x75, 0x05, 0xA1, 0xB0, 0x6F, 0x7B, 0x05, 0xC3, 0xCC}}, // kControlledActorGetterRva
{0x033E6970, {0xE9, 0x72, 0x59, 0x66, 0x03, 0x8D, 0x64, 0x24, 0x04, 0x57, 0xE9, 0xD8, 0x98, 0x62, 0x03, 0x75}}, // kProjectionReaderSetCursorRva
{0x033E6980, {0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0xA3, 0xD4, 0x9A, 0x7F, 0x05, 0x5D, 0xC3, 0xCC, 0xCC, 0xCC}}, // kProjectionReaderSetLengthRva
{0x033E7DA0, {0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0x8B, 0x4D, 0x0C, 0xA3, 0xD8, 0x9A, 0x7F, 0x05, 0xA1, 0xF8}}, // kProjectionReaderSetPacketRva
{0x024EDD90, {0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x58, 0x9D, 0x0D, 0x04, 0x64, 0xA1, 0x00, 0x00, 0x00, 0x00}}, // kAIParameterRva
{0x024FA120, {0x56, 0x8B, 0xF1, 0x80, 0x7E, 0x6D, 0x00, 0x75, 0x27, 0x83, 0x7E, 0x64, 0x00, 0x74, 0x21, 0x8B}}, // kAICooldownRva
{0x024FA520, {0x55, 0x8B, 0xEC, 0x53, 0x56, 0x8B, 0xF1, 0x83, 0x7E, 0x4C, 0x00, 0x57, 0x0F, 0x84, 0xEB, 0x00}}, // kAIKeyReadyRva
{0x0351BA30, {0x8D, 0x41, 0x68, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC}}, // kActorClockRva
{0x03416310, {0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x10, 0x56, 0x8B, 0xF1, 0x8B, 0x46, 0x0C, 0x8D, 0x4D, 0xF8, 0x51}}, // kClockReadRva
{0x0211E340, {0x55, 0x8B, 0xEC, 0x53, 0x8B, 0x5D, 0x0C, 0x57, 0xC6, 0x05, 0x48, 0x27, 0x68, 0x05, 0x00, 0x0F}}, // kClass0Dispatch
{0x02123C8A, {0x8A, 0x45, 0xA3, 0xC7, 0x45, 0xAC, 0x01, 0x00, 0x00, 0x00, 0x3C, 0x01, 0x0F, 0x85, 0xAB, 0x02}}, // kDecodedProjectionCapture
{0x02364160, {0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x68, 0xB3, 0xF7, 0x03, 0x64, 0xA1, 0x00, 0x00, 0x00, 0x00}}, // kAicCopyHook
{0x0248A260, {0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0x0D, 0x30, 0x1B, 0x04, 0x64, 0xA1, 0x00, 0x00, 0x00, 0x00}}, // kRegistrationHook
{0x024F98B0, {0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x14, 0x53, 0x56, 0x8B, 0xF1, 0x8B, 0x46, 0x1C, 0x57, 0x83, 0xF8}}, // kAIConditionHook
{0x034FB0E0, {0xE9, 0x00, 0x11, 0x52, 0x03, 0x8D, 0x64, 0x24, 0x04, 0x53, 0x50, 0x56, 0x9C, 0x55, 0x57, 0xE9}}, // kPeerDeliveryHook
{0x02E83610, {0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x08, 0x53, 0x56, 0x8B, 0x75, 0x10, 0x57, 0xBB, 0xB5, 0x3E, 0x45}}, // kPayloadEncoderHook
{0x0256D730, {0x55, 0x8B, 0xEC, 0xA1, 0x58, 0x37, 0x6A, 0x05, 0x83, 0xEC, 0x1C, 0x53, 0x66, 0x8B, 0x5D, 0x08}}, // kActorLookupHook
{0x033DF130, {0x55, 0x8B, 0xEC, 0xA1, 0x78, 0x8A, 0x7F, 0x05, 0x40, 0x8B, 0xD0, 0xC1, 0xEA, 0x08, 0x0F, 0xB6}}, // levelWriter0x33df130
{0x033DF1B0, {0x55, 0x8B, 0xEC, 0xA1, 0x78, 0x8A, 0x7F, 0x05, 0x40, 0x8B, 0xD0, 0xC1, 0xEA, 0x08, 0x0F, 0xB6}}, // levelWriter0x33df1b0
{0x033DF230, {0x55, 0x8B, 0xEC, 0xA1, 0x78, 0x8A, 0x7F, 0x05, 0x40, 0x8B, 0xD0, 0xC1, 0xEA, 0x08, 0x0F, 0xB6}}, // levelWriter0x33df230
{0x033DF2B0, {0x55, 0x8B, 0xEC, 0xA1, 0x78, 0x8A, 0x7F, 0x05, 0x40, 0x8B, 0xD0, 0xC1, 0xEA, 0x08, 0x0F, 0xB6}}, // levelWriter0x33df2b0
{0x033DF010, {0x55, 0x8B, 0xEC, 0xA1, 0x78, 0x8A, 0x7F, 0x05, 0x40, 0x8B, 0xD0, 0xC1, 0xEA, 0x08, 0x0F, 0xB6}}, // levelWriter0x33df010
};
} // namespace dnf90_companion_p0
@@ -0,0 +1,189 @@
#pragma once
// Offline-tested 32-bit layouts from current 90CN EXE 8a7af614... .
// NOT a network packet builder, callable native adapter, or deployment switch.
// See docs/evidence/companion-v1-native-create-20260923.md.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <type_traits>
namespace dnf90_companion {
constexpr std::size_t kNativeRequestSize = 0x54;
constexpr std::size_t kNativeTailSize = 0x29;
constexpr std::uint32_t kAllocateNativeObjectKey = 0xffff;
constexpr std::uint32_t kProtocolVersion = 1;
// Keep target addresses 32-bit even when tests run in a 64-bit host process.
// Unresolved fields are named by offset, not assigned speculative semantics.
#pragma pack(push, 4)
struct NativeApcRequest32 {
std::uint32_t template_id; // 00
std::uint32_t source_value04; // 04: owner vtable+370 result
std::uint32_t level; // 08: narrowed to u8 by serializer
std::uint32_t x, y, z, direction; // 0c..18
std::uint32_t mode1c; // 1c: mode 2 rejected by sender
std::uint32_t extra_address32, extra_size; // 20..24
std::uint8_t flag28, padding29[3];
std::uint32_t object_key; // 2c: ffff asks native allocator
std::uint8_t flag30, flag31, padding32[2];
std::uint32_t tail_address32; // 34: optional 29-byte native tail
std::uint8_t flag38, padding39[3];
std::int32_t auxiliary3c;
std::uint8_t flag40, padding41[3];
std::uint32_t byte_list_address32; // 44: native container, NOT byte[]
std::uint8_t flag48, padding49[3];
std::uint32_t auxiliary4c;
std::uint8_t flag50, padding51[3];
};
#pragma pack(pop)
// Native initializer 037E6B30 has unaligned fields. Do not use a host ABI
// structure without packing, or memcpy these bytes into an upper game packet.
#pragma pack(push, 1)
struct NativeSummonTail32 {
std::uint8_t flag00, flag01, flag02;
std::uint32_t duration_value; // 03: item description's fourth int
std::uint16_t owner07, owner09;
std::uint8_t flag0b, reserved0c;
float rate0d;
std::uint8_t flag11;
std::uint16_t value12;
std::uint32_t value14, value18, value1c, value20;
std::uint8_t flag24;
std::int32_t value25;
};
#pragma pack(pop)
static_assert(sizeof(NativeApcRequest32) == kNativeRequestSize, "request ABI");
static_assert(alignof(NativeApcRequest32) == 4, "request alignment");
static_assert(offsetof(NativeApcRequest32, source_value04) == 0x04, "field04");
static_assert(offsetof(NativeApcRequest32, mode1c) == 0x1c, "field1c");
static_assert(offsetof(NativeApcRequest32, flag28) == 0x28, "field28");
static_assert(offsetof(NativeApcRequest32, object_key) == 0x2c, "object key");
static_assert(offsetof(NativeApcRequest32, flag31) == 0x31, "field31");
static_assert(offsetof(NativeApcRequest32, tail_address32) == 0x34, "tail pointer");
static_assert(offsetof(NativeApcRequest32, auxiliary3c) == 0x3c, "field3c");
static_assert(offsetof(NativeApcRequest32, byte_list_address32) == 0x44, "field44");
static_assert(offsetof(NativeApcRequest32, auxiliary4c) == 0x4c, "field4c");
static_assert(offsetof(NativeApcRequest32, flag50) == 0x50, "field50");
static_assert(sizeof(NativeSummonTail32) == kNativeTailSize, "tail ABI");
static_assert(offsetof(NativeSummonTail32, duration_value) == 3, "tail duration");
static_assert(offsetof(NativeSummonTail32, rate0d) == 0x0d, "tail rate");
static_assert(offsetof(NativeSummonTail32, value25) == 0x25, "tail ending");
static_assert(sizeof(float) == 4 && std::numeric_limits<float>::is_iec559,
"native float representation");
static_assert(std::is_trivially_copyable<NativeApcRequest32>::value, "request POD");
inline NativeApcRequest32 DefaultRequest() {
NativeApcRequest32 value;
// Native padding is not fully initialized; zero it to avoid leaked stack data.
std::memset(&value, 0, sizeof(value));
value.direction = 4;
value.object_key = kAllocateNativeObjectKey;
value.auxiliary3c = -1;
value.auxiliary4c = 5;
return value;
}
inline NativeSummonTail32 DefaultTail() {
NativeSummonTail32 value;
std::memset(&value, 0, sizeof(value));
value.owner07 = value.owner09 = 0xffff;
value.flag0b = 1;
value.rate0d = 1.0f; // actual constant at preferred VA 04B99848
value.value25 = -1;
return value;
}
struct RequestValues {
std::uint32_t template_id;
std::uint32_t source_value04;
std::uint32_t level;
std::int64_t x, y, z;
std::uint32_t direction;
};
enum class RequestError {
None, MissingOutput, TemplateRange, SourceValueRange, LevelRange,
CoordinateRange, DirectionRange
};
inline bool FitsCoordinate(std::int64_t value) { return value >= 0 && value <= 65535; }
inline RequestError BuildRequest(const RequestValues& values, NativeApcRequest32* out) {
if (!out) return RequestError::MissingOutput;
// Factory 02DE5CA0 supports positive ids <= 80000, but catalog existence
// and job/profile compatibility MUST still be proved separately.
if (values.template_id == 0 || values.template_id > 80000)
return RequestError::TemplateRange;
if (values.source_value04 > 65535) return RequestError::SourceValueRange;
if (values.level == 0 || values.level > 255) return RequestError::LevelRange;
if (!FitsCoordinate(values.x) || !FitsCoordinate(values.y) || !FitsCoordinate(values.z))
return RequestError::CoordinateRange;
// Native initializer uses 4; the item route uses the actor facing result.
// Only the scalar width is proved here, not a complete direction enum.
if (values.direction > 255) return RequestError::DirectionRange;
auto result = DefaultRequest();
result.template_id = values.template_id;
result.source_value04 = values.source_value04;
result.level = values.level;
result.x = static_cast<std::uint32_t>(values.x);
result.y = static_cast<std::uint32_t>(values.y);
result.z = static_cast<std::uint32_t>(values.z);
result.direction = values.direction;
*out = result; // errors leave output unchanged; used on the game thread
return RequestError::None;
}
// Pure receipt matcher for the future game-thread adapter. A sender returning
// does not prove creation. The actual receiver must independently observe
// registration, per-instance state and the engine's safe AI pause boundary.
struct SpawnIdentity {
std::uint64_t session_generation, run_token, request_sequence;
std::uint16_t owner_character;
};
inline bool SameSpawn(const SpawnIdentity& a, const SpawnIdentity& b) {
return a.session_generation == b.session_generation && a.run_token == b.run_token &&
a.request_sequence == b.request_sequence && a.owner_character == b.owner_character;
}
struct NativeReceipt {
SpawnIdentity identity;
std::uint32_t template_id;
std::uint32_t actor_address32;
std::uint32_t object_key;
std::uint32_t actor_generation;
bool registered;
bool inactive_confirmed;
};
enum class ReceiptError {
None, StaleSpawn, InvalidIdentity, WrongTemplate, InvalidActor,
InvalidKey, KeyCollision, NotRegistered, AINotPaused, ConflictingDuplicate
};
class PendingSpawn {
public:
PendingSpawn(SpawnIdentity identity, std::uint32_t template_id)
: identity_(identity), template_id_(template_id) {}
ReceiptError Observe(const NativeReceipt& receipt, bool key_collision) {
if (!SameSpawn(identity_, receipt.identity)) return ReceiptError::StaleSpawn;
if (!identity_.session_generation || !identity_.run_token ||
!identity_.request_sequence || !identity_.owner_character ||
!template_id_ || template_id_ > 80000) return ReceiptError::InvalidIdentity;
if (receipt.template_id != template_id_) return ReceiptError::WrongTemplate;
if (!receipt.actor_address32 || !receipt.actor_generation) return ReceiptError::InvalidActor;
if (!receipt.object_key || receipt.object_key >= 65535) return ReceiptError::InvalidKey;
if (key_collision) return ReceiptError::KeyCollision;
if (!receipt.registered) return ReceiptError::NotRegistered;
if (!receipt.inactive_confirmed) return ReceiptError::AINotPaused;
if (has_receipt_ && (receipt_.actor_address32 != receipt.actor_address32 ||
receipt_.object_key != receipt.object_key ||
receipt_.actor_generation != receipt.actor_generation))
return ReceiptError::ConflictingDuplicate;
receipt_ = receipt;
has_receipt_ = true;
return ReceiptError::None;
}
bool CanBindOwner() const { return has_receipt_; }
private:
SpawnIdentity identity_;
std::uint32_t template_id_;
bool has_receipt_ = false;
NativeReceipt receipt_{};
};
} // namespace dnf90_companion
+335
View File
@@ -0,0 +1,335 @@
#pragma once
#include "90CN_companion_p0.h"
#include "90CN_companion_projection.h"
#include <windows.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
// DB4 client only. The installer owns exact-image/signature checks. These
// adapters preserve the shared companion parser, queue and lifecycle owner.
namespace dnf90_companion_new_native {
using NewNativeClass0DispatchFn = int (__stdcall*)(int, int);
using NewNativeCaptureLogFn = void (*)(const char*, ...);
static std::uintptr_t g_captureBase = 0;
static std::uintptr_t g_captureResume = 0;
static std::uintptr_t g_captureConsumed = 0;
static NewNativeClass0DispatchFn g_originalClass0Dispatch = nullptr;
static NewNativeCaptureLogFn g_captureLog = nullptr;
static DWORD g_dispatchThread = 0;
static HWND g_safePointWindow = nullptr;
constexpr UINT_PTR kSafePointTimer = 0x90C07F41u;
constexpr std::size_t kCapturePacketCapacity =
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kBodyHeaderSize +
dnf90_companion_projection::kMaxDynamicAicBytes +
2 * dnf90_companion_projection::kMaxProjectionBody;
constexpr std::uintptr_t kReaderStateRva = 0x053F9AD0;
struct NewNativeReaderState {
const unsigned char* cursor;
int remaining;
const unsigned char* packet;
int packetLength;
};
static_assert(sizeof(NewNativeReaderState) == 16, "DB4 x86 native reader ABI");
// SetTimer's callback is dispatched by the window's own message thread. Only
// the native packet thread is allowed to own this timer or call shared APC code.
static void CALLBACK NewNativeSafePointTimer(HWND window, UINT, UINT_PTR id, DWORD) {
if (id != kSafePointTimer || window != g_safePointWindow ||
GetCurrentThreadId() != g_dispatchThread) return;
dnf90_companion_p0::NewNativeSafePoint(-2);
}
static BOOL CALLBACK FindNewNativeThreadWindow(HWND window, LPARAM parameter) {
DWORD process = 0;
const DWORD thread = GetWindowThreadProcessId(window, &process);
if (process != GetCurrentProcessId() || thread != g_dispatchThread ||
!IsWindowVisible(window) || GetWindow(window, GW_OWNER)) return TRUE;
*reinterpret_cast<HWND*>(parameter) = window;
return FALSE;
}
static void EnsureNewNativeSafePointTimer() {
if (!g_dispatchThread) g_dispatchThread = GetCurrentThreadId();
if (GetCurrentThreadId() != g_dispatchThread) return;
if (g_safePointWindow && IsWindow(g_safePointWindow)) return;
HWND window = nullptr;
EnumThreadWindows(g_dispatchThread, FindNewNativeThreadWindow,
reinterpret_cast<LPARAM>(&window));
if (!window) return;
if (SetTimer(window, kSafePointTimer, 100, NewNativeSafePointTimer)) {
g_safePointWindow = window;
if (g_captureLog) g_captureLog(
"new-native companion UI safe-point timer installed window=%p thread=%lu interval_ms=100",
window, g_dispatchThread);
}
}
struct NewNativeTownSnapshot {
const unsigned char* start = nullptr;
std::size_t bodyLength = 0;
std::size_t consumedMinimum = 0;
std::uint16_t localKey = 0;
};
struct NewNativeDungeonInfoSnapshot {
const unsigned char* start = nullptr;
std::size_t bodyLength = 0;
std::uint32_t dungeon = 0;
};
// Current class0 op28 is registered to 01FF5050 at 02001F73. This is a
// per-dungeon notification, while op29 is also used for ordinary room changes.
// Only the former opens a new run; no duplicate companion lifecycle is stored.
static NewNativeDungeonInfoSnapshot SnapshotNewNativeDungeonInfo(
int opcode, const unsigned char* header) {
NewNativeDungeonInfoSnapshot result{};
if (opcode != 28 || !g_captureBase || !header) return result;
__try {
const std::uint32_t total = dnf90_companion_projection::Read32(header + 3);
if (total < 52 || total > kCapturePacketCapacity) return result;
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::size_t bodyLength = total - 16;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) return result;
const std::uint32_t dungeon = dnf90_companion_projection::Read32(reader.cursor);
if (!dungeon) return result;
result.start = reader.cursor;
result.bodyLength = bodyLength;
result.dungeon = dungeon;
} __except (EXCEPTION_EXECUTE_HANDLER) {
result = {};
}
return result;
}
static bool NewNativeBodyWasConsumed(
const unsigned char* startPointer, std::size_t bodyLength, std::size_t minimum) {
if (!startPointer || !g_captureBase) return false;
__try {
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::uintptr_t start = reinterpret_cast<std::uintptr_t>(startPointer);
const std::uintptr_t cursor = reinterpret_cast<std::uintptr_t>(reader.cursor);
return reader.remaining >= 0 && cursor >= start + minimum &&
cursor <= start + bodyLength;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
// DB4 class0 initializer 02524320 calls 020460E0, which registers op24 to
// 02042900 at 020461A3. Its row reads at 020432C2..02043312 are 3 WORDs and
// 3 BYTEs (nine bytes, versus eight in the old client). Early-return gates at
// 02042948/02042956 run before any reader consumption.
static NewNativeTownSnapshot SnapshotNewNativeTown(int opcode, const unsigned char* header) {
NewNativeTownSnapshot result{};
if (opcode != 24 || !g_captureBase || !header) return result;
__try {
const std::uint32_t total = dnf90_companion_projection::Read32(header + 3);
if (total < 20 || total > kCapturePacketCapacity) return result;
const NewNativeReaderState reader = *reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
const std::size_t bodyLength = total - 16;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) return result;
const std::size_t rows = dnf90_companion_projection::Read16(reader.cursor + 2);
if (!rows || rows > (bodyLength - 4) / 9) return result;
const std::uint16_t key = dnf90_companion_projection::Read16(
reader.cursor + 4 + (rows - 1) * 9);
if (!key) return result;
result.start = reader.cursor;
result.bodyLength = bodyLength;
result.consumedMinimum = 4 + rows * 9;
result.localKey = key;
} __except (EXCEPTION_EXECUTE_HANDLER) {
result = {};
}
return result;
}
static bool NewNativeTownWasAccepted(const NewNativeTownSnapshot& snapshot) {
if (!snapshot.start || !snapshot.localKey || !g_captureBase) return false;
__try {
if (!NewNativeBodyWasConsumed(snapshot.start, snapshot.bodyLength,
snapshot.consumedMinimum)) return false;
using CurrentActor = void* (__cdecl*)();
using ActorByKey = void* (__cdecl*)(unsigned short);
void* current = reinterpret_cast<CurrentActor>(g_captureBase + 0x025610E0)();
void* selected = reinterpret_cast<ActorByKey>(g_captureBase + 0x0256D730)(snapshot.localKey);
return current && current == selected;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
static void ConfigureNewNativeCaptureResume(
std::uintptr_t base,
NewNativeCaptureLogFn logger = nullptr)
{
g_captureBase = base;
g_captureResume = base ? base + 0x02123C94 : 0;
g_captureConsumed = base ? base + 0x02123F50 : 0;
g_captureLog = logger;
}
static void SetOriginalNewNativeClass0Dispatch(void* original) {
g_originalClass0Dispatch =
reinterpret_cast<NewNativeClass0DispatchFn>(original);
}
// Called only at VA 02523C8A, after native decoding/decompression and reader
// binding, before admission is enforced. EDI may still be the protected outer
// packet, so its header is copied but plaintext always comes from reader.cursor.
// The native decompressor at 02523820 updates header+3 to decoded body size+16;
// the enclosing router then binds that body size as reader.remaining. Using the
// original socket receive length here would truncate compressed private frames.
// Return true for every recognized private frame, including malformed frames:
// the native router must never index its opcode tables with private opcode 7F41.
static bool __cdecl CaptureNewNativeDecodedProjection(
int sourceContext,
const unsigned char* header)
{
bool privatePacket = false;
bool captured = false;
unsigned char* ownedFrame = nullptr;
std::uint32_t packetLength = 0;
int remaining = -1;
unsigned long exceptionCode = 0;
__try {
do {
if (!g_captureBase || !header || header[0] != 0 ||
dnf90_companion_projection::Read16(header + 1) !=
dnf90_companion_projection::kPrivateOpcode) {
break;
}
privatePacket = true;
packetLength = dnf90_companion_projection::Read32(header + 3);
if (packetLength <
dnf90_companion_projection::kPacketHeaderSize +
dnf90_companion_projection::kBodyHeaderSize ||
packetLength > kCapturePacketCapacity) {
break;
}
const NewNativeReaderState reader =
*reinterpret_cast<const NewNativeReaderState*>(
g_captureBase + kReaderStateRva);
remaining = reader.remaining;
const std::size_t bodyLength = packetLength -
dnf90_companion_projection::kPacketHeaderSize;
if (!reader.cursor || reader.remaining < 0 ||
static_cast<std::size_t>(reader.remaining) < bodyLength) {
break;
}
ownedFrame = static_cast<unsigned char*>(
HeapAlloc(GetProcessHeap(), 0, packetLength));
if (!ownedFrame) break;
std::memcpy(ownedFrame, header,
dnf90_companion_projection::kPacketHeaderSize);
std::memcpy(ownedFrame +
dnf90_companion_projection::kPacketHeaderSize,
reader.cursor, bodyLength);
// Lifecycle work can reenter native readers. The complete private
// frame is owned before calling it, so no borrowed body survives.
dnf90_companion_p0::NewNativeSafePoint(-1);
captured = dnf90_companion_p0::CaptureProjectionClass0Packet(
sourceContext, reinterpret_cast<int>(ownedFrame));
if (captured) {
dnf90_companion_p0::NewNativeSafePoint(
dnf90_companion_projection::kPrivateOpcode);
} else {
dnf90_companion_p0::LogProjectionDecodeRejected(
ownedFrame, packetLength, "new-native-decoded-boundary");
}
} while (false);
} __except (EXCEPTION_EXECUTE_HANDLER) {
exceptionCode = GetExceptionCode();
}
if (ownedFrame) HeapFree(GetProcessHeap(), 0, ownedFrame);
if (privatePacket && g_captureLog) {
g_captureLog("new-native companion decoded capture context=%d length=%u remaining=%d captured=%u exception=0x%08lX",
sourceContext, static_cast<unsigned int>(packetLength), remaining,
captured ? 1u : 0u, exceptionCode);
}
return privatePacket;
}
// The displaced 10 bytes contain no relative instructions:
// 8A 45 A3 mov al,[ebp-5D]
// C7 45 AC 01 00 00 00 mov dword ptr [ebp-54],1
// Resuming at 02523C94 preserves admission for all non-private packets. The
// private branch enters the router's normal consumed epilogue at 02523F50.
// That epilogue writes EAX=1 itself and never reads [EBP-54], so the private
// branch deliberately does not initialize the displaced dispatch-result local.
__declspec(naked) static void ProxyNewNativeProjectionCapture() {
__asm {
pushfd
pushad
push edi
push dword ptr [ebp - 70h]
call CaptureNewNativeDecodedProjection
add esp, 8
test al, al
jnz private_consumed
popad
popfd
mov al, byte ptr [ebp - 5Dh]
mov dword ptr [ebp - 54h], 1
jmp dword ptr [g_captureResume]
private_consumed:
popad
popfd
jmp dword ptr [g_captureConsumed]
}
}
// Current VA 0251E340 has the same two-argument stdcall ABI as the shared
// legacy owner. Its callbacks consume global reader state before this point.
static int __stdcall ProxyNewNativeClass0Dispatch(int sourceContext, int packet) {
NewNativeClass0DispatchFn original = g_originalClass0Dispatch;
if (!original) return 1;
int opcode = -1;
__try {
const unsigned char* bytes =
reinterpret_cast<const unsigned char*>(packet);
if (bytes && bytes[0] == 0) {
opcode = dnf90_companion_projection::Read16(bytes + 1);
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
opcode = -1;
}
const NewNativeTownSnapshot town = SnapshotNewNativeTown(opcode,
reinterpret_cast<const unsigned char*>(packet));
const NewNativeDungeonInfoSnapshot dungeon = SnapshotNewNativeDungeonInfo(opcode,
reinterpret_cast<const unsigned char*>(packet));
const int result = original(sourceContext, packet);
EnsureNewNativeSafePointTimer();
if (NewNativeTownWasAccepted(town)) {
dnf90_companion_p0::CancelFixedApc5405();
if (g_captureLog) g_captureLog(
"new-native companion lifecycle cancelled reason=accepted-town opcode=24 key=%u consumed_min=%zu",
static_cast<unsigned int>(town.localKey), town.consumedMinimum);
return result;
}
if (dungeon.dungeon && NewNativeBodyWasConsumed(dungeon.start, dungeon.bodyLength, 36)) {
dnf90_companion_p0::CancelFixedApc5405();
if (g_captureLog) g_captureLog(
"new-native companion lifecycle reset reason=accepted-dungeon-info opcode=28 dungeon=%u",
static_cast<unsigned int>(dungeon.dungeon));
}
if (opcode >= 0) {
dnf90_companion_p0::NewNativeSafePoint(opcode);
}
return result;
}
} // namespace dnf90_companion_new_native
File diff suppressed because it is too large. Load diff
+493
View File
@@ -0,0 +1,493 @@
#pragma once
#include <cstdint>
#include <cstddef>
namespace dnf90_companion_p0 {
struct FixedApcDescriptor {
std::int32_t apcCode;
std::int32_t minimumLevel;
std::int32_t maximumLevel;
std::int32_t lifetimeValue;
};
constexpr std::int32_t kCompanionLifetimeValue = 0x7FFFFFFF;
constexpr FixedApcDescriptor FixedApc5405Descriptor() {
return {5405, 25, 25, kCompanionLifetimeValue};
}
constexpr FixedApcDescriptor FixedApc56901Descriptor() {
return {56901, 70, 70, kCompanionLifetimeValue};
}
struct ProjectionBaseline {
FixedApcDescriptor descriptor{};
std::uint8_t sourceJob = 0;
std::uint8_t sourceGrow = 0;
std::uint8_t aicJob = 0;
std::uint8_t aicGrow = 0;
bool bridged = false;
};
struct ProjectionBaselineEntry {
std::uint8_t sourceJob;
std::uint8_t sourceGrow;
std::int32_t apcCode;
std::int32_t baselineLevel;
std::uint8_t aicJob;
std::uint8_t aicGrow;
};
struct ProjectionBaselineContract {
static const ProjectionBaselineEntry* Entries(std::size_t* count) {
static const ProjectionBaselineEntry entries[] = {
{0,0,900,70,0,0},{0,1,1201,86,0,1},{0,2,1202,86,0,2},{0,3,1203,86,0,3},{0,4,1204,86,0,4},{0,5,900,70,0,0},
{1,0,5405,25,1,0},{1,1,6200,86,1,1},{1,2,6201,86,1,2},{1,3,6202,86,1,3},{1,4,6203,86,1,4},
{2,0,10903,70,2,0},{2,1,11201,86,2,1},{2,2,11202,86,2,2},{2,3,11203,86,2,3},{2,4,11204,86,2,4},{2,5,10903,70,2,0},
{3,0,15902,70,3,0},{3,1,15121,86,3,1},{3,2,15122,86,3,2},{3,3,15123,86,3,3},{3,4,15124,86,3,4},{3,5,15902,70,3,0},
{4,0,20902,70,4,0},{4,1,21201,86,4,1},{4,2,21202,86,4,2},{4,3,21203,86,4,3},{4,4,21204,86,4,4},{4,5,20902,70,4,0},
{5,0,25900,70,5,0},{5,1,26201,86,5,1},{5,2,26202,86,5,2},{5,3,26203,86,5,3},{5,4,26204,86,5,4},{5,5,25900,70,5,0},
{6,0,30901,70,6,0},{6,1,31200,86,6,1},{6,2,31201,86,6,2},{6,3,31202,86,6,3},{6,4,31203,86,6,4},{6,5,30901,70,6,0},
{7,0,35903,70,7,0},{7,1,36200,86,7,1},{7,2,36201,86,7,2},{7,3,36202,86,7,3},{7,4,36203,86,7,4},
{8,0,40800,75,8,0},{8,1,41201,86,8,1},{8,2,41202,86,8,2},{8,3,41210,86,8,3},{8,4,41213,86,8,4},{8,5,41216,86,8,5},
{9,0,902,70,0,0},
{10,0,15903,70,3,0},
{11,0,56901,70,11,0},{11,1,56200,86,11,1},{11,2,56201,86,11,2},{11,3,56202,86,11,3},{11,4,56203,86,11,4},{11,5,56901,70,11,0},
{12,0,60901,70,12,0},{12,1,61200,86,12,1},{12,2,61209,86,12,2},{12,3,61203,86,12,3},{12,4,61206,86,12,4},
{13,0,65001,70,13,0},{13,1,65121,86,13,1},{13,2,65122,86,13,2},{13,3,65001,70,13,0},{13,4,65001,70,13,0},
{14,0,70003,70,14,0},{14,1,71200,86,14,1},{14,2,71203,86,14,2},{14,3,71206,86,14,3},{14,4,71209,86,14,4},
{15,0,75001,70,15,0},{15,1,75001,70,15,0},{15,2,75001,70,15,0},{15,3,75001,70,15,0},{15,4,75001,70,15,0},
};
if (count) {
*count = sizeof(entries) / sizeof(entries[0]);
}
return entries;
}
static std::size_t EntryCount() {
std::size_t count = 0;
Entries(&count);
return count;
}
static bool EntryAt(
std::size_t index,
ProjectionBaselineEntry* output)
{
if (!output) return false;
std::size_t count = 0;
const ProjectionBaselineEntry* entries =
Entries(&count);
if (index >= count) return false;
*output = entries[index];
return true;
}
static bool TrySelect(
std::uint8_t job,
std::uint8_t packedGrow,
ProjectionBaseline* output)
{
if (!output) return false;
const std::uint8_t firstGrow =
static_cast<std::uint8_t>(packedGrow & 0x0Fu);
const std::uint8_t awakening =
static_cast<std::uint8_t>((packedGrow >> 4) & 0x0Fu);
if (awakening > 2) return false;
std::size_t count = 0;
const ProjectionBaselineEntry* entries =
Entries(&count);
const ProjectionBaselineEntry* exact = nullptr;
const ProjectionBaselineEntry* stable = nullptr;
for (std::size_t index = 0;
index < count;
++index) {
const ProjectionBaselineEntry& entry =
entries[index];
if (entry.sourceJob != job) {
continue;
}
if (!exact && entry.sourceGrow == firstGrow) {
exact = &entry;
}
if (!stable &&
entry.sourceGrow == 0 &&
entry.aicJob == job &&
entry.aicGrow == 0) {
stable = &entry;
}
}
if (!exact) {
return false;
}
// PVP/branch-specific APCs often carry their own movement/FSM policy.
// Prefer one same-job grow=0 PVE shell and project the selected
// character's grow/awakening/skills onto the per-instance AIC copy.
// Jobs without a same-job stable shell keep their exact legacy entry.
const ProjectionBaselineEntry& selected =
stable ? *stable : *exact;
output->descriptor = {
selected.apcCode,
selected.baselineLevel,
selected.baselineLevel,
kCompanionLifetimeValue
};
output->sourceJob = job;
output->sourceGrow = firstGrow;
output->aicJob = selected.aicJob;
output->aicGrow = selected.aicGrow;
output->bridged =
selected.aicJob != job ||
selected.aicGrow != firstGrow;
return true;
}
};
struct ProjectionQuickSkillContract {
static bool PairValid(
std::uint32_t id,
std::uint32_t level)
{
return (id == 0) == (level == 0);
}
};
struct ProjectionGrowBridgeContract {
static std::uint8_t FirstGrow(
std::uint8_t packedGrow)
{
return static_cast<std::uint8_t>(
packedGrow & 0x0Fu);
}
static std::uint8_t AwakeningStage(
std::uint8_t packedGrow)
{
return static_cast<std::uint8_t>(
(packedGrow >> 4) & 0x0Fu);
}
static bool SameJob(
const ProjectionBaseline& baseline)
{
return baseline.sourceJob == baseline.aicJob;
}
static bool ShouldApplyFirstGrow(
const ProjectionBaseline& baseline)
{
return SameJob(baseline) &&
baseline.sourceGrow != baseline.aicGrow;
}
static bool ShouldApplyAwakening(
const ProjectionBaseline& baseline,
std::uint8_t packedGrow)
{
return SameJob(baseline) &&
AwakeningStage(packedGrow) != 0;
}
};
struct ProjectionRuntimeKeyContract {
static constexpr std::uint32_t kQuickSlotCount = 12;
static constexpr std::uint32_t kDirectPoolBase = 12;
static constexpr std::uint32_t kBasicAttackProgramIndex = 24;
static constexpr std::uint32_t kRequiredProgramCount = 25;
// Native AIC loaders are not uniform: some job shells expose one
// additional trailing runtime program. The companion-owned programs
// remain indices 0..24, so accept the proven 25/26 shapes and validate
// every mutable direct-pool slot before writing anything.
static constexpr std::uint32_t kMaxCompatibleProgramCount = 26;
static constexpr std::uint32_t kPlaceholderSkillId = 511;
static constexpr std::uint32_t kSkillCodeBias = 0x100;
static bool EncodeSkill(
std::uint32_t skillId,
std::uint32_t* encoded)
{
if (!encoded || skillId == 0 ||
skillId > 0xFFFFu) {
return false;
}
*encoded = kSkillCodeBias + skillId;
return true;
}
static bool ProgramCountValid(
std::size_t count)
{
return count >= kRequiredProgramCount &&
count <= kMaxCompatibleProgramCount;
}
};
static_assert(sizeof(FixedApcDescriptor) == 16,
"native helper reads exactly four dwords");
class FixedApcRetryPolicy {
public:
void Arm() {
pending_ = true;
submitted_ = false;
}
void Cancel() {
pending_ = false;
submitted_ = false;
}
bool ShouldSubmit(
bool routeReady,
bool nativeReady,
bool sceneReady,
bool projectionReady) {
if (!pending_ || submitted_ ||
!routeReady || !nativeReady || !sceneReady ||
!projectionReady) {
return false;
}
submitted_ = true;
pending_ = false;
return true;
}
bool Pending() const { return pending_; }
bool Submitted() const { return submitted_; }
private:
bool pending_ = false;
bool submitted_ = false;
};
struct DynamicAicApplyContract {
static bool Matches(
bool projectionPending,
bool insideP0Receive,
bool alreadyApplied,
std::uint32_t currentThread,
std::uint32_t armedThread,
const void* destination,
const void* source)
{
return projectionPending &&
insideP0Receive &&
!alreadyApplied &&
armedThread != 0 &&
currentThread == armedThread &&
destination != nullptr &&
source != nullptr &&
destination != source;
}
static bool VectorFits(
std::uintptr_t begin,
std::uintptr_t end,
std::uintptr_t capacityEnd,
std::size_t count,
std::size_t elementSize)
{
if (!begin || elementSize == 0 ||
end < begin || capacityEnd < end) return false;
if (((end - begin) % elementSize) != 0 ||
((capacityEnd - begin) % elementSize) != 0) return false;
return count <= (capacityEnd - begin) / elementSize;
}
static bool EquipmentFits(
std::uintptr_t begin,
std::uintptr_t end,
std::uintptr_t capacityEnd,
std::size_t count)
{
return VectorFits(begin, end, capacityEnd, count, 12u);
}
static bool SkillFits(
std::uintptr_t begin,
std::uintptr_t end,
std::uintptr_t capacityEnd,
std::size_t count)
{
return VectorFits(begin, end, capacityEnd, count, 8u);
}
};
struct ProjectionLookupContract {
static bool Matches(
bool applyActive,
std::uint16_t requestedKey,
std::uint16_t apcKey,
const void* apcActor)
{
return applyActive &&
requestedKey != 0 &&
requestedKey != 0xFFFF &&
requestedKey == apcKey &&
apcActor != nullptr;
}
};
struct SceneModeEntryContract {
static bool IsRelativeJump(std::uint8_t opcode) {
return opcode == 0xE9;
}
static bool JumpTargetInsideModule(
std::uintptr_t entry,
std::int32_t relative,
std::uintptr_t moduleBase,
std::size_t moduleSize)
{
if (!entry || !moduleBase || moduleSize == 0) return false;
const std::uintptr_t target =
entry + 5u + static_cast<std::intptr_t>(relative);
return target >= moduleBase &&
target < moduleBase + moduleSize;
}
};
struct ProjectionRegistrationContract {
static bool Matches(
bool p0Armed,
bool insideP0Receive,
std::uint32_t currentThread,
std::uint32_t armedThread,
std::uint32_t objectKey,
const void* actor,
unsigned char nativeResult)
{
return p0Armed &&
insideP0Receive &&
armedThread != 0 &&
currentThread == armedThread &&
objectKey != 0 &&
objectKey < 0xFFFFu &&
actor != nullptr &&
nativeResult != 0;
}
};
struct DirectOp40SendContract {
static bool Matches(
std::uint32_t routeKind,
std::int32_t targetSlot,
const void* actor,
const void* armedActor,
std::uint32_t mode)
{
return routeKind == 1 &&
targetSlot == -1 &&
actor != nullptr &&
actor == armedActor &&
mode == 3;
}
};
struct DirectOp40PacketContract {
static constexpr std::size_t kHeaderSize = 7;
static constexpr std::uint8_t kClassification = 2;
static constexpr std::uint16_t kMessageID = 40;
static bool Matches(const std::uint8_t* packet, std::size_t length) {
if (!packet || length < kHeaderSize) return false;
const std::uint16_t messageID =
static_cast<std::uint16_t>(packet[1]) |
static_cast<std::uint16_t>(packet[2] << 8);
return packet[0] == kClassification &&
messageID == kMessageID;
}
};
struct DirectOp40PeerDeliveryContract {
static bool Matches(
bool armed,
bool handled,
bool inReceive,
std::uint32_t currentThread,
std::uint32_t armedThread,
const std::uint8_t* packet,
std::size_t length,
std::uint32_t routeKind,
std::uint32_t mode)
{
return armed &&
handled &&
!inReceive &&
armedThread != 0 &&
currentThread == armedThread &&
routeKind == 1 &&
mode == 3 &&
DirectOp40PacketContract::Matches(packet, length);
}
};
bool DirectOp40Enabled();
bool ConfigureNewNativeBindings();
void SetNewNativeHooksReady(bool ready);
void NewNativeSafePoint(int opcode);
void SetOriginalPayloadEncode(void* original);
void __cdecl ProxyPayloadEncode(
std::uint8_t* payload,
std::int32_t length,
void* keyState);
void SetOriginalPeerDelivery(void* original);
void SetOriginalNetworkRouter(void* original);
int __fastcall ProxyNetworkRouter(
void* self,
void* unusedEdx,
std::int32_t sourceContext,
std::uint8_t* packet,
std::int32_t totalLength);
void SetOriginalAicCopy(void* original);
void* __fastcall ProxyAicCopy(
void* destination,
void* unusedEdx,
const void* source);
void SetOriginalApcRegistration(void* original);
void SetOriginalAICondition(void* original);
unsigned char __fastcall ProxyAICondition(void* condition, void* unusedEdx);
unsigned char __fastcall ProxyApcRegistration(
void* self,
void* unusedEdx,
void* ownerActor,
std::uint32_t flags,
std::uint32_t objectKey,
void* actor,
std::uint32_t value5,
std::uint32_t value6,
std::uint32_t value7,
std::uint32_t value8);
void SetProjectionClass0Dispatch(void* original);
bool CaptureProjectionClass0Packet(int dispatcherArg, int packet);
void LogProjectionDecodeRejected(
const unsigned char* bytes,
std::size_t packetLength,
const char* stage);
bool ApplyPendingProjection();
void* ProjectionActorOverrideForObjectKey(std::uint16_t objectKey);
bool ProjectionMode0OwnerContextOverride();
void __fastcall ProxyPeerDelivery(
void* self,
void* unusedEdx,
std::uint8_t* packet,
std::int32_t length,
std::int32_t slot,
std::uint32_t routeKind,
std::uint32_t mode);
// Test-only P0 lifecycle. Arm on confirmed dungeon entry, poll from subsequent
// native dispatch callbacks, cancel on accepted return-to-town.
void ArmFixedApc5405();
bool PollFixedApc5405();
void CancelFixedApc5405();
} // namespace dnf90_companion_p0
+240
View File
@@ -0,0 +1,240 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace dnf90_companion_projection {
constexpr std::uint16_t kPrivateOpcode = 0x7F41;
constexpr std::uint32_t kMagic = 0x314A5250u; // "PRJ1" little-endian
constexpr std::uint8_t kVersion = 3;
constexpr std::uint8_t kFlagPresent = 0x01;
constexpr std::uint8_t kFlagProgression = 0x02;
constexpr std::uint8_t kFlagAiPlan = 0x04;
constexpr std::size_t kPacketHeaderSize = 16; // network router/class0 dispatch packet
constexpr std::size_t kBodyHeaderSize = 72;
constexpr std::size_t kMode0ObjectKeyOffset = 0x4C;
constexpr std::size_t kMode1ObjectKeyOffset = 0x15;
constexpr std::size_t kMaxProjectionBody = 32768;
constexpr std::size_t kMaxDynamicAicBytes = 2048;
constexpr std::size_t kMaxAicNameUnits = 32;
constexpr std::size_t kMaxAicEquipment = 32;
constexpr std::size_t kMaxAicSkills = 128;
constexpr std::size_t kMaxAicQuickSkills = 16;
constexpr std::uint16_t kPlaceholderObjectKey = 0xFFFF;
inline std::uint16_t Read16(const std::uint8_t* p) {
return static_cast<std::uint16_t>(p[0]) |
static_cast<std::uint16_t>(p[1] << 8);
}
inline std::uint32_t Read32(const std::uint8_t* p) {
return static_cast<std::uint32_t>(p[0]) |
(static_cast<std::uint32_t>(p[1]) << 8) |
(static_cast<std::uint32_t>(p[2]) << 16) |
(static_cast<std::uint32_t>(p[3]) << 24);
}
inline std::uint64_t Read64(const std::uint8_t* p) {
return static_cast<std::uint64_t>(Read32(p)) |
(static_cast<std::uint64_t>(Read32(p + 4)) << 32);
}
inline void Write16(std::uint8_t* p, std::uint16_t v) {
p[0] = static_cast<std::uint8_t>(v);
p[1] = static_cast<std::uint8_t>(v >> 8);
}
struct View {
bool present = false;
bool progression = false;
std::uint32_t experience = 0;
std::uint8_t aiBehavior = 0;
std::uint8_t aiComboPresetHash = 0;
std::uint16_t aiClass = 0;
bool aiComboEnabled = false;
std::uint16_t sourceCharacter = 0;
std::uint64_t revision = 0;
const std::uint8_t* fingerprint = nullptr;
std::uint8_t job = 0;
std::uint8_t grow = 0;
std::uint16_t level = 0;
const std::uint8_t* nameUtf16 = nullptr;
std::size_t nameUnits = 0;
const std::uint8_t* equipmentIds = nullptr;
std::size_t equipmentCount = 0;
const std::uint8_t* skills = nullptr;
std::size_t skillCount = 0;
const std::uint8_t* quickSkills = nullptr;
std::size_t quickSkillCount = 0;
const std::uint8_t* mode0 = nullptr;
std::size_t mode0Length = 0;
const std::uint8_t* mode1 = nullptr;
std::size_t mode1Length = 0;
};
inline bool BodiesHavePlaceholder(
const std::uint8_t* mode0, std::size_t mode0Length,
const std::uint8_t* mode1, std::size_t mode1Length)
{
if (!mode0 ||
mode0Length <= kMode0ObjectKeyOffset + 1 ||
mode0[0] != 0 ||
Read16(mode0 + 1) != 1 ||
Read16(mode0 + kMode0ObjectKeyOffset) !=
kPlaceholderObjectKey) {
return false;
}
if (mode1Length == 0) {
return mode1 == nullptr;
}
return mode1 &&
mode1Length > kMode1ObjectKeyOffset + 1 &&
mode1[0] == 1 &&
Read16(mode1 + 1) == 1 &&
Read16(mode1 + kMode1ObjectKeyOffset) ==
kPlaceholderObjectKey;
}
inline bool DecodeBody(
const std::uint8_t* body,
std::size_t bodyLength,
View* out)
{
if (!body || !out || bodyLength < kBodyHeaderSize) {
return false;
}
if (Read32(body) != kMagic || body[4] != kVersion ||
(body[5] & ~(kFlagPresent | kFlagProgression | kFlagAiPlan)) != 0) {
return false;
}
View value{};
value.present = (body[5] & kFlagPresent) != 0;
value.progression = (body[5] & kFlagProgression) != 0;
value.experience = Read32(body + 68);
const bool hasAiPlan = (body[5] & kFlagAiPlan) != 0;
if (hasAiPlan && value.progression) return false;
if (hasAiPlan) {
value.aiBehavior = static_cast<std::uint8_t>(value.experience & 0x03);
value.aiComboEnabled = (value.experience & 0x04) != 0;
value.aiComboPresetHash = static_cast<std::uint8_t>((value.experience >> 8) & 0xFF);
value.aiClass = static_cast<std::uint16_t>((value.experience >> 16) & 0xFFFF);
if (value.aiBehavior < 1 || value.aiBehavior > 3) return false;
}
value.sourceCharacter = Read16(body + 6);
value.revision = Read64(body + 8);
value.mode0Length = Read32(body + 16);
value.mode1Length = Read32(body + 20);
value.fingerprint = body + 24;
value.job = body[56];
value.grow = body[57];
value.nameUnits = Read16(body + 58);
value.equipmentCount = Read16(body + 60);
value.level = Read16(body + 62);
value.skillCount = Read16(body + 64);
value.quickSkillCount = Read16(body + 66);
if (value.revision == 0 || (!value.progression && !hasAiPlan && value.experience != 0)) return false;
if (!value.present) {
if (value.progression || value.sourceCharacter != 0 ||
value.mode0Length != 0 || value.mode1Length != 0 ||
value.nameUnits != 0 || value.equipmentCount != 0 ||
value.level != 0 || value.skillCount != 0 ||
value.quickSkillCount != 0 ||
bodyLength != kBodyHeaderSize || hasAiPlan) {
return false;
}
*out = value;
return true;
}
if (value.sourceCharacter == 0 ||
value.mode0Length == 0 ||
value.mode0Length > kMaxProjectionBody ||
value.mode1Length > kMaxProjectionBody ||
value.level == 0 || value.level > 90 ||
value.nameUnits == 0 ||
value.nameUnits > kMaxAicNameUnits ||
value.equipmentCount > kMaxAicEquipment ||
value.skillCount > kMaxAicSkills ||
value.quickSkillCount > kMaxAicQuickSkills) {
return false;
}
const std::size_t nameBytes = value.nameUnits * 2;
const std::size_t equipmentBytes = value.equipmentCount * 4;
const std::size_t skillBytes = value.skillCount * 8;
const std::size_t quickSkillBytes = value.quickSkillCount * 8;
const std::size_t dynamicBytes =
nameBytes + equipmentBytes + skillBytes + quickSkillBytes;
if (dynamicBytes > kMaxDynamicAicBytes) return false;
const std::size_t wanted =
kBodyHeaderSize + dynamicBytes +
value.mode0Length + value.mode1Length;
if (wanted != bodyLength) return false;
value.nameUtf16 = body + kBodyHeaderSize;
value.equipmentIds = value.nameUtf16 + nameBytes;
value.skills = value.equipmentIds + equipmentBytes;
value.quickSkills = value.skills + skillBytes;
value.mode0 = value.quickSkills + quickSkillBytes;
value.mode1 = value.mode1Length != 0
? value.mode0 + value.mode0Length
: nullptr;
if (!BodiesHavePlaceholder(
value.mode0, value.mode0Length,
value.mode1, value.mode1Length)) {
return false;
}
// Live progression carries the existing native 92-byte base-stat block,
// but never dispatches mode0/mode1 (which can reconstruct actor resources).
if (value.progression && (value.mode1Length < 123 ||
Read32(value.mode1 + 27) != 92)) return false;
for (std::size_t i = 0; value.progression && i < value.skillCount; ++i) {
if (Read32(value.skills + i * 8) == 0 || Read32(value.skills + i * 8) >= 512 ||
Read32(value.skills + i * 8 + 4) == 0 || Read32(value.skills + i * 8 + 4) > 255) return false;
}
*out = value;
return true;
}
inline bool IsPrivateNetworkPacketHeader(
const std::uint8_t* packet,
std::size_t totalLength)
{
if (!packet ||
totalLength < kPacketHeaderSize + kBodyHeaderSize) {
return false;
}
const std::uint32_t packetLength = Read32(packet + 3);
return packet[0] == 0 &&
Read16(packet + 1) == kPrivateOpcode &&
packetLength == totalLength;
}
inline bool DecodePacket(
const std::uint8_t* packet,
std::size_t available,
View* out)
{
if (!IsPrivateNetworkPacketHeader(packet, available) || !out) {
return false;
}
return DecodeBody(
packet + kPacketHeaderSize,
available - kPacketHeaderSize,
out);
}
inline bool PatchTargetObjectKey(
std::uint8_t* mode0, std::size_t mode0Length,
std::uint8_t* mode1, std::size_t mode1Length,
std::uint16_t objectKey)
{
if (objectKey == 0 || objectKey == kPlaceholderObjectKey ||
!BodiesHavePlaceholder(mode0, mode0Length, mode1, mode1Length)) {
return false;
}
Write16(mode0 + kMode0ObjectKeyOffset, objectKey);
if (mode1Length != 0) {
Write16(mode1 + kMode1ObjectKeyOffset, objectKey);
}
return true;
}
} // namespace dnf90_companion_projection
@@ -0,0 +1,62 @@
#pragma once
#include "90CN_companion_skill_data.h"
namespace dnf90_companion_quickbar {
enum class Purpose { Empty, Combat, PartyBuff, Heal, Passive, SupportAwakening, SelfBuff, DefensiveSupport, Unsupported, Summon, SelfHeal };
inline bool IsAssistMode(std::uint8_t behavior) { return behavior != 2; }
inline bool SupportsQuickbarPolicy(std::uint8_t job) { return job < 16; }
inline bool NativeActionRecent(std::uint32_t now, std::uint32_t started, std::uint32_t window) { return now >= started && now - started < window; }
inline bool OrdinarySupportAllowed(std::uint8_t behavior, bool fighting, bool hurt) { return !hurt && (IsAssistMode(behavior) || !fighting); }
inline Purpose Classify(std::uint8_t job, std::uint32_t id, std::uint8_t grow = 0) {
if (!id) return Purpose::Empty;
const auto* r = dnf90_companion_skill_data::Find(job,id);
if (!r) return Purpose::Unsupported;
// Low nibble is the profession branch; the high nibble is awakening.
if (r->passiveGrow & (1u << (grow & 15))) return Purpose::Passive;
return static_cast<Purpose>(r->purpose);
}
inline std::uint32_t CooldownMS(std::uint8_t job, std::uint32_t id) { const auto* r=dnf90_companion_skill_data::Find(job,id); return r ? r->cooldown : 0; }
inline bool HasNativeBuffCheck(std::uint8_t job, std::uint32_t id) { const auto* r=dnf90_companion_skill_data::Find(job,id); return r && (r->flags & 2); }
inline bool IsAreaCombat(std::uint8_t job, std::uint32_t id) { const auto* r=dnf90_companion_skill_data::Find(job,id); return r && r->purpose==1 && (r->flags & 4); }
inline bool CombatPurposeAllowed(std::uint8_t behavior, Purpose purpose) { return !IsAssistMode(behavior) && purpose==Purpose::Combat; }
// Accepted dispatch is not an effect receipt. Refresh is a conservative AI
// policy. Switches and summons are issued once per native key program lifetime
// so another press cannot cancel an existing state.
inline bool SupportDispatchHandled(std::uint8_t job, std::uint32_t id, std::uint32_t now, std::uint32_t acceptedAt) {
const auto* r=dnf90_companion_skill_data::Find(job,id);
if (!r || !acceptedAt) return false;
if (r->flags & 1) return true;
return r->refresh && NativeActionRecent(now,acceptedAt,r->refresh);
}
inline bool EligibleSupportBuff(std::uint8_t job, std::uint32_t id, std::uint32_t learned, std::uint8_t grow=0) {
const auto p=Classify(job,id,grow);
return learned && (p==Purpose::PartyBuff || p==Purpose::SelfBuff || p==Purpose::Summon);
}
inline bool FirstSupportOption(std::uint8_t job, const std::uint32_t (&quick)[12], const std::uint32_t (&learned)[512], std::size_t slot, std::uint8_t grow) {
const auto id=quick[slot]; const auto* r=dnf90_companion_skill_data::Find(job,id);
for (std::size_t j=0; j<slot; ++j) {
if (quick[j]==id) return false;
const auto* prev=dnf90_companion_skill_data::Find(job,quick[j]);
if (r && r->group && prev && prev->group==r->group && EligibleSupportBuff(job,quick[j],learned[quick[j]],grow)) return false;
}
return true;
}
template<class Handled,class Ready>
int SelectSupportBuff(std::uint8_t job, const std::uint32_t (&quick)[12], const std::uint32_t (&learned)[512], Handled handled, Ready ready, std::uint8_t grow=0) {
constexpr Purpose priorities[]={Purpose::PartyBuff,Purpose::SelfBuff,Purpose::Summon};
for (const auto priority:priorities) for (std::size_t i=0;i<12;++i) {
const auto id=quick[i];
if (id>=512 || !EligibleSupportBuff(job,id,learned[id],grow) || Classify(job,id,grow)!=priority) continue;
if (FirstSupportOption(job,quick,learned,i,grow) && !handled(id) && ready(i)) return static_cast<int>(i);
}
return -1;
}
template<class Ready>
int SelectSupportAwakening(std::uint8_t job, const std::uint32_t (&quick)[12], const std::uint32_t (&learned)[512], Ready ready, std::uint8_t grow=0) {
for (std::size_t i=0;i<12;++i) {
const auto id=quick[i];
if (id>=512 || !learned[id] || Classify(job,id,grow)!=Purpose::SupportAwakening) continue;
if (FirstSupportOption(job,quick,learned,i,grow) && ready(i)) return static_cast<int>(i);
}
return -1;
}
}
File diff suppressed because it is too large. Load diff
+214
View File
@@ -0,0 +1,214 @@
// 90CN_digest_trace.cpp —— 章节过场(op1378 / story digest)客户端侧**只读**日志。
//
// 背景:服务端能证明它把 op1378 发出去了(日志事件
// game-current-story-digest-last-level-send 带 last_level),但现象是
// 「完成开区任务当次不播,小退重登才播」。要判断卡在哪一侧必须先确认:
//
// 1. 开区任务完成那一刻客户端**有没有收到** op1378、值是多少;
// 2. 它相对场景重建包 op24 是前是后。
//
// 实现:不新增 hook,只在补丁**生产环境实际安装**的 class0 派发代理
// (ProxyTownCopresenceClass0Dispatch)入口读一下已经到手的包结构并写日志。
// 不改行为、不改寄存器现场,全部包在 __try/__except 里。
//
// class0 包布局(与 CacheCurrentCharacterStatsFromClass0Packet 一致):
// bytes[0] = class(0)
// bytes[1..2] = opcode(u16 LE)
// bytes[3..6] = packetLength(u32 LE)
// bytes[16..] = body,长度 = packetLength - 16
//
// 诊断策略:前 kGenericLogLimit 个 class0 包**全部**记一行(含 opcode/长度),
// 用来确认布局与是否真的从这里过;op1378 与 op24 则完全不过滤、始终记录。
#include "90CN_digest_trace.h"
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
namespace
{
constexpr unsigned short kStoryDigestOpcode = 1378; // 0x0562
constexpr unsigned short kSceneTransitionOpcode = 24;
constexpr long kGenericLogLimit = 400;
volatile LONG g_totalPackets = 0;
volatile LONG g_digestCount = 0;
volatile LONG g_sceneCount = 0;
volatile LONG g_readyLogged = 0;
bool g_logPathReady = false;
char g_logPath[MAX_PATH] = {};
void EnsureLogPath()
{
if (g_logPathReady) return;
g_logPathReady = true;
char modulePath[MAX_PATH] = {};
HMODULE self = nullptr;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(&EnsureLogPath), &self) && self &&
GetModuleFileNameA(self, modulePath, MAX_PATH))
{
char* slash = strrchr(modulePath, '\\');
if (slash)
{
slash[1] = '\0';
sprintf_s(g_logPath, "%s90CN_digest.log", modulePath);
return;
}
}
strcpy_s(g_logPath, "90CN_digest.log");
}
void WriteLine(const char* text)
{
#ifdef DNF90_SILENT_LOG
(void)text; return;
#endif
if (!text) return;
EnsureLogPath();
SYSTEMTIME st;
GetLocalTime(&st);
FILE* file = nullptr;
if (fopen_s(&file, g_logPath, "a") == 0 && file)
{
fprintf(file, "%02d:%02d:%02d.%03d %s\n",
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text);
fclose(file);
}
}
}
namespace cn90
{
void LogClass0CutscenePacket(const unsigned char* bytes)
{
if (!bytes) return;
if (InterlockedCompareExchange(&g_readyLogged, 1, 0) == 0)
WriteLine("digest trace ready: class0 packet logging active");
const unsigned short opcode =
*reinterpret_cast<const unsigned short*>(bytes + 1);
const unsigned int packetLength =
*reinterpret_cast<const unsigned int*>(bytes + 3);
if (packetLength < 16 || packetLength > 0x100000) return;
const unsigned int bodyLength = packetLength - 16;
if (opcode == kStoryDigestOpcode)
{
unsigned int lastLevel = 0;
if (bodyLength >= 4)
lastLevel = *reinterpret_cast<const unsigned int*>(bytes + 16);
char text[256] = {};
sprintf_s(text, "op1378 story-digest received body_len=%u last_level=%u (digest#%ld)",
bodyLength, lastLevel, static_cast<long>(InterlockedIncrement(&g_digestCount)));
WriteLine(text);
return;
}
if (opcode == kSceneTransitionOpcode)
{
char text[224] = {};
sprintf_s(text, "op24 scene-transition received body_len=%u (scene#%ld)",
bodyLength, static_cast<long>(InterlockedIncrement(&g_sceneCount)));
WriteLine(text);
return;
}
// 诊断:前若干个包全记,确认布局与流量确实经过这里。
const long total = InterlockedIncrement(&g_totalPackets);
if (total <= kGenericLogLimit)
{
char text[192] = {};
sprintf_s(text, "class0 opcode=%u body_len=%u (packet#%ld)",
static_cast<unsigned>(opcode), bodyLength, total);
WriteLine(text);
}
}
void LogAviOpen(const char* path, const char* callerRvas)
{
char text[768] = {};
sprintf_s(text, "avi-open path=%s client_frames=%s",
path ? path : "(null)", (callerRvas && callerRvas[0]) ? callerRvas : "(none)");
WriteLine(text);
}
void LogMovieEntry(unsigned retRva, unsigned ecx, unsigned edx,
unsigned arg1, const char* s1, unsigned arg2, const char* s2,
unsigned arg3, const char* s3)
{
char text[900] = {};
sprintf_s(text,
"movie-fn-entry ret_rva=0x%08X ecx=0x%08X edx=0x%08X "
"arg1=0x%08X[%s] arg2=0x%08X[%s] arg3=0x%08X[%s]",
retRva, ecx, edx,
arg1, (s1 && s1[0]) ? s1 : "",
arg2, (s2 && s2[0]) ? s2 : "",
arg3, (s3 && s3[0]) ? s3 : "");
WriteLine(text);
}
void LogMovieInvoke(unsigned fnRva, unsigned thisPtr, const char* path, int willCall)
{
char text[640] = {};
sprintf_s(text, "movie-invoke fn_rva=0x%08X this=0x%08X will_call=%d path=%s",
fnRva, thisPtr, willCall, (path && path[0]) ? path : "(none)");
WriteLine(text);
}
void LogMovieChain(const char* chain)
{
char text[768] = {};
sprintf_s(text, "movie-call-chain %s", (chain && chain[0]) ? chain : "(none)");
WriteLine(text);
}
void LogMovieVtable(unsigned object, unsigned vtable, const char* slots)
{
char text[820] = {};
sprintf_s(text, "movie-vtable this=0x%08X vtbl=0x%08X %s", object, vtable,
(slots && slots[0]) ? slots : "(none)");
WriteLine(text);
}
void LogMovieStack(const char* frames)
{
char text[1640] = {};
sprintf_s(text, "movie-stack %s", (frames && frames[0]) ? frames : "(none)");
WriteLine(text);
}
void LogMovieOpen(unsigned retRva, unsigned thisPtr, unsigned argPtr,
const char* pathText, const char* stackText)
{
char text[1200] = {};
sprintf_s(text, "movie-open ret=0x%08X this=0x%08X arg=0x%08X path=\"%s\" stack=%s",
retRva, thisPtr, argPtr,
(pathText && pathText[0]) ? pathText : "",
(stackText && stackText[0]) ? stackText : "(none)");
WriteLine(text);
}
void LogMoviePlay(unsigned retRva, unsigned thisPtr, const char* stackText)
{
char text[1024] = {};
sprintf_s(text, "movie-play ret=0x%08X this=0x%08X stack=%s",
retRva, thisPtr, (stackText && stackText[0]) ? stackText : "(none)");
WriteLine(text);
}
void LogMovieRender(long count, unsigned thisPtr, unsigned retRva, const char* stackText)
{
char text[1024] = {};
sprintf_s(text, "movie-render #%ld this=0x%08X ret=0x%08X stack=%s",
count, thisPtr, retRva, (stackText && stackText[0]) ? stackText : "(none)");
WriteLine(text);
}
void LogStoryDigestTraceReady()
{
WriteLine("digest trace module loaded");
}
}
+44
View File
@@ -0,0 +1,44 @@
// 90CN_digest_trace.h —— 章节过场(op1378 / op24)客户端侧只读日志。
//
// 只在补丁已有的 class0 派发代理里调用,不新增 hook。
#pragma once
namespace cn90
{
// 传入 class0 包首地址(bytes[0]=class,bytes[1..2]=opcode)。
// 命中 op1378 / op24 时写一行到 90CN_digest.log;其它包直接返回。
void LogClass0CutscenePacket(const unsigned char* bytes);
// 记录一次 *.avi 打开:路径 + 调用点 RVA(用于定位客户端影片播放函数)。
void LogAviOpen(const char* path, const char* callerRvas);
// 记录影片函数入口的寄存器/栈参数,用于判定调用约定。
void LogMovieEntry(unsigned retRva, unsigned ecx, unsigned edx,
unsigned arg1, const char* s1, unsigned arg2, const char* s2,
unsigned arg3, const char* s3);
// 记录一次「主动调用客户端影片播放器」的尝试(willCall=1 表示真的调用了)。
void LogMovieInvoke(unsigned fnRva, unsigned thisPtr, const char* path, int willCall);
void LogMovieChain(const char* chain);
// 记录一次「原始栈扫描」结果:把栈上出现的 bdvid32.dll / DNF.exe 返回地址
// 连同「前一条指令像不像 call」一起写出来(带 * 的更像真正的返回地址)。
void LogMovieStack(const char* frames);
// 记录影片对象 Open(path) 的一次调用:返回地址、this、宽字符路径、栈上候选返回地址。
void LogMovieOpen(unsigned retRva, unsigned thisPtr, unsigned argPtr,
const char* pathText, const char* stackText);
// 记录影片对象 Play() 的一次调用。
void LogMoviePlay(unsigned retRva, unsigned thisPtr, const char* stackText);
// 记录影片 Render 的调用(计数 + this + 返回地址 + 栈上候选)。
void LogMovieRender(long count, unsigned thisPtr, unsigned retRva, const char* stackText);
// 一次性记录播放对象 vtable 的 12 个槽位(模块!RVA)。
void LogMovieVtable(unsigned object, unsigned vtable, const char* slots);
// DLL 初始化完成时写一行 "ready",便于确认补丁版本已加载。
void LogStoryDigestTraceReady();
}
@@ -0,0 +1,29 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace dnf90_dungeon_assist {
constexpr uint16_t kOpcode = 0x7f47;
constexpr size_t kHeaderSize = 46;
constexpr size_t kMaxKeys = 128;
inline uint16_t U16(const unsigned char* p) { return uint16_t(p[0]) | uint16_t(p[1]) << 8; }
inline uint32_t U32(const unsigned char* p) { return uint32_t(U16(p)) | uint32_t(U16(p + 2)) << 16; }
// A native active wave or a completed hit threshold must never be advanced
// again, even while a spawn animation has not produced its actors yet.
inline bool CanAdvanceGate(bool wavePresent, bool spawning, int hits, int threshold) {
return !wavePresent && !spawning && hits >= 0 && threshold > 0 && hits < threshold;
}
inline bool ValidQuery(const unsigned char* body, size_t size) {
if (!body || size < kHeaderSize + 1 || body[0] != 1 ||
body[33] > 1 || !U16(body + 1) || !U32(body + 19) || !U32(body + 23) || !U32(body + 27)) return false;
const size_t count = body[kHeaderSize];
if (!count || count > kMaxKeys || size != kHeaderSize + 1 + count * 2) return false;
for (size_t i = 0; i < count; ++i) {
const uint16_t key = U16(body + kHeaderSize + 1 + i * 2);
if (!key || key == 0xffff) return false;
for (size_t j = 0; j < i; ++j)
if (key == U16(body + kHeaderSize + 1 + j * 2)) return false;
}
return true;
}
}
@@ -0,0 +1,843 @@
#include "90CN_dungeon_skill_gate_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
namespace
{
// v7/v8 live evidence on the current NoPack EXE:
// sub_340BCF1 calls the native skill-state precheck, then executes
// test al, al
// je loc_340BD2E
// A non-zero result falls through into sub_99F840, which opens owner 0x49
// (the "initial skill state" warning) and then early-returns from the portal
// transition before ENUM_CMDPACKET_ENTER_SELECT_DUNGEON/msg15 is emitted.
// Force only this post-test branch to the native normal continuation. The
// precheck function itself still runs, preserving any side effects/state reads.
constexpr uintptr_t kSkillGateBranchRva = 0x0300BCFF; // 0340BCFF at base 00400000
constexpr DWORD kInstallDelayMs = 1200;
const unsigned char kExpectedBranch[2] = { 0x74, 0x2D }; // je +0x2D
const unsigned char kBypassBranch[2] = { 0xEB, 0x2D }; // jmp +0x2D
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
HANDLE g_worker = nullptr;
volatile LONG g_stop = 0;
volatile LONG g_installed = 0;
// The current 111 MB DB4 client does not contain the legacy 74 2D branch.
// Keep the first observation pass strictly passive: this function is the
// current selector/list state routine identified by the version-locked
// prologue at RVA 0300BB80. The probe records entry state and returns to the
// original function through a trampoline; it never changes a branch result.
constexpr uintptr_t kCurrentSelectorProbeRva = 0x0300BB80;
constexpr size_t kCurrentSelectorProbePatchLength = 10;
const unsigned char kCurrentSelectorProbePrologue[
kCurrentSelectorProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x6A, 0xFF, 0x68, 0xF8, 0x44, 0x27, 0x04
};
constexpr LONG kCurrentSelectorProbeMaxLoggedCalls = 4096;
HMODULE g_currentProbeModule = nullptr;
uintptr_t g_currentProbeBase = 0;
void* g_currentSelectorProbeOriginal = nullptr;
HANDLE g_currentProbeWorker = nullptr;
volatile LONG g_currentProbeStop = 0;
volatile LONG g_currentProbeInstalled = 0;
volatile LONG g_currentProbeCallCount = 0;
// The DB4 client no longer uses the legacy 0300BCFF branch. The first
// observation pass also showed that 0300BB80 is only a startup list routine.
// These two current-EXE owner/notice builders are therefore traced separately
// while reproducing the native "initial skill state" dialog. They remain
// observe-only: no return value, owner, or dialog state is changed.
constexpr uintptr_t kCurrentOwnerManagerProbeRva = 0x004A1170; // VA 008A1170
constexpr size_t kCurrentOwnerManagerProbePatchLength = 14;
const unsigned char kCurrentOwnerManagerProbePrologue[
kCurrentOwnerManagerProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x83, 0xE4, 0xF8, 0x83, 0xEC,
0x18, 0x56, 0x57, 0x8D, 0x45, 0x0C,
};
constexpr uintptr_t kCurrentNoticeBuilderProbeRva = 0x023B65B0; // VA 027B65B0
constexpr size_t kCurrentNoticeBuilderProbePatchLength = 12;
const unsigned char kCurrentNoticeBuilderProbePrologue[
kCurrentNoticeBuilderProbePatchLength] = {
0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x18, 0x83, 0x7D,
0x08, 0x00, 0x56, 0x8B,
};
constexpr LONG kCurrentOwnerProbeMaxLoggedCalls = 1024;
void* g_currentOwnerManagerProbeOriginal = nullptr;
void* g_currentNoticeBuilderProbeOriginal = nullptr;
volatile LONG g_currentOwnerProbeInstalled = 0;
volatile LONG g_currentNoticeProbeInstalled = 0;
volatile LONG g_currentOwnerProbeCallCount = 0;
// Current 111 MB DB4 client. In the owner-0x73 dungeon-selector function,
// the native count comparison at VA 0x02248CAC uses `jle 0x02248CE8` to
// continue normally. The fall-through path builds text id 0x11C79 and sends
// event 0x2D6 (the "skill is in initial state" notice) before hiding the
// selector owner. Force only this local branch to the already existing normal
// continuation. The exact prologue/branch bytes are checked at runtime.
constexpr uintptr_t kCurrentSkillGateBranchRva = 0x01E48CAC;
const unsigned char kCurrentSkillGateExpected[2] = { 0x7E, 0x3A };
const unsigned char kCurrentSkillGateBypass[2] = { 0xEB, 0x3A };
HMODULE g_currentSkillGateModule = nullptr;
uintptr_t g_currentSkillGateBase = 0;
HANDLE g_currentSkillGateWorker = nullptr;
volatile LONG g_currentSkillGateStop = 0;
volatile LONG g_currentSkillGateInstalled = 0;
void BuildLogPath(wchar_t* output, size_t outputCount)
{
if (!output || outputCount == 0) return;
output[0] = L'\0';
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, output, static_cast<DWORD>(outputCount))
: 0;
if (!length || length >= outputCount) {
wcscpy_s(output, outputCount, L"90CN_fix.log");
return;
}
wchar_t* slash = wcsrchr(output, L'\\');
wchar_t* slash2 = wcsrchr(output, L'/');
if (slash2 && (!slash || slash2 > slash)) slash = slash2;
if (slash) {
slash[1] = L'\0';
wcscat_s(output, outputCount, L"90CN_fix.log");
} else {
wcscpy_s(output, outputCount, L"90CN_fix.log");
}
}
void WriteLogLine(const char* text)
{
if (!text) return;
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
HANDLE file = CreateFileW(path, FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(file, text, static_cast<DWORD>(strlen(text)), &written, nullptr);
CloseHandle(file);
}
bool PatchBytes(unsigned char* target, const unsigned char* bytes, size_t count)
{
DWORD oldProtection = 0;
if (!VirtualProtect(target, count, PAGE_EXECUTE_READWRITE, &oldProtection)) {
return false;
}
memcpy(target, bytes, count);
DWORD ignoredProtection = 0;
VirtualProtect(target, count, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, count);
return memcmp(target, bytes, count) == 0;
}
// 探针开关:DLL 同目录出现 90CN_gate_probe.flag 时**不**打这个旁路,让客户端
// 按原逻辑走“技能处于初始状态”分支。用来确认该检查对当前角色是否真的成立,
// 不改任何数据、删掉 flag 即恢复。2026-10-19 蛇腹剑形态排查用。
static bool GateProbeBypassDisabled()
{
wchar_t path[MAX_PATH] = {};
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, path, MAX_PATH)
: 0;
if (!length || length >= MAX_PATH) return false;
wchar_t* slash = wcsrchr(path, L'\\');
if (!slash) return false;
slash[1] = L'\0';
if (wcscat_s(path, L"90CN_gate_probe.flag") != 0) return false;
return GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES;
}
bool InstallBypass()
{
if (!g_exeBase) return false;
if (GateProbeBypassDisabled()) {
WriteLogLine(
"dungeon-skill-gate-fix probe_mode=1 bypass_disabled=true "
"target=0340BCFF reason=90CN_gate_probe.flag\r\n");
return true;
}
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kSkillGateBranchRva);
unsigned char observed[2] = {};
__try {
memcpy(observed, target, sizeof(observed));
if (memcmp(observed, kBypassBranch, sizeof(kBypassBranch)) == 0) {
InterlockedExchange(&g_installed, 1);
WriteLogLine(
"dungeon-skill-gate-fix already_installed target=0340BCFF bytes=EB2D\r\n");
return true;
}
if (memcmp(observed, kExpectedBranch, sizeof(kExpectedBranch)) != 0) {
char line[256] = {};
sprintf_s(line,
"dungeon-skill-gate-fix install_failed target=0340BCFF "
"observed=%02X%02X expected=742D\r\n",
observed[0], observed[1]);
WriteLogLine(line);
return false;
}
if (!PatchBytes(target, kBypassBranch, sizeof(kBypassBranch))) {
WriteLogLine(
"dungeon-skill-gate-fix install_failed reason=patch-write target=0340BCFF\r\n");
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"dungeon-skill-gate-fix install_failed reason=exception target=0340BCFF\r\n");
return false;
}
InterlockedExchange(&g_installed, 1);
WriteLogLine(
"dungeon-skill-gate-fix installed target=0340BCFF original=742D replacement=EB2D "
"scope=initial-skill-selector-precheck-only preserves=native-skill-check-call-and-other-dungeon-gates\r\n");
return true;
}
void RestoreBypass()
{
if (InterlockedCompareExchange(&g_installed, 0, 0) == 0 || !g_exeBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kSkillGateBranchRva);
__try {
if (memcmp(target, kBypassBranch, sizeof(kBypassBranch)) == 0) {
if (PatchBytes(target, kExpectedBranch, sizeof(kExpectedBranch))) {
WriteLogLine(
"dungeon-skill-gate-fix restored target=0340BCFF bytes=742D\r\n");
} else {
WriteLogLine(
"dungeon-skill-gate-fix restore_failed target=0340BCFF\r\n");
}
} else {
char line[256] = {};
sprintf_s(line,
"dungeon-skill-gate-fix restore_skipped target=0340BCFF current=%02X%02X\r\n",
target[0], target[1]);
WriteLogLine(line);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"dungeon-skill-gate-fix restore_failed reason=exception target=0340BCFF\r\n");
}
InterlockedExchange(&g_installed, 0);
}
bool InstallCurrentSkillGateBypass()
{
if (!g_currentSkillGateBase) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentSkillGateBase + kCurrentSkillGateBranchRva);
unsigned char observed[2] = {};
__try {
memcpy(observed, target, sizeof(observed));
if (memcmp(observed, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass)) == 0) {
InterlockedExchange(&g_currentSkillGateInstalled, 1);
WriteLogLine(
"new-client dungeon-skill-gate already_installed "
"target=02248CAC bytes=EB3A scope=owner-73-selector\r\n");
return true;
}
if (memcmp(observed, kCurrentSkillGateExpected,
sizeof(kCurrentSkillGateExpected)) != 0) {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-skill-gate install_failed "
"target=02248CAC observed=%02X%02X expected=7E3A\r\n",
observed[0], observed[1]);
WriteLogLine(line);
return false;
}
if (!PatchBytes(target, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass))) {
WriteLogLine(
"new-client dungeon-skill-gate install_failed "
"reason=patch-write target=02248CAC\r\n");
return false;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"new-client dungeon-skill-gate install_failed "
"reason=exception target=02248CAC\r\n");
return false;
}
InterlockedExchange(&g_currentSkillGateInstalled, 1);
WriteLogLine(
"new-client dungeon-skill-gate installed target=02248CAC "
"original=7E3A replacement=EB3A "
"scope=owner-73-dungeon-selector-initial-skill-notice\r\n");
return true;
}
void RestoreCurrentSkillGateBypass()
{
if (InterlockedCompareExchange(&g_currentSkillGateInstalled, 0, 0) == 0 ||
!g_currentSkillGateBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentSkillGateBase + kCurrentSkillGateBranchRva);
__try {
if (memcmp(target, kCurrentSkillGateBypass,
sizeof(kCurrentSkillGateBypass)) == 0) {
if (PatchBytes(target, kCurrentSkillGateExpected,
sizeof(kCurrentSkillGateExpected))) {
WriteLogLine(
"new-client dungeon-skill-gate restored "
"target=02248CAC bytes=7E3A\r\n");
} else {
WriteLogLine(
"new-client dungeon-skill-gate restore_failed "
"target=02248CAC\r\n");
}
} else {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-skill-gate restore_skipped "
"target=02248CAC current=%02X%02X\r\n",
target[0], target[1]);
WriteLogLine(line);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLogLine(
"new-client dungeon-skill-gate restore_failed "
"reason=exception target=02248CAC\r\n");
}
InterlockedExchange(&g_currentSkillGateInstalled, 0);
}
DWORD WINAPI InstallCurrentSkillGateWorker(LPVOID)
{
g_currentSkillGateBase = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
if (!g_currentSkillGateBase || !g_currentSkillGateModule) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_currentSkillGateStop, 0, 0) == 0) {
InstallCurrentSkillGateBypass();
}
return 0;
}
DWORD WINAPI InstallWorker(LPVOID)
{
g_exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!g_exeBase || !g_selfModule) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
InstallBypass();
}
return 0;
}
void LogCurrentSelectorProbe(uintptr_t self, uintptr_t caller)
{
LONG callNumber = InterlockedIncrement(&g_currentProbeCallCount);
if (callNumber > kCurrentSelectorProbeMaxLoggedCalls) return;
const uintptr_t base = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
const unsigned int callerRva = (base && caller >= base)
? static_cast<unsigned int>(caller - base) : 0;
unsigned int state2fc = 0xFFFFFFFFu;
uintptr_t manager = 0;
__try {
if (self) {
state2fc = static_cast<unsigned int>(
*reinterpret_cast<volatile unsigned char*>(self + 0x2FC));
manager = *reinterpret_cast<volatile uintptr_t*>(self + 0xFC8);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
state2fc = 0xFFFFFFFEu;
manager = 0;
}
// The two statically identified callers are the useful selector/list
// paths. Keep a short prefix of other callers so a changed call graph is
// visible without flooding the client trace during startup.
const bool selectorCaller =
(callerRva >= 0x03011740u && callerRva <= 0x03011790u) ||
(callerRva >= 0x03012720u && callerRva <= 0x03012770u);
if (!selectorCaller && callNumber > 64) return;
char line[384] = {};
sprintf_s(line,
"new-client dungeon-selector-probe call=%ld self=0x%08X "
"caller_rva=0x%08X state_2fc=%u manager=0x%08X\r\n",
callNumber, static_cast<unsigned int>(self), callerRva,
state2fc, static_cast<unsigned int>(manager));
WriteLogLine(line);
}
bool CurrentProbeBytesMatch(const unsigned char* target,
const unsigned char* expected, size_t length);
void LogCurrentOwnerProbe(unsigned int kind, uintptr_t self,
uintptr_t caller, uintptr_t originalStack)
{
LONG callNumber = InterlockedIncrement(&g_currentOwnerProbeCallCount);
if (callNumber > kCurrentOwnerProbeMaxLoggedCalls) return;
const uintptr_t base = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
const unsigned int callerRva = (base && caller >= base)
? static_cast<unsigned int>(caller - base) : 0;
unsigned int arg0 = 0xFFFFFFFFu;
unsigned int arg1 = 0xFFFFFFFFu;
unsigned int arg2 = 0xFFFFFFFFu;
__try {
if (originalStack) {
arg0 = *reinterpret_cast<volatile unsigned int*>(originalStack + 4);
arg1 = *reinterpret_cast<volatile unsigned int*>(originalStack + 8);
arg2 = *reinterpret_cast<volatile unsigned int*>(originalStack + 0xC);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
arg0 = arg1 = arg2 = 0xFFFFFFFEu;
}
// Keep the normal call prefix short, but retain every call carrying the
// legacy warning owner/message values so the click window is unambiguous.
const bool suspicious = arg0 == 0x49u || arg1 == 0x49u || arg2 == 0x49u ||
arg0 == 0x73u || arg1 == 0x73u || arg2 == 0x73u ||
arg0 == 0xF6u || arg1 == 0xF6u || arg2 == 0xF6u;
if (!suspicious && callNumber > 96) return;
char line[448] = {};
sprintf_s(line,
"new-client dungeon-owner-probe kind=%u call=%ld self=0x%08X "
"caller_rva=0x%08X arg0=0x%08X arg1=0x%08X arg2=0x%08X "
"suspicious=%u\r\n",
kind, callNumber, static_cast<unsigned int>(self), callerRva,
arg0, arg1, arg2, suspicious ? 1u : 0u);
WriteLogLine(line);
}
__declspec(naked) void ProxyCurrentOwnerManagerProbe()
{
__asm {
pushfd
pushad
mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd)
mov eax, [ecx + 4] // native return address at original ESP
mov edx, [esp + 18h] // saved native ECX (this)
add ecx, 4 // original ESP, points at native return address
push ecx
push eax
push edx
push 1
call LogCurrentOwnerProbe
add esp, 10h
popad
popfd
jmp dword ptr [g_currentOwnerManagerProbeOriginal]
}
}
__declspec(naked) void ProxyCurrentNoticeBuilderProbe()
{
__asm {
pushfd
pushad
mov ecx, [esp + 0Ch] // pushad saved ESP (after pushfd)
mov eax, [ecx + 4] // native return address at original ESP
mov edx, [esp + 18h] // saved native ECX (this)
add ecx, 4 // original ESP, points at native return address
push ecx
push eax
push edx
push 2
call LogCurrentOwnerProbe
add esp, 10h
popad
popfd
jmp dword ptr [g_currentNoticeBuilderProbeOriginal]
}
}
bool InstallCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected,
size_t patchLength, void* detour, void** original, const char* label,
volatile LONG* installed)
{
if (!g_currentProbeBase || !expected || !detour || !original ||
!installed || patchLength < 5) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + rva);
if (!CurrentProbeBytesMatch(target, expected, patchLength)) {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe skipped label=%s "
"reason=prologue-mismatch rva=0x%08X\r\n",
label ? label : "unknown", static_cast<unsigned int>(rva));
WriteLogLine(line);
return false;
}
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
nullptr, patchLength + 5, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!trampoline) return false;
memcpy(trampoline, target, patchLength);
trampoline[patchLength] = 0xE9;
*reinterpret_cast<int32_t*>(trampoline + patchLength + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(target + patchLength) -
reinterpret_cast<intptr_t>(trampoline + patchLength + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
VirtualFree(trampoline, 0, MEM_RELEASE);
return false;
}
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
reinterpret_cast<intptr_t>(target + 5));
memcpy(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); i < patchLength; ++i) target[i] = 0x90;
DWORD ignoredProtection = 0;
VirtualProtect(target, patchLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, patchLength);
FlushInstructionCache(GetCurrentProcess(), trampoline, patchLength + 5);
*original = trampoline;
InterlockedExchange(installed, 1);
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe installed label=%s "
"rva=0x%08X patch_length=%u mode=observe-only\r\n",
label ? label : "unknown", static_cast<unsigned int>(rva),
static_cast<unsigned int>(patchLength));
WriteLogLine(line);
return true;
}
void RestoreCurrentOwnerProbe(uintptr_t rva, const unsigned char* expected,
size_t patchLength, void* detour, void** original, const char* label,
volatile LONG* installed)
{
if (!g_currentProbeBase || !expected || !detour || !original ||
!installed || InterlockedCompareExchange(installed, 0, 0) == 0) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + rva);
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(detour) -
reinterpret_cast<intptr_t>(target + 5));
bool ours = CurrentProbeBytesMatch(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); ours && i < patchLength; ++i) {
ours = target[i] == 0x90;
}
if (ours) {
DWORD oldProtection = 0;
if (VirtualProtect(target, patchLength, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
memcpy(target, expected, patchLength);
DWORD ignoredProtection = 0;
VirtualProtect(target, patchLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, patchLength);
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe restored label=%s "
"rva=0x%08X\r\n", label ? label : "unknown",
static_cast<unsigned int>(rva));
WriteLogLine(line);
}
} else {
char line[256] = {};
sprintf_s(line,
"new-client dungeon-owner-probe restore-skipped label=%s "
"reason=target-changed rva=0x%08X\r\n", label ? label : "unknown",
static_cast<unsigned int>(rva));
WriteLogLine(line);
}
if (*original) {
VirtualFree(*original, 0, MEM_RELEASE);
*original = nullptr;
}
InterlockedExchange(installed, 0);
}
// Preserve all native registers, flags, stack cleanup and the original
// return address. The stolen prologue contains no relative instruction, so
// the generic local trampoline can resume at target+10 safely.
__declspec(naked) void ProxyCurrentSelectorProbe()
{
__asm {
pushfd
pushad
mov eax, [esp + 24h] // native return address
mov edx, [esp + 18h] // saved native ECX (this)
push eax
push edx
call LogCurrentSelectorProbe
add esp, 8
popad
popfd
jmp dword ptr [g_currentSelectorProbeOriginal]
}
}
bool CurrentProbeBytesMatch(const unsigned char* target,
const unsigned char* expected, size_t length)
{
if (!target || !expected || length == 0) return false;
__try {
return memcmp(target, expected, length) == 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool InstallCurrentSelectorProbe()
{
if (!g_currentProbeBase) return false;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + kCurrentSelectorProbeRva);
if (!CurrentProbeBytesMatch(target, kCurrentSelectorProbePrologue,
kCurrentSelectorProbePatchLength)) {
WriteLogLine(
"new-client dungeon-selector-probe skipped: prologue-mismatch "
"rva=0300BB80 expected=558BEC6AFF68F8442704\r\n");
return false;
}
unsigned char* trampoline = static_cast<unsigned char*>(VirtualAlloc(
nullptr, kCurrentSelectorProbePatchLength + 5,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
if (!trampoline) {
WriteLogLine(
"new-client dungeon-selector-probe failed: trampoline-allocation\r\n");
return false;
}
memcpy(trampoline, target, kCurrentSelectorProbePatchLength);
trampoline[kCurrentSelectorProbePatchLength] = 0xE9;
*reinterpret_cast<int32_t*>(trampoline +
kCurrentSelectorProbePatchLength + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(target + kCurrentSelectorProbePatchLength) -
reinterpret_cast<intptr_t>(trampoline +
kCurrentSelectorProbePatchLength + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(target, kCurrentSelectorProbePatchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
VirtualFree(trampoline, 0, MEM_RELEASE);
WriteLogLine(
"new-client dungeon-selector-probe failed: virtualprotect\r\n");
return false;
}
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
reinterpret_cast<intptr_t>(target + 5));
memcpy(target, jump, sizeof(jump));
for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) {
target[i] = 0x90;
}
DWORD ignoredProtection = 0;
VirtualProtect(target, kCurrentSelectorProbePatchLength,
oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target,
kCurrentSelectorProbePatchLength);
FlushInstructionCache(GetCurrentProcess(), trampoline,
kCurrentSelectorProbePatchLength + 5);
g_currentSelectorProbeOriginal = trampoline;
InterlockedExchange(&g_currentProbeInstalled, 1);
WriteLogLine(
"new-client dungeon-selector-probe installed rva=0300BB80 "
"patch_length=10 mode=observe-only\r\n");
return true;
}
bool CurrentProbeDetourMatches(const unsigned char* target)
{
if (!target) return false;
unsigned char jump[5] = { 0xE9 };
*reinterpret_cast<int32_t*>(jump + 1) = static_cast<int32_t>(
reinterpret_cast<intptr_t>(&ProxyCurrentSelectorProbe) -
reinterpret_cast<intptr_t>(target + 5));
if (!CurrentProbeBytesMatch(target, jump, sizeof(jump))) return false;
for (size_t i = sizeof(jump); i < kCurrentSelectorProbePatchLength; ++i) {
if (target[i] != 0x90) return false;
}
return true;
}
void RestoreCurrentSelectorProbe()
{
if (InterlockedCompareExchange(&g_currentProbeInstalled, 0, 0) == 0 ||
!g_currentProbeBase) return;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_currentProbeBase + kCurrentSelectorProbeRva);
if (CurrentProbeDetourMatches(target)) {
DWORD oldProtection = 0;
if (VirtualProtect(target, kCurrentSelectorProbePatchLength,
PAGE_EXECUTE_READWRITE, &oldProtection)) {
memcpy(target, kCurrentSelectorProbePrologue,
kCurrentSelectorProbePatchLength);
DWORD ignoredProtection = 0;
VirtualProtect(target, kCurrentSelectorProbePatchLength,
oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target,
kCurrentSelectorProbePatchLength);
WriteLogLine(
"new-client dungeon-selector-probe restored rva=0300BB80\r\n");
}
} else {
WriteLogLine(
"new-client dungeon-selector-probe restore-skipped target-changed\r\n");
}
if (g_currentSelectorProbeOriginal) {
VirtualFree(g_currentSelectorProbeOriginal, 0, MEM_RELEASE);
g_currentSelectorProbeOriginal = nullptr;
}
InterlockedExchange(&g_currentProbeInstalled, 0);
}
DWORD WINAPI CurrentSelectorProbeWorker(LPVOID)
{
g_currentProbeBase = reinterpret_cast<uintptr_t>(
GetModuleHandleW(nullptr));
if (!g_currentProbeBase) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_currentProbeStop, 0, 0) == 0) {
InstallCurrentSelectorProbe();
InstallCurrentOwnerProbe(
kCurrentOwnerManagerProbeRva,
kCurrentOwnerManagerProbePrologue,
kCurrentOwnerManagerProbePatchLength,
&ProxyCurrentOwnerManagerProbe,
&g_currentOwnerManagerProbeOriginal,
"owner-manager-008A1170",
&g_currentOwnerProbeInstalled);
InstallCurrentOwnerProbe(
kCurrentNoticeBuilderProbeRva,
kCurrentNoticeBuilderProbePrologue,
kCurrentNoticeBuilderProbePatchLength,
&ProxyCurrentNoticeBuilderProbe,
&g_currentNoticeBuilderProbeOriginal,
"notice-builder-027B65B0",
&g_currentNoticeProbeInstalled);
}
return 0;
}
}
void StartDungeonSkillGateFix(HMODULE module)
{
if (g_worker) return;
g_selfModule = module;
InterlockedExchange(&g_stop, 0);
g_worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr);
}
void StopDungeonSkillGateFix()
{
InterlockedExchange(&g_stop, 1);
if (g_worker) {
WaitForSingleObject(g_worker, 1500);
CloseHandle(g_worker);
g_worker = nullptr;
}
RestoreBypass();
}
void StartCurrentDungeonSkillGateFix(HMODULE module)
{
if (g_currentSkillGateWorker) return;
g_currentSkillGateModule = module;
InterlockedExchange(&g_currentSkillGateStop, 0);
g_currentSkillGateWorker = CreateThread(nullptr, 0,
InstallCurrentSkillGateWorker, nullptr, 0, nullptr);
if (!g_currentSkillGateWorker) {
WriteLogLine(
"new-client dungeon-skill-gate failed: worker-create\r\n");
}
}
void StopCurrentDungeonSkillGateFix()
{
InterlockedExchange(&g_currentSkillGateStop, 1);
if (g_currentSkillGateWorker) {
WaitForSingleObject(g_currentSkillGateWorker, 1500);
CloseHandle(g_currentSkillGateWorker);
g_currentSkillGateWorker = nullptr;
}
RestoreCurrentSkillGateBypass();
}
void StartCurrentDungeonSelectorProbe(HMODULE module)
{
if (g_currentProbeWorker) return;
g_currentProbeModule = module;
InterlockedExchange(&g_currentProbeStop, 0);
g_currentProbeWorker = CreateThread(nullptr, 0,
CurrentSelectorProbeWorker, nullptr, 0, nullptr);
if (!g_currentProbeWorker) {
WriteLogLine(
"new-client dungeon-selector-probe failed: worker-create\r\n");
}
}
void StopCurrentDungeonSelectorProbe()
{
InterlockedExchange(&g_currentProbeStop, 1);
if (g_currentProbeWorker) {
WaitForSingleObject(g_currentProbeWorker, 1500);
CloseHandle(g_currentProbeWorker);
g_currentProbeWorker = nullptr;
}
RestoreCurrentSelectorProbe();
RestoreCurrentOwnerProbe(
kCurrentOwnerManagerProbeRva,
kCurrentOwnerManagerProbePrologue,
kCurrentOwnerManagerProbePatchLength,
&ProxyCurrentOwnerManagerProbe,
&g_currentOwnerManagerProbeOriginal,
"owner-manager-008A1170",
&g_currentOwnerProbeInstalled);
RestoreCurrentOwnerProbe(
kCurrentNoticeBuilderProbeRva,
kCurrentNoticeBuilderProbePrologue,
kCurrentNoticeBuilderProbePatchLength,
&ProxyCurrentNoticeBuilderProbe,
&g_currentNoticeBuilderProbeOriginal,
"notice-builder-027B65B0",
&g_currentNoticeProbeInstalled);
}
@@ -0,0 +1,22 @@
#pragma once
#include <windows.h>
// Bypass only the current NoPack client-side "initial skill state" dungeon
// selector precheck. The native skill-state probe still runs; this fix only
// forces its post-test branch onto the normal selector continuation path.
void StartDungeonSkillGateFix(HMODULE module);
void StopDungeonSkillGateFix();
// Current DB4 client: skip only the dungeon-selector branch that displays the
// "skill is in initial state" notice when the native selector check fails.
// This is a version-locked two-byte branch patch; the shared state predicate
// and all other selector gates remain native.
void StartCurrentDungeonSkillGateFix(HMODULE module);
void StopCurrentDungeonSkillGateFix();
// Read-only probe for the current DB4 client. It records calls into the
// selector/list state routine and then executes the stolen native prologue
// unchanged. This is deliberately separate from the legacy 74 2D bypass.
void StartCurrentDungeonSelectorProbe(HMODULE module);
void StopCurrentDungeonSelectorProbe();
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <stdint.h>
#include <string.h>
namespace dnf90_expert_job
{
// Current EXE 03A4C1A0 returns actor+18. Mode3 writes state+34C for
// personal info, but menu action4 / 00FF1FF0 reads state+368 instead.
constexpr size_t kPersonalInfoTypeOffset = 0x364;
constexpr size_t kFunctionTypeOffset = 0x380;
struct PreviousTypes
{
uint32_t personalInfo;
uint32_t function;
};
// The caller resolves the native current actor, validates the server op205
// type, and retains its existing SEH/actor identity boundary. This helper
// only mirrors that exact type; it does not grant or persist a profession.
inline bool MirrorType(void* actor, uint32_t type, PreviousTypes& previous)
{
if (!actor || type > 4) return false;
unsigned char* bytes = static_cast<unsigned char*>(actor);
memcpy(&previous.personalInfo, bytes + kPersonalInfoTypeOffset, sizeof(type));
memcpy(&previous.function, bytes + kFunctionTypeOffset, sizeof(type));
memcpy(bytes + kPersonalInfoTypeOffset, &type, sizeof(type));
memcpy(bytes + kFunctionTypeOffset, &type, sizeof(type));
return true;
}
}
@@ -0,0 +1,122 @@
#pragma once
namespace dnf90_first_login_recovery {
constexpr long kMaxFirstLoginRecoveryAttempts = 2;
inline bool IsFirstLoginServerProgressMessage(int messageType) {
return messageType == 1; // ENUM_CMDPACKET_LOGIN
}
inline bool IsFirstLoginClientProgressMessage(int messageType) {
switch (messageType) {
case 171: // ENUM_CMDPACKET_CLIENT_SPEC_STATISTIC
case 476: // ENUM_CMDPACKET_COMPATIBILITY_INDEX
case 1320: // ENUM_CMDPACKET_LOG_DOMAIN_CONNECT
case 1262: // ENUM_CMDPACKET_LOG_CONNECT_PROCESS
case 593: // ENUM_CMDPACKET_SECURITY_STATUS
case 8: // ENUM_CMDPACKET_GET_USERINFO
return true;
default:
return false;
}
}
// The auto-connect timer can run inside the native startup message pump.
// Opening a socket while the packet processor is uninitialized loses LOGIN:
// native 02261E30 returns 80000001, but 03456304 still consumes the packet.
constexpr unsigned int kPacketProcessorPollMs = 50;
constexpr unsigned long long kPacketProcessorWaitLimitMs = 30000;
enum class PacketProcessorReadiness { NotReady, Ready, ReadFailed };
enum class AutoConnectAction { Ignore, Wait, Connect, TimedOut, ReadFailed };
struct AutoConnectReadiness {
long generation = 0;
unsigned long long startedAt = 0;
bool pending = false;
bool waited = false;
void Begin(long newGeneration, unsigned long long now) {
generation = newGeneration;
startedAt = now;
pending = true;
waited = false;
}
void Cancel() { pending = false; }
AutoConnectAction Poll(long currentGeneration, unsigned long long now,
PacketProcessorReadiness readiness) {
if (!pending || generation != currentGeneration) {
return AutoConnectAction::Ignore;
}
if (readiness == PacketProcessorReadiness::ReadFailed) {
pending = false;
return AutoConnectAction::ReadFailed;
}
if (readiness == PacketProcessorReadiness::Ready) {
pending = false; // Claim exactly one connect for this directory.
return AutoConnectAction::Connect;
}
waited = true;
if (now - startedAt >= kPacketProcessorWaitLimitMs) {
pending = false;
return AutoConnectAction::TimedOut;
}
return AutoConnectAction::Wait;
}
};
struct WatchdogState {
long watchdogGeneration;
long currentGeneration;
bool active;
bool serverDataSeen;
bool clientDataSeen;
long recoveryCount;
};
inline bool ShouldRecover(const WatchdogState& state) {
// Native ChannelConnect success is only a transport candidate. The first
// login is healthy once the client emits any proved post-login progress.
// serverDataSeen is diagnostic only: a stale native socket can prevent the
// receive hook from observing LOGIN even when the server already sent it.
return state.active &&
state.recoveryCount < kMaxFirstLoginRecoveryAttempts &&
state.watchdogGeneration > 0 &&
state.currentGeneration == state.watchdogGeneration &&
!state.clientDataSeen;
}
inline bool ShouldRecoverAfterRejectedRetry(
long initialSuccessGeneration,
long retryGeneration,
bool clientDataSeen,
long recoveryCount,
bool retryConnected) {
return initialSuccessGeneration > 0 &&
retryGeneration > initialSuccessGeneration &&
!clientDataSeen &&
recoveryCount < kMaxFirstLoginRecoveryAttempts &&
!retryConnected;
}
inline bool ShouldForceReleaseStaleSocket(
bool residentResetSucceeded,
bool socketPresent) {
return residentResetSucceeded && socketPresent;
}
// A proved login must not permanently disable recovery after disconnect.
// Only a rejected later directory connect with no live transport may reset;
// a successful connect or a still-owned socket always remains native-owned.
inline bool ShouldRecoverDisconnectedRetry(long initialGeneration,
long retryGeneration, bool clientProgress, bool connected,
bool transportReadable, bool socketAbsent, long attempts) {
return initialGeneration > 0 && retryGeneration > initialGeneration &&
clientProgress && !connected && transportReadable && socketAbsent &&
attempts < kMaxFirstLoginRecoveryAttempts;
}
} // namespace dnf90_first_login_recovery
+162
View File
@@ -0,0 +1,162 @@
#include "90CN_op3_parent_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
namespace
{
constexpr uintptr_t kOp3ParentCallRva = 0x01988B9F; // 01D88B9F
constexpr uintptr_t kParentFunctionRva = 0x021B69E0; // 025B69E0
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
uintptr_t g_parentFunction = 0;
uintptr_t g_continueAfterParentCall = 0;
volatile LONG g_bypassSequence = 0;
void BuildLogPath(wchar_t* output, size_t outputCount)
{
if (!output || outputCount == 0) return;
output[0] = L'\0';
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, output, static_cast<DWORD>(outputCount))
: 0;
if (!length || length >= outputCount) {
wcscpy_s(output, outputCount, L"90CN_fix.log");
return;
}
wchar_t* slash = wcsrchr(output, L'\\');
wchar_t* slash2 = wcsrchr(output, L'/');
if (slash2 && (!slash || slash2 > slash)) slash = slash2;
if (slash) {
slash[1] = L'\0';
wcscat_s(output, outputCount, L"90CN_fix.log");
}
else {
wcscpy_s(output, outputCount, L"90CN_fix.log");
}
}
void WriteLogLine(const char* text)
{
if (!text) return;
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
HANDLE file = CreateFileW(path, FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(file, text, static_cast<DWORD>(strlen(text)), &written, nullptr);
CloseHandle(file);
}
void __stdcall RecordNullBypass(uintptr_t actor)
{
LONG sequence = InterlockedIncrement(&g_bypassSequence);
// Prevent pathological log growth while retaining enough evidence for support.
if (sequence > 64) return;
SYSTEMTIME now = {};
GetLocalTime(&now);
char line[512] = {};
sprintf_s(line,
"op3-parent-readiness-fix seq=%ld time=%04u-%02u-%02u %02u:%02u:%02u.%03u "
"tid=%lu actor=%08X action=bypass-null-parent continue=01D88BA4\r\n",
sequence,
now.wYear, now.wMonth, now.wDay,
now.wHour, now.wMinute, now.wSecond, now.wMilliseconds,
GetCurrentThreadId(), static_cast<unsigned int>(actor));
WriteLogLine(line);
}
// Replaces only the single CALL at 01D88B9F.
// Normal path is untouched: restore incoming EFLAGS and tail-jump to 025B69E0.
// Null path skips the known-invalid call, removes CALL return address + 2 args
// (12 bytes), and resumes at the original continuation 01D88BA4.
__declspec(naked) void ProxyOp3ParentCall()
{
__asm {
pushfd
test ecx, ecx
jz parent_is_null
popfd
jmp dword ptr [g_parentFunction]
parent_is_null:
popfd
pushfd
pushad
push esi
call RecordNullBypass
popad
popfd
add esp, 0Ch
jmp dword ptr [g_continueAfterParentCall]
}
}
DWORD WINAPI InstallOp3ParentReadinessFix(LPVOID)
{
HMODULE executable = GetModuleHandleW(nullptr);
g_exeBase = reinterpret_cast<uintptr_t>(executable);
if (!g_exeBase) return 0;
unsigned char* callSite = reinterpret_cast<unsigned char*>(
g_exeBase + kOp3ParentCallRva);
g_parentFunction = g_exeBase + kParentFunctionRva;
g_continueAfterParentCall = reinterpret_cast<uintptr_t>(callSite + 5);
bool verified = false;
uintptr_t observedTarget = 0;
__try {
if (callSite[0] == 0xE8) {
LONG currentRelative = *reinterpret_cast<LONG*>(callSite + 1);
observedTarget = reinterpret_cast<uintptr_t>(callSite + 5) + currentRelative;
verified = observedTarget == g_parentFunction;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
verified = false;
}
if (!verified) {
char line[512] = {};
sprintf_s(line,
"op3-parent-readiness-fix install_failed site=%08X opcode=%02X observed=%08X expected=%08X\r\n",
static_cast<unsigned int>(reinterpret_cast<uintptr_t>(callSite)),
static_cast<unsigned int>(callSite[0]),
static_cast<unsigned int>(observedTarget),
static_cast<unsigned int>(g_parentFunction));
WriteLogLine(line);
return 0;
}
const uintptr_t replacement = reinterpret_cast<uintptr_t>(&ProxyOp3ParentCall);
LONG replacementRelative = static_cast<LONG>(
replacement - reinterpret_cast<uintptr_t>(callSite + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(callSite, 5, PAGE_EXECUTE_READWRITE, &oldProtection)) {
WriteLogLine("op3-parent-readiness-fix install_failed reason=VirtualProtect\r\n");
return 0;
}
*reinterpret_cast<LONG*>(callSite + 1) = replacementRelative;
DWORD ignoredProtection = 0;
VirtualProtect(callSite, 5, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), callSite, 5);
WriteLogLine("op3-parent-readiness-fix installed site=01D88B9F native=025B69E0 continue=01D88BA4\r\n");
return 0;
}
}
void StartOp3ParentReadinessFix(HMODULE module)
{
g_selfModule = module;
HANDLE worker = CreateThread(nullptr, 0, InstallOp3ParentReadinessFix, nullptr, 0, nullptr);
if (worker) CloseHandle(worker);
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <windows.h>
void StartOp3ParentReadinessFix(HMODULE module);
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace dnf90_premium_clock {
constexpr size_t kNativeBodySize = 77; // success:u8, action:u16, blob74
constexpr size_t kServerHeaderSize = 16;
constexpr size_t kClockTrailerSize = 8;
constexpr size_t kPacketSize = kServerHeaderSize + kNativeBodySize + kClockTrailerSize;
inline uint32_t ReadU32(const unsigned char* bytes) {
return uint32_t(bytes[0]) | (uint32_t(bytes[1]) << 8) |
(uint32_t(bytes[2]) << 16) | (uint32_t(bytes[3]) << 24);
}
// Only our exact marked server16/class1/op903 action1 packet is eligible.
// The unchanged native prefix remains owned by the original handler.
inline bool Decode(const unsigned char* bytes, size_t available, int* serverUnix) {
if (!bytes || !serverUnix || available < kPacketSize ||
bytes[0] != 1 || bytes[1] != 0x87 || bytes[2] != 3 ||
ReadU32(bytes + 3) != kPacketSize ||
bytes[16] != 1 || bytes[17] != 1 || bytes[18] != 0) return false;
const unsigned char* trailer = bytes + kServerHeaderSize + kNativeBodySize;
if (trailer[0] != 'T' || trailer[1] != 'I' ||
trailer[2] != 'M' || trailer[3] != 'E') return false;
uint32_t timestamp = ReadU32(trailer + 4);
// The native clock uses signed time32. Refuse zero, wrapped and out-of-era
// timestamps rather than writing arbitrary values to native globals.
if (timestamp < 946684800u || timestamp > 0x7fffffffu) return false;
*serverUnix = static_cast<int>(timestamp);
return true;
}
} // namespace dnf90_premium_clock
+185
View File
@@ -0,0 +1,185 @@
#include "90CN_selected_page_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
namespace
{
// The cold-login failure occurs at the first field access inside
// sub_17D6BE0 when it is entered with ECX/this == NULL. Guard that function
// boundary itself instead of aborting the surrounding class1/op4 dispatcher;
// the latter owns additional HUD/state initialization that must still run.
constexpr uintptr_t kSelectedPageApplyRva = 0x013D6BE0; // 017D6BE0
constexpr size_t kHookLength = 5;
const unsigned char kSelectedPageApplyPrologue[kHookLength] = {
0x55, 0x8B, 0xEC, 0x6A, 0xFF
};
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
void* g_originalSelectedPageApply = nullptr;
volatile LONG g_bypassSequence = 0;
typedef void (__thiscall* SelectedPageApplyFn)(void*);
void BuildLogPath(wchar_t* output, size_t outputCount)
{
if (!output || outputCount == 0) return;
output[0] = L'\0';
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, output, static_cast<DWORD>(outputCount))
: 0;
if (!length || length >= outputCount) {
wcscpy_s(output, outputCount, L"90CN_fix.log");
return;
}
wchar_t* slash = wcsrchr(output, L'\\');
wchar_t* slash2 = wcsrchr(output, L'/');
if (slash2 && (!slash || slash2 > slash)) slash = slash2;
if (slash) {
slash[1] = L'\0';
wcscat_s(output, outputCount, L"90CN_fix.log");
}
else {
wcscpy_s(output, outputCount, L"90CN_fix.log");
}
}
void WriteLogLine(const char* text)
{
if (!text) return;
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
HANDLE file = CreateFileW(path, FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE) return;
DWORD written = 0;
WriteFile(file, text, static_cast<DWORD>(strlen(text)), &written, nullptr);
CloseHandle(file);
}
void RecordNullBypass()
{
LONG sequence = InterlockedIncrement(&g_bypassSequence);
if (sequence > 64) return;
SYSTEMTIME now = {};
GetLocalTime(&now);
char line[512] = {};
sprintf_s(line,
"selected-page-readiness-fix seq=%ld time=%04u-%02u-%02u %02u:%02u:%02u.%03u "
"tid=%lu action=skip-null-this target=017D6BE0 boundary=selected-page-entry\r\n",
sequence,
now.wYear, now.wMonth, now.wDay,
now.wHour, now.wMinute, now.wSecond, now.wMilliseconds,
GetCurrentThreadId());
WriteLogLine(line);
}
void __fastcall ProxySelectedPageApply(void* self, void* /*unused*/)
{
if (!self) {
RecordNullBypass();
return;
}
SelectedPageApplyFn original = reinterpret_cast<SelectedPageApplyFn>(
g_originalSelectedPageApply);
if (original) original(self);
}
bool BuildTrampoline(unsigned char* target)
{
const size_t trampolineLength = kHookLength + 5;
unsigned char* trampoline = reinterpret_cast<unsigned char*>(VirtualAlloc(
nullptr, trampolineLength, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!trampoline) return false;
memcpy(trampoline, target, kHookLength);
trampoline[kHookLength] = 0xE9;
*reinterpret_cast<LONG*>(trampoline + kHookLength + 1) =
static_cast<LONG>(
reinterpret_cast<uintptr_t>(target + kHookLength) -
reinterpret_cast<uintptr_t>(trampoline + trampolineLength));
FlushInstructionCache(GetCurrentProcess(), trampoline, trampolineLength);
g_originalSelectedPageApply = trampoline;
return true;
}
DWORD WINAPI InstallSelectedPageReadinessFix(LPVOID)
{
HMODULE executable = GetModuleHandleW(nullptr);
g_exeBase = reinterpret_cast<uintptr_t>(executable);
if (!g_exeBase || !g_selfModule) return 0;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kSelectedPageApplyRva);
bool verified = false;
unsigned char observed[kHookLength] = {};
__try {
memcpy(observed, target, sizeof(observed));
verified = memcmp(observed, kSelectedPageApplyPrologue,
sizeof(kSelectedPageApplyPrologue)) == 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
verified = false;
}
if (!verified) {
char line[512] = {};
sprintf_s(line,
"selected-page-readiness-fix install_failed target=017D6BE0 "
"observed=%02X%02X%02X%02X%02X expected=558BEC6AFF\r\n",
observed[0], observed[1], observed[2], observed[3], observed[4]);
WriteLogLine(line);
return 0;
}
if (!BuildTrampoline(target)) {
WriteLogLine(
"selected-page-readiness-fix install_failed reason=trampoline-allocation\r\n");
return 0;
}
const uintptr_t replacement = reinterpret_cast<uintptr_t>(
&ProxySelectedPageApply);
const LONG replacementRelative = static_cast<LONG>(
replacement - reinterpret_cast<uintptr_t>(target + kHookLength));
DWORD oldProtection = 0;
if (!VirtualProtect(target, kHookLength, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
VirtualFree(g_originalSelectedPageApply, 0, MEM_RELEASE);
g_originalSelectedPageApply = nullptr;
WriteLogLine(
"selected-page-readiness-fix install_failed reason=VirtualProtect\r\n");
return 0;
}
target[0] = 0xE9;
*reinterpret_cast<LONG*>(target + 1) = replacementRelative;
DWORD ignoredProtection = 0;
VirtualProtect(target, kHookLength, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, kHookLength);
WriteLogLine(
"selected-page-readiness-fix installed boundary=selected-page-entry "
"target=017D6BE0 scope=null-this-only preserves=class1-op4-continuation\r\n");
return 0;
}
}
void StartSelectedPageReadinessFix(HMODULE module)
{
g_selfModule = module;
HANDLE worker = CreateThread(
nullptr, 0, InstallSelectedPageReadinessFix, nullptr, 0, nullptr);
if (worker) CloseHandle(worker);
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <windows.h>
void StartSelectedPageReadinessFix(HMODULE module);
+413
View File
@@ -0,0 +1,413 @@
#include "90CN_seria_minimap.h"
#include "90CN_seria_room_policy.h"
#include <string.h>
namespace dnf90_seria {
static Logger LogLine;
static HMODULE g_image;
static volatile LONG g_ready;
static SRWLOCK g_lock = SRWLOCK_INIT;
static Lifecycle g_owner = {};
static void* g_original[8];
typedef uintptr_t (__thiscall* TownResourceLookup)(void*, int, int);
static TownResourceLookup g_titleTownLookup;
enum Kind { Setter, Bind, TownGet, Region, Draw, Marker, Rect };
static uintptr_t Va(uintptr_t address) { return reinterpret_cast<uintptr_t>(g_image) + address - 0x400000; }
// PUSHAD layout. Native argument words, register/flag/FPU/SSE state, return
// values and RET n cleanup are forwarded verbatim; no guessed C++ native ABI.
struct Registers { DWORD edi, esi, ebp, esp, ebx, edx, ecx, eax, flags; };
struct Display { Owner owner; uintptr_t ui, entry, marker; int region; };
struct Frame {
uintptr_t nativeStack, caller, self, esi;
Kind kind;
int town, area;
bool active;
Display display;
ULONGLONG entered;
};
struct PacketFrame { unsigned ticket; unsigned opcode; Notice notice; };
struct ThreadState {
Frame hooks[128]; unsigned depth;
PacketFrame packets[8]; unsigned packetDepth, nextTicket;
};
static __declspec(thread) ThreadState g_thread = {};
static bool ReadScene(Scene* scene) {
*scene = Scene();
__try {
uintptr_t root = *reinterpret_cast<uintptr_t*>(Va(0x051b1290));
if (!root) return false;
scene->world = *reinterpret_cast<uintptr_t*>(root + 0xb4);
if (!scene->world) return false;
scene->town = *reinterpret_cast<int*>(scene->world + 0x1e4);
uintptr_t area = *reinterpret_cast<uintptr_t*>(scene->world + 0x1e8);
scene->area = area ? *reinterpret_cast<int*>(area) : -1;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { *scene = Scene(); return false; }
}
static uintptr_t ReadUi() {
__try {
uintptr_t manager = *reinterpret_cast<uintptr_t*>(Va(0x051b2764));
return manager ? *reinterpret_cast<uintptr_t*>(manager + 0xb7f0) : 0;
} __except (EXCEPTION_EXECUTE_HANDLER) { return 0; }
}
static bool ReadDisplay(Display* value, bool binding) {
*value = Display();
Scene scene;
if (!ReadScene(&scene) || !Shared(scene)) return false;
AcquireSRWLockShared(&g_lock);
value->owner = g_owner.context;
ReleaseSRWLockShared(&g_lock);
if (value->owner.world != scene.world || !FindTown(value->owner.town)) return false;
value->ui = ReadUi();
if (!value->ui) return false;
__try {
value->entry = *reinterpret_cast<uintptr_t*>(value->ui + 0x2c);
if (!value->entry) return binding;
value->region = *reinterpret_cast<int*>(value->entry + 0x68);
value->marker = *reinterpret_cast<uintptr_t*>(value->entry + 0x64);
bool supported = value->marker && *reinterpret_cast<uintptr_t*>(value->marker) == Va(0x045a6dd8) &&
*reinterpret_cast<int*>(value->marker + 4) == 2 &&
*reinterpret_cast<WORD*>(value->marker + 0x5c) != 0xffff;
// Binding precedes native marker construction. A previous region's
// marker must not veto the FIRST resource selection on cold login.
return binding || supported;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
static bool SameDisplay(const Display& a, const Display& b) {
return a.owner.world == b.owner.world && a.owner.serial == b.owner.serial &&
a.owner.town == b.owner.town && a.entry == b.entry && a.marker == b.marker &&
b.region == b.owner.region;
}
static Frame* Current(Kind kind) {
for (unsigned n = g_thread.depth; n; --n) {
Frame* frame = g_thread.hooks + n - 1;
if (frame->kind != kind) continue;
// The innermost frame owns this call, even when it is ineligible.
return frame->active && GetTickCount64() - frame->entered < 1500 ? frame : 0;
}
return 0;
}
static bool ValidFrame(Frame* frame, Display* display) {
return frame && ReadDisplay(display, false) && SameDisplay(frame->display, *display);
}
static void BindPending() {
if (!g_thread.packetDepth) return;
PacketFrame* packet = g_thread.packets + g_thread.packetDepth - 1;
if (packet->opcode != 24) return;
Scene scene;
if (!ReadScene(&scene)) return;
AcquireSRWLockExclusive(&g_lock);
bool changed = g_owner.Bind(packet->notice, scene);
Owner owner = g_owner.context;
ReleaseSRWLockExclusive(&g_lock);
if (changed) LogLine("Seria minimap owner bound town=%d region=%d serial=%u (UI only)", owner.town, owner.region, owner.serial);
}
unsigned BeginPacket(int packet) {
if (!g_ready || !packet || g_thread.packetDepth >= 8) return 0;
unsigned opcode;
__try { opcode = *reinterpret_cast<unsigned short*>(packet + 1); }
__except (EXCEPTION_EXECUTE_HANDLER) { return 0; }
PacketFrame value = {};
value.opcode = opcode;
if (opcode == 24) ReadScene(&value.notice.before);
value.ticket = ++g_thread.nextTicket;
if (!value.ticket) value.ticket = ++g_thread.nextTicket;
g_thread.packets[g_thread.packetDepth++] = value;
return value.ticket;
}
void EndPacket(unsigned ticket) {
if (!ticket || !g_thread.packetDepth) return;
PacketFrame* value = g_thread.packets + g_thread.packetDepth - 1;
if (value->ticket != ticket) return;
if (value->opcode == 24) {
Scene after;
if (ReadScene(&after)) {
AcquireSRWLockExclusive(&g_lock);
g_owner.Finish(value->notice, after);
ReleaseSRWLockExclusive(&g_lock);
}
}
--g_thread.packetDepth;
}
static void ReturnThunk();
static void __cdecl Before(unsigned id, Registers* registers) {
if (!g_ready || id > Rect) return;
uintptr_t* stack = reinterpret_cast<uintptr_t*>(registers + 1);
// A native SEH unwind can bypass a return thunk. Discard only frames whose
// original stack was actually unwound; never reuse a stale marker scope.
while (g_thread.depth && *stack != reinterpret_cast<uintptr_t>(&ReturnThunk) &&
reinterpret_cast<uintptr_t>(stack) >= g_thread.hooks[g_thread.depth-1].nativeStack)
--g_thread.depth;
if (g_thread.depth == 128) return; // no return-address rewrite on overflow
Frame value = {};
value.kind = static_cast<Kind>(id);
value.nativeStack = reinterpret_cast<uintptr_t>(stack);
value.caller = *stack;
value.self = registers->ecx;
value.esi = registers->esi;
value.entered = GetTickCount64();
__try {
if (id == Setter || id == Region || id == Rect) value.town = static_cast<int>(stack[1]);
if (id == Rect) value.area = static_cast<int>(stack[2]);
} __except (EXCEPTION_EXECUTE_HANDLER) { return; }
if (id == Setter && value.caller == Va(0x01d82152) && g_thread.packetDepth &&
g_thread.packets[g_thread.packetDepth-1].opcode == 1375) {
Scene scene;
if (ReadScene(&scene) && scene.world == value.self) {
AcquireSRWLockExclusive(&g_lock);
g_owner.Seed(scene.world, value.town);
ReleaseSRWLockExclusive(&g_lock);
LogLine("Seria minimap explicit initial owner town=%d (native cache untouched)", value.town);
}
} else if (id == Bind) {
value.active = value.self && value.self == ReadUi();
if (value.active) BindPending(); // before the binder's FIRST town getter
} else if (id == Draw) {
value.active = ReadDisplay(&value.display, false) && value.self == value.display.entry &&
value.display.region == value.display.owner.region;
} else if (id == Marker) {
Frame* draw = Current(Draw);
value.active = ValidFrame(draw, &value.display) && value.self == value.display.marker;
}
g_thread.hooks[g_thread.depth++] = value;
*stack = reinterpret_cast<uintptr_t>(&ReturnThunk);
}
// Synthetic descriptors are DLL-owned UI data, not a native scene/PVF object.
struct Descriptor { int town, area; int* geometry; int rect[4]; };
static Descriptor g_rects[sizeof(kGlyphs)/sizeof(kGlyphs[0])];
static uintptr_t DescriptorFor(const GlyphRow* glyph) {
return glyph ? reinterpret_cast<uintptr_t>(g_rects + (glyph - kGlyphs)) : 0;
}
static uintptr_t ProjectRect(const Frame& frame, uintptr_t original) {
Display state;
if (frame.caller != Va(0x0224e083) || frame.town != 38 || frame.area != 1 ||
!ValidFrame(Current(Marker), &state) || frame.self != state.entry) return original;
const GlyphRow* glyph = 0;
__try {
const int* geometry = original ? *reinterpret_cast<int**>(original + 8) : 0;
glyph = BorrowGlyph(state.owner.town, original == 0, geometry);
} __except (EXCEPTION_EXECUTE_HANDLER) { return original; }
return glyph ? DescriptorFor(glyph) : original;
}
static void __cdecl After(Registers* registers) {
// Return target resides in the reserved word ABOVE saved flags. Popping
// this word with RET preserves every native output register and ESP.
uintptr_t* target = reinterpret_cast<uintptr_t*>(registers + 1);
if (!g_thread.depth) {
// A missing target is an invariant violation, not a recoverable UI
// state. It cannot be produced by disabled/overflow hooks (no rewrite).
RaiseFailFastException(0, 0, 0);
return;
}
Frame frame = g_thread.hooks[--g_thread.depth];
*target = frame.caller;
if (!g_ready) return;
Display state;
if (frame.kind == TownGet && registers->eax == 38 &&
ReadDisplay(&state, frame.caller == Va(0x01c987f6) || frame.caller == Va(0x01c9bc5b)) &&
frame.self == state.owner.world) {
Frame* bind = Current(Bind);
bool allowed = (frame.caller == Va(0x01c987f6) && bind && bind->self == state.ui) ||
frame.caller == Va(0x01c9bc5b) ||
(frame.caller == Va(0x022478bd) && frame.esi == state.marker && ValidFrame(Current(Draw), &state));
if (allowed) registers->eax = state.owner.town;
} else if (frame.kind == Region && frame.town == 38 && registers->eax == 18 &&
frame.caller == Va(0x0223c4fd) && ValidFrame(Current(Marker), &state)) {
registers->eax = state.owner.region;
} else if (frame.kind == Rect) {
registers->eax = static_cast<DWORD>(ProjectRect(frame, registers->eax));
}
}
// sub_3411790 binds the minimap, then independently copies the REAL town
// resource's name at 3412282. Its room38 name bypasses scene_town_get entirely.
// Redirect only that CALL's string source; native copy/destruction still owns
// the value, and the scene, town resource and global string copier stay intact.
static void __cdecl BeforeTitleCopy(Registers* registers) {
if (!g_ready || !g_titleTownLookup || !g_thread.packetDepth ||
g_thread.packets[g_thread.packetDepth-1].opcode != 24) return;
uintptr_t* stack = reinterpret_cast<uintptr_t*>(registers + 1);
if (*stack != Va(0x03412287)) return;
Display state;
if (!ReadDisplay(&state, true) || state.region != state.owner.region ||
registers->edi != state.owner.world || state.owner.town == 38) return;
__try {
// The guarded caller takes this exact source from its scene resource.
uintptr_t realTown = *reinterpret_cast<uintptr_t*>(registers->ebp - 0x24);
if (!realTown || stack[1] != realTown + 4) return;
uintptr_t loader = *reinterpret_cast<uintptr_t*>(Va(0x051b26d8));
// This singleton is constructed with town resource type 12 at
// 22938d1..22938df; the generic lookup must not parse another type.
if (!loader || *reinterpret_cast<int*>(loader + 0x22c) != 12) return;
// Same native cache-enabled resource lookup used by scene entry.
uintptr_t town = g_titleTownLookup(reinterpret_cast<void*>(loader), state.owner.town, 1);
if (!town) return;
uintptr_t name = town + 4;
unsigned length = *reinterpret_cast<unsigned*>(name + 0x14);
unsigned capacity = *reinterpret_cast<unsigned*>(name + 0x18);
if (!length || length > capacity) return;
Display current;
if (!ReadDisplay(&current, true) || !SameDisplay(state, current)) return;
stack[1] = name;
LogLine("Seria minimap title projected town=%d region=%d serial=%u (native resource name)",
state.owner.town, state.owner.region, state.owner.serial);
} __except (EXCEPTION_EXECUTE_HANDLER) { return; }
}
// Preserve flags and the complete x87/MMX/SSE state around our observer, not
// merely callee-saved integer registers. FX area is 16-byte aligned even when
// the game's incoming stack is not. The native function sees its exact stack.
#define SAVE_OBSERVER __asm { pushfd } __asm { pushad } __asm { sub esp, 528 } \
__asm { lea eax, [esp+15] } __asm { and eax, -16 } __asm { fxsave [eax] }
#define RESTORE_OBSERVER __asm { lea eax, [esp+15] } __asm { and eax, -16 } \
__asm { fxrstor [eax] } __asm { add esp, 528 } __asm { popad } __asm { popfd }
#define ENTRY(name, id, offset) static void __declspec(naked) name() { \
SAVE_OBSERVER __asm { lea eax, [esp+528] } __asm { push eax } __asm { push id } \
__asm { call Before } __asm { add esp, 8 } RESTORE_OBSERVER \
__asm { jmp dword ptr [g_original+offset] } }
ENTRY(SetterEntry, 0, 0)
ENTRY(BindEntry, 1, 4)
ENTRY(TownEntry, 2, 8)
ENTRY(RegionEntry, 3, 12)
ENTRY(DrawEntry, 4, 16)
ENTRY(MarkerEntry, 5, 20)
ENTRY(RectEntry, 6, 24)
static void __declspec(naked) TitleCopyEntry() {
SAVE_OBSERVER
__asm { lea eax, [esp+528] }
__asm { push eax }
__asm { call BeforeTitleCopy }
__asm { add esp, 4 }
RESTORE_OBSERVER
__asm { jmp dword ptr [g_original+28] }
}
static void __declspec(naked) ReturnThunk() {
__asm { push 0 }
SAVE_OBSERVER
__asm { lea eax, [esp+528] }
__asm { push eax }
__asm { call After }
__asm { add esp, 4 }
RESTORE_OBSERVER
__asm { ret }
}
#undef ENTRY
#undef SAVE_OBSERVER
#undef RESTORE_OBSERVER
struct Hook { const char* name; uintptr_t address; const char* bytes; unsigned size; void* entry; uintptr_t callTarget; BYTE saved[16]; };
static Hook g_hooks[] = {
{"world_town_set",0x03406800,"558bec568b7508",7,SetterEntry},
{"minimap_bind",0x01c987c0,"568bf1ff156c261a04",9,BindEntry},
{"scene_town_get",0x03406320,"8b81e4010000",6,TownEntry},
{"town_region_resolve",0x0392cc90,"558bec83ec18",6,RegionEntry},
{"minimap_draw_layers",0x022476c0,"558bec6aff",5,DrawEntry},
{"marker_update",0x0223c460,"558bec83ec0c",6,MarkerEntry},
{"map_rect_lookup",0x0224d960,"558bec83ec18",6,RectEntry},
{"minimap_title_copy",0x03412282,"e849a732fd",5,TitleCopyEntry,0x0073c9d0}
};
static const unsigned kHookCount = sizeof(g_hooks)/sizeof(g_hooks[0]);
struct Guard { uintptr_t address; const char* bytes; };
static const Guard g_guards[] = {
{0x03406818,"85c0740980b8f0000000017443"}, {0x01d8214d,"e8ae4668018be55dc3"},
{0x01c987f1,"e82adb760150e89444c901"}, {0x01c9bc56,"e8c5a6760150e82f10c901"},
{0x022478b8,"e863ea1b0150e8cd536e0183c4043b43687568"},
{0x02247884,"8b34b985f6744d837e0402753a"}, {0x0223c4f8,"e893076f018bf883c404"},
{0x0224e07e,"e8ddf8ffff8b4d0c"}, {0x03680720,"8b412cc3"}, {0x034e4b90,"8b4164c3"},
{0x01c43fa3,"e8e80b8a0183c008"}, {0x01c44239,"e8420b0500"},
{0x0224821e,"8b55e4897a64"}, {0x02240c56,"895e08895e0c"},
{0x022479a2,"c23800"},
{0x03411950,"6a01899fe40100008b0dd8261b0553e84c8704008945dc"},
{0x03412273,"8b45dc83ec2083c0048bcc8965d050e849a732fdc745fc03000000c745fcffffffff8bcee8142183fe"},
{0x0345a0b0,"558bec5356578bf1e873f9ffff85c074118b5d08538bc8e8c4aa19008bf885ff75095f5e33c05b5dc208008b450c508bce899e34020000e804ad37ff578bcee8acfc13005fc78634020000ffffffff5e5b5dc20800"},
{0x0073c9d0,"558bec6aff6868f6f90364a1000000005083ec1456a13c670e05"}
};
static int Hex(char c) { return c >= '0' && c <= '9' ? c-'0' : c >= 'a' && c <= 'f' ? c-'a'+10 : -1; }
static bool Match(uintptr_t address, const char* bytes) {
__try {
size_t length = strlen(bytes);
if (length % 2) return false;
for (size_t i = 0; i < length/2; ++i)
if (*reinterpret_cast<BYTE*>(Va(address)+i) != (Hex(bytes[2*i])*16+Hex(bytes[2*i+1]))) return false;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) { return false; }
}
static bool WriteCode(void* destination, const void* source, unsigned size) {
DWORD protection = 0;
if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &protection)) return false;
memcpy(destination, source, size);
FlushInstructionCache(GetCurrentProcess(), destination, size);
DWORD ignored;
return VirtualProtect(destination, size, protection, &ignored) != FALSE;
}
static void Jump(BYTE* destination, uintptr_t source, uintptr_t target) {
destination[0] = 0xe9;
*reinterpret_cast<DWORD*>(destination+1) = static_cast<DWORD>(target - source - 5);
}
bool Install(HMODULE image, Logger logger) {
if (g_ready) return true;
g_image = image;
LogLine = logger;
if (!image || !logger) return false;
// Verify ALL entry/caller/layout guards before allocation or any hook.
for (unsigned n = 0; n < sizeof(g_hooks)/sizeof(g_hooks[0]); ++n)
if (!Match(g_hooks[n].address, g_hooks[n].bytes)) {
LogLine("Seria minimap disabled: native entry mismatch %s", g_hooks[n].name); return false;
}
for (unsigned n = 0; n < sizeof(g_guards)/sizeof(g_guards[0]); ++n)
if (!Match(g_guards[n].address, g_guards[n].bytes)) {
LogLine("Seria minimap disabled: native guard mismatch %08X", static_cast<unsigned>(g_guards[n].address)); return false;
}
g_titleTownLookup = reinterpret_cast<TownResourceLookup>(Va(0x0345a0b0));
for (unsigned n = 0; n < sizeof(g_rects)/sizeof(g_rects[0]); ++n) {
g_rects[n].town = 38; g_rects[n].area = 1; g_rects[n].geometry = g_rects[n].rect;
memcpy(g_rects[n].rect, kGlyphs[n].rect, sizeof(g_rects[n].rect));
}
unsigned allocated = 0, written = 0;
bool prepared = true, patched = true;
for (; allocated != kHookCount; ++allocated) {
Hook& hook = g_hooks[allocated];
memcpy(hook.saved, reinterpret_cast<void*>(Va(hook.address)), hook.size);
if (hook.callTarget) {
g_original[allocated] = reinterpret_cast<void*>(Va(hook.callTarget));
continue;
}
BYTE* trampoline = static_cast<BYTE*>(VirtualAlloc(0, hook.size+5, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE));
if (!trampoline) { prepared = false; break; }
g_original[allocated] = trampoline;
memcpy(trampoline, hook.saved, hook.size);
Jump(trampoline+hook.size, reinterpret_cast<uintptr_t>(trampoline)+hook.size, Va(hook.address)+hook.size);
DWORD previous;
if (!VirtualProtect(trampoline, hook.size+5, PAGE_EXECUTE_READ, &previous)) { prepared = false; ++allocated; break; }
FlushInstructionCache(GetCurrentProcess(), trampoline, hook.size+5);
}
if (prepared && allocated == kHookCount) {
for (; written != kHookCount; ++written) {
Hook& hook = g_hooks[written]; BYTE replacement[16]; memset(replacement, 0x90, hook.size);
Jump(replacement, Va(hook.address), reinterpret_cast<uintptr_t>(hook.entry));
if (hook.callTarget) replacement[0] = 0xe8; // keep the native CALL/RET 4 ABI
if (!WriteCode(reinterpret_cast<void*>(Va(hook.address)), replacement, hook.size)) { patched = false; ++written; break; }
}
if (patched && written == kHookCount) {
InterlockedExchange(&g_ready, 1);
LogLine("Seria minimap ready: owning-town title hooks=8 guards=19 generated-rooms=16, recording independent");
return true;
}
}
// Roll back entry bytes BEFORE freeing executable trampolines.
bool restored = true;
for (unsigned n = 0; n != written; ++n)
restored = WriteCode(reinterpret_cast<void*>(Va(g_hooks[n].address)), g_hooks[n].saved, g_hooks[n].size) && restored;
if (restored) for (unsigned n = 0; n != allocated; ++n) {
if (g_original[n] && !g_hooks[n].callTarget) VirtualFree(g_original[n], 0, MEM_RELEASE);
g_original[n] = 0;
}
LogLine("Seria minimap disabled: install failed; entry rollback=%d", restored ? 1 : 0);
return false;
}
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <windows.h>
// UI-only shared-room ownership. Packet callbacks use the existing dispatcher;
// this module never replaces a scene, actor, quest, PVF row, or game field.
namespace dnf90_seria {
typedef void (__cdecl* Logger)(const char* format, ...);
bool Install(HMODULE image, Logger logger);
unsigned BeginPacket(int packet);
void EndPacket(unsigned ticket);
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
// Generated from the complete hash-checked current PVF/NPK census. Do not edit city rows.
// Input: data/seria_room_catalog.json; generator: tools/generate_seria_room_catalog.py.
#include <stdint.h>
namespace dnf90_seria {
struct TownRow { int town; int region; uint64_t areas; };
struct GlyphRow { int town; int rect[4]; };
static const char kPvfSha256[] = "510daa58d01bc0f483b146b80faca9e21af4e85ad099467058d67f84dce6af5c";
static const char kNpkSha256[] = "e107637a2990d1d4d5802700a4ece64cf8338058c986b23cc337fc749ee66248";
static const TownRow kTowns[] = {
{6, 21, 0x1fULL},
{7, 4, 0x3fULL},
{9, 21, 0x3ULL},
{10, 5, 0xffULL},
{11, 5, 0x7ULL},
{12, 5, 0x1ULL},
{13, 5, 0x1ULL},
{14, 22, 0x1fULL},
{17, 6, 0x1fULL},
{18, 5, 0xfULL},
{19, 3, 0x7ULL},
{20, 22, 0x7ULL},
{22, 7, 0x1fULL},
{28, 11, 0x3ULL},
{30, 13, 0x7ULL},
{31, 13, 0xfULL},
{32, 14, 0x1fULL},
{35, 13, 0x3ULL},
{36, 16, 0x1ULL},
{37, 17, 0x1ULL},
{38, 18, 0x7dULL},
{39, 19, 0x3fffffffffULL},
{40, 19, 0x3fULL},
{41, 20, 0x1fULL},
{42, 20, 0xfULL},
{43, 18, 0x7ULL},
{46, 23, 0x7fULL},
{98, 24, 0x1ffffffffULL},
{106, 1, 0x1ULL},
{107, 15, 0x1ULL},
};
static const GlyphRow kGlyphs[] = {
{6, {280, 118, 20, 16}},
{9, {390, 326, 20, 16}},
{11, {371, 276, 20, 16}},
{14, {347, 442, 20, 16}},
{17, {400, 227, 20, 16}},
{18, {255, 415, 20, 16}},
{20, {159, 157, 20, 16}},
{22, {117, 448, 20, 16}},
{30, {65, 66, 20, 16}},
{31, {523, 278, 20, 16}},
{39, {293, 289, 20, 16}},
{40, {514, 218, 20, 16}},
{41, {350, 214, 20, 16}},
{42, {416, 361, 20, 16}},
{43, {578, 105, 20, 16}},
{46, {546, 208, 20, 16}},
};
static const int kNativeSharedProjectRect[] = {302, 357, 16, 10};
}
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include "90CN_seria_room_catalog.h"
#include <stddef.h>
namespace dnf90_seria {
inline const TownRow* FindTown(int town) {
for (size_t i = 0; i < sizeof(kTowns)/sizeof(kTowns[0]); ++i)
if (kTowns[i].town == town) return kTowns+i;
return 0;
}
inline const GlyphRow* FindGlyph(int town) {
for (size_t i = 0; i < sizeof(kGlyphs)/sizeof(kGlyphs[0]); ++i)
if (kGlyphs[i].town == town) return kGlyphs+i;
return 0;
}
struct Scene { uintptr_t world; int town; int area; };
inline bool Shared(const Scene& scene) { return scene.world && scene.town == 38 && scene.area == 1; }
inline bool ValidOrigin(const Scene& scene) {
const TownRow* row = FindTown(scene.town);
return scene.world && row && scene.area >= 0 && scene.area < 64 &&
(row->areas & (uint64_t(1) << scene.area)) != 0;
}
struct Owner { uintptr_t world; int town; int region; unsigned serial; };
struct Notice { Scene before; bool bound; };
struct Lifecycle {
Owner context;
uintptr_t seedWorld;
int seedTown;
unsigned serial;
void Clear() { context = Owner(); seedWorld = 0; seedTown = 0; ++serial; }
void Seed(uintptr_t world, int town) {
Clear();
if (world && FindTown(town)) { seedWorld = world; seedTown = town; }
}
bool Bind(Notice& notice, const Scene& after) {
if (notice.bound || after.world != notice.before.world || !Shared(after)) return false;
int town = seedWorld == after.world ? seedTown : (ValidOrigin(notice.before) ? notice.before.town : 0);
seedWorld = 0; seedTown = 0;
if (town) {
const TownRow* row = FindTown(town);
context = Owner{after.world, town, row->region, ++serial};
} else if (!(Shared(notice.before) && context.world == after.world)) {
context = Owner(); ++serial;
}
notice.bound = true;
return true;
}
void Finish(Notice& notice, const Scene& after) {
if (!after.world || notice.before.world != after.world) return;
if (!Shared(after)) Clear(); else Bind(notice, after);
}
};
inline bool OriginalSharedRect(const int* rect) {
if (!rect) return false;
for (int i = 0; i != 4; ++i) if (rect[i] != kNativeSharedProjectRect[i]) return false;
return true;
}
// Unknown non-NULL native geometry wins. Native owner38 needs no synthetic row.
inline const GlyphRow* BorrowGlyph(int town, bool nativeNull, const int* originalRect) {
return (nativeNull || OriginalSharedRect(originalRect)) ? FindGlyph(town) : 0;
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <stdint.h>
// Current-client pointer-vector snapshot; this policy never writes native state.
namespace dnf90 {
struct UiIteratorSnapshot {
uintptr_t owner, proxy, iteratorProxy, proxyOwner, begin, end, current;
};
inline bool CanAdvanceUiIterator(const UiIteratorSnapshot& s)
{
return s.owner != 0 && s.proxy != 0 && s.iteratorProxy == s.proxy &&
s.proxyOwner == s.owner && s.begin != 0 && s.end >= s.begin &&
s.current >= s.begin && s.current < s.end &&
(s.end - s.begin) % 4 == 0 && (s.current - s.begin) % 4 == 0;
}
}
@@ -0,0 +1,293 @@
#include "90CN_weapon_skill_relogin_fix.h"
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <wchar.h>
namespace
{
constexpr uintptr_t kRequirementBranchRva = 0x01C07739;
constexpr uintptr_t kRequirementInvalidResumeRva = 0x01C07748;
constexpr uintptr_t kRequirementValidResumeRva = 0x01C07780;
constexpr uintptr_t kNativeSkillValidityRva = 0x0225F610;
constexpr uintptr_t kReloginEquipmentCallerReturnRva = 0x01C0859E;
constexpr DWORD kInstallDelayMs = 1500;
// Native bytes immediately after actor->lookup(requirementId, 1):
// je invalid
// push 1
// mov ecx,eax
const unsigned char kExpectedBytes[6] = { 0x74, 0x0D, 0x6A, 0x01, 0x8B, 0xC8 };
HMODULE g_selfModule = nullptr;
uintptr_t g_exeBase = 0;
HANDLE g_worker = nullptr;
volatile LONG g_stop = 0;
volatile LONG g_installed = 0;
volatile LONG g_bypassLogCount = 0;
SRWLOCK g_logLock = SRWLOCK_INIT;
void* g_nativeSkillValidity = nullptr;
void* g_invalidResume = nullptr;
void* g_validResume = nullptr;
uintptr_t g_expectedCallerReturn = 0;
void BuildLogPath(wchar_t* output, size_t outputCount)
{
if (!output || outputCount == 0) return;
output[0] = L'\0';
DWORD length = g_selfModule
? GetModuleFileNameW(g_selfModule, output,
static_cast<DWORD>(outputCount))
: 0;
if (!length || length >= outputCount) {
wcscpy_s(output, outputCount, L"90CN_fix.log");
return;
}
wchar_t* slash = wcsrchr(output, L'\\');
wchar_t* slash2 = wcsrchr(output, L'/');
if (slash2 && (!slash || slash2 > slash)) slash = slash2;
if (slash) {
slash[1] = L'\0';
wcscat_s(output, outputCount, L"90CN_fix.log");
} else {
wcscpy_s(output, outputCount, L"90CN_fix.log");
}
}
void WriteLog(const char* format, ...)
{
if (!format) return;
char message[2048] = {};
va_list args;
va_start(args, format);
_vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args);
va_end(args);
SYSTEMTIME now = {};
GetLocalTime(&now);
char line[2304] = {};
_snprintf_s(line, sizeof(line), _TRUNCATE,
"%04u-%02u-%02u %02u:%02u:%02u.%03u tid=%lu %s\r\n",
now.wYear, now.wMonth, now.wDay,
now.wHour, now.wMinute, now.wSecond, now.wMilliseconds,
static_cast<unsigned long>(GetCurrentThreadId()), message);
wchar_t path[MAX_PATH] = {};
BuildLogPath(path, _countof(path));
AcquireSRWLockExclusive(&g_logLock);
HANDLE file = CreateFileW(path, FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file != INVALID_HANDLE_VALUE) {
DWORD written = 0;
WriteFile(file, line, static_cast<DWORD>(strlen(line)), &written, nullptr);
CloseHandle(file);
}
ReleaseSRWLockExclusive(&g_logLock);
}
__declspec(noinline) void __cdecl LogReloginBypass(
unsigned int requirementId, uintptr_t callerReturn)
{
LONG count = InterlockedIncrement(&g_bypassLogCount);
if (count > 8) return;
WriteLog(
"weapon-relogin-fix bypass hit=%ld requirement_id=%u caller_rva=0x%08X "
"lookup_record=present native_validity=0 action=treat-valid "
"scope=op24-relogin-main-weapon-only",
count, requirementId,
g_exeBase && callerReturn >= g_exeBase
? static_cast<unsigned int>(callerReturn - g_exeBase)
: 0u);
}
// Original sequence at RVA 0x01C07737:
// test eax,eax
// je invalid
// push 1
// mov ecx,eax
// call sub_265F610
// test eax,eax
// jne valid
//
// The caller has already decoded the equipped main weapon's requirement id,
// skipped requirementId == -1, and resolved actor->lookup(requirementId, 1).
// Live evidence proved that op24 can reach this point with a real requirement
// record while sub_265F610 transiently returns 0 before skill initialization is
// fully settled. Preserve every native decision except that timing window.
//
// The standard EBP frame lets us require [EBP+4] == base + 0x01C0859E, the
// observed op24 relogin main-weapon revalidation caller. Therefore normal
// manual equipping, other callers, missing requirement records, and native
// validity successes all keep their original behavior. No skill/profession
// allow list is needed, so the same mechanism covers other exclusive weapons.
__declspec(naked) void ProxyWeaponSkillReloginFix()
{
__asm {
// EAX is actor->lookup(requirementId, 1). A missing record remains
// invalid exactly as in native code.
test eax, eax
jz requirement_invalid
// Preserve the native validity call and accept native success first.
push 1
mov ecx, eax
call dword ptr [g_nativeSkillValidity]
test eax, eax
jnz requirement_valid
// Only repair the known relogin main-weapon revalidation call site.
// EBX still holds the decoded requirement id for logging.
mov edx, dword ptr [ebp + 4]
cmp edx, dword ptr [g_expectedCallerReturn]
jne requirement_invalid
pushfd
pushad
push edx
push ebx
call LogReloginBypass
add esp, 8
popad
popfd
mov eax, 1
jmp dword ptr [g_validResume]
requirement_invalid:
jmp dword ptr [g_invalidResume]
requirement_valid:
jmp dword ptr [g_validResume]
}
}
bool PatchFix(unsigned char* target)
{
if (!target) return false;
unsigned char patch[6] = { 0xE9, 0, 0, 0, 0, 0x90 };
LONG relative = static_cast<LONG>(
reinterpret_cast<uintptr_t>(&ProxyWeaponSkillReloginFix) -
reinterpret_cast<uintptr_t>(target + 5));
*reinterpret_cast<LONG*>(patch + 1) = relative;
DWORD oldProtection = 0;
if (!VirtualProtect(target, sizeof(patch), PAGE_EXECUTE_READWRITE,
&oldProtection)) {
return false;
}
memcpy(target, patch, sizeof(patch));
DWORD ignored = 0;
VirtualProtect(target, sizeof(patch), oldProtection, &ignored);
FlushInstructionCache(GetCurrentProcess(), target, sizeof(patch));
return memcmp(target, patch, sizeof(patch)) == 0;
}
bool RestoreFix(unsigned char* target)
{
if (!target) return false;
DWORD oldProtection = 0;
if (!VirtualProtect(target, sizeof(kExpectedBytes), PAGE_EXECUTE_READWRITE,
&oldProtection)) {
return false;
}
memcpy(target, kExpectedBytes, sizeof(kExpectedBytes));
DWORD ignored = 0;
VirtualProtect(target, sizeof(kExpectedBytes), oldProtection, &ignored);
FlushInstructionCache(GetCurrentProcess(), target, sizeof(kExpectedBytes));
return memcmp(target, kExpectedBytes, sizeof(kExpectedBytes)) == 0;
}
DWORD WINAPI InstallWorker(LPVOID)
{
g_exeBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
if (!g_exeBase || !g_selfModule) return 0;
DWORD slept = 0;
while (slept < kInstallDelayMs &&
InterlockedCompareExchange(&g_stop, 0, 0) == 0) {
Sleep(100);
slept += 100;
}
if (InterlockedCompareExchange(&g_stop, 0, 0) != 0) return 0;
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kRequirementBranchRva);
g_nativeSkillValidity = reinterpret_cast<void*>(
g_exeBase + kNativeSkillValidityRva);
g_invalidResume = reinterpret_cast<void*>(
g_exeBase + kRequirementInvalidResumeRva);
g_validResume = reinterpret_cast<void*>(
g_exeBase + kRequirementValidResumeRva);
g_expectedCallerReturn = g_exeBase + kReloginEquipmentCallerReturnRva;
__try {
if (memcmp(target, kExpectedBytes, sizeof(kExpectedBytes)) != 0) {
WriteLog(
"weapon-relogin-fix install-failed target_rva=0x%08X "
"observed=%02X%02X%02X%02X%02X%02X expected=740D6A018BC8",
static_cast<unsigned int>(kRequirementBranchRva),
target[0], target[1], target[2], target[3], target[4], target[5]);
return 0;
}
if (!PatchFix(target)) {
WriteLog(
"weapon-relogin-fix install-failed reason=patch-write target_rva=0x%08X",
static_cast<unsigned int>(kRequirementBranchRva));
return 0;
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
WriteLog("weapon-relogin-fix install exception code=0x%08X",
GetExceptionCode());
return 0;
}
InterlockedExchange(&g_installed, 1);
WriteLog(
"weapon-relogin-fix installed target_rva=0x%08X native_validity_rva=0x%08X "
"caller_rva=0x%08X requirement_scope=any-present-record "
"scope=op24-relogin-main-weapon-only",
static_cast<unsigned int>(kRequirementBranchRva),
static_cast<unsigned int>(kNativeSkillValidityRva),
static_cast<unsigned int>(kReloginEquipmentCallerReturnRva));
return 0;
}
}
void StartWeaponSkillReloginFix(HMODULE module)
{
if (g_worker || !module) return;
g_selfModule = module;
InterlockedExchange(&g_stop, 0);
g_worker = CreateThread(nullptr, 0, InstallWorker, nullptr, 0, nullptr);
if (!g_worker) {
WriteLog("weapon-relogin-fix start failed reason=CreateThread error=%lu",
GetLastError());
}
}
void StopWeaponSkillReloginFix()
{
InterlockedExchange(&g_stop, 1);
if (g_worker) {
WaitForSingleObject(g_worker, 1500);
CloseHandle(g_worker);
g_worker = nullptr;
}
if (InterlockedCompareExchange(&g_installed, 0, 0) != 0 && g_exeBase) {
unsigned char* target = reinterpret_cast<unsigned char*>(
g_exeBase + kRequirementBranchRva);
bool restored = false;
__try {
restored = RestoreFix(target);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
restored = false;
}
WriteLog("weapon-relogin-fix restore result=%d", restored ? 1 : 0);
InterlockedExchange(&g_installed, 0);
}
}
@@ -0,0 +1,10 @@
#pragma once
#include <windows.h>
// Narrow client-side fix for relogin weapon revalidation. It preserves the
// native lookup and validity checks, and only repairs the op24 login-equipment
// path when a real requirement record exists but its transient native validity
// state still reports false. No profession or skill-id allow list is used.
void StartWeaponSkillReloginFix(HMODULE module);
void StopWeaponSkillReloginFix();
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <windows.h>
#include <unordered_map>
class InlineHook
{
public:
static InlineHook& Instance()
{
static InlineHook inst;
return inst;
}
bool Add(void* target, void* detour)
{
SIZE_T patchLen = CalcPatchLen(target, 5);
if (patchLen < 5) return false;
auto trampoline = (BYTE*)VirtualAlloc(NULL, patchLen + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!trampoline) return false;
memcpy(trampoline, target, patchLen);
trampoline[patchLen] = 0xE9;
*(DWORD*)(trampoline + patchLen + 1) = (DWORD)((BYTE*)target + patchLen) - (DWORD)(trampoline + patchLen + 5);
DWORD old;
VirtualProtect(target, patchLen, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)target = 0xE9;
*(DWORD*)((BYTE*)target + 1) = (DWORD)(BYTE*)detour - (DWORD)((BYTE*)target + 5);
for (SIZE_T i = 5; i < patchLen; i++)
*((BYTE*)target + i) = 0x90;
VirtualProtect(target, patchLen, old, &old);
m_trampolines[(uintptr_t)target] = trampoline;
return true;
}
void* GetTrampoline(uintptr_t target)
{
auto it = m_trampolines.find(target);
return it != m_trampolines.end() ? it->second : nullptr;
}
private:
std::unordered_map<uintptr_t, BYTE*> m_trampolines;
SIZE_T CalcPatchLen(void* addr, SIZE_T minLen)
{
SIZE_T len = 0;
BYTE* p = (BYTE*)addr;
while (len < minLen)
len += InsnLen(p + len);
return len;
}
SIZE_T InsnLen(BYTE* p)
{
if (p[0] == 0xCC || p[0] == 0xC3 || p[0] == 0x90) return 1;
if (p[0] == 0x55 || p[0] == 0x53 || p[0] == 0x56 || p[0] == 0x57) return 1;
if (p[0] == 0x5D || p[0] == 0x5B || p[0] == 0x5E || p[0] == 0x5F) return 1;
if (p[0] == 0x50 || p[0] == 0x51 || p[0] == 0x52) return 1;
if (p[0] == 0x58 || p[0] == 0x59 || p[0] == 0x5A) return 1;
if (p[0] == 0x60 || p[0] == 0x61 || p[0] == 0x9C || p[0] == 0x9D) return 1;
if (p[0] == 0x8B && (p[1] & 0xC0) == 0xC0) return 2;
if (p[0] == 0x8B && p[1] == 0xEC) return 2;
if (p[0] == 0x8B && p[1] == 0xFF) return 2;
if (p[0] == 0x89 && (p[1] & 0xC0) == 0xC0) return 2;
if (p[0] == 0x33 && (p[1] & 0xC0) == 0xC0) return 2;
if (p[0] == 0x6A) return 2;
if (p[0] == 0xEB) return 2;
if (p[0] == 0xB8 || p[0] == 0xB9 || p[0] == 0xBA || p[0] == 0xBB ||
p[0] == 0xBC || p[0] == 0xBD || p[0] == 0xBE || p[0] == 0xBF) return 5;
if (p[0] == 0xE8 || p[0] == 0xE9) return 5;
if (p[0] == 0x68) return 5;
if (p[0] == 0x83 && p[1] == 0xEC) return 3;
if (p[0] == 0x83 && p[1] == 0xE4) return 3;
if (p[0] == 0x81 && p[1] == 0xEC) return 6;
if (p[0] == 0xFF && p[1] == 0x25) return 6;
if (p[0] == 0x8D && (p[1] & 0xC7) == 0x44) return 4;
if (p[0] == 0x8D && (p[1] & 0xC0) == 0x40) return 3;
if (p[0] == 0x64 && p[1] == 0xA1) return 6;
return 1;
}
};
inline BOOLEAN Hook_Inline(void* target, void* detour)
{
return InlineHook::Instance().Add(target, detour) ? TRUE : FALSE;
}
inline void* Hook_GetTrampoline(uintptr_t target)
{
return InlineHook::Instance().GetTrampoline(target);
}
+121
View File
@@ -0,0 +1,121 @@
#include <windows.h>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <vector>
#include <string>
#include <sstream>
#include <fcntl.h>
#include <io.h>
// 2026-10-21: 性能版构建开关——关闭所有日志写盘(战力插件等功能不受影响)。
#ifndef DNF90_SILENT_LOG
#define DNF90_SILENT_LOG 1
#endif
static std::wstring HexToWString(const unsigned char* data, size_t length)
{
std::wstringstream wss;
wss << std::hex << std::uppercase << std::setfill(L'0');
for (size_t i = 0; i < length; ++i) {
wss << std::setw(2) << static_cast<int>(data[i]);
if (i != length - 1) {
wss << L" ";
}
}
return wss.str();
}
static std::vector<std::wstring> HexToWStrings(const unsigned char* data, size_t length, size_t bytesPerLine = 16)
{
std::vector<std::wstring> lines;
std::wstringstream wss;
wss << std::hex << std::uppercase << std::setfill(L'0');
for (size_t i = 0; i < length; ++i) {
wss << std::setw(2) << static_cast<int>(data[i]);
if ((i + 1) % bytesPerLine == 0 || i == length - 1) {
lines.push_back(wss.str());
wss.str(L"");
wss.clear();
}
else {
wss << L" ";
}
}
return lines;
}
static void AppendFileLogLine(const wchar_t* filename, const std::wstring& line)
{
#ifdef DNF90_SILENT_LOG
(void)filename; (void)line; return;
#endif
FILE* fp = NULL;
_wfopen_s(&fp, filename, L"a, ccs=UTF-8");
if (fp) {
fwprintf(fp, L"%s\n", line.c_str());
fclose(fp);
}
}
static void AppendFileLogLines(const wchar_t* filename, const std::vector<std::wstring>& lines)
{
#ifdef DNF90_SILENT_LOG
(void)filename; (void)lines; return;
#endif
FILE* fp = NULL;
_wfopen_s(&fp, filename, L"a, ccs=UTF-8");
if (fp) {
for (const auto& line : lines) {
fwprintf(fp, L"%s\n", line.c_str());
}
fclose(fp);
}
}
static void AppendFileLogFormatLine(const wchar_t* filename, const wchar_t* format, ...)
{
#ifdef DNF90_SILENT_LOG
(void)filename; (void)format; return;
#endif
va_list args;
va_start(args, format);
int neededSize = _vscwprintf(format, args) + 1;
va_end(args);
wchar_t stackBuffer[2048];
wchar_t* buffer = stackBuffer;
if (neededSize > _countof(stackBuffer)) {
buffer = (wchar_t*)malloc(neededSize * sizeof(wchar_t));
if (!buffer) return;
}
va_start(args, format);
vswprintf_s(buffer, neededSize > _countof(stackBuffer) ? neededSize : _countof(stackBuffer), format, args);
va_end(args);
AppendFileLogLine(filename, std::wstring(buffer));
if (buffer != stackBuffer)
free(buffer);
}
static void AppendHexLogLine(const wchar_t* filename, const unsigned char* data, size_t length)
{
#ifdef DNF90_SILENT_LOG
(void)filename; (void)data; (void)length; return;
#endif
std::wstring hexString = HexToWString(data, length);
AppendFileLogLine(filename, hexString);
}
static void AppendHexLogLines(const wchar_t* filename, const unsigned char* data, size_t length, size_t bytesPerLine = 16)
{
#ifdef DNF90_SILENT_LOG
(void)filename; (void)data; (void)length; (void)bytesPerLine; return;
#endif
std::vector<std::wstring> hexLines = HexToWStrings(data, length, bytesPerLine);
AppendFileLogLines(filename, hexLines);
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
// Production bitmap resources embedded in 90CN.dll for supported DLL-owned
// panels. Native Seria-luck UI resources are intentionally not embedded here.
#define IDR_COMBAT_POWER_PANEL_SKIN_V2 301
#define IDR_COMBAT_RANK_EXPLORE_V3 302
#define IDR_COMBAT_RANK_PIONEER_V3 303
#define IDR_COMBAT_RANK_FEARLESS_V3 304
#define IDR_COMBAT_RANK_CONQUER_V3 305
#define IDR_COMBAT_RANK_BATTLE_PINNACLE_V3 306
#define IDR_COMBAT_RANK_HEROIC_V3 307
#define IDR_COMBAT_RANK_MARTIAL_MASTERY_V3 308
#define IDR_COMBAT_POWER_RANK_TOOLTIP_V2 309
+312
View File
@@ -0,0 +1,312 @@
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include <wchar.h>
#include <string.h>
#include <intrin.h>
#include "90CN.h"
#include "90CN_op3_parent_fix.h"
#include "90CN_selected_page_fix.h"
#include "90CN_dungeon_skill_gate_fix.h"
#include "90CN_weapon_skill_relogin_fix.h"
#include "90CN_3149_writer_resync_fix.h"
#define EXPAPI(Name) EXTERN_C __declspec(dllexport) void Name() {}
EXPAPI(A)
namespace
{
// Current NoPack's direct-scene route predicate is replaced asynchronously by
// 90CN.cpp. The imported source baseline accidentally omitted class-1 opcode
// 47 even though the 2026-08-19 live fix proved that DROP_ITEM acknowledgements
// must enter the native class-1 handler to release the client's discard
// pending token. Keep this compatibility shim deliberately narrower than the
// main allow-list: opcode 47 is admitted here and every other opcode tail-calls
// the already-installed scene-route proxy unchanged.
constexpr uintptr_t kDprotoRoutePredicateRva = 0x030734E0;
// Current-EXE PVP ready gate. Runtime proof on 2026-09-09 showed that the
// resident PK element is a real type-40 channel object and that the virtual
// predicate at 0x018928E0 returns false solely because sub_36B0C90's legacy
// type whitelist does not contain 40. The same predicate may be used by other
// channel/UI paths, so do not modify the global whitelist. Admit type 40 only
// when the call comes from the proven PVP-ready vcall return at 0x0189537A.
constexpr uintptr_t kPvpReadyElementPredicateRva = 0x014928E0;
constexpr uintptr_t kPvpReadyTypeWhitelistRva = 0x032B0C90;
constexpr uintptr_t kPvpReadyElementCallReturnRva = 0x0149537A;
constexpr unsigned int kPvpNativeChannelType = 40;
HMODULE g_selfModule = nullptr;
PVOID g_previousSceneRoute = nullptr;
typedef int (__cdecl* PvpReadyTypePredicateFn)(unsigned int);
PvpReadyTypePredicateFn g_pvpReadyTypePredicate = nullptr;
void PvpTraceLine(const char* format, ...)
{
wchar_t exePath[MAX_PATH] = {};
if (!GetModuleFileNameW(nullptr, exePath, MAX_PATH)) return;
wchar_t* slash = wcsrchr(exePath, L'\\');
if (slash) slash[1] = L'\0';
wchar_t logPath[MAX_PATH] = {};
_snwprintf_s(logPath, _countof(logPath), _TRUNCATE,
L"%ls90CN_trace.log", exePath);
char body[768] = {};
va_list args;
va_start(args, format);
_vsnprintf_s(body, sizeof(body), _TRUNCATE, format, args);
va_end(args);
SYSTEMTIME now = {};
GetLocalTime(&now);
char line[1024] = {};
int length = _snprintf_s(line, sizeof(line), _TRUNCATE,
"[%04u-%02u-%02u %02u:%02u:%02u.%03u] [TRACE] [pid=%lu tid=%lu] %s\r\n",
now.wYear, now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond,
now.wMilliseconds, GetCurrentProcessId(), GetCurrentThreadId(), body);
if (length <= 0) return;
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) return;
DWORD ignored = 0;
WriteFile(file, line, static_cast<DWORD>(length), &ignored, nullptr);
CloseHandle(file);
}
int __fastcall ProxyPvpReadyElement(void* element, void*)
{
uintptr_t executable = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
uintptr_t caller = reinterpret_cast<uintptr_t>(_ReturnAddress());
uintptr_t callerRva = caller >= executable ? caller - executable : 0;
unsigned int type = 0;
__try {
type = *reinterpret_cast<unsigned int*>(
reinterpret_cast<unsigned char*>(element) + 8);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
PvpTraceLine(
"pvp ready element compatibility read failed caller_rva=0x%08X element=%p",
static_cast<unsigned int>(callerRva), element);
return 0;
}
if (callerRva == kPvpReadyElementCallReturnRva &&
type == kPvpNativeChannelType) {
PvpTraceLine(
"pvp ready element type40 admitted caller_rva=0x%08X element=%p type=%u reason=proved_current_pvp_ready_gate_20260909",
static_cast<unsigned int>(callerRva), element, type);
return 1;
}
return g_pvpReadyTypePredicate ? g_pvpReadyTypePredicate(type) : 0;
}
DWORD WINAPI InstallPvpReadyElementCompatibility(LPVOID)
{
HMODULE executable = GetModuleHandleW(nullptr);
if (!executable || !g_selfModule) return 0;
unsigned char* base = reinterpret_cast<unsigned char*>(executable);
unsigned char* target = base + kPvpReadyElementPredicateRva;
unsigned char* whitelist = base + kPvpReadyTypeWhitelistRva;
const uintptr_t replacement = reinterpret_cast<uintptr_t>(
&ProxyPvpReadyElement);
static const unsigned char kExpectedTarget[] = {
0x8B, 0x41, 0x08, 0x50, 0xE8, 0xA7, 0xE3, 0xE1, 0x01
};
static const unsigned char kExpectedWhitelist[] = {
0x55, 0x8B, 0xEC, 0x8B, 0x45, 0x08, 0x83, 0xF8, 0x04
};
unsigned int waitedMs = 0;
for (; waitedMs < 120000; waitedMs += 50) {
bool ready = false;
__try {
ready = memcmp(target, kExpectedTarget, sizeof(kExpectedTarget)) == 0 &&
memcmp(whitelist, kExpectedWhitelist,
sizeof(kExpectedWhitelist)) == 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return 0;
}
if (ready) break;
Sleep(50);
}
if (waitedMs >= 120000) {
PvpTraceLine(
"pvp ready element compatibility skipped target=0x%08X whitelist=0x%08X reason=native-bytes-timeout",
reinterpret_cast<uintptr_t>(target),
reinterpret_cast<uintptr_t>(whitelist));
return 0;
}
g_pvpReadyTypePredicate = reinterpret_cast<PvpReadyTypePredicateFn>(
whitelist);
DWORD oldProtection = 0;
if (!VirtualProtect(target, 5, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
PvpTraceLine(
"pvp ready element compatibility failed reason=VirtualProtect gle=%lu",
GetLastError());
return 0;
}
target[0] = 0xE9;
*reinterpret_cast<LONG*>(target + 1) = static_cast<LONG>(
replacement - reinterpret_cast<uintptr_t>(target + 5));
DWORD ignoredProtection = 0;
VirtualProtect(target, 5, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, 5);
PvpTraceLine(
"pvp ready element compatibility installed target=0x%08X whitelist=0x%08X pvp_return_rva=0x%08X admitted_type=%u waited_ms=%u",
reinterpret_cast<uintptr_t>(target),
reinterpret_cast<uintptr_t>(whitelist),
static_cast<unsigned int>(kPvpReadyElementCallReturnRva),
kPvpNativeChannelType, waitedMs);
return 0;
}
__declspec(naked) void ProxyDropItemAckSceneRoute()
{
__asm {
cmp word ptr [esp + 4], 47
jne drop_item_route_fallback
mov eax, 1
ret 4
drop_item_route_fallback:
jmp dword ptr [g_previousSceneRoute]
}
}
DWORD WINAPI InstallDropItemAckSceneRoute(LPVOID)
{
HMODULE executable = GetModuleHandleW(nullptr);
if (!executable || !g_selfModule) return 0;
unsigned char* target = reinterpret_cast<unsigned char*>(executable) +
kDprotoRoutePredicateRva;
const uintptr_t replacement = reinterpret_cast<uintptr_t>(
&ProxyDropItemAckSceneRoute);
// Start90CNPatch installs the ordinary route proxy on its own worker.
// Wait until its verified E9+NOP patch is present, then chain this one
// opcode in front of it. If DNF_ROUTE_COMPAT=0, no matching patch appears
// and this worker exits without changing the native predicate.
for (unsigned int waitedMs = 0; waitedMs < 120000; waitedMs += 100) {
__try {
if (target[0] != 0xE9 || target[5] != 0x90) {
Sleep(100);
continue;
}
LONG previousRelative = *reinterpret_cast<LONG*>(target + 1);
unsigned char* previous = target + 5 + previousRelative;
if (reinterpret_cast<uintptr_t>(previous) == replacement) {
return 0;
}
MEMORY_BASIC_INFORMATION memory = {};
if (VirtualQuery(previous, &memory, sizeof(memory)) != sizeof(memory) ||
memory.AllocationBase != g_selfModule) {
Sleep(100);
continue;
}
g_previousSceneRoute = previous;
LONG replacementRelative = static_cast<LONG>(
replacement - reinterpret_cast<uintptr_t>(target + 5));
DWORD oldProtection = 0;
if (!VirtualProtect(target, 6, PAGE_EXECUTE_READWRITE,
&oldProtection)) {
return 0;
}
target[0] = 0xE9;
*reinterpret_cast<LONG*>(target + 1) = replacementRelative;
target[5] = 0x90;
DWORD ignoredProtection = 0;
VirtualProtect(target, 6, oldProtection, &ignoredProtection);
FlushInstructionCache(GetCurrentProcess(), target, 6);
return 0;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return 0;
}
}
return 0;
}
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_selfModule = hModule;
DisableThreadLibraryCalls(hModule);
// The new standalone client has a different PE layout from the
// production NoPack executable. Keep every legacy RVA hook out of
// that process; only the proven native channel selection bridge is
// safe there. The current-client dungeon selector/owner/state
// observer rewrites the DB4 native entry points and was only added
// for a short diagnostic capture. Keep it opt-in so a normal game
// launch cannot be destabilised by an observation probe.
if (IsNewClientCompatibilityMode()) {
wchar_t probeEnabled[8] = {};
DWORD probeLength = GetEnvironmentVariableW(
L"DNF_DUNGEON_SELECTOR_PROBE", probeEnabled,
static_cast<DWORD>(_countof(probeEnabled)));
if (probeLength > 0 && probeEnabled[0] == L'1') {
StartCurrentDungeonSelectorProbe(hModule);
PvpTraceLine(
"new-client dungeon selector probe enabled by environment");
} else {
PvpTraceLine(
"new-client dungeon selector/owner/state probes disabled "
"for normal launch");
}
StartCurrentDungeonSkillGateFix(hModule);
StartNewClientChannelCompatibility();
break;
}
StartOp3ParentReadinessFix(hModule);
Start90CNPatch();
Start3149WriterResyncFix(hModule);
StartSelectedPageReadinessFix(hModule);
StartDungeonSkillGateFix(hModule);
StartWeaponSkillReloginFix(hModule);
{
HANDLE worker = CreateThread(
nullptr, 0, InstallPvpReadyElementCompatibility,
nullptr, 0, nullptr);
if (worker) CloseHandle(worker);
}
{
HANDLE worker = CreateThread(
nullptr, 0, InstallDropItemAckSceneRoute, nullptr, 0, nullptr);
if (worker) CloseHandle(worker);
}
break;
case DLL_PROCESS_DETACH:
StopCurrentDungeonSelectorProbe();
StopCurrentDungeonSkillGateFix();
StopWeaponSkillReloginFix();
StopDungeonSkillGateFix();
break;
}
return TRUE;
}
File diff suppressed because it is too large. Load diff