Jump to content

A Technical Retrospective on Trait-Based Native Gender Swaps in CK3 | 技术讨论:CK3 按特性完整切换男女原生肖像模型:失败路线、测试结果与最终方案


Recommended Posts

Posted (edited)

CK3按特性完整切换单人女性肖像模型:失败路线、测试结果与最终方案

这里记录的是CK3 / CBO / Gender Metamorphosis 兼容补充项目的技术复盘。光给角色室外挂一个身体网格是不够的。我想指定种族的角色真正走套原来的肖像类型,把身体、头、骨骼、眼球、牙齿、头发、服装、动画路由整套一起切过去。

序号:心路历程

在蜕变之路(GM)的开发过程中,我思考过很多种“变性”方案,也踩过很多坑。把尝试过的方案和走过的弯路如实记下来,一方面是为了以后做类似MOD的人留下一部分参考,另外也想请社区里的高人不吝指导。

为了先求稳定,GM走的是MESH修改法:在男性身体上挂女性身体部位、靠变形来改变外观。这条路能用,但接口始终是预警——它本质上是在“改变身体”,每遇到一个新的MOD的生态,都可能要重新批发。 

转机出现在我看到了LL上的dude-looks-like-a-lady这个MOD。它的思路给了我一个新的启示:既然CK3在渲染层面就已经区分男女肖像,那不可能不碰mesh,而是在渲染层上直接交换男女肖像类型,从根本上实现“变男”和“变性女”?  

如果这个修改思路成立,兼容性会比MESH修改法好:因为它是游戏渲染层面的人交互,而不是靠身体网格,所以天然可以兼容CBO、Phaze等依赖于肖像生态的MOD。

于是,我开始了这个轮子实验。

一、项目目标

最初目标很简单,但实现非常绕:

  • 带 gm_full_female_test 的男性,视觉上完整使用女性原生肖像模型。
  • 带 gm_full_male_test 的女性,视觉上完整使用男性原生肖像模型。
  • 角色的游戏逻辑性别不变,只改变 portrait 渲染路线。
  • 普通男性、普通女性不受影响。
  • 需要兼容 CBO / Better Barbershop / Gender Metamorphosis 的基本使用场景。
  • 需要让目标角色使用目标性别的原生骨架、头身、眼球、牙齿、发型、衣服和动画契约。

这个目标听起来像“给男性换一个女性身体”,但真正踩坑后才发现:CK3 的身体、头、pose source、附件、DNA morph、动画缓存是一个整体系统。只换 mesh 是错误抽象层。

顺带说明:LL 上 dude-looks-like-a-lady 这个 MOD 的实现方式是全体替换——它直接对所有角色按性别整体切换,而不是针对某个特性(trait)做替换。至于“按某个特性替换”的路线,我试过很多次,原生引擎好像并不支持。

 

二、为什么不能只换身体

CK3人物肖像的基础结构大致是:

character sex
    -> portrait_type
        -> torso entity
        -> head entity
        -> sex-specific genes / morphs / attachments / animation routing

common/portrait_types/00_human_types.txt 按动静态分配头部和躯干。脚本层没有找到可用的:

set_portrait_type
change_portrait_type
set_portrait

也就是说,Jomini 脚本和 trait portrait modifier 可以追加、替换一些附件或 gene 表现,但不能在运行时把“某个有 trait 的男性”直接改成女性 portrait type。

这也是本项目最关键的结论:

轻量脚本路线可以做“外观附件”,但不能可靠地做“完整原生性别模型切换”。

一句话点破本质:不要换身体,换 portrait type 的选择结果。 后面所有的路线、失败现象与最终突破,都是在反复验证这句话。

三、测试过的技术路线

