Files
jakub c6ee764c6c
continuous-integration/drone/push Build is passing
Add after-mission statistics; record Workshop 0.0.125
2026-10-30 12:59:32 +01:00

83 lines
8.0 KiB
Python

"""Execute production counter edges and authoritative death attribution with native doubles."""
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_RoundStatistics.c'
methods='\n'.join(body(p,s) for s in ['protected ERR_RoundPlayerStats Body(', 'static void Consciousness(', 'static void CountDeath(', 'void Killed(', 'protected string Outcome('])
for a,b in [('protected ',''),('ERR_RoundPlayerStats','Row*'),('IEntity','Entity*'),('Instigator instigator','Instigator* instigator'),('foreach (Row* row :','for (Row* row :'),('row.','row->'),('victim.','victim->'),('killer.','killer->'),('m_Mode.','m_Mode->'),('instigator.','instigator->'),('damage.','damage->'),('SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(victim->FindComponent(SCR_CharacterDamageManagerComponent));','Damage* damage = victim->damage;'),('ChimeraCharacter.Cast(victim)','victim && victim->character'),('ChimeraCharacter.Cast(source)','source && source->character'),('Replication.IsServer()','authority'),('ERR_Actors.Alive(row->Actor)','Alive(row->Actor)'),('SCR_FactionManager.SGetFaction(victim)','victim->faction'),('Faction faction','int faction'),('InstigatorType.INSTIGATOR_GM','99'),('string Outcome','std::string Outcome')]:methods=methods.replace(a,b)
methods=methods.replace('null','nullptr').replace('!victim && victim->character','!(victim && victim->character)').replace('m_Mode->Extraction().','m_Mode->Extraction()->').replace('!source && source->character','!(source && source->character)')
run(r'''
#include <cassert>
#include <string>
#include <vector>
bool authority=true;
struct Damage {bool dead=true,claimed=false;bool IsDestroyed(){return dead;}bool ERR_ClaimRoundDeath(){if(!authority||claimed)return false;claimed=true;return true;}};
struct Entity {bool character=true,alive=false;int faction=2;Damage* damage;};
bool Alive(Entity* e){return e&&e->alive;}
struct Instigator {Entity* entity=nullptr;int type=1;Entity* GetInstigatorEntity(){return entity;}int GetInstigatorType(){return type;}};
struct Row {int Account=1;Entity* Actor=nullptr;bool Down=false,Connected=true;int Kills=0,FriendlyKills=0,Deaths=0,Downed=0,Recoveries=0;};
struct Extraction {int escaped=0;bool DidEscape(int account){return account==escaped;}};
struct Mode {bool active=true;struct Extraction result;bool Active(){return active;}int PlayerFaction(){return 1;}struct Extraction* Extraction(){return &result;}};
struct Stats {Mode mode;Mode* m_Mode=&mode;bool m_Finished=false;std::vector<Row*> m_Players;void Sync(){}
'''+methods+r'''
};
int main(){
Stats s;Row player,friendRow;Damage pd,fd,ed;Entity p{true,true,1,&pd},f{true,false,1,&fd},enemy{true,false,2,&ed};player.Actor=&p;friendRow.Actor=&f;friendRow.Account=2;s.m_Players={&player,&friendRow};Instigator cause{&p};
s.Consciousness(&player,false);s.Consciousness(&player,false);assert(player.Downed==1&&player.Recoveries==0);
s.Consciousness(&player,true);s.Consciousness(&player,true);s.Consciousness(&player,false);assert(player.Downed==2&&player.Recoveries==1);
s.Killed(&enemy,&p,&cause);s.Killed(&enemy,&p,&cause);assert(player.Kills==1); // duplicate native events
s.Killed(&f,&p,&cause);assert(player.FriendlyKills==1&&friendRow.Deaths==1&&player.Kills==1);
s.Killed(&p,&p,&cause);assert(player.Deaths==1&&player.FriendlyKills==1&&player.Kills==1&&!player.Down); // suicide
Damage otherDamage;Entity other{true,false,2,&otherDamage};s.m_Finished=true;s.Killed(&other,&p,&cause);assert(!otherDamage.claimed);s.m_Finished=false;
authority=false;s.Killed(&other,&p,&cause);assert(!otherDamage.claimed);authority=true;
s.mode.active=false;s.Killed(&other,&p,&cause);assert(!otherDamage.claimed);s.mode.active=true;
other.character=false;s.Killed(&other,&p,&cause);assert(!otherDamage.claimed);other.character=true;
otherDamage.dead=false;s.Killed(&other,&p,&cause);assert(!otherDamage.claimed);otherDamage.dead=true;
cause.type=99;s.Killed(&other,&p,&cause);assert(player.Kills==1); // GM kills do not inflate score
Damage unknownDamage;Entity unknown{true,false,2,&unknownDamage};Instigator noOwner{&unknown};s.Killed(&unknown,&unknown,&noOwner);assert(!unknownDamage.claimed&&player.Kills==1);
player.Connected=false;player.Down=true;assert(s.Outcome(&player)=="Disconnected - did not escape");
s.mode.result.escaped=player.Account;assert(s.Outcome(&player)=="Escaped"); // disconnected escapee is still an escapee
s.mode.result.escaped=0;player.Connected=true;p.alive=false;assert(s.Outcome(&player)=="Dead");
player.Deaths=0;p.alive=true;assert(s.Outcome(&player)=="Down - did not escape");player.Down=false;assert(s.Outcome(&player)=="Did not escape");
}
''')
lifecycle=(p.with_name('ERR_RoundStatisticsLifecycle.c')).read_text()
assert lifecycle.index('m_ERRStatistics.Finish();') < lifecycle.index('super.OnGameModeEnd(endData);')
assert 'telemetryEnabled' not in p.read_text()+lifecycle
assert 'GetInstigatorPlayerID' not in p.read_text()
assert 'm_Finished = true;' in body(p,'void Finish(')
assert 'm_Players.Count() >= 128' in body(p,'void Sync(')
ui=p.with_name('ERR_EscapeGameOverScreenInfo.c').read_text()
assert 'ERR_OnStatisticsChanged().Insert(ERR_RefreshStatistics)' in ui
assert 'ERR_OnStatisticsChanged().Remove(ERR_RefreshStatistics)' in body(p.with_name('ERR_EscapeGameOverScreenInfo.c'),'override void HandlerDeattached(')
print('PASS: authoritative kills/deaths, duplicate-event protection, friendly kills, suicide, GM and vehicle exclusions, down/recovery edges, disconnected escapees and terminal gates')
# Real roster reconciliation: preserve account-owned counters across disconnects
# and reconnects, without inheriting stats when a transient connection ID is reused.
sync='\n'.join(body(p,s) for s in ['ERR_RoundPlayerStats Find(', 'void Sync('])
sync=sync.replace('new ERR_RoundPlayerStats()', 'new Row()')
for a,b in [('ERR_RoundPlayerStats','Row*'),('UUID account','int account'),('array<ref ERR_PlayerLife> lives = {};','std::vector<Life*> lives;'),('foreach (ERR_PlayerLife life :','for (Life* life :'),('foreach (Row* row :','for (Row* row :'),('row.','row->'),('life.','life->'),('m_Mode.','m_Mode->'),('Replication.IsServer()','authority'),('m_Players.Count()','m_Players.size()'),('m_Players.Insert(row)','m_Players.push_back(row)'),('m_Mode->Players().','m_Mode->Players()->'),('null','nullptr')]:sync=sync.replace(a,b)
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
bool authority=true;
struct Life{int AccountId=1,ConnectionId=1,Actor=1;bool Connected=true;};
struct Row{int Account=0,Actor=0,Kills=0;bool Connected=false,Down=false;std::string Name;};
struct Roster{std::vector<Life*> lives;void Participants(std::vector<Life*>& out){out=lives;}};
struct Mode{Roster roster;Roster* Players(){return &roster;}};
struct Manager{std::map<int,std::string> names;std::string GetPlayerName(int id){return names[id];}};
struct Game{Manager manager;Manager& GetPlayerManager(){return manager;}} game;Game& GetGame(){return game;}
struct Stats{Mode mode;Mode* m_Mode=&mode;bool m_Finished=false,m_Truncated=false;std::vector<Row*> m_Players;std::string DisplayName(std::string name){return name;}
'''+sync+r'''
};
int main(){
Stats s;Life a;s.mode.roster.lives={&a};game.manager.names[1]="Alice";s.Sync();Row* old=s.Find(1);assert(old&&old->Name=="Alice");old->Kills=3;
a.Connected=false;game.manager.names[1]="Bob";Life b;b.AccountId=2;b.Actor=2;s.mode.roster.lives.push_back(&b);s.Sync();
assert(old->Name=="Alice"&&!old->Connected&&old->Kills==3);assert(s.Find(2)->Name=="Bob"&&s.Find(2)->Kills==0);
a.ConnectionId=3;a.Connected=true;game.manager.names[3]="Alice renamed";s.Sync();assert(s.Find(1)==old&&old->Kills==3&&old->Name=="Alice renamed");
s.m_Finished=true;game.manager.names[3]="late";s.Sync();assert(old->Name=="Alice renamed");s.m_Finished=false;
std::vector<Life> extra(130);for(int i=0;i<130;i++){extra[i].AccountId=100+i;s.mode.roster.lives.push_back(&extra[i]);}s.Sync();assert(s.m_Players.size()==128&&s.m_Truncated);
}
''')
print('PASS: production roster reconciliation preserves disconnected names and reconnect counters, refuses connection-ID inheritance, freezes records and bounds retained participants')