new:进一步完善加点代码导入
This commit is contained in:
@@ -0,0 +1,409 @@
|
|||||||
|
# DNF 加点代码解析说明
|
||||||
|
|
||||||
|
本文档说明 [`skillTree.py`](./skillTree.py) 当前实现的加点代码解析规则。
|
||||||
|
|
||||||
|
规则来源包括普通职业和黑暗武士的实际加点代码、游戏内技能栏截图,以及项目本地 OpenAPI 技能树数据。文档只把已经能够稳定复现的结构列为已解析字段;`remaining` 中尚未确认的内容继续保留为原始字节。
|
||||||
|
|
||||||
|
## 1. 解码流程
|
||||||
|
|
||||||
|
加点代码依次经过:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Base64 字符串
|
||||||
|
-> base64.b64decode
|
||||||
|
-> zlib.decompress
|
||||||
|
-> DNF 加点二进制
|
||||||
|
```
|
||||||
|
|
||||||
|
顶层二进制结构:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Header(固定 14 字节)
|
||||||
|
角色名(UTF-16LE,变长)
|
||||||
|
Section1(技能等级,变长)
|
||||||
|
Section2(进化、强化、快捷栏,变长)
|
||||||
|
remaining(黑武排列前缀及其他未解析数据)
|
||||||
|
尾部数据
|
||||||
|
```
|
||||||
|
|
||||||
|
入口函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
decode_skill_hash(encoded: str) -> dict
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Header
|
||||||
|
|
||||||
|
Header 固定为 14 字节。
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 当前字段 | 解析方式 |
|
||||||
|
| ---: | ---: | --- | --- |
|
||||||
|
| `0` | 1 | `magic` | 原始字节,已观察到 `0xF3` |
|
||||||
|
| `1` | 1 | `version` | 编码格式版本 |
|
||||||
|
| `2` | 1 | `job_bytes[0]` | 职业大类字节 |
|
||||||
|
| `3` | 1 | `job_bytes[1]` | 职业分支字节 |
|
||||||
|
| `4` | 1 | 未解析 | 保留在原始数据中 |
|
||||||
|
| `5` | 1 | `level` | 角色等级 |
|
||||||
|
| `6..7` | 2 | 未解析 | 保留在原始数据中 |
|
||||||
|
| `8` | 1 | `job_marker` | 当前按单字符读取,样本中为 `K` |
|
||||||
|
| `9..12` | 4 | 未解析 | 保留在原始数据中 |
|
||||||
|
| `13` | 1 | `name_byte_len` | 角色名的字节长度 |
|
||||||
|
|
||||||
|
角色名紧跟 Header:
|
||||||
|
|
||||||
|
```python
|
||||||
|
name = raw[14 : 14 + name_byte_len].decode('utf-16-le')
|
||||||
|
```
|
||||||
|
|
||||||
|
当前已验证的职业头示例:
|
||||||
|
|
||||||
|
| `job_bytes` | 职业 |
|
||||||
|
| --- | --- |
|
||||||
|
| `(5, 4)` | 女弹药 |
|
||||||
|
| `(9, 0)` | 黑暗武士 |
|
||||||
|
|
||||||
|
完整职业大类转换使用 `role_map`;职业分支在 `get_skill_tree_info()` 中结合 `get_adv_info()` 解析。
|
||||||
|
|
||||||
|
## 3. Section1:技能等级
|
||||||
|
|
||||||
|
角色名结束后的数据先按小端序 `uint16` 读取:
|
||||||
|
|
||||||
|
```python
|
||||||
|
uint16s = struct.unpack('<H', ...)
|
||||||
|
high = value >> 8
|
||||||
|
low = value & 0xFF
|
||||||
|
```
|
||||||
|
|
||||||
|
每个 `uint16` 的高字节为主要数值,低字节为标志位。
|
||||||
|
|
||||||
|
### 3.1 技能数量
|
||||||
|
|
||||||
|
第一个 `uint16` 的高字节表示技能记录数量:
|
||||||
|
|
||||||
|
```python
|
||||||
|
num_pairs = high[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 技能记录
|
||||||
|
|
||||||
|
每条技能记录占两个 `uint16`,即 4 字节:
|
||||||
|
|
||||||
|
```text
|
||||||
|
技能索引 uint16
|
||||||
|
技能等级 uint16
|
||||||
|
```
|
||||||
|
|
||||||
|
当前输出字段:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'skill_index': int,
|
||||||
|
'level': int,
|
||||||
|
'skill_flag': int,
|
||||||
|
'level_flag': int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
完整技能索引按当前样本规则计算:
|
||||||
|
|
||||||
|
```python
|
||||||
|
skill_index = skill_index_high + (0x100 if level_flag else 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
高字节 0x98,level_flag = 1
|
||||||
|
=> skill_index = 0x98 + 0x100 = 0x198 = 408
|
||||||
|
```
|
||||||
|
|
||||||
|
技能记录区总长度:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(1 + num_pairs * 2) * 2
|
||||||
|
```
|
||||||
|
|
||||||
|
其中第一个 `1` 是技能数量所在的 `uint16`。
|
||||||
|
|
||||||
|
## 4. Section2:进化、强化和快捷栏
|
||||||
|
|
||||||
|
Section2 从 Section1 结束位置开始,按字节解析。
|
||||||
|
|
||||||
|
```text
|
||||||
|
marker 1 字节
|
||||||
|
evolution_count 1 字节
|
||||||
|
evolutions evolution_count * 3 字节
|
||||||
|
enhancement_count 1 字节
|
||||||
|
enhancements enhancement_count * 3 字节
|
||||||
|
key_count 1 字节
|
||||||
|
keybinds key_count * 2 字节
|
||||||
|
remaining 剩余全部字节
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 Section2 marker
|
||||||
|
|
||||||
|
Section2 第一个字节当前只跳过,尚未赋予业务含义:
|
||||||
|
|
||||||
|
```python
|
||||||
|
off += 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 进化选择
|
||||||
|
|
||||||
|
每条进化记录占 3 字节:
|
||||||
|
|
||||||
|
```text
|
||||||
|
技能索引低字节
|
||||||
|
技能索引标志
|
||||||
|
进化类型原始值
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'skill_index': index_byte + (0x100 if flag else 0),
|
||||||
|
'flag': flag,
|
||||||
|
'type': raw_type + 1,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 强化选择
|
||||||
|
|
||||||
|
强化记录与进化记录格式相同,每条占 3 字节:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'skill_index': index_byte + (0x100 if flag else 0),
|
||||||
|
'flag': flag,
|
||||||
|
'type': raw_type + 1,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 普通技能栏
|
||||||
|
|
||||||
|
`key_count` 表示后续快捷栏记录数量。当前普通职业和黑暗武士样本均为 14。
|
||||||
|
|
||||||
|
每个快捷栏槽位占 2 字节:
|
||||||
|
|
||||||
|
```text
|
||||||
|
技能索引低字节
|
||||||
|
技能索引标志
|
||||||
|
```
|
||||||
|
|
||||||
|
索引计算:
|
||||||
|
|
||||||
|
```python
|
||||||
|
skill_index = index_byte + (0x100 if flag else 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
当计算后的索引为 `0` 时,输出 `None`,表示空槽。
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'slot': 0, # 从 0 开始
|
||||||
|
'skill_index': 57, # 空槽为 None
|
||||||
|
'flag': 0,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
黑暗武士的快捷栏前六项通常不是普通技能,而是连续技排列槽索引 `118` 到 `123`。需要结合 `arrangements` 继续展开。
|
||||||
|
|
||||||
|
## 5. remaining
|
||||||
|
|
||||||
|
`_parse_section2()` 完成快捷栏解析后,将剩余字节原样返回:
|
||||||
|
|
||||||
|
```python
|
||||||
|
remaining = sec2[off:]
|
||||||
|
```
|
||||||
|
|
||||||
|
公开字段保持为:
|
||||||
|
|
||||||
|
```python
|
||||||
|
'section2_remaining': remaining
|
||||||
|
```
|
||||||
|
|
||||||
|
除黑暗武士排列前缀外,后续内容尚未确认,当前不会输出 `unknown`、`trailer`、偏移或诊断等推测字段。
|
||||||
|
|
||||||
|
### 当前实现的补零行为
|
||||||
|
|
||||||
|
`decode_skill_hash()` 为了按 `uint16` 读取数据,会在角色名之后的数据总长度为奇数时补一个 `0x00`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if len(data) % 2:
|
||||||
|
data += b'\x00'
|
||||||
|
```
|
||||||
|
|
||||||
|
因此部分样本返回的 `section2_remaining` 末尾为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
... F3 00
|
||||||
|
```
|
||||||
|
|
||||||
|
其中最后的 `00` 是当前解析实现补入的对齐字节,不是解压后原始数据的一部分。该行为为兼容现有返回格式而保留。
|
||||||
|
|
||||||
|
## 6. 黑暗武士连续技排列
|
||||||
|
|
||||||
|
只有满足以下条件时,顶层结果才增加 `arrangements`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
header['job_bytes'][0] == 9
|
||||||
|
```
|
||||||
|
|
||||||
|
解析函数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_parse_dark_knight_arrangements(remaining: bytes) -> list[dict]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.1 排列区结构
|
||||||
|
|
||||||
|
排列区位于 `remaining` 开头:
|
||||||
|
|
||||||
|
```text
|
||||||
|
arrangement_count 1 字节
|
||||||
|
|
||||||
|
重复 arrangement_count 次:
|
||||||
|
slot_index 2 字节
|
||||||
|
skill_count 1 字节
|
||||||
|
skills skill_count * 2 字节
|
||||||
|
|
||||||
|
后续数据 不解析,仍保留在 section2_remaining 中
|
||||||
|
```
|
||||||
|
|
||||||
|
排列槽索引和技能索引均使用与快捷栏一致的两字节规则:
|
||||||
|
|
||||||
|
```python
|
||||||
|
index = index_byte + (0x100 if flag else 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
排列函数只读取已确认的前缀,不修改或截断原始 `section2_remaining`。
|
||||||
|
|
||||||
|
### 6.2 返回格式
|
||||||
|
|
||||||
|
排列结果只包含实际解析出的字段:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'slot_index': 118,
|
||||||
|
'skills': [60],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'slot_index': 119,
|
||||||
|
'skills': [8, 5, 58, 117, 82],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
列表顺序就是游戏内排列列顺序,`skills` 顺序就是该列从第一阶段到最后阶段的顺序。
|
||||||
|
|
||||||
|
### 6.3 已验证样本
|
||||||
|
|
||||||
|
黑暗武士“霧失樓臺”样本解析为 6 列:
|
||||||
|
|
||||||
|
| 列 | `slot_index` | 技能索引 |
|
||||||
|
| ---: | ---: | --- |
|
||||||
|
| 1 | 118 | `60` |
|
||||||
|
| 2 | 119 | `8, 5, 58, 117, 82` |
|
||||||
|
| 3 | 120 | `108, 64, 65, 46, 253` |
|
||||||
|
| 4 | 121 | `72, 50, 111, 24, 73` |
|
||||||
|
| 5 | 122 | `107, 77, 20, 31, 252` |
|
||||||
|
| 6 | 123 | `68, 112, 44, 9, 251` |
|
||||||
|
|
||||||
|
对应技能名称:
|
||||||
|
|
||||||
|
1. 禁魂斩
|
||||||
|
2. 滑斩 → 暗影斩 → 旋涡斩 → 邪影·波动剑 → 冥炎破空斩
|
||||||
|
3. 跃斩 → 黑暗十字刃 → 跃地斩 → 挑击 → 邪影升龙斩
|
||||||
|
4. 瞬影斩 → 冥光斩 → 暗影鞭 → 暗劲爆发 → 魔影剑舞
|
||||||
|
5. 突刺 → 暗月斩 → 暗影·波动剑 → 暗魂之手 → 暗炎冥魂波
|
||||||
|
6. 暗影升龙击 → 暗影三击剑 → 冥魂之陨 → 破魂斩 → 斩龙破
|
||||||
|
|
||||||
|
## 7. `decode_skill_hash()` 返回结构
|
||||||
|
|
||||||
|
普通职业:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'header': dict,
|
||||||
|
'skills': list,
|
||||||
|
'evolutions': list,
|
||||||
|
'enhancements': list,
|
||||||
|
'keybinds': list,
|
||||||
|
'section2_remaining': bytes,
|
||||||
|
'raw_size': int,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
黑暗武士在上述结构基础上增加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'arrangements': list,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
普通职业不会增加空的 `arrangements` 字段,以保持原有返回结构不变。
|
||||||
|
|
||||||
|
## 8. 技能索引到名称的解析
|
||||||
|
|
||||||
|
`decode_skill_hash()` 只负责二进制结构和技能索引,不直接写入名称。
|
||||||
|
|
||||||
|
`get_skill_tree_info()` 根据职业加载:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/openapi/static/data/cn/{job_name}/{job_grow_name}/skill_tree.json
|
||||||
|
```
|
||||||
|
|
||||||
|
并通过 `load_skill_tree_name_map()` 建立:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OpenAPI 技能索引 -> 技能名称
|
||||||
|
```
|
||||||
|
|
||||||
|
随后再按技能名称连接到 `character.skills` 中的计算对象。
|
||||||
|
|
||||||
|
需要区分:
|
||||||
|
|
||||||
|
- 加点代码中的技能索引:二进制原始索引;
|
||||||
|
- OpenAPI 技能名称:本地 `skill_tree.json` 解析结果;
|
||||||
|
- `character.skills` 中的技能 ID:项目计算对象 ID。
|
||||||
|
|
||||||
|
三者不能在没有映射证据时直接视为同一个字段。
|
||||||
|
|
||||||
|
## 9. 已知边界
|
||||||
|
|
||||||
|
当前已经验证:
|
||||||
|
|
||||||
|
- Base64 和 zlib 解码链路;
|
||||||
|
- 14 字节 Header 及 UTF-16LE 角色名;
|
||||||
|
- Section1 技能数量、索引、等级和标志位;
|
||||||
|
- Section2 进化、强化和 14 槽快捷栏;
|
||||||
|
- 高位技能索引的 `+0x100` 规则;
|
||||||
|
- 黑暗武士连续技排列的组数、槽索引、组内技能数和技能顺序;
|
||||||
|
- 黑暗武士快捷栏 `118..123` 与六组排列的连接关系。
|
||||||
|
|
||||||
|
当前尚未解析:
|
||||||
|
|
||||||
|
- Header 中未命名的常量或状态字节;
|
||||||
|
- Section2 首字节 marker 的业务含义;
|
||||||
|
- 黑暗武士排列前缀之后的 remaining 数据;
|
||||||
|
- 普通职业 remaining 中非零字段的业务含义;
|
||||||
|
- 尾部 `0xF3` 之前其他状态值的业务含义。
|
||||||
|
|
||||||
|
在获得“只修改一个游戏设置后重新导出的对照代码”之前,不应为这些字节添加业务字段。
|
||||||
|
|
||||||
|
## 10. 维护要求
|
||||||
|
|
||||||
|
后续扩展解析时应遵守:
|
||||||
|
|
||||||
|
1. 保留完整 `section2_remaining`,不要因为新增解析而改变原返回数据。
|
||||||
|
2. 只解析能够通过多个样本或游戏界面对照确认的字段。
|
||||||
|
3. 新职业专属结构应抽取为独立函数,并按职业头条件调用。
|
||||||
|
4. 不向公开结果加入 `raw_hex`、猜测偏移、临时诊断或未确认的 trailer 字段。
|
||||||
|
5. 新增高位索引样本时,同时验证 Section1、进化、强化、快捷栏和职业专属结构。
|
||||||
|
6. 普通职业的返回结构应保持兼容;职业专属字段只在对应职业出现。
|
||||||
|
|
||||||
+103
-21
@@ -81,7 +81,7 @@ def decode_skill_hash(encoded: str) -> dict:
|
|||||||
break
|
break
|
||||||
skills.append(
|
skills.append(
|
||||||
{
|
{
|
||||||
'skill_index': high[i],
|
'skill_index': high[i] + 0x100 if low[i + 1] else high[i],
|
||||||
'level': high[i + 1],
|
'level': high[i + 1],
|
||||||
'skill_flag': low[i],
|
'skill_flag': low[i],
|
||||||
'level_flag': low[i + 1],
|
'level_flag': low[i + 1],
|
||||||
@@ -91,7 +91,7 @@ def decode_skill_hash(encoded: str) -> dict:
|
|||||||
sec2_bytes = data[(1 + num_pairs * 2) * 2 :]
|
sec2_bytes = data[(1 + num_pairs * 2) * 2 :]
|
||||||
evolutions, enhancements, keybinds, remaining = _parse_section2(sec2_bytes)
|
evolutions, enhancements, keybinds, remaining = _parse_section2(sec2_bytes)
|
||||||
|
|
||||||
return {
|
result = {
|
||||||
'header': header,
|
'header': header,
|
||||||
'skills': skills,
|
'skills': skills,
|
||||||
'evolutions': evolutions,
|
'evolutions': evolutions,
|
||||||
@@ -100,6 +100,10 @@ def decode_skill_hash(encoded: str) -> dict:
|
|||||||
'section2_remaining': remaining,
|
'section2_remaining': remaining,
|
||||||
'raw_size': len(raw),
|
'raw_size': len(raw),
|
||||||
}
|
}
|
||||||
|
# 黑暗武士才有技能排列;其他职业保持原有返回结构不变。
|
||||||
|
if header['job_bytes'][0] == 9:
|
||||||
|
result['arrangements'] = _parse_dark_knight_arrangements(remaining)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _parse_header(raw: bytes) -> dict:
|
def _parse_header(raw: bytes) -> dict:
|
||||||
@@ -141,7 +145,7 @@ def _parse_section2(sec2: bytes):
|
|||||||
break
|
break
|
||||||
evolutions.append(
|
evolutions.append(
|
||||||
{
|
{
|
||||||
'skill_index': sec2[off],
|
'skill_index': sec2[off] + 0x100 if sec2[off + 1] else sec2[off],
|
||||||
'flag': sec2[off + 1],
|
'flag': sec2[off + 1],
|
||||||
'type': sec2[off + 2] + 1,
|
'type': sec2[off + 2] + 1,
|
||||||
}
|
}
|
||||||
@@ -159,7 +163,7 @@ def _parse_section2(sec2: bytes):
|
|||||||
break
|
break
|
||||||
enhancements.append(
|
enhancements.append(
|
||||||
{
|
{
|
||||||
'skill_index': sec2[off],
|
'skill_index': sec2[off] + 0x100 if sec2[off + 1] else sec2[off],
|
||||||
'flag': sec2[off + 1],
|
'flag': sec2[off + 1],
|
||||||
'type': sec2[off + 2] + 1,
|
'type': sec2[off + 2] + 1,
|
||||||
}
|
}
|
||||||
@@ -175,7 +179,7 @@ def _parse_section2(sec2: bytes):
|
|||||||
for slot in range(key_count):
|
for slot in range(key_count):
|
||||||
if off + 2 > len(sec2):
|
if off + 2 > len(sec2):
|
||||||
break
|
break
|
||||||
idx = sec2[off]
|
idx = sec2[off] + 0x100 if sec2[off + 1] else sec2[off]
|
||||||
flag = sec2[off + 1]
|
flag = sec2[off + 1]
|
||||||
keybinds.append(
|
keybinds.append(
|
||||||
{
|
{
|
||||||
@@ -190,23 +194,61 @@ def _parse_section2(sec2: bytes):
|
|||||||
return evolutions, enhancements, keybinds, remaining
|
return evolutions, enhancements, keybinds, remaining
|
||||||
|
|
||||||
|
|
||||||
def _resolve_openapi_skill_id(skill_index: int, level_flag: int, skill_name_map: dict[str, str]) -> int | None:
|
def _parse_dark_knight_arrangements(remaining: bytes) -> list[dict]:
|
||||||
if level_flag:
|
"""从原始 remaining 前缀中读取黑暗武士技能排列。
|
||||||
high_skill_id = skill_index + 0x100
|
|
||||||
if str(high_skill_id) in skill_name_map:
|
黑暗武士的排列格式为:排列组数(1B),随后每组依次存储
|
||||||
return high_skill_id
|
排列槽索引(2B)、组内技能数(1B)和对应数量的技能索引(2B)。
|
||||||
|
|
||||||
|
本函数只读取已确认的排列前缀,不解析或截断排列后的未知数据;
|
||||||
|
decode_skill_hash 仍按原格式返回完整的 section2_remaining。
|
||||||
|
"""
|
||||||
|
if not remaining:
|
||||||
|
return []
|
||||||
|
|
||||||
|
arrangement_count = remaining[0]
|
||||||
|
off = 1
|
||||||
|
arrangements = []
|
||||||
|
for _ in range(arrangement_count):
|
||||||
|
if off + 3 > len(remaining):
|
||||||
|
break
|
||||||
|
|
||||||
|
slot_index = remaining[off] + 0x100 if remaining[off + 1] else remaining[off]
|
||||||
|
off += 2
|
||||||
|
skill_count = remaining[off]
|
||||||
|
off += 1
|
||||||
|
if off + skill_count * 2 > len(remaining):
|
||||||
|
break
|
||||||
|
|
||||||
|
skills = []
|
||||||
|
for _ in range(skill_count):
|
||||||
|
skill_index = remaining[off] + 0x100 if remaining[off + 1] else remaining[off]
|
||||||
|
skills.append(skill_index)
|
||||||
|
off += 2
|
||||||
|
|
||||||
|
arrangements.append(
|
||||||
|
{
|
||||||
|
'slot_index': slot_index,
|
||||||
|
'skills': skills,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return arrangements
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_openapi_skill_id(skill_index: int, skill_name_map: dict[str, str]) -> int | None:
|
||||||
if str(skill_index) in skill_name_map:
|
if str(skill_index) in skill_name_map:
|
||||||
return skill_index
|
return skill_index
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _resolve_section_skill_id(skill_index: int, skill_name_map: dict[str, str]) -> int | None:
|
# def _resolve_section_skill_id(skill_index: int, skill_name_map: dict[str, str]) -> int | None:
|
||||||
high_skill_id = skill_index + 0x100
|
# high_skill_id = skill_index + 0x100
|
||||||
if str(high_skill_id) in skill_name_map:
|
# if str(high_skill_id) in skill_name_map:
|
||||||
return high_skill_id
|
# return high_skill_id
|
||||||
if str(skill_index) in skill_name_map:
|
# if str(skill_index) in skill_name_map:
|
||||||
return skill_index
|
# return skill_index
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
|
|
||||||
def get_skill_tree_info(encoded: str) -> dict:
|
def get_skill_tree_info(encoded: str) -> dict:
|
||||||
@@ -241,7 +283,7 @@ def get_skill_tree_info(encoded: str) -> dict:
|
|||||||
skillMap = {}
|
skillMap = {}
|
||||||
for index, i in enumerate(r['skills']):
|
for index, i in enumerate(r['skills']):
|
||||||
skillIndex = i['skill_index']
|
skillIndex = i['skill_index']
|
||||||
actualSkillId = _resolve_openapi_skill_id(skillIndex, i.get('level_flag'), skill_name_map)
|
actualSkillId = _resolve_openapi_skill_id(skillIndex, skill_name_map)
|
||||||
skillName = skill_name_map.get(str(actualSkillId)) if actualSkillId is not None else None
|
skillName = skill_name_map.get(str(actualSkillId)) if actualSkillId is not None else None
|
||||||
skills = [skill for skill in character.skills if skill.name == skillName] if skillName else []
|
skills = [skill for skill in character.skills if skill.name == skillName] if skillName else []
|
||||||
i['actual_skill_id'] = actualSkillId
|
i['actual_skill_id'] = actualSkillId
|
||||||
@@ -273,14 +315,14 @@ def get_skill_tree_info(encoded: str) -> dict:
|
|||||||
}
|
}
|
||||||
# 处理进化
|
# 处理进化
|
||||||
for evo in r['evolutions']:
|
for evo in r['evolutions']:
|
||||||
actualSkillId = _resolve_section_skill_id(evo['skill_index'], skill_name_map)
|
actualSkillId = _resolve_openapi_skill_id(evo['skill_index'], skill_name_map)
|
||||||
for s in skillMap.get(actualSkillId, []):
|
for s in skillMap.get(actualSkillId, []):
|
||||||
if getattr(s, 'hasVP', False) and str(s.id) in skillLearn:
|
if getattr(s, 'hasVP', False) and str(s.id) in skillLearn:
|
||||||
skillLearn[str(s.id)]['vp'] = evo['type']
|
skillLearn[str(s.id)]['vp'] = evo['type']
|
||||||
|
|
||||||
# 处理强化
|
# 处理强化
|
||||||
for enh in r['enhancements']:
|
for enh in r['enhancements']:
|
||||||
actualSkillId = _resolve_section_skill_id(enh['skill_index'], skill_name_map)
|
actualSkillId = _resolve_openapi_skill_id(enh['skill_index'], skill_name_map)
|
||||||
for s in skillMap.get(actualSkillId, []):
|
for s in skillMap.get(actualSkillId, []):
|
||||||
if getattr(s, 'hasUP', False) and str(s.id) in skillLearn:
|
if getattr(s, 'hasUP', False) and str(s.id) in skillLearn:
|
||||||
skillLearn[str(s.id)]['up'] = enh['type']
|
skillLearn[str(s.id)]['up'] = enh['type']
|
||||||
@@ -292,11 +334,51 @@ def get_skill_tree_info(encoded: str) -> dict:
|
|||||||
'vp': 0,
|
'vp': 0,
|
||||||
'name': i.name,
|
'name': i.name,
|
||||||
}
|
}
|
||||||
|
shortcuts = []
|
||||||
|
for k in r['keybinds']:
|
||||||
|
actualSkillId = _resolve_openapi_skill_id(k['skill_index'], skill_name_map)
|
||||||
|
if actualSkillId is None:
|
||||||
|
shortcuts.append({
|
||||||
|
'slot': k['slot'],
|
||||||
|
'skill_id': None,
|
||||||
|
'skill_name': None,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
for s in skillMap.get(actualSkillId, []):
|
||||||
|
if str(s.id) in skillLearn:
|
||||||
|
shortcuts.append({
|
||||||
|
'slot': k['slot'],
|
||||||
|
'skill_id': s.id,
|
||||||
|
'skill_name': s.name,
|
||||||
|
})
|
||||||
|
if s is None:
|
||||||
|
shortcuts.append({
|
||||||
|
'slot': k['slot'],
|
||||||
|
'skill_id': None,
|
||||||
|
'skill_name': None,
|
||||||
|
})
|
||||||
|
combos = []
|
||||||
|
for a in r.get('arrangements', []):
|
||||||
|
combo = {
|
||||||
|
'slot_index': a['slot_index'],
|
||||||
|
'skills': [],
|
||||||
|
}
|
||||||
|
for skillIndex in a['skills']:
|
||||||
|
actualSkillId = _resolve_openapi_skill_id(skillIndex, skill_name_map)
|
||||||
|
for s in skillMap.get(actualSkillId, []):
|
||||||
|
if str(s.id) in skillLearn:
|
||||||
|
combo['skills'].append({
|
||||||
|
'skill_id': s.id,
|
||||||
|
'skill_name': s.name,
|
||||||
|
})
|
||||||
|
combos.append(combo)
|
||||||
return {
|
return {
|
||||||
"name": header['name'],
|
"name": header['name'],
|
||||||
"level": header['level'],
|
"level": header['level'],
|
||||||
"job":advInfo,
|
"job":advInfo,
|
||||||
"skills": skillLearn
|
"skills": skillLearn,
|
||||||
|
"shortcuts":shortcuts,
|
||||||
|
"combos":combos,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user