路线 做法 测试结果 结论
1. 只挂女性身体附件 保留男性基础 portrait,在 trait modifier 里追加 female body 能看到女性身体,但男性头、男性骨架、男性附件仍在 失败,只是“男体上外挂女体”
2. game_entity_override = torso/head 尝试用 trait 覆盖 torso/head entity 出现双身体、男头残留、女头缺失、眼球牙齿漂浮 失败,不能可靠替代 portrait type 创建的基础实体
3. no_portrait / CBO hidden morph 借用 CBO 的 no portrait mesh 把男性 mesh 移走 普通肖像部分隐藏,BB 或动画中又暴露巨大飞头、拉伸三角形 失败,适合静态隐藏,不适合全身动画
4. 自制 hide male blendshape 把男性身体/头压缩到极小 静态可隐藏大部分,但 morph 叠加后出现残肢、小人、碎片 失败,不能抵消后续所有 DNA/morph 层
5. 缩小男性骨架 给男性 body/head 播放缩放动画 大体隐藏,但场景中仍有微缩男性骨架;衣服还会挂在它上面 失败,骨架缩放有下限且附件契约被破坏
6. 统一 230 骨女性实体 在 Blender 合并女性 torso 169 骨 + head 61 骨 裸体模型和部分动画可运行,但眼球、牙齿、头发、衣服生态失效 半成功,不适合直接接入原生附件池
7. Head pose proxy 给统一实体补一个标准 61 骨 head proxy 极简 proxy 曾导致 C0000005 崩溃 理论有价值,但工程风险很高
8. 批量重绑附件 把女性眼球、牙齿、发型、衣服全部重绑到统一 230 骨 未全面投入,因为工作量巨大且后续维护困难 可行但不划算
9. Cheat Engine 改 portrait selector 在运行时把 selector 的 sex 参数从 male 改为 female 全局 POC 成功,男性完整走女性原生模型 成功验证正确抽象层
10. Native DLL hook Hook CK3 原生 portrait-type selector,并按 trait 判断 最终成功,普通角色不受影响,trait 角色完整切换 最终方案

四、关键失败现象与根源

1.身体更换顺利,头部非常麻烦

身体看起来只是 torso mesh,但头部系统包含更多东西:

  • head mesh
  • head skeleton
  • eyes
  • eyelids
  • teeth
  • tongue / mouth parts
  • expression morph
  • hair / beard / headgear attachment
  • portrait camera 所依赖的 head root

所以“显示一个女性头 mesh”并不等于“获得完整女性头部系统”。很多版本中头能出现,但眼球、牙齿、嘴巴、头发仍按男性 head 的位置或缓存运行,于是出现漂浮、错位、飞头。

image.png.24fdf963023406565db10e63b746c307.pngimage.png.da37e2f978ce0bbec75950057d3d4126.png

2. 男性动画驱动女性身体会破坏手

测试中反复确认:

  • 女性身体播放女性动画,手部正常。
  • 女性身体播放男性动画,手和肩部容易扭曲、穿模。
  • 一些移植的雄性头部本身有牙齿外露等问题。

这说明光有“女身体 + 男动画”根本不够,得让角色真正进入目标性别的原生动画路由才行。

image.png.ab3daa547727e87deb4c9b518d36db10.png

3. 隐藏男性身体不是删除男性系统

把男性 mesh 缩小、移走、压扁,只是在渲染层暂时欺骗眼睛。男性 torso/head 仍然存在,衣服、动画、pose source 仍可能绑定在它上面。只要进入 BB、切动画、换镜头、触发 morph,就会暴露残余。

后来我借鉴了黑暗亲王(Princes of Darkness)里变身狼人的做法,试着用 CBO 把原来的男性建模压小——思路是既然狼人变身能靠缩放把原始人体“藏”起来,那也许能用同样的手法把男性身体压到看不见。但结果印证了上面的判断:压小只是视觉上把它缩成一团,男性基础实体、骨架和附件契约都还在,一旦进入动画或 BB,缩小的男性身体又会以碎片、微缩人、残肢的形式冒出来。

这也是“微缩男性骨架”现象的根因:它看起来只是一个小碎片,但实际上是原男性基础实体还活着。

 

