diff --git a/GameMod/GameMod.csproj b/GameMod/GameMod.csproj
index cb8e7ba8..7d47b2ad 100644
--- a/GameMod/GameMod.csproj
+++ b/GameMod/GameMod.csproj
@@ -153,6 +153,7 @@
+
diff --git a/GameMod/MPDestructibleMissiles.cs b/GameMod/MPDestructibleMissiles.cs
new file mode 100644
index 00000000..54d70275
--- /dev/null
+++ b/GameMod/MPDestructibleMissiles.cs
@@ -0,0 +1,418 @@
+using HarmonyLib;
+using Newtonsoft.Json.Linq;
+using Overload;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace GameMod
+{
+ // server side hp tracker attached to each live missile
+ class DestructibleMissile : MonoBehaviour
+ {
+ private float m_hp;
+ private bool m_destroyed;
+ private Projectile m_proj;
+
+ // prevents multi collisions by tracking attacker id and collider
+ private readonly HashSet m_hit_by = new HashSet();
+ private readonly List m_ignored = new List();
+
+ public void Initialize(Projectile proj, float hp)
+ {
+ RestoreIgnoredCollisions();
+ m_hp = hp;
+ m_proj = proj;
+ m_destroyed = false;
+ m_hit_by.Clear();
+ }
+
+ // returns true when the missile survived a plain projectile hit and the attacker should be consumed
+ public bool AbsorbHit(Projectile attacker)
+ {
+ if (ShouldIgnore(attacker))
+ return false;
+
+ var other = MPDestructibleMissiles.FindInParents(
+ attacker.c_collider != null ? attacker.c_collider.gameObject : attacker.c_go);
+ if (other != null)
+ {
+ ResolveMissileDuel(other, attacker);
+ return false;
+ }
+ return TakeHit(attacker);
+ }
+
+ private bool ShouldIgnore(Projectile attacker)
+ {
+ if (m_destroyed || m_proj == null || !m_proj.m_alive)
+ return true;
+ if (attacker == null || attacker == m_proj || !attacker.m_alive)
+ return true;
+ if (attacker.m_owner == m_proj.m_owner)
+ return true;
+ if (m_proj.m_mp_team != MpTeam.ANARCHY && attacker.m_mp_team == m_proj.m_mp_team)
+ return true;
+ return !m_hit_by.Add(AttackerKey(attacker));
+ }
+
+ private bool TakeHit(Projectile attacker)
+ {
+ m_hp -= MPDestructibleMissiles.Damage(attacker);
+ Settle(this, attacker);
+ return !m_destroyed;
+ }
+
+ // both missiles take the damage of the weaker one
+ private void ResolveMissileDuel(DestructibleMissile other, Projectile attacker)
+ {
+ other.m_hit_by.Add(AttackerKey(m_proj)); // the mirrored collision event becomes a no op
+ float dmg = Mathf.Min(MPDestructibleMissiles.Damage(attacker), MPDestructibleMissiles.Damage(m_proj));
+ m_hp -= dmg;
+ other.m_hp -= dmg;
+ Settle(this, attacker);
+ Settle(other, m_proj);
+ if (!m_destroyed && !other.m_destroyed)
+ IgnoreFurtherContacts(other);
+ }
+
+ // explode when hp runs out
+ private static void Settle(DestructibleMissile m, Projectile attacker)
+ {
+ if (m.m_hp > 0f)
+ return;
+ m.m_destroyed = true;
+ MPDestructibleMissiles.Destroy(m.m_proj, attacker);
+ }
+
+ private static int AttackerKey(Projectile p)
+ {
+ return p.m_projectile_id >= 0 ? p.m_projectile_id : p.GetInstanceID();
+ }
+
+ // survivors of a mutual hit stop bouncing off each other
+ private void IgnoreFurtherContacts(DestructibleMissile other)
+ {
+ Collider a = m_proj != null ? m_proj.c_collider : null;
+ Collider b = other.m_proj != null ? other.m_proj.c_collider : null;
+ if (a == null || b == null)
+ return;
+ Physics.IgnoreCollision(a, b, true);
+ m_ignored.Add(b);
+ other.m_ignored.Add(a);
+ }
+
+ private void RestoreIgnoredCollisions()
+ {
+ Collider mine = m_proj != null ? m_proj.c_collider : null;
+ foreach (var other in m_ignored)
+ if (mine != null && other != null &&
+ mine.enabled && mine.gameObject.activeInHierarchy &&
+ other.enabled && other.gameObject.activeInHierarchy)
+ Physics.IgnoreCollision(mine, other, false);
+ m_ignored.Clear();
+ }
+ }
+
+ static class MPDestructibleMissiles
+ {
+ public const int MISSILE_LAYER = 27; // rp ignore layer
+ public static bool Enabled = false;
+
+ // falcon pod hunt creeper nova dev time vortex
+ public static readonly float[] FullDamage = { 22f, 8f, 11f, 16.75f, 120f, 120f, 120f, 56f };
+
+ // default hp is half of what the missile itself deals
+ public static readonly float[] DefaultHealth = { 11f, 4f, 5.5f, 8.3525f, 60f, 60f, 60f, 28f };
+
+ public static float[] MissileTypeHealth = (float[])DefaultHealth.Clone();
+ private static float[] serverOverrides;
+
+ private static readonly System.Reflection.FieldInfo ProjDamageField =
+ AccessTools.Field(typeof(Projectile), "m_damage");
+
+ public static float Damage(Projectile p)
+ {
+ int idx = MissileIndex(p.m_type);
+ return idx >= 0 ? FullDamage[idx] : (float)ProjDamageField.GetValue(p);
+ }
+
+ public static void ApplyServerOverrides()
+ {
+ if (!GameplayManager.IsDedicatedServer() && !Overload.NetworkManager.IsServer())
+ return;
+ if (serverOverrides == null)
+ {
+ serverOverrides = new float[8];
+ var j = Config.Settings != null ? Config.Settings["missileTypeHealth"] as JObject : null;
+ if (j != null)
+ for (int i = 0; i < 8; i++)
+ if (j.TryGetValue(((MissileType)i).ToString(), System.StringComparison.OrdinalIgnoreCase, out JToken tok))
+ serverOverrides[i] = (float)tok;
+ }
+ for (int i = 0; i < 8; i++)
+ if (serverOverrides[i] > 0f)
+ MissileTypeHealth[i] = serverOverrides[i];
+ }
+
+ public static T FindInParents(GameObject go) where T : Component
+ {
+ for (Transform t = go != null ? go.transform : null; t != null; t = t.parent)
+ {
+ T c = t.GetComponent();
+ if (c != null)
+ return c;
+ }
+ return null;
+ }
+
+ public static int MissileIndex(ProjPrefab t)
+ {
+ switch (t)
+ {
+ case ProjPrefab.missile_falcon: return (int)MissileType.FALCON;
+ case ProjPrefab.missile_pod: return (int)MissileType.MISSILE_POD;
+ case ProjPrefab.missile_hunter: return (int)MissileType.HUNTER;
+ case ProjPrefab.missile_creeper: return (int)MissileType.CREEPER;
+ case ProjPrefab.missile_smart: return (int)MissileType.NOVA;
+ case ProjPrefab.missile_devastator: return (int)MissileType.DEVASTATOR;
+ case ProjPrefab.missile_timebomb: return (int)MissileType.TIMEBOMB;
+ case ProjPrefab.missile_vortex: return (int)MissileType.VORTEX;
+ default: return -1;
+ }
+ }
+
+ // server relayers the missile and tracks its hp
+ public static void SetupServer(Projectile proj, int idx)
+ {
+ GameObject holder = proj.c_collider != null ? proj.c_collider.gameObject : proj.c_go;
+ holder.layer = MISSILE_LAYER;
+ proj.c_go.layer = MISSILE_LAYER;
+ var comp = holder.GetComponent() ?? holder.AddComponent();
+ comp.Initialize(proj, MissileTypeHealth[idx]);
+ }
+
+ public static void SetupClient(Projectile proj)
+ {
+ GameObject holder = proj.c_collider != null ? proj.c_collider.gameObject : proj.c_go;
+ if (holder.layer == MISSILE_LAYER)
+ holder.layer = proj.c_go.layer;
+ }
+
+ // server side kill, attacker is only set when a missile got shot down
+ public static void Destroy(Projectile proj, Projectile attacker = null)
+ {
+ if (proj == null || !proj.m_alive)
+ return;
+ if (Server.ProjectileTypeHasLaunchDataSynced(proj.m_type))
+ {
+ var attackerId = attacker != null && attacker.m_owner_player != null
+ ? attacker.m_owner_player.netId : default(NetworkInstanceId);
+ BroadcastDestroy(proj.m_type, proj.m_projectile_id, proj.c_transform.position, attackerId);
+ }
+ proj.Explode(true);
+ }
+
+ public static void BroadcastDestroy(ProjPrefab type, int projId, Vector3 pos, NetworkInstanceId attackerId)
+ {
+ SendToCapable(MessageTypes.MsgDestroyMissile, new DestroyMissileMessage
+ {
+ m_proj_type = type,
+ m_proj_id = projId,
+ m_pos = pos,
+ m_attacker_net_id = attackerId
+ });
+ }
+
+ private static void SendToCapable(short msgType, MessageBase msg)
+ {
+ foreach (var conn in NetworkServer.connections)
+ if (conn != null && MPTweaks.ClientHasTweak(conn.connectionId, "destructiblemissiles"))
+ conn.SendByChannel(msgType, msg, 0);
+ }
+
+ // reliable vs unreliable channel potential time difference. keep information a bit longer to potentially resend
+ private const float PENDING_TTL = 1f;
+
+ private class PendingDestroy
+ {
+ public ProjPrefab type;
+ public int id;
+ public Vector3 pos;
+ public float deadline;
+ }
+
+ private static readonly List s_pending = new List();
+
+ public static void ApplyOrQueueDestroy(ProjPrefab type, int id, Vector3 pos)
+ {
+ var u = new PendingDestroy { type = type, id = id, pos = pos };
+ if (!TryApply(u))
+ {
+ u.deadline = Time.unscaledTime + PENDING_TTL;
+ s_pending.Add(u);
+ }
+ }
+
+ private static bool TryApply(PendingDestroy u)
+ {
+ var proj = ProjectileManager.FindProjectileById(u.type, u.id);
+ if (proj == null || !proj.m_alive)
+ return false;
+ if (proj.c_transform != null)
+ proj.c_transform.position = u.pos;
+ proj.Explode(true);
+ return true;
+ }
+
+ public static void ProcessPending()
+ {
+ if (s_pending.Count == 0)
+ return;
+ float now = Time.unscaledTime;
+ for (int i = s_pending.Count - 1; i >= 0; i--)
+ if (TryApply(s_pending[i]) || s_pending[i].deadline < now)
+ s_pending.RemoveAt(i);
+ }
+
+ public static void PlayHitConfirm(NetworkInstanceId attackerId)
+ {
+ var local = GameManager.m_local_player;
+ if (local == null || attackerId != local.netId)
+ return;
+ SFXCueManager.PlayCue2D(SFXCue.impact_energy, 0.8f, 0.25f);
+ }
+ }
+
+ class DestroyMissileMessage : MessageBase
+ {
+ public ProjPrefab m_proj_type;
+ public int m_proj_id;
+ public Vector3 m_pos;
+ public NetworkInstanceId m_attacker_net_id;
+
+ public override void Serialize(NetworkWriter writer)
+ {
+ writer.Write((int)m_proj_type);
+ writer.Write(m_proj_id);
+ writer.Write(m_pos);
+ writer.Write(m_attacker_net_id);
+ }
+
+ public override void Deserialize(NetworkReader reader)
+ {
+ m_proj_type = (ProjPrefab)reader.ReadInt32();
+ m_proj_id = reader.ReadInt32();
+ m_pos = reader.ReadVector3();
+ m_attacker_net_id = reader.ReadNetworkId();
+ }
+ }
+
+ [HarmonyPatch(typeof(GameManager), "Awake")]
+ class MPDestructibleMissiles_GameManager_Awake
+ {
+ // 11 robots
+ // 13 projectiles
+ // 14 level
+ // 16 ships
+ // 17 doors
+ // 18 destroyables
+ // 23 solid
+ // 24 area effects
+ // 26 lava
+ // 31 monsterball
+ static readonly int[] RelevantLayers = { 11, 13, 14, 16, 17, 18, 23, 24, 26, 31 };
+
+ static void Postfix()
+ {
+ Physics.IgnoreLayerCollision(MPDestructibleMissiles.MISSILE_LAYER, MPDestructibleMissiles.MISSILE_LAYER, false);
+ foreach (int layer in RelevantLayers)
+ Physics.IgnoreLayerCollision(MPDestructibleMissiles.MISSILE_LAYER, layer, false);
+
+ MPDestructibleMissiles.ApplyServerOverrides();
+ }
+ }
+
+ [HarmonyPatch(typeof(GameManager), "Update")]
+ class MPDestructibleMissiles_GameManager_Update
+ {
+ static void Postfix()
+ {
+ MPDestructibleMissiles.ProcessPending();
+ }
+ }
+
+ [HarmonyPatch(typeof(Projectile), "ProcessCollision")]
+ class MPDestructibleMissiles_Projectile_ProcessCollision
+ {
+ static bool Prefix(Projectile __instance, GameObject collider)
+ {
+ if (!MPDestructibleMissiles.Enabled)
+ return true;
+
+ if (__instance.c_go.layer == MPDestructibleMissiles.MISSILE_LAYER && collider.layer == 13)
+ return false;
+
+ if (collider.layer == MPDestructibleMissiles.MISSILE_LAYER && Overload.NetworkManager.IsServer())
+ {
+ var missile = MPDestructibleMissiles.FindInParents(collider);
+ if (missile != null)
+ {
+ if (missile.AbsorbHit(__instance))
+ MPDestructibleMissiles.Destroy(__instance);
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ [HarmonyPatch(typeof(Projectile), "OnTriggerEnter")]
+ class MPDestructibleMissiles_Projectile_OnTriggerEnter
+ {
+ static bool Prefix(Projectile __instance, Collider other)
+ {
+ if (!MPDestructibleMissiles.Enabled || other == null || !Overload.NetworkManager.IsServer())
+ return true;
+
+ var missile = MPDestructibleMissiles.FindInParents(other.gameObject);
+ if (missile == null)
+ return true;
+ if (missile.AbsorbHit(__instance))
+ MPDestructibleMissiles.Destroy(__instance);
+ return false;
+ }
+ }
+
+ [HarmonyPatch(typeof(Projectile), "Fire")]
+ class MPDestructibleMissiles_Projectile_Fire
+ {
+ static void Postfix(Projectile __instance)
+ {
+ int idx = MPDestructibleMissiles.MissileIndex(__instance.m_type);
+ if (idx < 0)
+ return;
+ if (MPDestructibleMissiles.Enabled && GameplayManager.IsMultiplayerActive && Overload.NetworkManager.IsServer())
+ MPDestructibleMissiles.SetupServer(__instance, idx);
+ else
+ MPDestructibleMissiles.SetupClient(__instance);
+ }
+ }
+
+ [HarmonyPatch(typeof(Client), "RegisterHandlers")]
+ class MPDestructibleMissiles_Client_RegisterHandlers
+ {
+ static void Postfix(UnityEngine.Networking.NetworkClient ___m_network_client)
+ {
+ ___m_network_client.RegisterHandler(MessageTypes.MsgDestroyMissile, OnDestroyMissile);
+ }
+
+ static void OnDestroyMissile(NetworkMessage netMsg)
+ {
+ var msg = netMsg.ReadMessage();
+ MPDestructibleMissiles.PlayHitConfirm(msg.m_attacker_net_id);
+ MPDestructibleMissiles.ApplyOrQueueDestroy(msg.m_proj_type, msg.m_proj_id, msg.m_pos);
+ }
+ }
+}
diff --git a/GameMod/MPDoors.cs b/GameMod/MPDoors.cs
index efa5a0f4..1c6d730e 100644
--- a/GameMod/MPDoors.cs
+++ b/GameMod/MPDoors.cs
@@ -11,7 +11,8 @@ internal class MPShootDoors
// In Multiplayer, players now open doors by shooting them or touching them.
private static void Postfix(GameObject go, ref bool __result)
{
- if (go != null && (go.layer == 13 || go.layer == 9 || go.layer == 31) && GameplayManager.IsMultiplayerActive)
+ // layer 27: server-side destructible missiles (re-layered from 13 by MPDestructibleMissiles)
+ if (go != null && (go.layer == 13 || go.layer == 9 || go.layer == 31 || go.layer == MPDestructibleMissiles.MISSILE_LAYER) && GameplayManager.IsMultiplayerActive)
__result = true;
}
}
diff --git a/GameMod/MPMatchInfo.cs b/GameMod/MPMatchInfo.cs
index 1f827e1c..2c2b9719 100644
--- a/GameMod/MPMatchInfo.cs
+++ b/GameMod/MPMatchInfo.cs
@@ -118,6 +118,12 @@ public static void DrawMatchInfo(UIElement uie, Vector2 position)
uie.DrawStringSmall("SMASH ATTACK ENABLED", position - Vector2.right * LEFT_OFFSET, TEXT_SIZE, StringOffset.LEFT, UIManager.m_col_ui1, 1f, 200f);
position.y += LINE_SIZE;
}
+ if (MPModPrivateData.DestructibleMissiles)
+ {
+ show = true;
+ uie.DrawStringSmall("DESTRUCTIBLE MISSILES ENABLED", position - Vector2.right * LEFT_OFFSET, TEXT_SIZE, StringOffset.LEFT, UIManager.m_col_ui1, 1f, 200f);
+ position.y += LINE_SIZE;
+ }
if (MPModPrivateData.CtfCarrierBoostEnabled)
{
show = true;
diff --git a/GameMod/MPModPrivateData.cs b/GameMod/MPModPrivateData.cs
index e8b43f9f..b650e791 100644
--- a/GameMod/MPModPrivateData.cs
+++ b/GameMod/MPModPrivateData.cs
@@ -1282,7 +1282,7 @@ public static int LoadoutFilterBitmask
get { return MPLoadouts.LoadoutFilterBitmask; }
set { MPLoadouts.LoadoutFilterBitmask = value; }
}
-
+
public static bool ClientPhysics
{
get { return MPServerOptimization.enabled; }
@@ -1303,6 +1303,18 @@ public static int RollSpeedLimit
}
}
+ public static bool DestructibleMissiles
+ {
+ get { return MPDestructibleMissiles.Enabled; }
+ set { MPDestructibleMissiles.Enabled = value; }
+ }
+
+ public static float[] MissileTypeHealth
+ {
+ get { return MPDestructibleMissiles.MissileTypeHealth; }
+ set { MPDestructibleMissiles.MissileTypeHealth = value; }
+ }
+
public static JObject Serialize()
{
JObject jobject = new JObject();
@@ -1333,6 +1345,8 @@ public static JObject Serialize()
jobject["loadoutfilter"] = (int)LoadoutFilterBitmask;
jobject["clientphysics"] = ClientPhysics;
jobject["rollspeedlimit"] = RollSpeedLimit;
+ jobject["destructiblemissiles"] = DestructibleMissiles;
+ jobject["missiletypehealth"] = new JArray(MissileTypeHealth.Select(h => (JToken)(float)h));
return jobject;
}
@@ -1369,6 +1383,12 @@ public static void Deserialize(JToken root)
LoadoutFilterBitmask = root["loadoutfilter"].GetInt(MPLoadouts.MASK_DEFAULT);
ClientPhysics = root["clientphysics"].GetBool(false);
RollSpeedLimit = root["rollspeedlimit"].GetInt(7);
+ DestructibleMissiles = root["destructiblemissiles"].GetBool(false);
+ var hArr = root["missiletypehealth"] as JArray;
+ if (hArr != null && hArr.Count == 8)
+ MissileTypeHealth = hArr.Select((t, i) => t?.Value() ?? MPDestructibleMissiles.DefaultHealth[i]).ToArray();
+ // a server's olmodsettings.json HP overrides win over the match creator's values
+ MPDestructibleMissiles.ApplyServerOverrides();
}
public static string GetModeString(MatchMode mode)
@@ -1666,6 +1686,7 @@ public static void PatchModPrivateData(MatchmakerPlayerRequest matchmakerPlayerR
MPModPrivateData.LoadoutFilterBitmask = MPLoadouts.LoadoutFilterBitmask;
MPModPrivateData.ClientPhysics = MPServerOptimization.prefEnabled;
MPModPrivateData.RollSpeedLimit = MPServerOptimization.RollSpeedLimit;
+ MPModPrivateData.DestructibleMissiles = Menus.mms_destructible_missiles;
if (Menus.mms_mp_projdata_fn == "STOCK") {
MPModPrivateData.CustomProjdata = string.Empty;
} else {
diff --git a/GameMod/MPSetup.cs b/GameMod/MPSetup.cs
index 1e01aa2e..20ed728c 100644
--- a/GameMod/MPSetup.cs
+++ b/GameMod/MPSetup.cs
@@ -235,6 +235,7 @@ static void Postfix(string filename)
Menus.mms_classic_spawns = ModPrefs.GetBool("MP_CLASSIC_SPAWNS", Menus.mms_classic_spawns);
Menus.mms_assist_scoring = ModPrefs.GetBool("MP_ASSIST_SCORING", Menus.mms_assist_scoring);
Menus.mms_allow_smash = ModPrefs.GetBool("MP_ALLOW_SMASH", Menus.mms_allow_smash);
+ Menus.mms_destructible_missiles = ModPrefs.GetBool("MP_DESTRUCTIBLE_MISSILES", Menus.mms_destructible_missiles);
JoystickRotationFix.alt_turn_ramp_mode = ModPrefs.GetBool("SCALE_UP_ROTATION", JoystickRotationFix.alt_turn_ramp_mode);
MPColoredPlayerNames.isActive = ModPrefs.GetBool("MP_COLORED_NAMES", MPColoredPlayerNames.isActive);
@@ -391,6 +392,7 @@ private static void Prefix(string filename)
ModPrefs.SetBool("MP_CLASSIC_SPAWNS", Menus.mms_classic_spawns);
ModPrefs.SetBool("MP_ASSIST_SCORING", Menus.mms_assist_scoring);
ModPrefs.SetBool("MP_ALLOW_SMASH", Menus.mms_allow_smash);
+ ModPrefs.SetBool("MP_DESTRUCTIBLE_MISSILES", Menus.mms_destructible_missiles);
ModPrefs.SetBool("MP_CREEPER_COLORS", Menus.mms_creeper_colors);
ModPrefs.SetBool("MP_AUDIOTAUNTS_ACTIVE", MPAudioTaunts.AClient.active);
ModPrefs.SetInt("MP_AUDIOTAUNT_VOLUME", MPAudioTaunts.AClient.audio_taunt_volume);
diff --git a/GameMod/MPTweaks.cs b/GameMod/MPTweaks.cs
index d189153a..6014a717 100644
--- a/GameMod/MPTweaks.cs
+++ b/GameMod/MPTweaks.cs
@@ -287,7 +287,7 @@ private static void Prefix(AcceptedToLobbyMessage accept_msg)
caps.Add("ModVersion", OlmodVersion.FullVersionString);
caps.Add("Modded", Core.GameMod.Modded ? "1" : "0");
caps.Add("ModsLoaded", Core.GameMod.ModsLoaded);
- caps.Add("SupportsTweaks", "changeteam,deathreview,sniper,jip,nocompress_0_3_6,customloadouts,damagenumbers,ctfjoin,efirepacket,incompleteloadouts,cphysics" + (MPAudioTaunts.AClient.active ? ",audiotaunts":""));
+ caps.Add("SupportsTweaks", "changeteam,deathreview,sniper,jip,nocompress_0_3_6,customloadouts,damagenumbers,ctfjoin,efirepacket,incompleteloadouts,cphysics,destructiblemissiles"+ (MPAudioTaunts.AClient.active ? ",audiotaunts":""));
caps.Add("ModPrivateData", "1");
caps.Add("ClassicWeaponSpawns", "1");
caps.Add("NetVersion", MPTweaks.NET_VERSION.ToString());
diff --git a/GameMod/Menus.cs b/GameMod/Menus.cs
index 55fc67de..edae3a2b 100644
--- a/GameMod/Menus.cs
+++ b/GameMod/Menus.cs
@@ -40,6 +40,7 @@ public static bool mms_scale_respawn_time
public static bool mms_always_cloaked { get; set; }
public static bool mms_allow_smash { get; set; }
public static bool mms_damage_numbers { get; set; }
+ public static bool mms_destructible_missiles { get; set; }
public static bool mms_client_damage_numbers { get; set; } = true;
public static bool mms_assist_scoring { get; set; } = true;
public static bool mms_team_color_default { get; set; } = true;
@@ -131,6 +132,11 @@ public static string GetMMSDamageNumbers()
return MenuManager.GetToggleSetting(Convert.ToInt32(mms_damage_numbers));
}
+ public static string GetMMSDestructibleMissiles()
+ {
+ return MenuManager.GetToggleSetting(Convert.ToInt32(mms_destructible_missiles));
+ }
+
public static string GetMMSScaleRespawnTime()
{
return MenuManager.GetToggleSetting(Convert.ToInt32(mms_scale_respawn_time));
@@ -323,11 +329,11 @@ private static void DrawRightColumn(UIElement uie, ref Vector2 position)
position.x += 600f;
position.y = col_top - 250f;
uie.SelectAndDrawStringOptionItem(Loc.LS("ALLOW REAR VIEW CAMERA"), position, 11, Menus.GetMMSRearViewPIP(), Loc.LS("CLIENTS CAN CHOOSE TO HAVE REAR VIEW"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("ALWAYS CLOAKED"), position, 15, Menus.GetMMSAlwaysCloaked(), Loc.LS("SHIPS ARE ALWAYS CLOAKED"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("CLASSIC SPAWNS"), position, 13, Menus.GetMMSClassicSpawns(), Loc.LS("SPAWN WITH IMPULSE+ DUALS AND FALCONS"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
if (MenuManager.mms_mode == ExtMatchMode.CTF)
{
@@ -338,22 +344,25 @@ private static void DrawRightColumn(UIElement uie, ref Vector2 position)
uie.SelectAndDrawStringOptionItem(Loc.LS("ASSISTS"), position, 18, Menus.GetMMSAssistScoring(), Loc.LS("AWARD POINTS FOR ASSISTING WITH KILLS"), 1f, false);
}
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("PROJECTILE DATA"), position, 16, Menus.mms_mp_projdata_fn == "STOCK" ? "STOCK" : System.IO.Path.GetFileName(Menus.mms_mp_projdata_fn), string.Empty, 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("ALLOW SMASH ATTACK"), position, 17, Menus.GetMMSAllowSmash(), Loc.LS("ALLOWS PLAYERS TO USE THE SMASH ATTACK"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("TB PENETRATION"), position, 20, MPThunderboltPassthrough.isAllowed ? "ON" : "OFF", Loc.LS("ALLOWS THUNDERBOLT SHOTS TO PENETRATE SHIPS"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("DAMAGE NUMBERS"), position, 21, Menus.GetMMSDamageNumbers(), Loc.LS("SHOWS THE DAMAGE YOU DO TO OTHER SHIPS"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("CLIENT-SIDE PHYSICS"), position, 23, MPServerOptimization.prefEnabled ? "ENABLED" : "DISABLED", Loc.LS("ALLOWS CLIENTS TO PASS PRE-PROCESSED PHYSICS RATHER THAN RESIMULATING INPUTS ON THE SERVER"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
//uie.SelectAndDrawStringOptionItem(Loc.LS("SERVER INPUT BUFFER LENGTH"), position, 24, MPServerOptimization.InputBufferLength.ToString(), Loc.LS("NUMBER OF FRAMES TO BUFFER BEFORE SERVER PROCESSES INPUT IT HAS RECEIVED (OVERLOAD STOCK IS 3, TESTING VERSION IS 2)"), 1f, false); // TESTING
uie.SelectAndDrawStringOptionItem(Loc.LS("ROLL SPEED LIMIT"), position, 24, "+" + (MPServerOptimization.RollSpeedLimit - 3), Loc.LS("MAXIMUM ROLL SPEED MODIFIER TO ALLOW FOR THIS ROUND"), 1f, false);
- position.y += 50f;
+ position.y += 45f;
uie.SelectAndDrawStringOptionItem(Loc.LS("COLLISION MESH"), position, 22, Menus.GetMMSCollisionMesh(), Loc.LS("COLLIDER TO USE FOR PROJECTILE->SHIP COLLISIONS"), 1f, false);
- }
+ position.y += 45f;
+ uie.SelectAndDrawStringOptionItem(Loc.LS("DESTRUCTIBLE MISSILES"), position, 25, Menus.GetMMSDestructibleMissiles(), Loc.LS("ALLOWS MISSILES TO BE SHOT DOWN IN FLIGHT"), 1f, false);
+
+ }
private static void AdjustAdvancedPositionCenterColumn(ref Vector2 position)
{
@@ -690,6 +699,11 @@ static void ProcessAdditional()
MPServerOptimization.RollSpeedLimit = 3 + ((2 + MPServerOptimization.RollSpeedLimit + UIManager.m_select_dir) % 5);
MenuManager.PlayCycleSound(1f, (float)UIManager.m_select_dir);
break;
+ case 25:
+ Menus.mms_destructible_missiles = !Menus.mms_destructible_missiles;
+ MenuManager.PlayCycleSound(1f, (float)UIManager.m_select_dir);
+ break;
+
}
}
else if (MenuManager.m_menu_micro_state == 10)
diff --git a/GameMod/MessageTypes.cs b/GameMod/MessageTypes.cs
index 235eb552..c55e5db8 100644
--- a/GameMod/MessageTypes.cs
+++ b/GameMod/MessageTypes.cs
@@ -51,8 +51,11 @@ public class MessageTypes
public const short MsgAudioTauntPacket = 153;
public const short MsgEnhancedFirePacket = 154;
+
public const short MsgPlayerPhysics = 155;
+ public const short MsgDestroyMissile = 156;
+
// Do not use 400, it is in use by Mod-Projdata.dll.
}
}