diff --git a/addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et b/addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et index 9e5f589..de9ec8c 100644 --- a/addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et +++ b/addons/core/Prefabs/Items/Equipment/ERRM_AtlasLeaf.et @@ -1,4 +1,5 @@ GenericEntity { + ID "E83CA269A0D647B1" components { MeshObject { Object "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob" InheritVisibility 1 } } diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c index 9f06214..0c9bb01 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c @@ -48,7 +48,9 @@ class ERR_TerrainDisplayName seen.Insert(world); // Native subscene names can be plain paths. They are already the loaded // terrain, not GUID resources that LoadContainer can traverse safely. - if (!world.IsExternal() && !world.IsInternal()) break; + // IsExternal/IsInternal also accept ordinary paths in the native runtime. + // Inspect the serialized GUID prefix without invoking resource resolution. + if (world.Length() < 18 || world.Substring(0, 1) != "{" || world.Substring(17, 1) != "}") break; Resource resource = BaseContainerTools.LoadContainer(world); if (!resource || !resource.IsValid()) break; BaseContainer container = resource.GetResource().ToBaseContainer(); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c index 0f601b2..3b6afdd 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQExecution.c @@ -103,6 +103,7 @@ class ERR_HQSuppressActivity : SCR_AISuppressActivity } } +[BaseContainerProps()] modded class SCR_AIGoalReaction_Suppress { override void PerformReaction(notnull SCR_AIUtilityComponent utility, SCR_AIMessageBase message) diff --git a/tests/test_gameplay_session_regressions.py b/tests/test_gameplay_session_regressions.py index 5a67060..24a209c 100644 --- a/tests/test_gameplay_session_regressions.py +++ b/tests/test_gameplay_session_regressions.py @@ -68,6 +68,7 @@ for name in ('unit', 'utility'): movement = movement.replace(name+'.', name+'->') for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'): movement = movement.replace(name+'.', name+'->') +movement = movement.replace('Physics physics =', 'Physics* physics =').replace('physics.GetVelocity()', 'physics->GetVelocity()') movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)') movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event').replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active') movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement) @@ -85,6 +86,8 @@ struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utilit namespace ERR_HunterCombat {bool fresh=false;bool Active(Group*){return fresh;}}; struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}}; struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}}; +// Actual abandonment/exit behavior is covered by test_hunter_stuck.py. +int abandonments=0;void AbandonStuckVehicle(MovingHunter*){abandonments++;} Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;} template std::string Format(Args...){return "log";} void Event(std::string,std::string){} @@ -95,7 +98,9 @@ int main(){ ObserveMovement(&hunter,60000);assert(hunter.Waypoint==&delivered&&hunter.Repaths==0); ObserveMovement(&hunter,60001);assert(hunter.Repaths==1&&hunter.Waypoint->pos.x==100&&hunter.Waypoint->pos.z==200);assert(hunter.Destination.x==900); ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3); - ObserveMovement(&hunter,240001);assert(hunter.Repaths==3); + ObserveMovement(&hunter,240001);assert(hunter.Repaths==3&&abandonments==1); + // A separate still-owned patrol makes progress and resumes after contact. + hunter={&vehicle,&group,&delivered,{900,900}}; vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0); perception.m_aTargets.empty=false;ERR_HunterCombat::fresh=true;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0); ERR_HunterCombat::fresh=false;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0); diff --git a/tests/test_ground_depot_provenance.py b/tests/test_ground_depot_provenance.py index 4de2473..bd28ecf 100644 --- a/tests/test_ground_depot_provenance.py +++ b/tests/test_ground_depot_provenance.py @@ -28,7 +28,7 @@ for match in re.finditer(r' ERR_SiteDefinition \{',conf): else:assert not mines assert len(camps)==5 assert 'ERR_InfantryMineTransfer' not in S.read_text(), 'Core must not depend on the AI mine transfer layer' -source=S.read_text();start=source.index(' if (spec.Kind == ERR_LocationKind.OUTPOST');capture=source[start:source.index(' placement.Item = item;',start)] +capture=body(S, 'if (spec.Kind == ERR_LocationKind.OUTPOST && !spec.RevealOnEscape && stock.TargetId.IsEmpty()') access='\n'.join(body(S,k) for k in ('int DepotMagazineCount(', 'int DepotMineCount(', 'int DepotItemCount(', 'IEntity DepotItem(', 'void DepotStock(', 'IEntity DepotMagazine(', 'bool DepotOriginal(')) admit='\n'.join(body(I,k) for k in ('bool DepotCurrent(', 'bool DepotAvailable(', 'bool DepotNeeded(', 'bool AdmitDepot(', 'bool DepotMineAvailable(', 'bool DepotMineNeeded(', 'bool AdmitDepotMine(', 'protected bool CaptureMines(')) delivered=body(L,'bool DeliveredTo(') diff --git a/tests/test_hint_delivery.py b/tests/test_hint_delivery.py index ea4f090..8bde4e8 100644 --- a/tests/test_hint_delivery.py +++ b/tests/test_hint_delivery.py @@ -55,6 +55,7 @@ bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;} struct Controller { Controller(){game.local=this;} void ERR_UpdateRecoveryCamera(ERR_GameMode*){} + string epiHint;string ERR_SelfEpinephrineHint(){return epiHint;} int nativeChanges=0,starts=0;bool m_ERRGuideStarted=false;void ERR_StartGuide(){starts++;StartActual();} int m_ERRGuidePhase=-1;string m_ERRGuideMessage;bool m_ERRObserverMonitor=false;Actor* m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1; Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE; @@ -167,6 +168,10 @@ int main(){ mode.active=false;mode.recovery="old round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&!newCasualty.m_ERRObserverReady&&newCasualty.m_ERRShownRecoveryStatus.empty()); // A later active round's first observer hint contains only its current fact. mode.active=true;mode.recovery="new round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+2&&hints.last.find("new round recovery")!=string::npos&&hints.last.find("old round recovery")==string::npos); + // An admin-enabled self-epi hint refreshes once, and disappears when disabled. + newCasualty.epiHint="Press Space to use epinephrine";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+3&&hints.last.find(newCasualty.epiHint)!=string::npos); + newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+3); + newCasualty.epiHint="";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+4&&hints.last.find("epinephrine")==string::npos); } ''') for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')): diff --git a/tests/test_optional_transport.py b/tests/test_optional_transport.py index d97b232..26ddedf 100644 --- a/tests/test_optional_transport.py +++ b/tests/test_optional_transport.py @@ -43,7 +43,7 @@ run(r''' struct string:std::string {using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}}; templatestring Format(string s,A...){return s;} templatevoid PrintFormat(string,A...){} -namespace LogLevel{constexpr int ERROR=1;} +namespace LogLevel{constexpr int ERROR=1,WARNING=2;} void Print(string,int){} templatestruct Array:std::vector{using std::vector::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}void Insert(T t){this->push_back(t);}void Clear(){this->clear();}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Remove(int n){this->erase(this->begin()+n);}void RemoveItem(T t){this->erase(std::remove(this->begin(),this->end(),t),this->end());}}; struct vector {float a[3]{};float&operator[](int n){return a[n];}vector operator+(vector v){return {{a[0]+v.a[0],a[1]+v.a[1],a[2]+v.a[2]}};}}; @@ -73,7 +73,7 @@ struct Definition {ERR_SiteTransport Transport=nullptr;string Id="clue-site";vec struct LocationType{bool prepared=false;void SetPrepared(bool value){prepared=value;}}; struct Site {LocationType*Location=nullptr;struct Definition* Definition=nullptr;IEntity Transport=nullptr,TransportEntity=nullptr;bool TransportAttempted=false,TransportOmitted=false;float TransportDeadline=-1;Site* Responders=nullptr;int guard=PENDING;};using ERR_SiteRuntime=Site*; struct Inventory{Arrayitems;bool accept=true;int calls=0;void GetItems(Array&out){out=items;}bool TryInsertItem(IEntity){calls++;return accept;}}; -struct Placement{IEntity Item=nullptr;Inventory* Inventory_=nullptr;bool Accepted=false;}; +struct Placement{IEntity Item=nullptr;Inventory* Inventory_=nullptr;bool Accepted=false,Optional=false,Skipped=false;}; struct Extraction{ERR_PrepareResult result=READY;int stops=0;ERR_PrepareResult Poll(float){return result;}string FailureReason(){return "aircraft failed";}void Stop(){stops++;}}; struct ExtractionStatus{bool pickup=true;bool PickupConfigured(){return pickup;}}; struct Mode{ExtractionStatus status;ExtractionStatus&Extraction(){return status;}Arrayowned;void Own(IEntity e){owned.Insert(e);}}; diff --git a/tests/test_terrain_presentation.py b/tests/test_terrain_presentation.py index 29e8a95..9dddb0a 100644 --- a/tests/test_terrain_presentation.py +++ b/tests/test_terrain_presentation.py @@ -121,8 +121,8 @@ struct string:std::string { void Replace(string a,string b){size_t n=0;while((n=find(a,n))!=npos){replace(n,a.size(),b);n+=b.size();}} string Trim(){auto a=find_first_not_of(" \r\n");if(a==npos)return "";return substr(a,find_last_not_of(" \r\n")-a+1);} string GetPath(){auto n=find('}');return n==npos?*this:string(substr(n+1));} - bool IsExternal(){return size()>18&&front()=='{'&&at(17)=='}';} - bool IsInternal(){return size()==18&&front()=='{'&&at(17)=='}';} + bool IsExternal(){assert(false && "Do not resolve a raw terrain path");return true;} + bool IsInternal(){assert(false && "Do not resolve a raw terrain path");return true;} }; string StripPath(string s){auto n=s.find_last_of('/');return n==s.npos?s:string(s.substr(n+1));} templatestruct Array:std::vector{void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};