image.png.c76e2cc245d73a719b269ed4a38a39e8.pngimage.png.93e33d86d18831f21196c8a90c12db83.png

五、真正的突破:Hook portrait-type selector

Cheat Engine POC 找到的关键函数是 CK3 的原生 portrait type 选择器。它的逻辑大致是:

SPortraitType* ResolvePortraitType(
    SPortraitTypeGroup* group,
    int age,
    uint8_t portraitSex
);

其中 portraitSex 决定本次 portrait 构建要走 male 还是 female 类型。

早期错误尝试是在 portrait context 已经初始化以后替换 type,结果 sex-specific cache 已经按旧性别建好,容易崩溃。

正确位置是在 selector 入口处,也就是 context/gene/entity 初始化之前。

把指定角色的 portraitSex 从 male 改为 female,CK3 自己会正常选择女性 portrait type。这样得到的是完整原生女性链路:

  • female torso
  • female head
  • female skeleton
  • female eyes / teeth
  • female hair / clothes
  • female morph contract
  • female animation routing

同理,女性带 gm_full_male_test 时可以反向走 male portrait type。

六、最终方案

最终落地的方案彻底放弃了 mesh 替换,改走原生 DLL hook 这条路:

为什么最终选了 DLL 注入,而且特意挑了 version.dll?这里交代一下由来。最早我把 hook 写完之后,还得手动加载,体验很糟。后来在 LL 上看到一篇旧帖:在官方还不支持同性结婚的年代,有人直接改 ck3.exe、注入 DLL 来强行开启同性结婚.

https://waffleironer.medium.com/legalizing-gay-marriage-in-crusader-kings-iii-with-ghidra-2602e6aa8689

——这让我意识到,只要能在进程里挂上自己的代码,就能突破引擎原生的限制,给了我第一个启发。再往后我回想起来,CK2、EU4 时期中文本地玩家每次小更新都要重装"双字节补丁":那个补丁的本质就是替换 / 劫持 version.dll,从而突破原引擎的文字编码限制、在游戏内正常显示中文等双字节文字。既然"用 version.dll 做入口"已被前辈验证过能稳定旁加载、又不被游戏逻辑察觉,那我做 DLL 注入时也就顺理成章地选了 version.dll 作为注入点。

portrait build starts
    -> CK3 calls native portrait-type selector
        -> hook checks current character
            -> male + gm_full_female_test: route as female portrait type
            -> female + gm_full_male_test: route as male portrait type
            -> otherwise: keep vanilla route
    -> CK3 builds portrait normally using the selected native type

这个方案的关键点:

  • 不改角色逻辑性别。
  • 不复制大量 CBO/vanilla mesh。
  • 不手工拼接头、身体、眼球、牙齿。
  • 不让衣服挂到假骨架或微缩骨架上。
  • 让 CK3 自己完成目标性别的原生资源选择。

最终测试结果:

  • 普通男性正常。
  • 普通女性正常。
  • 带 gm_full_female_test 的男性完整变成女性模型。
  • 带 gm_full_male_test 的女性完整变成男性模型。
  • BB 中能看到目标性别原生模型和服装链路。
  • 早期的双身体、飞头、眼球漂浮、微缩男性残留问题消失。

image.png.bfede54fd5886cb32f1c094519f7cf4b.png

 

 

image.png.cd905140e807fdba2c4a4ca8fddeb6a2.pngimage.png.f3958888f1adc0b6593dfc07d3e88e0a.png

 

image.png.cbd464c0dce6b141de2d7936fcf0f348.pngimage.png.230d9bbe9c88578bbb2a699777f93ca0.png

 

七、角色设计器的特殊坑

角色设计器里的候选人不一定是正式 CCharacter。早期版本里只检查正式角色 trait,所以角色设计器中添加 trait 后不立刻生效。

后续修复方向是读取设计器窗口里的 trait slot 数组,而不是扫描整个窗口的 trait 指针。扫描整个窗口会留下旧 vector 容量里的残留指针,导致新建第二个角色继承上一个角色的模型状态。

