From 65f7cd8cca802b7657ba1b3685951c1f6fdd0722 Mon Sep 17 00:00:00 2001 From: Lukas Denda Date: Mon, 6 Jul 2026 01:01:53 +0200 Subject: [PATCH 1/4] Adds destructible missiles as a match setting --- GameMod/MPDestructibleMissiles.cs | 422 ++++++++++++++++++++++++ GameMod/MPDoors.cs | 3 +- GameMod/MPMatchInfo.cs | 6 + GameMod/MPModPrivateData.cs | 23 +- GameMod/MPSetup.cs | 2 + GameMod/MPTweaks.cs | 2 +- GameMod/Menus.cs | 36 +- GameMod/MessageTypes.cs | 3 + GameMod/VersionHandling/OlmodVersion.cs | 2 +- 9 files changed, 484 insertions(+), 15 deletions(-) create mode 100644 GameMod/MPDestructibleMissiles.cs diff --git a/GameMod/MPDestructibleMissiles.cs b/GameMod/MPDestructibleMissiles.cs new file mode 100644 index 00000000..219ed10d --- /dev/null +++ b/GameMod/MPDestructibleMissiles.cs @@ -0,0 +1,422 @@ +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 + + // mptweaks capability, the server only sends missile messages to clients that share this flag + public const string TWEAK_NAME = "destructiblemissiles"; + + 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, TWEAK_NAME)) + 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..e04cd243 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," + MPDestructibleMissiles.TWEAK_NAME + (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. } } diff --git a/GameMod/VersionHandling/OlmodVersion.cs b/GameMod/VersionHandling/OlmodVersion.cs index 8dd559f5..8de90e51 100644 --- a/GameMod/VersionHandling/OlmodVersion.cs +++ b/GameMod/VersionHandling/OlmodVersion.cs @@ -29,7 +29,7 @@ public static string FullVersionString // do not include revision unless explicitly set to non-zero in the assembly version string maybeRevision = RunningVersion.Revision > 0 ? $".{RunningVersion.Revision}" : ""; - _fullVersionString = $"olmod {RunningVersion.ToString(3)}{maybeRevision}{(Modded ? " **MODDED**" : "")}"; + _fullVersionString = $"olmod {RunningVersion.ToString(3)}{maybeRevision}-destructible-V2{(Modded ? " **MODDED**" : "")}"; } return _fullVersionString; } From 3320f310a6760b410395f32f6057880cab332529 Mon Sep 17 00:00:00 2001 From: Lukas Denda Date: Mon, 6 Jul 2026 01:07:39 +0200 Subject: [PATCH 2/4] Remove unique version name --- GameMod/VersionHandling/OlmodVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GameMod/VersionHandling/OlmodVersion.cs b/GameMod/VersionHandling/OlmodVersion.cs index 8de90e51..8dd559f5 100644 --- a/GameMod/VersionHandling/OlmodVersion.cs +++ b/GameMod/VersionHandling/OlmodVersion.cs @@ -29,7 +29,7 @@ public static string FullVersionString // do not include revision unless explicitly set to non-zero in the assembly version string maybeRevision = RunningVersion.Revision > 0 ? $".{RunningVersion.Revision}" : ""; - _fullVersionString = $"olmod {RunningVersion.ToString(3)}{maybeRevision}-destructible-V2{(Modded ? " **MODDED**" : "")}"; + _fullVersionString = $"olmod {RunningVersion.ToString(3)}{maybeRevision}{(Modded ? " **MODDED**" : "")}"; } return _fullVersionString; } From 2d2f1434c1980ee64086981dee076c6e4cac173d Mon Sep 17 00:00:00 2001 From: Lukas Denda Date: Sun, 12 Jul 2026 18:19:33 +0200 Subject: [PATCH 3/4] Revert MPTweaks change since it is ugly to scale even if it would directly point at the corresponding file --- GameMod/MPDestructibleMissiles.cs | 4 ---- GameMod/MPTweaks.cs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/GameMod/MPDestructibleMissiles.cs b/GameMod/MPDestructibleMissiles.cs index 219ed10d..2fe308d4 100644 --- a/GameMod/MPDestructibleMissiles.cs +++ b/GameMod/MPDestructibleMissiles.cs @@ -118,10 +118,6 @@ private void RestoreIgnoredCollisions() static class MPDestructibleMissiles { public const int MISSILE_LAYER = 27; // rp ignore layer - - // mptweaks capability, the server only sends missile messages to clients that share this flag - public const string TWEAK_NAME = "destructiblemissiles"; - public static bool Enabled = false; // falcon pod hunt creeper nova dev time vortex diff --git a/GameMod/MPTweaks.cs b/GameMod/MPTweaks.cs index e04cd243..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," + MPDestructibleMissiles.TWEAK_NAME + (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()); From 831c72cf4e323858ef1d84b3cec580078e3cf87a Mon Sep 17 00:00:00 2001 From: Lukas Denda Date: Sat, 18 Jul 2026 20:27:48 +0200 Subject: [PATCH 4/4] Fixes missed reference in the last commit --- GameMod/MPDestructibleMissiles.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GameMod/MPDestructibleMissiles.cs b/GameMod/MPDestructibleMissiles.cs index 2fe308d4..69ed3150 100644 --- a/GameMod/MPDestructibleMissiles.cs +++ b/GameMod/MPDestructibleMissiles.cs @@ -228,7 +228,7 @@ public static void BroadcastDestroy(ProjPrefab type, int projId, Vector3 pos, Ne private static void SendToCapable(short msgType, MessageBase msg) { foreach (var conn in NetworkServer.connections) - if (conn != null && MPTweaks.ClientHasTweak(conn.connectionId, TWEAK_NAME)) + if (conn != null && MPTweaks.ClientHasTweak(conn.connectionId, destructiblemissiles)) conn.SendByChannel(msgType, msg, 0); }