NightCoopFlight.reds
ports/flight/r6/scripts/NightCoopFlight/NightCoopFlight.reds · 727 lines · 30.0 KB · Download
The port: one redscript file.
The SDK and its documentation are in English.
// SPDX-License-Identifier: MIT — Copyright (c) 2026 NightCoop (see sdk/LICENSE); Let There Be Flight is MIT by Jack Humbert (THIRD_PARTY.md)
// NightCoop port of Let There Be Flight (LTBF): flying together.
//
// NightCoop gives every shared vehicle one owner, the machine of its driver. The owner's LTBF flies it; every partner
// sees a NightCoop mirror of it, which NightCoop moves along the owner's path (kinematic, no physics of its own). Every
// frame (NightCoopSdk.ListenFrame) this bridge does three things:
// 1. Ride — our player sits in a partner's vehicle: its flight is the driver's. LTBF's own gate keeps our player's
// flight off for the ride.
// 2. Own — a vehicle we lead flies: its flight goes to the partners as two entity states (key 1: flying, gravity,
// thrusters left; key 2: the thrust and the sound, 15 Hz), and NightCoop is told the vehicle is held in the
// air (contacts, return home, the wheel, and partners without this port see it without wheels).
// 3. Mirror — a partner's vehicle flies: LTBF's thrusters, their FX and sound are shown on our mirror, fed from the
// owner's stream; LTBF's flight physics never run on it. Landed — LTBF puts them away, the wheels come back.
// A vehicle that changes hands in the air goes on: ours now — our LTBF takes the flight over; a partner's now — it stays a
// flying mirror until its new owner's word comes.
//
// Built only on NightCoop's public SDK and LTBF's public members. LTBF's own classes cannot be wrapped from another mod,
// so the bridge wraps a few game classes instead (at the end of the file). Without NightCoop or LTBF it compiles to nothing.
@if(ModuleExists("NightCoop"))
import NightCoop.*
// ------------------------------------------------------------------------------------------------ constants
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public abstract class NcFlight {
public static func ModId() -> String = "nightcoop.port.let_there_be_flight";
public static func ModName() -> String = "Let There Be Flight (NightCoop port)";
public static func ModVersion() -> String = "2.0";
public static func Protocol() -> Int32 = 2; // the layout of the two entity states below
// Partners without it get a note in the mods check; it changes vehicles; it puts entity states on our vehicles.
public static func ModFlags() -> Int32 = EnumInt(NightCoopSdkModFlag.NeededByAll) | EnumInt(NightCoopSdkModFlag.ChangesWorld)
| EnumInt(NightCoopSdkModFlag.UsesEntityState);
public static func KeyFlight() -> Int32 = 1; // reliable, on change
public static func KeyThrust() -> Int32 = 2; // stream, at most every ThrustEvery
public static func ThrustEvery() -> Float = 0.066;
public static func MaxOwn() -> Int32 = 16;
// LTBF's gate of its VehicleFlight state machine: a status effect with the tag VehicleBlockFlight on the player
// (DefaultTransition.IsPlayerAllowedToEnterFlight). The record is LTBF's; nothing else applies it.
public static func BlockFlight() -> TweakDBID = t"GameplayRestriction.VehicleBlockFlight";
public static func On() -> Bool = NightCoopSdk.Version() > 0;
public static func Log(text: String) -> Void {
NightCoopSdk.Log(NcFlight.ModId(), 1, text);
}
public static func Warn(text: String) -> Void {
NightCoopSdk.Log(NcFlight.ModId(), 2, text);
}
public static func Id(id: EntityID) -> String = EntityID.ToDebugString(id);
public static func YesNo(value: Bool) -> String = value ? "yes" : "no";
}
// ------------------------------------------------------------------------------------------------ the wire
// Key 1: version, flags (1 flying, 2 gravity off), the thrusters still attached (a bit each, 8 at most).
// Key 2: version, the force and torque LTBF's thrusters got (normalized), LTBF's 8 sound parameters; -1..1 as a byte.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public abstract class NcFlightWire {
public static func Version() -> Int32 = 2;
public static func Flying() -> Int32 = 1;
public static func GravityOff() -> Int32 = 2;
public static func FlightBytes(fc: ref<FlightComponent>, flying: Bool, gravityOff: Bool) -> array<Uint8> {
let flags = (flying ? NcFlightWire.Flying() : 0) | (gravityOff ? NcFlightWire.GravityOff() : 0);
let attached = 255;
if IsDefined(fc) && IsDefined(fc.configuration) {
attached = 0;
let bit = 1;
for thruster in fc.configuration.thrusters {
if bit < 256 && thruster.attached {
attached = attached | bit;
}
bit *= 2;
}
}
let bytes: array<Uint8>;
ArrayPush(bytes, Cast<Uint8>(NcFlightWire.Version()));
ArrayPush(bytes, Cast<Uint8>(flags));
ArrayPush(bytes, Cast<Uint8>(attached));
return bytes;
}
public static func ReadFlight(bytes: array<Uint8>, m: ref<NcFlightMirror>) -> Void {
if ArraySize(bytes) < 3 || Cast<Int32>(bytes[0]) != NcFlightWire.Version() {
m.flying = false; // gone, or a layout we do not know
m.gravityOff = false;
return;
}
let flags = Cast<Int32>(bytes[1]);
m.flying = (flags & NcFlightWire.Flying()) != 0;
m.gravityOff = (flags & NcFlightWire.GravityOff()) != 0;
m.attached = Cast<Int32>(bytes[2]);
}
public static func ThrustBytes(fc: ref<FlightComponent>) -> array<Uint8> {
let force: Vector4;
let torque: Vector4;
for thruster in fc.configuration.thrusters {
if thruster.attached {
force = thruster.force; // LTBF gives every thruster the same force and torque
torque = thruster.torque;
break;
}
}
let a = fc.audioUpdate;
let bytes: array<Uint8>;
ArrayPush(bytes, Cast<Uint8>(NcFlightWire.Version()));
ArrayPush(bytes, NcFlightWire.Byte(force.X));
ArrayPush(bytes, NcFlightWire.Byte(force.Y));
ArrayPush(bytes, NcFlightWire.Byte(force.Z));
ArrayPush(bytes, NcFlightWire.Byte(torque.X));
ArrayPush(bytes, NcFlightWire.Byte(torque.Y));
ArrayPush(bytes, NcFlightWire.Byte(torque.Z));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.surge : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.lift : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.roll : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.pitch : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.yaw : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.sway : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.brake : 0.0));
ArrayPush(bytes, NcFlightWire.Byte(IsDefined(a) ? a.damage : 0.0));
return bytes;
}
public static func ReadThrust(bytes: array<Uint8>, m: ref<NcFlightMirror>) -> Void {
if ArraySize(bytes) < 15 || Cast<Int32>(bytes[0]) != NcFlightWire.Version() {
return;
}
// IFlightThruster.Update normalizes a force longer than 10 and divides a shorter one by 10: send x 10 back
m.force = NcFlightWire.Vec(NcFlightWire.Value(bytes[1]), NcFlightWire.Value(bytes[2]), NcFlightWire.Value(bytes[3])) * 10.0;
m.torque = NcFlightWire.Vec(NcFlightWire.Value(bytes[4]), NcFlightWire.Value(bytes[5]), NcFlightWire.Value(bytes[6]));
m.surge = NcFlightWire.Value(bytes[7]);
m.lift = NcFlightWire.Value(bytes[8]);
m.roll = NcFlightWire.Value(bytes[9]);
m.pitch = NcFlightWire.Value(bytes[10]);
m.yaw = NcFlightWire.Value(bytes[11]);
m.sway = NcFlightWire.Value(bytes[12]);
m.brake = NcFlightWire.Value(bytes[13]);
m.damage = NcFlightWire.Value(bytes[14]);
}
// -1..1 -> 1..255, 128 is 0 (no negative casts)
public static func Byte(value: Float) -> Uint8 {
return Cast<Uint8>(RoundF(ClampF(value, -1.0, 1.0) * 127.0) + 128);
}
public static func Value(b: Uint8) -> Float {
return Cast<Float>(Cast<Int32>(b) - 128) / 127.0;
}
public static func Vec(x: Float, y: Float, z: Float) -> Vector4 {
let v: Vector4;
v.X = x;
v.Y = y;
v.Z = z;
return v;
}
public static func Same(a: array<Uint8>, b: array<Uint8>) -> Bool {
if ArraySize(a) != ArraySize(b) {
return false;
}
let i = 0;
while i < ArraySize(a) {
if a[i] != b[i] {
return false;
}
i += 1;
}
return true;
}
}
// ------------------------------------------------------------------------------------------------ records
// A partner's vehicle: what its owner told us. Kept by our entity of it whether the mirror is in the world or not.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public class NcFlightMirror {
public let entity: EntityID;
public let flying: Bool;
public let gravityOff: Bool;
public let attached: Int32 = 255;
public let shown: Bool; // LTBF's thrusters are on our mirror
public let force: Vector4;
public let torque: Vector4;
public let surge: Float;
public let lift: Float;
public let roll: Float;
public let pitch: Float;
public let yaw: Float;
public let sway: Float;
public let brake: Float;
public let damage: Float;
}
// A vehicle we lead that LTBF flies (or a quickhack of ours lands on): what the partners and NightCoop were told.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public class NcFlightOwn {
public let entity: EntityID;
public let flight: array<Uint8>; // key 1 as told
public let thrust: array<Uint8>; // key 2 as told
public let thrustAt: Float;
public let air: Int32; // NightCoopSdkAirborneFlag bits NightCoop holds it with
public let lastResult: Int32;
}
// A quickhack of ours landed on this vehicle (from the game's thread of the hack to the system's).
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public class NcFlightWatchRequest extends ScriptableSystemRequest {
public let vehicle: EntityID;
}
// ------------------------------------------------------------------------------------------------ the system
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
public class NcFlightSystem extends ScriptableSystem {
private let m_on: Bool;
private let m_mirrors: array<ref<NcFlightMirror>>;
private let m_own: array<ref<NcFlightOwn>>;
private let m_blockAsked: Bool; // LTBF's flight block applied for this ride, not seen on the player yet
private let m_blocked: Bool; // ... seen on the player
private let m_ctlrOff: Bool; // LTBF's flight controller turned off for this ride
private let m_saidNoBlock: Bool; // said once: this LTBF has no block record
public static func Get(game: GameInstance) -> ref<NcFlightSystem> {
return GameInstance.GetScriptableSystemsContainer(game).Get(n"NcFlightSystem") as NcFlightSystem;
}
public func IsOn() -> Bool = this.m_on;
// ---------------------------------------------------------------- lifecycle
private func OnAttach() -> Void {
if !NcFlight.On() {
return; // NightCoop's SDK is switched off: LTBF works as in single player
}
// Every load of a save attaches a new system: the same id registers again, NightCoop gives it the partners' states
// again, and the frame is asked again (the old object is gone).
this.m_on = NightCoopSdk.RegisterMod(NcFlight.ModId(), NcFlight.ModName(), NcFlight.ModVersion(), NcFlight.Protocol(),
NcFlight.ModFlags());
if !this.m_on {
NcFlight.Warn("bridge off: NightCoop refused the registration");
return;
}
NightCoopSdk.Listen(NcFlight.ModId(), this, n"OnNightCoopEvent");
if !NightCoopSdk.ListenFrame(NcFlight.ModId(), this, n"OnFrame") {
NcFlight.Warn("bridge off: NightCoop refused the frame");
this.m_on = false;
return;
}
NcFlight.Log("bridge on: the driver's flight goes to the partners, a partner's flight is shown on our mirror, a ride in a partner's vehicle leaves its flight to the driver");
}
private func OnDetach() -> Void {
if !this.m_on {
return;
}
// The world goes with the session: LTBF puts its components away itself. NightCoop keeps the mod registered (partners
// never see it come and go), so our holds on vehicles are released here.
for own in this.m_own {
this.SetAir(own, 0);
}
ArrayClear(this.m_own);
ArrayClear(this.m_mirrors);
NightCoopSdk.ListenFrame(NcFlight.ModId(), null, n"None");
NightCoopSdk.Listen(NcFlight.ModId(), null, n"None");
this.m_on = false;
}
// ---------------------------------------------------------------- events: a partner's word about its vehicle
public func OnNightCoopEvent(evt: ref<NightCoopSdkEvent>) -> Void {
if !this.m_on || !IsDefined(evt) || evt.kind != EnumInt(NightCoopSdkEventKind.EntityState) {
return;
}
if evt.a == NcFlight.KeyFlight() {
let m = this.MirrorOf(evt.entity, true);
let wasFlying = m.flying;
let bytes: array<Uint8>;
if !evt.removed {
bytes = evt.bytes;
}
NcFlightWire.ReadFlight(bytes, m);
if (m.flying && !wasFlying) || (!m.flying && wasFlying) { // redscript 0.5: no != for Bool
NcFlight.Log("vehicle " + NcFlight.Id(m.entity) + ": its owner says flying " + NcFlight.YesNo(m.flying) + ", gravity " + (m.gravityOff ? "off" : "on") + ", thrusters " + IntToString(m.attached));
}
return;
}
if evt.a == NcFlight.KeyThrust() {
let t = this.MirrorOf(evt.entity, false);
if IsDefined(t) && !evt.removed {
NcFlightWire.ReadThrust(evt.bytes, t);
}
}
}
// A quickhack of ours landed on a vehicle (LTBF's flight hacks complete through ScriptableDeviceAction.CompleteAction).
private func OnNcFlightWatchRequest(request: ref<NcFlightWatchRequest>) -> Void {
let vehicle = GameInstance.FindEntityByID(this.GetGameInstance(), request.vehicle) as VehicleObject;
if this.m_on && IsDefined(vehicle) && NightCoopSdk.EntityIsMine(request.vehicle) != 0 {
this.Watch(vehicle);
}
}
// ---------------------------------------------------------------- the frame
public func OnFrame(dt: Float) -> Void {
if !this.m_on {
return;
}
let game = this.GetGameInstance();
let player = GetPlayer(game);
if !IsDefined(player) {
return;
}
let seat = GetMountedVehicle(player);
this.Ride(game, player, seat);
this.Own(game, seat);
let still = GameInstance.GetPhotoModeSystem(game).IsPhotoModeActive() || GameInstance.GetTimeSystem(game).IsPausedState();
let i = ArraySize(this.m_mirrors) - 1;
while i >= 0 {
if !this.Mirror(game, this.m_mirrors[i], dt, still) {
ArrayErase(this.m_mirrors, i);
}
i -= 1;
}
}
// ---------------------------------------------------------------- 1. ride: our player in a partner's vehicle
private func Ride(game: GameInstance, player: ref<PlayerPuppet>, seat: wref<VehicleObject>) -> Void {
let partners = IsDefined(seat) && NightCoopSdk.EntityIsMine(seat.GetEntityID()) == 0;
this.SetBlock(game, player, partners);
let ctlr = FlightController.GetInstance();
if !IsDefined(ctlr) {
return;
}
if partners {
// LTBF hands its flight controls to whoever gets into a flying vehicle, and our flight key can still switch it on
// before the block shows: on a partner's vehicle neither is ours. LTBF's own stop takes them back; the owner's word
// shows the flight again next.
let fc = seat.GetFlightComponent();
if IsDefined(fc) && (ctlr.IsActive() || fc.hasUpdate) {
this.PutAway(fc);
let m = this.MirrorOf(seat.GetEntityID(), false);
if IsDefined(m) {
m.shown = false;
}
NcFlight.Log("our player's LTBF took the partner's vehicle " + NcFlight.Id(seat.GetEntityID()) + " — given back to its owner");
}
if ctlr.IsEnabled() {
ctlr.Disable(); // no "activate flight" hint on a partner's vehicle
this.m_ctlrOff = true;
}
return;
}
if this.m_ctlrOff {
this.m_ctlrOff = false;
if IsDefined(seat) && !ctlr.IsEnabled() {
ctlr.Enable(); // the vehicle is ours now; out of it, LTBF disabled the controller itself on the way out
}
}
}
// LTBF's block on our player, by what is seen each frame: on for a ride in a partner's vehicle, off otherwise. Status
// effects land through a queue, so a block asked for is not asked again until it shows.
private func SetBlock(game: GameInstance, player: ref<PlayerPuppet>, want: Bool) -> Void {
let id = NcFlight.BlockFlight();
let has = GameInstance.GetStatusEffectSystem(game).HasStatusEffect(player.GetEntityID(), id);
if has && want && !this.m_blocked {
NcFlight.Log("our player rides in a partner's vehicle — LTBF's flight is the driver's (VehicleBlockFlight)");
}
if !has && this.m_blocked {
NcFlight.Log("our player is out of the partner's vehicle — LTBF's flight is ours again");
}
this.m_blocked = has && want;
if want {
if has {
this.m_blockAsked = false;
return;
}
if this.m_blockAsked {
return;
}
if !IsDefined(TweakDBInterface.GetStatusEffectRecord(id)) {
if !this.m_saidNoBlock {
this.m_saidNoBlock = true;
NcFlight.Warn("this LTBF has no GameplayRestriction.VehicleBlockFlight (an older LTBF?) — a ride in a partner's vehicle is not blocked");
}
return;
}
this.m_blockAsked = StatusEffectHelper.ApplyStatusEffect(player, id);
return;
}
this.m_blockAsked = false;
if has {
StatusEffectHelper.RemoveStatusEffect(player, id); // also one left from a save, or asked for on a mount of our own
}
}
// ---------------------------------------------------------------- 2. own: vehicles we lead that fly
private func Own(game: GameInstance, seat: wref<VehicleObject>) -> Void {
let inSession = NightCoopSdk.InSession();
if inSession && IsDefined(seat) {
let fc = seat.GetFlightComponent();
if IsDefined(fc) && fc.active && NightCoopSdk.EntityIsMine(seat.GetEntityID()) != 0 {
this.Watch(seat); // ours (or not shared yet): LTBF flies it for our player, driver or passenger
}
}
let now = EngineTime.ToFloat(GameInstance.GetSimTime(game));
let i = ArraySize(this.m_own) - 1;
while i >= 0 {
let own = this.m_own[i];
if !this.TickOwn(game, own, now, inSession) {
this.SetAir(own, 0);
ArrayErase(this.m_own, i);
}
i -= 1;
}
}
// false — forget it
private func TickOwn(game: GameInstance, own: ref<NcFlightOwn>, now: Float, inSession: Bool) -> Bool {
let vehicle = GameInstance.FindEntityByID(game, own.entity) as VehicleObject;
if !inSession || !IsDefined(vehicle) {
return false; // no partners to tell, or the vehicle is gone
}
let fc = vehicle.GetFlightComponent();
let flying = IsDefined(fc) && fc.active;
let gravityOff = !vehicle.HasGravity();
let mine = NightCoopSdk.EntityIsMine(own.entity);
if mine == 0 {
this.HandedOver(vehicle, fc, gravityOff);
return false;
}
if mine != 1 {
// not shared (no partner near enough): told in full when NightCoop shares it
this.SetAir(own, 0);
ArrayClear(own.flight);
ArrayClear(own.thrust);
return flying || gravityOff;
}
let flight = NcFlightWire.FlightBytes(fc, flying, gravityOff);
if !NcFlightWire.Same(flight, own.flight) {
let result = NightCoopSdk.SetEntityStateBytes(NcFlight.ModId(), own.entity, NcFlight.KeyFlight(), flight, false);
if result >= 0 {
own.flight = flight;
NcFlight.Log("our vehicle " + NcFlight.Id(own.entity) + ": flying " + NcFlight.YesNo(flying) + ", gravity " + (gravityOff ? "off" : "on") + ", thrusters " + IntToString(Cast<Int32>(flight[2])) + " — told to the partners");
} else {
if result != own.lastResult {
NcFlight.Warn("our vehicle " + NcFlight.Id(own.entity) + ": its flight is not told to the partners (" + IntToString(result) + ") — tried again every frame");
}
}
own.lastResult = result;
}
if flying && (ArraySize(own.thrust) == 0 || now - own.thrustAt >= NcFlight.ThrustEvery() || now < own.thrustAt) {
let thrust = NcFlightWire.ThrustBytes(fc);
if !NcFlightWire.Same(thrust, own.thrust) && NightCoopSdk.SetEntityStateBytes(NcFlight.ModId(), own.entity, NcFlight.KeyThrust(), thrust, true) >= 0 {
own.thrust = thrust;
own.thrustAt = now;
}
}
if !flying && ArraySize(own.thrust) > 0 {
NightCoopSdk.ClearEntityState(NcFlight.ModId(), own.entity, NcFlight.KeyThrust());
ArrayClear(own.thrust);
}
let air = 0;
if flying {
air = EnumInt(NightCoopSdkAirborneFlag.Hold) | EnumInt(NightCoopSdkAirborneFlag.NoWheels); // LTBF hides its wheels
} else {
if gravityOff {
air = EnumInt(NightCoopSdkAirborneFlag.Hold);
}
}
this.SetAir(own, air);
// landed (or never flew: a quickhack that did not switch it on), and the partners know it
return flying || gravityOff || !NcFlightWire.Same(flight, own.flight);
}
// NightCoop made our flying vehicle a partner's (they took the wheel, it went home): their word leads it now. Our LTBF's
// physics must not fight NightCoop's mirror; its thrusters stay on, a flying mirror, until that word comes.
private func HandedOver(vehicle: ref<VehicleObject>, fc: ref<FlightComponent>, gravityOff: Bool) -> Void {
if !IsDefined(fc) || !fc.active {
return;
}
fc.hasUpdate = false;
let m = this.MirrorOf(vehicle.GetEntityID(), true);
m.flying = true;
m.gravityOff = gravityOff;
m.shown = true;
NcFlight.Log("our flying vehicle " + NcFlight.Id(m.entity) + " is a partner's now — our LTBF's physics are off, it flies on as their mirror");
}
public func Watch(vehicle: wref<VehicleObject>) -> Void {
let entity = vehicle.GetEntityID();
for own in this.m_own {
if Equals(own.entity, entity) {
return;
}
}
if ArraySize(this.m_own) >= NcFlight.MaxOwn() {
this.SetAir(this.m_own[0], 0);
ArrayErase(this.m_own, 0);
}
let made = new NcFlightOwn();
made.entity = entity;
ArrayPush(this.m_own, made);
}
// What NightCoop holds the vehicle with (SetEntityAirborneFlags; 0 — on the ground).
private func SetAir(own: ref<NcFlightOwn>, air: Int32) -> Void {
if air != own.air && NightCoopSdk.SetEntityAirborneFlags(NcFlight.ModId(), own.entity, air) >= 0 {
own.air = air;
}
}
// ---------------------------------------------------------------- 3. mirror: a partner's vehicle that flies
// false — forget it
private func Mirror(game: GameInstance, m: ref<NcFlightMirror>, dt: Float, still: Bool) -> Bool {
let mine = NightCoopSdk.EntityIsMine(m.entity);
let vehicle = GameInstance.FindEntityByID(game, m.entity) as VehicleObject;
let fc: ref<FlightComponent>;
if IsDefined(vehicle) {
fc = vehicle.GetFlightComponent();
}
if mine == 1 {
this.TakenOver(vehicle, fc, m);
return false;
}
if mine != 0 {
// NightCoop does not sync it any more (the session is over, it is not shared)
this.Hide(fc, m, "is not a partner's mirror any more");
return false;
}
if m.flying && NightCoopSdk.PeerHasMod(NcFlight.ModId(), NightCoopSdk.EntityLeader(m.entity)) == 0 {
m.flying = false; // its owner has no port (it changed hands): nobody flies it there
}
if IsDefined(vehicle) {
let hasGravity = vehicle.HasGravity();
if (hasGravity && m.gravityOff) || (!hasGravity && !m.gravityOff) {
vehicle.EnableGravity(!m.gravityOff); // the flag only (NightCoop moves the mirror): right if it becomes ours
}
}
// Landed, destroyed, streamed out or not spawned yet, or LTBF has no flight component on this vehicle (switched off
// for it — wheels, as LTBF would show): nothing shown now. The word is kept while the owner says it is in the air.
if !m.flying || !IsDefined(fc) || vehicle.IsDestroyed() {
this.Hide(fc, m, m.flying ? "destroyed" : "landed");
return m.flying || m.gravityOff;
}
if !m.shown || !fc.active {
m.shown = this.Show(fc); // LTBF not set up yet — the next frame
if m.shown {
NcFlight.Log("mirror " + NcFlight.Id(m.entity) + ": its owner flies it — LTBF's thrusters, FX and sound are shown here");
}
return true;
}
fc.hasUpdate = false; // the mirror's physics are NightCoop's, never LTBF's
this.Detached(fc, m.attached);
if !still {
this.Feed(fc, m, dt);
}
return true;
}
// LTBF's own Activate without its flight physics: hasUpdate off at once (no forces, no ForceEnablePhysics), and the
// controls are not handed to a player in the vehicle (isPlayerMounted off for the call).
private func Show(fc: ref<FlightComponent>) -> Bool {
if !IsDefined(fc.configuration) {
return false;
}
if !fc.active {
let mounted = fc.isPlayerMounted;
fc.isPlayerMounted = false;
fc.Activate(true);
fc.isPlayerMounted = mounted;
}
fc.hasUpdate = false;
return fc.active;
}
// LTBF's own stop: thrusters, FX, sound, colliders away, the wheels back, a player's controls back — silent.
private func PutAway(fc: ref<FlightComponent>) -> Void {
if fc.active {
fc.Deactivate(true);
}
fc.hasUpdate = false;
}
private func Hide(fc: ref<FlightComponent>, m: ref<NcFlightMirror>, why: String) -> Void {
if m.shown && IsDefined(fc) {
this.PutAway(fc);
NcFlight.Log("mirror " + NcFlight.Id(m.entity) + ": " + why + " — LTBF's thrusters are put away, the wheels are back");
}
m.shown = false;
}
// The vehicle is ours now while its owner flew it (we took the wheel, it came home to us): our LTBF goes on with the
// flight; with our player in it, LTBF hands them the controls.
private func TakenOver(vehicle: ref<VehicleObject>, fc: ref<FlightComponent>, m: ref<NcFlightMirror>) -> Void {
if !IsDefined(fc) {
return;
}
if m.shown {
this.PutAway(fc);
}
if m.flying && !vehicle.IsDestroyed() {
fc.Activate(true);
this.Watch(vehicle);
NcFlight.Log("vehicle " + NcFlight.Id(m.entity) + " is ours now while it flies — our LTBF goes on with the flight");
}
}
// A thruster the owner lost (the game threw the part off) goes from our mirror too: LTBF's Detach stops its FX and
// sound, the mesh is hidden here.
private func Detached(fc: ref<FlightComponent>, mask: Int32) -> Void {
let lost = false;
let bit = 1;
for thruster in fc.configuration.thrusters {
if bit < 256 && thruster.attached && (mask & bit) == 0 {
thruster.Detach();
if IsDefined(thruster.meshComponent) {
thruster.meshComponent.visualScale = new Vector3(0.0, 0.0, 0.0);
thruster.meshComponent.Toggle(false);
}
lost = true;
}
bit *= 2;
}
if lost {
fc.thrusterTensor = fc.configuration.GetThrusterTensor();
}
}
// What FlightComponent.OnUpdate does for the look and the sound, from the owner's stream.
private func Feed(fc: ref<FlightComponent>, m: ref<NcFlightMirror>, dt: Float) -> Void {
let a = fc.audioUpdate;
if IsDefined(fc.stats) && IsDefined(a) { // FlightComponent.stats is LTBF's private field; LTBF's thrusters read it too
fc.stats.UpdateDynamic();
a.surge = m.surge;
a.lift = m.lift;
a.roll = m.roll;
a.pitch = m.pitch;
a.yaw = m.yaw;
a.sway = m.sway;
a.brake = MaxF(m.brake, 0.0);
a.damage = MaxF(m.damage, 0.0);
fc.UpdateAudioParams(dt);
}
for thruster in fc.configuration.thrusters {
thruster.Update(m.force, m.torque);
}
}
private func MirrorOf(entity: EntityID, create: Bool) -> ref<NcFlightMirror> {
for m in this.m_mirrors {
if Equals(m.entity, entity) {
return m;
}
}
if !create {
return null;
}
let made = new NcFlightMirror();
made.entity = entity;
ArrayPush(this.m_mirrors, made);
return made;
}
}
// ------------------------------------------------------------------------------------------------ the game's classes
// Our player gets into a vehicle: LTBF adds its VehicleFlight state machine on this same mount, and a machine that reaches
// "active" before the block stays there — so the block goes on now. This event does not run on NightCoop's thread, so it
// cannot ask whose vehicle it is: the next frame lifts the block at once when the vehicle is not a partner's.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
@wrapMethod(VehicleComponent)
protected cb func OnMountingEvent(evt: ref<MountingEvent>) -> Bool {
let result = wrappedMethod(evt);
let vehicle = this.GetVehicle();
if IsDefined(vehicle) {
let system = NcFlightSystem.Get(vehicle.GetGame());
let player = GetPlayer(vehicle.GetGame());
if IsDefined(system) && system.IsOn() && IsDefined(player) && Equals(evt.request.lowLevelMountingInfo.childId, player.GetEntityID()) {
StatusEffectHelper.ApplyStatusEffect(player, NcFlight.BlockFlight());
}
}
return result;
}
// LTBF offers its quickhacks by its replacement of this function while FlightComponent.isQuickHackable is on: not on a
// partner's vehicle (they would switch LTBF's physics on our mirror). The game's own vehicle quickhacks go to the owner
// through NightCoop.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
@wrapMethod(VehicleObject)
protected func SendQuickhackCommands(shouldOpen: Bool) -> Void {
let fc = this.GetFlightComponent();
if shouldOpen && IsDefined(fc) && fc.isQuickHackable && NightCoopSdk.EntityIsMine(this.GetEntityID()) == 0 {
fc.isQuickHackable = false;
wrappedMethod(shouldOpen);
fc.isQuickHackable = true;
return;
}
wrappedMethod(shouldOpen);
}
// LTBF's flight quickhacks complete through this: the vehicle one of ours lands on is watched, so its flight goes to the
// partners even with nobody in it.
@if(ModuleExists("NightCoop") && ModuleExists("LetThereBeFlight"))
@wrapMethod(ScriptableDeviceAction)
public func CompleteAction(gameInstance: GameInstance) -> Void {
wrappedMethod(gameInstance);
let action = this as FlightAction;
let system = NcFlightSystem.Get(gameInstance);
if IsDefined(action) && IsDefined(action.m_owner) && IsDefined(system) {
let request = new NcFlightWatchRequest();
request.vehicle = action.m_owner.GetEntityID();
system.QueueRequest(request);
}
}