155 lines
9.3 KiB
Python
155 lines
9.3 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,conscious=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;}static bool CanFight(IEntity e){return Alive(e)&&e->conscious;}};
|
|
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;actor.conscious=false;Follow casualty;casualty.m_PlayerView=true;casualty.Frame(p);
|
|
assert(near(casualty.m_Yaw,189)&&near(world.last.Start,actor.transform[3]+vector(0,.4,0)));
|
|
inputs.zoom=.1;float previousDistance=casualty.m_Distance;casualty.Frame(p);assert(casualty.m_Distance>previousDistance);
|
|
actor.conscious=true;inputs={1,0,0};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')
|