这个坑说明 native hook 里不能只看“有没有某个 trait 指针出现过”,必须严格读取当前有效元素范围。

八、发布与兼容性经验

Native hook 最大的问题是版本绑定。

它不是普通 CK3 mod,不是放进 Documents\Paradox Interactive\Crusader Kings III\mod 就完事。它依赖 ck3.exe 内部函数地址和 ABI。

所以发布时必须说明:

  • 一个 CK3 版本对应一个 hook DLL。
  • 只看游戏版本号不够,最好校验 ck3.exe SHA-256。
  • CK3 更新后不能直接沿用旧 DLL。
  • DLL loader 可以通用,但真正的 hook DLL 需要按版本适配。
  • 不要同时加载旧 hook、Cheat Engine POC 和新 hook。
  • 不要把旧实验 mesh / asset 混入最终包。

另外补充一点关于 Cheat Engine 路线的经验:CE 扫描得到的内存地址是强绑定游戏版本和电脑型号的。我的电脑是 64 位 Windows,所以我估计只有同型号电脑才能直接用 CE 的定位结果;一旦跨版本、跨平台,基本会失效。至少我试过 CK3 1.19.0.4 和 CK3 1.19.0.6 两个小版本,直接扫出来的内存地址就已经有差异。所以我后续做了改进,加了一层兼容层来对抗地址漂移,但并不保证在所有环境下都能生效——这也是为什么最终还是收敛到按 exe hash 选版本化 DLL 的方案。

目前包内保留了两个已验证方向:

CK3 1.18.2: gm_portrait_hook_ck3_1.18.2.dll
CK3 1.19.0.4: gm_portrait_hook_ck3_1.19.0.4.dll

发布给普通玩家时,用“双字节补丁式”的加载:

把 version.dll 或启动器放在 ck3.exe 同目录,由 loader 根据 exe hash 选择对应 hook DLL。

九、给后来者的建议:如何复刻这套方案

如果你也想做"按特性切换完整原生性别模型",下面是我踩完坑后整理的可操作路径。

9.1 我们的 DLL 到底是怎么生效的

  • 旁加载 version.dll 代理:CK3 进程天然 import VERSION.dll。把我们的同名 DLL 放在 ck3.exe 同目录,它会被抢先加载;代理 DLL 转发所有真实 VERSION.dll 的导出,并在加载线程里用 MinHook 劫持渲染层。
  • 劫持肖像类型选择器:核心目标是 ResolvePortraitType(或等价函数),在 portrait context / gene / entity 初始化之前把决定肖像性别的 portrait_sex 参数改成标记 trait 指定的性别。
  • 标记 trait 是两层之间的契约:gm_full_female_test(男→女视觉)、gm_full_male_test(女→男视觉)这两个 trait 由脚本层加/去,DLL 只读取它们,不碰游戏逻辑 sex。
  • 结果:CK3 自己用目标性别的原生资源链路建模型——女性 torso / head / skeleton / 眼 / 牙 / 发 / 衣 / 动画路由全对。我们改的只是"这次构建选哪个 portrait type",游戏本身完成了其余工作。

9.2 换一个 CK3 版本要改哪里

一个 DLL 不可能靠硬编码地址通吃所有版本,我们的做法是三层兼容:

  1. Phase 1 精确匹配:manifest 里按版本号 + ck3.exe SHA-256 记录固定 RVA,已知版本直接命中。
  2. Phase 2 同 major.minor 回退:没有精确匹配时,同前缀小版本(如 1.19.0.6 复用 1.19.0.4 的 RVA)直接复用。
  3. Phase 3 特征码扫描:未知版本用 32 字节 pattern + 通配符 mask 扫 .text 段动态解析函数地址;任一签名不匹配,DLL 自动 ABORT,不装任何 Hook,不会比不加载更危险。

