Integrate mission-owned chat administration and menu

This commit is contained in:
2026-09-19 23:49:54 +02:00
parent 27989b85a3
commit 5e742691ad
154 changed files with 19498 additions and 134 deletions
+112
View File
@@ -0,0 +1,112 @@
"""Exercise real menu routing/capability bodies and the authored control contract.
This does not claim native focus, rendering or multiplayer RPC coverage.
"""
import re
from policy_harness import ROOT, body, run
source = ROOT / 'addons/core/Scripts/Game/ERR/ERR_AdminMenu.c'
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(ROOT / 'addons/core/Scripts/Game/ERR/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 10 and set(keys) == set(settings)
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
def validate_layout_structure(text):
tokens = re.findall(r'"(?:[^"\\]|\\.)*"|[{}]|[^\s{}"]+', text)
stack = []
prefix = []
for token in tokens:
if token.endswith('WidgetClass'):
assert not stack or stack[-1] == 'children', 'Widget outside parent Children block: ' + token
if token == '{':
kind = 'property'
if any(part.endswith('WidgetClass') for part in prefix):
kind = 'widget'
elif not prefix:
kind = 'children'
stack.append(kind)
prefix = []
elif token == '}':
assert stack, 'Unexpected closing brace'
stack.pop()
prefix = []
else:
prefix.append(token)
assert not stack, 'Unclosed layout block'
validate_layout_structure(layout.read_text())
try:
validate_layout_structure(layout.read_text().replace('Name "JournalState"', 'Name "JournalState" {', 1))
except AssertionError:
pass
else:
raise AssertionError('Malformed child structure must be rejected')
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key))
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
assert name in names
journal = body(source, 'static string JournalText(')
follow = body(source, 'static bool FollowJournal(')
command = body(source, 'static string Command(')
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
known = ' || '.join('key == "' + key + '"' for key in keys)
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct string : std::string {
using std::string::string;
string(std::string s):std::string(s){}
bool IsEmpty(){return empty();}
int Length(){return size();}
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
};
struct Fields {
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
};
int main() {
string raw="#24 t=7s M3 observe guard";
assert(Fields::JournalText(raw)=="No. 24 t=7s M3 observe guard");
assert(raw=="#24 t=7s M3 observe guard");
assert(Fields::JournalText("unprefixed event")=="unprefixed event");
assert(Fields::JournalText("#25 delivered #24")=="No. 25 delivered No. 24");
assert(Fields::JournalText("").empty());
assert(!Fields::FollowJournal(false,0.5,1000,400,false));
assert(!Fields::FollowJournal(false,0,1000,400,false));
assert(!Fields::FollowJournal(false,0.97,1000,400,false));
assert(Fields::FollowJournal(false,1,1000,400,false));
assert(Fields::FollowJournal(false,0,1000,400,true));
assert(Fields::FollowJournal(false,0,200,400,false));
assert(Fields::FollowJournal(true,0,0,400,false));
std::vector<std::pair<string,string>> cases={
{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"").empty());
assert(Fields::Command(pair.first," \t ").empty());
assert(Fields::Command(pair.first,string(33,'x')).empty());
assert(Fields::Available(pair.first,true,true));
}
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
for(string key:{"heliTimer","convoyCount","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
assert(Fields::Available("aiIntelChancePercent",false,true));
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
print('PASS: actual menu command routing covers all ten settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
+109
View File
@@ -0,0 +1,109 @@
"""Exercise actual admin utility command/state methods through native API adapters."""
from policy_harness import ROOT, body, run
source=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminPlayerTools.c'
def adapt(s):
s=s.replace('protected ', '').replace('null', 'nullptr')
s=s.replace('array<string> replies','Array<String>& replies')
s=s.replace('string ', 'String ').replace('ResourceName ', 'String ')
s=s.replace('args.Trim()', 'args.Trim()')
for typ in ['ERR_GameMode', 'SCR_PlayerController', 'PlayerManager', 'IEntity', 'DamageManagerComponent', 'SCR_InventoryStorageManagerComponent']:
pass
for name in ['actor','m_Actor','mode','controller','manager','m_Mode','m_Controller','m_Damage','inventory','m_Grants','Operation']:
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Players().','Players()->')
s=s.replace('ERR_GameMode.Get()', 'Mode::Get()').replace('Replication.IsServer()', 'Replication::IsServer()').replace('SCR_Global.IsAdmin(', 'Global::IsAdmin(').replace('ERR_Actors.Alive(', 'Actors::Alive(')
s=s.replace('FindComponent(DamageManagerComponent)', 'FindDamage()').replace('FindComponent(SCR_InventoryStorageManagerComponent)', 'FindInventory()')
s=s.replace('DamageManagerComponent.Cast(', 'Damage::Cast(').replace('SCR_InventoryStorageManagerComponent.Cast(', 'Inventory::Cast(')
s=s.replace('purpose: EStoragePurpose.PURPOSE_DEPOSIT, cb: m_Grants->Operation', '1, m_Grants->Operation')
s=s.replace('String item;', 'String item;')
return s
binding=adapt(body(source,'void Bind('))
constructor=adapt(body(source,'void ERR_AdminPlayerTools(')).replace('void ERR_AdminPlayerTools(ERR_AdminGrantState grants = nullptr)', 'Tools(GrantState* grants = nullptr)')
methods='\n'.join(adapt(body(source,signature)) for signature in [
'protected IEntity CurrentBody(', 'protected ResourceName ItemPrefab(', 'protected void RestoreDamage(',
'bool Execute(', 'void Tick(', 'void Stop('])
run(r'''
#include <cassert>
#include <algorithm>
#include <cctype>
#include <string>
#include <vector>
struct String:std::string {
using std::string::string;String()=default;String(const std::string& s):std::string(s){}
String Trim(){auto a=find_first_not_of(" \t\n");return a==npos?String():String(substr(a,find_last_not_of(" \t\n")-a+1));}
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
bool StartsWith(const char* p){return rfind(p,0)==0;}bool IsEmpty(){return empty();}
int Length(){return size();}String Substring(int start,int count){return substr(start,count);}
};
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}};
struct ERR_AdminItemCallback{bool Finished=false,Succeeded=false;};
struct Damage{bool enabled=true;int changes=0;bool IsDamageHandlingEnabled(){return enabled;}void EnableDamageHandling(bool value){enabled=value;changes++;}static Damage* Cast(Damage* value){return value;}};
using DamageManagerComponent=Damage*;
struct Inventory{bool accept=true;int submissions=0;String prefab;ERR_AdminItemCallback* callback=nullptr;static Inventory* Cast(Inventory* value){return value;}
bool TrySpawnPrefabToStorage(String p,int purpose,ERR_AdminItemCallback* cb){assert(purpose==1);submissions++;prefab=p;callback=cb;return accept;}
};using SCR_InventoryStorageManagerComponent=Inventory*;
struct Entity{bool alive=true;Damage damage;Inventory inventory;bool hasDamage=true,hasInventory=true;Damage* FindDamage(){return hasDamage?&damage:nullptr;}Inventory* FindInventory(){return hasInventory?&inventory:nullptr;}};using IEntity=Entity*;
struct Controller{int id=1;Entity* actor=nullptr;int GetPlayerId(){return id;}Entity* GetControlledEntity(){return actor;}};using SCR_PlayerController=Controller*;
struct Manager{Controller* controller=nullptr;Entity* actor=nullptr;Controller* GetPlayerController(int){return controller;}Entity* GetPlayerControlledEntity(int){return actor;}};using PlayerManager=Manager*;
struct Roster{Entity* actor=nullptr;bool CurrentActor(Entity* a){return actor==a;}};
struct Mode{bool active=true;Roster roster;bool hasRoster=true;static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}Roster* Players(){return hasRoster?&roster:nullptr;}};Mode* Mode::current=nullptr;using ERR_GameMode=Mode*;
struct Game{float now=0;Manager manager;Manager* GetPlayerManager(){return &manager;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}}game;Game* GetGame(){return &game;}
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Global{bool admin=true;bool IsAdmin(int){return admin;}}
namespace Actors{bool Alive(Entity* a){return a&&a->alive;}}
struct GrantState{IEntity Actor=nullptr;ERR_AdminItemCallback* Operation=nullptr;String Item;float Deadline=0;bool TimedOut=false;
'''+binding+r'''
};
struct Tools{
ERR_GameMode m_Mode=nullptr;SCR_PlayerController m_Controller=nullptr;IEntity m_Actor=nullptr;DamageManagerComponent m_Damage=nullptr;
bool m_PreviousDamage=false,m_God=false,m_Stopped=false;GrantState* m_Grants=new GrantState();
'''+constructor+methods+r'''
};
struct Fixture{
Mode mode;Controller controller;Entity actor;Tools tools;Array<String> replies;
Fixture(){Mode::current=&mode;controller.actor=&actor;mode.roster.actor=&actor;game.manager.controller=&controller;game.manager.actor=&actor;game.now=0;Global::admin=true;Replication::server=true;}
bool command(String args){replies.clear();return tools.Execute(&mode,&controller,args,replies);}void tick(){replies.clear();tools.Tick(replies);}
};
int main(){
{Fixture f;assert(!f.command("mission time show")&&f.replies.empty());assert(f.command(" GOD SET ON ")&&!f.actor.damage.enabled);assert(f.tools.m_God);f.command("god");assert(f.actor.damage.changes==1);f.command("god show");assert(f.replies[0].find("on")!=String::npos);f.command("ungod");assert(f.actor.damage.enabled&&f.actor.damage.changes==2);f.tools.Stop();assert(f.actor.damage.changes==2);}
{Fixture f;f.actor.damage.enabled=false;f.command("god");f.command("god set off");assert(!f.actor.damage.enabled);}
{Fixture f;f.command("god");Global::admin=false;f.tick();assert(f.actor.damage.enabled&&f.tools.m_Stopped&&f.replies.empty());Global::admin=true;f.command("god");assert(f.actor.damage.enabled);}
{Fixture f;f.command("god");Entity replacement;f.controller.actor=&replacement;game.manager.actor=&replacement;f.mode.roster.actor=&replacement;f.tick();assert(f.actor.damage.enabled&&replacement.damage.enabled&&f.tools.m_Stopped);}
{Fixture f;f.command("god");f.mode.active=false;f.tick();assert(f.actor.damage.enabled&&f.tools.m_Stopped);}
{Fixture f;Global::admin=false;f.command("cheat map");assert(f.actor.inventory.submissions==0);}
{Fixture f;Controller impostor;game.manager.controller=&impostor;f.command("god");assert(f.actor.damage.enabled);}
{Fixture f;f.mode.roster.actor=nullptr;f.command("cheat map");assert(f.actor.inventory.submissions==0);}
{Fixture f;f.actor.alive=false;f.command("god");assert(f.actor.damage.enabled);}
{Fixture f;f.actor.hasDamage=false;f.command("god");assert(!f.tools.m_God);}
{Fixture f;f.actor.hasInventory=false;f.command("cheat map");assert(!f.tools.m_Grants->Operation);}
{Fixture f;f.command("help god");assert(f.replies[0].find("god show")!=String::npos);f.command("help cheat");assert(f.replies[0].find("PASGT")!=String::npos&&f.actor.inventory.submissions==0);f.command("god invalid");assert(f.actor.damage.enabled);f.command("cheat invalid");assert(f.actor.inventory.submissions==0);}
{Fixture f;f.command("cheat map");assert(f.actor.inventory.submissions==1&&f.tools.m_Grants->Operation);for(int i=0;i<5;i++){f.command("cheat helmet");f.tick();}assert(f.actor.inventory.submissions==1);f.actor.inventory.callback->Finished=true;f.actor.inventory.callback->Succeeded=true;f.tick();assert(!f.tools.m_Grants->Operation&&f.replies.size()==1);f.tick();assert(f.replies.empty());f.command("cheat uniform");assert(f.actor.inventory.submissions==2&&f.actor.inventory.prefab.find("Jacket_US_BDU")!=String::npos);}
{Fixture f;f.command("cheat helmet");assert(f.actor.inventory.prefab.find("Helmet_PASGT_01_base")!=String::npos);f.actor.inventory.callback->Finished=true;f.actor.inventory.callback->Succeeded=false;f.tick();assert(f.replies.size()==1&&f.replies[0].find("could not")!=String::npos&&!f.tools.m_Grants->Operation);}
{Fixture f;f.actor.inventory.accept=false;f.command("cheat map");assert(f.actor.inventory.submissions==1&&!f.tools.m_Grants->Operation);f.tick();assert(f.replies.empty());}
{Fixture f;f.command("cheat map");game.now=29999;f.tick();assert(!f.tools.m_Grants->TimedOut);game.now=30000;f.tick();assert(f.tools.m_Grants->TimedOut&&f.replies.size()==1);f.actor.inventory.callback->Finished=true;f.actor.inventory.callback->Succeeded=true;f.tick();assert(f.replies.empty());f.command("cheat map");assert(f.actor.inventory.submissions==1);f.command("god");assert(!f.actor.damage.enabled);}
{Fixture f;f.command("cheat map");auto callback=f.actor.inventory.callback;f.tools.Stop();callback->Finished=true;callback->Succeeded=true;f.tick();assert(f.replies.empty()&&f.actor.inventory.submissions==1);}
{Fixture f;f.command("god");f.actor.alive=false;f.tick();assert(f.actor.damage.enabled&&f.tools.m_Stopped);}
// Same physical body keeps accepted insertion state after revoke/regrant.
{Fixture f;f.command("god");f.command("cheat map");auto shared=f.tools.m_Grants;auto pending=shared->Operation;Global::admin=false;f.tick();assert(f.actor.damage.enabled&&f.tools.m_Stopped);Global::admin=true;Tools renewed(shared);renewed.Execute(&f.mode,&f.controller,"cheat map",f.replies);assert(f.actor.inventory.submissions==1&&shared->Operation==pending);pending->Finished=true;pending->Succeeded=true;renewed.Tick(f.replies);assert(!shared->Operation);renewed.Execute(&f.mode,&f.controller,"cheat map",f.replies);assert(f.actor.inventory.submissions==2);}
// Timeout quarantine survives fresh sessions even after a late callback.
{Fixture f;f.actor.damage.enabled=false;f.command("god");f.command("cheat map");game.now=30000;f.tick();auto shared=f.tools.m_Grants;Global::admin=false;f.tick();assert(!f.actor.damage.enabled);Global::admin=true;shared->Operation->Finished=true;shared->Operation->Succeeded=true;Tools renewed(shared);renewed.Execute(&f.mode,&f.controller,"cheat helmet",f.replies);renewed.Tick(f.replies);assert(f.actor.inventory.submissions==1&&shared->TimedOut);}
// A different canonical body gets a fresh grant state, never god mode.
{Fixture f;f.command("god");f.command("cheat map");auto shared=f.tools.m_Grants;auto oldCallback=shared->Operation;f.tools.Stop();Entity next;f.controller.actor=&next;game.manager.actor=&next;f.mode.roster.actor=&next;Tools renewed(shared);renewed.Execute(&f.mode,&f.controller,"cheat helmet",f.replies);assert(f.actor.damage.enabled&&next.damage.enabled&&next.inventory.submissions==1&&shared->Actor==&next&&shared->Operation!=oldCallback);oldCallback->Finished=true;oldCallback->Succeeded=true;f.replies.clear();renewed.Tick(f.replies);assert(f.replies.empty());}
}
''')
s=source.read_text()
assert 'PURPOSE_DEPOSIT' in s and 'PURPOSE_ANY' not in s
assert 'FullHeal' not in s and 'DeleteEntity' not in s and 'CallLater' not in s
assert 'protected override void OnComplete() { Finish(true); }' in s
assert 'protected override void OnFailed() { Finish(false); }' in s
assert 'protected void Finish(bool success) { if (!Finished)' in s
print('PASS: actual admin body authorization, god restoration, vanilla item deposit, callback completion/failure, timeout quarantine and stop/revocation paths')
bridge=(ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerAdminCommands.c').read_text()
session=(ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminSession.c').read_text()
assert 'protected ref ERR_AdminGrantState m_ERRAdminGrants = new ERR_AdminGrantState();' in bridge
assert 'new ERR_AdminSession(mode, this, token, m_ERRAdminGrants)' in bridge
assert session.count('new ERR_AdminPlayerTools(m_Grants)')==2
assert 'm_Grants' not in body(source, 'void Stop(')
print('PASS: same-body revoke/regrant preserves pending insertions and timeout quarantine; new-body binding isolates late callbacks and never inherits god mode')
+72
View File
@@ -0,0 +1,72 @@
"""Exercise actual registration/dispatch bodies without requiring optional addons.
Native script compilation and authenticated RPC tests remain separate checks.
"""
from policy_harness import ROOT, body, run
core = ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
hq = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQAdminProvider.c'
intel = ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelAdminProvider.c'
source=core.read_text()
assert 'ERR_HQ' not in source and 'ERR_IntelService' not in source
assert 'modded class' not in (ROOT/'addons/chat/Scripts/Game/ERR/ERR_AdminCommands.c').read_text()
assert 'Admin().Register(new ERR_HQAdminProvider(hq))' in (ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c').read_text()
assert 'Admin().Register(new ERR_IntelAdminProvider())' in (ROOT/'addons/intel/Scripts/Game/ERR/ERR_IntelService.c').read_text()
execute=body(core,'void Execute(')
assert execute.index('if (!Replication.IsServer()') < execute.index('if (!authorized)') < execute.index('Extension(mode, arguments, replies)')
assert 'arguments = "mission " + arguments' in execute
def translate(s):
return (s.replace('protected ', '').replace('override ', '')
.replace('array<string>', 'Array<string>&').replace('out int ', 'int& ')
.replace('ERR_AdminProvider provider', 'Provider* provider').replace('ERR_GameMode mode','Mode* mode')
.replace('foreach (Provider* provider : m_Providers)', 'for (Provider* provider : m_Providers)')
.replace('ERR_CommandArguments.CoreSetting(', 'CoreSetting(')
.replace('provider.', 'provider->').replace('m_Providers[i].', 'm_Providers[i]->')
.replace('.IsEmpty()', '.empty()').replace('bool handled;', 'bool handled = false;'))
methods='\n'.join(translate(body(core,sig)) for sig in ('void Register(', 'void Capabilities(', 'int JournalSequence(', 'bool ReadJournal(', 'protected string Setting(', 'protected bool Extension('))
aliases='struct HQAliases{'+translate(body(hq,'override string Setting('))+'};\nstruct IntelAliases{'+translate(body(intel,'override string Setting('))+'};'
help_bodies='struct HQHelp{'+translate(body(hq,'override bool Help('))+'};\nstruct IntelHelp{'+translate(body(intel,'override bool Help('))+'};'
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}int Count(){return this->size();}};
struct Mode{};
string CoreSetting(string section,string option){if(section=="intel"&&option=="reveal")return "intelRevealChancePercent";return "";}
struct Provider{
string id;int sequence=-1;int calls=0;bool accepts=false;int reads=0;
string Id(){return id;}void Capabilities(Array<string>& values){values.Insert("supports="+id);}
int JournalSequence(){return sequence;}
bool ReadJournal(int after,Array<string>& lines,int& cursor,int limit=8){if(sequence<0)return false;reads++;cursor=std::min(sequence,after+limit);lines.Insert(id);return true;}
string Setting(string section,string){return section==id?id+"Setting":"";}
bool Execute(Mode*,string,Array<string>& replies){calls++;if(accepts)replies.Insert(id);return accepts;}
};
struct Admin{Array<Provider*>m_Providers;
'''+methods+'};\n'+aliases+help_bodies+r'''
int main(){
Admin admin;Mode mode;Array<string> lines;int cursor=0;
assert(admin.JournalSequence()==-1&&!admin.ReadJournal(0,lines,cursor));
assert(admin.Setting("intel","reveal")=="intelRevealChancePercent"&&admin.Setting("hq","squads").empty());
admin.Register(nullptr);Provider empty;admin.Register(&empty);assert(admin.m_Providers.empty());
Provider hq;hq.id="hq";hq.sequence=12;hq.accepts=true;
Provider intel;intel.id="intel";intel.accepts=true;
admin.Register(&hq);admin.Register(&hq);admin.Register(&intel);assert(admin.m_Providers.size()==2);
admin.Capabilities(lines);assert(lines.size()==2&&lines[0]=="supports=hq"&&lines[1]=="supports=intel");
assert(admin.JournalSequence()==12);assert(admin.ReadJournal(2,lines,cursor,4)&&cursor==6&&lines.size()==1&&lines[0]=="hq");
assert(hq.reads==1&&intel.reads==0);
lines.clear();assert(admin.Extension(&mode,"status",lines)&&lines.size()==2&&hq.calls==1&&intel.calls==1);
lines.clear();assert(admin.Extension(&mode,"hq show",lines)&&lines.size()==1&&hq.calls==2&&intel.calls==1);
hq.accepts=false;intel.accepts=false;lines.clear();assert(!admin.Extension(&mode,"unknown",lines)&&lines.empty());
Provider replacement;replacement.id="hq";replacement.sequence=20;
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
HQAliases ha;IntelAliases ia;
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger").empty());
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=4);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
lines.clear();assert(!hh.Help("unknown",lines)&&!ih.Help("unknown",lines)&&lines.empty());
}
''')
print('PASS: mission provider registration is idempotent, optional dispatch is ordered, status aggregates, journals preserve cursors, aliases stay with their addons and topic help is bounded')
+83
View File
@@ -0,0 +1,83 @@
"""Actual session authority, transport scope and throttle policies; RPC delivery is native."""
from policy_harness import ROOT, body, run
session=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminSession.c'
bridge=ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerAdminCommands.c'
client=ROOT/'addons/core/Scripts/Game/ERR/ERR_ChatClient.c'
valid=body(session,'bool Valid(')
server=body(bridge,'protected ERR_AdminSession ERR_ServerSession(')
# Execute the exact admission/token guard prefix, before session allocation.
server=server[:server.index(' if (m_ERRAdminSession && token <')]+ ' return true;\n }'
server=server.replace('protected ERR_AdminSession ERR_ServerSession(', 'bool Admit(').replace('return null;', 'return false;')
command=body(session,'void Command(')
command=command[:command.index(' CheckBody();')]+' accepted++;\n }'
snapshot=body(session,'void Snapshot(')
snapshot=snapshot[:snapshot.index(' array<string> values')]+ ' snapshots++;\n }'
scope=body(client,'int Scope(')
accept=body(client,'bool Accept(')
def adapt(s):
s=s.replace('ERR_GameMode mode','Mode* mode').replace('PlayerManager players','Players* players').replace('ERR_AdminMenu menu','Menu* menu')
for n in ('Replication','ERR_GameMode','SCR_Global','ERR_MissionRules','ERR_AdminMenu'):
s=s.replace(n+'.',n+'::')
for n in ('m_Controller','m_Mode','mode','players','menu'):
s=s.replace(n+'.',n+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->')
s=s.replace('arguments.Length()', 'arguments.size()')
return s
run(r'''
#include <cassert>
#include <string>
using string=std::string;
struct Controller {int id=7;int GetPlayerId(){return id;}};
struct Mode {int phase=1;int Phase(){return phase;}};
struct Players {Controller* owned=nullptr;Controller* GetPlayerController(int id){return owned&&id==owned->id?owned:nullptr;}};
struct Game {Players players;Controller* local=nullptr;float now=0;Players* GetPlayerManager(){return &players;}Controller* GetPlayerController(){return local;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
Game* gameptr=&game;Game* GetGame(){return gameptr;}
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace SCR_Global{bool admin=true;bool IsAdmin(int id){return admin&&id==7;}}
namespace ERR_GameMode{Mode* current=nullptr;Mode* Get(){return current;}}
namespace ERR_MissionRules{bool Terminal(int phase){return phase==5;}}
struct Menu {int closes=0;void Close(){closes++;}} menu;
namespace ERR_AdminMenu{Menu* opened=nullptr;Menu* GetOpenMenu(){return opened;}}
struct Authority{bool m_Stopped=false;Controller* m_Controller=nullptr;Mode* m_Mode=nullptr;
'''+adapt(valid)+r'''
};
struct Transport{Mode* m_ERRServerChatMode=nullptr;int m_ERRServerChatToken=0;
'''+adapt(server)+r'''
};
struct Rate{bool allowed=true;float m_LastRequest=-1000,m_LastSnapshot=-1000;int accepted=0,snapshots=0;bool Authorize(){return allowed;}
'''+adapt(command)+adapt(snapshot)+r'''
};
struct Client{bool m_Stopped=false;Controller* m_Controller=nullptr;Mode* m_Mode=nullptr;int m_Token=1;
'''+adapt(scope)+adapt(accept)+r'''
};
int main(){
Controller owner,foreign;foreign.id=8;Mode first,second;game.local=&owner;game.players.owned=&owner;ERR_GameMode::current=&first;
Authority authority{false,&owner,&first};assert(authority.Valid());
SCR_Global::admin=false;assert(!authority.Valid());SCR_Global::admin=true;
Replication::server=false;assert(!authority.Valid());Replication::server=true;
game.players.owned=&foreign;assert(!authority.Valid());game.players.owned=&owner;
ERR_GameMode::current=&second;assert(!authority.Valid());ERR_GameMode::current=&first;
authority.m_Stopped=true;assert(!authority.Valid());authority.m_Stopped=false;
Transport transport;assert(!transport.Admit(0));assert(transport.Admit(1));
assert(transport.Admit(1));assert(!transport.Admit(2));assert(!transport.Admit(999)); // cannot reset session/rate/item state within round
ERR_GameMode::current=&second;assert(!transport.Admit(1));assert(transport.Admit(3));assert(!transport.Admit(2));assert(!transport.Admit(4));assert(transport.Admit(3));
second.phase=5;assert(!transport.Admit(3));second.phase=1;
Replication::server=false;assert(!transport.Admit(3));Replication::server=true;
Rate rate;game.now=0;rate.Command("status",false);assert(rate.accepted==1);rate.Command("status",false);assert(rate.accepted==1);
game.now=249;rate.Command("status",false);assert(rate.accepted==1);game.now=250;rate.Command("status",true);assert(rate.accepted==2);
game.now=1000;rate.Command(string(161,'x'),false);assert(rate.accepted==2);rate.allowed=false;rate.Command("status",false);rate.Snapshot(true);assert(rate.accepted==2&&rate.snapshots==0);rate.allowed=true;
rate.Snapshot();rate.Snapshot();assert(rate.snapshots==1);game.now=1499;rate.Snapshot();assert(rate.snapshots==1);game.now=1500;rate.Snapshot();assert(rate.snapshots==2);
Client client{false,&owner,&second,3};assert(client.Accept(3));assert(!client.Accept(2));
ERR_AdminMenu::opened=&menu;ERR_GameMode::current=&first;assert(!client.Accept(3));assert(client.m_Token==4&&menu.closes==1);assert(client.Accept(4)&&menu.closes==1);
game.local=&foreign;assert(!client.Accept(4));game.local=&owner;client.m_Stopped=true;assert(!client.Accept(4));
}
''')
source=bridge.read_text()
for name in ('ERR_AdminOpenResponse','ERR_AdminReplyResponse','ERR_AdminSnapshotResponse','ERR_AdminLogResponse'):
signature=source.index('protected void '+name+'(')
assert '[RplRpc(RplChannel.Reliable, RplRcver.Owner)]' in source[max(0,signature-65):signature]
assert 'm_ERRChatClient.Accept(token)' in body(bridge,'protected void '+name+'(')
assert 'm_Tools.Stop();' in body(session,'void Stop(')
assert 'm_Tools.Stop();' in body(session,'protected void CheckBody(')
assert body(session,'void Command(').index('if (!Authorize()) return;') < body(session,'void Command(').index('m_Tools.Execute(')
print('PASS: actual current-controller/admin/mode authorization, per-round anti-replay, token-rotation refusal, command/snapshot throttles and client scope; owner-only replies and player-tool cleanup wiring')
+51
View File
@@ -0,0 +1,51 @@
"""Actual session Tick and journal router keep tool completions/feed deliveries separate."""
from policy_harness import ROOT, body, run
session=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminSession.c'
admin=ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
tick=body(session,'bool Tick(')
reader=body(admin,'bool ReadJournal(')
def adapt(s):
s=s.replace('array<string> replies = {};','Array<string> replies;')
s=s.replace('array<string> lines','Array<string>& lines').replace('out int cursor','int& cursor')
s=s.replace('foreach (string reply : replies)', 'for (string reply : replies)').replace('foreach (string line : replies)', 'for (string line : replies)')
s=s.replace('foreach (ERR_AdminProvider provider : m_Providers)', 'for (Provider* provider : m_Providers)')
for name in ['m_Tools','m_Controller','m_Mode','provider']:
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('Admin().','Admin()->')
return s
run(r'''
#include <cassert>
#include <string>
#include <vector>
using string=std::string;
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}void Insert(T value){this->push_back(value);}};
struct Provider{Array<string> journal;int calls=0;bool ReadJournal(int after,Array<string>& lines,int& cursor,int limit){calls++;cursor=after;for(int i=after;i<int(journal.size())&&limit>0;i++,limit--){lines.Insert(journal[i]);cursor=i+1;}return true;}};
struct Admin{Array<Provider*> m_Providers;
'''+adapt(reader)+r'''
};
struct Mode{struct Admin admin;struct Admin* Admin(){return &admin;}};
struct Tools{Array<string> pending;int ticks=0;void Tick(Array<string>& replies){ticks++;for(auto line:pending)replies.Insert(line);pending.clear();}};
struct Controller{Array<string> received;void ERR_AdminDeliverReply(int token,string reply,bool menu){assert(token==7&&!menu);received.Insert(reply);}};
struct Game{float now=0;Game* GetWorld(){return this;}float GetWorldTime(){return now;}}game;Game* GetGame(){return &game;}
struct Session{
bool allowed=true,m_Live=true;int m_Token=7,m_Cursor=0,bodyChecks=0;float m_NextFeed=0;Mode* m_Mode;Tools* m_Tools;Controller* m_Controller;
bool Authorize(){return allowed;}void CheckBody(){bodyChecks++;}
'''+adapt(tick)+r'''
};
int main(){
Mode mode;Tools tools;Controller controller;Provider provider;mode.admin.m_Providers.Insert(&provider);
provider.journal.Insert("hq1");provider.journal.Insert("hq2");tools.pending.Insert("grant complete");
Session session{true,true,7,0,0,0,&mode,&tools,&controller};
assert(session.Tick());assert(controller.received.size()==3);
assert(controller.received[0]=="grant complete"&&controller.received[1]=="hq1"&&controller.received[2]=="hq2");
assert(session.m_Cursor==2&&session.m_NextFeed==2000);
game.now=1000;assert(session.Tick());assert(controller.received.size()==3&&provider.calls==1);
game.now=2000;tools.pending.Insert("grant failed");assert(session.Tick());assert(controller.received.size()==4&&controller.received[3]=="grant failed");
game.now=4000;provider.journal.Insert("hq3");assert(session.Tick());assert(controller.received.size()==5&&controller.received[4]=="hq3");
session.m_Live=false;game.now=6000;tools.pending.Insert("grant timeout");assert(session.Tick());assert(controller.received.size()==6&&provider.calls==3);
session.m_Live=true;session.allowed=false;game.now=8000;tools.pending.Insert("must not deliver");assert(!session.Tick());assert(controller.received.size()==6&&provider.calls==3&&tools.pending.size()==1);
// No provider still clears tool replies before the live-feed loop.
session.allowed=true;mode.admin.m_Providers.clear();assert(session.Tick());assert(controller.received.size()==7&&controller.received.back()=="must not deliver");
}
''')
print('PASS: actual session Tick + journal router delivers each grant completion and new HQ line once, without repeats on empty/unavailable feeds or unauthorized ticks')
+2 -1
View File
@@ -23,7 +23,7 @@ class ArchiveControl(unittest.TestCase):
root=Path(tmp.name);(root/'addons/core').mkdir(parents=True)
(root/'addons/core/addon.gproj').write_text('Synthetic fixture source, not a native project')
tools=root/'tools';tools.mkdir()
for name in ('archive_native.py','native_report.py','source_manifest.py','native_provenance.py','headless_game.py'):
for name in ('archive_native.py','native_report.py','source_manifest.py','native_provenance.py','headless_game.py','project_schema.py'):
shutil.copyfile(ROOT/'tools'/name,tools/name)
session=root/'session';session.mkdir()
(session/'console.log').write_text(content if content is not None else BASE if project=='vanilla-control' else ORDINARY)
@@ -38,6 +38,7 @@ class ArchiveControl(unittest.TestCase):
dest=root/'docs/evidence/control'
self.assertTrue(dest.exists(),done.stderr)
self.assertEqual((dest/'console.log.txt').read_bytes(),(session/'console.log').read_bytes())
self.assertTrue((dest/'report.json').is_file(), done.stderr)
return done,json.loads((dest/'report.json').read_text()),command,dest
def test_clean_control_gets_own_result_and_no_successor_qualification(self):
result,report,command,dest=self.archive('vanilla-control','vanilla-control-started',True)
+3 -3
View File
@@ -1,12 +1,12 @@
"""Run actual companion dispatch and discovery ledger bodies; native RPC is separate."""
from policy_harness import ROOT, body, run
admin = ROOT / 'addons/core/Scripts/Game/ERR/ERR_AdminService.c'
chat = ROOT / 'addons/chat/Scripts/Game/ERR/ERR_AdminCommands.c'
provider = ROOT / 'addons/intel/Scripts/Game/ERR/ERR_IntelAdminProvider.c'
discovery = ROOT / 'addons/core/Scripts/Game/ERR/ERR_DiscoveryService.c'
execute = body(admin, 'void Execute(')
assert execute.index('if (!Replication.IsServer()') < execute.index('if (!authorized)') < execute.index('if (Extension(mode, arguments, replies))')
extension = body(chat, 'protected override bool Extension(')
extension = (extension.replace('protected override ', '').replace('array<string>', 'Array<string>&')
extension = body(provider, 'override bool Execute(')
extension = (extension.replace('override ', '').replace('bool Execute(', 'bool Extension(').replace('array<string>', 'Array<string>&')
.replace('ERR_GameMode mode', 'Mode* mode').replace('ERR_IntelService intel', 'Intel* intel')
.replace('ERR_HQ hq', 'HQ* hq').replace('ERR_HQ.Cast(', 'HQ::Cast(')
.replace('super.Extension(', 'Base::Extension(').replace('Math.Max(', 'std::max(')
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Strict Core menu project extension in both source and packed validators."""
from pathlib import Path
import re
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT/'tools'))
import headless_game as packed
import source_bundle as bundle
from project_schema import parse_project
CURRENT = (ROOT/'addons/core/addon.gproj').read_text()
MENU = CURRENT[CURRENT.index(' Configurations {'):CURRENT.rindex('}')]
MINIMAL = CURRENT[:CURRENT.index(' Configurations {')]+'}\n'
CORE_PROJECT = 'addons/core/addon.gproj'
MENU_FILE = 'addons/core/Configs/ERR/AdminMenus.conf'
class ProjectSchemaTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.payload = {name: (ROOT/name).read_bytes() for name in bundle.inventory(ROOT)}
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='erri-project-schema-')
self.addCleanup(self.temp.cleanup)
self.candidate = Path(self.temp.name)
(self.candidate/'data.pak').write_bytes(b'SYNTHETIC PACKAGE')
(self.candidate/'resourceDatabase.rdb').write_bytes(b'SYNTHETIC DATABASE')
def packed(self, text, project='core'):
(self.candidate/'addon.gproj').write_text(text)
return packed.packed_inventory(self.candidate, project)
def source(self, text):
payload = dict(self.payload)
payload[CORE_PROJECT] = text.encode()
return bundle.validate(payload, ROOT)
def test_old_minimal_and_exact_owned_menu_extension_are_accepted(self):
for text in (MINIMAL, CURRENT, re.sub(r'\s+', ' ', CURRENT)):
with self.subTest(text=text):
match = parse_project(text, allow_core_menu=True)
self.assertIsNotNone(match)
self.assertEqual(match.group(1, 2), ('ERR_Core', packed.PACKED_GUID))
self.assertEqual(set(self.packed(text)), packed.PACKED_FILES)
self.assertEqual(self.source(text)['core']['dependencies'], [packed.PACKED_BASE])
self.assertIsNotNone(parse_project(MINIMAL))
self.assertIsNone(parse_project(CURRENT))
def test_menu_extension_cannot_redirect_override_or_add_unreviewed_settings(self):
mutations = (
CURRENT.replace('B48C7FA12692DE03', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('50C890C996B0A6FC', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('Configs/ERR/AdminMenus.conf', 'Configs/Foreign/Menu.conf'),
CURRENT.replace('MenuConfigs {', 'MenuConfigs +{'),
CURRENT.replace('MenuConfigs {', 'MenuConfigs -{'),
CURRENT.replace('C747AFB6B750CE9A', 'FFFFFFFFFFFFFFFF'),
CURRENT.replace('Configs/System/chimeraMenus.conf', 'Configs/Foreign/Menu.conf'),
CURRENT.replace('"{C747AFB6B750CE9A}Configs/System/chimeraMenus.conf"', ''),
CURRENT.replace('GameProjectConfig PC', 'GameProjectConfig HEADLESS'),
CURRENT.replace('GameProjectConfig PC', 'GameProjectConfig XBOX_SERIES'),
CURRENT.replace('GameProjectConfig PC', 'UnknownConfig PC'),
CURRENT.replace('MenuManagerSettings MenuManagerSettings', 'MenuManagerSettings OtherSettings'),
CURRENT.replace(' MenuConfigs', ' UnknownSetting 1\n MenuConfigs'),
CURRENT.replace('"{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf"', '"{B48C7FA12692DE03}Configs/ERR/AdminMenus.conf" "{FFFFFFFFFFFFFFFF}Extra.conf"'),
CURRENT.replace(MENU, MENU+MENU),
CURRENT+'GameProject {}\n',
)
for text in mutations:
with self.subTest(text=text):
self.assertIsNone(parse_project(text, allow_core_menu=True))
with self.assertRaises(ValueError): self.packed(text)
with self.assertRaisesRegex(ValueError, 'identity/dependency'): self.source(text)
def test_identity_and_exact_dependencies_remain_required_with_extension(self):
for text in (
CURRENT.replace('ERR_Core', 'ERR_Chat'),
CURRENT.replace(packed.PACKED_GUID, 'FFFFFFFFFFFFFFFF'),
CURRENT.replace(packed.PACKED_BASE, '5F16D7E4A1CBE075'),
CURRENT.replace('Dependencies {', 'Dependencies { "58D0FB3206B6F859"'),
CURRENT.replace('Dependencies {', 'Dependencies { "5192138CB9D5B911"'),
):
with self.subTest(text=text):
with self.assertRaises(ValueError): self.packed(text)
with self.assertRaisesRegex(ValueError, 'identity/dependency'): self.source(text)
def test_extension_is_core_only(self):
for name, (identity, guid, deps) in packed.PACKED_PROJECTS.items():
if name == 'core': continue
text = (ROOT/'addons'/name/'addon.gproj').read_text()
text = text[:text.rindex('}')]+MENU+'}\n'
with self.subTest(project=name):
with self.assertRaises(ValueError): self.packed(text, name)
payload = dict(self.payload)
payload['addons/'+name+'/addon.gproj'] = text.encode()
with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.validate(payload, ROOT)
def test_source_menu_reference_must_resolve_to_its_owned_resource(self):
payload = dict(self.payload)
payload.pop(MENU_FILE)
payload.pop(MENU_FILE+'.meta')
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla'): bundle.validate(payload, ROOT)
payload = dict(self.payload)
payload[MENU_FILE+'.meta'] = payload[MENU_FILE+'.meta'].replace(b'B48C7FA12692DE03', b'FFFFFFFFFFFFFFFF')
with self.assertRaisesRegex(ValueError, 'unresolved or nonvanilla'): bundle.validate(payload, ROOT)
if __name__ == '__main__':
unittest.main()
+2 -2
View File
@@ -39,8 +39,8 @@ assert monitor.index('if (!hints) return;')<monitor.index('hints.ShowCustom(')<m
assert 'm_ERRShownObserverReason = m_ERRObserverReason;' in monitor and 'm_ERRShownObserverAvailable = survivorAvailable;' in monitor
assert 'm_ERRObserverReady = false;' in body(p,'protected void ERR_CloseObserverCamera(')
assert 'CallLater' not in monitor, 'No additional timer belongs to guidance'
commands=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerAdminCommands.c','protected void ERR_OnCommand(')
assert commands.index('ERR_CycleObserver(direction);')<commands.index('Rpc(ERR_Request, arguments);')
commands=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_PlayerAdminCommands.c','void ERR_AdminSubmit(')
assert commands.index('ERR_CycleObserver(direction);')<commands.index('Rpc(ERR_AdminRequest,')
admin=body(ROOT/'addons/core/Scripts/Game/ERR/ERR_AdminService.c','void Execute(')
assert 'if (!authorized) { replies.Insert("Administrator access required."); return; }' in admin
print('PASS: actual reason/availability guidance and refresh matrix; no-target copy omits cycling, hint retry retained, existing command/admin paths unchanged; native UI pending')