--- /dev/null +++ b/addons/core/Prefabs/ERR/CrashSite.et @@ -0,0 +1,7 @@ +GenericEntity { + ID "46B3FAD3A89A4EA3" + components { + ERR_LocationComponent "{606DF66235E34CA9}" { m_Kind 0 } + RplComponent "{B6AB8FE43FE143B4}" { Streamable Disabled } + } +} --- /dev/null +++ b/addons/core/Prefabs/ERR/CrashSite.et.meta @@ -0,0 +1,7 @@ +MetaFileClass { + Name "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et" + Configurations { + ResourceClass PC {} + ResourceClass HEADLESS : PC {} + } +} --- /dev/null +++ b/addons/core/Prefabs/ERR/CrashWreck.et @@ -0,0 +1,9 @@ +GenericEntity : "{74DCE1AEC6832E18}Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et" { + ID "BDBAE020F10A41DF" + components { + Hierarchy "{BD920DE465D04435}" {} + RplComponent "{7E4852E4567E49D6}" { Streamable Disabled } + } + coords 0 0 0 + angles 0 0 0 +} --- /dev/null +++ b/addons/core/Prefabs/ERR/CrashWreck.et.meta @@ -0,0 +1,7 @@ +MetaFileClass { + Name "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et" + Configurations { + ResourceClass PC {} + ResourceClass HEADLESS : PC {} + } +} --- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_CrashSiteResources.c @@ -0,0 +1,12 @@ +// Owned static aftermath; no aircraft, crew, flight or inventory systems. +class ERR_CrashSiteResources +{ + static ResourceName SitePrefab() { return "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"; } + static ResourceName WreckPrefab() { return "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"; } + static bool Bounds(out vector minimum, out vector maximum) + { + // Fail closed until the exact owned wrapper has native measured bounds. + minimum = vector.Zero; maximum = vector.Zero; + return false; + } +} --- /dev/null +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeCrashGeometryProbe.c @@ -0,0 +1,82 @@ +// Explicit WORKBENCH measurement only. Never moves or seats a player/AI. +#ifdef WORKBENCH +class ERR_NativeCrashGeometryProbe +{ + protected static bool Finite(vector value) + { + for (int axis = 0; axis < 3; axis++) + if (value[axis] != value[axis] || Math.AbsFloat(value[axis]) > 1000000) return false; + return true; + } + protected static bool Measure(IEntity root, out vector minimum, out vector maximum) + { + minimum = "1000000 1000000 1000000"; maximum = "-1000000 -1000000 -1000000"; + if (!root || !root.GetVObject() || !root.FindComponent(RplComponent)) return false; + Physics physics = root.GetPhysics(); + if (!physics || physics.IsDynamic()) return false; + array nodes = {root}; + int meshes; + for (int index = 0; index < nodes.Count(); index++) + { + IEntity entity = nodes[index]; + if (!entity) return false; + Physics childPhysics = entity.GetPhysics(); + if (childPhysics && childPhysics.IsDynamic()) return false; + if (entity.GetVObject()) + { + vector low, high; entity.GetBounds(low, high); + if (!Finite(low) || !Finite(high)) return false; + for (int axis = 0; axis < 3; axis++) + if (high[axis] <= low[axis] || high[axis] - low[axis] > 40) return false; + meshes++; + for (int x = 0; x < 2; x++) + { + for (int y = 0; y < 2; y++) + { + for (int z = 0; z < 2; z++) + { + vector corner = low; + if (x) corner[0] = high[0]; + if (y) corner[1] = high[1]; + if (z) corner[2] = high[2]; + vector local = root.CoordToLocal(entity.CoordToParent(corner)); + if (!Finite(local)) return false; + for (int component = 0; component < 3; component++) + { + minimum[component] = Math.Min(minimum[component], local[component]); + maximum[component] = Math.Max(maximum[component], local[component]); + } + } + } + } + } + IEntity child = entity.GetChildren(); + while (child) + { + if (nodes.Count() >= 32 || nodes.Contains(child)) return false; + nodes.Insert(child); child = child.GetSibling(); + } + } + if (!meshes || !Finite(minimum) || !Finite(maximum)) return false; + for (int dimension = 0; dimension < 3; dimension++) + if (maximum[dimension] <= minimum[dimension] || maximum[dimension] - minimum[dimension] > 40) return false; + PrintFormat("ERR CRASH GEOMETRY: nodes=%1 meshes=%2 static=1 replicatedRoot=1 minX=%3 minY=%4 minZ=%5", nodes.Count(), meshes, minimum[0], minimum[1], minimum[2]); + PrintFormat("ERR CRASH GEOMETRY: maxX=%1 maxY=%2 maxZ=%3 resource=%4", maximum[0], maximum[1], maximum[2], ERR_CrashSiteResources.WreckPrefab()); + return true; + } + static bool Run() + { + if (!Replication.IsServer()) return false; + // At high altitude, independent of the pending crash-start setup and terrain. + // This static prop is the only owned probe entity; every return after creation deletes it. + IEntity wreck = ERR_Actors.Spawn(ERR_CrashSiteResources.WreckPrefab(), "1000 2500 1000"); + if (!wreck) return false; + vector minimum, maximum; + bool measured = Measure(wreck, minimum, maximum); + ERR_Actors.DeleteSpawned(wreck); + bool deleted = wreck == null; + PrintFormat("ERR CRASH GEOMETRY CLEANUP: measured=%1 deleted=%2; no terrain placement or multiplayer visibility claim", measured, deleted); + return measured && deleted; + } +} +#endif --- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c +++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c @@ -29,6 +29,7 @@ return camps > 0 && seen.Count() == camps * 2; } + protected bool m_ERRTestCrashGeometry; protected bool m_ERRTestPatrolHQ; protected ref ERR_NativePatrolProbe m_ERRHQPatrolMovement; protected ref ERR_NativePatrolHQProbe m_ERRHQPatrolOrder; @@ -137,6 +138,7 @@ m_ERRTestMortarCourier = outcome == "mortar-courier"; m_ERRTestMortarSalvo = outcome == "mortar-salvo" || outcome == "mortar-refresh"; m_ERRTestMortarTransfer = outcome == "mortar-transfer"; + m_ERRTestCrashGeometry = outcome == "crash-geometry"; m_ERRTestPatrolHQ = outcome == "patrol-hq"; m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar || m_ERRTestHQMemory || m_ERRTestHQBody || m_ERRTestConvoy || m_ERRTestInfantryTransfer; if (m_ERRTestLocalHandoff || m_ERRTestMountedAmmo || m_ERRTestAlarmHearing) m_ERRTestPlayerDeath = true; @@ -322,6 +324,14 @@ protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current) { if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return; + if (m_ERRTestCrashGeometry) + { + if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 3501, "static wreck geometry measured and probe deleted before explicit fixture reset")) return; + m_ERRSmokeDone = true; + GetGame().GetCallqueue().Remove(ERR_SmokeTick); + Print("ERR SMOKE COMPLETE: owned static crash wreck native bounds and local cleanup; terrain placement and multiplayer unqualified"); + return; + } if (m_ERRTestSupplyTravel) { if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 3201, "production supply travel checked before explicit fixture reset")) return; @@ -673,6 +683,13 @@ if (!ERR_Check(CanStartSpawnPreload() && m_ERRSpawnPreloadRequested && !m_SpawnPreload, "prisoner admitted only after enabled native spawn preload finishes")) return; m_ERRSmokePlayer = players[0]; if (!ERR_CheckRoster() || !ERR_NativeContractsProbe.Run(this, m_ERRSmokePlayer) || !ERR_CheckCompanionCommands()) return; + if (m_ERRTestCrashGeometry) + { + if (!ERR_Check(ERR_NativeCrashGeometryProbe.Run(), "owned static wreck finite aggregate bounds, static physics and local deletion")) return; + m_ERRSmokeStep = 3501; + Transition(ERR_Phase.DEFEAT); + return; + } if (m_ERRTestSupplyTravel) { ERR_BeginSupplyTravel(now); return; } if (m_ERRTestAircraftField) { --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') +p.add_argument('--smoke', choices=['crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') @@ -35,6 +35,7 @@ p.add_argument('--timeout', type=int, default=360) p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)') a = p.parse_args() +if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds') if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds') if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1') if a.smoke == 'unconscious-camera' and a.project != 'core': p.error('unconscious-camera requires --project core to keep the general smoke fixture inactive') --- a/docs/vanilla-resources.json +++ b/docs/vanilla-resources.json @@ -104,6 +104,7 @@ "Prefabs/Props/Furniture/Table_01.et": "53506DCB398C7EBE", "Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et": "B3949F6D215A9F78", "Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et": "CFFEB23FBEA00322", + "Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et": "74DCE1AEC6832E18", "Prefabs/Structures/BuildingParts/Doors/DoorSet_Base.et": "87CADB5ADC989993", "Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et": "34B08027DBDE322A", "Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et": "67F17696FF278AE0",