真正的 hook 本体 gm_hook_unified.dll 把所有版本的 RVA 集编译进一个 RvaSet 结构体,运行时按匹配结果选择,不再用条件编译。构建用 VS2019 14.29.30133 + Windows SDK 10.0.19041.0,build_ps.ps1 末尾自动把产物拷贝到部署目录。

9.3 给想做类似功能的人的提醒

  • 先确认抽象层对不对:目标是"换 portrait type 的选择结果",不是"换 mesh"。前者一个dll注入,后者是无底洞(头、眼、牙、衣、动画、pose source 全要手工拼)。
  • selector 入口要早:必须在 portrait context 初始化之前改 portrait_sex;context 建好之后再强换 type,sex-specific 缓存已经按旧性别建好,必然崩溃。
  • 别在游戏进程注册 VEH:CK3 用 C++ 异常做控制流恢复,顶层异常拦截会打断它导致崩溃。
  • 热路径禁止日志 I/O:Hook 函数每帧高频调用,任何 fopen/mutex 都会阻塞渲染线程拖垮游戏。
  • 特征码扫描不只是代码段地址:Hook 函数内部的"数据段句柄依赖""caller 地址硬编码"同样会因版本漂移而失效,要用 thread_local 传指针、用语义匹配替代固定 RVA。
  • 用标记 trait 解耦两层:引擎层只读 trait,脚本层只加/去 trait,Mod 自包含、不依赖修改别的 Mod 文件。

English Version

CK3 Trait-Based Full Native Gender Portrait Switching: Failed Routes, Test Results, and Final Solution

This is a technical postmortem for a CK3 / CBO / Gender Metamorphosis companion project.

The goal was not to attach a female body mesh on top of a male character. The goal was to make selected trait characters use the complete native portrait type of the opposite sex: body, head, skeleton, eyes, teeth, hair, clothes, morph contract, and animation routing.

Prologue: How This Started

During the development of Gender Metamorphosis (GM), I imagined many different approaches to sex change and went down quite a few wrong paths. I am writing down the approaches I tried and the detours I took, partly to give others working on similar mods a reference, and partly to invite more experienced modders to point out where I could do better.

To keep things stable at first, GM used a MESH modification method: attaching a female body onto the male body and doing morph replacements. This worked, but compatibility was always a hidden risk — because it essentially "modifies the body", every new attachment, animation, or mod ecosystem might require re-adaptation.

The turning point came when I saw the dude-looks-like-a-lady mod on LL. Its approach gave me a new idea: since CK3 already distinguishes male and female portraits at the rendering level, could we avoid touching the mesh and instead swap the male/female portrait type directly at the rendering layer, thereby achieving "trans male" and "trans female" from the root?

If this idea holds, compatibility would be far better than the mesh modification method: because it is a male/female swap at the game's rendering level rather than a body-mesh edit, it would naturally be compatible with mods like CBO and Phaze that depend on the native portrait ecosystem.

So I started this round of experiments.

Goal

  • A male character with gm_full_female_test should visually use the complete native female portrait type.
  • A female character with gm_full_male_test should visually use the complete native male portrait type.
  • The character's gameplay sex should remain unchanged.
  • Normal male and normal female characters should not be affected.
  • The result should work with CBO / Better Barbershop / Gender Metamorphosis use cases.

This goal sounds like "giving a male a female body", but after hitting real problems I found that CK3's body, head, pose source, attachments, DNA morph, and animation cache are one integrated system. Replacing only the mesh is the wrong abstraction layer.

One more note: the dude-looks-like-a-lady mod on LL works by global replacement — it switches all characters by sex at once, rather than replacing per trait. As for a per-trait replacement route, I tried it many times and the native engine does not seem to support it.

Why Mesh Replacement Was Not Enough

CK3 human portraits are roughly built like this:

character sex
    -> portrait_type
        -> torso entity
        -> head entity
        -> sex-specific genes / morphs / attachments / animation routing

