Add front-passenger handbrake control; record Workshop 0.0.132
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-30 20:21:27 +01:00
parent 2dcbe3107d
commit 6738eda748
9 changed files with 1142 additions and 1 deletions
@@ -0,0 +1,148 @@
// Front-seat passenger assistance is an extra held input, not a parking-brake
// toggle. The driver's persistent brake, gears and service brake stay native.
class ERR_PassengerHandbrake
{
static bool Beside(vector delta)
{
float lateral = Math.AbsFloat(delta[0]);
return lateral >= 0.35 && lateral <= 1.7 && Math.AbsFloat(delta[1]) <= 0.5 && Math.AbsFloat(delta[2]) <= 0.65;
}
static bool FrontSeat(IEntity vehicle, BaseCompartmentSlot slot)
{
if (!vehicle || !slot || slot.GetType() != ECompartmentType.CARGO) return false;
CarControllerComponent car = CarControllerComponent.Cast(vehicle.FindComponent(CarControllerComponent));
if (!car) return false;
BaseCompartmentSlot pilot = car.GetPilotCompartmentSlot();
if (!pilot || pilot == slot || !slot.GetOwner() || slot.GetOwner().GetRootParent() != vehicle.GetRootParent()) return false;
PointInfo passengerPoint = slot.GetPassengerPointInfo(), pilotPoint = pilot.GetPassengerPointInfo();
if (!passengerPoint || !pilotPoint) return false;
vector passenger = vehicle.CoordToLocal(passengerPoint.GetWorldTransformAxis(3));
vector driver = vehicle.CoordToLocal(pilotPoint.GetWorldTransformAxis(3));
return Beside(passenger - driver);
}
static IEntity VehicleFor(IEntity actor)
{
if (!ERR_Actors.CanFight(actor)) return null;
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(actor.FindComponent(CompartmentAccessComponent));
if (!access) return null;
BaseCompartmentSlot slot = access.GetCompartment();
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(actor);
if (!slot || slot.GetOccupant() != actor || !FrontSeat(vehicle, slot)) return null;
return vehicle;
}
static bool Requested(IEntity driver)
{
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(driver);
if (!vehicle) return false;
CarControllerComponent car = CarControllerComponent.Cast(vehicle.FindComponent(CarControllerComponent));
if (!car || !car.GetPilotCompartmentSlot() || car.GetPilotCompartmentSlot().GetOccupant() != driver) return false;
// Native held-handbrake input releases the parking latch. A parked car
// already satisfies the passenger's demand, so leave that latch untouched.
if (car.GetPersistentHandBrake()) return false;
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(vehicle.FindComponent(BaseCompartmentManagerComponent));
if (!manager) return false;
array<BaseCompartmentSlot> slots = {};
manager.GetCompartments(slots);
foreach (BaseCompartmentSlot slot : slots)
{
IEntity actor = slot.GetOccupant();
if (!actor || actor == driver) continue;
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(actor.FindComponent(SCR_CharacterControllerComponent));
if (character && character.ERR_PassengerBrakeHeld() && VehicleFor(actor) == vehicle) return true;
}
return false;
}
}
modded class SCR_PlayerController
{
protected SCR_CharacterControllerComponent m_ERRBrakingPassenger;
// No caller-supplied body, seat or vehicle: authority derives all three.
void ERR_SendPassengerBrake(bool held)
{
if (this != GetGame().GetPlayerController()) return;
if (Replication.IsServer()) ERR_RequestPassengerBrake(held);
else Rpc(ERR_RequestPassengerBrake, held);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_RequestPassengerBrake(bool held)
{
if (!Replication.IsServer()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
SCR_CharacterControllerComponent character;
if (actor) character = SCR_CharacterControllerComponent.Cast(actor.FindComponent(SCR_CharacterControllerComponent));
if (m_ERRBrakingPassenger && (m_ERRBrakingPassenger != character || !held)) m_ERRBrakingPassenger.ERR_SetPassengerBrake(false);
m_ERRBrakingPassenger = null;
if (!character) return;
bool allowed = held && mode && mode.Active() && mode.Players() && mode.Players().CurrentActor(actor) && ERR_PassengerHandbrake.VehicleFor(actor);
character.ERR_SetPassengerBrake(allowed);
if (allowed) m_ERRBrakingPassenger = character;
}
}
modded class SCR_CharacterControllerComponent
{
[RplProp()] protected bool m_ERRPassengerBrake;
protected int m_ERRPassengerBrakeHeartbeat;
protected bool m_ERRPassengerBrakeSent;
protected int m_ERRPassengerBrakeNextSend;
protected IEntity m_ERRPassengerBrakeHintVehicle;
bool ERR_PassengerBrakeHeld() { return m_ERRPassengerBrake; }
void ERR_SetPassengerBrake(bool held)
{
if (!Replication.IsServer()) return;
m_ERRPassengerBrakeHeartbeat = System.GetTickCount();
if (held == m_ERRPassengerBrake) return;
m_ERRPassengerBrake = held;
Replication.BumpMe();
GetGame().GetCallqueue().Remove(ERR_ExpirePassengerBrake);
if (held) GetGame().GetCallqueue().CallLater(ERR_ExpirePassengerBrake, 250, true);
}
protected void ERR_ExpirePassengerBrake()
{
if (!Replication.IsServer() || !m_ERRPassengerBrake) return;
ERR_GameMode mode = ERR_GameMode.Get();
int age = System.GetTickCount() - m_ERRPassengerBrakeHeartbeat;
if (!mode || !mode.Active() || age < 0 || age > 1000 || !ERR_PassengerHandbrake.VehicleFor(GetOwner())) ERR_SetPassengerBrake(false);
}
void ~SCR_CharacterControllerComponent()
{
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(ERR_ExpirePassengerBrake);
}
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
{
ERR_GameMode mode = ERR_GameMode.Get();
// Feed the actor's native action before it prepares driving controls. The
// device/AI refreshes this value each tick; only add a held demand, never
// write a release over the driver's own handbrake or parking-brake input.
if (am && mode && mode.Active() && !am.GetActionTriggered("CarHandBrakePersistent") && ERR_PassengerHandbrake.Requested(owner)) am.SetActionValue("CarHandBrake", 1);
super.OnPrepareControls(owner, am, dt, player);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller || controller.GetControlledEntity() != owner) return;
IEntity vehicle;
if (mode && mode.Active()) vehicle = ERR_PassengerHandbrake.VehicleFor(owner);
bool held;
InputManager input = GetGame().GetInputManager();
if (vehicle && input && GetGame().GetMenuManager() && !GetGame().GetMenuManager().GetTopMenu() && !ERR_ObserverChatInput.Capture())
{
input.ActivateContext("ERR_PassengerBrakeContext", 1);
held = input.GetActionValue("ERR_PassengerBrake") > 0.5;
if (m_ERRPassengerBrakeHintVehicle != vehicle)
{
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (hints) hints.ShowCustom("Hold Space to apply the handbrake. Release Space to let go.", "Passenger handbrake", 5);
m_ERRPassengerBrakeHintVehicle = vehicle;
}
}
if (!vehicle) m_ERRPassengerBrakeHintVehicle = null;
int now = System.GetTickCount();
if (held != m_ERRPassengerBrakeSent || (held && now >= m_ERRPassengerBrakeNextSend))
{
controller.ERR_SendPassengerBrake(held);
m_ERRPassengerBrakeSent = held;
m_ERRPassengerBrakeNextSend = now + 250;
}
}
}