Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
152 lines
8.9 KiB
Python
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')
|