Files
EscapistsReintroduced/docs/native-shared-map-ownership.md
jakub cab998fb64 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.
2026-09-20 16:42:52 +02:00

139 lines
9.6 KiB
Markdown

# Native shared-map replacement fixture
`addons/core/Scripts/Game/ERR/30_Map/ERRM_NativeMapOwnershipProbe.c` is entirely
`WORKBENCH` guarded and inert unless `-errSmoke 1 -errSmokeOutcome map-ownership`
selects it. It does not change production map code, inventory, player state or
settings. The first native execution compiled and passed both replacement/callback
sequences but failed its immediate deletion assertion. It remains a failed run;
the revised bounded deletion observation passed in native run 12:06:33. Offline regressions are
not native rendering or multiplayer evidence.
Chat smoke integration: after the real local player and map entity initialize,
retain `ref ERRM_NativeMapOwnershipProbe`, create it without constructor arguments,
and call `Tick(GetWorld().GetWorldTime())` on the existing smoke cadence. Return
`0` means pending, `-1` failed, `1` complete with owned cleanup finished. Call
`Stop()` during smoke teardown and release the reference. The caller owns the
final smoke completion marker. Add the outcome to the Chat runner and the test
`tests/test_native_shared_map_ownership.py` to the repository check list. The
fixture's own deadline is 15 seconds after first tick; normal stages take about
9 seconds with a 500 ms caller cadence, with at most three seconds allowed for
native deletion before the final 1.5 second callback window.
The fixture refuses an already-open map or a current camera other than the
actual local player's camera. It spawns two real successor map prefabs next to
that player, without inserting them into anyone's inventory. Their real
components initialize their own render roots. A third hidden root is used for
the replacement view. All native map opens/closes target these verified roots;
no menu is opened and no synthetic map component replaces the actual prefab.
First, the production capture method opens the owned root. The fixture closes
only that verified view and opens a distinct native `MapConfiguration` pointing
to the hidden replacement root. The distinct config copies the native plain
config's fields, retaining the same settings/module references; it does not edit
the resource or invoke fullscreen-specific UI modules. The second pass calls
native `SetupMapConfig(PLAIN, ..., replacementRoot)` and requires that vanilla
reuse the **same config object** while changing its root. This is the ownership
case that a config-pointer-only check misses.
For each pass the fixture waits for the actual native map-open-complete event.
It checks native current pan and target zoom around the real stale centering
method, then schedules the actual production center and finish callbacks through
`ScriptCallQueue`. WORKBENCH adapters count their execution and always call
`super`; the fixture fails if either callback never ran or ran twice. Native
map-close events, active config/root, pan, zoom and successor completion calls
are checked after the callbacks. The ordinary production stop and real vanilla
`ModeClear(IN_HAND)` chain then run. Both actual map entities are deleted,
including the never-capturing spare, while the replacement remains open. Each
delete request is issued exactly once. The probe waits at most three seconds
for **all four** native entity/component references to become null, plus exactly
one observed `OnDelete` completion per tagged component **after** the complete
successor/vanilla `super.OnDelete(owner)` chain returns. The observer also checks
that production released its root, target, capture references and in-hand state;
it never clears those fields itself. Missing/duplicate callbacks, remaining
references or cleanup fields fail. A final 1.5 second observation period begins
only after this proof, rejecting late close/restore/completion callbacks.
Stop removes only this probe's subscriptions, entities and widgets, and closes
the native map only if its exact current config/root is still the replacement.
It does not restore or close an unrelated view that took over during a failure.
The original 100/500 ms capture callbacks are deliberately removed only on the
fixture-owned component and rescheduled at 100/200 ms **after** the replacement's
native initialization. This ensures a stale pan would affect a live map, rather
than passing because vanilla ignored a pan before initialization. Production
cadence outside these two owned prefabs is unchanged. The fixture invokes the
same stale camera-restore method explicitly, verifying that its authorization
rejects the replacement; it never intercepts or suppresses a native operation.
Camera evidence has a precise limit. Installed `PlayerController` exposes
`bool SetCharacterCameraRenderActive(bool active)` but no render-enabled getter;
`PlayerCamera`/`CameraBase` also provide no such getter. This fixture therefore
records successor restore-method calls and their exact ownership authorization,
and actual native map-close events. It does **not** claim a measured camera render
flag or visual screenshot. Native `OpenMap` and `CloseMap` retain their normal
camera behavior, and no guessed API or global camera override is introduced.
The ordinary actor's camera identity is checked, not its undocumented render
state. This remains a private diagnostic UI exercise, not human interaction or
remote/JIP qualification.
Installed-source audit (SHA-256 of the extracted read-only files):
| Source | Relevant behavior | SHA-256 |
| --- | --- | --- |
| `scripts/Game/Map/SCR_MapEntity.c` | `SetupMapConfig` reuses equal-mode config/root; `OpenMap`/`CloseMap` perform native camera changes; map-open-complete, pan/zoom and current-root APIs | `09ca0631ce2f5a196b7074e12a67c08255b7119b9600da635894097b35e4ad92` |
| `scripts/Game/Components/Gadgets/SCR_MapGadgetComponent.c` | no deletion override; `ModeClear` only unfocuses an in-hand map belonging to the local actor when actually focused | `3bfc0b6715a173654a3f9ecc5a4538be508905eb9d50aef82cb80262950fec9e` |
| `scripts/Game/Components/Gadgets/SCR_GadgetComponent.c` | `OnDelete` disconnects from gadget system then delegates; ordinary `ModeClear` only removes its own loadout subscription | `015c18b4c94245e72c923e718b74cf933cd516c0a9dc24946735482c2e4e191b` |
| `scripts/Game/generated/Player/PlayerController.c` | native camera-render setter without getter | `832c5925edbd41e05c469803abdea3b202c1de58d58251d77a8a7e136bdb7edc` |
| `scripts/GameCode/Map/MapConfiguration.c` | exact fields used by the distinct plain-config copy | `4ceb904d36c7e466ed5e02e7d72fe24944fb6dd4f7cae07809a2428fdf4d234e` |
An unused/spare map has no vanilla character owner or focused state, so its real
superclass cleanup cannot schedule native `ToggleMapGadget(false)`. A previously
focused stock map has a separate native delayed focus/menu path and is outside
this fixture's claim. The tests execute the actual fixture cleanup and evidence
decisions against explicit API doubles, including foreign config/root, changed
singleton, duplicate cleanup, unrelated observer ownership, missing callbacks,
changed pan and close/reopen rejection.
## First deletion observation and bounded follow-up
[Native run 2026-09-19 11:56:46](evidence/native-2026-09-19-115646-map-deletion-refusal/console.log.txt)
passed both actual map replacement sequences, native open-complete, delayed
callback execution, unchanged pan/zoom and actual stop/mode-clear checks. At
11:57:47.732 it failed immediately after requesting the two deletions, before
waiting a native frame. That record does not identify which reference remained
or whether either `OnDelete` had run, so deferred deletion is a hypothesis, not
a proven explanation or a reason to relabel the failed run.
Installed `scripts/Game/Helpers/SCR_EntityHelper.c:177` is only a wrapper for
`RplComponent.DeleteRplEntity(entity, false)`. The generated native contract
describes authority/proxy physical deletion, but promises no synchronous
reference invalidation before the method returns. Source SHA-256 values:
`31a7f7651237cd859400774fb6be62a0079f204e593831db55093ab59c69812c`
(helper) and
`0bdf89c3c65e23fa3cf1cab27f8899fd89f3c5fbb6e6c3431f5fdb007590f34c`
(`scripts/Game/generated/Components/RplComponent.c`). The revised probe logs
non-sensitive reference-presence booleans and callback counts at request return,
at actual post-super cleanup, and at later completion or timeout. These
observations distinguish delayed invalidation from incomplete cleanup without
manual reference clearing, repeated delete requests, weakened acceptance or
changing production deletion behavior. The offline regression now exercises
request-versus-completion separation, exact once-only requests, missing/duplicate
post-super evidence and retained component references.
## Native deletion diagnosis and scoped pass
[Native run 2026-09-19 12:06:33](evidence/native-2026-09-19-120633-map-ownership/console.log.txt)
passed the complete fixture. At 12:07:34.567 both tagged components reported
`OnDelete` **after super** with `released=1`, one callback each; both entity
references were already null, but both component references still existed. At
12:07:35.069 the next probe tick found all four references null, with no duplicate
callbacks or leftover production state. The unchanged 1.5 second observation
window passed at 12:07:36.582. This establishes delayed component-reference
invalidation as the cause of the original same-stack assertion failure; actual
superclass cleanup was already synchronous in this observed run.
This evidence covers the two real replacement/config-root cases, real queued
callbacks and native prefab deletion in the isolated local Workbench runtime.
It does not establish a measured camera render flag, remote/JIP behavior, a
previously focused native map's unrelated menu callbacks, or performance. The
11:56:46 attempt remains recorded as failed.