Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
482 lines
21 KiB
C
482 lines
21 KiB
C
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 ref ERR_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay();
|
|
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_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_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();
|
|
float fov, nearPlane;
|
|
if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane))
|
|
{
|
|
param.fov = fov; GetCameraEntity().SetNearPlane(nearPlane);
|
|
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();
|
|
}
|
|
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_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";
|
|
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();
|
|
}
|
|
}
|
|
}
|