Add recon drones and body burial; stabilize spectator camera
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user