Files
dcalc/backend/core/open/skillTree.py
T

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}: (空)')