133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
import base64
|
|
import struct
|
|
import zlib
|
|
from contextlib import nullcontext
|
|
|
|
import pytest
|
|
|
|
from core.open import skillTree
|
|
|
|
from api.utils.skill_tree import load_skill_tree_map
|
|
from core.open.skillTree import decode_skill_hash, get_openapi_skill_tree_info, get_skill_tree_info
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def no_live_redis(monkeypatch):
|
|
monkeypatch.setattr(skillTree, 'get_redis_client', lambda: nullcontext(None))
|
|
|
|
|
|
def encode(skills=(), job=(0, 3), remaining=b''):
|
|
header = bytearray(14)
|
|
header[0] = 0xF3
|
|
header[2:4] = bytes(job)
|
|
header[5] = 115
|
|
header[8] = ord('K')
|
|
data = struct.pack('<H', len(skills) << 8)
|
|
for index, level in skills:
|
|
data += struct.pack('<HH', (index & 255) << 8, (level << 8) | (index >> 8))
|
|
# 无进化/强化,两个快捷栏:鬼斩、空槽。
|
|
data += bytes([0, 0, 0, 2, 5, 0, 0, 0]) + remaining
|
|
return base64.b64encode(zlib.compress(header + data)).decode()
|
|
|
|
|
|
def test_decode_adds_missing_auto_skills_without_overwriting_encoded_levels():
|
|
result = decode_skill_hash(encode([(5, 7)]))
|
|
assert result['skills'][0]['level'] == 7
|
|
automatic = {item['skill_index']: item['level'] for item in result['auto_skills']}
|
|
tree = load_skill_tree_map('swordman_male', 'berserker')
|
|
assert automatic == {
|
|
int(key): item['autoLevel'] for key, item in tree.items()
|
|
if 'autoLevel' in item and key != '5'
|
|
}
|
|
assert automatic[174] == 115
|
|
assert 'arrangements' not in result
|
|
|
|
|
|
def test_openapi_result_does_not_create_character(monkeypatch):
|
|
import core.basic.character as character
|
|
|
|
def fail(*args, **kwargs):
|
|
raise AssertionError('must not create a character')
|
|
|
|
monkeypatch.setattr(character, 'createCharacter', fail)
|
|
result = get_openapi_skill_tree_info(encode())
|
|
assert result['skills']['5']['lv'] == 1
|
|
assert result['shortcuts'] == [
|
|
{'slot': 0, 'skill_id': 5, 'skill_name': '鬼斩'},
|
|
{'slot': 1, 'skill_id': None, 'skill_name': None},
|
|
]
|
|
|
|
|
|
def test_calculator_receives_automatic_levels_and_shortcuts():
|
|
result = get_skill_tree_info(encode())
|
|
skills = {item['name']: item for item in result['skills'].values()}
|
|
assert skills['鬼斩']['lv'] == 1
|
|
assert skills['基础精通']['lv'] == 115
|
|
assert result['shortcuts'][0]['skill_name'] == '鬼斩'
|
|
|
|
|
|
def test_dark_knight_arrangements_and_remaining_are_preserved():
|
|
remaining = bytes([1, 118, 0, 1, 60, 0])
|
|
result = decode_skill_hash(encode(job=(9, 0), remaining=remaining))
|
|
assert result['arrangements'] == [{'slot_index': 118, 'skills': [60]}]
|
|
assert result['section2_remaining'] == remaining
|
|
assert result['auto_skills']
|
|
|
|
|
|
def test_context_uses_separate_redis_map_cache(monkeypatch):
|
|
from config.main import config
|
|
|
|
monkeypatch.setattr(config, 'DEBUG_MODE', False)
|
|
|
|
class FakeRedis:
|
|
value = None
|
|
keys = []
|
|
|
|
def get(self, key):
|
|
self.keys.append(key)
|
|
return self.value
|
|
|
|
def set(self, key, value):
|
|
self.value = value
|
|
|
|
redis = FakeRedis()
|
|
monkeypatch.setattr(skillTree, 'get_redis_client', lambda: nullcontext(redis))
|
|
header = {'job_bytes': (0, 3)}
|
|
_, original = skillTree._get_skill_tree_context(header)
|
|
|
|
def fail(*args):
|
|
raise AssertionError('cache hit must not load local data')
|
|
|
|
monkeypatch.setattr(skillTree, 'load_skill_tree_map', fail)
|
|
_, cached = skillTree._get_skill_tree_context(header)
|
|
assert cached == original
|
|
assert cached['174']['autoLevel'] == 115
|
|
assert redis.keys == ['openapi:skillTreeMap:v1:swordman_male:berserker:map'] * 2
|
|
|
|
|
|
@pytest.mark.parametrize(('code', 'interval_ms'), [
|
|
('eJytjb9OwnAABu9CmpQG0MUYjBYHaCRi8ytQW/7sDiQ46OxLuMArsTCwM/lGvICBZ+Cbbrjcd4o6UeOXzQqk1Qo/26/P+Jt1O3ykLI1c2HZuk5mxtXdW3vvuLaUPTtWJN47tUpgZTM199c3c0YUSX0wYGnxSMnWg9JVHE/bIDumpqXpEDsREuRZaayWlNEZSSS2dc3ro1MyZE/8srA0uXJ7VIzugLzw7cM/1Znl5qoTTP4KaGC0=', 0),
|
|
('eJytjT9OwnAARt8LaVIb/iXGEI1WB2gkQvOrUFuE3cFEBp25hIvewLOwMLgzcSMuQOAMfNMbXt63j9pR45ufd5BmM6x+P5fxFx+t8JayMHJuy1cvmBlbe2Vlzxe7lN44VSd2fPaawsxgau6TY3NHJ0p8NGFo8E7J1IHSV25N2CBr5F5N1S3yT0yUa6G1VlJKYySV1NI+podOzZw5cWdhbXDu4qhuWQN94cGBG843y9NT5d/l/gCEWBjI', 5000),
|
|
])
|
|
def test_confirmed_skill_combo_samples(code, interval_ms):
|
|
result = decode_skill_hash(code)
|
|
assert result['skill_combo'] == {
|
|
'skills': [309, 305, 312, 311], 'interval_ms': interval_ms,
|
|
}
|
|
raw = zlib.decompress(base64.b64decode(code))
|
|
assert result['section2_remaining'] == raw[-103:] + b'\x00'
|
|
|
|
|
|
def test_skill_combo_preserves_empty_slots_and_rejects_truncated_layout():
|
|
remaining = bytearray(103)
|
|
remaining[92:100] = struct.pack('<4H', 98, 114, 93, 0)
|
|
remaining[102] = 0xF3
|
|
original = bytes(remaining)
|
|
assert skillTree._parse_skill_combo(original) == {
|
|
'skills': [98, 114, 93, None], 'interval_ms': 0,
|
|
}
|
|
assert bytes(remaining) == original
|
|
assert skillTree._parse_skill_combo(original[:102]) is None
|
|
assert skillTree._parse_skill_combo(bytes(103)) is None
|
|
assert 'skill_combo' not in decode_skill_hash(encode(job=(9, 0)))
|