common/portrait_types/00_human_types.txt statically maps sex to the base head and torso entities. I could not find any script effect such as:

set_portrait_type
change_portrait_type
set_portrait

So Jomini script and trait portrait modifiers can add or replace accessories and gene-driven visuals, but they cannot switch one specific traited male character to the native female portrait type at runtime.

That was the main discovery:

the script layer can do visual add-ons, but it cannot reliably perform a full native gender portrait switch.

The essence in one line: do not replace the body, replace the portrait type selection result. Every route, failure pattern and breakthrough that follows is just this sentence being proven over and over.

Tested Routes

Route Method Result Verdict
1. Add female body accessory Keep male base portrait and add female body through trait modifier Female body appears, but male head, skeleton and attachments remain Failed
2. game_entity_override = torso/head Try to override torso/head entities from a trait Double bodies, male head remains, female head missing, floating eyes/teeth Failed
3. no_portrait / hidden morph Move male mesh away using CBO-style no portrait mesh Works partially in static portraits, breaks in BB/full-body animations Failed
4. Custom hide blendshapes Shrink male body/head to tiny size Leaves fragments, tiny male body, morphs can pull parts back Failed
5. Scale male skeleton Play scaling animations on male body/head roots Tiny male skeleton still visible; clothes can remain attached to it Failed
6. Unified 230-bone female entity Merge female torso and head in Blender Naked body and some animations worked, but native attachments broke Partial success, not practical
7. Head pose proxy Add a standard 61-bone head proxy for attachments Minimal proxy caused C0000005 crashes Risky, not completed
8. Batch rebind attachments Rebind eyes, teeth, hair, clothes to the unified skeleton Massive workload and high maintenance cost Possible but not worth it
9. Cheat Engine portrait selector patch Change selector sex argument at runtime Global POC successfully routed males to female native portraits Correct abstraction proven
10. Native DLL hook Hook portrait-type selector and gate it by trait Final success Final solution

Important Failure Patterns

Body was easy, head was not

The torso looked easy because a body mesh can be attached or swapped visually. The head is a much larger system:

  • head mesh
  • head skeleton
  • eyes
  • eyelids
  • teeth
  • mouth parts
  • expression morphs
  • hair / beard / headgear attachments
  • portrait camera dependencies

Displaying a female head mesh is not the same thing as getting the complete native female head system.

 

image.png.24fdf963023406565db10e63b746c307.pngimage.png.da37e2f978ce0bbec75950057d3d4126.png

Female bodies need female animation routing

Repeated tests showed:

  • Female body + female animation: hands behaved correctly.
  • Female body + male animation: hands and shoulders often twisted or clipped.
  • Some ported male head animations had their own teeth exposure bugs.

So the real requirement was not "female mesh with male animation". The character needed to enter the target sex's native animation routing.

 

image.png.ab3daa547727e87deb4c9b518d36db10.png

Hiding the male mesh does not remove the male system

Shrinking, moving, or flattening the male mesh only hides it visually. The male torso/head entities still exist, and clothes, pose sources and animations may still bind to them. In BB or animation scenes, those hidden remnants can reappear.

Later I borrowed the werewolf transformation trick from Princes of Darkness and tried using CBO to shrink the original male model — the idea being that if werewolf transformation can "hide" the base human body by scaling, maybe the same approach could shrink the male body out of sight. But the result confirmed the point above: shrinking only scales it down to a tiny lump visually, while the male base entity, skeleton and attachment contract are all still there. Once an animation or BB kicks in, the shrunken male body reappears as fragments, a tiny person, or detached limbs.

This was the root cause of the tiny male skeleton issue.

 

 

image.png.c76e2cc245d73a719b269ed4a38a39e8.pngimage.png.93e33d86d18831f21196c8a90c12db83.png

 

The Breakthrough: Hook the Portrait-Type Selector

The Cheat Engine POC identified the native selector that chooses a portrait type. Its shape is roughly:

