Expand achievement exports with progress and metadata

This commit is contained in:
Golumpa 2026-08-19 12:32:16 +01:00
parent 35918f24f1
commit 40b44521aa
17 changed files with 898 additions and 52 deletions

View file

@ -0,0 +1,22 @@
[
{
"Rows": {
"battle_25": {
"Amout": 50,
"AchievementMainType": "AchievementMainType::MainTypeFight",
"AwardInfo": [
{"ItemID": "Annulith", "Amount": 10}
],
"TitleId": {
"TableId": "/Game/Text/ST_Achievement.ST_Achievement",
"Key": "battle_25_Title"
},
"ContentID": {
"TableId": "/Game/Text/ST_Achievement.ST_Achievement",
"Key": "battle_25_Content"
},
"Quality": "EHTAchievementQuality::High"
}
}
}
]

View file

@ -1,4 +1,8 @@
{
"ST_Achievement": {
"battle_25_Title": "Devil Within II",
"battle_25_Content": "Trigger Hexed ×50."
},
"ST_Player": {
"character_1003": "Old Character Renamed",
"character_1099": "New Character"

View file

@ -4,6 +4,7 @@ import unittest
from nte_history_exporter.decoder.achievement import (
ACHIEVEMENT_RECORD_MARKER,
extract_achievement_ids,
extract_achievement_records,
reassemble_tcp_segments,
)
@ -22,6 +23,13 @@ class AchievementPacketTests(unittest.TestCase):
self.assertEqual(extract_achievement_ids(stream), ["Battle_30"])
self.assertEqual(extract_achievement_ids(b"\x01\0\0\0\0"), [])
records = extract_achievement_records(stream)
self.assertEqual(len(records), 2)
self.assertEqual(records[0].achievement_id, "Battle_30")
self.assertTrue(records[0].completed)
self.assertEqual(records[1].achievement_id, "Playstation_035")
self.assertEqual(records[1].status, "not_started")
def test_reassembles_tcp_data(self):
self.assertEqual(
reassemble_tcp_segments([(104, b"bar"), (100, b"foobar"), (100, b"foobar")]),

View file

@ -1,5 +1,8 @@
from tests.support import * # noqa: F401,F403
from nte_history_exporter import __version__
from nte_history_exporter.decoder.achievement import AchievementRecord
from nte_history_exporter.export.json_export import build_achievement_export_json
from nte_history_exporter.live_capture.runner import _achievement_path
class ExportContractTests(unittest.TestCase):
@ -117,6 +120,83 @@ class ExportContractTests(unittest.TestCase):
r"^218216016349_Limited_\d{8}_\d{6}(?:_\d+)?\.json$",
)
def test_achievement_path_includes_user_uid_and_timestamp(self):
path = _achievement_path("218216016349")
self.assertRegex(
path.name,
r"^218216016349_Achievements_\d{8}_\d{6}(?:_\d+)?\.json$",
)
def test_achievement_export_has_versioned_metadata_and_records(self):
export = build_achievement_export_json(
[
AchievementRecord("Battle_30", 10, 639_136_215_846_370_000),
AchievementRecord("Battle_25", 20, 0),
AchievementRecord("Playstation_017", 0, 0),
],
source="live_capture",
capture_source="npcap",
user_uid="218216016349",
server_id="23003",
)
self.assertEqual(export["format"], "nte-achievement-export")
self.assertEqual(export["format_version"], 1)
self.assertEqual(export["capture_source"], "npcap")
self.assertEqual(export["user_uid"], "218216016349")
self.assertEqual(export["account_region"], "EU")
self.assertEqual(
export["scan"],
{
"in_game": {
"total_achievements": 2,
"completed_achievements": 1,
"in_progress_achievements": 1,
"not_started_achievements": 0,
},
"playstation": {
"total_achievements": 1,
"completed_achievements": 0,
"in_progress_achievements": 0,
"not_started_achievements": 1,
},
},
)
battle_30, battle_25 = export["categories"]["battle"]
self.assertEqual(battle_30["name"], "Death Nova I")
self.assertEqual(battle_30["completed_at"], "2026-05-05 23:46:24")
self.assertEqual(battle_25["name"], "Devil Within II")
self.assertEqual(battle_25["description"], "Trigger Hexed ×50.")
self.assertEqual(battle_25["progress"], 20)
self.assertEqual(battle_25["target"], 50)
self.assertEqual(battle_25["quality"], "high")
self.assertEqual(
battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}]
)
self.assertEqual(
export["categories"]["playstation"][0]["name"], "Speed Above All"
)
def test_unmapped_achievement_still_exports_capture_data(self):
export = build_achievement_export_json(
[AchievementRecord("FutureCategory_999", 7, 0)]
)
self.assertEqual(
export["categories"]["futurecategory"],
[
{
"id": "FutureCategory_999",
"platform": "in_game",
"status": "in_progress",
"progress": 7,
"completed": False,
"completed_at": None,
}
],
)
def test_extracts_user_uid_from_record_context(self):
payload = (
b"\x00" * 24

View file

@ -36,6 +36,7 @@ def sample_current():
"DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"},
"OldTicket": {"type": "item", "name": "Retired Ticket", "rank": "B"},
},
"achievements.json": {},
}
@ -54,11 +55,22 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("fork_retired", result.mappings["arcs.json"])
self.assertNotIn("OldTicket", result.mappings["items.json"])
self.assertNotEqual(result.mappings["characters.json"]["1003"], current["characters.json"]["1003"])
self.assertEqual(result.report["changes"], {"additions": 4, "updates": 2, "deletions": 3})
self.assertEqual(result.report["changes"], {"additions": 5, "updates": 2, "deletions": 3})
self.assertIs(result.report["safety"]["authoritative_snapshot"], True)
self.assertIs(result.report["safety"]["deletions_allowed"], True)
self.assertIs(result.report["safety"]["uid_inputs_touched"], False)
self.assertIs(result.report["safety"]["pool_mappings_touched"], False)
self.assertEqual(
result.mappings["achievements.json"]["battle_25"],
{
"name": "Devil Within II",
"description": "Trigger Hexed ×50.",
"category": "fight",
"quality": "high",
"target": 50,
"rewards": [{"item_id": "Annulith", "amount": 10}],
},
)
def test_items_use_illustrations_as_filter_and_asset_tables_as_authority(self):
_current, result = build_sample_update()
@ -71,6 +83,7 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("DIceNormal", result.mappings["items.json"])
self.assertNotIn("UnusedItem", result.mappings["items.json"])
self.assertFalse(any(key.startswith("Characterawaken_") for key in result.mappings["items.json"]))
self.assertFalse(any(key.startswith("Fashion_vehicle_") for key in result.mappings["items.json"]))
def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self):
assets = load_assets(assets_root=SAMPLE_ASSETS)

View file

@ -1,4 +1,5 @@
from tests.support import * # noqa: F401,F403
from nte_history_exporter.mappings import ACHIEVEMENTS
class MappingAndUpdateTests(unittest.TestCase):
@ -79,6 +80,14 @@ class MappingAndUpdateTests(unittest.TestCase):
self.assertIn("name", info)
self.assertIn(info.get("rank"), ("S", "A"))
self.assertTrue(ACHIEVEMENTS)
battle_25 = ACHIEVEMENTS["battle_25"]
self.assertEqual(battle_25["name"], "Devil Within II")
self.assertEqual(battle_25["target"], 50)
self.assertEqual(
battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}]
)
self.assertTrue(ITEMS)
for item_id, info in ITEMS.items():
with self.subTest(item_id=item_id):