Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,8 @@
{
"addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c": "83cdb308c43d261cf7fdf6961ffe8fa35dc1ea10a0456546f2d7a6da82462f22",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c": "3a4d20d1b42e40d6831b2ce2d85348f57a16a77c06fd936e32f1aa69f31cccfb",
"addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c": "8d05074992004297bc04a51f309760090d4ea1598a81468eaafb9b5dcd316a53",
"tests/test_observer_view_relay.py": "0ef0ab00900650d21e8cda0d8b71d70ebb692abde1981d76ff9f624370852f11",
"tests/test_observer_view_buffer.py": "5c3bd10615f6f2aba9a328e6c95f4b9313d3ae6214d8787a9cb4e294724e2a58",
"tests/test_observer_scope.py": "f70d9dea673fb680c589fce8eb143380fd2ce407be602c04835b0e887904d0fb"
}
@@ -0,0 +1,463 @@
enum ERR_ObserverReason
{
NONE,
SPENT_LIFE,
LATE_EXTRACTION,
SPAWN_UNAVAILABLE,
UNCONSCIOUS
}
class ERR_ObserverRules
{
static int Cycle(bool enabled, bool left, bool right, bool previousLeft, bool previousRight)
{
if (!enabled || left == right) return 0;
if (left && !previousLeft) return -1;
if (right && !previousRight) return 1;
return 0;
}
static bool Toggle(bool enabled, bool pressed, bool previous)
{
return enabled && pressed && !previous;
}
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
{
return active && currentBody && alive && !conscious;
}
static bool View(int reason, bool active, bool alive, bool conscious)
{
if (!active || reason == ERR_ObserverReason.NONE) return false;
if (reason == ERR_ObserverReason.UNCONSCIOUS) return alive && !conscious;
return !alive;
}
}
// Presentation only. Existing server admission and observer selection own state.
class ERR_ObserverGuidance
{
static string Message(int reason, bool survivorAvailable)
{
if (reason == ERR_ObserverReason.NONE) return "";
string message = "You are spectating until the next round.";
if (reason == ERR_ObserverReason.SPENT_LIFE)
message = "Your life for this round has been used. You can play again next round.";
else if (reason == ERR_ObserverReason.LATE_EXTRACTION)
message = "Extraction has started. New prisoners can join next round.";
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
message = "Your prisoner could not be spawned. You are spectating until the next round.";
else if (reason == ERR_ObserverReason.UNCONSCIOUS)
{
message = "You are unconscious. Your body remains where you fell and teammates can still treat you. Your view returns automatically when you wake up.";
if (!survivorAvailable) return message + " No conscious teammate is currently available; the camera stays near your body.";
}
if (!survivorAvailable)
return message + " No teammate or body is currently available to follow.";
return message + " Press Left / Right arrow to change teammate or body (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
}
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
{
return reason != ERR_ObserverReason.NONE && (!shown || previousReason != reason || previousAvailable != survivorAvailable);
}
}
class ERR_ObserverCameraClass : SCR_ManualCameraClass {}
// Gate before SCR_ManualCamera activates its context: disabling only the follow
// component is too late, since the camera context can already consume chat keys.
class ERR_ObserverCamera : SCR_ManualCamera
{
override protected bool IsDisabledByMenu()
{
if (super.IsDisabledByMenu()) return true;
if (GetGame().GetInputManager().IsContextActive("ChatContext")) return true;
Widget focused = GetGame().GetWorkspace().GetFocusedWidget();
while (focused)
{
if (EditBoxWidget.Cast(focused)) return true;
focused = focused.GetParent();
}
return false;
}
}
// A local follow camera. It never possesses its target or offers free flight.
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
{
protected IEntity m_Target;
protected IEntity m_Vehicle;
protected float m_Yaw;
protected float m_Pitch = 20;
protected float m_Distance = 5;
protected bool m_LeftDown, m_RightDown, m_EnterDown;
protected int m_InputResumeFrames;
protected bool m_PlayerView;
protected ref ERR_ObserverHeadVisibility m_ViewHead = new ERR_ObserverHeadVisibility();
protected float m_OrbitFov, m_OrbitNear;
protected ref ERR_ObserverScopeDisplay m_ScopeDisplay = new ERR_ObserverScopeDisplay();
protected bool m_InputContextActive;
bool InputContextActive() { return m_InputContextActive; }
bool IsPlayerView() { return m_PlayerView; }
// Process one edge per press; menus still consume the state so holding a key
// while closing a menu cannot unexpectedly change the followed teammate.
void Input(bool enabled, bool left, bool right, bool enter)
{
// An inactive context reports zero even while a physical key is held. Let
// the reactivated context settle, then require release before accepting keys.
if (!enabled) m_InputResumeFrames = 2;
else if (m_InputResumeFrames > 0)
{
enabled = false;
if (m_InputResumeFrames > 1 || (!left && !right && !enter)) m_InputResumeFrames--;
}
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView;
m_LeftDown = left; m_RightDown = right; m_EnterDown = enter;
}
override bool EOnCameraInit()
{
m_OrbitFov = GetCameraEntity().GetVerticalFOV();
m_OrbitNear = GetCameraEntity().GetNearPlane();
return true;
}
override void EOnCameraExit() { m_ScopeDisplay.Close(); m_ViewHead.Restore(); }
override void EOnCameraFrame(SCR_ManualCameraParam param)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5);
IEntity target = controller.ERR_ObservedEntity();
float fov, nearPlane;
if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane))
{
param.fov = fov; GetCameraEntity().SetNearPlane(nearPlane);
m_ViewHead.Update(target, param.transform[3]);
RplId scopeWeapon; vector scopeData;
controller.ERR_ReadScope(scopeWeapon, scopeData);
SCR_2DOpticsComponent optic = ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData);
IEntity weapon;
if (optic)
{
RplComponent weaponRpl = RplComponent.Cast(Replication.FindItem(scopeWeapon));
if (weaponRpl) weapon = weaponRpl.GetEntity();
}
if (!param.isManualInputEnabled || !optic || !weapon || !m_ScopeDisplay.RenderScope(optic, weapon, fov, scopeData)) m_ScopeDisplay.Close();
param.isDirty = true;
return;
}
// No estimated head camera: corpses, streaming gaps and expired samples use
// the stable follow view until an authenticated live camera stream arrives.
m_ScopeDisplay.Close();
m_ViewHead.Restore();
param.fov = m_OrbitFov; GetCameraEntity().SetNearPlane(m_OrbitNear);
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
vector position = aim + "0 4 -6";
IEntity exclude = target;
if (target)
{
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
vector transform[4];
if (vehicle)
{
vehicle.GetWorldTransform(transform);
vector lower, upper;
vehicle.GetWorldBounds(lower, upper);
aim = (lower + upper) * 0.5;
exclude = vehicle;
}
else
{
target.GetWorldTransform(transform);
aim = transform[3] + "0 1.5 0";
if (!ERR_Actors.Alive(target)) aim = transform[3] + "0 0.4 0";
}
if (target != m_Target || vehicle != m_Vehicle)
{
vector angles = transform[2].VectorToAngles();
m_Yaw = angles[0] + 180; m_Pitch = 20; m_Distance = 5;
if (vehicle) m_Distance = 12;
m_Target = target; m_Vehicle = vehicle;
}
if ((!m_PlayerView || !ERR_Actors.Alive(target)) && param.isManualInputEnabled)
{
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
float scale = Math.Clamp(1 + GetInputManager().GetActionValue("ManualCameraZoom") * 12 * param.timeSlice, 0.5, 1.5);
float minimum = 2, maximum = 12;
if (vehicle) { minimum = 6; maximum = 25; }
m_Distance = Math.Clamp(m_Distance * scale, minimum, maximum);
}
float yaw = m_Yaw * Math.DEG2RAD, pitch = m_Pitch * Math.DEG2RAD;
vector offset = Vector(Math.Sin(yaw) * Math.Cos(pitch), Math.Sin(pitch), Math.Cos(yaw) * Math.Cos(pitch));
position = aim + offset * m_Distance;
}
TraceParam trace = new TraceParam();
trace.Start = aim;
trace.End = position;
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = exclude;
float fraction = GetCameraEntity().GetWorld().TraceMove(trace, null);
if (fraction < 1) position = vector.Lerp(aim, position, Math.Max(0.05, fraction - 0.05));
Math3D.DirectionAndUpMatrix(vector.Direction(position, aim), "0 1 0", param.transform);
param.transform[3] = position;
param.isDirty = true;
}
}
modded class SCR_PlayerController
{
[RplProp()]
protected ERR_ObserverReason m_ERRObserverReason;
[RplProp()]
protected RplId m_ERRObservedId = RplId.Invalid();
[RplProp()]
protected vector m_ERRObserverAnchor;
protected IEntity m_ERRServerObserved;
protected IEntity m_ERRServerObservedVehicle;
protected SCR_ManualCamera m_ERRObserverCamera;
protected bool m_ERRObserverMonitor;
protected bool m_ERRObserverReady;
protected ERR_ObserverReason m_ERRShownObserverReason;
protected bool m_ERRShownObserverAvailable;
protected string m_ERRShownRecoveryStatus;
protected float m_ERRLastCycle = -1000;
protected IEntity m_ERRRecoveryBody;
protected float m_ERRRecoveryDeadline = -1;
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
IEntity ERR_ObservedEntity()
{
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRObservedId));
if (!component) return null;
return component.GetEntity();
}
bool ERR_HasObserverCamera() { return m_ERRObserverCamera != null; }
void ERR_SetObserver(ERR_ObserverReason reason)
{
if (!Replication.IsServer()) return;
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity()) && reason != ERR_ObserverReason.UNCONSCIOUS) return;
m_ERRObserverReason = reason;
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
if (reason == ERR_ObserverReason.NONE) ERR_ReleaseObserved();
Replication.BumpMe();
ERR_UpdateObserverServer();
}
protected void ERR_StreamObserved(IEntity entity, bool normalStreaming)
{
RplIdentity identity = GetRplIdentity();
if (!identity.IsValid() || identity == RplIdentity.Local() || !entity) return;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component) component.EnableStreamingConNode(identity, normalStreaming);
}
protected void ERR_ReleaseObserved()
{
ERR_StreamObserved(m_ERRServerObserved, true);
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
m_ERRServerObserved = null;
m_ERRServerObservedVehicle = null;
m_ERRObservedId = RplId.Invalid();
}
void ERR_UpdateObserverServer(int direction = 0)
{
if (!Replication.IsServer()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity body = GetControlledEntity();
bool casualty = mode && ERR_ObserverRules.Casualty(mode.Active(), mode.Players() && mode.Players().CurrentActor(body) != null, ERR_Actors.Alive(body), ERR_Actors.CanFight(body));
if (casualty && m_ERRObserverReason != ERR_ObserverReason.UNCONSCIOUS)
{
m_ERRObserverReason = ERR_ObserverReason.UNCONSCIOUS;
m_ERRObserverAnchor = body.GetOrigin();
}
if (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !ERR_Actors.Alive(body)) m_ERRObserverReason = ERR_ObserverReason.SPENT_LIFE;
// Death can leave the controller attached to its corpse, so entity-lost is
// not a reliable trigger (especially with the last/only player).
ERR_PlayerLife life;
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
if (mode && mode.Active() && life && life.UsedLife && life.Actor && !ERR_Actors.Alive(life.Actor) && !ERR_Actors.Alive(body) && m_ERRObserverReason == ERR_ObserverReason.NONE)
{
m_ERRObserverReason = ERR_ObserverReason.SPENT_LIFE;
m_ERRObserverAnchor = life.Actor.GetOrigin();
}
if (m_ERRObserverReason == ERR_ObserverReason.NONE) return;
if (!mode || !mode.Active() || (ERR_Actors.Alive(body) && !casualty) || (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !casualty))
{
m_ERRObserverReason = ERR_ObserverReason.NONE;
ERR_ReleaseObserved();
Replication.BumpMe();
return;
}
array<IEntity> survivors = {};
ERR_Actors.Players(survivors);
Faction friendly = mode.PlayerFaction();
for (int i = survivors.Count() - 1; i >= 0; i--)
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
// Dead teammates remain selectable alongside survivors, including our own body.
ERR_ObserverTargets.Bodies(mode, survivors);
int index = survivors.Find(m_ERRServerObserved);
if (index < 0)
{
if (life && life.Actor) index = survivors.Find(life.Actor);
if (index < 0) index = 0;
}
else index += direction;
if (index < 0) index = survivors.Count() - 1;
if (index >= survivors.Count()) index = 0;
IEntity target;
if (!survivors.IsEmpty()) target = survivors[index];
if (target != m_ERRServerObserved)
{
ERR_ReleaseObserved();
m_ERRServerObserved = target;
if (target)
{
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
if (component) m_ERRObservedId = component.Id();
ERR_StreamObserved(target, false);
}
}
if (target)
{
m_ERRObserverAnchor = target.GetOrigin();
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
if (vehicle != m_ERRServerObservedVehicle)
{
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
m_ERRServerObservedVehicle = vehicle;
ERR_StreamObserved(vehicle, false);
}
}
else if (casualty) m_ERRObserverAnchor = body.GetOrigin();
Replication.BumpMe();
}
void ERR_CycleObserver(int direction)
{
if (this == GetGame().GetPlayerController()) Rpc(ERR_RequestObserverCycle, direction);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_RequestObserverCycle(int direction)
{
if (!Replication.IsServer() || (direction != -1 && direction != 1)) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now - m_ERRLastCycle < 250) return;
m_ERRLastCycle = now;
ERR_UpdateObserverServer(direction);
}
void ERR_StartObserverMonitor()
{
if (this != GetGame().GetPlayerController() || System.IsConsoleApp() || m_ERRObserverMonitor) return;
m_ERRObserverMonitor = true;
ERR_OnObserverMonitorStarted();
GetGame().GetCallqueue().CallLater(ERR_UpdateObserverClient, 250, true);
}
protected void ERR_OnObserverMonitorStarted() {}
protected void ERR_UpdateObserverClient()
{
if (!GetGame()) return;
if (this != GetGame().GetPlayerController())
{
// A replaced controller may retain its timer until native destruction.
// Release only its presentation; never restore its old body camera.
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
m_ERRObserverMonitor = false;
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
ERR_CloseObserverCamera();
return;
}
ERR_GameMode mode = ERR_GameMode.Get();
ERR_UpdateRecoveryCamera(mode);
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
if (!observe)
{
ERR_CloseObserverCamera();
return;
}
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
if (editor && editor.IsOpened()) return;
if (!m_ERRObserverCamera)
{
Resource resource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
if (!resource || !resource.IsValid()) return;
EntitySpawnParams params = new EntitySpawnParams();
params.TransformMode = ETransformMode.WORLD;
params.Transform[3] = m_ERRObserverAnchor + "0 5 -6";
m_ERRObserverCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefabLocal(resource, GetGame().GetWorld(), params));
if (!m_ERRObserverCamera) return;
}
CameraManager manager = GetGame().GetCameraManager();
if (!manager) return;
// Native death/respawn camera changes can arrive after our camera is created.
if (manager.CurrentCamera() != m_ERRObserverCamera) manager.SetCamera(m_ERRObserverCamera);
if (manager.CurrentCamera() != m_ERRObserverCamera) return;
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
if (respawn) respawn.DestroyLoadingPlaceholder();
// The server-selected replicated ID distinguishes no target from a target
// whose local entity is still streaming. Never imply no survivors from delay.
bool survivorAvailable = m_ERRObservedId != RplId.Invalid();
string recovery = mode.RecoveryStatus();
if (ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable) || m_ERRShownRecoveryStatus != recovery)
{
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints) return; // retry on the existing monitor; do not lose the first hint
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
if (!recovery.IsEmpty()) message += "\n" + recovery;
if (!hints.ShowCustom(message, "Spectating", 12)) return;
m_ERRObserverReady = true;
m_ERRShownObserverReason = m_ERRObserverReason;
m_ERRShownObserverAvailable = survivorAvailable;
m_ERRShownRecoveryStatus = recovery;
}
}
// Track the actual local body even when an editor/other camera owns the view.
// Run once per recovery, with retries for asynchronous editor close/camera setup.
protected void ERR_UpdateRecoveryCamera(ERR_GameMode mode)
{
IEntity body = GetControlledEntity();
if (!mode || !mode.Active() || !ERR_Actors.Alive(body))
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
if (!ERR_Actors.CanFight(body))
{ m_ERRRecoveryBody = body; m_ERRRecoveryDeadline = -1; return; }
if (!m_ERRRecoveryBody) return;
if (body != m_ERRRecoveryBody)
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
float now = GetGame().GetWorld().GetWorldTime();
if (m_ERRRecoveryDeadline < 0) m_ERRRecoveryDeadline = now + 10000;
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
if (editor && editor.IsOpened()) editor.Close(false);
CameraManager manager = GetGame().GetCameraManager();
CameraBase playerCamera = GetPlayerCamera();
if (manager && playerCamera) manager.SetCamera(playerCamera);
if ((manager && playerCamera && manager.CurrentCamera() == playerCamera && (!editor || !editor.IsOpened())) || now >= m_ERRRecoveryDeadline)
{
if (now >= m_ERRRecoveryDeadline) Print("Reintroduced [spectator]: recovery camera return timed out waiting for editor/camera; player body remains controlled", LogLevel.WARNING);
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
}
}
protected void ERR_CloseObserverCamera()
{
m_ERRObserverReady = false;
m_ERRShownRecoveryStatus = "";
if (!m_ERRObserverCamera) return;
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
m_ERRObserverCamera.Terminate();
delete m_ERRObserverCamera;
m_ERRObserverCamera = null;
}
void ~SCR_PlayerController()
{
if (GetGame())
{
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
ERR_CloseObserverCamera();
if (Replication.IsServer()) ERR_ReleaseObserved();
}
}
}
@@ -0,0 +1,259 @@
// Camera poses are cosmetic, owner-sampled and relayed only to authorized
// spectators of that body. No client-supplied position affects gameplay.
class ERR_ObserverViewRules
{
// The body and camera arrive through independently timed replication streams.
// Keep the sampled world axes and world-axis eye offset together. Applying
// the proxy body's rotation to delayed body-local aim adds its yaw error to
// both aim and eye position, even when the owner's camera is steady.
static void Encode(vector body[4], vector view[4], out vector pose[4])
{
for (int axis = 0; axis < 3; axis++) pose[axis] = view[axis];
pose[3] = view[3] - body[3];
}
static void Decode(vector body[4], vector pose[4], out vector view[4])
{
for (int axis = 0; axis < 3; axis++) view[axis] = pose[axis];
view[3] = body[3] + pose[3];
}
static bool Sample(vector position, vector forward, vector up, float fov, float nearPlane)
{
// Negated bounded comparisons also reject NaN/infinity before matrix math.
if (!(position.LengthSq() <= 900) || !(fov >= 1 && fov <= 150) || !(nearPlane >= 0.005 && nearPlane <= 2)) return false;
if (!(forward.LengthSq() >= 0.9 && forward.LengthSq() <= 1.1) || !(up.LengthSq() >= 0.9 && up.LengthSq() <= 1.1)) return false;
return Math.AbsFloat(vector.Dot(forward, up)) <= 0.05;
}
static bool Fresh(float now, float received) { return received >= 0 && now >= received && now - received <= 350; }
}
// Keep actual sampled poses on a short timeline instead of restarting a blend
// on every packet arrival. Only presentation is delayed; possession is unchanged.
class ERR_ObserverViewSample
{
int Stamp;
vector Position, Forward, Up;
float Fov, Near;
}
class ERR_ObserverViewBuffer
{
protected RplId m_Target = RplId.Invalid();
protected int m_Stamp = -1;
protected RplId m_ScopeWeapon = RplId.Invalid();
protected vector m_ScopeData;
protected float m_Received = -1;
protected float m_Playback, m_ReadAt;
protected ref array<ref ERR_ObserverViewSample> m_Samples = {};
void Scope(out RplId weapon, out vector data) { weapon = m_ScopeWeapon; data = m_ScopeData; }
void Clear()
{
m_Target = RplId.Invalid(); m_Stamp = -1; m_Received = -1;
m_ScopeWeapon = RplId.Invalid(); m_ScopeData = vector.Zero;
m_Samples.Clear();
}
void Push(RplId target, int stamp, float now, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
{
if (!ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
if (!ERR_ObserverViewRules.Sample(position, forward, up, fov, nearPlane)) return;
if (target == m_Target && stamp <= m_Stamp) return;
if (target != m_Target || scopeWeapon != m_ScopeWeapon || scopeData[0] != m_ScopeData[0] || !ERR_ObserverViewRules.Fresh(now, m_Received))
{
m_Samples.Clear();
m_Playback = stamp - 100; m_ReadAt = now;
}
ERR_ObserverViewSample sample = new ERR_ObserverViewSample();
sample.Stamp = stamp; sample.Position = position; sample.Forward = forward; sample.Up = up;
sample.Fov = fov; sample.Near = nearPlane;
m_Samples.Insert(sample);
// Bounded even when first-person view is not being rendered.
while (m_Samples.Count() > 12) m_Samples.Remove(0);
m_ScopeWeapon = scopeWeapon; m_ScopeData = scopeData;
m_Target = target; m_Stamp = stamp; m_Received = now;
}
bool Read(RplId target, float now, float seconds, out vector local[4], out float fov, out float nearPlane)
{
if (target != m_Target || !ERR_ObserverViewRules.Fresh(now, m_Received) || m_Samples.IsEmpty()) return false;
// Use elapsed local time, not the number of reads or a packet's arrival time.
// No clock synchronization between clients is required: stamps are relative.
if (now - m_ReadAt > 250) m_Playback = m_Stamp - 100;
else m_Playback += Math.Max(0, now - m_ReadAt);
m_ReadAt = now;
// A long render stall resumes near live rather than replaying old movement.
if (m_Stamp - m_Playback > 250) m_Playback = m_Stamp - 100;
// Never extrapolate through walls or invent aim during a packet gap.
m_Playback = Math.Min(m_Playback, m_Stamp);
while (m_Samples.Count() > 2 && m_Samples[1].Stamp <= m_Playback) m_Samples.Remove(0);
ERR_ObserverViewSample start = m_Samples[0];
ERR_ObserverViewSample end = start;
if (m_Samples.Count() > 1) end = m_Samples[1];
float blend = 0;
if (end.Stamp > start.Stamp) blend = Math.Clamp((m_Playback - start.Stamp) / (end.Stamp - start.Stamp), 0, 1);
vector forward = vector.Lerp(start.Forward, end.Forward, blend);
vector up = vector.Lerp(start.Up, end.Up, blend);
if (forward.LengthSq() < 0.1 || up.LengthSq() < 0.1) { forward = end.Forward; up = end.Up; }
forward.Normalize(); up.Normalize();
if (Math.AbsFloat(vector.Dot(forward, up)) > 0.95) { forward = end.Forward; up = end.Up; }
Math3D.DirectionAndUpMatrix(forward, up, local);
local[3] = vector.Lerp(start.Position, end.Position, blend);
fov = Math.Lerp(start.Fov, end.Fov, blend); nearPlane = end.Near;
return true;
}
}
modded class SCR_PlayerController
{
protected ref ERR_ObserverViewBuffer m_ERRViewBuffer = new ERR_ObserverViewBuffer();
protected ref array<SCR_PlayerController> m_ERRViewWatchers = {};
protected float m_ERRViewServerRequestAt = -1000;
protected float m_ERRViewRequestAt = -1000, m_ERRViewLeaseUntil = -1;
protected float m_ERRViewPublishUntil = -1, m_ERRViewLeaseSent = -1000;
protected float m_ERRViewSendWait, m_ERRViewLastPacket = -1000;
protected int m_ERRViewSequence, m_ERRViewInputSequence;
protected IEntity m_ERRViewSourceBody;
bool ERR_ViewRecipient(IEntity target, float now)
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Active() && now <= m_ERRViewLeaseUntil && target && target == m_ERRServerObserved && ERR_Actors.CanFight(target) && mode.Players().CurrentActor(target) && SCR_FactionManager.SGetFaction(target) == mode.PlayerFaction() && GetGame().GetPlayerManager().GetPlayerController(GetPlayerId()) == this && ERR_ObserverRules.View(m_ERRObserverReason, true, ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
}
void ERR_ReadScope(out RplId weapon, out vector data) { m_ERRViewBuffer.Scope(weapon, data); }
bool ERR_ReadPlayerView(IEntity target, float seconds, out vector transform[4], out float fov, out float nearPlane)
{
if (this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
float now = GetGame().GetWorld().GetWorldTime();
if (now - m_ERRViewRequestAt >= 500)
{ m_ERRViewRequestAt = now; Rpc(ERR_RequestPlayerView); }
vector pose[4], world[4];
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
RplId scopeWeapon; vector scopeData;
m_ERRViewBuffer.Scope(scopeWeapon, scopeData);
if (scopeWeapon != RplId.Invalid() && !ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData)) return false;
target.GetWorldTransform(world);
ERR_ObserverViewRules.Decode(world, pose, transform);
return true;
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_RequestPlayerView()
{
if (!Replication.IsServer()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now - m_ERRViewServerRequestAt < 250) return;
m_ERRViewServerRequestAt = now;
// A lease grants no observer rights; all existing server selection/roster
// checks still apply, both here and when each sample is forwarded.
m_ERRViewLeaseUntil = now + 1500;
if (!ERR_ViewRecipient(m_ERRServerObserved, now)) return;
ERR_PlayerLife life = ERR_GameMode.Get().Players().CurrentActor(m_ERRServerObserved);
SCR_PlayerController source = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId));
if (source) source.ERR_AddViewWatcher(this, now);
}
void ERR_AddViewWatcher(SCR_PlayerController watcher, float now)
{
if (!Replication.IsServer() || !watcher) return;
for (int i = m_ERRViewWatchers.Count() - 1; i >= 0; i--)
if (!m_ERRViewWatchers[i] || !m_ERRViewWatchers[i].ERR_ViewRecipient(GetControlledEntity(), now)) m_ERRViewWatchers.Remove(i);
if (!m_ERRViewWatchers.Contains(watcher) && m_ERRViewWatchers.Count() < 64) m_ERRViewWatchers.Insert(watcher);
if (now - m_ERRViewLeaseSent < 250) return;
m_ERRViewLeaseSent = now;
Rpc(ERR_EnableViewPublisher);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_EnableViewPublisher()
{
if (this == GetGame().GetPlayerController()) m_ERRViewPublishUntil = GetGame().GetWorld().GetWorldTime() + 2000;
}
void ERR_PublishPlayerView(float seconds)
{
if (this != GetGame().GetPlayerController() || System.IsConsoleApp()) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now > m_ERRViewPublishUntil) return;
m_ERRViewSendWait += seconds;
if (m_ERRViewSendWait < 0.05) return;
m_ERRViewSendWait = 0;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
PlayerCamera camera = GetPlayerCamera();
CameraManager manager = GetGame().GetCameraManager();
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor) || !camera || !manager || manager.CurrentCamera() != camera) return;
vector world[4], view[4], pose[4];
actor.GetWorldTransform(world);
camera.GetWorldCameraTransform(view);
float fov = camera.GetVerticalFOV(), nearPlane = camera.GetNearPlane();
RplId scopeWeapon; vector scopeData;
ERR_ObserverScope.Sample(actor, view, fov, scopeWeapon, scopeData);
ERR_ObserverViewRules.Encode(world, view, pose);
if (!ERR_ObserverViewRules.Sample(pose[3], pose[2], pose[1], fov, nearPlane)) return;
Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, pose[3], pose[2], pose[1], fov, nearPlane, scopeWeapon, scopeData);
}
[RplRpc(RplChannel.Unreliable, RplRcver.Server)]
protected void ERR_SubmitPlayerView(int sequence, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
{
if (!Replication.IsServer() || !ERR_ObserverScope.Valid(scopeWeapon, scopeData) || !ERR_ObserverViewRules.Sample(position, forward, up, fov, nearPlane)) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor)) return;
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
if (!life || GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId) != this) return;
float now = GetGame().GetWorld().GetWorldTime();
if (actor != m_ERRViewSourceBody) { m_ERRViewSourceBody = actor; m_ERRViewInputSequence = 0; }
if (sequence <= m_ERRViewInputSequence || now - m_ERRViewLastPacket < 35) return;
m_ERRViewInputSequence = sequence; m_ERRViewLastPacket = now;
RplComponent component = RplComponent.Cast(actor.FindComponent(RplComponent));
if (!component) return;
for (int i = m_ERRViewWatchers.Count() - 1; i >= 0; i--)
{
SCR_PlayerController watcher = m_ERRViewWatchers[i];
if (!watcher || !watcher.ERR_ViewRecipient(actor, now)) { m_ERRViewWatchers.Remove(i); continue; }
watcher.ERR_DeliverPlayerView(component.Id(), now, position, forward, up, fov, nearPlane, scopeWeapon, scopeData);
}
}
void ERR_DeliverPlayerView(RplId target, int stamp, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
{
Rpc(ERR_ReceivePlayerViewPacket, target, stamp, position, forward, up, Vector(fov, nearPlane, 0), scopeWeapon, scopeData);
}
[RplRpc(RplChannel.Unreliable, RplRcver.Owner)]
protected void ERR_ReceivePlayerViewPacket(RplId target, int stamp, vector position, vector forward, vector up, vector lens, RplId scopeWeapon, vector scopeData)
{
ERR_ReceivePlayerView(target, stamp, position, forward, up, lens[0], lens[1], scopeWeapon, scopeData);
}
protected void ERR_ReceivePlayerView(RplId target, int stamp, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (this != GetGame().GetPlayerController() || target == RplId.Invalid() || target != m_ERRObservedId || !mode || !ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()))) return;
m_ERRViewBuffer.Push(target, stamp, GetGame().GetWorld().GetWorldTime(), position, forward, up, fov, nearPlane, scopeWeapon, scopeData);
}
}
modded class ArmaReforgerScripted
{
override void OnUpdate(BaseWorld world, float timeslice)
{
super.OnUpdate(world, timeslice);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetPlayerController());
if (controller) controller.ERR_PublishPlayerView(timeslice);
}
}
// Use the game's own head/helmet fade rather than moving the camera ahead of
// the face. Changes are local and reversed when the observer stops using them.
class ERR_ObserverHeadVisibility
{
protected SCR_ChimeraCharacter m_Character;
protected SCR_CharacterCameraHandlerComponent m_Handler;
protected int m_Previous, m_Applied;
void Update(IEntity target, vector cameraPosition)
{
if (target != m_Character)
{
Restore();
m_Character = SCR_ChimeraCharacter.Cast(target);
if (!m_Character || !m_Character.GetCharacterController()) return;
m_Handler = SCR_CharacterCameraHandlerComponent.Cast(m_Character.GetCharacterController().GetCameraHandlerComponent());
m_Previous = m_Character.m_fFaceAlphaTest;
}
if (!m_Handler || !m_Character.GetAnimation()) return;
m_Handler.UpdateHeadVisibility(cameraPosition);
m_Applied = m_Character.m_fFaceAlphaTest;
}
void Restore()
{
if (m_Handler && m_Character && m_Character.m_fFaceAlphaTest == m_Applied) m_Handler.OnAlphatestChange(m_Previous);
m_Character = null; m_Handler = null;
}
void ~ERR_ObserverHeadVisibility() { Restore(); }
}
@@ -0,0 +1,134 @@
// Presentation only. Scoped spectators use a private full-screen optic display;
// never activate the remote sight (which registers weapon input and changes HUD).
class ERR_ObserverScope
{
static bool Valid(RplId weapon, vector data)
{
if (weapon == RplId.Invalid()) return data == vector.Zero;
return data[0] >= 1 && data[0] <= 32 && data[0] == Math.Round(data[0]) && data[1] >= -90 && data[1] <= 90 && (data[2] == 0 || data[2] == 1);
}
static void Sample(IEntity actor, out vector view[4], out float fov, out RplId weaponId, out vector data)
{
weaponId = RplId.Invalid(); data = vector.Zero;
ChimeraCharacter character = ChimeraCharacter.Cast(actor);
if (!character) return;
CharacterControllerComponent control = character.GetCharacterController();
if (!control || !control.IsWeaponADS()) return;
BaseWeaponManagerComponent weapons = control.GetWeaponManagerComponent();
if (!weapons || !weapons.GetCurrentWeapon()) return;
SCR_2DOpticsComponent optic = SCR_2DOpticsComponent.Cast(weapons.GetCurrentSights());
if (!optic || !optic.IsSightADSActive()) return;
RplComponent rpl = RplComponent.Cast(weapons.GetCurrentWeapon().GetOwner().FindComponent(RplComponent));
if (!rpl || rpl.Id() == RplId.Invalid()) return;
float scopeFov = optic.GetFOV();
if (!(scopeFov >= 1 && scopeFov <= 150)) return;
vector sample = Vector(weapons.GetCurrentSightsIndex() + 1, optic.GetCurrentReticleOffsetY(), 0);
if (optic.ERR_ObserverIlluminated()) sample[2] = 1;
if (!Valid(rpl.Id(), sample)) return;
// A PiP sight has its own sight-line camera. Use that direction rather than
// magnifying the peripheral eye camera, which need not point down the scope.
SCR_2DPIPSightsComponent pip = SCR_2DPIPSightsComponent.Cast(optic);
if (pip && pip.IsPIPEnabled())
{
if (!pip.GetPIPCamera()) return;
pip.GetPIPCamera().GetWorldCameraTransform(view);
}
weaponId = rpl.Id(); data = sample; fov = scopeFov;
}
static SCR_2DOpticsComponent Resolve(IEntity target, RplId weaponId, vector data)
{
if (weaponId == RplId.Invalid() || !Valid(weaponId, data)) return null;
ChimeraCharacter character = ChimeraCharacter.Cast(target);
if (!character || !character.GetCharacterController()) return null;
BaseWeaponManagerComponent weapons = character.GetCharacterController().GetWeaponManagerComponent();
if (!weapons || !weapons.GetCurrentWeapon() || weapons.GetCurrentSightsIndex() + 1 != data[0]) return null;
RplComponent rpl = RplComponent.Cast(weapons.GetCurrentWeapon().GetOwner().FindComponent(RplComponent));
if (!rpl || rpl.Id() != weaponId) return null;
return SCR_2DOpticsComponent.Cast(weapons.GetCurrentSights());
}
}
modded class SCR_2DOpticsComponent
{
bool ERR_ObserverIlluminated() { return m_bIsIlluminationOn; }
}
// Reuse the native reticle textures, angular sizing and vignette layout, without
// registering with the player's HUD, ADS invokers, weapon input or visibility.
class ERR_ObserverScopeDisplay : SCR_2DOpticsDisplay
{
protected ref Widget m_ERRScopeRoot; // Native HUD normally owns this strong reference.
protected IEntity m_ERRHiddenWeapon, m_ERRHiddenOptic;
protected float m_ERRFov;
bool RenderScope(SCR_2DOpticsComponent optic, IEntity weapon, float fov, vector data)
{
if (!optic || !weapon) { Close(); return false; }
if (!m_wRoot)
{
m_ERRScopeRoot = GetGame().GetWorkspace().CreateWidgets("{4CE66FA8219D33D7}UI/layouts/Sights/Optic_Default.layout");
m_wRoot = m_ERRScopeRoot;
if (!m_wRoot) return false;
FindWidgets();
if (!m_wLayoutRear || !m_wLayoutFront || !m_wImgReticle || !m_wImgCover || !m_wBlur || !m_wRearFillLeft || !m_wRearFillTop || !m_wFrontFillLeft || !m_wFrontFillTop) { Close(); return false; }
m_wRoot.SetFlags(WidgetFlags.IGNORE_CURSOR);
m_wRoot.SetZOrder(1); // Standalone workspace root must be above the game viewport.
}
m_ERRFov = fov;
if (m_Optic != optic)
{
RestoreWeapon();
m_Optic = optic;
GetOpticData();
SetupOpticImage();
// Hide only this locally rendered weapon, never the soldier or inventory.
if ((weapon.GetFlags() & EntityFlags.VISIBLE) != 0)
{ m_ERRHiddenWeapon = weapon; weapon.ClearFlags(EntityFlags.VISIBLE, false); }
IEntity opticOwner = optic.GetOwner();
if (opticOwner != weapon && (opticOwner.GetFlags() & EntityFlags.VISIBLE) != 0)
{ m_ERRHiddenOptic = opticOwner; opticOwner.ClearFlags(EntityFlags.VISIBLE, false); }
}
UpdateCurrentFov();
// Native layout uses fill widgets rather than centered anchors. Recompute
// these for resolution/DPI changes, without adding remote animation sway.
WorkspaceWidget workspace = GetGame().GetWorkspace();
float width, height;
workspace.GetScreenSize(width, height);
width = workspace.DPIUnscale(width); height = workspace.DPIUnscale(height);
UpdateScale(1, 1, 1);
m_wFrontFillLeft.SetWidthOverride(LayerDefaultPosition(width, m_fObjectiveSize));
m_wFrontFillTop.SetHeightOverride(LayerDefaultPosition(height, m_fObjectiveSize));
m_wRearFillLeft.SetWidthOverride(LayerDefaultPosition(width, m_fVignetteSize));
m_wRearFillTop.SetHeightOverride(LayerDefaultPosition(height, m_fVignetteSize));
SCR_2DOpticsComponentClass settings = SCR_2DOpticsComponentClass.Cast(optic.GetComponentData(optic.GetOwner()));
if (settings)
{
if (data[2] == 1 && settings.HasIllumination()) ChangeReticleTint(settings.GetReticleIlluminationColor(), settings.GetReticleIlluminationColor());
else ChangeReticleTint(settings.GetReticleColor(), settings.GetReticleOutlineColor());
}
// The relayed pose is already the aiming camera; don't add another local
// weapon-animation sway (remote proxies can be several frames behind).
SetReticleOffset(optic.GetReticleOffsetX(), data[1]);
m_wImgCover.SetVisible(false); m_wBlur.SetIntensity(0); m_wBlur.SetOpacity(0);
m_wRoot.SetOpacity(1); m_wRoot.SetVisible(true);
return true;
}
override void UpdateCurrentFov(bool forceUpdate = false)
{
if (!forceUpdate && float.AlmostEqual(m_fOpticCurrentFov, m_ERRFov)) return;
m_fOpticCurrentFov = m_ERRFov;
UpdateReticleSize();
}
protected void RestoreWeapon()
{
if (m_ERRHiddenWeapon && !m_ERRHiddenWeapon.IsDeleted()) m_ERRHiddenWeapon.SetFlags(EntityFlags.VISIBLE, false);
if (m_ERRHiddenOptic && !m_ERRHiddenOptic.IsDeleted()) m_ERRHiddenOptic.SetFlags(EntityFlags.VISIBLE, false);
m_ERRHiddenWeapon = null; m_ERRHiddenOptic = null;
}
void Close()
{
RestoreWeapon();
m_Optic = null;
if (m_wRoot) m_wRoot.RemoveFromHierarchy();
m_wRoot = null; m_ERRScopeRoot = null;
}
void ~ERR_ObserverScopeDisplay() { Close(); }
}
@@ -0,0 +1,52 @@
"""Actual scope sampling/identity selection; native weapon/camera APIs doubled."""
from policy_harness import ROOT, body, run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c'
def adapt(s):
import re
s=s.replace('out vector view[4]', 'vector (&view)[4]').replace('out vector data','vector& data').replace('out float fov','float& fov').replace('out RplId weaponId','RplId& weaponId').replace('null','nullptr')
for cls in ('ChimeraCharacter','CharacterControllerComponent','BaseWeaponManagerComponent','SCR_2DOpticsComponent','SCR_2DPIPSightsComponent','RplComponent'):
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s);s=s.replace(cls+'.',cls+'::')
for name in ('character','control','weapons','optic','rpl','pip','actor'):
s=s.replace(name+'.',name+'->')
for name in ('GetCharacterController()','GetCurrentWeapon()','GetOwner()','GetPIPCamera()'):
s=s.replace(name+'.',name+'->')
s=s.replace('FindComponent(RplComponent)','FindComponent()').replace('RplId.','RplId::').replace('Math.','Math::').replace('vector.Zero','vector::Zero')
return s
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
struct vector{float a=0,b=0,c=0;float&operator[](int i){return i==0?a:(i==1?b:c);}bool operator==(vector o){return a==o.a&&b==o.b&&c==o.c;}static vector Zero;};vector vector::Zero;
vector Vector(float a,float b,float c){return {a,b,c};}
struct RplId{int id=-1;RplId(int n=-1):id(n){}static RplId Invalid(){return {};}bool operator==(RplId o){return id==o.id;}bool operator!=(RplId o){return id!=o.id;}};
namespace Math{float Round(float n){return std::round(n);}}
struct RplComponent{RplId id=10;RplId Id(){return id;}static RplComponent*Cast(RplComponent*p){return p;}};
struct Camera{void GetWorldCameraTransform(vector (&v)[4]){v[3]={1,2,3};}};
struct SCR_2DOpticsComponent{bool ads=true,lit=false;float fov=12,offset=.1;virtual ~SCR_2DOpticsComponent(){}static SCR_2DOpticsComponent*Cast(SCR_2DOpticsComponent*p){return p;}bool IsSightADSActive(){return ads;}float GetFOV(){return fov;}float GetCurrentReticleOffsetY(){return offset;}bool ERR_ObserverIlluminated(){return lit;}};
struct SCR_2DPIPSightsComponent:SCR_2DOpticsComponent{bool enabled=true;Camera*camera=nullptr;static SCR_2DPIPSightsComponent*Cast(SCR_2DOpticsComponent*p){return dynamic_cast<SCR_2DPIPSightsComponent*>(p);}bool IsPIPEnabled(){return enabled;}Camera*GetPIPCamera(){return camera;}};
struct Weapon{RplComponent rpl;Weapon*GetOwner(){return this;}RplComponent*FindComponent(){return &rpl;}};
struct BaseWeaponManagerComponent{Weapon*weapon=nullptr;SCR_2DOpticsComponent*optic=nullptr;int index=0;Weapon*GetCurrentWeapon(){return weapon;}SCR_2DOpticsComponent*GetCurrentSights(){return optic;}int GetCurrentSightsIndex(){return index;}};
struct CharacterControllerComponent{bool ads=true;BaseWeaponManagerComponent weapons;bool IsWeaponADS(){return ads;}BaseWeaponManagerComponent*GetWeaponManagerComponent(){return &weapons;}};
struct ChimeraCharacter{CharacterControllerComponent control;CharacterControllerComponent*GetCharacterController(){return &control;}static ChimeraCharacter*Cast(ChimeraCharacter*p){return p;}};using IEntity=ChimeraCharacter*;
struct Scope{
'''+''.join(adapt(body(p,s)) for s in ('static bool Valid(', 'static void Sample(', 'static SCR_2DOpticsComponent Resolve('))+r'''
};
int main(){
ChimeraCharacter actor;Weapon weapon;SCR_2DOpticsComponent optic;actor.control.weapons.weapon=&weapon;actor.control.weapons.optic=&optic;
vector view[4],data;float fov=60;RplId id;
Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(10)&&fov==12&&data[0]==1&&data[1]==.1f);
assert(Scope::Resolve(&actor,id,data)==&optic);
actor.control.weapons.index=1;assert(!Scope::Resolve(&actor,id,data));actor.control.weapons.index=0;
assert(!Scope::Resolve(&actor,11,data));assert(!Scope::Resolve(nullptr,id,data));
actor.control.ads=false;fov=60;Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(-1)&&data==vector::Zero&&fov==60);actor.control.ads=true;
optic.ads=false;Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(-1));optic.ads=true;
optic.fov=std::numeric_limits<float>::quiet_NaN();Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(-1));optic.fov=12;
SCR_2DPIPSightsComponent pip;actor.control.weapons.optic=&pip;
Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(-1)); // no camera yet
Camera camera;pip.camera=&camera;pip.lit=true;Scope::Sample(&actor,view,fov,id,data);
assert(id==RplId(10)&&data[2]==1&&view[3]==Vector(1,2,3)&&fov==12);
pip.enabled=false;view[3]={7,8,9};Scope::Sample(&actor,view,fov,id,data);assert(view[3]==Vector(7,8,9)); // 2D keeps sampled player pose
actor.control.weapons.weapon=nullptr;Scope::Sample(&actor,view,fov,id,data);assert(id==RplId(-1));
}
''')
print('PASS: production scope sampling preserves ordinary views, selects active ADS only, carries zoom/zeroing/illumination and rejects mismatched/unstreamed optics; native APIs doubled')
@@ -0,0 +1,114 @@
"""Execute actual camera sample validation/interpolation; native math substituted."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
def adapt(s):
for name in ['ERR_ObserverScope','ERR_ObserverViewRules','Math','Math3D','vector','RplId']:s=s.replace(name+'.',name+'::')
s=s.replace('out vector','vector').replace('out float ','float& ')
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('])
methods='\n'.join(adapt(body(p,k)) for k in ['void Clear(', 'void Push(', 'bool Read('])
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('])
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <vector>
#include <iostream>
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;vector operator+(vector v){return{x+v.x,y+v.y,z+v.z};}vector operator-(vector v){return{x-v.x,y-v.y,z-v.z};}vector operator*(float n){return{x*n,y*n,z*n};}
float LengthSq(){return x*x+y*y+z*z;}void Normalize(){float n=sqrt(LengthSq());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;}static vector Lerp(vector a,vector b,float f){return a+(b-a)*f;}
};
vector vector::Zero;
namespace Math {float Round(float f){return std::round(f);}float AbsFloat(float f){return fabs(f);}float Max(float a,float b){return std::max(a,b);}float Min(float a,float b){return std::min(a,b);}float Clamp(float n,float a,float b){return std::clamp(n,a,b);}float Lerp(float a,float b,float f){return a+(b-a)*f;}}
vector cross(vector a,vector b){return{a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x};}
namespace Math3D {void DirectionAndUpMatrix(vector forward,vector up,vector* out){auto right=cross(up,forward);right.Normalize();out[0]=right;out[1]=cross(forward,right);out[2]=forward;}}
struct RplId {int value=-1;RplId(int v=-1):value(v){}static RplId Invalid(){return{};}bool operator==(RplId a){return value==a.value;}bool operator!=(RplId a){return value!=a.value;}};
struct ERR_ObserverViewRules {'''+rules+r'''};
struct ERR_ObserverScope {'''+adapt(body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c','static bool Valid('))+r'''};
struct ERR_ObserverViewSample {int Stamp;vector Position,Forward,Up;float Fov,Near;};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}void Remove(int i){this->erase(this->begin()+i);}};
struct Buffer {RplId m_ScopeWeapon;vector m_ScopeData;RplId m_Target;int m_Stamp=-1;float m_Received=-1,m_Playback=0,m_ReadAt=0;Array<ERR_ObserverViewSample*> m_Samples;
'''+methods+r'''};
struct Baseline {RplId m_ScopeWeapon;vector m_ScopeData;RplId m_Target;int m_Stamp=-1;float m_Received=-1;vector m_Position,m_Forward,m_Up,m_StartPosition,m_StartForward,m_StartUp;float m_Fov=0,m_Near=0,m_StartFov=0,m_Blend=1;
'''+old_methods+r'''};
bool close(float a,float b){return fabs(a-b)<0.0001;}
int main(){
vector pos{.1,1.6,.2},forward{0,0,1},up{0,1,0};
assert(ERR_ObserverViewRules::Sample(pos,forward,up,60,.0125));
assert(!ERR_ObserverViewRules::Sample({31,0,0},forward,up,60,.0125));
assert(!ERR_ObserverViewRules::Sample(pos,{},up,60,.0125));
assert(!ERR_ObserverViewRules::Sample(pos,forward,forward,60,.0125));
float nan=std::numeric_limits<float>::quiet_NaN();
assert(!ERR_ObserverViewRules::Sample({nan,0,0},forward,up,60,.0125));
assert(!ERR_ObserverViewRules::Sample(pos,{nan,0,1},up,60,.0125));
assert(!ERR_ObserverViewRules::Sample(pos,forward,up,nan,.0125));
assert(!ERR_ObserverViewRules::Sample(pos,forward,up,60,nan));
assert(!ERR_ObserverViewRules::Sample(pos,forward,up,0,.0125));
Buffer b;vector matrix[4];float fov,nearPlane;
assert(!b.Read(1,100,1,matrix,fov,nearPlane));
b.Push(1,100,100,pos,forward,up,60,.0125, -1, {});assert(b.Read(1,100,.01,matrix,fov,nearPlane));assert(close(matrix[3].y,1.6)&&close(fov,60));
// Actual lean/roll and zoom, without forcing world-up or a hard-coded camera FOV.
vector leanedUp{.6,.8,0};b.Push(1,150,150,{.3,1.4,.2},forward,leanedUp,30,.02, -1, {});
assert(b.Read(1,225,.025,matrix,fov,nearPlane));assert(close(matrix[3].x,.2)&&close(fov,45)&&matrix[1].x>0);
assert(b.Read(1,250,.025,matrix,fov,nearPlane));assert(close(matrix[3].x,.3)&&close(fov,30)&&close(matrix[1].x,.6)&&close(nearPlane,.02));
b.Push(1,149,270,{9,9,9},forward,up,80,.1, -1, {}); // out-of-order packet cannot rewind the view
assert(b.Read(1,275,.025,matrix,fov,nearPlane)&&close(matrix[3].x,.3));
for(int i=0;i<1000;i++)assert(b.Read(1,300,.016,matrix,fov,nearPlane)&&close(matrix[3].x,.3)); // no integration drift
assert(!b.Read(1,501,.01,matrix,fov,nearPlane)); // gap beyond 350 ms, no stale head approximation
assert(!b.Read(2,250,.01,matrix,fov,nearPlane)); // target switch never inherits previous pose
b.Push(2,200,600,{1,2,3},forward,up,75,.1, -1, {});assert(b.Read(2,600,0,matrix,fov,nearPlane)&&close(matrix[3].x,1));
b.Clear();assert(!b.Read(2,600,0,matrix,fov,nearPlane));
b.Push(1,700,700,pos,forward,up,12,.02,22,{1,0,1});
assert(b.Read(1,700,0,matrix,fov,nearPlane)&&close(fov,12)&&b.m_ScopeWeapon==RplId(22));
b.Push(1,701,710,pos,forward,up,60,.02,22,{nan,0,1});
assert(b.m_Stamp==700&&b.m_ScopeData[0]==1);
b.Push(1,702,720,pos,forward,up,60,.02,-1,{});
assert(b.Read(1,720,0,matrix,fov,nearPlane)&&close(fov,60)&&b.m_ScopeWeapon==RplId(-1));
assert(!ERR_ObserverScope::Valid(-1,{1,0,0}));
assert(!ERR_ObserverScope::Valid(22,{1.5,0,0}));
assert(!ERR_ObserverScope::Valid(22,{1,91,0}));
assert(!ERR_ObserverScope::Valid(22,{1,0,2}));
assert(!ERR_ObserverViewRules::Fresh(100,101));
// 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}){
Buffer smooth;Baseline old;int packet=0;float prevNew=0,prevOld=0,lastNewStep=0,lastOldStep=0;
float newVariation=0,oldVariation=0;int delay[]={0,25,5,35,0,15};
for(float t=0;t<2500;t+=frame){
while(packet<=50&&packet*50+delay[packet%6]<=t){
if(packet%13!=12){ // occasional single-packet loss
vector location{packet*.05f,1.6,0};
smooth.Push(1,70000+packet*50,10000+t,location,forward,up,60,.02,-1,{});
old.Push(1,70000+packet*50,10000+t,location,forward,up,60,.02,-1,{});
}
packet++;
}
assert(smooth.Read(1,10000+t,frame/1000,matrix,fov,nearPlane));float nextNew=matrix[3].x;
assert(old.Read(1,10000+t,frame/1000,matrix,fov,nearPlane));float nextOld=matrix[3].x;
float newStep=nextNew-prevNew,oldStep=nextOld-prevOld;
assert(newStep>=-0.0001); // no timeline rewind
if(t>500){newVariation+=fabs(newStep-lastNewStep);oldVariation+=fabs(oldStep-lastOldStep);}
prevNew=nextNew;prevOld=nextOld;lastNewStep=newStep;lastOldStep=oldStep;
assert(smooth.m_Samples.Count()<=12);
}
std::cout<<"frame_ms="<<frame<<" buffered_step_variation="<<newVariation<<" baseline="<<oldVariation<<"\n";
assert(newVariation<oldVariation*.25f);
}
// Holding an unrendered stream stays bounded; opening it resumes near live.
Buffer parked;
for(int i=0;i<1000;i++)parked.Push(1,i*50,i*50,pos,forward,up,60,.02,-1,{});
assert(parked.m_Samples.Count()==12);
assert(parked.Read(1,49950,0,matrix,fov,nearPlane)&&close(matrix[3].x,pos.x));
// Scope changes, reacquisition after expiry and target changes snap to their
// own pose instead of blending through another sight or another player.
parked.Push(1,50000,50400,{2,1,0},forward,up,30,.02,-1,{});
assert(parked.Read(1,50400,0,matrix,fov,nearPlane)&&close(matrix[3].x,2));
parked.Clear();assert(parked.m_Samples.IsEmpty()&&parked.m_ScopeWeapon==RplId(-1));
}
''')
print('PASS: actual view buffer validates samples, follows position/roll/FOV, rejects stale/foreign/reordered data, buffers jitter without drift, bounds memory and expires gaps')
@@ -0,0 +1,85 @@
"""Production relay authority/leases and delivery, with owner RPC/native data doubled."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
def adapt(s):
s=s.replace('protected ', '').replace('null','nullptr')
for cls in ['ERR_GameMode','ERR_PlayerLife','SCR_PlayerController','RplComponent']:
s=re.sub(r'\b'+cls+r' (\w+)\b',cls+r'* \1',s)
for obj in ['mode','life','watcher','source','component']:s=s.replace(obj+'.',obj+'->')
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('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('])
run(r'''
#include <cassert>
#include <vector>
#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;}};
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 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;
namespace ERR_Actors {bool Alive(IEntity a){return a&&a->alive;}bool CanFight(IEntity a){return Alive(a)&&a->conscious;}}
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'''};
struct ERR_ObserverViewRules {'''+adapt(body(p,'static bool Sample('))+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;
struct Game {float now=1000;SCR_PlayerController* local=nullptr;std::map<int,SCR_PlayerController*> controllers;
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;}
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 Buffer{int pushes=0;void Push(RplId,int,float,vector,vector,vector,float,float,RplId,vector){pushes++;}};
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;
Array<SCR_PlayerController*> m_ERRViewWatchers;Buffer m_ERRViewBuffer;
int GetPlayerId(){return id;}IEntity GetControlledEntity(){return controlled;}static SCR_PlayerController* Cast(SCR_PlayerController* p){return p;}
void SendLease(){leases++;}
void ERR_DeliverPlayerView(RplId,int,vector,vector,vector,float,float,RplId,vector){deliveries++;}
'''+methods+r'''
};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;Actor target,dead,revived;dead.alive=false;
ERR_PlayerLife sourceLife;mode.roster.lives[&target]=&sourceLife;
SCR_PlayerController source,watcher,stranger;source.id=1;watcher.id=2;stranger.id=3;
source.controlled=&target;watcher.controlled=&dead;watcher.m_ERRServerObserved=&target;watcher.m_ERRObservedId={11};
game.controllers={{1,&source},{2,&watcher},{3,&stranger}};
assert(!watcher.ERR_ViewRecipient(&target,game.now)); // no lease
watcher.ERR_RequestPlayerView();assert(source.m_ERRViewWatchers.Count()==1&&source.leases==1);
watcher.ERR_RequestPlayerView();assert(source.leases==1); // bounded requests
vector position{0,1.6,0},forward{0,0,1},up{0,1,0};
source.ERR_SubmitPlayerView(1,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==1);
source.ERR_SubmitPlayerView(2,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==1); // packet rate bound
game.now+=50;source.ERR_SubmitPlayerView(1,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==1); // replay
source.ERR_SubmitPlayerView(2,{999,0,0},forward,up,60,.0125, -1, {});assert(watcher.deliveries==1);
source.ERR_SubmitPlayerView(2,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2);
game.now+=50;source.ERR_SubmitPlayerView(3,position,forward,up,12,.0125,22,{99,0,0});assert(watcher.deliveries==2&&source.m_ERRViewInputSequence==2); // malformed scope cannot advance sequence or deliver
game.now+=50;watcher.controlled=&revived;source.ERR_SubmitPlayerView(3,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2&&source.m_ERRViewWatchers.Count()==0); // no stream to recovered players
watcher.controlled=&dead;game.now+=500;watcher.ERR_RequestPlayerView();assert(source.m_ERRViewWatchers.Count()==1);
target.faction=2;game.now+=50;source.ERR_SubmitPlayerView(4,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2);target.faction=1;
game.now+=500;watcher.ERR_RequestPlayerView();game.now+=1600;source.ERR_SubmitPlayerView(5,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2&&source.m_ERRViewWatchers.Count()==0); // lease expired
watcher.ERR_RequestPlayerView();watcher.m_ERRServerObserved=nullptr;game.now+=50;source.ERR_SubmitPlayerView(6,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2); // target switch
watcher.m_ERRServerObserved=&target;game.now+=500;watcher.ERR_RequestPlayerView();game.controllers[1]=&stranger;source.ERR_SubmitPlayerView(7,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2);game.controllers[1]=&source;
sourceLife.valid=false;game.now+=50;source.ERR_SubmitPlayerView(7,position,forward,up,60,.0125, -1, {});assert(watcher.deliveries==2);sourceLife.valid=true;
game.local=&watcher;watcher.ERR_ReceivePlayerView({11},1,position,forward,up,60,.0125, -1, {});assert(watcher.m_ERRViewBuffer.pushes==1);
watcher.ERR_ReceivePlayerView({22},2,position,forward,up,60,.0125, -1, {});assert(watcher.m_ERRViewBuffer.pushes==1);
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=&source;source.ERR_EnableViewPublisher();assert(source.m_ERRViewPublishUntil==game.now+2000);
}
''')
print('PASS: actual relay rejects noncanonical sources, live/foreign/stale viewers, invalid/replayed/fast packets; bounds leases and owner-only target delivery')
@@ -0,0 +1,4 @@
22:11:34.306 SCRIPT : Module: Workbench; loaded 18x files; 36x classes; used 125K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: 90ce0460
22:11:34.307 SCRIPT : Module: GameLib; loaded 466x files; 648x classes; used 828K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: d66caacb
22:11:36.902 SCRIPT : Module: WorkbenchGame; loaded 186x files; 657x classes; used 2754K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE"; CRC32: 5613aa11
22:11:36.910 SCRIPT : Module: Game; loaded 6732x files; 13860x classes; used 42570K of static memory; defines: "ENF_WB,WORKBENCH,PLATFORM_WINDOWS,ENABLE_DIAG,ENF_ENABLE_VHC_OVERRIDE,ENF_ENABLE_RPLCOMPONENT_IN_GAMECODE,ENABLE_BASE_DESTRUCTION,BUDGET_OPTIMIZATION_CHECKS,SHOW_DISCLAIMER,DEBUG,RESPAWN_TIMER_COMPONENT_DEBUG,GAME_MODE_DEBUG,DEBUG_CONFIGURABLE_DIALOGS,DISABLE_WEAPON_SWITCHING,SCRIPTED_AIM_MODIFIER_DEBUG,WCS_DEFINES_ARMAMENTS,WCS_DEFINES_SPACECORE"; CRC32: 94b9e85d
@@ -0,0 +1,507 @@
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c
@@ -95,6 +95,7 @@
protected ref ERR_ObserverHeadVisibility m_ViewHead = new ERR_ObserverHeadVisibility();
protected float m_OrbitFov, m_OrbitNear;
protected ref ERR_ObserverScopeDisplay m_ScopeDisplay = new ERR_ObserverScopeDisplay();
+ protected ref ERR_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay();
protected bool m_InputContextActive;
bool InputContextActive() { return m_InputContextActive; }
bool IsPlayerView() { return m_PlayerView; }
@@ -122,11 +123,11 @@
m_OrbitNear = GetCameraEntity().GetNearPlane();
return true;
}
- override void EOnCameraExit() { m_ScopeDisplay.Close(); m_ViewHead.Restore(); }
+ override void EOnCameraExit() { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); }
override void EOnCameraFrame(SCR_ManualCameraParam param)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
- if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
+ if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5);
IEntity target = controller.ERR_ObservedEntity();
@@ -134,7 +135,6 @@
if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane))
{
param.fov = fov; GetCameraEntity().SetNearPlane(nearPlane);
- m_ViewHead.Update(target, param.transform[3]);
RplId scopeWeapon; vector scopeData;
controller.ERR_ReadScope(scopeWeapon, scopeData);
SCR_2DOpticsComponent optic = ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData);
@@ -144,13 +144,31 @@
RplComponent weaponRpl = RplComponent.Cast(Replication.FindItem(scopeWeapon));
if (weaponRpl) weapon = weaponRpl.GetEntity();
}
- if (!param.isManualInputEnabled || !optic || !weapon || !m_ScopeDisplay.RenderScope(optic, weapon, fov, scopeData)) m_ScopeDisplay.Close();
+ vector eye[4], geometry;
+ float eyeFov, eyeNear, pipFov, vignette;
+ // Read both timelines every camera frame, including the 2D fallback, so
+ // enabling PiP cannot resume an eye camera from a different time.
+ bool haveEye = controller.ERR_ReadPlayerEye(target, param.timeSlice, eye, eyeFov, eyeNear, pipFov, geometry, vignette);
+ bool pip = haveEye && param.isManualInputEnabled && optic && weapon;
+ if (pip) pip = m_PIPDisplay.Render(optic, param.transform, eye, fov, pipFov, scopeData, geometry, vignette);
+ if (pip)
+ {
+ m_ScopeDisplay.Close();
+ Math3D.MatrixCopy(eye, param.transform);
+ param.fov = eyeFov; GetCameraEntity().SetNearPlane(eyeNear);
+ }
+ else
+ {
+ m_PIPDisplay.Close();
+ if (!param.isManualInputEnabled || !optic || !weapon || !m_ScopeDisplay.RenderScope(optic, weapon, fov, scopeData)) m_ScopeDisplay.Close();
+ }
+ m_ViewHead.Update(target, param.transform[3]);
param.isDirty = true;
return;
}
// No estimated head camera: corpses, streaming gaps and expired samples use
// the stable follow view until an authenticated live camera stream arrives.
- m_ScopeDisplay.Close();
+ m_PIPDisplay.Close(); m_ScopeDisplay.Close();
m_ViewHead.Restore();
param.fov = m_OrbitFov; GetCameraEntity().SetNearPlane(m_OrbitNear);
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c
@@ -2,6 +2,18 @@
// spectators of that body. No client-supplied position affects gameplay.
class ERR_ObserverViewRules
{
+ // One unreliable packet contains both cameras, so loss/reordering cannot
+ // pair a new eye pose with an old aiming pose. Four vectors for ordinary/2D,
+ // nine for PiP: scope pose/lens, eye pose/lens, native lens geometry.
+ static bool Packet(array<vector> packet)
+ {
+ if (!packet || (packet.Count() != 4 && packet.Count() != 9)) return false;
+ if (!Sample(packet[0], packet[1], packet[2], packet[3][0], packet[3][1])) return false;
+ if (packet.Count() == 4) return packet[3][2] == 0;
+ if (!(packet[3][2] >= 0.01 && packet[3][2] <= 150)) return false;
+ if (!Sample(packet[4], packet[5], packet[6], packet[7][0], packet[7][1])) return false;
+ return packet[7][2] >= 0 && packet[7][2] <= 100 && Math.AbsFloat(packet[8][0]) <= 90 && Math.AbsFloat(packet[8][1]) <= 90 && packet[8][2] >= 0.001 && packet[8][2] <= 100;
+ }
// The body and camera arrive through independently timed replication streams.
// Keep the sampled world axes and world-axis eye offset together. Applying
// the proxy body's rotation to delayed body-local aim adds its yaw error to
@@ -32,6 +44,8 @@
int Stamp;
vector Position, Forward, Up;
float Fov, Near;
+ float PipFov, Vignette;
+ vector Geometry;
}
class ERR_ObserverViewBuffer
{
@@ -41,15 +55,23 @@
protected vector m_ScopeData;
protected float m_Received = -1;
protected float m_Playback, m_ReadAt;
+ protected float m_PipFov, m_Vignette;
+ protected vector m_Geometry;
protected ref array<ref ERR_ObserverViewSample> m_Samples = {};
void Scope(out RplId weapon, out vector data) { weapon = m_ScopeWeapon; data = m_ScopeData; }
+ bool Accepts(RplId target, int stamp) { return target != m_Target || stamp > m_Stamp; }
+ float Playback() { return m_Playback; }
+ void Align(float playback, float now) { m_Playback = playback; m_ReadAt = now; }
+ void PIP(out float fov, out vector geometry, out float vignette)
+ { fov = m_PipFov; geometry = m_Geometry; vignette = m_Vignette; }
void Clear()
{
m_Target = RplId.Invalid(); m_Stamp = -1; m_Received = -1;
m_ScopeWeapon = RplId.Invalid(); m_ScopeData = vector.Zero;
m_Samples.Clear();
- }
- void Push(RplId target, int stamp, float now, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
+ m_PipFov = 0; m_Vignette = 0; m_Geometry = vector.Zero;
+ }
+ void Push(RplId target, int stamp, float now, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData, float pipFov = 0, vector geometry = "0 0 0", float vignette = 0)
{
if (!ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
if (!ERR_ObserverViewRules.Sample(position, forward, up, fov, nearPlane)) return;
@@ -62,6 +84,7 @@
ERR_ObserverViewSample sample = new ERR_ObserverViewSample();
sample.Stamp = stamp; sample.Position = position; sample.Forward = forward; sample.Up = up;
sample.Fov = fov; sample.Near = nearPlane;
+ sample.PipFov = pipFov; sample.Geometry = geometry; sample.Vignette = vignette;
m_Samples.Insert(sample);
// Bounded even when first-person view is not being rendered.
while (m_Samples.Count() > 12) m_Samples.Remove(0);
@@ -94,12 +117,17 @@
Math3D.DirectionAndUpMatrix(forward, up, local);
local[3] = vector.Lerp(start.Position, end.Position, blend);
fov = Math.Lerp(start.Fov, end.Fov, blend); nearPlane = end.Near;
+ m_PipFov = Math.Lerp(start.PipFov, end.PipFov, blend);
+ m_Geometry = vector.Lerp(start.Geometry, end.Geometry, blend);
+ m_Vignette = Math.Lerp(start.Vignette, end.Vignette, blend);
return true;
}
}
modded class SCR_PlayerController
{
protected ref ERR_ObserverViewBuffer m_ERRViewBuffer = new ERR_ObserverViewBuffer();
+ protected ref ERR_ObserverViewBuffer m_ERREyeBuffer = new ERR_ObserverViewBuffer();
+ protected bool m_ERRHasPIP;
protected ref array<SCR_PlayerController> m_ERRViewWatchers = {};
protected float m_ERRViewServerRequestAt = -1000;
protected float m_ERRViewRequestAt = -1000, m_ERRViewLeaseUntil = -1;
@@ -113,6 +141,19 @@
return mode && mode.Active() && now <= m_ERRViewLeaseUntil && target && target == m_ERRServerObserved && ERR_Actors.CanFight(target) && mode.Players().CurrentActor(target) && SCR_FactionManager.SGetFaction(target) == mode.PlayerFaction() && GetGame().GetPlayerManager().GetPlayerController(GetPlayerId()) == this && ERR_ObserverRules.View(m_ERRObserverReason, true, ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
}
void ERR_ReadScope(out RplId weapon, out vector data) { m_ERRViewBuffer.Scope(weapon, data); }
+ bool ERR_ReadPlayerEye(IEntity target, float seconds, out vector transform[4], out float fov, out float nearPlane, out float pipFov, out vector geometry, out float vignette)
+ {
+ if (!m_ERRHasPIP || this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
+ vector pose[4], world[4];
+ float now = GetGame().GetWorld().GetWorldTime();
+ // Use the exact aiming playhead even after a streamed optic temporarily
+ // fails resolution and this eye buffer is not read for several frames.
+ m_ERREyeBuffer.Align(m_ERRViewBuffer.Playback(), now);
+ if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, pose, fov, nearPlane)) return false;
+ target.GetWorldTransform(world); ERR_ObserverViewRules.Decode(world, pose, transform);
+ m_ERRViewBuffer.PIP(pipFov, geometry, vignette);
+ return true;
+ }
bool ERR_ReadPlayerView(IEntity target, float seconds, out vector transform[4], out float fov, out float nearPlane)
{
if (this != GetGame().GetPlayerController() || !target || !ERR_Actors.CanFight(target)) return false;
@@ -171,20 +212,37 @@
PlayerCamera camera = GetPlayerCamera();
CameraManager manager = GetGame().GetCameraManager();
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor) || !camera || !manager || manager.CurrentCamera() != camera) return;
- vector world[4], view[4], pose[4];
+ vector world[4], view[4], pose[4], eye[4];
actor.GetWorldTransform(world);
camera.GetWorldCameraTransform(view);
float fov = camera.GetVerticalFOV(), nearPlane = camera.GetNearPlane();
+ float eyeFov = fov;
+ ERR_ObserverViewRules.Encode(world, view, eye);
RplId scopeWeapon; vector scopeData;
ERR_ObserverScope.Sample(actor, view, fov, scopeWeapon, scopeData);
ERR_ObserverViewRules.Encode(world, view, pose);
if (!ERR_ObserverViewRules.Sample(pose[3], pose[2], pose[1], fov, nearPlane)) return;
- Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, pose[3], pose[2], pose[1], fov, nearPlane, scopeWeapon, scopeData);
+ array<vector> packet = {pose[3], pose[2], pose[1], Vector(fov, nearPlane, 0)};
+ SCR_2DPIPSightsComponent pip = SCR_2DPIPSightsComponent.Cast(ERR_ObserverScope.Resolve(actor, scopeWeapon, scopeData));
+ if (pip && pip.IsPIPEnabled() && pip.GetPIPCamera())
+ {
+ packet[3] = Vector(fov, nearPlane, pip.GetPIPCamera().GetVerticalFOV());
+ packet.Insert(eye[3]); packet.Insert(eye[2]); packet.Insert(eye[1]);
+ packet.Insert(Vector(eyeFov, nearPlane, pip.ERR_ObserverPIPVignette(actor, camera))); packet.Insert(pip.ERR_ObserverPIPGeometry());
+ if (!ERR_ObserverViewRules.Packet(packet))
+ {
+ // An unusual addon lens must not suppress an otherwise valid 2D view.
+ packet.Resize(4); packet[3] = Vector(fov, nearPlane, 0);
+ }
+ }
+ if (!ERR_ObserverViewRules.Packet(packet)) return;
+ Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, packet, scopeWeapon, scopeData);
}
[RplRpc(RplChannel.Unreliable, RplRcver.Server)]
- protected void ERR_SubmitPlayerView(int sequence, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
- {
- if (!Replication.IsServer() || !ERR_ObserverScope.Valid(scopeWeapon, scopeData) || !ERR_ObserverViewRules.Sample(position, forward, up, fov, nearPlane)) return;
+ protected void ERR_SubmitPlayerView(int sequence, array<vector> packet, RplId scopeWeapon, vector scopeData)
+ {
+ if (!Replication.IsServer() || !ERR_ObserverScope.Valid(scopeWeapon, scopeData) || !ERR_ObserverViewRules.Packet(packet)) return;
+ if (packet.Count() == 9 && scopeWeapon == RplId.Invalid()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
if (!mode || !mode.Active() || !ERR_Actors.CanFight(actor)) return;
@@ -200,23 +258,38 @@
{
SCR_PlayerController watcher = m_ERRViewWatchers[i];
if (!watcher || !watcher.ERR_ViewRecipient(actor, now)) { m_ERRViewWatchers.Remove(i); continue; }
- watcher.ERR_DeliverPlayerView(component.Id(), now, position, forward, up, fov, nearPlane, scopeWeapon, scopeData);
- }
- }
- void ERR_DeliverPlayerView(RplId target, int stamp, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
- {
- Rpc(ERR_ReceivePlayerViewPacket, target, stamp, position, forward, up, Vector(fov, nearPlane, 0), scopeWeapon, scopeData);
+ watcher.ERR_DeliverPlayerView(component.Id(), now, packet, scopeWeapon, scopeData);
+ }
+ }
+ void ERR_DeliverPlayerView(RplId target, int stamp, array<vector> packet, RplId scopeWeapon, vector scopeData)
+ {
+ Rpc(ERR_ReceivePlayerViewPacket, target, stamp, packet, scopeWeapon, scopeData);
}
[RplRpc(RplChannel.Unreliable, RplRcver.Owner)]
- protected void ERR_ReceivePlayerViewPacket(RplId target, int stamp, vector position, vector forward, vector up, vector lens, RplId scopeWeapon, vector scopeData)
- {
- ERR_ReceivePlayerView(target, stamp, position, forward, up, lens[0], lens[1], scopeWeapon, scopeData);
- }
- protected void ERR_ReceivePlayerView(RplId target, int stamp, vector position, vector forward, vector up, float fov, float nearPlane, RplId scopeWeapon, vector scopeData)
+ protected void ERR_ReceivePlayerViewPacket(RplId target, int stamp, array<vector> packet, RplId scopeWeapon, vector scopeData)
+ {
+ ERR_ReceivePlayerView(target, stamp, packet, scopeWeapon, scopeData);
+ }
+ protected void ERR_ReceivePlayerView(RplId target, int stamp, array<vector> packet, RplId scopeWeapon, vector scopeData)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (this != GetGame().GetPlayerController() || target == RplId.Invalid() || target != m_ERRObservedId || !mode || !ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()))) return;
- m_ERRViewBuffer.Push(target, stamp, GetGame().GetWorld().GetWorldTime(), position, forward, up, fov, nearPlane, scopeWeapon, scopeData);
+ if (!ERR_ObserverViewRules.Packet(packet) || !ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
+ if (packet.Count() == 9 && scopeWeapon == RplId.Invalid()) return;
+ // Reject before updating either stream or PiP metadata.
+ if (!m_ERRViewBuffer.Accepts(target, stamp)) return;
+ float now = GetGame().GetWorld().GetWorldTime();
+ bool pip = packet.Count() == 9;
+ if (pip != m_ERRHasPIP) { m_ERRViewBuffer.Clear(); m_ERREyeBuffer.Clear(); }
+ vector geometry; float vignette;
+ if (pip) { geometry = packet[8]; vignette = packet[7][2]; }
+ m_ERRViewBuffer.Push(target, stamp, now, packet[0], packet[1], packet[2], packet[3][0], packet[3][1], scopeWeapon, scopeData, packet[3][2], geometry, vignette);
+ m_ERRHasPIP = pip;
+ if (pip)
+ {
+ m_ERREyeBuffer.Push(target, stamp, now, packet[4], packet[5], packet[6], packet[7][0], packet[7][1], scopeWeapon, scopeData);
+ }
+ else m_ERREyeBuffer.Clear();
}
}
modded class ArmaReforgerScripted
--- before/addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c
+++ after/addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c
@@ -1,5 +1,5 @@
-// Presentation only. Scoped spectators use a private full-screen optic display;
-// never activate the remote sight (which registers weapon input and changes HUD).
+// Presentation only. PiP spectators use the lens renderer; other scopes use a
+// private full-screen display. Never activate remote ADS input or its HUD.
class ERR_ObserverScope
{
static bool Valid(RplId weapon, vector data)
@@ -132,3 +132,123 @@
}
void ~ERR_ObserverScopeDisplay() { Close(); }
}
+
+// Use only the native PiP rendering lifecycle, never OnSightADSActivated or
+// HandleSightActivation (those bind weapon input and change the player's HUD).
+modded class SCR_2DPIPSightsComponent
+{
+ protected bool m_ERRObserverPIPOwned;
+ vector ERR_ObserverPIPGeometry() { return Vector(m_fParallaxX, m_fParallaxY, m_fEdgeScale); }
+ float ERR_ObserverPIPVignette(IEntity actor, CameraBase camera)
+ {
+ SCR_2DPIPSightsComponentClass settings = SCR_2DPIPSightsComponentClass.Cast(GetComponentData(GetOwner()));
+ ChimeraCharacter character = ChimeraCharacter.Cast(actor);
+ if (!settings || !camera || !character || !character.GetCharacterController()) return 0;
+ BaseWeaponManagerComponent weapons = character.GetCharacterController().GetWeaponManagerComponent();
+ vector eye[4], sight[4]; float ignoredFov;
+ if (!weapons || !weapons.GetCurrentSightsCameraTransform(sight, ignoredFov)) return settings.GetBasicParallax();
+ camera.GetLocalTransform(eye);
+ float alignment = vector.Dot(eye[2], sight[2]);
+ if (Math.AbsFloat(alignment) < 0.001) return settings.GetMaxParallax();
+ // The same eye-to-reticle-plane distance used by native UpdateScopeMaterial.
+ float distance = Math.AbsFloat(vector.Dot(sight[3] - eye[3], sight[2]) / alignment) - settings.GetCenterDistance();
+ float power = settings.GetDistanceMoveNear();
+ if (distance > 0) power = settings.GetDistanceMoveFar();
+ float falloff = 1 - 1 / Math.Log2(power * Math.AbsFloat(distance) + 2);
+ return Math.Clamp(falloff * 100, settings.GetBasicParallax(), settings.GetMaxParallax());
+ }
+ bool ERR_ObserverPIPBegin()
+ {
+ if (m_ERRObserverPIPOwned) return m_bPIPIsEnabled && m_PIPCamera && m_pMaterial;
+ if (SCR_Global.IsScope2DEnabled() || IsPIPActive() || m_bPIPIsEnabled || !m_pMaterial || m_fScopeRadius <= 0) return false;
+ if (m_iReticleScaleIndex < 0 || m_iReticleColorIndex < 0 || m_iEdgeMinIndex < 0 || m_iEdgeMaxIndex < 0 || m_iVignettePowerIndex < 0) return false;
+ SCR_2DPIPSightsComponentClass data = SCR_2DPIPSightsComponentClass.Cast(GetComponentData(GetOwner()));
+ // Never reuse the main camera slot or a camera owned by another sight.
+ if (!data || data.GetCameraIndex() <= 0 || data.GetCameraIndex() > 31) return false;
+ m_ERRObserverPIPOwned = true;
+ SetPIPEnabled(true);
+ if (!m_bPIPIsEnabled || !m_PIPCamera || !m_wRenderTargetTextureWidget)
+ { ERR_ObserverPIPEnd(); return false; }
+ return true;
+ }
+ void ERR_ObserverPIPEnd()
+ {
+ if (!m_ERRObserverPIPOwned) return;
+ m_ERRObserverPIPOwned = false;
+ if (m_bPIPIsEnabled) SetPIPEnabled(false);
+ else Destroy(); // also dispose a partially allocated camera/layout
+ }
+ bool ERR_ObserverPIPRender(vector scopeView[4], vector eyeView[4], float scopeFov, float pipFov, vector data, vector geometry, float vignette)
+ {
+ if (!m_ERRObserverPIPOwned || !m_bPIPIsEnabled || !m_PIPCamera || !m_pMaterial || !GetOwner() || GetOwner().IsDeleted()) return false;
+ if (!(pipFov >= 0.01 && pipFov <= 150) || !(scopeFov >= 1 && scopeFov <= 150)) return false;
+ SCR_2DPIPSightsComponentClass settings = SCR_2DPIPSightsComponentClass.Cast(GetComponentData(GetOwner()));
+ if (!settings) return false;
+ float reticlePIPScale = settings.GetReticlePIPScale();
+ if (!(reticlePIPScale > 0)) return false;
+ // Camera pose and lens geometry come from the owner. Never add proxy weapon
+ // animation/parallax a second time to an already sampled aiming direction.
+ m_PIPCamera.SetWorldTransform(scopeView);
+ m_PIPCamera.SetVerticalFOV(pipFov);
+ m_PIPCamera.ApplyProps(settings.GetCameraIndex());
+ UpdateHDR();
+ float angularSize, portion, baseZoom;
+ GetReticleData(angularSize, portion, baseZoom);
+ float reticleFov = scopeFov;
+ if (baseZoom > 0) reticleFov = CalculateZoomFOV(baseZoom);
+ else if (baseZoom < 0) reticleFov = -baseZoom;
+ float reticleScale = 1;
+ if (angularSize > 0) reticleScale = reticleFov * (pipFov / scopeFov) * portion / angularSize;
+ float parallaxX = geometry[0], parallaxY = geometry[1];
+ float objective = m_fObjectiveFov * m_fObjectiveScale;
+ float vignetteScale;
+ if (objective > 0) vignetteScale = settings.GetVignetteParallaxScale() / objective;
+ // Lens shading was sampled on the owner too. Native UpdateScopeMaterial
+ // would consult the spectator's own corpse/weapon manager instead.
+ m_pMaterial.SetParamByIndex(m_iVignetteCenterXIndex, -parallaxX * vignetteScale / pipFov);
+ m_pMaterial.SetParamByIndex(m_iVignetteCenterYIndex, -parallaxY * vignetteScale / pipFov);
+ m_pMaterial.SetParamByIndex(m_iVignettePowerIndex, vignette);
+ m_pMaterial.SetParamByIndex(m_iEdgeCenterXIndex, parallaxX * reticlePIPScale / pipFov);
+ m_pMaterial.SetParamByIndex(m_iEdgeCenterYIndex, parallaxY * reticlePIPScale / pipFov);
+ float edgeSize = objective * geometry[2] / pipFov;
+ m_pMaterial.SetParamByIndex(m_iEdgeMinIndex, 0.5 * reticlePIPScale * edgeSize * edgeSize * settings.GetObjectivePIPEdgeMin());
+ m_pMaterial.SetParamByIndex(m_iEdgeMaxIndex, 0.5 * reticlePIPScale * edgeSize * edgeSize * settings.GetObjectivePIPEdgeMax());
+ m_pMaterial.SetParamByIndex(m_iReticleScaleIndex, reticleScale / reticlePIPScale);
+ m_pMaterial.SetParamByIndex(m_iReticleOffsetXIndex, reticleScale * (parallaxX + GetReticleOffsetX()) / pipFov);
+ m_pMaterial.SetParamByIndex(m_iReticleOffsetYIndex, reticleScale * (parallaxY + data[1]) / pipFov);
+ Color color = settings.GetReticleColor();
+ if (data[2] == 1 && settings.HasIllumination()) color = settings.GetReticleIlluminationColor();
+ float rgba[4] = {color.R(), color.G(), color.B(), color.A()};
+ m_pMaterial.SetParamByIndex(m_iReticleColorIndex, rgba);
+ return true;
+ }
+}
+
+class ERR_ObserverPIPDisplay
+{
+ protected SCR_2DPIPSightsComponent m_Optic;
+ protected SCR_2DPIPSightsComponent m_FailedOptic;
+ protected float m_RetryAt;
+ bool Render(SCR_2DOpticsComponent optic, vector scopeView[4], vector eyeView[4], float scopeFov, float pipFov, vector data, vector geometry, float vignette)
+ {
+ SCR_2DPIPSightsComponent pip = SCR_2DPIPSightsComponent.Cast(optic);
+ if (!pip || SCR_Global.IsScope2DEnabled()) { Close(); return false; }
+ float now = GetGame().GetWorld().GetWorldTime();
+ if (m_Optic != pip)
+ {
+ Close();
+ if (m_FailedOptic == pip && now < m_RetryAt) return false;
+ if (!pip.ERR_ObserverPIPBegin()) { m_FailedOptic = pip; m_RetryAt = now + 2000; return false; }
+ m_Optic = pip; m_FailedOptic = null;
+ }
+ if (m_Optic.ERR_ObserverPIPRender(scopeView, eyeView, scopeFov, pipFov, data, geometry, vignette)) return true;
+ Close(); m_FailedOptic = pip; m_RetryAt = now + 2000;
+ return false;
+ }
+ void Close()
+ {
+ if (m_Optic && m_Optic.GetOwner() && !m_Optic.GetOwner().IsDeleted()) m_Optic.ERR_ObserverPIPEnd();
+ m_Optic = null;
+ }
+ void ~ERR_ObserverPIPDisplay() { Close(); }
+}
--- before/tests/test_observer_view_relay.py
+++ after/tests/test_observer_view_relay.py
@@ -3,7 +3,7 @@
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
def adapt(s):
- s=s.replace('protected ', '').replace('null','nullptr')
+ s=s.replace('vector geometry; float vignette;', 'vector geometry; float vignette=0;').replace('protected ', '').replace('null','nullptr').replace('array<vector> packet', 'Array<vector>* packet').replace('packet.Count()', 'packet->Count()').replace('packet[', '(*packet)[')
for cls in ['ERR_GameMode','ERR_PlayerLife','SCR_PlayerController','RplComponent']:
s=re.sub(r'\b'+cls+r' (\w+)\b',cls+r'* \1',s)
for obj in ['mode','life','watcher','source','component']:s=s.replace(obj+'.',obj+'->')
@@ -33,22 +33,26 @@
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'''};
-struct ERR_ObserverViewRules {'''+adapt(body(p,'static bool Sample('))+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_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;
struct Game {float now=1000;SCR_PlayerController* local=nullptr;std::map<int,SCR_PlayerController*> controllers;
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;}
-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 Buffer{int pushes=0;void Push(RplId,int,float,vector,vector,vector,float,float,RplId,vector){pushes++;}};
+
+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 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;
- Array<SCR_PlayerController*> m_ERRViewWatchers;Buffer m_ERRViewBuffer;
+ Array<SCR_PlayerController*> m_ERRViewWatchers;Buffer m_ERRViewBuffer,m_ERREyeBuffer;bool m_ERRHasPIP=false;
int GetPlayerId(){return id;}IEntity GetControlledEntity(){return controlled;}static SCR_PlayerController* Cast(SCR_PlayerController* p){return p;}
void SendLease(){leases++;}
- void ERR_DeliverPlayerView(RplId,int,vector,vector,vector,float,float,RplId,vector){deliveries++;}
+ 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);}
'''+methods+r'''
};
int main(){
@@ -79,6 +83,14 @@
watcher.ERR_ReceivePlayerView({22},2,position,forward,up,60,.0125, -1, {});assert(watcher.m_ERRViewBuffer.pushes==1);
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);
+ 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);
+ 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);
+ dual.pop_back();assert(!ERR_ObserverViewRules::Packet(&dual));assert(!ERR_ObserverViewRules::Packet(nullptr));
mode.active=true;game.local=&source;source.ERR_EnableViewPublisher();assert(source.m_ERRViewPublishUntil==game.now+2000);
}
''')
--- before/tests/test_observer_view_buffer.py
+++ after/tests/test_observer_view_buffer.py
@@ -3,13 +3,14 @@
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c'
def adapt(s):
+ s=s.replace('vector geometry = "0 0 0"', 'vector geometry = {}')
for name in ['ERR_ObserverScope','ERR_ObserverViewRules','Math','Math3D','vector','RplId']:s=s.replace(name+'.',name+'::')
s=s.replace('out vector','vector').replace('out float ','float& ')
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('])
-methods='\n'.join(adapt(body(p,k)) for k in ['void Clear(', 'void Push(', 'bool Read('])
+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('])
run(r'''
@@ -30,9 +31,9 @@
struct RplId {int value=-1;RplId(int v=-1):value(v){}static RplId Invalid(){return{};}bool operator==(RplId a){return value==a.value;}bool operator!=(RplId a){return value!=a.value;}};
struct ERR_ObserverViewRules {'''+rules+r'''};
struct ERR_ObserverScope {'''+adapt(body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c','static bool Valid('))+r'''};
-struct ERR_ObserverViewSample {int Stamp;vector Position,Forward,Up;float Fov,Near;};
+struct ERR_ObserverViewSample {int Stamp;vector Position,Forward,Up;float Fov,Near,PipFov=0,Vignette=0;vector Geometry;};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T v){this->push_back(v);}void Remove(int i){this->erase(this->begin()+i);}};
-struct Buffer {RplId m_ScopeWeapon;vector m_ScopeData;RplId m_Target;int m_Stamp=-1;float m_Received=-1,m_Playback=0,m_ReadAt=0;Array<ERR_ObserverViewSample*> m_Samples;
+struct Buffer {RplId m_ScopeWeapon;vector m_ScopeData;RplId m_Target;int m_Stamp=-1;float m_Received=-1,m_Playback=0,m_ReadAt=0,m_PipFov=0,m_Vignette=0;vector m_Geometry;Array<ERR_ObserverViewSample*> m_Samples;
'''+methods+r'''};
struct Baseline {RplId m_ScopeWeapon;vector m_ScopeData;RplId m_Target;int m_Stamp=-1;float m_Received=-1;vector m_Position,m_Forward,m_Up,m_StartPosition,m_StartForward,m_StartUp;float m_Fov=0,m_Near=0,m_StartFov=0,m_Blend=1;
'''+old_methods+r'''};
@@ -99,6 +100,22 @@
std::cout<<"frame_ms="<<frame<<" buffered_step_variation="<<newVariation<<" baseline="<<oldVariation<<"\n";
assert(newVariation<oldVariation*.25f);
}
+ // PiP zoom/parallax/shading must use the same delayed time as the camera.
+ Buffer paired;
+ paired.Push(1,100,100,pos,forward,up,12,.02,22,{1,0,0},6,{0,0,1},.8);
+ paired.Push(1,150,150,pos,forward,up,6,.02,22,{1,0,0},3,{2,4,2},1.2);
+ assert(paired.Read(1,225,.016,matrix,fov,nearPlane));
+ assert(close(fov,9)&&close(paired.m_PipFov,4.5)&&close(paired.m_Geometry.x,1)&&close(paired.m_Vignette,1));
+ // A temporarily unresolved optic may skip eye reads. Rejoining uses the
+ // aiming playhead exactly, rather than an independently restarted delay.
+ Buffer aim,eye;
+ for(int i=0;i<20;i++){
+ aim.Push(1,i*50,i*50,{i*.1f,1.6,0},forward,up,12,.02,22,{1,0,0});
+ eye.Push(1,i*50,i*50,{i*.1f,1.6,0},forward,up,60,.02,22,{1,0,0});
+ assert(aim.Read(1,i*50,.05,matrix,fov,nearPlane));
+ }
+ assert(aim.Read(1,975,.025,matrix,fov,nearPlane));float aimX=matrix[3].x;
+ eye.Align(aim.Playback(),975);assert(eye.Read(1,975,.025,matrix,fov,nearPlane));assert(close(aimX,matrix[3].x));
// Holding an unrendered stream stays bounded; opening it resumes near live.
Buffer parked;
for(int i=0;i<1000;i++)parked.Push(1,i*50,i*50,pos,forward,up,60,.02,-1,{});
@@ -0,0 +1,54 @@
{
"implemented": [
"Atomic bounded four/nine-vector RPC packet with scope and eye poses, scope identity and native lens geometry. No extra per-frame HTTP or server rendering.",
"Two bounded pose buffers use an exact shared playback time, including after unresolved optics. PiP FOV, parallax and vignette interpolate with aim.",
"Native local PiP rendering lifecycle on compatible streamed optic; no ADS activation, weapon input registration or zoom commands. Main view uses owner eye camera, lens uses owner aiming camera.",
"Native authored reticle geometry, zoom, zeroing, illumination, parallax and owner-computed lens shading. Existing 2D presentation fallback and recent world-axis camera correction retained.",
"Cleanup on target/view changes, expired stream and camera exit; partial initialization cleanup and two-second retry backoff. Refuse other active PiP ownership or main camera slot."
],
"settings": "Both watched player and spectator must use PiP (2D scopes disabled). A 2D source supplies only one camera and remains 2D. Unknown/custom renderers or incompatible materials fall back.",
"tests_passed": [
"test_observer_pip.py: actual acquisition/cleanup/fallback bodies with native rendering doubled",
"test_observer_view_relay.py: actual authority/packet handling, atomic dual delivery, malformed eye rejection, stale metadata refusal, 2D transitions",
"test_observer_view_buffer.py: actual interpolation, joint lens timeline, eye recovery alignment, jitter, target/scope changes, expiry and memory bound",
"test_observer_view_anchor.py",
"test_observer_scope.py",
"test_observer_head_visibility.py",
"test_spectator_recovery.py"
],
"limitations": [
"No live multiplayer visual verification. Native weapon proxy animation may still differ from the owner-sampled eye pose.",
"Addon-specific PiP materials, render texture output, reticle visual alignment and GPU/FPS impact require in-game verification.",
"Native compiler success verifies API/type compatibility, not rendered optics or RPC delivery between two running clients."
],
"published": false,
"server_restarted": false,
"sha256": {
"addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c": "ed64c5e39766baf0616071e5eb7f3127ebbe8459721febeeb9a80fecb283131b",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c": "c4f5d325c61f87a4c88a0a1b2fe1e6eea8b02f1e4e24e49b13d145a301067cc3",
"addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverScope.c": "97d29535a0ca1571ceb86996f58b68e5ba0944edfe30370455c52a321772f85e",
"tests/test_observer_view_relay.py": "8853817e96c590a057051b682753f177ca4ce6fc39c99147f3be016a2ed13911",
"tests/test_observer_view_buffer.py": "b0678f585e93d5d879e45fdd2b4f982f8f8eb70a3cba7096bef7baad58506fef",
"tests/test_observer_pip.py": "45e51eb12ff4056630b8982a40fabbfa05d851fe8e4b43a90601e360466c7eaf",
"tests/test_observer_view_anchor.py": "76fb68241e5aa9125cd3dab5aefd7992ca3417f86fb39a4cfc7b92b72aac5e52",
"docs/spectator-guidance.md": "8f7a7c5a5fb4bb9453c1ab0df48a9ca4d72e469204a560576dc33229d5b8f8de"
},
"native_compile": {
"result": "compiled",
"cleanup": {
"cleanup_confirmed": true,
"idle_verified": true,
"errors": [],
"orphan_cleanup": {
"attempted": true,
"report": {
"scope": "/tmp/erri-headless-compat/pfx",
"candidates": [],
"term_sent": [],
"kill_sent": [],
"cleared": true
}
}
}
}
}