330 lines
9.5 KiB
Python
330 lines
9.5 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],
|
|
'level': high[i + 1],
|
|
'skill_flag': low[i],
|
|
# 0 被动 1 主动
|
|
'skill_type': low[i + 1],
|
|
}
|
|
)
|
|
|
|
sec2_bytes = data[(1 + num_pairs * 2) * 2 :]
|
|
evolutions, enhancements, keybinds, remaining = _parse_section2(sec2_bytes)
|
|
|
|
return {
|
|
'header': header,
|
|
'skills': skills,
|
|
'evolutions': evolutions,
|
|
'enhancements': enhancements,
|
|
'keybinds': keybinds,
|
|
'section2_remaining': remaining,
|
|
'raw_size': len(raw),
|
|
}
|
|
|
|
|
|
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],
|
|
'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],
|
|
'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]
|
|
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 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,
|
|
)
|
|
|
|
def get_skill_name(skill):
|
|
return skill_name_map.get(str(skill.id), skill.name)
|
|
|
|
character = createCharacter(className)
|
|
skillMap = {}
|
|
for index, i in enumerate(r['skills']):
|
|
skillIndex = i['skill_index']
|
|
skillUuids = skill_map.get(str(skillIndex), [None])
|
|
skills = []
|
|
for uuid in skillUuids:
|
|
skill = next((s for s in character.skills if s.uuid == uuid), None)
|
|
if skill is not None:
|
|
skills.append(skill)
|
|
skillMap[skillIndex] = 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
|
|
skillIndex = skill['skill_index']
|
|
candidates = skillMap.get(skillIndex, [])
|
|
chosen = None
|
|
if len(candidates) == 1:
|
|
chosen = candidates[0]
|
|
elif len(candidates) > 1:
|
|
chosen = next((x for x in candidates if x.type == ('active' if skill['skill_type'] == 1 else 'passive')), None)
|
|
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': get_skill_name(chosen),
|
|
}
|
|
# 处理进化
|
|
for evo in r['evolutions']:
|
|
for s in skillMap.get(evo['skill_index'], []):
|
|
if getattr(s, 'hasVP', False) and str(s.id) in skillLearn:
|
|
skillLearn[str(s.id)]['vp'] = evo['type']
|
|
|
|
# 处理强化
|
|
for enh in r['enhancements']:
|
|
for s in skillMap.get(enh['skill_index'], []):
|
|
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': get_skill_name(i),
|
|
}
|
|
return {
|
|
"name": header['name'],
|
|
"level": header['level'],
|
|
"job":advInfo,
|
|
"skills": skillLearn
|
|
}
|
|
|
|
|
|
# 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}: (空)')
|