Add recon drones and body burial; stabilize spectator camera

This commit is contained in:
2026-10-21 15:28:32 +02:00
parent f41d8ba2f9
commit ac285b5c97
80 changed files with 13545 additions and 126 deletions
@@ -46,7 +46,7 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; }
array<string> names = {"SitesOn", "SitesOff", "PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedPlane", "RecommendedWeapons"};
array<string> names = {"SitesOn", "SitesOff", "PlaneOn", "PlaneOff", "Close", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "SelfEpiOn", "SelfEpiOff", "IntelSummary", "RerollIntel", "RandomDay_startTimeMinutes", "Default_viewDistanceMetres", "RestartEmptyOn", "RestartEmptyOff", "RecommendedPlane", "RecommendedWeapons", "RecommendedDrones"};
foreach (string mainButton : ERR_AdminMenuPages.MainButtons()) names.Insert(mainButton);
foreach (string pageButton : Navigation()) names.Insert(pageButton);
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
@@ -198,6 +198,7 @@ class ERR_AdminMenu : ChimeraMenuBase
int divider = entry.IndexOf("=");
if (divider <= 0) continue;
string key = entry.Substring(0, divider);
if (key == "droneRuntime") { SetText("DroneRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key == "recommendedBikeRuntime") { SetText("RecommendedBikeRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key.IndexOf("recommended_") == 0)
{
@@ -339,6 +340,7 @@ class ERR_AdminMenu : ChimeraMenuBase
if (name == "Close") { Close(); return; }
int section = ERR_AdminMenuPages.MainButtons().Find(name);
if (section >= 0) { ShowPage(ERR_AdminMenuPages.MainPage(section)); return; }
if (name == "RecommendedDrones") { ShowPage(ERR_AdminPage.DRONES); return; }
if (name == "RecommendedPlane") { ShowPage(ERR_AdminPage.PLANE); return; }
if (name == "RecommendedWeapons") { ShowPage(ERR_AdminPage.ITEMS); return; }
int page = Navigation().Find(name);
@@ -3,12 +3,12 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "droneReconCount", "droneCooldownSeconds", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiDoctrine" || key == "aiShadow") return hq;
if (key == "sarkaAngerPerCar") return hq;
if (key == "sarkaAngerPerCar" || key == "droneReconCount" || key == "droneCooldownSeconds") return hq;
if (key == "aiIntelChancePercent") return intel;
if (key == "automaticEquipmentChancePercent" || key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
@@ -48,6 +48,8 @@ class ERR_AdminMenuFields
else if (key == "automaticEquipmentChancePercent") prefix = "hq equipment";
else if (key == "sarkaAngerPerCar") prefix = "mission sarkapoints";
else if (key == "motorbikeCount") prefix = "mission motorbikes";
else if (key == "droneReconCount") prefix = "mission drones";
else if (key == "droneCooldownSeconds") prefix = "mission dronecooldown";
else if (key == "planeReconEnabled") prefix = "mission plane";
else if (key == "planeAltitudeMetres") prefix = "mission planealtitude";
else if (key == "planeReplacementSeconds") prefix = "mission planereplacement";
@@ -23,17 +23,18 @@ enum ERR_AdminPage
AI = 19,
STARTER = 20,
SARKA = 21,
DRONES = 22,
}
class ERR_AdminMenuPages
{
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka"}; }
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage", "DronesPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka", "OpenDrones"}; }
static array<string> MainButtons() { return {"Main_Mission", "Main_Forces", "Main_HQ", "Main_Intel", "Main_Items", "Main_Mods", "Main_Audio", "Main_Status"}; }
static array<string> Sidebars() { return {"Sidebar_Mission", "Sidebar_Forces", "Sidebar_HQ", "Sidebar_Intel", "Sidebar_Items", "Sidebar_Mods", "Sidebar_Audio", "Sidebar_Status"}; }
static int Group(int page)
{
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1};
array<int> groups = {0, 2, 7, 5, 1, 0, 3, 7, 0, 4, 6, 1, 1, 1, 2, 5, 4, 4, 4, 2, 4, 1, 2};
if (page < 0 || page >= groups.Count()) return -1;
return groups[page];
}
@@ -11,6 +11,8 @@ class ERR_CommandArguments
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
if (section == "mission" && option == "planesector") return "planeSectorSeconds";
if (section == "mission" && option == "drones") return "droneReconCount";
if (section == "mission" && option == "dronecooldown") return "droneCooldownSeconds";
if (section == "mission" && option == "plane") return "planeReconEnabled";
if (section == "mission" && option == "camps") return "fieldCampCount";
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
@@ -174,6 +176,8 @@ class ERR_AdminService
if (key == "startMagazineCount") return "0..48 spare pistol magazines in the shared prison/crash supply backpack; default 8. Applies next round.";
if (key == "sarkaAngerPerCar") return "1-30 anger points per car; 0 disables new waves. Default 3. Whole thresholds only: 5 means one car at 5 anger, two at 10. Four special-forces crew per car. Applies to new waves; existing teams remain.";
if (key == "motorbikeCount") return "0-6; default 2; 0 disabled. Optional WCS Motorbikes, two response specialists per bike. Applies next round; losses are not replaced.";
if (key == "droneReconCount") return "0-3; default 0. Applies next round.";
if (key == "droneCooldownSeconds") return "60-1800; default 300. Applies next round.";
if (key == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
if (key == "planeReplacementSeconds") return "30-1800; default 60. Applies next round.";
if (key == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
@@ -199,7 +203,7 @@ class ERR_AdminService
}
protected string RoadblockTiming(string key)
{
if (key == "aiDoctrine" || key == "aiShadow") return " for next round";
if (key == "aiDoctrine" || key == "aiShadow" || key == "droneReconCount" || key == "droneCooldownSeconds") return " for next round";
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount") return " for next round";
return "";
}
@@ -0,0 +1,91 @@
// The native action supplies held progress/cancellation. Authority rechecks the
// current player, shovel and corpse immediately before replicated deletion.
class ERR_Burial
{
static bool Dead(IEntity entity)
{
ChimeraCharacter character = ChimeraCharacter.Cast(entity);
return character && character.GetCharacterController()
&& character.GetCharacterController().GetLifeState() == ECharacterLifeState.DEAD;
}
static bool ShovelHeld(IEntity user)
{
ChimeraCharacter character = ChimeraCharacter.Cast(user);
if (!character || !character.GetCharacterController()) return false;
CharacterControllerComponent controller = character.GetCharacterController();
// Gadgets occupy the left-hand slot; GetCurrentItemInHands alone only finds
// weapons. Inventory ownership and a selected quickslot are not sufficient.
IEntity item = controller.GetAttachedGadgetAtLeftHandSlot();
if (!item) item = controller.GetCurrentItemInHands();
return item && item.FindComponent(SCR_CampaignBuildingGadgetToolComponent) != null;
}
static bool Reach(float distance)
{
return distance >= 0 && distance <= 2.5;
}
static bool Clear(IEntity user, IEntity body)
{
TraceParam trace = new TraceParam();
trace.Start = user.GetOrigin() + Vector(0, 1.2, 0);
trace.End = body.GetOrigin() + Vector(0, 0.25, 0);
trace.Exclude = user; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
IEntity hit = trace.TraceEnt;
for (int depth = 0; hit && depth < 16; depth++)
{
if (hit == body) return true;
hit = hit.GetParent();
}
return false;
}
static string Rejection(IEntity user, IEntity body)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !mode.Active()) return "Mission is not active";
if (!ERR_Actors.Player(user) || !ERR_Actors.CanFight(user)) return "You must be alive and conscious";
if (user == body || !Dead(body)) return "Only dead bodies can be buried";
if (!ShovelHeld(user)) return "Take a shovel into your hands";
if (CompartmentAccessComponent.GetVehicleIn(user) || CompartmentAccessComponent.GetVehicleIn(body)) return "Move yourself and the body out of the vehicle";
if (!Reach(vector.Distance(user.GetOrigin(), body.GetOrigin()))) return "Move closer to the body";
if (!Clear(user, body)) return "The body is obstructed";
return string.Empty;
}
static bool Permitted(IEntity user, IEntity body)
{
if (!Replication.IsServer()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Players() && mode.Players().CurrentActor(user) && Rejection(user, body).IsEmpty();
}
static void Complete(IEntity user, IEntity body)
{
if (!Permitted(user, body)) return;
// Corpses and their remaining inventory disappear on every peer. Already
// queued HQ casualty reports retain their immutable attribution and position.
SCR_EntityHelper.DeleteEntityAndChildren(body);
}
}
class ERR_BuryBodyAction : ScriptedUserAction
{
override bool HasLocalEffectOnlyScript() { return false; }
override bool CheckOnServerFirstScript() { return true; }
override bool CanBroadcastScript() { return false; }
override bool CanBeShownScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
return mode && mode.Active() && user != GetOwner() && ERR_Burial.Dead(GetOwner()) && ERR_Burial.ShovelHeld(user);
}
override bool CanBePerformedScript(IEntity user)
{
string reason = ERR_Burial.Rejection(user, GetOwner());
SetCannotPerformReason(reason);
return reason.IsEmpty();
}
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
if (pOwnerEntity != GetOwner() || !ERR_Burial.Permitted(pUserEntity, pOwnerEntity)) return;
// Let the native action finish before destroying its owning manager. The
// deferred callback revalidates, so duplicate completions are harmless.
GetGame().GetCallqueue().CallLater(ERR_Burial.Complete, 1, false, pUserEntity, pOwnerEntity);
}
}
@@ -3,30 +3,44 @@
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.
// pair a new eye pose with an old aiming pose. Five vectors for ordinary/2D,
// ten for PiP: the existing pose/lens data followed by the sampled body origin.
// Older offset-only packets are rejected rather than decoded as world poses.
static bool Packet(array<vector> packet)
{
if (!packet || (packet.Count() != 4 && packet.Count() != 9)) return false;
if (!packet || (packet.Count() != 5 && packet.Count() != 10)) return false;
vector origin = packet[packet.Count() - 1];
if (!Origin(origin) || !Origin(WorldPosition(origin, packet[0]))) 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.Count() == 5) return packet[3][2] == 0;
if (!(packet[3][2] >= 0.01 && packet[3][2] <= 150)) return false;
if (!Origin(WorldPosition(origin, packet[4]))) 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
// both aim and eye position, even when the owner's camera is steady.
// Sample axes, world-axis eye offset and body origin on the owner together.
// Neither the proxy body rotation nor its position is used during playback.
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])
// Reconstruct before interpolation, once per received sample. Both eye and
// scope use the exact same captured origin, never the current proxy position.
static vector WorldPosition(vector origin, vector offset) { return origin + offset; }
static bool Origin(vector position)
{
for (int axis = 0; axis < 3; axis++) view[axis] = pose[axis];
view[3] = body[3] + pose[3];
// Finite coordinate bounds cover large terrains while rejecting NaN/infinity.
return Math.AbsFloat(position[0]) <= 10000000 && Math.AbsFloat(position[1]) <= 10000000 && Math.AbsFloat(position[2]) <= 10000000;
}
static bool NearBody(vector origin, vector authoritative)
{
if (!Origin(origin) || !Origin(authoritative)) return false;
return (origin - authoritative).LengthSq() <= 10000;
}
static bool WorldSample(vector position, vector forward, vector up, float fov, float nearPlane)
{
return Origin(position) && Sample(vector.Zero, forward, up, fov, nearPlane);
}
static bool Sample(vector position, vector forward, vector up, float fov, float nearPlane)
{
@@ -42,6 +56,7 @@ class ERR_ObserverViewRules
class ERR_ObserverViewSample
{
int Stamp;
// Complete sampled world pose, not an offset from the replicated proxy.
vector Position, Forward, Up;
float Fov, Near;
float PipFov, Vignette;
@@ -74,7 +89,7 @@ class ERR_ObserverViewBuffer
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;
if (!ERR_ObserverViewRules.WorldSample(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))
{
@@ -144,13 +159,11 @@ modded class SCR_PlayerController
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);
if (!m_ERREyeBuffer.Read(m_ERRObservedId, now, seconds, transform, fov, nearPlane)) return false;
m_ERRViewBuffer.PIP(pipFov, geometry, vignette);
return true;
}
@@ -160,13 +173,10 @@ modded class SCR_PlayerController
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;
if (!m_ERRViewBuffer.Read(m_ERRObservedId, now, seconds, transform, 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)]
@@ -229,12 +239,14 @@ modded class SCR_PlayerController
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());
packet.Insert(world[3]);
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);
packet.Resize(4); packet[3] = Vector(fov, nearPlane, 0); packet.Insert(world[3]);
}
}
if (packet.Count() == 4) packet.Insert(world[3]);
if (!ERR_ObserverViewRules.Packet(packet)) return;
Rpc(ERR_SubmitPlayerView, ++m_ERRViewSequence, packet, scopeWeapon, scopeData);
}
@@ -242,12 +254,13 @@ modded class SCR_PlayerController
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;
if (packet.Count() == 10 && scopeWeapon == RplId.Invalid()) 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;
if (!ERR_ObserverViewRules.NearBody(packet[packet.Count() - 1], actor.GetOrigin())) 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;
@@ -275,19 +288,20 @@ modded class SCR_PlayerController
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;
if (!ERR_ObserverViewRules.Packet(packet) || !ERR_ObserverScope.Valid(scopeWeapon, scopeData)) return;
if (packet.Count() == 9 && scopeWeapon == RplId.Invalid()) return;
if (packet.Count() == 10 && 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;
bool pip = packet.Count() == 10;
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);
vector origin = packet[packet.Count() - 1];
m_ERRViewBuffer.Push(target, stamp, now, ERR_ObserverViewRules.WorldPosition(origin, 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);
m_ERREyeBuffer.Push(target, stamp, now, ERR_ObserverViewRules.WorldPosition(origin, packet[4]), packet[5], packet[6], packet[7][0], packet[7][1], scopeWeapon, scopeData);
}
else m_ERREyeBuffer.Clear();
}
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
#ifdef WORKBENCH
string historicalFixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
if (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
if (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "unconscious-camera" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
#endif
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
// Existing deterministic probes intentionally use authored coordinates.
string fixture;
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
if (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "unconscious-camera" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
#endif
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
@@ -9,7 +9,7 @@ class ERR_Settings
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "droneReconCount", "droneCooldownSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "showAllSites", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
}
static bool Valid(string key, int value)
@@ -17,6 +17,8 @@ class ERR_Settings
if (key == "aiDoctrine" || key == "aiShadow") return value == 0 || value == 1;
if (key == "sarkaAngerPerCar") return value >= 0 && value <= 30;
if (key == "motorbikeCount") return value >= 0 && value <= 6;
if (key == "droneReconCount") return value >= 0 && value <= 3;
if (key == "droneCooldownSeconds") return value >= 60 && value <= 1800;
if (key == "planeAltitudeMetres") return value >= 150 && value <= 500;
if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800;
if (key == "planeSectorSeconds") return value >= 30 && value <= 600;
@@ -121,6 +123,8 @@ class ERR_Settings
if (key == "aiDoctrine" || key == "aiShadow") return 0;
if (key == "sarkaAngerPerCar") return 3;
if (key == "motorbikeCount") return 2;
if (key == "droneReconCount") return 0;
if (key == "droneCooldownSeconds") return 300;
if (key == "planeAltitudeMetres") return 250;
if (key == "planeReplacementSeconds") return 60;
if (key == "planeSectorSeconds") return 120;
@@ -35,11 +35,10 @@ modded class SCR_PlayerController
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
if (!component) return false;
float now = GetGame().GetWorld().GetWorldTime();
if (!m_ERRProbeViewMissing) m_ERRProbeViewBuffer.Push(component.Id(), now, now, "0 1.6 0", "0 0 1", "0 1 0", 45, 0.0125, RplId.Invalid(), vector.Zero);
vector local[4], world[4];
if (!m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, local, fov, nearPlane)) return false;
target.GetWorldTransform(world); Math3D.MatrixMultiply4(world, local, transform);
return true;
// Feed complete sampled world poses, as the real receiver does. The target
// remains a fixture stand-in; this does not qualify live multiplayer relay.
if (!m_ERRProbeViewMissing) m_ERRProbeViewBuffer.Push(component.Id(), now, now, target.GetOrigin() + "0 1.6 0", "0 0 1", "0 1 0", 45, 0.0125, RplId.Invalid(), vector.Zero);
return m_ERRProbeViewBuffer.Read(component.Id(), now, seconds, transform, fov, nearPlane);
}
string ERR_ProbeAcceptedRecoveryStatus() { return m_ERRShownRecoveryStatus; }
@@ -0,0 +1,180 @@
// Optional RCD 2.3.7 adapter. Foreign types are compiled only after the addon
// is mounted. No workshop asset or player controller override is shipped here.
class ERR_DroneBridge
{
static const string ADDON = "65AD60E204191D37";
protected static ref ScriptModule s_Module;
protected static bool s_Attempted;
static bool Available()
{
array<string> loaded = {}; GameProject.GetLoadedAddons(loaded);
return loaded.Contains(ADDON);
}
static bool Initialize()
{
if (s_Module) return true;
if (s_Attempted || !Replication.IsServer() || !Available()) return false;
s_Attempted = true;
#ifndef WORKBENCH
if (!System.IsConsoleApp()) return false;
#endif
string code;
code += "class ERR_OptionalDroneAPI {\n";
code += "static bool ERR_DroneReady() {\n";
code += "return SAL_DroneConnectionManager.GetInstance()!=null;\n";
code += "}\n";
code += "static int ERR_DroneState(IEntity entity) {\n";
code += "int state=0;\n";
code += "if(entity) {\n";
code += "SAL_DroneControllerComponent control=SAL_DroneControllerComponent.Cast(entity.FindComponent(SAL_DroneControllerComponent));\n";
code += "if(control) {\n";
code += "if(!control.m_bIsDestroyed) state=1;\n";
code += "if(control.m_iOwner != -1 || control.m_bIsConnected || entity.GetParent()) state=2;\n";
code += "}\n";
code += "}\n";
code += "return state;\n";
code += "}\n";
code += "static bool ERR_DroneStart(IEntity e) {\n";
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
code += "SAL_DroneCleanUpComponent clean=SAL_DroneCleanUpComponent.Cast(e.FindComponent(SAL_DroneCleanUpComponent));\n";
code += "SAL_DroneBatteryComponent b=SAL_DroneBatteryComponent.Cast(e.FindComponent(SAL_DroneBatteryComponent));\n";
code += "if(!c || !clean || !b || !c.m_DroneManager || !c.m_CameraManager || !c.m_Drone || !e.GetPhysics() || ERR_DroneState(e)!=1) return false;\n";
code += "c.ClearEventMask(e,EntityEvent.SIMULATE|EntityEvent.FIXEDFRAME);\n";
code += "b.ClearEventMask(e,EntityEvent.FIXEDFRAME);\n";
code += "clean.m_bIsConnected=true;\n";
code += "c.m_bIsArmed=true;\n";
code += "c.m_bIsActive=true;\n";
code += "SAL_DroneNetworkPacket p=new SAL_DroneNetworkPacket();\n";
code += "p.SetDrone(SAL_DroneConnectionManager.GetRplId(e));\n";
code += "p.SetIsArmed(true);\n";
code += "c.m_DroneManager.DisarmDrone(p);\n";
code += "return true;\n";
code += "}\n";
code += "static bool ERR_DroneStep(IEntity e, vector controls, float dt, bool send) {\n";
code += "if(ERR_DroneState(e)!=1) return false;\n";
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
code += "if(!c.m_BatteryComponent || c.m_BatteryComponent.m_fCurrentBattery<=0) return false;\n";
code += "c.m_iThrottle=controls[0];\n";
code += "c.m_iPitch=controls[1];\n";
code += "c.m_iRoll=controls[2];\n";
code += "c.m_iYaw=0;\n";
code += "e.GetPhysics().EnableGravity(true);\n";
code += "e.GetPhysics().SetActive(true);\n";
code += "int old=c.m_iOwner;\n";
code += "c.m_iOwner=SCR_PlayerController.GetLocalPlayerId();\n";
code += "c.EOnSimulate(e,dt);\n";
code += "c.m_iOwner=old;\n";
code += "c.m_BatteryComponent.m_fCurrentBattery=Math.Max(0,c.m_BatteryComponent.m_fCurrentBattery-dt*(1+Math.AbsFloat(controls[1])+Math.AbsFloat(controls[2])));\n";
code += "if(send) c.SendPacketServer(e,dt);\n";
code += "return true;\n";
code += "}\n";
code += "static void ERR_DroneRelease(IEntity e) {\n";
code += "if(!e) return;\n";
code += "SAL_DroneControllerComponent c=SAL_DroneControllerComponent.Cast(e.FindComponent(SAL_DroneControllerComponent));\n";
code += "SAL_DroneBatteryComponent b=SAL_DroneBatteryComponent.Cast(e.FindComponent(SAL_DroneBatteryComponent));\n";
code += "SAL_DroneCleanUpComponent clean=SAL_DroneCleanUpComponent.Cast(e.FindComponent(SAL_DroneCleanUpComponent));\n";
code += "if(!c) return;\n";
code += "c.SetEventMask(e,EntityEvent.SIMULATE|EntityEvent.FIXEDFRAME);\n";
code += "if(b) b.SetEventMask(e,EntityEvent.FIXEDFRAME);\n";
code += "if(ERR_DroneState(e)==2) return;\n";
code += "if(clean) clean.m_bIsConnected=false;\n";
code += "c.m_bIsArmed=false;\n";
code += "c.m_iThrottle=0;\n";
code += "SAL_DroneNetworkPacket p=new SAL_DroneNetworkPacket();\n";
code += "p.SetDrone(SAL_DroneConnectionManager.GetRplId(e));\n";
code += "p.SetIsArmed(false);\n";
code += "if(c.m_DroneManager)c.m_DroneManager.DisarmDrone(p);\n";
code += "}\n";
code += "static bool ERR_DroneJammed(IEntity e) {\n";
code += "SAL_DroneConnectionManager m=SAL_DroneConnectionManager.GetInstance();\n";
code += "if(!m)return true;\n";
code += "int count=0;\n";
code += "foreach(RplId id:m.m_aJammers) {\n";
code += "if(count++>=64)break;\n";
code += "RplComponent r=RplComponent.Cast(Replication.FindItem(id));\n";
code += "if(!r)continue;\n";
code += "IEntity jammer=r.GetEntity();\n";
code += "if(!jammer)continue;\n";
code += "SAL_DroneJammerComponent j=SAL_DroneJammerComponent.Cast(jammer.FindComponent(SAL_DroneJammerComponent));\n";
code += "if(j && vector.Distance(e.GetOrigin(),jammer.GetOrigin())<j.m_fJammingRange)return true;\n";
code += "}\n";
code += "return false;\n";
code += "}\n";
code += "}\n";
code += "bool ERR_DroneReady() {\n";
code += "return ERR_OptionalDroneAPI.ERR_DroneReady();\n";
code += "}\n";
code += "int ERR_DroneState(IEntity e) {\n";
code += "return ERR_OptionalDroneAPI.ERR_DroneState(e);\n";
code += "}\n";
code += "bool ERR_DroneStart(IEntity e) {\n";
code += "return ERR_OptionalDroneAPI.ERR_DroneStart(e);\n";
code += "}\n";
code += "bool ERR_DroneStep(IEntity e, vector controls, float dt, bool send) {\n";
code += "return ERR_OptionalDroneAPI.ERR_DroneStep(e,controls,dt,send);\n";
code += "}\n";
code += "void ERR_DroneRelease(IEntity e) {\n";
code += "ERR_OptionalDroneAPI.ERR_DroneRelease(e);\n";
code += "}\n";
code += "bool ERR_DroneJammed(IEntity e) {\n";
code += "return ERR_OptionalDroneAPI.ERR_DroneJammed(e);\n";
code += "}\n";
string error; int line;
s_Module = ScriptModule.CompileScript(GetGame().GetScriptModule(), code, error, line);
if (!s_Module)
{
PrintFormat("Reintroduced [drone adapter]: line=%1 error=%2", line, error);
ERR_HQ.Event("DRONE UNAVAILABLE", string.Format("optional adapter line=%1: %2", line, error));
}
return s_Module != null;
}
static bool Ready()
{
bool result;
return s_Module && s_Module.Call(null, "ERR_DroneReady", false, result) && result;
}
static int State(IEntity entity)
{
int result;
if (!entity || !s_Module || !s_Module.Call(null, "ERR_DroneState", false, result, entity)) return 0;
return result;
}
static bool Start(IEntity entity)
{
bool result;
return s_Module && s_Module.Call(null, "ERR_DroneStart", false, result, entity) && result;
}
static bool Step(IEntity entity, vector controls, float dt, bool send)
{
bool result;
return s_Module && s_Module.Call(null, "ERR_DroneStep", false, result, entity, controls, dt, send) && result;
}
static void Release(IEntity entity)
{
int ignored;
if (entity && s_Module) s_Module.Call(null, "ERR_DroneRelease", false, ignored, entity);
}
static bool Jammed(IEntity entity)
{
bool result;
return !s_Module || !s_Module.Call(null, "ERR_DroneJammed", false, result, entity) || result;
}
}
class ERR_DroneCatalog
{
ResourceName Prefab;
void Find()
{
if (!ERR_DroneBridge.Available()) return;
SearchResourcesFilter filter = new SearchResourcesFilter();
filter.fileExtensions = {"et"}; filter.recursive = true;
filter.rootPath = "$" + GameProject.GetAddonID(ERR_DroneBridge.ADDON) + ":Prefabs/Drones/DJIBase";
ResourceDatabase.SearchResources(filter, Add);
}
protected void Add(ResourceName resource, string path = "")
{
string name = resource; name.ToLower();
// Exact inspected unarmed airframe; never a dropper, FPV, placeholder or wreck.
if (name.EndsWith("/djibase.et")) Prefab = resource;
}
}
@@ -0,0 +1,259 @@
// Bounded, delivered-report-driven reconnaissance. This scheduler never follows
// a target entity: only the camera sensor may sample a visible actor's position.
class ERR_DroneSchedule
{
float NextLaunch;
float LastObservation = -1;
int Launched, Failures;
bool Stopped;
bool Due(float now, int count, int limit) { return !Stopped && limit > 0 && count < limit && now >= NextLaunch && Launched < 64 && Failures < 3; }
bool Fresh(float observed, float now) { return observed > LastObservation && observed >= 0 && observed <= now && now - observed <= 60000; }
void Attempt(float observed, float now, int cooldown) { LastObservation = observed; NextLaunch = now + cooldown * 1000; }
void Result(bool success) { if (success) Failures = 0; else Failures++; }
}
class ERR_DroneFlightRules
{
static vector Controls(vector velocity, vector desired, vector forward, vector right)
{
vector error = desired - velocity;
return Vector(Math.Clamp(0.49 + error[1] * 0.12, 0.15, 0.85), Math.Clamp(-vector.Dot(error, forward) * 0.12, -0.5, 0.5), Math.Clamp(-vector.Dot(error, right) * 0.12, -0.5, 0.5));
}
static bool CameraCone(vector origin, vector aim, vector point)
{
vector offset = point - origin;
float distance = offset.Length();
return distance >= 5 && distance <= 350 && vector.Dot(offset.Normalized(), (aim - origin).Normalized()) >= 0.8660254;
}
static bool HitsTarget(IEntity hit, IEntity target)
{
if (!target) return false;
// Helmets and other attached equipment have their own collision entities.
// Accept only the intended character's descendants, not an unrelated blocker.
for (int depth = 0; hit && depth < 16; depth++)
{
if (hit == target) return true;
hit = hit.GetParent();
}
return false;
}
static bool Clear(vector from, vector to, IEntity exclude, IEntity target = null)
{
TraceParam trace = new TraceParam(); trace.Start = from; trace.End = to;
trace.Exclude = exclude; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
float fraction = GetGame().GetWorld().TraceMove(trace, null);
return fraction >= 1 || HitsTarget(trace.TraceEnt, target);
}
}
class ERR_DroneFlight
{
IEntity Drone;
IEntity Operator;
SCR_AIGroup Group;
vector Home, Search;
float Created, LastStep, NextPacket, NextScan, NextReport, SearchStarted = -1;
bool Started, Returning, Finished, Released, Counted;
int Candidate;
float NextLink; bool LinkGood;
string Reason = "preparing";
bool Link()
{
float now = GetGame().GetWorld().GetWorldTime();
if (now < NextLink) return LinkGood;
NextLink = now + 1000;
LinkGood = ERR_Actors.CanFight(Operator) && Drone && vector.Distance(Drone.GetOrigin(), Operator.GetOrigin()) <= 1800
&& !ERR_DroneBridge.Jammed(Drone)
&& ERR_DroneFlightRules.Clear(Drone.GetOrigin(), Operator.GetOrigin() + Vector(0, 1.5, 0), Drone, Operator);
return LinkGood;
}
void Finish(string reason)
{
if (Finished) return;
Finished = true; Reason = reason;
ERR_DroneBridge.Release(Drone); Released = true;
ERR_HQ.Event("DRONE RETIRED", reason);
}
void Advance(float now)
{
if (Finished) return;
if (!Drone) { Finish("airframe removed"); return; }
int state = ERR_DroneBridge.State(Drone);
if (state == 2) { Finish("player takeover; control released"); return; }
if (state == 0 || !ERR_Actors.Alive(Drone)) { Finish("airframe destroyed"); return; }
if (!Started)
{
if (now - Created < 1000) return;
if (!ERR_DroneBridge.Start(Drone))
{
if (now - Created >= 5000) Finish("flight adapter initialization failed");
return;
}
Started = true; LastStep = now; Reason = "taking off";
}
float dt = Math.Clamp((now - LastStep) / 1000, 0, 0.1); LastStep = now;
if (dt <= 0) return;
vector position = Drone.GetOrigin();
if (now - Created >= 600000) { Finish("flight time limit reached"); return; }
if (!ERR_Actors.CanFight(Operator)) Returning = true;
if (now - Created > 15000 && !Link()) Returning = true;
if (vector.DistanceXZ(position, Search) < 120 && SearchStarted < 0) SearchStarted = now;
if (SearchStarted >= 0 && now - SearchStarted >= 120000) Returning = true;
if (now - Created >= 360000) Returning = true;
vector goal = Search;
if (Returning) goal = Home;
float ground = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
float altitude = Math.Max(ground, GetGame().GetWorld().GetSurfaceY(goal[0], goal[2])) + 70;
vector horizontal = goal - position; horizontal[1] = 0;
float distance = horizontal.Length();
vector desired = horizontal.Normalized() * Math.Min(8, distance * 0.2);
// Clear obstacles vertically before departing; return to the original clear pad.
if (now - Created < 20000 && position[1] < Home[1] + 30) desired = vector.Zero;
if (Returning && distance < 3) { altitude = Home[1] + 0.15; desired = horizontal * 0.3; }
vector ahead = position + desired * 3;
if (!(Returning && distance < 3)) altitude = Math.Max(altitude, GetGame().GetWorld().GetSurfaceY(ahead[0], ahead[2]) + 40);
desired[1] = Math.Clamp((altitude - position[1]) * 0.6, -2, 3);
if (Returning && distance < 3 && position[1] < Home[1] + 0.7 && now - Created > 10000) { Finish("returned to launch site"); return; }
if (!Returning && now - Created > 45000 && position[1] < ground + 2) { Finish("unable to take off or flight obstructed"); return; }
Physics physics = Drone.GetPhysics();
if (!physics) { Finish("physics unavailable"); return; }
vector controls = ERR_DroneFlightRules.Controls(physics.GetVelocity(), desired, Drone.GetTransformAxis(2), Drone.GetTransformAxis(0));
bool send = now >= NextPacket;
if (send) NextPacket = now + 100;
if (!ERR_DroneBridge.Step(Drone, controls, dt, send)) { Finish("battery exhausted or control lost"); return; }
if (Returning) Reason = "returning";
else Reason = "searching reported area";
}
void Observe(ERR_HQ hq, float now)
{
if (!Started || Finished || Returning || now < NextScan || now < NextReport) return;
NextScan = now + 1000;
if (!Link()) return;
array<IEntity> candidates = {}; ERR_Actors.Players(candidates, true);
if (candidates.IsEmpty()) return;
IEntity actor = candidates[Candidate++ % candidates.Count()];
// Mounted occupants are not visible through a vehicle's hull. A later
// infantry sighting can still dispatch another search at its reported sector.
if (!actor || CompartmentAccessComponent.GetVehicleIn(actor)) return;
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(actor.FindComponent(FactionAffiliationComponent));
if (!affiliation || affiliation.GetAffiliatedFaction() != ERR_GameMode.Get().PlayerFaction()) return;
vector point = actor.GetOrigin() + Vector(0, 1.2, 0);
if (!ERR_DroneFlightRules.CameraCone(Drone.GetOrigin(), Search, point)) return;
if (!ERR_DroneFlightRules.Clear(Drone.GetOrigin(), point, Drone, actor)) return;
ERR_HQReport report = new ERR_HQReport();
report.Target = actor; report.Position = point; report.ObservedAt = now; report.Source = Group;
report.TravelMode = "on foot"; report.SeenEscapists = 1; report.DetailsObservedAt = now; report.DroneObservation = true;
if (hq.QueueDroneObservation(Group, report, now)) NextReport = now + ERR_GameMode.Get().Settings().Runtime("reportIntervalSeconds") * 1000;
}
}
class ERR_DroneRecon
{
protected ERR_GameMode m_Mode;
protected ref ERR_DroneSchedule m_Schedule = new ERR_DroneSchedule();
protected ref ERR_DroneCatalog m_Catalog = new ERR_DroneCatalog();
protected ref array<ref ERR_DroneFlight> m_Flights = {};
protected int m_Limit, m_Cooldown;
protected bool m_Checked, m_Ready, m_Running;
protected string m_Reason = "disabled";
void ERR_DroneRecon(ERR_GameMode mode)
{
m_Mode = mode; m_Limit = mode.Settings().Get("droneReconCount"); m_Cooldown = mode.Settings().Get("droneCooldownSeconds");
if (m_Limit > 0) m_Reason = "waiting for prison escape";
}
bool Ready() { return m_Ready; }
string Status() { return string.Format("Recon drones: %1/%2 active; %3", m_Flights.Count(), m_Limit, m_Reason); }
protected bool Prepare()
{
if (m_Checked) return m_Ready;
m_Checked = true;
if (!ERR_DroneBridge.Available()) { m_Reason = "Realistic Combat Drones missing"; return false; }
if (!ERR_DroneBridge.Initialize()) { m_Reason = "flight adapter unavailable on this host/version"; return false; }
if (!ERR_DroneBridge.Ready()) { m_Reason = "drone manager unavailable"; return false; }
m_Catalog.Find();
if (m_Catalog.Prefab.IsEmpty()) { m_Reason = "supported unarmed DJI prefab unavailable"; return false; }
m_Ready = true; m_Reason = "waiting for a recent HQ sighting";
return true;
}
void Tick(float now)
{
if (!Replication.IsServer() || m_Schedule.Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
for (int i = m_Flights.Count() - 1; i >= 0; i--)
{
ERR_DroneFlight flight = m_Flights[i];
if (!flight.Counted && flight.Started && flight.Drone && flight.Drone.GetOrigin()[1] > flight.Home[1] + 10)
{ flight.Counted = true; m_Schedule.Result(true); }
if (flight.Finished)
{
if (!flight.Counted && flight.Reason != "player takeover; control released") m_Schedule.Result(false);
m_Reason = flight.Reason;
if (m_Schedule.Failures >= 3) m_Reason = "paused after three launch failures";
m_Flights.Remove(i); continue;
}
flight.Observe(hq, now);
}
if (m_Limit == 0 || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return;
array<IEntity> players = {}; ERR_Actors.Players(players, true);
if (players.IsEmpty() || !Prepare() || !m_Schedule.Due(now, m_Flights.Count(), m_Limit)) return;
array<ref ERR_HQReport> contacts = {}; hq.SupportContacts(contacts, now);
ERR_HQReport chosen;
foreach (ERR_HQReport report : contacts)
{
if (!report || report.DroneObservation || report.Distress || !report.Source || !m_Schedule.Fresh(report.ObservedAt, now)) continue;
bool assigned;
foreach (ERR_DroneFlight active : m_Flights) if (active.Group == report.Source || vector.DistanceXZ(active.Search, report.Position) < 250) assigned = true;
if (assigned || !ERR_Actors.CanFight(report.Source.GetLeaderEntity())) continue;
if (!chosen || report.ObservedAt > chosen.ObservedAt) chosen = report;
}
if (!chosen) return;
m_Schedule.Attempt(chosen.ObservedAt, now, m_Cooldown);
bool spawned = Launch(chosen, now, players);
if (spawned) m_Schedule.Launched++;
else m_Schedule.Result(false);
if (m_Schedule.Failures >= 3) m_Reason = "paused after three launch failures";
if (m_Schedule.Launched >= 64) m_Reason = "round launch limit reached";
}
protected bool Launch(ERR_HQReport report, float now, array<IEntity> players)
{
IEntity caller = report.Source.GetLeaderEntity();
if (!ERR_Actors.CanFight(caller) || ERR_HQRadio.Capability(caller) == 0 || vector.DistanceXZ(caller.GetOrigin(), report.Position) > 1200) { m_Reason = "reporting squad has no suitable drone operator"; return false; }
vector pad; bool found;
for (int attempt = 0; attempt < 8; attempt++)
{
float angle = Math.PI2 * attempt / 8;
vector point = ERR_Actors.Ground(caller.GetOrigin() + Vector(Math.Cos(angle) * 8, 0, Math.Sin(angle) * 8));
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) continue;
bool near;
foreach (IEntity player : players) if (vector.DistanceXZ(point, player.GetOrigin()) < 100) near = true;
if (near) continue;
TraceBox trace = new TraceBox(); trace.Start = point + Vector(0, 0.5, 0); trace.End = point + Vector(0, 80, 0);
trace.Mins = "-0.5 -0.25 -0.5"; trace.Maxs = "0.5 0.25 0.5"; trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
pad = trace.Start; found = true; break;
}
if (!found) { m_Reason = "reporting squad has no clear launch pad"; return false; }
IEntity drone = ERR_Actors.Spawn(m_Catalog.Prefab, pad);
if (!drone) { m_Reason = "airframe creation failed"; return false; }
if (!ERR_Actors.SetFaction(drone, m_Mode.EnemyFaction())) { ERR_Actors.DeleteSpawned(drone); m_Reason = "airframe faction unavailable"; return false; }
m_Mode.Own(drone);
ERR_DroneFlight flight = new ERR_DroneFlight();
flight.Drone = drone; flight.Operator = caller; flight.Group = report.Source; flight.Home = pad; flight.Search = report.Position; flight.Created = now;
m_Flights.Insert(flight); m_Reason = "responding to reported sightings";
if (!m_Running) { m_Running = true; GetGame().GetCallqueue().CallLater(Advance, 50, true); }
ERR_HQ.Event("DRONE LAUNCH", string.Format("report M%1; launch=%2; search=%3", report.MessageId, pad, report.Position));
return true;
}
protected void Advance()
{
if (m_Schedule.Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return; }
float now = GetGame().GetWorld().GetWorldTime();
foreach (ERR_DroneFlight flight : m_Flights) flight.Advance(now);
if (m_Flights.IsEmpty()) { GetGame().GetCallqueue().Remove(Advance); m_Running = false; }
}
void Stop()
{
if (m_Schedule.Stopped) return;
m_Schedule.Stopped = true; m_Reason = "mission stopped";
GetGame().GetCallqueue().Remove(Advance); m_Running = false;
foreach (ERR_DroneFlight flight : m_Flights) flight.Finish("mission stopped");
m_Flights.Clear();
}
}
+25 -1
View File
@@ -124,6 +124,8 @@ class ERR_HQ : ERR_CommandService
ERR_AirAssaultSystem AirAssault() { return m_AirAssault; }
protected ref ERR_AirReconSystem m_Recon;
protected ref ERR_PlaneRecon m_PlaneRecon;
protected ref ERR_DroneRecon m_DroneRecon;
ERR_DroneRecon DroneRecon() { return m_DroneRecon; }
ERR_PlaneRecon PlaneRecon() { return m_PlaneRecon; }
ERR_AirReconSystem Recon() { return m_Recon; }
ERR_ScoutSystem Scouts() { return m_Scouts; }
@@ -180,6 +182,7 @@ class ERR_HQ : ERR_CommandService
m_Scouts = new ERR_ScoutSystem(mode);
m_Recon = new ERR_AirReconSystem(mode);
m_PlaneRecon = new ERR_PlaneRecon(mode);
m_DroneRecon = new ERR_DroneRecon(mode);
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
m_Airfield = new ERR_AirfieldSupport(this, mode); m_Recovery = new ERR_BodyRecovery(this, mode);
bool legacyFixture;
@@ -504,6 +507,24 @@ class ERR_HQ : ERR_CommandService
}
return null;
}
// A camera observation goes through the physical operator's existing radio
// route and delivery queue. It never writes directly into HQ memory.
bool QueueDroneObservation(SCR_AIGroup group, ERR_HQReport report, float now)
{
if (!Replication.IsServer() || !group || !report || !report.DroneObservation || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return false;
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != group || unit.Uplink || !unit.Radio || !ERR_Actors.CanFight(unit.Radio)) continue;
ERR_HQRoute route = ObservationRoute(unit);
if (!route) return false;
report.MessageId = m_Journal.Message(); report.Source = group;
route.MessageId = report.MessageId; route.Payload = "drone camera; " + report.Description(now);
unit.PendingReport = report.Snapshot(); unit.Uplink = route;
m_Journal.Write("DRONE REPORT", route.Payload, report.MessageId);
return true;
}
return false;
}
void SupportContacts(out array<ref ERR_HQReport> contacts, float now)
{
contacts.Clear();
@@ -537,10 +558,11 @@ class ERR_HQ : ERR_CommandService
ERR_HQJournal Journal() { return m_Journal; }
string Status()
{
return string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
string status = string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
+ string.Format("\nHQ memory: %1 tracked reports; %2 searched areas.\nAggression: %3 of 30, based on %4 identified bodies (one level per three bodies).", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies())
+ "\n" + m_Hunters.Status() + "\n" + m_Mode.Forces().Status() + "\n" + m_Mortars.Status()
+ "\n" + m_Logistics.Status() + "\n" + m_Resupply.Status() + "\n" + m_Recon.Status() + "\n" + m_PlaneRecon.Status();
return status + "\n" + m_DroneRecon.Status();
}
protected void BindPhysical(ERR_HQUnit unit)
@@ -1746,6 +1768,7 @@ class ERR_HQ : ERR_CommandService
// Aircraft already airborne keep flying even when the server is empty.
m_Recon.Tick(now);
m_PlaneRecon.Tick(now);
m_DroneRecon.Tick(now);
m_AirAssault.Maintain(now);
m_Airfield.Maintain(now);
}
@@ -1880,6 +1903,7 @@ class ERR_HQ : ERR_CommandService
m_AirAssault.Stop();
m_Recon.Stop();
m_PlaneRecon.Stop();
m_DroneRecon.Stop();
m_Scouts.Stop();
m_ConvoyStartup.Stop();
if (m_SupplyConvoys) m_SupplyConvoys.Stop();
@@ -51,6 +51,7 @@ class ERR_HQReport
bool HasVelocity;
bool Engaged;
bool Distress;
bool DroneObservation;
int SeenEscapists = -1;
string TravelMode = "unknown";
string RoadStatus = "road unknown";
@@ -72,6 +73,7 @@ class ERR_HQReport
copy.ObservedAt = ObservedAt;
copy.Engaged = Engaged;
copy.Distress = Distress;
copy.DroneObservation = DroneObservation;
copy.HasVelocity = HasVelocity;
copy.SeenEscapists = SeenEscapists;
copy.TravelMode = TravelMode;
@@ -67,5 +67,19 @@ class ERR_RecommendedAddons
Entry(values, "weapons", loaded, {"BFC890A6F1603EC0"}, {"Phaseline Marksman Rifles"}, mode.Settings().Get("automaticEquipmentChancePercent") != 0);
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
if (hq) values.Insert("recommendedBikeRuntime=" + hq.Hunters().MotorbikeStatus());
string droneState = "Missing: Realistic Combat Drones";
if (ERR_DroneBridge.Available())
{
droneState = "Loaded - disabled in mission settings";
if (mode.Settings().Get("droneReconCount") > 0) droneState = "Loaded - enabled for next round; see current-round status below";
}
values.Insert("recommended_drones=" + droneState);
int droneReady;
if (hq && hq.DroneRecon())
{
values.Insert("droneRuntime=" + hq.DroneRecon().Status());
if (hq.DroneRecon().Ready()) droneReady = 1;
}
values.Insert("recommendedReady_drones=" + droneReady.ToString());
}
}