428 lines
13 KiB
Python
428 lines
13 KiB
Python
import base64
|
|
import zlib
|
|
import struct
|
|
|
|
from api.core.Redis import get_redis_info
|
|
from api.utils.skill_tree import SKILL_TREE_NAME_MAP_VERSION, load_skill_tree_name_map
|
|
from core.basic.character import createCharacter
|
|
from core.character.adventure import get_adv_info
|
|
from database.connect import get_redis_client
|
|
|
|
role_map = {
|
|
# swordman_male
|
|
'0': 0,
|
|
# fighter_female
|
|
'1': 3,
|
|
# gunner_male
|
|
'2': 4,
|
|
# mage_female
|
|
'3': 7,
|
|
# priest_male
|
|
'4': 8,
|
|
# gunner_female
|
|
'5': 5,
|
|
# thief
|
|
'6': 10,
|
|
# fighter_male
|
|
'7': 2,
|
|
# mage_male
|
|
'8': 6,
|
|
# 黑暗武士
|
|
'9': 15,
|
|
# 缔造者
|
|
'10': 15,
|
|
# swordman_female
|
|
'11': 1,
|
|
# knight
|
|
'12': 11,
|
|
# demonic_lancer
|
|
'13': 12,
|
|
# priest_female
|
|
'14': 9,
|
|
# gunblader
|
|
'15': 13,
|
|
# archer
|
|
'16': 14,
|
|
# imperialknight
|
|
'17': 16,
|
|
}
|
|
|
|
|
|
def decode_skill_hash(encoded: str) -> dict:
|
|
"""
|
|
解码 DNF 技能加点编码。
|
|
|
|
编码流程: base64 -> zlib decompress -> 二进制
|
|
二进制结构:
|
|
[0xF3] [Header 14B] [UTF-16LE 角色名]
|
|
[Section1: N个(技能索引,等级)对]
|
|
[Section2: 进化 | 强化 | 键位 | 其他]
|
|
[零填充] [0xF3]
|
|
|
|
Section1 中每个 uint16 的高字节=数据值,低字节=标志位。
|
|
Section2 按字节读取,包含三部分定长子结构。
|
|
"""
|
|
raw = zlib.decompress(base64.b64decode(encoded))
|
|
header = _parse_header(raw)
|
|
|
|
data = raw[14 + header['name_byte_len'] :]
|
|
if len(data) % 2:
|
|
data += b'\x00'
|
|
|
|
uint16s = [struct.unpack_from('<H', data, i)[0] for i in range(0, len(data), 2)]
|
|
high = [v >> 8 for v in uint16s]
|
|
low = [v & 0xFF for v in uint16s]
|
|
|
|
num_pairs = high[0]
|
|
skills = []
|
|
for p in range(num_pairs):
|
|
i = 1 + p * 2
|
|
if i + 1 >= len(high):
|
|
break
|
|
skills.append(
|
|
{
|
|
'skill_index': high[i] + 0x100 if low[i + 1] else high[i],
|
|
'level': high[i + 1],
|
|
'skill_flag': low[i],
|
|
'level_flag': low[i + 1],
|
|
}
|
|
)
|
|
|
|
sec2_bytes = data[(1 + num_pairs * 2) * 2 :]
|
|
evolutions, enhancements, keybinds, remaining = _parse_section2(sec2_bytes)
|
|
|
|
result = {
|
|
'header': header,
|
|
'skills': skills,
|
|
'evolutions': evolutions,
|
|
'enhancements': enhancements,
|
|
'keybinds': keybinds,
|
|
'section2_remaining': remaining,
|
|
'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:
|
|
nlen = raw[13]
|
|
return {
|
|
'magic': raw[0],
|
|
'version': raw[1],
|
|
'job_bytes': (raw[2], raw[3]),
|
|
'level': raw[5],
|
|
'job_marker': chr(raw[8]),
|
|
'name': raw[14 : 14 + nlen].decode('utf-16-le'),
|
|
'name_byte_len': nlen,
|
|
}
|
|
|
|
|
|
def _parse_section2(sec2: bytes):
|
|
"""
|
|
Section2 子结构:
|
|
byte[0]: 标记位
|
|
byte[1]: 进化数量 E
|
|
byte[2..2+3E-1]: E 个进化条目,每条 3 字节 (技能索引, 标志, 类型-1)
|
|
byte[2+3E]: 强化数量 H
|
|
byte[2+3E+1..2+3E+3H]: H 个强化条目,每条 3 字节
|
|
byte[2+3E+3H+1]: 键位数量 K (固定 0x0E=14)
|
|
byte[..+2K]: K 个键位条目,每条 2 字节 (技能索引, 标志)
|
|
剩余: 其他数据
|
|
"""
|
|
if len(sec2) < 2:
|
|
return [], [], [], sec2
|
|
|
|
off = 0
|
|
off += 1 # byte[0]: marker
|
|
|
|
evo_count = sec2[off]
|
|
off += 1
|
|
evolutions = []
|
|
for _ in range(evo_count):
|
|
if off + 3 > len(sec2):
|
|
break
|
|
evolutions.append(
|
|
{
|
|
'skill_index': sec2[off] + 0x100 if sec2[off + 1] else sec2[off],
|
|
'flag': sec2[off + 1],
|
|
'type': sec2[off + 2] + 1,
|
|
}
|
|
)
|
|
off += 3
|
|
|
|
if off >= len(sec2):
|
|
return evolutions, [], [], b''
|
|
|
|
enh_count = sec2[off]
|
|
off += 1
|
|
enhancements = []
|
|
for _ in range(enh_count):
|
|
if off + 3 > len(sec2):
|
|
break
|
|
enhancements.append(
|
|
{
|
|
'skill_index': sec2[off] + 0x100 if sec2[off + 1] else sec2[off],
|
|
'flag': sec2[off + 1],
|
|
'type': sec2[off + 2] + 1,
|
|
}
|
|
)
|
|
off += 3
|
|
|
|
if off >= len(sec2):
|
|
return evolutions, enhancements, [], b''
|
|
|
|
key_count = sec2[off]
|
|
off += 1
|
|
keybinds = []
|
|
for slot in range(key_count):
|
|
if off + 2 > len(sec2):
|
|
break
|
|
idx = sec2[off] + 0x100 if sec2[off + 1] else sec2[off]
|
|
flag = sec2[off + 1]
|
|
keybinds.append(
|
|
{
|
|
'slot': slot,
|
|
'skill_index': idx if idx != 0 else None,
|
|
'flag': flag,
|
|
}
|
|
)
|
|
off += 2
|
|
|
|
remaining = sec2[off:]
|
|
return evolutions, enhancements, keybinds, remaining
|
|
|
|
|
|
def _parse_dark_knight_arrangements(remaining: bytes) -> list[dict]:
|
|
"""从原始 remaining 前缀中读取黑暗武士技能排列。
|
|
|
|
黑暗武士的排列格式为:排列组数(1B),随后每组依次存储
|
|
排列槽索引(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:
|
|
return skill_index
|
|
return None
|
|
|
|
|
|
# def _resolve_section_skill_id(skill_index: int, skill_name_map: dict[str, str]) -> int | None:
|
|
# high_skill_id = skill_index + 0x100
|
|
# if str(high_skill_id) in skill_name_map:
|
|
# return high_skill_id
|
|
# if str(skill_index) in skill_name_map:
|
|
# return skill_index
|
|
# return None
|
|
|
|
|
|
def get_skill_tree_info(encoded: str) -> dict:
|
|
r = decode_skill_hash(encoded)
|
|
header = r['header']
|
|
role = role_map.get(str(header['job_bytes'][0]), None)
|
|
job_id = int(header['job_bytes'][1]) - 1
|
|
# 黑暗武士
|
|
if header['job_bytes'][0] == 9:
|
|
job_id = 1
|
|
# 缔造者
|
|
if header['job_bytes'][0] == 10:
|
|
job_id = 0
|
|
id = base64.b64encode(f'{role:02}{job_id:02}00'.encode()).decode()
|
|
advInfo = get_adv_info(id)
|
|
className = advInfo['class']
|
|
class_parts = className.split('.')
|
|
job_name = class_parts[1]
|
|
job_grow_name = class_parts[2].replace('_carry', '')
|
|
|
|
def get_skill_name_map():
|
|
return load_skill_tree_name_map(job_name, job_grow_name)
|
|
|
|
with get_redis_client() as redis:
|
|
skill_name_map = get_redis_info(
|
|
redis,
|
|
f'openapi:{SKILL_TREE_NAME_MAP_VERSION}:{job_name}:{job_grow_name}:nameMap',
|
|
get_skill_name_map,
|
|
)
|
|
|
|
character = createCharacter(className)
|
|
skillMap = {}
|
|
for index, i in enumerate(r['skills']):
|
|
skillIndex = i['skill_index']
|
|
actualSkillId = _resolve_openapi_skill_id(skillIndex, skill_name_map)
|
|
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 []
|
|
i['actual_skill_id'] = actualSkillId
|
|
i['skill_name'] = skillName
|
|
i['candidates'] = skills[0] if skills else None
|
|
if actualSkillId is not None:
|
|
skillMap[actualSkillId] = skills
|
|
if len(skills) == 0:
|
|
r['skills'][index] = None
|
|
for k, v in skillMap.items():
|
|
if len(v) > 1:
|
|
skillMap[k] = [s for s in v if s.learnLv not in [50, 85]]
|
|
skillLearn = {}
|
|
# 处理技能等级
|
|
for skill in r['skills']:
|
|
if skill is None:
|
|
continue
|
|
candidates = skill.get('candidates', [])
|
|
skillName = skill.get('skill_name')
|
|
chosen = None
|
|
if candidates:
|
|
chosen = candidates
|
|
if chosen and skillLearn.get(str(chosen.id), {'lv': 0})['lv'] < skill['level']:
|
|
skillLearn[str(chosen.id)] = {
|
|
'lv': skill['level'],
|
|
'up': 0,
|
|
'vp': 0,
|
|
'name': skillName or chosen.name,
|
|
}
|
|
# 处理进化
|
|
for evo in r['evolutions']:
|
|
actualSkillId = _resolve_openapi_skill_id(evo['skill_index'], skill_name_map)
|
|
for s in skillMap.get(actualSkillId, []):
|
|
if getattr(s, 'hasVP', False) and str(s.id) in skillLearn:
|
|
skillLearn[str(s.id)]['vp'] = evo['type']
|
|
|
|
# 处理强化
|
|
for enh in r['enhancements']:
|
|
actualSkillId = _resolve_openapi_skill_id(enh['skill_index'], skill_name_map)
|
|
for s in skillMap.get(actualSkillId, []):
|
|
if getattr(s, 'hasUP', False) and str(s.id) in skillLearn:
|
|
skillLearn[str(s.id)]['up'] = enh['type']
|
|
for i in character.skills:
|
|
if str(i.id) not in skillLearn:
|
|
skillLearn[str(i.id)] = {
|
|
'lv': i.calculate_lv() if (i.learnLv in [50,85] or i.name == '基础精通') and str(i.id) else 0,
|
|
'up': 0,
|
|
'vp': 0,
|
|
'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 {
|
|
"name": header['name'],
|
|
"level": header['level'],
|
|
"job":advInfo,
|
|
"skills": skillLearn,
|
|
"shortcuts":shortcuts,
|
|
"combos":combos,
|
|
}
|
|
|
|
|
|
# def print_decoded(encoded: str):
|
|
# """解码并打印技能编码的全部信息。"""
|
|
# r = decode_skill_hash(encoded)
|
|
# h = r['header']
|
|
|
|
# print('=' * 70)
|
|
# print('DNF 技能加点编码解析')
|
|
# print('=' * 70)
|
|
|
|
# print(f'\n【角色信息】')
|
|
# print(f' 角色名: {h["name"]}')
|
|
# print(f' 等级: {h["level"]}')
|
|
# print(f' 职业字节: ({int(h["job_bytes"][0])}, {int(h["job_bytes"][1])})')
|
|
# print(f' 编码版本: {h["version"]}')
|
|
# print(f' 数据大小: {r["raw_size"]} bytes')
|
|
|
|
# print(f'\n 主动投入技能 (等级 > 1):')
|
|
# for s in reversed(r['skills']):
|
|
# f = ''
|
|
# if s['skill_flag']:
|
|
# f += f' sf={s["skill_flag"]}'
|
|
# if s['level_flag']:
|
|
# f += f' lf={s["level_flag"]}'
|
|
# print(f' 技能 {int(s["skill_index"]):<5d} Lv.{s["level"]:<3d}{f}')
|
|
|
|
# print(f'\n【进化选择】({len(r["evolutions"])} 个)')
|
|
# for e in r['evolutions']:
|
|
# t = '选项一' if e['type'] == 1 else '选项二'
|
|
# print(f' 技能 {int(e["skill_index"]):<5d} {t}')
|
|
|
|
# print(f'\n【强化选择】({len(r["enhancements"])} 个)')
|
|
# for e in r['enhancements']:
|
|
# t = f'类型{e["type"]}'
|
|
# print(f' 技能 {int(e["skill_index"]):<5d} {t}')
|
|
|
|
# active = [k for k in r['keybinds'] if k['skill_index'] is not None]
|
|
# print(f'\n【快捷键】({len(active)}/{len(r["keybinds"])} 槽位已使用)')
|
|
# for k in r['keybinds']:
|
|
# if k['skill_index'] is not None:
|
|
# f = f' (flag={k["flag"]})' if k['flag'] else ''
|
|
# print(f' 槽位 {k["slot"]:2d}: 技能 {int(k["skill_index"]):<5d}{f}')
|
|
# else:
|
|
# print(f' 槽位 {k["slot"]:2d}: (空)')
|