Limit repeated AI driver honking; record Workshop 0.0.129
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-30 15:38:44 +01:00
parent 681d5e5eae
commit 08e5cbf5ea
10 changed files with 268 additions and 1 deletions
@@ -0,0 +1 @@
15:30:50.026 SCRIPT (E): ERR SMOKE FAIL: horn driver seat
@@ -0,0 +1,5 @@
15:34:03.323 SCRIPT : ERR HORN RESULT: prefab={AD80FB47D868C775}Prefabs/Vehicles/Wheeled/S105/S105_green.et allowedFrames=31 blockedFrames=878 violations=0
15:34:03.323 SCRIPT : ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown
15:34:03.324 SCRIPT : ERR HORN RESULT: prefab={0B4DEA8078B78A9B}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469_PKM.et allowedFrames=31 blockedFrames=878 violations=0
15:34:03.324 SCRIPT : ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown
15:34:03.324 SCRIPT : ERR SMOKE COMPLETE: native UAZ and civilian AI horn input limits
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Frozen local Workbench ai-driver-horn fixture; no live server or installed asset writes."""
from pathlib import Path
import hashlib,json,shutil,subprocess,sys,os,time,re
ROOT=Path(__file__).resolve().parents[3]
sys.path.insert(0,str(ROOT/'tools'))
from source_manifest import addon_manifest
from headless_game import assert_prefix_idle
name=sys.argv[1]
if not name.isalnum(): raise ValueError('Use an alphanumeric run suffix')
source=Path('/tmp/erri-ai-driver-horn-source-'+name)
profile=Path('/tmp/erri-ai-driver-horn-run-'+name)
compat=ROOT/'.local/playtest-compat-20260920'
assert_prefix_idle(compat)
source.mkdir();profile.mkdir()
for rel in addon_manifest(ROOT):
target=source/rel;target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(ROOT/rel,target)
shutil.copytree(ROOT/'tools',source/'tools',ignore=shutil.ignore_patterns('__pycache__'))
(source/'docs').mkdir();shutil.copy2(ROOT/'docs/external-addons.json',source/'docs/external-addons.json')
fixture=Path(__file__).with_name('native-fixture.c')
target=source/'addons/core/Scripts/Game/ERR/10_AI/ZZZ_HornFixture.c'
target.write_text(fixture.read_text())
# Opt-in fixture is independent of the historical generic Core smoke.
p=source/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c'
p.write_text(p.read_text().replace('array<string> independentFixtures = {','array<string> independentFixtures = {"ai-driver-horn", '))
p=source/'tools/headless_workbench.py'
p.write_text(p.read_text().replace("choices=['coordinated-ai',", "choices=['ai-driver-horn', 'coordinated-ai',").replace("a.smoke not in {'coordinated-ai',", "a.smoke not in {'ai-driver-horn', 'coordinated-ai',"))
# The existing fatal-loss diagnostic is guarded by errSmoke; this Core-only
# runner uses errBaseSmoke. Adapt only that test-helper flag in the frozen copy.
p=source/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
p.write_text(p.read_text().replace('System.GetCLIParam("errSmoke", enabled)', 'System.GetCLIParam("errBaseSmoke", enabled)'))
installed=Path.home()/'.local/share/Steam/steamapps/compatdata/1874880/pfx/drive_c/users/steamuser/Documents/My Games/ArmaReforger/addons'
(profile/'profile').mkdir()
(profile/'profile/EscapistsFactions.json').write_text(json.dumps({'escapists':'ussr','enemy':'us','independent':'fia'})+'\n')
(profiles:=profile/'fixture-inputs.json').write_text(json.dumps({'fixture_sha256':hashlib.sha256(fixture.read_bytes()).hexdigest(),'production_sources':addon_manifest(ROOT),'source':str(source),'profile':str(profile)},indent=2)+'\n')
command=[sys.executable,str(source/'tools/headless_workbench.py'),'--compat',str(compat),'--profile',str(profile),'--external-addons',str(installed),'--project','core','--smoke','ai-driver-horn','--rounds','1','--timeout','300']
result=subprocess.run(command)
raise SystemExit(result.returncode)
@@ -0,0 +1,101 @@
#ifdef WORKBENCH
// Inject continuous horn demand into two fixture-owned AI drivers, then observe
// the actual production OnApplyControls chain. No human input is generated.
class ERR_HornFixtureDriver
{
IEntity Driver;
Vehicle Car;
SCR_AIGroup Group;
int Started = -1;
int Allowed;
int Blocked;
int Violations;
}
class ERR_HornFixture
{
static ref array<ref ERR_HornFixtureDriver> Drivers = {};
static ERR_HornFixtureDriver Find(IEntity actor)
{
foreach (ERR_HornFixtureDriver driver : Drivers) if (driver.Driver == actor) return driver;
return null;
}
}
modded class SCR_CharacterControllerComponent
{
protected override void OnApplyControls(IEntity owner, float timeSlice)
{
ERR_HornFixtureDriver fixture = ERR_HornFixture.Find(owner);
CharacterInputContext input = GetInputContext();
bool seated = fixture && CompartmentAccessComponent.GetVehicleIn(owner) == fixture.Car;
if (seated && input) input.SetVehicleHorn(1);
super.OnApplyControls(owner, timeSlice);
if (!seated || !input) return;
int now = System.GetTickCount();
if (fixture.Started < 0) fixture.Started = now;
int elapsed = now - fixture.Started;
bool sounding = input.GetVehicleHorn() != 0;
if (sounding) fixture.Allowed++; else fixture.Blocked++;
if (sounding && elapsed >= 600 && elapsed < 29900) fixture.Violations++;
}
}
modded class ERR_GameMode
{
protected bool m_ERRHornSpawned;
protected int m_ERRHornDeadline;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (Replication.IsServer() && !SCR_Global.IsEditMode()) GetGame().GetCallqueue().CallLater(ERR_HornTest, 500, true);
}
protected void ERR_HornTest()
{
if (!Active()) return;
SCR_PlayerController player = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!player || !player.GetControlledEntity()) return;
if (!m_ERRHornSpawned)
{
m_ERRHornSpawned = true; m_ERRHornDeadline = System.GetTickCount() + 60000;
for (int i = 0; i < 2; i++)
{
ERR_HornFixtureDriver driver = new ERR_HornFixtureDriver();
vector position = ERR_Actors.Ground(player.GetControlledEntity().GetOrigin() + Vector(30 + i * 15, 0, 0));
ResourceName prefab = ERR_CivilianTraffic.CarPrefab(0);
if (i == 1) prefab = ERR_ConvoyResources.VehiclePrefab(0);
driver.Car = Vehicle.Cast(ERR_Actors.Spawn(prefab, position));
driver.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
if (!driver.Car || !driver.Group) { Print("ERR SMOKE FAIL: horn vehicle/group creation", LogLevel.ERROR); return; }
driver.Group.SetPermanentLOD(0); driver.Group.ActivateAI();
ERR_HornFixture.Drivers.Insert(driver);
}
return;
}
bool ready = true;
foreach (ERR_HornFixtureDriver driver : ERR_HornFixture.Drivers)
{
// Vehicle compartments are registered asynchronously after spawning.
if (!driver.Driver)
{
BaseVehicleControllerComponent controls = BaseVehicleControllerComponent.Cast(driver.Car.FindComponent(BaseVehicleControllerComponent));
if (controls && controls.GetPilotCompartmentSlot())
{
AIGroup group = driver.Group;
driver.Driver = controls.GetPilotCompartmentSlot().SpawnCharacterInCompartment(ERR_CivilianTraffic.DriverPrefab(), group, ERR_ConvoyResources.GroupPrefab());
}
}
array<AIAgent> agents = {}; driver.Group.GetAgents(agents);
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
if (driver.Started < 0 || System.GetTickCount() - driver.Started < 32000) ready = false;
}
if (!ready && System.GetTickCount() < m_ERRHornDeadline) return;
GetGame().GetCallqueue().Remove(ERR_HornTest);
foreach (ERR_HornFixtureDriver driver : ERR_HornFixture.Drivers)
{
PrintFormat("ERR HORN RESULT: prefab=%1 allowedFrames=%2 blockedFrames=%3 violations=%4", driver.Car.GetPrefabData().GetPrefabName(), driver.Allowed, driver.Blocked, driver.Violations);
if (!ready || driver.Allowed == 0 || driver.Blocked == 0 || driver.Violations != 0) { Print("ERR SMOKE FAIL: native AI horn limit", LogLevel.ERROR); return; }
Print("ERR SMOKE PASS: native AI driver continuous horn demand bounded with cooldown");
}
ERR_HornFixture.Drivers.Clear();
Print("ERR SMOKE COMPLETE: native UAZ and civilian AI horn input limits");
}
}
#endif
@@ -0,0 +1,26 @@
{
"change": "Half-second horn pulses with 30 seconds between starts, scoped to authoritative AI road drivers in Escape. Player controls and the separate breach alarm are unchanged.",
"policy_tests": [
"test_ai_driver_horn.py",
"test_vehicle_alarm.py",
"test_civilian_traffic.py"
],
"native": {
"horn01": {
"complete": false,
"passes": 0,
"failures": [
"15:30:50.026 SCRIPT (E): ERR SMOKE FAIL: horn driver seat"
]
},
"horn02": {
"complete": true,
"passes": 2,
"failures": []
}
},
"fixture_correction": "First fixture queried the pilot compartment immediately after vehicle spawning. The final fixture waits for asynchronous seat registration and runs its demand injector outside the production control override.",
"limits": "Injected continuous demand in actual UAZ and S105 driver control chains. No reproduction of the reported traffic scene, audible waveform measurement or remote-client qualification. Native engine asset errors are reported by the runner separately.",
"published": true,
"publication": "User confirmed successful Workshop upload; local publishing manifest confirms 0.0.129."
}