Add configurable anonymous site markers and record Workshop 117
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user