Add configurable anonymous site markers and record Workshop 117

This commit is contained in:
2026-10-20 18:35:22 +02:00
parent 87d85ff02e
commit f41d8ba2f9
17 changed files with 265 additions and 70 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ navigation = source.with_name('ERR_AdminMenuPages.c')
layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(fields, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "selfEpinephrine", "planeReconEnabled", "restartOnEmpty"}
assert len(keys) == len(set(keys)) and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "selfEpinephrine", "planeReconEnabled", "restartOnEmpty", "showAllSites"}
assert set(keys) <= set(settings) # Every menu field must have a server setting.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
+3
View File
@@ -45,6 +45,9 @@ int main(){
assert(Valid("fieldHospitalCount",1)&&Valid("fieldHospitalCount",6)&&Valid("fieldHospitalCount",8));
assert(!Valid("fieldHospitalCount",0)&&!Valid("fieldHospitalCount",9));
assert(CoreSetting("intel","sites")=="showAllSites");
assert(Valid("showAllSites",0)&&Valid("showAllSites",1));
assert(!Valid("showAllSites",-1)&&!Valid("showAllSites",2));
assert(CoreSetting("intel","reveal")=="intelRevealChancePercent");
assert(CoreSetting("mission","radio")=="radioDefenceSeconds");
assert(CoreSetting("mission","objectguards")=="objectGuardCount");
+2 -2
View File
@@ -26,7 +26,7 @@ template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v
template<class T> struct Set:std::set<T>{bool Contains(T v){return this->count(v);}void Insert(T v){this->insert(v);}int Count(){return this->size();}};
namespace Replication { bool server=true; bool IsServer(){return server;} }
struct Number {int n;string ToString(){return std::to_string(n);}};
struct Settings {Number Get(string key){return {key=="aiIntelChancePercent"?60:15};}};
struct Settings {Number Get(string key){return {key=="aiIntelChancePercent"?60:(key=="showAllSites"?0:15)};}};
struct Intel {int starts=0;bool canStart=true;bool BeginReroll(){starts++;return canStart;}string RerollStatus(){return "batch status";}};
struct Journal {int Sequence(){return 0;}void Read(int,Array<string>&,int&) {}};
struct HQ {struct Journal journal;static HQ* Cast(HQ* h){return h;}string Status(){return "hq";}struct Journal* Journal(){return &journal;}};
@@ -40,7 +40,7 @@ struct Ledger {Set<IEntity> m_Spent;
};
int main(){
Admin admin;Mode mode;Array<string> replies;Intel intel;mode.intel=&intel;
assert(admin.Extension(&mode,"intel show",replies));assert(replies.size()==2&&replies[0]=="aiIntelChancePercent=60"&&replies[1]=="intelRevealChancePercent=15"&&intel.starts==0);
assert(admin.Extension(&mode,"intel show",replies));assert(replies.size()==3&&replies[0]=="aiIntelChancePercent=60"&&replies[1]=="showAllSites=0"&&replies[2]=="intelRevealChancePercent=15"&&intel.starts==0);
replies.clear();assert(admin.Extension(&mode,"intel reroll show",replies));assert(intel.starts==0&&replies.size()==1&&replies[0]=="batch status");
replies.clear();assert(admin.Extension(&mode,"intel reroll",replies));assert(intel.starts==1&&replies.size()==1);
intel.canStart=false;replies.clear();assert(admin.Extension(&mode,"intel reroll",replies));assert(intel.starts==2&&replies.size()==2&&replies[1]=="batch status");
+1 -1
View File
@@ -5,7 +5,7 @@ from policy_harness import ROOT,body,run
spec=importlib.util.spec_from_file_location('import_settings',BASE/'tools/import_settings.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
result,deferred=m.merge({'configVersion':1,'reportIntervalSeconds':20.0,'supportSquadLimit':2,'responseRadiusMetres':1500,'convoyCount':3},{'aiIntelChancePercent':0,'intelRevealChancePercent':100})
assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert result==dict(aiDoctrine=0,aiShadow=0,planeAltitudeMetres=250,planeReplacementSeconds=60,planeSectorSeconds=120,planeReconEnabled=1,roadblockCount=4,civilianTrafficCount=3,startMode=0,keepWeaponOnUnconscious=1,selfEpinephrine=0,restartOnEmpty=1,emptyRestartHours=5,reportIntervalSeconds=20,supportSquadLimit=2,responseRadiusMetres=1500,aiIntelChancePercent=0,intelRevealChancePercent=100,showAllSites=0,radioDefenceSeconds=180,startTimeMinutes=840,timeMultiplier=1,startDateCode=0,weatherProfile=0,viewDistanceMetres=0,automaticEquipmentChancePercent=25,antiAirChancePercent=7,antiPersonnelMineChancePercent=60,automaticConvoyEnabled=1,convoyCount=3,heliTimerMinutes=15)
assert deferred==[]
for count in (0,1,3,20):
values,notes=m.merge(ai={'convoyCount':count})
+105
View File
@@ -0,0 +1,105 @@
"""Run production marker transitions and discovery with engine boundary doubles."""
from policy_harness import ROOT, body, run
base = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
map_source = base/'ERR_DiscoveryMap.c'
rules = '\n'.join(body(map_source, s) for s in ('static int DisplayKnowledge(', 'static int Coordinate(')).replace('Math.Round','std::round').replace('Math.Floor','std::floor')
update = body(map_source, 'protected void ERR_UpdateMarker(').replace('protected ', '')
discover = body(base/'ERR_Location.c', 'bool Discover(')
icon = body(map_source, 'void SetLocationIcon(')
for name in ('ERR_GameMode', 'SCR_MapMarkerManagerComponent', 'ERR_DiscoveryMapRules', 'ERR_LocationKind', 'SCR_EMapMarkerType', 'Replication', 'ERR_MissionRules'):
update = update.replace(name+'.', name+'::')
discover = discover.replace(name+'.', name+'::')
for typename in ('ERR_GameMode', 'SCR_MapMarkerManagerComponent'):
update = update.replace(typename+' ', typename+'* ')
discover = discover.replace('ERR_GameMode mode', 'ERR_GameMode* mode')
for name in ('mode', 'manager', 'oldManager', 'm_ERRMarker'):
update = update.replace(name+'.',name+'->')
discover = discover.replace('mode.', 'mode->')
update = update.replace('GetGame().','GetGame()->').replace('null','nullptr')
mission = body(base/'ERR_MissionRules.c', 'static int Discovery(')
run(r'''
#include <cassert>
#include <cmath>
#include <string>
#include <memory>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
namespace ERR_LocationKind {enum Kind{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
namespace SCR_EMapMarkerType {constexpr int PLACED_CUSTOM=0;}
struct Game{bool controller=true;bool GetPlayerController(){return controller;}}game;
Game* GetGame(){return &game;}
struct Replication{static inline bool server=true;static bool IsServer(){return server;}static void BumpMe(){}};
struct ERR_GameMode{bool enabled=false;int events=0;static ERR_GameMode* Get();bool ShowAllSites(){return enabled;}void LocationDiscovered(void*,int,int){events++;}}mode;
ERR_GameMode* ERR_GameMode::Get(){return &mode;}
struct ERR_MissionRules {'''+mission+r'''};
struct ERR_DiscoveryMapRules {'''+rules+r'''};
struct ERR_DiscoveryMarker{
string label,m_Icon;int x=0,refreshes=0;bool removed=false;
static string Icon(int kind,string){return kind==ERR_LocationKind::RADIO?"radio":"supplies";}
void ApplyIcon(){}void SetType(int){}void SetMarkerConfigID(int){}
void SetCustomText(string v){label=v;}void RefreshPresentation(){refreshes++;}
void SetDiscoveryPosition(float p,int knowledge){x=ERR_DiscoveryMapRules::Coordinate(p,knowledge);}
void OnDelete(){removed=true;}
'''+icon.replace('ERR_LocationKind kind','int kind')+r'''
};
struct SCR_MapMarkerManagerComponent{
static inline bool ready=true;int inserts=0,removes=0;
static SCR_MapMarkerManagerComponent* GetInstance();
bool GetMarkerConfig(){return ready;}
void ERR_InsertDiscoveryMarker(ERR_DiscoveryMarker*){inserts++;}
void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker* marker){removes++;delete marker;}
}manager;
SCR_MapMarkerManagerComponent* SCR_MapMarkerManagerComponent::GetInstance(){return &manager;}
struct Location{
ERR_DiscoveryMarker* m_ERRMarker=nullptr;int m_ERRShown=-1,m_Knowledge=0;string m_ERRShownLabel;
bool m_Prepared=true;int kind=ERR_LocationKind::RADIO;
int Knowledge(){return m_Knowledge;}bool Prepared(){return m_Prepared;}int Kind(){return kind;}
string Label(){return "Secret radio station";}string Id(){return "secret-radio-id";}float Position(){return 6123.25f;}
'''+update+'\n'+discover+r'''
~Location(){delete m_ERRMarker;}
};
int main(){
Location site;site.ERR_UpdateMarker();assert(!site.m_ERRMarker&&site.m_Knowledge==0);
mode.enabled=true;site.ERR_UpdateMarker();assert(site.m_ERRMarker);
assert(site.m_ERRMarker->label=="Unknown site"&&site.m_ERRMarker->m_Icon=="mark-question"&&site.m_ERRMarker->x==6123);
assert(site.m_Knowledge==0&&mode.events==0); // Visibility never grants discovery or objective progress.
auto* first=site.m_ERRMarker;site.ERR_UpdateMarker();assert(site.m_ERRMarker==first&&manager.inserts==1);
mode.enabled=false;site.ERR_UpdateMarker();assert(!site.m_ERRMarker&&manager.removes==1);
mode.enabled=true;site.ERR_UpdateMarker();assert(site.m_ERRMarker&&manager.inserts==2);
assert(site.Discover(99,0)); // An actual clue identifies even when normal exact chance is zero.
site.ERR_UpdateMarker();assert(site.Knowledge()==2&&mode.events==1);
assert(site.m_ERRMarker->label=="Secret radio station"&&site.m_ERRMarker->m_Icon=="radio");
mode.enabled=false;site.ERR_UpdateMarker();assert(site.m_ERRMarker&&site.m_ERRMarker->x==6123);
assert(!site.Discover(0,100)&&mode.events==1);
Location rough;assert(rough.Discover(99,0)&&rough.Knowledge()==1);rough.ERR_UpdateMarker();
assert(rough.m_ERRMarker->label=="Unknown location (500 m area)"&&rough.m_ERRMarker->x==6250);
mode.enabled=true;rough.ERR_UpdateMarker();assert(rough.Knowledge()==1&&rough.m_ERRMarker->x==6123&&rough.m_ERRMarker->m_Icon=="mark-question");
mode.enabled=false;rough.ERR_UpdateMarker();assert(rough.m_ERRMarker->x==6250);
mode.enabled=true;assert(rough.Discover(99,0)&&rough.Knowledge()==2);
Location pending;pending.m_Prepared=false;pending.ERR_UpdateMarker();assert(!pending.m_ERRMarker&&!pending.Discover(0,100));
pending.m_Prepared=true;pending.ERR_UpdateMarker();assert(pending.m_ERRMarker);
pending.m_Prepared=false;pending.ERR_UpdateMarker();assert(!pending.m_ERRMarker);
Location prison;prison.kind=ERR_LocationKind::PRISON;prison.ERR_UpdateMarker();assert(!prison.m_ERRMarker);
Location late;manager.ready=false;late.ERR_UpdateMarker();assert(!late.m_ERRMarker);
manager.ready=true;late.ERR_UpdateMarker();assert(late.m_ERRMarker&&late.m_ERRMarker->m_Icon=="mark-question");
Replication::server=false;assert(!late.Discover(0,100)&&late.Knowledge()==0);
for(int kind=ERR_LocationKind::RADIO;kind<=ERR_LocationKind::HOSPITAL;kind++){
Location other;other.kind=kind;other.ERR_UpdateMarker();assert(other.m_ERRMarker&&other.m_ERRMarker->label=="Unknown site"&&other.m_ERRMarker->m_Icon=="mark-question");
}
}
''')
# Persisted authority and runtime presentation wiring (including late-join replication).
settings=base/'ERR_Settings.c'
assert '"showAllSites"' in body(settings,'static array<string> Keys(')
assert 'if (key == "showAllSites") return 0;' in body(settings,'static int AddedDefault(')
assert 'showAllSites' not in body(settings,'static bool Randomizable(')
mode_source=(base/'ERR_GameMode.c').read_text()
assert '[RplProp()] protected bool m_ShowAllSites;' in mode_source
assert 'PublishSiteVisibility();' in body(base/'ERR_GameMode.c','protected override void EOnInit(')
publish=body(base/'ERR_GameMode.c','void PublishSiteVisibility(')
assert '!Replication.IsServer()' in publish and 'Replication.BumpMe()' in publish
admin=body(base/'ERR_AdminService.c','void Execute(')
assert admin.index('!authorized') < admin.index('mode.PublishSiteVisibility()')
assert admin.index('SettingCommand(mode.Settings(), key, parts, replies);') < admin.index('mode.PublishSiteVisibility()')
click=body(base/'ERR_AdminMenu.c','protected void OnButton(')
assert click.index('!m_Ready') < click.index('"intel sites set "')
print('PASS: hidden/anonymous/rough/identified markers, toggle transitions, prepared sites, no identity leakage, intel discovery and replicated authority wiring')