SPortraitType* ResolvePortraitType(
    SPortraitTypeGroup* group,
    int age,
    uint8_t portraitSex
);

The portraitSex argument decides whether this portrait build should use the male or female portrait type.

An earlier late-switch attempt changed the portrait type after the context had already been initialized. That was invalid because sex-specific gene/cache arrays were already built for the old sex, causing crashes.

The correct point is the selector entry, before context/gene/entity initialization. If a selected male character's portraitSex is changed from male to female there, CK3 naturally selects and initializes the native female portrait type.

That gives the complete native chain:

  • female torso
  • female head
  • female skeleton
  • female eyes / teeth
  • female hair / clothes
  • female morph contract
  • female animation routing

The reverse direction works the same way for female-to-male.

Final Solution

The final solution is a native DLL hook, not a mesh replacement:

Why did I end up with DLL injection, and why specifically version.dll? Here is the backstory. The first version of my hook still had to be loaded manually, which was a terrible experience. Later I saw an old post on LL: back when the game did not officially support same-sex marriage, people patched ck3.exe and injected a DLL to force-enable it

 

 

— that showed me that once you can run your own code inside the process, you can break the engine's native limits, and it gave me the first spark. Then I recalled the "double-byte patch" that Chinese and Japanese players had to reinstall after every small update in the CK2 and EU4 eras: that patch essentially replaced / hijacked version.dll to break the engine's text-encoding limit and display double-byte characters like Chinese in-game. Since "using version.dll as the entry point" was already proven by predecessors to side-load stably and stay invisible to game logic, it was only natural for me to pick version.dll as the injection point when I built the DLL injection.

portrait build starts
    -> CK3 calls the native portrait-type selector
        -> hook checks the current character
            -> male + gm_full_female_test: route as female portrait type
            -> female + gm_full_male_test: route as male portrait type
            -> otherwise: keep vanilla route
    -> CK3 builds the portrait normally using the selected native type

The hook does not change gameplay sex. It only changes the portrait type selected for rendering.

Final verified behavior:

  • Normal males remain normal.
  • Normal females remain normal.
  • Males with gm_full_female_test use the complete native female model.
  • Females with gm_full_male_test use the complete native male model.
  • Better Barbershop shows the target native model and clothing chain.
  • The old double-body, floating eyes, detached head, and tiny male remnant issues are gone.

 

image.png.bfede54fd5886cb32f1c094519f7cf4b.png

 

 

image.png.cd905140e807fdba2c4a4ca8fddeb6a2.pngimage.png.f3958888f1adc0b6593dfc07d3e88e0a.png

 

 

 

 

 

 

 

image.png.cbd464c0dce6b141de2d7936fcf0f348.pngimage.png.230d9bbe9c88578bbb2a699777f93ca0.png

Ruler Designer Pitfall

Ruler Designer candidates are not always normal CCharacter objects. Early hook versions checked only regular character traits, so adding the trait in the designer did not immediately update the model.

Another bug came from scanning the whole designer window for trait pointers. Removed vector elements could leave stale pointers in unused capacity, causing newly created characters to inherit the previous model state.

The fix was to read only the active trait slot arrays and only the valid element range.

Release and Compatibility Notes

This is not a normal CK3 mod file. It depends on native ck3.exe function addresses and ABI details.

Release rules:

  • One CK3 build needs one matching hook DLL.
  • Game version label alone is not enough; verify the ck3.exe hash.
  • After a CK3 update, do not reuse the old DLL blindly.
  • The loader can be generic, but the actual hook DLL is version-specific.
  • Do not run an old hook, the CE prototype, and the current hook at the same time.
  • Do not package old experimental mesh/entity files into the final release.

One more note from the Cheat Engine route: the memory addresses CE scans are strongly bound to both the game version and the computer model. My PC is 64-bit Windows, so I estimate only the same model of PC can directly reuse CE's located addresses; across versions or platforms it should simply fail. At the very least, I tried CK3 1.19.0.4 and CK3 1.19.0.6 — two minor versions — and the directly scanned memory addresses already differed. So I later improved the approach and added a compatibility layer to resist address drift, but I do not guarantee it works in every environment — which is also why the final design converges on choosing a versioned DLL by exe hash.

