Files
EscapistsReintroduced/docs/native-shared-map-ownership.md
T

9.6 KiB

Native shared-map replacement fixture

addons/map/Scripts/Game/ERRMap/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 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 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.