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:
@@ -11,19 +11,21 @@ def adapt(s):
|
||||
s=s.replace('ERR_GameMode.Get()','Mode::Get()').replace('ERR_GameMode mode','Mode* mode')
|
||||
s=s.replace('ERR_FailureGameOverScreenInfo.Cast(', 'FailureInfo::Cast(').replace('ERR_EscapeGameOverScreenInfo.Cast(', 'EscapeInfo::Cast(')
|
||||
s=s.replace('ERR_FailureGameOverScreenInfo info = new ERR_FailureGameOverScreenInfo();','FailureInfo info;')
|
||||
s=s.replace('TextWidget.Cast(', 'TextWidget::Cast(').replace('TextWidget debrief','TextWidget* debrief')
|
||||
s=s.replace('TextWidget.Cast(', 'TextWidget::Cast(').replace('TextWidget debrief','TextWidget* debrief').replace('TextWidget text','TextWidget* text')
|
||||
s=s.replace('SCR_PlayerController.ERR_OnEscapeResultChanged()','escapeEvent')
|
||||
s=s.replace('SCR_GameOverScreenUIContentData endScreenUIContent','Data* endScreenUIContent').replace('Widget w','int w')
|
||||
s=s.replace('array<int> otherPlayerIds','void* otherPlayerIds').replace('string ','std::string ')
|
||||
s=s.replace('super.InitContent(endScreenUIContent);','nativeInit++;').replace('super.HandlerDeattached(w);','nativeDetach++;')
|
||||
for name in ('m_ERRFailureMode','m_wRoot','debrief','mode','endScreenUIContent'):s=s.replace(name+'.',name+'->')
|
||||
for name in ('m_ERRFailureMode','m_ERRStatisticsMode','m_wRoot','debrief','text','mode','endScreenUIContent'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('OnEndReasonChanged().','OnEndReasonChanged()->').replace('EndReason().IsEmpty()','EndReason().empty()')
|
||||
s=s.replace('Insert(ERR_RefreshFailureContent)','Insert(1)').replace('Remove(ERR_RefreshFailureContent)','Remove(1)')
|
||||
s=s.replace('Insert(ERR_RefreshEscapeContent)','Insert(2)').replace('Remove(ERR_RefreshEscapeContent)','Remove(2)')
|
||||
s=s.replace('ERR_OnStatisticsChanged().','ERR_OnStatisticsChanged()->').replace('summary.IsEmpty()','summary.empty()')
|
||||
s=s.replace('Insert(ERR_RefreshStatistics)','Insert(3)').replace('Remove(ERR_RefreshStatistics)','Remove(3)')
|
||||
return s
|
||||
failure=body(SOURCE,'class ERR_FailureGameOverScreenInfo')
|
||||
failure=failure[failure.index(' override string'):failure.rindex('}')]
|
||||
callbacks='\n'.join(body(SOURCE,x) for x in ('override void InitContent(','protected void ERR_RefreshFailureContent(','override void HandlerDeattached('))
|
||||
callbacks='\n'.join(body(SOURCE,x) for x in ('override void InitContent(','protected void ERR_RefreshFailureContent(','protected void ERR_RefreshStatistics(','override void HandlerDeattached('))
|
||||
notify=body(MODE,'protected void ERR_OnEndReasonReplicated(')
|
||||
assert '[RplProp(onRplName: "ERR_OnEndReasonReplicated")]' in MODE.read_text()
|
||||
run(r'''
|
||||
@@ -31,7 +33,7 @@ run(r'''
|
||||
#include <string>
|
||||
#include <functional>
|
||||
struct Event{int count=0;std::function<void()> handler;void Insert(int){count=1;}void Remove(int){count=0;}void Invoke(){if(count&&handler)handler();}};
|
||||
struct Mode{static inline Mode* current=nullptr;Event m_OnEndReasonChanged;std::string reason;static Mode* Get(){return current;}std::string EndReason(){return reason;}Event* OnEndReasonChanged(){return &m_OnEndReasonChanged;}
|
||||
struct Mode{static inline Mode* current=nullptr;Event m_OnEndReasonChanged,statsChanged;std::string reason,summary;Event* ERR_OnStatisticsChanged(){return &statsChanged;}std::string ERR_RoundSummary(){return summary;}static Mode* Get(){return current;}std::string EndReason(){return reason;}Event* OnEndReasonChanged(){return &m_OnEndReasonChanged;}
|
||||
NOTIFY
|
||||
};
|
||||
struct FailureInfo{static bool Cast(int kind){return kind==1;}
|
||||
@@ -39,15 +41,17 @@ FAILURE
|
||||
};
|
||||
struct EscapeInfo{static EscapeInfo* Cast(int kind){static EscapeInfo value;return kind==2?&value:nullptr;}};
|
||||
struct TextWidget{std::string text;int writes=0;static TextWidget* Cast(TextWidget* w){return w;}void SetText(std::string value){text=value;writes++;}};
|
||||
struct Root{TextWidget debrief;bool exists=true;TextWidget* FindAnyWidget(std::string){return exists?&debrief:nullptr;}};
|
||||
struct Root{TextWidget debrief,statistics;bool exists=true;TextWidget* FindAnyWidget(std::string name){return exists?(name=="RoundStatistics"?&statistics:&debrief):nullptr;}};
|
||||
struct Data{int m_GameOverInfo;};
|
||||
struct Content{Event escapeEvent;bool m_ERREscapeContent=false;Mode* m_ERRFailureMode=nullptr;Root* m_wRoot=nullptr;std::string m_sDebriefingName="debrief";int nativeInit=0,nativeDetach=0,escapeRefresh=0;void ERR_RefreshEscapeContent(){escapeRefresh++;}
|
||||
struct Content{Event escapeEvent;bool m_ERREscapeContent=false;Mode* m_ERRFailureMode=nullptr;Mode* m_ERRStatisticsMode=nullptr;Root* m_wRoot=nullptr;std::string m_sDebriefingName="debrief";int nativeInit=0,nativeDetach=0,escapeRefresh=0;void ERR_RefreshEscapeContent(){escapeRefresh++;}
|
||||
CALLBACKS
|
||||
};
|
||||
int main(){Mode mode;Mode::current=&mode;Root root;Content content;content.m_wRoot=&root;mode.m_OnEndReasonChanged.handler=[&](){content.ERR_RefreshFailureContent();};Data failure{1},escape{2},other{3};
|
||||
int main(){Mode mode;Mode::current=&mode;Root root;Content content;content.m_wRoot=&root;mode.m_OnEndReasonChanged.handler=[&](){content.ERR_RefreshFailureContent();};Data failure{1},escape{2},other{3};mode.statsChanged.handler=[&](){content.ERR_RefreshStatistics();};
|
||||
content.InitContent(&failure);assert(mode.m_OnEndReasonChanged.count==1&&root.debrief.text.find("No surviving")!=std::string::npos);
|
||||
assert(mode.statsChanged.count==1&&root.statistics.text=="Waiting for mission statistics...");
|
||||
mode.summary="Player: kills 2, escaped";mode.statsChanged.Invoke();assert(root.statistics.text==mode.summary);
|
||||
mode.reason="An administrator abandoned this round.";mode.ERR_OnEndReasonReplicated();assert(root.debrief.text==mode.reason&&root.debrief.writes==2);
|
||||
content.HandlerDeattached(0);assert(mode.m_OnEndReasonChanged.count==0&&content.m_ERRFailureMode==nullptr);mode.reason="late";mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==2);
|
||||
content.HandlerDeattached(0);assert(mode.statsChanged.count==0&&content.m_ERRStatisticsMode==nullptr);assert(mode.m_OnEndReasonChanged.count==0&&content.m_ERRFailureMode==nullptr);mode.reason="late";mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==2);
|
||||
content.InitContent(&failure);assert(root.debrief.writes==3);content.InitContent(&escape);assert(mode.m_OnEndReasonChanged.count==0&&content.escapeEvent.count==1&&content.escapeRefresh==1);mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==3);
|
||||
content.InitContent(&failure);assert(content.escapeEvent.count==0);Mode replacement;Mode::current=&replacement;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4);
|
||||
Mode::current=&mode;content.m_wRoot=nullptr;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4);content.m_wRoot=&root;root.exists=false;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user