Add after-mission statistics; record Workshop 0.0.125
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frozen local Workbench round-statistics fixture; no live server or installed asset writes."""
|
||||
from pathlib import Path
|
||||
import hashlib,json,shutil,subprocess,sys,os,time
|
||||
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-round-statistics-source-'+name)
|
||||
profile=Path('/tmp/erri-round-statistics-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')
|
||||
shutil.copy2(fixture,source/'addons/core/Scripts/Game/ERR/00_Core/ERR_ZRoundStatisticsFixture.c')
|
||||
# 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 = {"round-statistics", '))
|
||||
p=source/'tools/headless_workbench.py'
|
||||
p.write_text(p.read_text().replace("choices=['coordinated-ai',", "choices=['round-statistics', 'coordinated-ai',").replace("a.smoke not in {'coordinated-ai',", "a.smoke not in {'round-statistics', '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','round-statistics','--rounds','1','--timeout','300']
|
||||
with (profile/'runner-output.log').open('w') as output:
|
||||
result=subprocess.Popen(command,stdout=output,stderr=subprocess.STDOUT)
|
||||
captured=False
|
||||
while result.poll() is None:
|
||||
if not captured and any('ERR STATS UI READY' in log.read_text(errors='replace') for log in (profile/'logs').glob('*/console.log')):
|
||||
for proc in Path('/proc').iterdir():
|
||||
if not proc.name.isdigit(): continue
|
||||
try:
|
||||
args=(proc/'cmdline').read_bytes().decode(errors='replace')
|
||||
if 'ArmaReforgerWorkbenchSteamDiag.exe' not in args or str(source/'addons/core/addon.gproj') not in args: continue
|
||||
values=dict(x.split('=',1) for x in (proc/'environ').read_bytes().decode(errors='replace').split('\0') if '=' in x)
|
||||
if 'DISPLAY' not in values: continue
|
||||
env=os.environ.copy()
|
||||
for key in ['DISPLAY','XAUTHORITY']:
|
||||
if key in values: env[key]=values[key]
|
||||
capture=subprocess.run(['import','-display',values['DISPLAY'],'-window','root',str(profile/'results-desktop.png')],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10)
|
||||
if capture.returncode==0: captured=True;break
|
||||
except (OSError,ValueError,subprocess.TimeoutExpired): pass
|
||||
time.sleep(1)
|
||||
print(json.dumps({'exit':result.returncode,'source':str(source),'profile':str(profile)}))
|
||||
print('\n'.join((profile/'runner-output.log').read_text().splitlines()[-45:]))
|
||||
raise SystemExit(result.returncode)
|
||||
@@ -0,0 +1,85 @@
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRStatsStage;
|
||||
protected IEntity m_ERRStatsActor;
|
||||
protected IEntity m_ERRStatsFriend;
|
||||
protected UUID m_ERRStatsAccount;
|
||||
protected SCR_CharacterDamageManagerComponent m_ERRStatsDamage;
|
||||
protected float m_ERRStatsWait;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (Replication.IsServer() && !SCR_Global.IsEditMode()) GetGame().GetCallqueue().CallLater(ERR_StatsPoll, 500, true);
|
||||
}
|
||||
protected bool ERR_StatsCheck(bool ok, string detail)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: statistics " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: statistics " + detail, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_StatsPoll); return false;
|
||||
}
|
||||
protected void ERR_StatsPoll()
|
||||
{
|
||||
if (!PreparationError().IsEmpty()) { ERR_StatsCheck(false, PreparationError()); return; }
|
||||
if (GetWorld().GetWorldTime() < m_ERRStatsWait) return;
|
||||
if (m_ERRStatsStage == 0)
|
||||
{
|
||||
if (!Active()) return;
|
||||
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!pc || !pc.GetControlledEntity() || !Players().CurrentActor(pc.GetControlledEntity())) return;
|
||||
m_ERRStatsActor = pc.GetControlledEntity(); m_ERRStatsAccount = Players().Get(pc.GetPlayerId()).AccountId;
|
||||
m_ERRStatsDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRStatsActor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
ERR_SiteRuntime site = Sites().Find("everon-prison-1");
|
||||
array<AIAgent> agents = {}; site.Group.GetAgents(agents);
|
||||
IEntity enemy = agents[0].GetControlledEntity();
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(enemy.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true); damage.Kill(Instigator.CreateInstigator(m_ERRStatsActor));
|
||||
m_ERRStatsDamage.EnableDamageHandling(true); m_ERRStatsDamage.ForceUnconsciousness();
|
||||
m_ERRStatsStage = 1; m_ERRStatsWait = GetWorld().GetWorldTime() + 2000; return;
|
||||
}
|
||||
ERR_RoundPlayerStats row = ERR_Statistics().Find(m_ERRStatsAccount);
|
||||
if (!ERR_StatsCheck(row != null, "participant record exists")) return;
|
||||
if (m_ERRStatsStage == 1)
|
||||
{
|
||||
PrintFormat("ERR STATS VALUES: kills=%1 friendly=%2 deaths=%3 downed=%4 recoveries=%5", row.Kills, row.FriendlyKills, row.Deaths, row.Downed, row.Recoveries);
|
||||
if (!ERR_StatsCheck(row.Kills == 1 && row.Deaths == 0 && row.Downed == 1, "native enemy kill and unconscious callback counted separately")) return;
|
||||
m_ERRStatsDamage.FullHeal(); m_ERRStatsStage = 2; m_ERRStatsWait = GetWorld().GetWorldTime() + 2000; return;
|
||||
}
|
||||
if (m_ERRStatsStage == 2)
|
||||
{
|
||||
if (row.Recoveries < 1) return;
|
||||
if (!ERR_StatsCheck(row.Recoveries == 1, "native recovery counted once")) return;
|
||||
m_ERRStatsFriend = ERR_Actors.Spawn("{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et", ERR_Actors.Ground(m_ERRStatsActor.GetOrigin() + "10 0 10"));
|
||||
if (!ERR_StatsCheck(m_ERRStatsFriend != null, "friendly NPC created")) return;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRStatsFriend.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true); damage.Kill(Instigator.CreateInstigator(m_ERRStatsActor));
|
||||
m_ERRStatsStage = 3; m_ERRStatsWait = GetWorld().GetWorldTime() + 1000; return;
|
||||
}
|
||||
if (m_ERRStatsStage == 3)
|
||||
{
|
||||
if (!ERR_StatsCheck(row.FriendlyKills == 1 && row.Kills == 1, "friendly kill separated from normal kills")) return;
|
||||
ERR_Statistics().Killed(m_ERRStatsFriend, m_ERRStatsActor, Instigator.CreateInstigator(m_ERRStatsActor));
|
||||
if (!ERR_StatsCheck(row.FriendlyKills == 1, "duplicate native callback does not double count")) return;
|
||||
if (!ERR_StatsCheck(m_ERRStatsDamage.ERR_ForceFatalLossForTest(Instigator.CreateInstigator(m_ERRStatsActor), m_ERRStatsActor), "fatal player fixture requested")) return;
|
||||
m_ERRStatsStage = 4; m_ERRStatsWait = GetWorld().GetWorldTime() + 1000; return;
|
||||
}
|
||||
if (m_ERRStatsStage == 4)
|
||||
{
|
||||
if (!ERR_StatsCheck(row.Deaths == 1 && row.Kills == 1 && row.FriendlyKills == 1, "actual fatal player loss counted once without self-kill credit")) return;
|
||||
if (!ERR_StatsCheck(Transition(ERR_Phase.DEFEAT), "normal mission end requested")) return;
|
||||
m_ERRStatsStage = 5; m_ERRStatsWait = GetWorld().GetWorldTime() + 4000; return;
|
||||
}
|
||||
if (m_ERRStatsStage == 5)
|
||||
{
|
||||
if (!ERR_StatsCheck(!ERR_RoundSummary().IsEmpty() && ERR_RoundSummary().Contains("Death") && ERR_RoundSummary().Contains("Dead"), "frozen end summary contains participant death outcome")) return;
|
||||
MenuBase menu = GetGame().GetMenuManager().GetTopMenu(); if (!menu) return;
|
||||
TextWidget text = TextWidget.Cast(menu.GetRootWidget().FindAnyWidget("RoundStatistics"));
|
||||
if (!ERR_StatsCheck(text && text.GetText() == ERR_RoundSummary(), "real native game-over screen displays replicated summary")) return;
|
||||
float x, y, width, height; text.GetScreenPos(x, y); text.GetScreenSize(width, height);
|
||||
PrintFormat("ERR STATS UI READY: text x=%1 y=%2 width=%3 height=%4 visible=%5", x, y, width, height, text.IsVisible());
|
||||
System.MakeScreenshot("$logs:err-round-statistics");
|
||||
m_ERRStatsStage = 6; m_ERRStatsWait = GetWorld().GetWorldTime() + 15000; return;
|
||||
}
|
||||
Print("ERR SMOKE COMPLETE: round statistics native death/knockdown/recovery/friendly-kill and end-screen integration");
|
||||
GetGame().GetCallqueue().Remove(ERR_StatsPoll);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user