DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -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>
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
void StartOp3ParentReadinessFix(HMODULE module);
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
void StartSelectedPageReadinessFix(HMODULE module);
|
||||
@@ -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(¤t, 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
Reference in new issue
Block a user