Combine Workshop addon and fix prison startup

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.
This commit is contained in:
2026-09-20 16:42:52 +02:00
parent 7d94953b53
commit cab998fb64
513 changed files with 18940 additions and 862 deletions
+28 -24
View File
@@ -47,8 +47,8 @@ class NativePackageTests(unittest.TestCase):
def test_closure_order(self):
self.assertEqual(n.project_order('core'), ['core'])
self.assertEqual(n.project_order('ai'), ['core', 'ai'])
self.assertEqual(n.project_order('chat'), ['core', 'ai', 'intel', 'map', 'chat'])
with self.assertRaises(ValueError): n.project_order('ai')
with self.assertRaises(ValueError): n.project_order('chat')
with self.assertRaises(ValueError): n.project_order('original')
def test_supported_command_is_private_and_never_publishes_or_plays(self):
@@ -126,6 +126,8 @@ class NativePackageTests(unittest.TestCase):
def native_double(self, authored, timeout=30, mutate=False, failure=None, native_code=0, write_status=True, launcher_code=0, silent_workbench=False, exit_after_init=False, cleanup_proof=None, prefix_remains_active=False):
if cleanup_proof is None:
cleanup_proof = {'cleanup_confirmed': True, 'idle_verified': True, 'errors': [], 'orphan_cleanup': {'attempted': False}}
context = n.external_request('core', self.external_fixture(), self.source)
n.capture_external(context, self.session)
child = Mock(); child.poll.return_value = launcher_code
def spawn(*args, **kwargs):
self.assertNotIn('WINEPREFIX', kwargs['env'])
@@ -152,7 +154,7 @@ class NativePackageTests(unittest.TestCase):
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
patch.object(n, 'cleanup_owned_run', return_value=cleanup_proof) as cleanup, patch.object(n.subprocess, 'Popen', side_effect=spawn), \
patch.object(n.subprocess, 'check_output', side_effect=check_output), patch.object(n.subprocess, 'run') as process:
result = n.run_project(self.steam, self.compat, self.session, 'core', timeout, authored, silent_workbench, exit_after_init)
result = n.run_project(self.steam, self.compat, self.session, 'core', timeout, authored, silent_workbench, exit_after_init, external=context)
cleanup.assert_called_once_with(child, Path(n.command_for(self.steam, self.session, 'core')[n.command_for(self.steam, self.session, 'core').index('--')+1]), unittest.mock.ANY, self.compat)
self.assertTrue(any(call.args[0] == ['pactl', 'unload-module', '987'] for call in process.call_args_list))
return result
@@ -189,6 +191,8 @@ class NativePackageTests(unittest.TestCase):
def test_orphaned_prefix_after_launcher_exit_cannot_qualify(self):
import itertools
authored = n.copy_payload(self.source, self.session)
context = n.external_request('core', self.external_fixture(), self.source)
n.capture_external(context, self.session)
child = Mock(); child.poll.return_value = 0
probes = 0
def idle(*args):
@@ -199,7 +203,7 @@ class NativePackageTests(unittest.TestCase):
patch.object(n, 'cleanup_owned_run', return_value={'cleanup_confirmed': False, 'idle_verified': False, 'errors': ['synthetic owned prefix child remains']}) as cleanup, patch.object(n.subprocess, 'Popen', return_value=child), \
patch.object(n.subprocess, 'check_output', side_effect=lambda args, **kw: '[]' if 'sink-inputs' in args else '987'), \
patch.object(n.subprocess, 'run'), patch.object(n.time, 'monotonic', side_effect=itertools.count(0, 6)), patch.object(n.time, 'sleep'):
report = n.run_project(self.steam, self.compat, self.session, 'core', 100, authored)
report = n.run_project(self.steam, self.compat, self.session, 'core', 100, authored, external=context)
cleanup.assert_called_once()
self.assertEqual(report['result'], 'native-or-launcher-exited-prefix-active')
self.assertFalse(report['candidate_created'])
@@ -276,7 +280,7 @@ class NativePackageTests(unittest.TestCase):
import io
fresh = Path('/tmp')/('erri-package-orchestration-'+uuid.uuid4().hex)
self.addCleanup(lambda: shutil.rmtree(fresh, ignore_errors=True))
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='chat', timeout=30, external_addons=self.external_fixture())
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='core', timeout=30, external_addons=self.external_fixture())
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle'), patch.object(n.shutil, 'which', return_value='/mock'), \
patch.object(n, 'run_project', return_value={'candidate_created':False}) as run, patch.object(n.subprocess, 'Popen') as child, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(n.run(args, self.source), 1)
@@ -292,7 +296,7 @@ class NativePackageTests(unittest.TestCase):
import fcntl
import uuid
fresh = Path('/tmp')/('erri-package-lock-'+uuid.uuid4().hex)
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='core', timeout=30)
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh, project='core', timeout=30, external_addons=self.external_fixture())
with (self.compat/'.erri-workbench.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
with patch.object(n, 'assert_no_game'), patch.object(n.shutil, 'which', return_value='/mock'), patch.object(n, 'copy_payload') as copy:
@@ -307,7 +311,7 @@ class NativePackageTests(unittest.TestCase):
fresh = Path('/tmp')/('erri-package-orchestration-'+uuid.uuid4().hex)
self.addCleanup(lambda: shutil.rmtree(fresh, ignore_errors=True))
args = argparse.Namespace(steam=self.steam, compat=self.compat, session=fresh,
project='chat', timeout=30, continue_on_failure=continuation, exit_after_init=exit_after_init,
project='core', timeout=30, continue_on_failure=continuation, exit_after_init=exit_after_init,
external_addons=self.external_fixture())
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle', side_effect=idle), \
patch.object(n.shutil, 'which', return_value='/mock'), \
@@ -328,9 +332,9 @@ class NativePackageTests(unittest.TestCase):
return report
def test_diagnostic_continuation_runs_full_closure_keeps_failures_and_nonzero(self):
reports = [self.completed_report(success=i not in (0, 2)) for i in range(5)]
reports = [self.completed_report(success=i not in (0, 2)) for i in range(1)]
code, summary, calls = self.orchestration_double(reports)
self.assertEqual([call.args[3] for call in calls], ['core', 'ai', 'intel', 'map', 'chat'])
self.assertEqual([call.args[3] for call in calls], ['core'])
self.assertEqual(len({str(call.args[2]) for call in calls}), 1) # one private copy/session
self.assertEqual(summary['projects'], reports) # raw failures retained
self.assertEqual(code, 1)
@@ -344,7 +348,7 @@ class NativePackageTests(unittest.TestCase):
self.assertEqual(code, 1)
self.assertEqual(len(calls), 1)
self.assertFalse(summary['continue_on_failure'])
self.assertEqual(summary['skipped_projects'], ['ai', 'intel', 'map', 'chat'])
self.assertEqual(summary['skipped_projects'], [])
self.assertIn('continuation not requested', summary['stopped_reason'])
def test_continuation_cannot_override_cleanup_runner_source_or_completion_failure(self):
@@ -357,7 +361,7 @@ class NativePackageTests(unittest.TestCase):
self.assertEqual(code, 1)
self.assertEqual(len(calls), 1)
self.assertTrue(summary['stopped_reason'])
self.assertEqual(len(summary['skipped_projects']), 4)
self.assertEqual(len(summary['skipped_projects']), 0)
def cleaned_report(self, **changes):
values = dict(completion_observed='native-or-launcher-exited-prefix-active',
@@ -367,9 +371,9 @@ class NativePackageTests(unittest.TestCase):
return self.completed_report(**values)
def test_verified_post_cleanup_allows_only_diagnostic_later_jobs(self):
reports = [self.cleaned_report()] + [self.completed_report() for _ in range(4)]
reports = [self.cleaned_report()] + [self.completed_report() for _ in range(0)]
code, summary, calls = self.orchestration_double(reports)
self.assertEqual(len(calls), 5)
self.assertEqual(len(calls), 1)
self.assertEqual(code, 1)
self.assertEqual(summary['projects'], reports)
self.assertFalse(summary['all_candidates_created'])
@@ -408,16 +412,14 @@ class NativePackageTests(unittest.TestCase):
self.assertIn('authored inputs changed', summary['stopped_reason'])
self.assertEqual(summary['diagnostic_continuations'], [])
def test_prefix_becoming_busy_between_addons_stops_before_next_launch(self):
# First probe owns idle prefix before copy; second is between addons.
code, summary, calls = self.orchestration_double(
[self.completed_report()], idle=[None, ValueError('synthetic prefix still active')])
self.assertEqual(code, 1)
def test_single_addon_build_has_no_second_native_job(self):
code, summary, calls = self.orchestration_double([self.completed_report(True)])
self.assertEqual(code, 0)
self.assertEqual(len(calls), 1)
self.assertIn('prefix not verifiably idle', summary['stopped_reason'])
self.assertEqual(summary['skipped_projects'], [])
def test_all_clean_candidates_still_unqualified_and_flag_defaults_false(self):
code, summary, calls = self.orchestration_double([self.completed_report(True) for _ in range(5)])
code, summary, calls = self.orchestration_double([self.completed_report(True) for _ in range(1)])
self.assertEqual(code, 0)
self.assertTrue(summary['all_candidates_created'])
self.assertFalse(summary['release_qualified'])
@@ -447,7 +449,7 @@ class NativePackageTests(unittest.TestCase):
def test_added_authored_file_is_not_mistaken_for_generated_cache(self):
authored = n.copy_payload(self.source, self.session)
(self.session/'source/addons/core/Scripts/Game/ERR/Injected.c').write_text('// synthetic new authored content')
(self.session/'source/addons/core/Scripts/Game/ERR/00_Core/Injected.c').write_text('// synthetic new authored content')
self.assertFalse(n.copied_authored_unchanged(self.session/'source', authored))
def test_native_failure_not_masked_by_successful_gamescope_exit(self):
@@ -540,9 +542,9 @@ class NativePackageTests(unittest.TestCase):
def test_exit_after_init_forwards_across_dependency_closure(self):
code, summary, calls = self.orchestration_double(
[self.completed_report(True) for _ in range(5)], exit_after_init=True)
[self.completed_report(True) for _ in range(1)], exit_after_init=True)
self.assertEqual(code, 0)
self.assertEqual(len(calls), 5)
self.assertEqual(len(calls), 1)
self.assertTrue(all(call.args[-2:] == (False, True) for call in calls))
self.assertFalse(summary['release_qualified'])
@@ -637,12 +639,14 @@ class NativePackageTests(unittest.TestCase):
def test_failed_native_launch_does_not_call_post_run_cleanup(self):
authored = n.copy_payload(self.source, self.session)
context = n.external_request('core', self.external_fixture(), self.source)
n.capture_external(context, self.session)
with patch.object(n, 'assert_no_game'), patch.object(n, 'assert_prefix_idle'), \
patch.object(n.subprocess, 'check_output', return_value='987'), \
patch.object(n.subprocess, 'run'), \
patch.object(n.subprocess, 'Popen', side_effect=OSError('synthetic launch refusal')), \
patch.object(n, 'cleanup_owned_run') as cleanup:
report = n.run_project(self.steam, self.compat, self.session, 'core', 30, authored)
report = n.run_project(self.steam, self.compat, self.session, 'core', 30, authored, external=context)
cleanup.assert_not_called()
self.assertIsNone(report['cleanup_report'])
self.assertEqual(report['completion_observed'], 'runner-failed')