Files
EscapistsReintroduced/tests/test_observer_orbit.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

152 lines
8.9 KiB
Python

"""Execute the production orbit branch with native entity/input/trace doubles.
This proves the missing-target fallback responds to the same controls as a
corpse. It does not qualify physical mouse delivery or multiplayer streaming.
"""
import re
from policy_harness import ROOT, body, run
source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
frame = body(source, 'override void EOnCameraFrame(')
# The early live-camera return is covered by the relay tests. Execute the
# complete orbit branch below it, including target resolution and collision.
orbit = frame[frame.index(' vector aim = controller.ERR_ObserverAnchor()'):]
orbit = 'void Frame(Param& param) { IEntity target = controller->target;\n' + orbit
orbit = orbit.replace('IEntity vehicle;', 'IEntity vehicle = nullptr;')
for name in ('controller', 'target', 'vehicle'):
orbit = orbit.replace(name + '.', name + '->')
for name in ('CompartmentAccessComponent', 'ERR_Actors', 'Math', 'Math3D', 'TraceFlags', 'vector'):
orbit = orbit.replace(name + '.', name + '::')
orbit = orbit.replace('GetInputManager().', 'GetInputManager()->')
orbit = orbit.replace('GetCameraEntity().', 'GetCameraEntity()->')
orbit = orbit.replace('GetWorld().', 'GetWorld()->')
orbit = orbit.replace('TraceParam trace = new TraceParam();', 'TraceParam trace;')
orbit = re.sub(r'\bnull\b', 'nullptr', orbit)
fields = []
for name in ('m_Yaw', 'm_Pitch', 'm_Distance'):
declaration = re.search(r'protected float ' + name + r'(?: = [^;]+)?;', source.read_text()).group()
declaration = declaration.replace('protected ', '')
if '=' not in declaration:
declaration = declaration.replace(';', ' = 0;')
fields.append(declaration)
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <cstdio>
struct vector {
float data[3]={};
vector()=default;
vector(float x,float y,float z):data{x,y,z}{}
vector(const char* s){std::sscanf(s,"%f %f %f",&data[0],&data[1],&data[2]);}
float& operator[](int i){return data[i];}
float operator[](int i)const{return data[i];}
vector operator+(vector b)const{return{data[0]+b[0],data[1]+b[1],data[2]+b[2]};}
vector operator-(vector b)const{return{data[0]-b[0],data[1]-b[1],data[2]-b[2]};}
vector operator*(float k)const{return{data[0]*k,data[1]*k,data[2]*k};}
vector VectorToAngles(){return{std::atan2(data[0],data[2])*180.f/3.141592653589793f,0,0};}
static vector Lerp(vector a,vector b,float t){return a+(b-a)*t;}
static vector Direction(vector a,vector b){vector v=b-a;float n=std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);return v*(1/n);}
};
vector Vector(float x,float y,float z){return{x,y,z};}
float distance(vector a,vector b){vector v=a-b;return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}
bool near(float a,float b){return std::abs(a-b)<0.0001f;}
bool near(vector a,vector b){return distance(a,b)<0.0001f;}
struct Math {
static constexpr float DEG2RAD=3.141592653589793f/180;
static float Clamp(float v,float a,float b){return std::clamp(v,a,b);}
static float Max(float a,float b){return std::max(a,b);}
static float Sin(float v){return std::sin(v);}
static float Cos(float v){return std::cos(v);}
};
struct Entity {
bool alive=true;Entity* vehicle=nullptr;
vector transform[4]={{1,0,0},{0,1,0},{0,0,1},{200,30,300}};
vector lower={190,28,290},upper={210,36,310};
void GetWorldTransform(vector* out){for(int i=0;i<4;i++)out[i]=transform[i];}
void GetWorldBounds(vector& lo,vector& hi){lo=lower;hi=upper;}
};
using IEntity=Entity*;
struct CompartmentAccessComponent {static IEntity GetVehicleIn(IEntity e){return e->vehicle;}};
struct ERR_Actors {static bool Alive(IEntity e){return e&&e->alive;}};
struct Controller {IEntity target=nullptr;vector anchor={100,20,300};vector ERR_ObserverAnchor(){return anchor;}} owner;
Controller* controller=&owner;
struct Inputs {
float yaw=0,pitch=0,zoom=0;
float GetActionValue(const char* name){if(!std::strcmp(name,"ManualCameraRotateYaw"))return yaw;if(!std::strcmp(name,"ManualCameraRotatePitch"))return pitch;assert(!std::strcmp(name,"ManualCameraZoom"));return zoom;}
} inputs;
Inputs* GetInputManager(){return &inputs;}
struct TraceParam {vector Start,End;int Flags=0,LayerMask=0;IEntity Exclude=nullptr;};
namespace TraceFlags {enum{WORLD=1,ENTS=2};}constexpr int TRACE_LAYER_CAMERA=42;
struct World {float fraction=1;TraceParam last;float TraceMove(TraceParam trace,void*){last=trace;return fraction;}} world;
struct Camera {World* GetWorld(){return &world;}} camera;
Camera* GetCameraEntity(){return &camera;}
struct Math3D {static void DirectionAndUpMatrix(vector direction,vector up,vector* out){out[1]=up;out[2]=direction;}};
struct Param {bool isManualInputEnabled=true,isDirty=false;float timeSlice=.1;vector transform[4];};
struct Follow {
IEntity m_Target=nullptr,m_Vehicle=nullptr;bool m_PlayerView=false;
''' + '\n'.join(fields) + orbit + r'''
};
int main(){
// No surviving target: orbit the replicated anchor, with no free translation.
Follow f;Param p;f.Frame(p);vector start=p.transform[3];
vector anchorAim=owner.anchor+vector(0,1.5,0);
assert(p.isDirty&&world.last.Exclude==nullptr);
float initialYaw=f.m_Yaw,initialPitch=f.m_Pitch,initialDistance=f.m_Distance;
inputs.yaw=1;inputs.pitch=.5;inputs.zoom=.1;f.Frame(p);
assert(f.m_Yaw>initialYaw&&f.m_Pitch<initialPitch&&f.m_Distance>initialDistance);
assert(near(f.m_Yaw,189)&&near(f.m_Pitch,15.5)&&near(f.m_Distance,5.6));
assert(distance(start,p.transform[3])>.5&&near(distance(p.transform[3],anchorAim),5.6));
assert(near(world.last.Start,anchorAim));
assert(world.last.Flags==(TraceFlags::WORLD|TraceFlags::ENTS)&&world.last.LayerMask==TRACE_LAYER_CAMERA);
// Selecting player view cannot freeze a camera with no available target.
f.m_PlayerView=true;float before=f.m_Yaw;f.Frame(p);assert(f.m_Yaw>before);
// Bounds apply repeatedly; pitch cannot pass overhead/under the ground.
inputs.pitch=100;inputs.zoom=-100;
for(int i=0;i<20;i++)f.Frame(p);
assert(near(f.m_Pitch,5)&&near(f.m_Distance,2));
inputs.pitch=-100;inputs.zoom=100;
for(int i=0;i<20;i++)f.Frame(p);
assert(near(f.m_Pitch,75)&&near(f.m_Distance,12));
// Menus/chat disable all orbit input, even if action values remain nonzero.
p.isManualInputEnabled=false;float yaw=f.m_Yaw,pitch=f.m_Pitch,zoom=f.m_Distance;
f.Frame(p);assert(near(f.m_Yaw,yaw)&&near(f.m_Pitch,pitch)&&near(f.m_Distance,zoom));
// A moving anchor still follows; it does not grant independent movement.
vector prior=p.transform[3];owner.anchor=owner.anchor+vector(3,2,1);f.Frame(p);
assert(near(p.transform[3]-prior,vector(3,2,1)));
// Collision remains active for the anchor-only camera.
world.fraction=.5;f.Frame(p);assert(near(distance(p.transform[3],world.last.Start),zoom*.45));world.fraction=1;
// Corpse and living third-person targets retain their established framing.
Entity actor;actor.alive=false;owner.target=&actor;inputs={1,0,0};p.isManualInputEnabled=true;
Follow corpse;corpse.m_PlayerView=true;corpse.Frame(p);
assert(near(corpse.m_Yaw,189)&&near(world.last.Start,actor.transform[3]+vector(0,.4,0)));
assert(world.last.Exclude==&actor);
actor.alive=true;Follow living;living.Frame(p);
assert(near(living.m_Yaw,189)&&near(world.last.Start,actor.transform[3]+vector(0,1.5,0)));
// A living teammate's unavailable sampled player view keeps its existing
// stable fallback; this fix must not introduce mouse-driven shoulder drift.
living.m_PlayerView=true;before=living.m_Yaw;float d=living.m_Distance;inputs={1,1,1};living.Frame(p);
assert(near(living.m_Yaw,before)&&near(living.m_Distance,d));
// Vehicle orbit uses vehicle bounds/exclusion and its larger distance range.
Entity vehicle;actor.vehicle=&vehicle;inputs={0,0,0};Follow mounted;mounted.Frame(p);
assert(near(mounted.m_Distance,12)&&near(world.last.Start,(vehicle.lower+vehicle.upper)*.5));
assert(world.last.Exclude==&vehicle);
inputs.zoom=-100;for(int i=0;i<20;i++)mounted.Frame(p);assert(near(mounted.m_Distance,6));
inputs.zoom=100;for(int i=0;i<20;i++)mounted.Frame(p);assert(near(mounted.m_Distance,25));
// Losing that vehicle/target resumes anchor-only controls with foot bounds.
owner.target=nullptr;inputs={1,0,0};mounted.Frame(p);
assert(near(mounted.m_Distance,12)&&world.last.Exclude==nullptr&&near(world.last.Start,owner.anchor+vector(0,1.5,0)));
assert(mounted.m_Target==nullptr&&mounted.m_Vehicle==nullptr);
before=mounted.m_Yaw;mounted.Frame(p);assert(mounted.m_Yaw>before);
// Reappearance adopts that entity's heading once; ordinary following must
// preserve the spectator's accumulated orbit rather than reset each frame.
owner.target=&actor;actor.vehicle=nullptr;actor.transform[2]={1,0,0};inputs={0,0,0};mounted.Frame(p);
assert(near(mounted.m_Yaw,270)&&near(mounted.m_Distance,5)&&mounted.m_Target==&actor);
inputs.yaw=1;mounted.Frame(p);assert(near(mounted.m_Yaw,279));
mounted.Frame(p);assert(near(mounted.m_Yaw,288));
}
''')
print('PASS: actual orbit branch rotates and zooms without a target; anchor tracking, bounds, collision and menu gating; corpse/vehicle orbit and living player-view fallback preserved')