Add recon drones and body burial; stabilize spectator camera
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-21 15:28:32 +02:00
parent f41d8ba2f9
commit ac285b5c97
80 changed files with 13545 additions and 126 deletions
+3 -3
View File
@@ -55,7 +55,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a
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, 'Help_' + key))
if key not in ('sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
if key not in ('droneReconCount','droneCooldownSeconds','sarkaAngerPerCar','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
@@ -102,7 +102,7 @@ int main() {
assert(Fields::FollowJournal(false,0,200,400,false));
assert(Fields::FollowJournal(true,0,0,400,false));
std::vector<std::pair<string,string>> cases={
{"emptyRestartHours","mission emptyhours"},{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"droneReconCount","mission drones"},{"droneCooldownSeconds","mission dronecooldown"},{"emptyRestartHours","mission emptyhours"},{"aiDoctrine","mission doctrine"},{"aiShadow","mission aishadow"},{"automaticEquipmentChancePercent","hq equipment"},{"startMode","mission start"},{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
@@ -121,7 +121,7 @@ int main() {
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"aiDoctrine","aiShadow","planeAltitudeMetres","planeReplacementSeconds","planeSectorSeconds","objectGuardCount","heliTimerMinutes","reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
for(string key:{"droneReconCount","droneCooldownSeconds","aiDoctrine","aiShadow","planeAltitudeMetres","planeReplacementSeconds","planeSectorSeconds","objectGuardCount","heliTimerMinutes","reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
+95
View File
@@ -0,0 +1,95 @@
"""Execute burial eligibility/deletion with native boundaries substituted."""
import re
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_BuryBodyAction.c'
def cpp(s):
s=s.replace('override ', '').replace('string.Empty','string{}').replace('.IsEmpty()', '.empty()')
for typ in ('ChimeraCharacter','CharacterControllerComponent','ERR_GameMode'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for typ in ('ChimeraCharacter','ECharacterLifeState','ERR_GameMode','ERR_Actors','CompartmentAccessComponent','vector','Replication','SCR_EntityHelper','ERR_Burial'):
s=s.replace(typ+'.',typ+'::')
for var in ('character','controller','item','mode','user','body'):
s=s.replace(var+'.',var+'->')
s=s.replace('GetCharacterController().','GetCharacterController()->').replace('mode->Players().','mode->Players()->').replace('null','nullptr')
return s
methods='\n'.join(cpp(body(p,s)) for s in ('static bool Dead(', 'static bool ShovelHeld(', 'static bool Reach(', 'static string Rejection(', 'static bool Permitted(', 'static void Complete('))
run(r'''
#include <cassert>
#include <string>
#include <cmath>
#include <limits>
using string=std::string;
struct Entity;using IEntity=Entity*;
namespace ECharacterLifeState {constexpr int ALIVE=0,UNCONSCIOUS=1,DEAD=2;}
constexpr int SCR_CampaignBuildingGadgetToolComponent=1;
struct CharacterControllerComponent {
int life=0;IEntity gadget=nullptr,weapon=nullptr;
int GetLifeState(){return life;}IEntity GetAttachedGadgetAtLeftHandSlot(){return gadget;}IEntity GetCurrentItemInHands(){return weapon;}
};
struct vector {float x=0;static float Distance(vector a,vector b){return std::abs(a.x-b.x);}};
struct Entity {
bool character=true,player=false,shovel=false,aboard=false,deleted=false,controllerExists=true;
vector pos;CharacterControllerComponent controller;
static Entity* Cast(IEntity e){return e&&e->character&&!e->deleted?e:nullptr;}
CharacterControllerComponent* GetCharacterController(){return controllerExists?&controller:nullptr;}
void* FindComponent(int){return shovel?this:nullptr;}vector GetOrigin(){return pos;}
};
using ChimeraCharacter=Entity;
struct Roster {IEntity admitted=nullptr;bool CurrentActor(IEntity e){return e&&e==admitted&&!e->deleted;}};
struct ERR_GameMode {bool active=true,rosterExists=true;Roster roster;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Roster* Players(){return rosterExists?&roster:nullptr;}};
ERR_GameMode* ERR_GameMode::current=nullptr;
namespace ERR_Actors {bool Player(IEntity e){return e&&!e->deleted&&e->player;}bool CanFight(IEntity e){return e&&!e->deleted&&e->controller.life==ECharacterLifeState::ALIVE;}}
namespace CompartmentAccessComponent {bool GetVehicleIn(IEntity e){return e&&e->aboard;}}
namespace Replication {bool server=true;bool IsServer(){return server;}}
namespace SCR_EntityHelper {int deletions=0;void DeleteEntityAndChildren(IEntity e){assert(e&&!e->deleted);e->deleted=true;deletions++;}}
struct ERR_Burial {static bool clear;static bool Clear(IEntity,IEntity){return clear;}
''' + methods + r'''
};bool ERR_Burial::clear=true;
int main(){
Entity player,corpse,shovel,rifle;player.player=true;corpse.controller.life=2;shovel.shovel=true;shovel.character=false;rifle.character=false;
ERR_GameMode mode;ERR_GameMode::current=&mode;mode.roster.admitted=&player;
assert(ERR_Burial::Dead(&corpse)&&!ERR_Burial::Dead(nullptr)&&!ERR_Burial::Dead(&shovel));
assert(!ERR_Burial::ShovelHeld(nullptr)&&!ERR_Burial::ShovelHeld(&player));
player.controller.weapon=&rifle;assert(!ERR_Burial::ShovelHeld(&player));
player.controller.gadget=&shovel;assert(ERR_Burial::ShovelHeld(&player));
assert(ERR_Burial::Permitted(&player,&corpse));
for(int variation=0;variation<17;variation++){
if(variation==0)Replication::server=false;
if(variation==1)ERR_GameMode::current=nullptr;
if(variation==2)mode.active=false;
if(variation==3)mode.rosterExists=false;
if(variation==4)mode.roster.admitted=nullptr;
if(variation==5)player.player=false;
if(variation==6)player.controller.life=1;
if(variation==7)player.controller.life=2;
if(variation==8)corpse.controller.life=0;
if(variation==9)corpse.controller.life=1;
if(variation==10)player.controller.gadget=nullptr;
if(variation==11)player.aboard=true;
if(variation==12)corpse.aboard=true;
if(variation==13)corpse.pos.x=2.501;
if(variation==14)ERR_Burial::clear=false;
if(variation==15)corpse.character=false;
if(variation==16)player.controllerExists=false;
assert(!ERR_Burial::Permitted(&player,&corpse));ERR_Burial::Complete(&player,&corpse);assert(!corpse.deleted&&SCR_EntityHelper::deletions==0);
Replication::server=true;ERR_GameMode::current=&mode;mode.active=true;mode.rosterExists=true;mode.roster.admitted=&player;
player.player=true;player.controller.life=0;corpse.controller.life=2;player.controller.gadget=&shovel;
player.aboard=false;corpse.aboard=false;corpse.pos.x=0;ERR_Burial::clear=true;corpse.character=true;player.controllerExists=true;
}
assert(!ERR_Burial::Permitted(&player,nullptr)&&!ERR_Burial::Permitted(&player,&player));
assert(ERR_Burial::Reach(2.5)&&!ERR_Burial::Reach(-1)&&!ERR_Burial::Reach(std::numeric_limits<float>::quiet_NaN()));
corpse.pos.x=2.5;ERR_Burial::Complete(&player,&corpse);
assert(corpse.deleted&&!player.deleted&&!shovel.deleted&&SCR_EntityHelper::deletions==1);
ERR_Burial::Complete(&player,&corpse);assert(SCR_EntityHelper::deletions==1);
}
''')
# Verify native wiring is additive and that completion revalidates after delay.
s=p.read_text();action=body(p,'override void PerformAction(')
assert 'pOwnerEntity != GetOwner()' in action and 'ERR_Burial.Permitted(pUserEntity, pOwnerEntity)' in action
assert 'CallLater(ERR_Burial.Complete' in action
prefab=(ROOT/'addons/core/Prefabs/Characters/Core/Character_Base.et').read_text()
assert 'ERR_BuryBodyAction' in prefab and 'Duration 8' in prefab and '"Chest" "LowerTorso"' in prefab
assert 'SCR_BandageUserAction' not in prefab # minimal inherited addition, no replacement action list
assert 'override bool CanBroadcastScript() { return false; }' in s
assert 'override bool CheckOnServerFirstScript() { return true; }' in s
print('PASS: burial requires a conscious current player, actual held shovel, dead nearby unobstructed corpse and no vehicle; authority and duplicate deletion guards preserve player/tool')
+155
View File
@@ -0,0 +1,155 @@
"""Execute the production drone scheduling policy and validate integration boundaries."""
from policy_harness import ROOT, body, run
import re
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_DroneRecon.c'
schedule = body(path, 'class ERR_DroneSchedule').replace('class ERR_DroneSchedule', 'struct ERR_DroneSchedule') + ';'
run('#include <cassert>\n' + schedule + r'''
int main() {
ERR_DroneSchedule s{};
assert(!s.Due(0,0,0));
assert(s.Due(0,0,1));
assert(!s.Due(0,1,1));
assert(!s.Fresh(-1,0)); assert(!s.Fresh(1,0));
assert(s.Fresh(0,60000)); assert(!s.Fresh(0,60001));
s.Attempt(1000,2000,300);
assert(!s.Fresh(1000,3000)); assert(!s.Fresh(999,3000));
assert(s.Fresh(1001,3000));
assert(!s.Due(301999,0,1)); assert(s.Due(302000,0,1));
s.Result(false); s.Result(false); assert(s.Due(302000,0,1));
s.Result(false); assert(!s.Due(302000,0,1));
ERR_DroneSchedule limit{};
for(int i=0;i<64;i++) { limit.Launched++; limit.Result(true); }
assert(!limit.Due(9999999,0,3));
ERR_DroneSchedule stopped{}; stopped.Stopped=true;
assert(!stopped.Due(9999999,0,3));
}
''')
# Foreign types must remain inside the optional dynamically compiled adapter.
bridge = (path.parent / 'ERR_DroneBridge.c').read_text()
without_strings = re.sub(r'"(?:[^"\\]|\\.)*"', '""', bridge)
assert 'SAL_' not in without_strings
assert '65AD60E204191D37' not in (ROOT/'addons/core/addon.gproj').read_text()
assert 'name.EndsWith("/djibase.et")' in bridge
assert 'ERR_DroneState(e)!=1' in bridge
assert 'c.m_iOwner=old' in bridge
assert 'SetOrigin(' not in bridge and 'SetVelocity(' not in bridge
s = path.read_text()
assert 'report.DroneObservation' in s
assert 'm_Schedule.Fresh(report.ObservedAt, now)' in s
assert 'report.Position' in s and 'report.Target.GetOrigin' not in s
assert 'TraceFlags.ENTS | TraceFlags.WORLD' in s
assert 'CameraCone(Drone.GetOrigin(), Search, point)' in s
assert 'QueueDroneObservation(Group, report, now)' in s
assert 'GetCallqueue().Remove(Advance)' in s
hq = (path.parent/'ERR_HQ.c').read_text()
queue = body(path.parent/'ERR_HQ.c',' bool QueueDroneObservation')
assert 'ObservationRoute(unit)' in queue and 'unit.PendingReport = report.Snapshot()' in queue
assert 'Accept(' not in queue and 'm_Memory' not in queue
assert 'm_DroneRecon.Stop()' in hq
print('PASS: drone cooldown, capacity, freshness, replay, bounded retries, round cap, stop, optional API and radio boundaries')
rules = '\n'.join(body(path, signature) for signature in ('static vector Controls(', 'static bool CameraCone('))
rules = rules.replace('Math.', 'Math::').replace('vector.', 'vector::')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
struct vector {
float x=0,y=0,z=0;
float operator[](int i)const{return i==0?x:(i==1?y:z);}
vector operator-(vector v)const{return{x-v.x,y-v.y,z-v.z};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();if(n==0)return{};return{x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
};
vector Vector(float x,float y,float z){return{x,y,z};}
namespace Math {float Clamp(float x,float a,float b){return std::clamp(x,a,b);}}
''' + rules + r'''
int main() {
assert(CameraCone({0,70,0},{0,0,0},{0,0,0}));
assert(CameraCone({0,70,0},{0,0,0},{30,0,0}));
assert(!CameraCone({0,70,0},{0,0,0},{50,0,0}));
assert(!CameraCone({0,70,0},{0,0,0},{0,80,0}));
assert(!CameraCone({0,400,0},{0,0,0},{0,0,0}));
assert(!CameraCone({0,1,0},{0,0,0},{0,0,0}));
auto hover=Controls({0,0,0},{0,0,0},{0,0,1},{1,0,0});
assert(hover.x==.49f && hover.y==0 && hover.z==0);
assert(Controls({0,5,0},{0,0,0},{0,0,1},{1,0,0}).x<hover.x);
assert(Controls({0,-5,0},{0,0,0},{0,0,1},{1,0,0}).x>hover.x);
assert(Controls({0,0,0},{0,0,8},{0,0,1},{1,0,0}).y<0);
assert(Controls({0,0,0},{8,0,0},{0,0,1},{1,0,0}).z<0);
for(float speed:{-1000.f,1000.f}) {
auto c=Controls({speed,speed,speed},{0,0,0},{0,0,1},{1,0,0});
assert(c.x>=.15f && c.x<=.85f && std::abs(c.y)<=.5f && std::abs(c.z)<=.5f);
}
}
''')
print('PASS: production camera cone/range rejects behind-camera and distant actors; control directions, damping and limits')
# Execute the actual enqueue method; the route boundary is substituted, so these
# checks prove rejection/snapshot semantics rather than native radio reception.
queue = queue.replace('foreach (ERR_HQUnit unit : m_Units)', 'for (ERR_HQUnit* unit : m_Units)')
for name in ('SCR_AIGroup', 'ERR_HQReport', 'ERR_HQRoute'):
queue = re.sub(r'\b'+name+r' (?=\w)', name+'* ', queue)
for name in ('unit','report','route'):
queue = queue.replace(name+'.', name+'->')
for name in ('Replication','ERR_Actors','ERR_Phase'):
queue = queue.replace(name+'.', name+'::')
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct SCR_AIGroup {};
struct Actor { bool conscious=true; };
namespace Replication { bool server=true; bool IsServer(){return server;} }
namespace ERR_Actors {bool CanFight(Actor* a){return a&&a->conscious;}}
namespace ERR_Phase {constexpr int RADIO_SEARCH=2;}
struct ERR_HQReport {
bool DroneObservation=true;int MessageId=0,Position=19;SCR_AIGroup* Source=nullptr;
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}
std::string Description(float){return "camera";}
};
struct ERR_HQRoute {int MessageId=0;std::string Payload;};
struct ERR_HQUnit {SCR_AIGroup* Group=nullptr;Actor* Radio=nullptr;ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;};
struct HQ {
struct Mode {int phase=2;int Phase(){return phase;}} m_Mode;
struct Journal {int messages=0,writes=0;int Message(){return ++messages;}void Write(std::string,std::string,int){writes++;}} m_Journal;
std::vector<ERR_HQUnit*> m_Units;ERR_HQRoute* available=nullptr;int routeCalls=0;
ERR_HQRoute* ObservationRoute(ERR_HQUnit*){routeCalls++;return available;}
''' + queue + r'''
};
int main(){
HQ h;SCR_AIGroup group,other;Actor radio;ERR_HQUnit unit;ERR_HQReport report;ERR_HQRoute route;
unit.Group=&group;unit.Radio=&radio;h.m_Units.push_back(&unit);h.available=&route;
Replication::server=false;assert(!h.QueueDroneObservation(&group,&report,1000));Replication::server=true;
h.m_Mode.phase=1;assert(!h.QueueDroneObservation(&group,&report,1000));h.m_Mode.phase=2;
assert(!h.QueueDroneObservation(nullptr,&report,1000));assert(!h.QueueDroneObservation(&group,nullptr,1000));
report.DroneObservation=false;assert(!h.QueueDroneObservation(&group,&report,1000));report.DroneObservation=true;
assert(!h.QueueDroneObservation(&other,&report,1000));
radio.conscious=false;assert(!h.QueueDroneObservation(&group,&report,1000));radio.conscious=true;
unit.Radio=nullptr;assert(!h.QueueDroneObservation(&group,&report,1000));unit.Radio=&radio;
unit.Uplink=&route;assert(!h.QueueDroneObservation(&group,&report,1000));unit.Uplink=nullptr;
assert(h.routeCalls==0&&h.m_Journal.messages==0);
h.available=nullptr;assert(!h.QueueDroneObservation(&group,&report,1000));assert(!unit.PendingReport&&!unit.Uplink);
h.available=&route;assert(h.QueueDroneObservation(&group,&report,1000));
assert(unit.PendingReport!=&report&&unit.PendingReport->DroneObservation&&unit.PendingReport->Source==&group);
assert(unit.Uplink==&route&&route.MessageId==report.MessageId&&h.m_Journal.writes==1);
report.Position=77;assert(unit.PendingReport->Position==19);
assert(!h.QueueDroneObservation(&group,&report,2000));assert(h.m_Journal.messages==1);
delete unit.PendingReport;
}
''')
print('PASS: production drone radio queue rejects wrong phase/authority/group, busy or unconscious radios and absent routes; snapshots only eligible observations')
# Native traces can hit the operator's helmet instead of the character root.
hit = body(path, 'static bool HitsTarget(').replace('IEntity ', 'Entity* ').replace('null', 'nullptr').replace('hit.GetParent()', 'hit->GetParent()')
run('#include <cassert>\nstruct Entity {Entity* parent=nullptr;Entity* GetParent(){return parent;}};\n' + hit + r'''
int main(){
Entity actor,helmet{&actor},attachment{&helmet},wall,other,foreignHelmet{&other};
assert(HitsTarget(&actor,&actor)&&HitsTarget(&helmet,&actor)&&HitsTarget(&attachment,&actor));
assert(!HitsTarget(&wall,&actor)&&!HitsTarget(&foreignHelmet,&actor));
assert(!HitsTarget(nullptr,&actor)&&!HitsTarget(&wall,nullptr));
Entity cycle;cycle.parent=&cycle;assert(!HitsTarget(&cycle,&actor));
}
''')
print('PASS: target equipment counts as character contact; unrelated blockers and cyclic hierarchies stay blocked')
+8
View File
@@ -72,6 +72,14 @@ class ExternalAddonsTests(unittest.TestCase):
(self.addons/'original').rmdir();guid=ext.DIRECT[0]
(self.addons/('ERRI_'+guid)).symlink_to(self.source/ext.DIRECT[1],target_is_directory=True)
with self.assertRaisesRegex(ValueError,'Unexpected private'):ext.prepare_profile(self.addons,self.source,self.value,self.cache)
def test_workshop_preview_media_is_hashed_without_accepting_loose_code(self):
folder=self.source/ext.DIRECT[0]
(folder/'preview0_576x324.jpg').write_bytes(b'preview fixture')
(folder/'preview0_576x324.jpg_2.3.7_manifest.json').write_text('{}')
records=ext.prepare_profile(self.addons,self.source,self.value,self.cache)
self.assertTrue(ext.verify_profile(records,self.source,self.value,self.cache,self.addons))
(folder/'unreviewed.c').write_text('class Surprise {}')
with self.assertRaisesRegex(ValueError,'Unreviewed external packed input'):ext.installed(self.source,self.value)
def test_symlinked_runtime_file_fails(self):
pak=self.source/ext.DIRECT[0]/'data.pak';pak.unlink();pak.symlink_to(self.source/ext.DIRECT[1]/'data.pak')
with self.assertRaisesRegex(ValueError,'regular file'):ext.installed(self.source,self.value)
+1 -1
View File
@@ -83,7 +83,7 @@ struct ERR_HQPursuitRules{
};
'''+helper+ammo+mounted+r'''
struct ERR_HQReport{
string RoadStatus="road unknown"; string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
string RoadStatus="road unknown"; string TravelMode="body",CasualtySourceId="patrol-1";bool Distress=false,DroneObservation=false;int MessageId=99,CasualtySlot=0,CasualtyStrength=4;
vector Position{900,30,900},Velocity;float ObservedAt=1000,DetailsObservedAt=-1;
void* Target=nullptr;void* Source=nullptr;bool HasVelocity=false,Engaged=false;
int SeenEscapists=-1;string SeenWeapons="unknown";
+7
View File
@@ -12,3 +12,10 @@ with tempfile.TemporaryDirectory(prefix='erri-observer-selector-') as folder:
assert 'unconscious-camera requires --project core' in result.stderr,result.stderr
assert not list(root.iterdir()),'Invalid selection must not create native runtime/profile files'
print('PASS: wrong observer fixture selector rejected before runtime/profile setup; no engine launched')
# This camera fixture requires no historical site coordinates. Use the current
# generated scenario so the two-aircraft landing survey can find enough space.
scenario=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c').read_text()
for variable in ('historicalFixture','fixture'):
assert variable+' != "unconscious-camera"' in scenario
print('PASS: native camera fixture uses current generated sites instead of the obsolete fixed extraction pickup')
+72 -64
View File
@@ -1,82 +1,90 @@
"""Execute the production pose conversion against independent body/camera timing.
"""Production reader regression: proxy body corrections cannot move the camera.
This isolates frame-composition error; it does not measure live network jitter
or the engine's replicated weapon animation.
Native animation/weapon alignment and live network behavior remain mission tests.
"""
from policy_harness import ROOT, body, run
source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
methods = '\n'.join(body(source, signature) for signature in
('static void Encode(', 'static void Decode(')).replace('out vector', 'vector')
# Ensure both actual relay ends use the tested conversion, including scoped poses.
publisher = body(source, 'void ERR_PublishPlayerView(')
reader = body(source, 'bool ERR_ReadPlayerView(')
assert publisher.index('ERR_ObserverScope.Sample(') < publisher.index('ERR_ObserverViewRules.Encode(world, view, pose)')
assert 'ERR_ObserverViewRules.Decode(world, pose, transform)' in reader
assert 'MatrixInvMultiply4' not in publisher and 'MatrixMultiply4' not in reader
conversion = '\n'.join(body(source, signature) for signature in
('static void Encode(', 'static vector WorldPosition(')).replace('out vector', 'vector')
readers = '\n'.join(body(source, signature) for signature in
('bool ERR_ReadPlayerView(', 'bool ERR_ReadPlayerEye('))
for cls in ('ERR_Actors', 'ERR_ObserverScope', 'RplId'):
readers = readers.replace(cls+'.', cls+'::')
readers = (readers.replace('out vector', 'vector').replace('out float ', 'float& ')
.replace('GetGame().', 'GetGame()->').replace('GetWorld().', 'GetWorld()->')
.replace('Rpc(ERR_RequestPlayerView)', 'Request()'))
# Both reader bodies execute below. Buffer interpolation and receive/authority
# are executed separately by view_buffer and view_relay, not replaced by regex guards.
run(r'''
#include <cassert>
#include <cmath>
#include <iostream>
#include <algorithm>
struct vector {
double x=0,y=0,z=0;
float x=0,y=0,z=0;
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}
vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}
vector operator*(double n)const{return{x*n,y*n,z*n};}
vector operator*(float n)const{return{x*n,y*n,z*n};}
};
bool close(vector a,vector b){return std::abs(a.x-b.x)<.002&&std::abs(a.y-b.y)<.002&&std::abs(a.z-b.z)<.002;}
void yaw(float a,vector origin,vector* m){m[0]={std::cos(a),0,-std::sin(a)};m[1]={0,1,0};m[2]={std::sin(a),0,std::cos(a)};m[3]=origin;}
struct Rules {'''+conversion+r'''};
struct RplId{int id=-1;static RplId Invalid(){return{-1};}bool operator!=(RplId b){return id!=b.id;}};
struct Actor{bool canFight=true;vector root;};using IEntity=Actor*;
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->canFight;}};
struct ERR_ObserverScope{static inline bool resolved=true;static bool Resolve(IEntity,RplId,vector){return resolved;}};
struct Game{void* local=nullptr;float now=100;void* GetPlayerController(){return local;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}}game;
Game* GetGame(){return &game;}
struct Buffer{
vector position{15000.125,301.5,-15999.75};float play=123,aligned=-1;int reads=0;bool fresh=true;RplId weapon;
bool Read(RplId,float,float,vector* out,float& fov,float& nearPlane){reads++;if(!fresh)return false;yaw(.7,position,out);fov=60;nearPlane=.02;return true;}
float Playback(){return play;}void Align(float time,float){aligned=time;}
void Scope(RplId& id,vector& data){id=weapon;data={};}
void PIP(float& fov,vector& geometry,float& vignette){fov=6;geometry={0,0,1};vignette=.5;}
};
struct Controller{
bool m_ERRHasPIP=true;float m_ERRViewRequestAt=0;RplId m_ERRObservedId{1};int requests=0;
Buffer m_ERRViewBuffer,m_ERREyeBuffer;
void Request(){requests++;}
'''+readers+r'''
};
double dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
double length(vector a){return std::sqrt(dot(a,a));}
bool close(vector a,vector b){return length(a-b)<1e-8;}
vector rotate(vector* m,vector v){return m[0]*v.x+m[1]*v.y+m[2]*v.z;}
vector inverse(vector* m,vector v){return{dot(m[0],v),dot(m[1],v),dot(m[2],v)};}
void yaw(double angle,vector origin,vector* m){
m[0]={std::cos(angle),0,-std::sin(angle)};m[1]={0,1,0};
m[2]={std::sin(angle),0,std::cos(angle)};m[3]=origin;
}
struct Rules {'''+methods+r'''};
int main(){
vector owner[4],proxy[4],view[4],pose[4],result[4];
// Rotate proxy independently through a whole turn, with lean/roll and eye
// offsets. The receiver follows root translation but cannot inject root yaw.
for(int a=-180;a<=180;a+=15)for(int b=-180;b<=180;b+=15){
yaw(a*.0174532925199433,{15000,300,16000},owner);
yaw(b*.0174532925199433,{15002,301,16003},proxy);
yaw(.7,{15000.3,301.6,16000.2},view);
// Encoding preserves sampled world axes/roll. Reconstruction uses the origin
// captured with that pose, including far and negative terrain coordinates.
vector owner[4],view[4],pose[4];
for(int a=-180;a<=180;a+=15){
yaw(a*.0174532925,{15000,300,-16000},owner);
yaw(.7,{15000.125,301.5,-15999.75},view);
auto right=view[0],up=view[1];view[0]=right*.8+up*.6;view[1]=up*.8-right*.6;
Rules::Encode(owner,view,pose);Rules::Decode(proxy,pose,result);
for(int axis=0;axis<3;axis++)assert(close(result[axis],view[axis]));
assert(close(result[3]-proxy[3],view[3]-owner[3]));
Rules::Decode(owner,pose,result);for(int axis=0;axis<4;axis++)assert(close(result[axis],view[axis]));
Rules::Encode(owner,view,pose);
for(int axis=0;axis<3;axis++)assert(close(pose[axis],view[axis]));
assert(close(Rules::WorldPosition(owner[3],pose[3]),view[3]));
}
// Reproduce the reported contrast: a turning root has phase error between
// streams; a strafing root stays fixed even as camera aim continues turning.
for(bool strafing : {false,true}){
double oldMaxAngle=0,oldMaxEye=0,newMaxAngle=0,newMaxEye=0;
for(int frame=0;frame<600;frame++){
double t=frame/60.0,delay=.15;
double rootYaw=strafing?0:std::sin(t*2)*.6;
double proxyYaw=strafing?0:std::sin((t+delay)*2)*.6;
vector origin{t*2,0,0},remoteOrigin{(t+delay)*2,0,0};
yaw(rootYaw,origin,owner);yaw(proxyYaw,remoteOrigin,proxy);
yaw(t*.25,origin+rotate(owner,{.15,1.6,.2}),view);
// Previous encoding and reconstruction: inverse owner then current proxy.
vector oldForward=rotate(proxy,inverse(owner,view[2]));
vector oldOffset=rotate(proxy,inverse(owner,view[3]-owner[3]));
Rules::Encode(owner,view,pose);Rules::Decode(proxy,pose,result);
auto angle=[](vector a,vector b){return std::acos(std::clamp(dot(a,b),-1.0,1.0));};
oldMaxAngle=std::max(oldMaxAngle,angle(oldForward,view[2]));
oldMaxEye=std::max(oldMaxEye,length(oldOffset-(view[3]-owner[3])));
newMaxAngle=std::max(newMaxAngle,angle(result[2],view[2]));
newMaxEye=std::max(newMaxEye,length((result[3]-proxy[3])-(view[3]-owner[3])));
}
assert(newMaxAngle<1e-7&&newMaxEye<1e-8);
if(strafing)assert(oldMaxAngle<1e-7&&oldMaxEye<1e-8);
else assert(oldMaxAngle>.15&&oldMaxEye>.04);
std::cout<<"strafing="<<strafing<<" old_frame_error_rad="<<oldMaxAngle<<" corrected="<<newMaxAngle
<<" old_eye_error_m="<<oldMaxEye<<" corrected="<<newMaxEye<<"\n";
Controller c;Actor proxy;game.local=&c;vector out[4],eye[4],geometry;float fov,near,pip,vignette;
c.m_ERREyeBuffer.position={15000,301.625,-16000};
// A jittering proxy root used to move the camera by exactly its correction.
// Actual production readers must leave both sampled positions unchanged.
for(int frame=0;frame<300;frame++){
proxy.root={15000+((frame%2)? .25f:-.25f),300,-16000+frame*.02f};
assert(c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
assert(close(out[3],c.m_ERRViewBuffer.position)&&fov==60&&near==.02f);
assert(c.ERR_ReadPlayerEye(&proxy,.016,eye,fov,near,pip,geometry,vignette));
assert(close(eye[3],c.m_ERREyeBuffer.position)&&pip==6&&vignette==.5);
assert(c.m_ERREyeBuffer.aligned==c.m_ERRViewBuffer.Playback());
}
// Scope resolution may refuse a frame without exposing a proxy head estimate.
c.m_ERRViewBuffer.weapon={22};ERR_ObserverScope::resolved=false;
assert(!c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
ERR_ObserverScope::resolved=true;assert(c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
assert(close(out[3],c.m_ERRViewBuffer.position));
c.m_ERRViewBuffer.fresh=false;assert(!c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
c.m_ERRViewBuffer.fresh=true;c.m_ERREyeBuffer.fresh=false;
assert(!c.ERR_ReadPlayerEye(&proxy,.016,eye,fov,near,pip,geometry,vignette));
c.m_ERREyeBuffer.fresh=true;c.m_ERRHasPIP=false;
assert(!c.ERR_ReadPlayerEye(&proxy,.016,eye,fov,near,pip,geometry,vignette));
proxy.canFight=false;assert(!c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
proxy.canFight=true;game.local=nullptr;assert(!c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));
game.local=&c;game.now=600;assert(c.ERR_ReadPlayerView(&proxy,.016,out,fov,near));assert(c.requests==1);
assert(!c.ERR_ReadPlayerView(nullptr,.016,out,fov,near));
}
''')
print('PASS: actual pose conversion removes independently replicated root rotation from aim/eye offset, preserves roll and follows body translation')
print('PASS: production first-person and PiP readers ignore independently corrected proxy position, retain captured world poses, align playback and preserve scope/stale/ownership gates')
+24 -1
View File
@@ -9,7 +9,7 @@ def adapt(s):
s=re.sub(r'ERR_ObserverViewSample (\w+)', r'ERR_ObserverViewSample* \1',s)
s=re.sub(r'(sample|start|end)\.', r'\1->',s)
return re.sub(r'(m_Samples\[\d+\])\.',r'\1->',s)
rules='\n'.join(adapt(body(p,k)) for k in ['static bool Sample(', 'static bool Fresh('])
rules='\n'.join(adapt(body(p,k)) for k in ['static bool Sample(', 'static bool Fresh(', 'static bool Origin(', 'static bool WorldSample(', 'static vector WorldPosition('])
methods='\n'.join(adapt(body(p,k)) for k in ['void Clear(', 'void Push(', 'bool Read(', 'float Playback(', 'void Align('])
baseline=ROOT/'docs/evidence/observer-smoothing-2026-10-09/baseline/addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
old_methods='\n'.join(adapt(body(baseline,k)) for k in ['void Clear(', 'void Push(', 'bool Read('])
@@ -75,6 +75,29 @@ int main(){
assert(!ERR_ObserverScope::Valid(22,{1,91,0}));
assert(!ERR_ObserverScope::Valid(22,{1,0,2}));
assert(!ERR_ObserverViewRules::Fresh(100,101));
// The buffer now stores complete world positions, including distant/negative maps.
assert(ERR_ObserverViewRules::WorldSample({15000,300,-16000},forward,up,60,.02));
assert(!ERR_ObserverViewRules::WorldSample({nan,0,0},forward,up,60,.02));
assert(!ERR_ObserverViewRules::WorldSample({0,std::numeric_limits<float>::infinity(),0},forward,up,60,.02));
Buffer world;
vector capturedRoot{15000,300,-16000},offset{.125,1.5,.25};
vector worldEye=ERR_ObserverViewRules::WorldPosition(capturedRoot,offset);
world.Push(1,100,100,worldEye,forward,up,60,.02,-1,{});
// Independently corrected proxy position cannot affect the buffered world pose.
for(int i=0;i<60;i++){
assert(world.Read(1,100+i,1.f/60,matrix,fov,nearPlane));
assert(close(matrix[3].x,15000.125)&&close(matrix[3].y,301.5)&&close(matrix[3].z,-15999.75));
}
// Walking advances sampled origins and offsets together on the same timeline.
world.Push(1,150,150,ERR_ObserverViewRules::WorldPosition({15002,300,-16000},offset),forward,up,60,.02,-1,{});
assert(world.Read(1,225,.016,matrix,fov,nearPlane)&&close(matrix[3].x,15001.125));
world.Push(1,160,160,{nan,0,0},forward,up,60,.02,-1,{});assert(world.m_Stamp==150);
// Expiry/reacquisition and target changes snap to the new full position.
assert(!world.Read(1,501,.016,matrix,fov,nearPlane));
world.Push(1,600,600,{-12000,50,9000},forward,up,60,.02,-1,{});
assert(world.Read(1,600,.016,matrix,fov,nearPlane)&&close(matrix[3].x,-12000));
world.Push(2,650,650,{24000,50,9000},forward,up,60,.02,-1,{});
assert(world.Read(2,650,.016,matrix,fov,nearPlane)&&close(matrix[3].x,24000));
// Real interpolation bodies, same constant-speed poses, uneven packet arrivals.
// Different client/server clock origins, dropped packets and varied render FPS.
for(float frame:{1000.f/30,1000.f/60,1000.f/144}){
+29 -8
View File
@@ -10,7 +10,7 @@ def adapt(s):
s=s.replace('m_ERRViewWatchers[i].','m_ERRViewWatchers[i]->')
for cls in ['ERR_ObserverScope','ERR_GameMode','ERR_Actors','ERR_ObserverRules','ERR_ObserverViewRules','Replication','SCR_PlayerController','RplComponent','SCR_FactionManager','RplId','Math','vector']:s=s.replace(cls+'.',cls+'::')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('Players().','Players()->').replace('Get().','Get()->')
s=s.replace('actor.FindComponent(RplComponent)', 'actor->FindComponent()')
s=s.replace('actor.FindComponent(RplComponent)', 'actor->FindComponent()').replace('actor.GetOrigin()', 'actor->GetOrigin()')
s=s.replace('Rpc(ERR_EnableViewPublisher)', 'SendLease()')
return s
methods='\n'.join(adapt(body(p,k)) for k in ['bool ERR_ViewRecipient(', 'protected void ERR_RequestPlayerView(', 'void ERR_AddViewWatcher(', 'protected void ERR_EnableViewPublisher(', 'protected void ERR_SubmitPlayerView(', 'protected void ERR_ReceivePlayerView('])
@@ -20,12 +20,13 @@ run(r'''
#include <map>
#include <algorithm>
#include <cmath>
struct vector {float operator[](int i){return i==0?x:(i==1?y:z);}bool operator==(vector b){return x==b.x&&y==b.y&&z==b.z;}static vector Zero;float x=0,y=0,z=0;float LengthSq(){return x*x+y*y+z*z;}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
#include <limits>
struct vector {vector operator+(vector b){return{x+b.x,y+b.y,z+b.z};}vector operator-(vector b){return{x-b.x,y-b.y,z-b.z};}float operator[](int i){return i==0?x:(i==1?y:z);}bool operator==(vector b){return x==b.x&&y==b.y&&z==b.z;}static vector Zero;float x=0,y=0,z=0;float LengthSq(){return x*x+y*y+z*z;}static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}};
vector vector::Zero;
namespace Math{float Round(float f){return std::round(f);}float AbsFloat(float a){return fabs(a);}}
struct RplId{int value=-1;RplId(int i=-1):value(i){}static RplId Invalid(){return{-1};}bool operator==(RplId b){return value==b.value;}bool operator!=(RplId b){return value!=b.value;}};
struct RplComponent{RplId id{11};RplId Id(){return id;}static RplComponent* Cast(RplComponent* p){return p;}};
struct Actor {bool alive=true,conscious=true;int faction=1;RplComponent replication;RplComponent* FindComponent(){return &replication;}};using IEntity=Actor*;
struct Actor {vector origin;vector GetOrigin(){return origin;}bool alive=true,conscious=true;int faction=1;RplComponent replication;RplComponent* FindComponent(){return &replication;}};using IEntity=Actor*;
struct ERR_PlayerLife {int ConnectionId=1;bool valid=true;};
struct Roster {std::map<IEntity,ERR_PlayerLife*> lives;ERR_PlayerLife* CurrentActor(IEntity a){auto p=lives.find(a);return p!=lives.end()&&p->second->valid?p->second:nullptr;}};
struct ERR_GameMode {bool active=true;Roster roster;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Roster* Players(){return &roster;}int PlayerFaction(){return 1;}};ERR_GameMode* ERR_GameMode::current;
@@ -34,7 +35,7 @@ namespace SCR_FactionManager{int SGetFaction(IEntity a){return a?a->faction:0;}}
namespace ERR_ObserverReason{int NONE=0,UNCONSCIOUS=4;}
struct ERR_ObserverRules {'''+body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c','static bool View(').replace('ERR_ObserverReason.','ERR_ObserverReason::')+r'''};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}void Remove(int i){this->erase(this->begin()+i);}};
struct ERR_ObserverViewRules {'''+adapt(body(p,'static bool Sample('))+adapt(body(p,'static bool Packet('))+r'''};
struct ERR_ObserverViewRules {'''+''.join(adapt(body(p,k)) for k in ('static bool Sample(', 'static bool Origin(', 'static bool NearBody(', 'static vector WorldPosition(', 'static bool Packet('))+r'''};
struct ERR_ObserverScope {'''+adapt(body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c','static bool Valid('))+r'''};
namespace Replication{bool server=true;bool IsServer(){return server;}}
struct SCR_PlayerController;
@@ -42,7 +43,7 @@ struct Game {float now=1000;SCR_PlayerController* local=nullptr;std::map<int,SCR
Game* GetWorld(){return this;}Game* GetPlayerManager(){return this;}float GetWorldTime(){return now;}SCR_PlayerController* GetPlayerController(){return local;}SCR_PlayerController* GetPlayerController(int id){return controllers[id];}} game;
Game* GetGame(){return &game;}
struct Buffer{float pipFov=0;int pushes=0,stamp=-1;RplId target;bool Accepts(RplId t,int s){return t!=target||s>stamp;}void Clear(){stamp=-1;target=-1;}void Push(RplId t,int s,float,vector,vector,vector,float,float,RplId,vector,float pf=0,vector g={},float v=0){pushes++;stamp=s;target=t;pipFov=pf;}};
struct Buffer{vector position;float pipFov=0;int pushes=0,stamp=-1;RplId target;bool Accepts(RplId t,int s){return t!=target||s>stamp;}void Clear(){stamp=-1;target=-1;}void Push(RplId t,int s,float,vector p,vector,vector,float,float,RplId,vector,float pf=0,vector g={},float v=0){position=p;pushes++;stamp=s;target=t;pipFov=pf;}};
struct SCR_PlayerController {
int id=0;IEntity controlled=nullptr,m_ERRServerObserved=nullptr,m_ERRViewSourceBody=nullptr;RplId m_ERRObservedId;
int m_ERRObserverReason=1,m_ERRViewInputSequence=0,leases=0,deliveries=0;float m_ERRViewLeaseUntil=-1,m_ERRViewServerRequestAt=-1000,m_ERRViewLeaseSent=-1000,m_ERRViewPublishUntil=-1,m_ERRViewLastPacket=-1000;
@@ -51,8 +52,8 @@ struct SCR_PlayerController {
void SendLease(){leases++;}
void ERR_DeliverPlayerView(RplId,int,Array<vector>*,RplId,vector){deliveries++;}
// Legacy scenario call shape only constructs the new on-wire packet.
void ERR_SubmitPlayerView(int seq,vector p,vector f,vector u,float fov,float near,RplId weapon,vector data){Array<vector> a;a.Insert(p);a.Insert(f);a.Insert(u);a.Insert({fov,near,0});ERR_SubmitPlayerView(seq,&a,weapon,data);}
void ERR_ReceivePlayerView(RplId target,int stamp,vector p,vector f,vector u,float fov,float near,RplId weapon,vector data){Array<vector> a;a.Insert(p);a.Insert(f);a.Insert(u);a.Insert({fov,near,0});ERR_ReceivePlayerView(target,stamp,&a,weapon,data);}
void ERR_SubmitPlayerView(int seq,vector p,vector f,vector u,float fov,float near,RplId weapon,vector data){Array<vector> a;a.Insert(p);a.Insert(f);a.Insert(u);a.Insert({fov,near,0});a.Insert({});ERR_SubmitPlayerView(seq,&a,weapon,data);}
void ERR_ReceivePlayerView(RplId target,int stamp,vector p,vector f,vector u,float fov,float near,RplId weapon,vector data){Array<vector> a;a.Insert(p);a.Insert(f);a.Insert(u);a.Insert({fov,near,0});a.Insert({});ERR_ReceivePlayerView(target,stamp,&a,weapon,data);}
'''+methods+r'''
};
int main(){
@@ -84,13 +85,33 @@ int main(){
game.local=&stranger;watcher.ERR_ReceivePlayerView({11},3,position,forward,up,60,.0125, -1, {});assert(watcher.m_ERRViewBuffer.pushes==1);
game.local=&watcher;mode.active=false;watcher.ERR_ReceivePlayerView({11},4,position,forward,up,60,.0125, -1, {});assert(watcher.m_ERRViewBuffer.pushes==1);
mode.active=true;game.local=&watcher;
Array<vector> dual;for(vector v:{position,forward,up,vector{12,.02,6},position,forward,up,vector{60,.02,0},vector{0,0,1}})dual.Insert(v);
Array<vector> dual;for(vector v:{position,forward,up,vector{12,.02,6},position,forward,up,vector{60,.02,0},vector{0,0,1},vector{15000,300,-16000}})dual.Insert(v);
watcher.ERR_ReceivePlayerView(11,20,&dual,22,{1,0,0});assert(watcher.m_ERRHasPIP&&watcher.m_ERREyeBuffer.stamp==20&&watcher.m_ERRViewBuffer.stamp==20&&watcher.m_ERRViewBuffer.pipFov==6);
assert(watcher.m_ERRViewBuffer.position==vector({15000,301.6,-16000}));
assert(watcher.m_ERREyeBuffer.position==vector({15000,301.6,-16000}));
dual[3].z=4;watcher.ERR_ReceivePlayerView(11,19,&dual,22,{1,0,0});assert(watcher.m_ERRViewBuffer.pipFov==6&&watcher.m_ERREyeBuffer.stamp==20);
dual[4]={999,0,0};watcher.ERR_ReceivePlayerView(11,21,&dual,22,{1,0,0});assert(watcher.m_ERRViewBuffer.stamp==20);dual[4]=position;
watcher.ERR_ReceivePlayerView(11,21,&dual,-1,{});assert(watcher.m_ERRViewBuffer.stamp==20); // PiP requires a scoped weapon
watcher.ERR_ReceivePlayerView(11,22,position,forward,up,60,.02,-1,{});assert(!watcher.m_ERRHasPIP&&watcher.m_ERREyeBuffer.stamp==-1&&watcher.m_ERRViewBuffer.stamp==22);
// Nonfinite origins and malformed/old packet shapes cannot advance either buffer.
dual[9]={std::numeric_limits<float>::quiet_NaN(),0,0};
watcher.ERR_ReceivePlayerView(11,23,&dual,22,{1,0,0});assert(watcher.m_ERRViewBuffer.stamp==22);
dual[9]={0,std::numeric_limits<float>::infinity(),0};assert(!ERR_ObserverViewRules::Packet(&dual));
dual[9]={0,0,10000001};assert(!ERR_ObserverViewRules::Packet(&dual));
dual[9]={0,10000000,0};assert(!ERR_ObserverViewRules::Packet(&dual)); // reconstructed eye must remain bounded too
dual.pop_back();assert(!ERR_ObserverViewRules::Packet(&dual));assert(!ERR_ObserverViewRules::Packet(nullptr));
// Authority compares the captured origin to the canonical body's position,
// without replacing that origin with the server's independently timed pose.
game.now+=500;watcher.ERR_RequestPlayerView();
Array<vector> ordinary;for(vector v:{position,forward,up,vector{60,.02,0},vector{15000,300,-16000}})ordinary.Insert(v);
target.origin={15000,300,-16000};int delivered=watcher.deliveries;
source.ERR_SubmitPlayerView(100,&ordinary,-1,{});assert(watcher.deliveries==delivered+1);
game.now+=50;ordinary[4]={15100,300,-16000};source.ERR_SubmitPlayerView(101,&ordinary,-1,{});assert(watcher.deliveries==delivered+2);
game.now+=50;ordinary[4]={15100.1,300,-16000};source.ERR_SubmitPlayerView(102,&ordinary,-1,{});
assert(watcher.deliveries==delivered+2&&source.m_ERRViewInputSequence==101);
ordinary[4]={std::numeric_limits<float>::quiet_NaN(),0,0};source.ERR_SubmitPlayerView(102,&ordinary,-1,{});assert(source.m_ERRViewInputSequence==101);
ordinary.pop_back();source.ERR_SubmitPlayerView(102,&ordinary,-1,{});assert(source.m_ERRViewInputSequence==101);
assert(!ERR_ObserverViewRules::Packet(&ordinary)); // legacy four-vector packet
mode.active=true;game.local=&source;source.ERR_EnableViewPublisher();assert(source.m_ERRViewPublishUntil==game.now+2000);
}
''')
+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,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 result==dict(droneReconCount=0,droneCooldownSeconds=300,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})
+21
View File
@@ -99,6 +99,27 @@ class SourceBundleTests(unittest.TestCase):
self.edit(name, '58D0FB3206B6F859', '5F16D7E4A1CBE075')
with self.assertRaisesRegex(ValueError, 'identity/dependency'): bundle.build(self.root)
def test_character_action_patch_is_the_only_allowed_vanilla_override(self):
_, manifest = bundle.build(self.root)
patch = 'addons/core/Prefabs/Characters/Core/Character_Base.et'
self.assertIn(patch, manifest['files'])
# The exception cannot be moved under a different resource path.
changed = 'addons/core/Prefabs/Characters/Core/Unreviewed.et'
(self.root / patch).rename(self.root / changed)
(self.root / (patch + '.meta')).rename(self.root / (changed + '.meta'))
self.edit(changed + '.meta', 'Character_Base.et', 'Unreviewed.et')
with self.assertRaisesRegex(ValueError, 'reuses a vanilla identity'):
bundle.build(self.root)
# Nor can the allowlisted path take an unrelated vanilla resource GUID.
(self.root / changed).rename(self.root / patch)
(self.root / (changed + '.meta')).rename(self.root / (patch + '.meta'))
self.edit(patch + '.meta', 'Unreviewed.et', 'Character_Base.et')
vanilla = json.loads((self.root / 'docs/vanilla-resources.json').read_text())
other = next(g for g in vanilla.values() if g not in bundle.VANILLA_OVERRIDES)
self.edit(patch + '.meta', '37578B1666981FCE', other)
with self.assertRaisesRegex(ValueError, 'reuses a vanilla identity'):
bundle.build(self.root)
def test_escaped_audio_graph_references_are_checked(self):
name = 'addons/core/Scripts/Game/ERR/10_AI/ERR_SoldierSpeechAudio.c'
bundle.build(self.root)