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