Currently validated examples:

CK3 1.18.2: gm_portrait_hook_ck3_1.18.2.dll
CK3 1.19.0.4: gm_portrait_hook_ck3_1.19.0.4.dll

For normal users, the best installation style is similar to CK3 double-byte/native text patches: place the loader next to ck3.exe, and let it choose the correct hook DLL by executable hash.

Advice for Future Experiments: How to Replicate This

If you also want "trait-based full native gender portrait switching", here is the actionable path after my detours, not just a hand-wavy "go hook it".

How our DLL actually takes effect

  • Side-load a version.dll proxy: CK3 natively import VERSION.dll. Placing our same-named DLL next to ck3.exe makes it load first; the proxy forwards all real VERSION.dll exports and uses MinHook in its loader thread to hijack the render layer.
  • Hook the portrait-type selector: the core target is ResolvePortraitType (or equivalent). Before portrait context / gene / entity initialization, it rewrites the portrait_sex argument — the parameter that decides the portrait sex — to the sex specified by the marker trait.
  • Marker traits are the contract between the two layers: gm_full_female_test (male→female visual) and gm_full_male_test (female→male visual) are added/removed by the script layer; the DLL only reads them and never touches the gameplay sex.
  • Result: CK3 itself builds the model using the target sex's native resource chain — female torso / head / skeleton / eyes / teeth / hair / clothes / animation routing all correct. We only changed "which portrait type to build this time"; the game did the rest.

What to change when porting to a new CK3 version

One DLL cannot hard-code addresses across all versions. Our approach is three-layer compatibility:

  1. Phase 1 exact match: the manifest records a fixed RVA by version number + ck3.exe SHA-256; known versions hit directly.
  2. Phase 2 same major.minor fallback: without an exact match, a same-prefix minor version (e.g. 1.19.0.6 reuses 1.19.0.4's RVA) is reused directly.
  3. Phase 3 signature scan: unknown versions use a 32-byte pattern + wildcard mask over the .text section to resolve function addresses dynamically; if any signature fails to match, the DLL auto-ABORTs, installs no hooks, and is no more dangerous than not loading at all.

The real hook body gm_hook_unified.dll compiles every version's RVA set into one RvaSet struct and picks at runtime by match result, replacing the old conditional compilation. Build with VS2019 14.29.30133 + Windows SDK 10.0.19041.0; build_ps.ps1 auto-copies the output to the deploy directory at the end.

Reminders for anyone building something similar

  • Check the abstraction layer first: the goal is "replace the portrait type selection result", not "replace the mesh". The former is one line; the latter is a bottomless pit (head, eyes, teeth, clothes, animation, pose source all need manual wiring).
  • Hook the selector early: you must rewrite portrait_sex before portrait context initialization; switching the type after the context is built crashes, because the sex-specific cache is already built for the old sex.
  • Do not register a VEH in the game process: CK3 uses C++ exceptions for control-flow recovery; a top-level exception handler breaks it and crashes.
  • No logging I/O on hot paths: the hook function is called every frame; any fopen/mutex blocks the render thread and kills the game.
  • Signature scanning is not just code addresses: the hook function's internal "data-section handle dependency" and "hard-coded caller address" also drift across versions; pass pointers via thread_local and use semantic matching instead of fixed RVAs.
  • Use marker traits to decouple the two layers: the engine layer only reads traits, the script layer only adds/removes them; the mod is self-contained and does not depend on editing other mods' files.
Edited by saleiyi
Posted (edited)
2 hours ago, Phaze Star said:

This is a lot of work and extensive research!
A lot of it has gone over my head I think 😅
But it is very impressive all the same!

Thank you for your support; this is closer to experimental work.😀

Edited by saleiyi

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...