Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
118 lines
5.9 KiB
Python
118 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Strict Core menu project extension in both source and packed validators."""
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT/'tools'))
|
|
import headless_game as packed
|
|
import source_bundle as bundle
|
|
import packed_external as merged
|
|
from external_addons import DIRECT
|
|
from project_schema import parse_project
|
|
|
|
CURRENT = (ROOT/'addons/core/addon.gproj').read_text()
|
|
MENU = CURRENT[CURRENT.index(' Configurations {'):CURRENT.rindex('}')]
|
|
MINIMAL = CURRENT[:CURRENT.index(' Configurations {')]+'}\n'
|
|
CORE_PROJECT = 'addons/core/addon.gproj'
|
|
MENU_FILE = 'addons/core/Configs/ERR/AdminMenus.conf'
|
|
|
|
|
|
class ProjectSchemaTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.payload = {name: (ROOT/name).read_bytes() for name in bundle.inventory(ROOT)}
|
|
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory(prefix='erri-project-schema-')
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.candidate = Path(self.temp.name)
|
|
(self.candidate/'data.pak').write_bytes(b'SYNTHETIC PACKAGE')
|
|
(self.candidate/'resourceDatabase.rdb').write_bytes(b'SYNTHETIC DATABASE')
|
|
|
|
def packed(self, text, project='core'):
|
|
(self.candidate/'addon.gproj').write_text(text)
|
|
return packed.packed_inventory(self.candidate, project)
|
|
|
|
def source(self, text):
|
|
payload = dict(self.payload)
|
|
payload[CORE_PROJECT] = text.encode()
|
|
return bundle.validate(payload, ROOT)
|
|
|
|
def test_old_minimal_and_exact_owned_menu_extension_are_accepted(self):
|
|
for text in (MINIMAL, CURRENT, re.sub(r'\s+', ' ', CURRENT)):
|
|
with self.subTest(text=text):
|
|
match = parse_project(text, allow_core_menu=True)
|
|
self.assertIsNotNone(match)
|
|
self.assertEqual(match.group(1, 2), ('ERR_Core', packed.PACKED_GUID))
|
|
legacy_text = text
|
|
for guid in DIRECT: legacy_text = legacy_text.replace('\"'+guid+'\"', '')
|
|
self.assertEqual(set(self.packed(legacy_text)), packed.PACKED_FILES)
|
|
with self.assertRaises(ValueError): self.packed(text)
|
|
self.assertEqual(set(self.source(text)['core']['dependencies']), {packed.PACKED_BASE, *DIRECT})
|
|
self.assertIsNotNone(parse_project(MINIMAL))
|
|
self.assertIsNone(parse_project(CURRENT))
|
|
|
|
def test_menu_extension_cannot_redirect_override_or_add_unreviewed_settings(self):
|
|
mutations = (
|
|
CURRENT.replace('B48C7FA12692DE03', 'FFFFFFFFFFFFFFFF'),
|
|
CURRENT.replace('50C890C996B0A6FC', 'FFFFFFFFFFFFFFFF'),
|
|
CURRENT.replace('Configs/ERR/AdminMenus.conf', 'Configs/Foreign/Menu.conf'),
|
|
CURRENT.replace('MenuConfigs {', 'MenuConfigs +{'),
|
|
CURRENT.replace('MenuConfigs {', 'MenuConfigs -{'),
|
|
CURRENT.replace('C747AFB6B750CE9A', 'FFFFFFFFFFFFFFFF'),
|
|
CURRENT.replace('Configs/System/chimeraMenus.conf', 'Configs/Foreign/Menu.conf'),
|
|
CURRENT.replace('"{C747AFB6B750CE9A}Configs/System/chimeraMenus.conf"', ''),
|
|
CURRENT.replace('GameProjectConfig PC', 'GameProjectConfig HEADLESS'),
|
|
CURRENT.replace('GameProjectConfig PC', 'GameProjectConfig XBOX_SERIES'),
|
|
CURRENT.replace('GameProjectConfig PC', 'UnknownConfig PC'),
|
|
CURRENT.replace('MenuManagerSettings MenuManagerSettings', 'MenuManagerSettings OtherSettings'),
|
|
CURRENT.replace(' MenuConfigs', ' UnknownSetting 1\n MenuConfigs'),
|
|
CURRENT.replace('"{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf"', '"{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf" "{FFFFFFFFFFFFFFFF}Extra.conf"'),
|
|
CURRENT.replace(MENU, MENU+MENU),
|
|
CURRENT+'GameProject {}\n',
|
|
)
|
|
for text in mutations:
|
|
with self.subTest(text=text):
|
|
self.assertIsNone(parse_project(text, allow_core_menu=True))
|
|
with self.assertRaises(ValueError): self.packed(text)
|
|
with self.assertRaisesRegex(ValueError, 'identity/dependency'): self.source(text)
|
|
|
|
def test_identity_and_exact_dependencies_remain_required_with_extension(self):
|
|
for text in (
|
|
CURRENT.replace('ERR_Core', 'ERR_Chat'),
|
|
CURRENT.replace(packed.PACKED_GUID, 'FFFFFFFFFFFFFFFF'),
|
|
CURRENT.replace(packed.PACKED_BASE, '5F16D7E4A1CBE075'),
|
|
CURRENT.replace('Dependencies {', 'Dependencies { "58D0FB3206B6F859"'),
|
|
CURRENT.replace('Dependencies {', 'Dependencies { "5192138CB9D5B911"'),
|
|
):
|
|
with self.subTest(text=text):
|
|
with self.assertRaises(ValueError): self.packed(text)
|
|
with self.assertRaisesRegex(ValueError, 'identity/dependency'): self.source(text)
|
|
|
|
def test_merged_packed_menu_extension(self):
|
|
folder = self.candidate/'core'
|
|
folder.mkdir()
|
|
for name in ('data.pak', 'resourceDatabase.rdb'):
|
|
(self.candidate/name).rename(folder/name)
|
|
(folder/'addon.gproj').write_text(CURRENT)
|
|
self.assertEqual(set(merged.inventory(self.candidate)), {'core/'+name for name in merged.FILES})
|
|
(folder/'addon.gproj').write_text(CURRENT.replace('Configs/ERR/AdminMenus.conf', 'Configs/Foreign/Menu.conf'))
|
|
with self.assertRaises(ValueError): merged.inventory(self.candidate)
|
|
|
|
def test_source_menu_reference_must_resolve_to_its_owned_resource(self):
|
|
payload = dict(self.payload)
|
|
payload.pop(MENU_FILE)
|
|
payload.pop(MENU_FILE+'.meta')
|
|
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla'): bundle.validate(payload, ROOT)
|
|
payload = dict(self.payload)
|
|
payload[MENU_FILE+'.meta'] = payload[MENU_FILE+'.meta'].replace(b'B48C7FA12692DE03', b'FFFFFFFFFFFFFFFF')
|
|
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla'): bundle.validate(payload, ROOT)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|