feat: expand gameplay and state persistence
- add character imprint, awakening, and equipment refinement - add equipment ticket gacha and persistent progression fields - add request-scoped account transactions with crash recovery - persist age confirmation and isolate client plugin patches - separate one-off save repair tools from shipped tooling
This commit is contained in:
@@ -18,16 +18,9 @@ sys.path.insert(0, str(TOOLS))
|
||||
|
||||
import gamedata_db
|
||||
import import_seed
|
||||
import save_checkpoint
|
||||
import deobfuscate_client_source
|
||||
import extract_client_proto
|
||||
import set_first_gacha_state
|
||||
import repair_equipment_ranks
|
||||
import repair_failed_collection_growth
|
||||
import migrate_costume_potential_state
|
||||
import migrate_equipment_upgrade_attempts
|
||||
import dev_mail_grant
|
||||
import repair_invalid_dev_mail_resources
|
||||
|
||||
|
||||
class Arguments:
|
||||
@@ -110,117 +103,6 @@ class ImportToolTests(unittest.TestCase):
|
||||
self.assertEqual(mailbox["mails"][0]["reward_counts"], [3])
|
||||
|
||||
|
||||
class CheckpointToolTests(unittest.TestCase):
|
||||
def test_invalid_dev_mail_resource_repair_preserves_grant_ledger(self):
|
||||
identity = "mail:13043056739:items"
|
||||
original = {
|
||||
"version": "2.34.13",
|
||||
"items": [
|
||||
{"inven_index": 10, "id": 90045, "type": 8, "count": 1},
|
||||
{"inven_index": 11, "id": 127, "type": 8, "count": 99},
|
||||
],
|
||||
"granted": {identity: True},
|
||||
"grant_items": {identity: [10]},
|
||||
}
|
||||
repaired, removed = repair_invalid_dev_mail_resources.repair(original)
|
||||
self.assertEqual(removed, [10])
|
||||
self.assertEqual([item["inven_index"] for item in repaired["items"]], [11])
|
||||
self.assertEqual(repaired["granted"], original["granted"])
|
||||
self.assertEqual(repaired["grant_items"], original["grant_items"])
|
||||
repeated, removed = repair_invalid_dev_mail_resources.repair(repaired)
|
||||
self.assertEqual(removed, [])
|
||||
self.assertEqual(repeated, repaired)
|
||||
|
||||
def test_process_check_fails_closed_when_tasklist_is_unavailable(self):
|
||||
failed = mock.Mock(returncode=1, stdout="", stderr="ERROR: Access denied")
|
||||
with mock.patch.object(save_checkpoint.os, "name", "nt"), mock.patch.object(save_checkpoint.subprocess, "run", return_value=failed):
|
||||
with self.assertRaisesRegex(RuntimeError, "cannot verify"):
|
||||
save_checkpoint.running_processes()
|
||||
|
||||
def test_equipment_upgrade_attempt_migration_is_explicit_and_idempotent(self):
|
||||
original = {"version": "2.34.13", "equipment": [{"inven_index": 1, "id": 10010}]}
|
||||
migrated, changed = migrate_equipment_upgrade_attempts.migrate(original)
|
||||
self.assertEqual(changed, [1])
|
||||
self.assertEqual(migrated["equipment"][0]["upgrade_attempts"], 0)
|
||||
self.assertNotIn("upgrade_attempts", original["equipment"][0])
|
||||
repeated, changed = migrate_equipment_upgrade_attempts.migrate(migrated)
|
||||
self.assertEqual(changed, [])
|
||||
self.assertEqual(repeated, migrated)
|
||||
|
||||
def test_costume_potential_state_migration_is_final_and_idempotent(self):
|
||||
original = {"version": "2.34.13", "costumes": [{"inven_index": 1, "id": 1001}]}
|
||||
migrated, changed = migrate_costume_potential_state.migrate(original)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(migrated["costume_potential"], {})
|
||||
self.assertNotIn("costume_potential", original)
|
||||
repeated, changed = migrate_costume_potential_state.migrate(migrated)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(repeated, migrated)
|
||||
with self.assertRaises(ValueError):
|
||||
migrate_costume_potential_state.migrate({"version": "2.34.13", "costumes": [{"potential_id": 1}]})
|
||||
|
||||
def test_failed_collection_growth_repair_requires_exact_six_unchanged_requests(self):
|
||||
module = repair_failed_collection_growth
|
||||
items = {"version": "2.34.13", "grant_items": {}, "next_index": 900000061, "items": [
|
||||
{"inven_index": index, "id": value[0], "type": 8, "count": value[1]}
|
||||
for index, value in module.CONSUMED.items()
|
||||
] + [
|
||||
{"inven_index": index, "id": value[0], "type": 8, "count": value[1]}
|
||||
for index, value in module.REFUNDS.items()
|
||||
]}
|
||||
wallet = {"version": "2.34.13", "gold": 56050, "spent": {module.IDENTITY: True}}
|
||||
collection = {"version": "2.34.13", "characters": [
|
||||
{"inven_index": 920000054, "id": 6510, "level": 1},
|
||||
]}
|
||||
line = f"WARN session packet rejected path=/CharGrowth error={module.ERROR}\n"
|
||||
corrected_items, corrected_wallet = module.repair(items, wallet, collection, line * 6)
|
||||
self.assertEqual(corrected_wallet["gold"], 66050)
|
||||
self.assertNotIn(module.IDENTITY, corrected_wallet["spent"])
|
||||
self.assertEqual(len(corrected_items["items"]), len(module.CONSUMED))
|
||||
self.assertEqual(next(item["count"] for item in corrected_items["items"] if item["inven_index"] == 900000042), 99256)
|
||||
self.assertEqual(items["items"][2]["count"], 94738)
|
||||
with self.assertRaises(ValueError):
|
||||
module.repair(items, wallet, collection, line * 5)
|
||||
|
||||
def test_equipment_rank_repair_only_fills_missing_arrays(self):
|
||||
original = {"version": "2.34.13", "equipment": [
|
||||
{"inven_index": 1, "id": 10010},
|
||||
{"inven_index": 2, "id": 943619, "rank": [0, 2, 0]},
|
||||
]}
|
||||
repaired, indices = repair_equipment_ranks.repair(original)
|
||||
self.assertEqual(indices, [1])
|
||||
self.assertEqual(repaired["equipment"][0]["rank"], [0, 0, 0])
|
||||
self.assertEqual(repaired["equipment"][1]["rank"], [0, 2, 0])
|
||||
self.assertNotIn("rank", original["equipment"][0])
|
||||
repeated, indices = repair_equipment_ranks.repair(repaired)
|
||||
self.assertEqual(indices, [])
|
||||
self.assertEqual(repeated, repaired)
|
||||
with self.assertRaises(ValueError):
|
||||
repair_equipment_ranks.repair({"version": "2.34.13", "equipment": [{"inven_index": 3, "rank": [1, 2]}]})
|
||||
|
||||
def test_checkpoint_hash_manifest(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
state = root / "state"
|
||||
target = root / "checkpoint"
|
||||
state.mkdir()
|
||||
for name in save_checkpoint.STATE_FILES:
|
||||
(state / name).write_text(json.dumps({"name": name}), encoding="utf-8")
|
||||
created = save_checkpoint.create(state, target, "test")
|
||||
self.assertEqual(save_checkpoint.verify(target), created)
|
||||
|
||||
def test_first_gacha_state_edit_is_explicit_and_idempotent(self):
|
||||
original = {"version": "2.34.13", "grants": {"draw": {}}}
|
||||
completed, changed = set_first_gacha_state.updated_collection(original, True)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(completed["grants"][set_first_gacha_state.IDENTITY], {})
|
||||
repeated, changed = set_first_gacha_state.updated_collection(completed, True)
|
||||
self.assertFalse(changed)
|
||||
pending, changed = set_first_gacha_state.updated_collection(repeated, False)
|
||||
self.assertTrue(changed)
|
||||
self.assertNotIn(set_first_gacha_state.IDENTITY, pending["grants"])
|
||||
|
||||
|
||||
class DevelopmentMailGrantToolTests(unittest.TestCase):
|
||||
def test_deterministic_random_box_is_replaced_by_direct_material(self):
|
||||
items = [
|
||||
|
||||
Reference in New Issue
Block a user