dsh-hoi4-modding 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +14 -1
- package/package.json +6 -3
- package/skills/hoi4-modding/INSTALL.md +2 -1
- package/skills/hoi4-modding/SKILL.md +21 -1
- package/skills/hoi4-modding/references/cheatsheet.md +57 -0
- package/skills/hoi4-modding/references/icon-dds-pipeline.md +123 -0
- package/skills/hoi4-modding/references/tank-designer.md +215 -0
- package/skills/hoi4-modding/references/tank-models-3d.md +154 -0
- package/skills/hoi4-modding/references/tech-tree-ui.md +45 -0
- package/skills/hoi4-modding/scripts/tank_designer_layout.py +234 -0
- package/skills/hoi4-modding/scripts/tank_designer_probe.py +280 -0
- package/skills/hoi4-modding/scripts/tech_icon_import.py +269 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# 坦克 3D 模型接入(HOI4 1.19.3 实战)
|
|
2
|
+
|
|
3
|
+
> 适用:给自定义坦克/底盘换 3D 模型、复用原版模型、修“模型不显示/贴图丢失/炮管不转”。
|
|
4
|
+
> 原则:**先复用原版实体把玩法跑通,再考虑自制 mesh**。
|
|
5
|
+
|
|
6
|
+
## 0. 一句话
|
|
7
|
+
|
|
8
|
+
一辆坦克的 3D 显示 = `graphic_db` 的 `models = { ENTITY }` → `gfx/entities/*.asset` 里的 `entity`(引用 `pdxmesh`)→
|
|
9
|
+
`gfx/entities/*.gfx` 里的 `pdxmesh`(引用 `.mesh` + `.anim` + 贴图)→ `gfx/models/units/tanks/*.mesh` + `*.dds`。
|
|
10
|
+
|
|
11
|
+
## 1. 四层文件
|
|
12
|
+
|
|
13
|
+
| 层 | 文件 | 关键字段 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 数据 | `gfx/interface/equipmentdesigner/graphic_db/*.txt` | `models = { GER_medium_armor_entity }`(可多个随机) |
|
|
16
|
+
| 实体 | `gfx/entities/units_tanks.asset` | `entity = { name = ... pdxmesh = ... }` + 动画状态 |
|
|
17
|
+
| mesh 声明 | `gfx/entities/tanks.gfx` | `pdxmesh = { name = ... file = "gfx/models/units/tanks/xxx.mesh" }` |
|
|
18
|
+
| 资源 | `gfx/models/units/tanks/` | `.mesh`(几何/骨骼)+ `.anim`(动画)+ `.dds`(贴图) |
|
|
19
|
+
|
|
20
|
+
原版德国坦克实体(可直接复用,省掉建模):
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
GER_iw_armor_entity # 一号/二号这类轻装甲
|
|
24
|
+
GER_light_armor_entity
|
|
25
|
+
GER_medium_armor_entity # 三号/四号/黑豹
|
|
26
|
+
GER_heavy_armor_entity # 虎式/虎王
|
|
27
|
+
GER_super_heavy_armor_entity # 鼠式
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 2. entity 定义示例(`.asset`)
|
|
31
|
+
|
|
32
|
+
```pdx
|
|
33
|
+
entity = {
|
|
34
|
+
name = "GER_medium_armor_entity"
|
|
35
|
+
pdxmesh = "GER_tank_medium_mesh"
|
|
36
|
+
|
|
37
|
+
state = { name = "idle" animation = "idle_1" animation_blend_time = 0.4 }
|
|
38
|
+
state = { name = "move" animation = "move_1" animation_blend_time = 0.4 }
|
|
39
|
+
state = { name = "attack" animation = "attack_1" animation_blend_time = 0.2 }
|
|
40
|
+
state = { name = "defend" animation = "defend_1" }
|
|
41
|
+
state = { name = "support_attack" animation = "attack_1" }
|
|
42
|
+
state = { name = "training" animation = "idle_1" }
|
|
43
|
+
state = { name = "death" animation = "death_1" }
|
|
44
|
+
|
|
45
|
+
# 特殊效果节点(必须是 mesh 里存在的空节点名)
|
|
46
|
+
# barrel / left_tracks / right_tracks / left_exhaust / right_exhaust
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `pdxmesh` 名字要与 `.gfx` 里注册的一致;`file` 路径相对 `gfx/models/`。
|
|
51
|
+
- 动画缺失会导致模型静止/报错;最少要 `idle`。
|
|
52
|
+
- 模型比例/朝向:原版坦克朝向 **+Z / 车头方向**,单位约等于米;自制模型导出前先在 Maya/Blender 里
|
|
53
|
+
统一 scale(10~14 之间常见)与原点(车体中心地面)。
|
|
54
|
+
|
|
55
|
+
## 3. 贴图规范
|
|
56
|
+
|
|
57
|
+
原版命名(同目录、同名后缀):
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
GER_tank_medium_diffuse.dds # RGB + alpha
|
|
61
|
+
GER_tank_medium_normal.dds # 法线
|
|
62
|
+
GER_tank_medium_specular.dds # 高光
|
|
63
|
+
GER_tank_medium_diffuse_desert.dds / _snow.dds # 可选季节/地形变体
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- 尺寸没有硬性 2 的幂要求,但建议 **1024/2048 的正方形或 2:1**;`.dds` 用 DXT5/BC3(带 alpha 用 DXT5,纯 diffuse 用 DXT1)。
|
|
67
|
+
- 自制模型最容易踩:**法线 (normal) 没转成切线空间/绿通道反了**(看起来“塑料感”或凹凸反了)。
|
|
68
|
+
HOI4 用 DXT5 存 normal(`ATI2/BC5` 也可),Y 通道按 DirectX 约定。
|
|
69
|
+
- 贴图走了 `gfx/models/...`,**不是** `gfx/interface/`;2D 图标/界面贴图是另一套。
|
|
70
|
+
|
|
71
|
+
## 4. 特殊节点(特效/动画挂点)
|
|
72
|
+
|
|
73
|
+
mesh 里需要存在这些同名 locator/空节点,引擎才能把特效挂在正确位置:
|
|
74
|
+
|
|
75
|
+
| 节点 | 用途 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `barrel` | 炮口火焰、炮管后坐 |
|
|
78
|
+
| `left_tracks` / `right_tracks` | 履带扬尘 |
|
|
79
|
+
| `left_exhaust` / `right_exhaust` | 排气烟 |
|
|
80
|
+
|
|
81
|
+
节点名与 entity 里引用的名称要一致,否则特效不出(日志通常有 `Failed to find locator` 类警告)。
|
|
82
|
+
|
|
83
|
+
## 5. 三条路线
|
|
84
|
+
|
|
85
|
+
### 路线 A:复用原版实体(最快)
|
|
86
|
+
在 mod 的 `graphic_db` 里给自定义底盘指同阵营/同吨位实体:
|
|
87
|
+
|
|
88
|
+
```pdx
|
|
89
|
+
GER_armor_medium_tank_2 = {
|
|
90
|
+
# ...
|
|
91
|
+
graphic_db = {
|
|
92
|
+
GFX_HTT_medium_tank_2_icon
|
|
93
|
+
models = { GER_medium_armor_entity }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
优点:零建模、动画/特效/国家涂装全包;缺点:30 辆坦克只有 5 张模型脸。
|
|
99
|
+
建议按吨位/年份分配:一号/二号→`GER_iw_armor_entity`,三/四号→`GER_light_armor_entity`/`GER_medium_armor_entity`,
|
|
100
|
+
黑豹/虎→`GER_medium_armor_entity`/`GER_heavy_armor_entity`,鼠/E-100→`GER_super_heavy_armor_entity`。
|
|
101
|
+
|
|
102
|
+
### 路线 B:换贴图(伪新模型)
|
|
103
|
+
复制一套原版 `.mesh/.anim` 到 mod(保持路径/名称),只替换 `_diffuse/_normal/_specular.dds`。
|
|
104
|
+
游戏内表现为“新涂装/新迷彩”,适合战役改色、夏冬涂装、部队专属涂装。
|
|
105
|
+
|
|
106
|
+
### 路线 C:自制 mesh(成本最高)
|
|
107
|
+
1. 建模/绑定(Maya 或 Blender),命名层级、原点、朝向按官方导出器要求;
|
|
108
|
+
2. 官方工具:Paradox Clausewitz Maya Exporter(Maya 版);社区:Blender 的
|
|
109
|
+
`io_scene_hoi4` / `Clausewitz-Maya-Exporter` 对应插件;
|
|
110
|
+
3. 导出 `.mesh` + `.anim`,贴图转 `.dds`;
|
|
111
|
+
4. 在 `.gfx` 注册 `pdxmesh`,在 `.asset` 写 `entity`(含全部 `state`);
|
|
112
|
+
5. `graphic_db` 里 `models = { 新 entity }`;
|
|
113
|
+
6. 进游戏用 `debug` 模式观察(模型比例/位置不对用 `nudge` 微调)。
|
|
114
|
+
|
|
115
|
+
> 自制模型常见失败:mesh 版本/头格式不对(导出器版本对不上 1.19)、缺 `pdxmesh` 注册、
|
|
116
|
+
> 动画名不存在、贴图路径大小写错(Linux/Proton 下大小写敏感)。
|
|
117
|
+
|
|
118
|
+
## 6. 2D 图标 ≠ 3D 模型
|
|
119
|
+
|
|
120
|
+
- 设计器右上角预览、科技卡图标是 **2D**(`gfx/interface/...` 下 DDS,经 `interface/*.gfx` 注册);
|
|
121
|
+
- 设计器中间/右下 3D 视图用 `models` 实体;
|
|
122
|
+
- “选择模型”按钮(`select3d_button` / `GFX_3d_view_placeholder`)走原版控件,不改 GUI 就能显示。
|
|
123
|
+
|
|
124
|
+
装备的 2D 图标池写法:
|
|
125
|
+
|
|
126
|
+
```pdx
|
|
127
|
+
graphic_db = {
|
|
128
|
+
GFX_HTT_medium_tank_2_icon
|
|
129
|
+
models = { GER_medium_armor_entity }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`GFX_..._icon` 是 `interface/*.gfx` 里的 sprite;`models` 是 3D。
|
|
134
|
+
|
|
135
|
+
## 7. 排错表
|
|
136
|
+
|
|
137
|
+
| 症状 | 原因 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| 设计器 3D 区域空白/黑块 | `models` 引用的 entity 不存在,或 `.gfx` 未注册 pdxmesh |
|
|
140
|
+
| 模型全黑 | diffuse 丢失/路径错;normal 缺失一般不会全黑 |
|
|
141
|
+
| 模型贴图花/发光 | 贴图格式(DXT)与 alpha 通道错;normal 不是切线空间 |
|
|
142
|
+
| 炮管/履带特效错位 | mesh 缺 `barrel`/`left_tracks` 节点或命名不一致 |
|
|
143
|
+
| 模型骑在地上/浮空/过大 | 导出 scale、原点、朝向不对;用 nudge 验证 |
|
|
144
|
+
| 模型是国家专属涂装 | 原版实体按国家分配贴图;想统一就用通用实体或自制 |
|
|
145
|
+
| Proton/Linux 下贴图丢 | 路径大小写不一致 |
|
|
146
|
+
|
|
147
|
+
## 8. 验收清单
|
|
148
|
+
|
|
149
|
+
- [ ] `graphic_db` 的 `models = { }` 实体在 `.asset` 中能找到;
|
|
150
|
+
- [ ] `.asset` 引用的 `pdxmesh` 在 `.gfx` 注册,`.mesh` 文件存在;
|
|
151
|
+
- [ ] entity 至少有 `idle`/`move`/`attack` 三个 state;
|
|
152
|
+
- [ ] 贴图 `.dds` 齐全(diffuse 必须,normal/specular 建议);
|
|
153
|
+
- [ ] 进游戏设计器/生产界面能看到模型,无 error.log 贴图/entity 报错;
|
|
154
|
+
- [ ] 旧存档与新开档都检查(生产中的旧装备可能保留旧模型)。
|
|
@@ -255,3 +255,48 @@ python scripts/item_sprite_check.py "<Mod目录>" --folder my_folder
|
|
|
255
255
|
- [ ] `tech_tree_audit.py` 报告 0 问题;
|
|
256
256
|
- [ ] error.log 里没有 `Found no grid box`、`Trying to draw a path in Folder`;
|
|
257
257
|
- [ ] 跨页 path 是否可接受(不能接受就合并页面)。
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
## 9. 多根分支(独立分支根)
|
|
261
|
+
|
|
262
|
+
一个科技页可以有多个 `<root>_tree` 网格框(多个 root),用于表达“互不相连的几条研发线”,
|
|
263
|
+
例如:一号坦克线(`GER_pz1_a` 根)+ 捷克 35(t) 线(`GER_pz35t` 根)。
|
|
264
|
+
|
|
265
|
+
```pdx
|
|
266
|
+
# interface/countrytechtreeview.gui(同一页里两个 gridbox)
|
|
267
|
+
gridboxtype = {
|
|
268
|
+
name = "GER_armor_tree"
|
|
269
|
+
position = { x = 180 y = 70 }
|
|
270
|
+
slotsize = { width = 96 height = 82 }
|
|
271
|
+
# ...
|
|
272
|
+
}
|
|
273
|
+
gridboxtype = {
|
|
274
|
+
name = "GER_pz35t_tree" # 第二个根网格,可不同原点
|
|
275
|
+
position = { x = 180 y = 610 }
|
|
276
|
+
slotsize = { width = 96 height = 82 }
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
数据侧:根技术 `path = { ... }` 的第一项决定了它画在哪个网格——
|
|
281
|
+
**`path` 里出现哪个 root 网格的 id,就连到哪个网格**,与 `dependencies` 无关。
|
|
282
|
+
|
|
283
|
+
- 每个 root 的 `position` 独立,两个网格可以上下/左右排;
|
|
284
|
+
- 同一页多个 root 时,**所有 root 都必须有对应 gridbox**,少一个 → 该根下所有科技报
|
|
285
|
+
`Found no grid box for tech ...`(该科技方框+线被跳过,日志有这条);
|
|
286
|
+
- 年份行号/列号在整个页内是**每个 root 各自独立**的,不要混用坐标。
|
|
287
|
+
- 生成器/手工都适用:只要 `folder` 注册了页面,页面里挂几个 `_tree` 都行。
|
|
288
|
+
|
|
289
|
+
## 10. 生成文件被外部改写的坑(科技树 + 生成式 Mod)
|
|
290
|
+
|
|
291
|
+
用脚本生成 `_data/*.json` → GUI/科技/装备文件的项目,常见事故:
|
|
292
|
+
**生成好的 `common/technologies/<tree>.txt` 被手工/其它工具改写,丢了 `path`/`dependencies`/`folder` 等字段**,
|
|
293
|
+
游戏加载时该科技找不到网格 → 报 `Found no grid box for tech X`(X 可能几十条),但文件本身语法合法。
|
|
294
|
+
|
|
295
|
+
排查顺序:
|
|
296
|
+
1. `common/technology_tags/00_technology.txt` 里 `folder` 是否注册、`available` 是否 `original_tag`;
|
|
297
|
+
2. 页面 GUI 里是否有该科技所属 `root` 的 gridbox;
|
|
298
|
+
3. 科技条目是否仍有 `path = { ... }`(每段第一个元素是 root 网格 id)与 `folder = <folder>`;
|
|
299
|
+
4. 对比 `_data` 源与生成文件:生成文件被改写后,重新生成并比对哈希即可定位。
|
|
300
|
+
|
|
301
|
+
**规矩:生成文件只由生成器写;要改先改数据源。** 生成器里给所有生成文件加统一前缀(如 `zz_`)
|
|
302
|
+
还能顺带解决“加载顺序早于原版定义”导致的 `Malformed token`(例如自定义底盘要晚于原版 `tank_chassis.txt` 解析)。
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""生成自定义槽位布局的 tank_designer_view.gui(坦克设计器)。
|
|
4
|
+
|
|
5
|
+
从原版 GUI 复制并改写,不修改游戏目录;输出到 mod 的 interface/ 下。
|
|
6
|
+
默认预设 = 16 槽紧凑布局(窗口高度不变 542):
|
|
7
|
+
上排 7 槽(y=0) + 第二排 2 槽(通信/工程,y=50,蓝图两角) + 下排 7 槽(y=300)
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python tank_designer_layout.py --mod "C:/.../my_mod" # 写入 mod/interface/tank_designer_view.gui
|
|
11
|
+
python tank_designer_layout.py --out ./tmp.gui --dry-run # 只看将生成什么
|
|
12
|
+
python tank_designer_layout.py --mod ... --preset 16-tall --background-sprite GFX_NS_bg
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
try:
|
|
23
|
+
from _paths import find_game_dir
|
|
24
|
+
except Exception: # pragma: no cover
|
|
25
|
+
def find_game_dir():
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
POS_PAT = re.compile(r'position\s*=\s*\{\s*x\s*=\s*(-?\d+)\s*y\s*=\s*(-?\d+)\s*\}')
|
|
29
|
+
|
|
30
|
+
SLOT_W = 76 # equipment_designer_module_slot_entry 的实际宽度
|
|
31
|
+
BOTTOM_ELEMENTS = [
|
|
32
|
+
"reset_button", "duplicate_button", "save_button", "experience_bg", "experience",
|
|
33
|
+
"resources_bg", "production_resources", "refit_cost", "production_value",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
PRESETS = {
|
|
37
|
+
"16-compact": dict(
|
|
38
|
+
window_h=542,
|
|
39
|
+
modules_size=(512, 350),
|
|
40
|
+
preview=(3, 50),
|
|
41
|
+
rows=(0, 50, 300),
|
|
42
|
+
note="16 槽紧凑版:不加高窗口,第二排两个槽压在蓝图顶部左右角",
|
|
43
|
+
),
|
|
44
|
+
"16-tall": dict(
|
|
45
|
+
window_h=760,
|
|
46
|
+
modules_size=(512, 570),
|
|
47
|
+
preview=(2, 182),
|
|
48
|
+
rows=(22, 100, 486),
|
|
49
|
+
note="16 槽加高版:需要自己提供加高后的窗口背景 sprite(否则内容溢出)",
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read(p: Path) -> str:
|
|
55
|
+
with open(p, "r", encoding="utf-8", newline="") as f:
|
|
56
|
+
return f.read()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def hide(text: str, name: str) -> str:
|
|
60
|
+
start = text.find(f'name = "{name}"')
|
|
61
|
+
if start < 0:
|
|
62
|
+
start = text.find(f'name="{name}"')
|
|
63
|
+
if start < 0:
|
|
64
|
+
print(f"[!] 元素不存在,跳过:{name}")
|
|
65
|
+
return text
|
|
66
|
+
m = POS_PAT.search(text, start)
|
|
67
|
+
if not m:
|
|
68
|
+
print(f"[!] 找不到 position,跳过:{name}")
|
|
69
|
+
return text
|
|
70
|
+
return text[:m.start()] + "position = { x = -4000 y = -4000 }" + text[m.end():]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def shift(text: str, name: str, dy: int) -> str:
|
|
74
|
+
if dy == 0:
|
|
75
|
+
return text
|
|
76
|
+
start = text.find(f'name = "{name}"')
|
|
77
|
+
if start < 0:
|
|
78
|
+
start = text.find(f'name="{name}"')
|
|
79
|
+
if start < 0:
|
|
80
|
+
print(f"[!] 元素不存在,跳过:{name}")
|
|
81
|
+
return text
|
|
82
|
+
m = POS_PAT.search(text, start)
|
|
83
|
+
if not m:
|
|
84
|
+
print(f"[!] 找不到 position,跳过:{name}")
|
|
85
|
+
return text
|
|
86
|
+
x, y = int(m.group(1)), int(m.group(2))
|
|
87
|
+
if x <= -1000:
|
|
88
|
+
return text # 已隐藏
|
|
89
|
+
return text[:m.start()] + f"position = {{ x = {x} y = {y + dy} }}" + text[m.end():]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_slot_section(rows: tuple[int, int, int]) -> str:
|
|
93
|
+
r0, r1, r2 = rows
|
|
94
|
+
lines = ["\t\t\t@fixed_btn_mod_col_0 = 0"]
|
|
95
|
+
for i in range(1, 7):
|
|
96
|
+
lines.append(f"\t\t\t@fixed_btn_mod_col_{i} = {72 * i}")
|
|
97
|
+
lines += [
|
|
98
|
+
"",
|
|
99
|
+
f"\t\t\t@fixed_btn_mod_row_0 = {r0}",
|
|
100
|
+
f"\t\t\t@fixed_btn_mod_row_1 = {r1}",
|
|
101
|
+
f"\t\t\t@fixed_btn_mod_row_2 = {r2}",
|
|
102
|
+
"",
|
|
103
|
+
"\t\t\tpositionType = {",
|
|
104
|
+
'\t\t\t\tname = "ypos_module_row_0"',
|
|
105
|
+
"\t\t\t\tposition = { x=0 y=@fixed_btn_mod_row_0 }",
|
|
106
|
+
"\t\t\t}",
|
|
107
|
+
"",
|
|
108
|
+
"\t\t\t# 上排 7 槽(0-6)",
|
|
109
|
+
]
|
|
110
|
+
for i in range(7):
|
|
111
|
+
lines += [
|
|
112
|
+
"\t\t\tpositionType = {",
|
|
113
|
+
f'\t\t\t\tname = "pos_custom_module_slot_window_{i}"',
|
|
114
|
+
f"\t\t\t\tposition = {{ x=@fixed_btn_mod_col_{i} y=@fixed_btn_mod_row_0 }}",
|
|
115
|
+
"\t\t\t}",
|
|
116
|
+
]
|
|
117
|
+
lines += [
|
|
118
|
+
"",
|
|
119
|
+
"\t\t\t# 第二排 2 槽(7=通信/左角,8=工程/右角)",
|
|
120
|
+
"\t\t\tpositionType = {",
|
|
121
|
+
'\t\t\t\tname = "pos_custom_module_slot_window_7"',
|
|
122
|
+
"\t\t\t\tposition = { x=@fixed_btn_mod_col_0 y=@fixed_btn_mod_row_1 }",
|
|
123
|
+
"\t\t\t}",
|
|
124
|
+
"\t\t\tpositionType = {",
|
|
125
|
+
'\t\t\t\tname = "pos_custom_module_slot_window_8"',
|
|
126
|
+
"\t\t\t\tposition = { x=@fixed_btn_mod_col_6 y=@fixed_btn_mod_row_1 }",
|
|
127
|
+
"\t\t\t}",
|
|
128
|
+
"",
|
|
129
|
+
"\t\t\t# 下排 7 槽(9-15)",
|
|
130
|
+
]
|
|
131
|
+
for i in range(7):
|
|
132
|
+
lines += [
|
|
133
|
+
"\t\t\tpositionType = {",
|
|
134
|
+
f'\t\t\t\tname = "pos_custom_module_slot_window_{9 + i}"',
|
|
135
|
+
f"\t\t\t\tposition = {{ x=@fixed_btn_mod_col_{i} y=@fixed_btn_mod_row_2 }}",
|
|
136
|
+
"\t\t\t}",
|
|
137
|
+
]
|
|
138
|
+
return "\n".join(lines) + "\n\n\t\t\t######################################################################\n\t\t\t"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def main() -> int:
|
|
142
|
+
ap = argparse.ArgumentParser(description="生成自定义坦克设计器布局(不修改游戏目录)")
|
|
143
|
+
ap.add_argument("--game", default=None)
|
|
144
|
+
ap.add_argument("--mod", default=None, help="mod 根目录(输出到 interface/tank_designer_view.gui)")
|
|
145
|
+
ap.add_argument("--out", default=None, help="直接指定输出 gui 路径")
|
|
146
|
+
ap.add_argument("--preset", default="16-compact", choices=sorted(PRESETS))
|
|
147
|
+
ap.add_argument("--height", type=int, default=None, help="覆盖窗口高度")
|
|
148
|
+
ap.add_argument("--background-sprite", default=None, help="替换窗口背景精灵名(加高窗口时必填)")
|
|
149
|
+
ap.add_argument("--dry-run", action="store_true")
|
|
150
|
+
args = ap.parse_args()
|
|
151
|
+
|
|
152
|
+
game = Path(args.game) if args.game else find_game_dir()
|
|
153
|
+
if not game or not (game / "interface" / "tank_designer_view.gui").exists():
|
|
154
|
+
print("[!] 找不到游戏目录,请用 --game 指定")
|
|
155
|
+
return 2
|
|
156
|
+
src = game / "interface" / "tank_designer_view.gui"
|
|
157
|
+
cfg = PRESETS[args.preset]
|
|
158
|
+
window_h = args.height or cfg["window_h"]
|
|
159
|
+
mod_w, mod_h = cfg["modules_size"]
|
|
160
|
+
px, py = cfg["preview"]
|
|
161
|
+
rows = cfg["rows"]
|
|
162
|
+
|
|
163
|
+
out = Path(args.out) if args.out else None
|
|
164
|
+
if not out and args.mod:
|
|
165
|
+
out = Path(args.mod) / "interface" / "tank_designer_view.gui"
|
|
166
|
+
if not out:
|
|
167
|
+
print("[!] 请给 --mod 或 --out")
|
|
168
|
+
return 2
|
|
169
|
+
out = out.resolve()
|
|
170
|
+
try:
|
|
171
|
+
out.relative_to(game.resolve())
|
|
172
|
+
print("[!] 拒绝写入游戏目录;请输出到 mod 目录")
|
|
173
|
+
return 2
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
text = read(src)
|
|
178
|
+
|
|
179
|
+
# 1) 窗口高度
|
|
180
|
+
text = text.replace("size = { width = 1091 height = 542 }",
|
|
181
|
+
f"size = {{ width = 1091 height = {window_h} }}", 1)
|
|
182
|
+
# 2) 模块容器 / 蓝图
|
|
183
|
+
text = re.sub(r'(name = "equipment_modules"\s*\n\s*position = \{[^}]*\}\s*\n\s*size = \{)[^}]*(\})',
|
|
184
|
+
lambda m: m.group(1) + f" width={mod_w} height={mod_h}" + m.group(2),
|
|
185
|
+
text, count=1)
|
|
186
|
+
text = re.sub(r'(name = "equipment_preview"\s*\n\s*position = \{)[^}]*\}',
|
|
187
|
+
lambda m: m.group(1) + f" x={px} y={py} }}", text, count=1)
|
|
188
|
+
# 3) 槽位位置段
|
|
189
|
+
start = text.find("@fixed_btn_mod_col_0 = 1")
|
|
190
|
+
end = text.find("# Equipment upgrades", start)
|
|
191
|
+
if start < 0 or end < 0:
|
|
192
|
+
print("[!] 原版 GUI 结构变化:找不到槽位段/升级区标记")
|
|
193
|
+
return 2
|
|
194
|
+
text = text[:start] + build_slot_section(rows) + text[end:]
|
|
195
|
+
# 4) 背景
|
|
196
|
+
if args.background_sprite:
|
|
197
|
+
text = text.replace('spriteType ="GFX_tank_designer_popup_bg"',
|
|
198
|
+
f'spriteType ="{args.background_sprite}"', 1)
|
|
199
|
+
# 5) 隐藏原版控件
|
|
200
|
+
for name in ("equipment_upgrade_0", "equipment_upgrade_1",
|
|
201
|
+
"auto_upgrade_button", "auto_button", "auto_upgraded_checkbox_button",
|
|
202
|
+
"niche_button", "tag_icon_bg"):
|
|
203
|
+
text = hide(text, name)
|
|
204
|
+
# 6) 加高时底部元素一起下移
|
|
205
|
+
dy = window_h - 542
|
|
206
|
+
for name in BOTTOM_ELEMENTS:
|
|
207
|
+
text = shift(text, name, dy)
|
|
208
|
+
|
|
209
|
+
# 校验括号
|
|
210
|
+
if text.count("{") != text.count("}"):
|
|
211
|
+
print(f"[!] 生成的 GUI 括号不配平:{{={text.count('{')} }}={text.count('}')},已中止")
|
|
212
|
+
return 2
|
|
213
|
+
|
|
214
|
+
# 报告
|
|
215
|
+
print(f"[布局] {args.preset}: 窗口 1091x{window_h}; 容器 {mod_w}x{mod_h}; 蓝图 ({px},{py}); 行 {rows}")
|
|
216
|
+
print(" 上排 7 槽 (y=%d) | 第二排 2 槽 (y=%d, 左角/右角) | 下排 7 槽 (y=%d)" % rows)
|
|
217
|
+
if SLOT_W > 72:
|
|
218
|
+
print(f" [!] 槽位宽 {SLOT_W}px > 列距 72px:相邻槽有 {SLOT_W - 72}px 叠压(原版同样如此;要消除需加宽容器)")
|
|
219
|
+
if window_h != 542 and not args.background_sprite:
|
|
220
|
+
print(" [!] 窗口加高但未换背景精灵:原版背景贴图固定 1092x553,内容会溢出(见 references/tank-designer.md §4.3)")
|
|
221
|
+
if args.dry_run:
|
|
222
|
+
print("[dry-run] 未写文件。目标:", out)
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
with open(out, "w", encoding="utf-8", newline="") as f:
|
|
227
|
+
f.write(text)
|
|
228
|
+
print("[ok] 写入", out, f"({len(text.split(chr(10)))} 行)")
|
|
229
|
+
print(" 下一步:把该文件所在 mod 装上,重启游戏开坦克设计器验收(新开档/旧档都看)。")
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
raise SystemExit(main())
|