Add recon drones and body burial; stabilize spectator camera
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user