diff --git a/GameMod/GameMod.csproj b/GameMod/GameMod.csproj index cb8e7ba8..bbb6a3b7 100644 --- a/GameMod/GameMod.csproj +++ b/GameMod/GameMod.csproj @@ -183,6 +183,7 @@ + diff --git a/GameMod/MPScoreboards.cs b/GameMod/MPScoreboards.cs index dfafc03d..103fa513 100644 --- a/GameMod/MPScoreboards.cs +++ b/GameMod/MPScoreboards.cs @@ -14,6 +14,23 @@ class MPScoreboards { public static bool ShowScores = true; + // Extra column showing reduced spawn health, only present while any + // player in the match uses the self-handicap + const float SpawnHealthCol = 420f; + + static void DrawSpawnHealthHeader(UIElement uie, Vector2 pos) + { + if (MPSpawnHealth.AnyPlayerHasReducedHealth) + uie.DrawStringSmall(Loc.LS("HP"), pos + Vector2.right * SpawnHealthCol, 0.4f, StringOffset.CENTER, UIManager.m_col_ui0, 1f, 85f); + } + + static void DrawSpawnHealthValue(UIElement uie, Vector2 pos, Player player, Color c, float alpha) + { + int health = MPSpawnHealth.GetDisplayedHealth(player); + if (MPSpawnHealth.AnyPlayerHasReducedHealth && health < MPSpawnHealth.MAX_HEALTH) + uie.DrawDigitsVariable(pos + Vector2.right * SpawnHealthCol, health, 0.65f, StringOffset.CENTER, c, alpha); + } + public class Anarchy { static FieldInfo m_alpha_Field = AccessTools.Field(typeof(UIElement), "m_alpha"); @@ -43,6 +60,7 @@ static void DrawScoreHeader(UIElement uie, Vector2 pos, float col1, float col2, uie.DrawStringSmall(Loc.LS("ASSISTS"), pos + Vector2.right * col3, 0.4f, StringOffset.CENTER, UIManager.m_col_ui0, 1f, 85f); uie.DrawStringSmall(Loc.LS("DEATHS"), pos + Vector2.right * col4, 0.4f, StringOffset.CENTER, UIManager.m_col_ui0, 1f, 85f); UIManager.DrawSpriteUI(pos + Vector2.right * col5, 0.13f, 0.13f, UIManager.m_col_ui0, m_alpha, 204); + DrawSpawnHealthHeader(uie, pos); } static void DrawScoresWithoutTeams(UIElement uie, Vector2 pos, float col1, float col2, float col3, float col4, float col5, bool score = false) @@ -99,6 +117,7 @@ static void DrawScoresWithoutTeams(UIElement uie, Vector2 pos, float col1, float } uie.DrawDigitsVariable(pos + Vector2.right * col2, player.m_kills, 0.65f, StringOffset.CENTER, c, m_alpha * num); uie.DrawDigitsVariable(pos + Vector2.right * col4, player.m_deaths, 0.65f, StringOffset.CENTER, c, m_alpha * num); + DrawSpawnHealthValue(uie, pos, player, c, m_alpha * num); c = uie.GetPingColor(player.m_avg_ping_ms); uie.DrawDigitsVariable(pos + Vector2.right * col5, player.m_avg_ping_ms, 0.65f, StringOffset.CENTER, c, m_alpha * num); pos.y += 25f; @@ -195,6 +214,7 @@ static int DrawScoresForTeam(UIElement uie, MpTeam team, Vector2 pos, float col1 if (MPModPrivateData.AssistScoring) uie.DrawDigitsVariable(pos + Vector2.right * col3, player.m_assists, 0.65f, StringOffset.CENTER, c, m_alpha * num); uie.DrawDigitsVariable(pos + Vector2.right * col4, player.m_deaths, 0.65f, StringOffset.CENTER, c, m_alpha * num); + DrawSpawnHealthValue(uie, pos, player, c, m_alpha * num); c = uie.GetPingColor(player.m_avg_ping_ms); uie.DrawDigitsVariable(pos + Vector2.right * col5, player.m_avg_ping_ms, 0.65f, StringOffset.CENTER, c, m_alpha * num); pos.y += 25f; @@ -216,6 +236,7 @@ static void DrawScoreHeader(UIElement uie, Vector2 pos, float col1, float col2, uie.DrawStringSmall(Loc.LS("ASSISTS"), pos + Vector2.right * col3, 0.4f, StringOffset.CENTER, UIManager.m_col_ui0, 1f, 85f); uie.DrawStringSmall(Loc.LS("DEATHS"), pos + Vector2.right * col4, 0.4f, StringOffset.CENTER, UIManager.m_col_ui0, 1f, 85f); UIManager.DrawSpriteUI(pos + Vector2.right * col5, 0.13f, 0.13f, UIManager.m_col_ui0, m_alpha, 204); + DrawSpawnHealthHeader(uie, pos); } public static bool DrawHUDScoreInfo(UIElement uie, Vector2 pos) diff --git a/GameMod/MPSetup.cs b/GameMod/MPSetup.cs index 1e01aa2e..3ff6163b 100644 --- a/GameMod/MPSetup.cs +++ b/GameMod/MPSetup.cs @@ -230,6 +230,7 @@ static void Postfix(string filename) Console.CustomUIColor = ModPrefs.GetInt("O_CUSTOM_UI_COLOR", Console.CustomUIColor); Menus.mms_damage_numbers = ModPrefs.GetBool("MP_DAMAGE_NUMBERS", Menus.mms_damage_numbers); Menus.mms_client_damage_numbers = ModPrefs.GetBool("MP_CLIENT_DAMAGE_NUMBERS", Menus.mms_client_damage_numbers); + Menus.mms_spawn_health = MPSpawnHealth.ClampToValidHealth(ModPrefs.GetInt("MP_SPAWN_HEALTH", Menus.mms_spawn_health)); MPThunderboltPassthrough.isAllowed = ModPrefs.GetBool("MP_THUNDERBOLT_PASSTHROUGH", MPThunderboltPassthrough.isAllowed); Menus.mms_always_cloaked = ModPrefs.GetBool("MP_ALWAYS_CLOAKED", Menus.mms_always_cloaked); Menus.mms_classic_spawns = ModPrefs.GetBool("MP_CLASSIC_SPAWNS", Menus.mms_classic_spawns); @@ -386,6 +387,7 @@ private static void Prefix(string filename) ModPrefs.SetInt("MP_PM_LOADOUT_GUNNER2_M1", (int)MPLoadouts.Loadouts[3].missiles[0]); ModPrefs.SetBool("MP_DAMAGE_NUMBERS", Menus.mms_damage_numbers); ModPrefs.SetBool("MP_CLIENT_DAMAGE_NUMBERS", Menus.mms_client_damage_numbers); + ModPrefs.SetInt("MP_SPAWN_HEALTH", Menus.mms_spawn_health); ModPrefs.SetBool("MP_THUNDERBOLT_PASSTHROUGH", MPThunderboltPassthrough.isAllowed); ModPrefs.SetBool("MP_ALWAYS_CLOAKED", Menus.mms_always_cloaked); ModPrefs.SetBool("MP_CLASSIC_SPAWNS", Menus.mms_classic_spawns); diff --git a/GameMod/MPSpawnHealth.cs b/GameMod/MPSpawnHealth.cs new file mode 100644 index 00000000..91e63fb8 --- /dev/null +++ b/GameMod/MPSpawnHealth.cs @@ -0,0 +1,216 @@ +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Overload; +using UnityEngine; +using UnityEngine.Networking; + +namespace GameMod +{ + // Allow clients to spawn with less than 100 health + public class MPSpawnHealth + { + public const int MIN_HEALTH = 10; + public const int MAX_HEALTH = 100; + + public static readonly Dictionary SpawnHealthByConnectionId = new Dictionary(); + public static readonly Dictionary LastSpawnHealthByPlayer = new Dictionary(); + + public static int GetHealthForConnectionId(int connectionId) => SpawnHealthByConnectionId.TryGetValue(connectionId, out int health) ? health : MAX_HEALTH; + + public static int GetDisplayedHealth(Player player) => LastSpawnHealthByPlayer.TryGetValue(player, out int health) ? health : MAX_HEALTH; + + public static void SetDisplayedHealth(Player player, int health) => LastSpawnHealthByPlayer[player] = health; + + public static bool AnyPlayerHasReducedHealth => LastSpawnHealthByPlayer.Any(entry => entry.Key && entry.Value < MAX_HEALTH); + + public static int ClampToValidHealth(int health) => Mathf.Clamp(health, MIN_HEALTH, MAX_HEALTH); + + public static void Reset(){ + SpawnHealthByConnectionId.Clear(); + LastSpawnHealthByPlayer.Clear(); + } + + // Server + public static void SendToSupportingClients(SpawnHealthMessage message){ + foreach (var connection in NetworkServer.connections) + if (connection != null && MPTweaks.ClientHasTweak(connection.connectionId, "spawnhealth")) + connection.Send(MessageTypes.MsgSpawnHealth, message); + } + + // Client + public static void SendLocalSpawnHealthPreferenceToServer(){ + var client = Client.GetClient(); + if (client != null && client.isConnected) + client.Send(MessageTypes.MsgSpawnHealth, + new SpawnHealthMessage { lobby_id = NetworkMatch.m_my_lobby_id, health = Menus.mms_spawn_health }); + } + + public static void CycleMenuOption(){ + Menus.mms_spawn_health += UIManager.m_select_dir * 10; + if (Menus.mms_spawn_health > MAX_HEALTH) + Menus.mms_spawn_health = MIN_HEALTH; + else if (Menus.mms_spawn_health < MIN_HEALTH) + Menus.mms_spawn_health = MAX_HEALTH; + MenuManager.PlayCycleSound(1f, (float)UIManager.m_select_dir); + SendLocalSpawnHealthPreferenceToServer(); + } + + // the applied value reaches clients indirectly over RpcSetHitpoints in Player.Update + public static void ApplyHealthOnServer(Player player) + { + if (!Overload.NetworkManager.IsServer() || player == null || player.m_spectator || player.connectionToClient == null) + return; + + int health = ClampToValidHealth(GetHealthForConnectionId(player.connectionToClient.connectionId)); + + SetDisplayedHealth(player, health); + player.m_hitpoints = (float)health; + //Debug.LogFormat("Server: MPSpawnHealth: spawned {0} with {1} hp", player.m_mp_name, health); + } + + public class SpawnHealthMessage : MessageBase{ + public int lobby_id; + public int health; + + public override void Serialize(NetworkWriter writer){ + writer.WritePackedUInt32((uint)lobby_id); + writer.WritePackedUInt32((uint)health); + } + + public override void Deserialize(NetworkReader reader){ + lobby_id = (int)reader.ReadPackedUInt32(); + health = (int)reader.ReadPackedUInt32(); + } + } + } + + //////////////////////////////////////// + // CLIENT + //////////////////////////////////////// + + // apply the reduced health after the ship has been restored + [HarmonyPatch(typeof(Client), "OnRespawnMsg")] + class MPSpawnHealth_Client_OnRespawnMsg + { + static void Postfix(NetworkMessage msg) + { + if (Overload.NetworkManager.IsServer()) + return; + msg.reader.SeekZero(); + RespawnMessage respawnMessage = msg.ReadMessage(); + GameObject playerObject = ClientScene.FindLocalObject(respawnMessage.m_net_id); + Player player = playerObject == null ? null : playerObject.GetComponent(); + + if (player == null || player.m_spectator) + return; + + int health = MPSpawnHealth.GetHealthForConnectionId(respawnMessage.lobby_id); + MPSpawnHealth.SetDisplayedHealth(player, health); + if (health < MPSpawnHealth.MAX_HEALTH) + player.m_hitpoints = (float)health; + } + } + + [HarmonyPatch(typeof(NetworkMatch), "InitBeforeEachMatch")] + class MPSpawnHealth_NetworkMatch_InitBeforeEachMatch + { + static void Postfix() + { + MPSpawnHealth.Reset(); + } + } + + [HarmonyPatch(typeof(Client), "SendPlayerLoadoutToServer")] + class MPSpawnHealth_Client_SendPlayerLoadoutToServer + { + static void Postfix() + { + MPSpawnHealth.SendLocalSpawnHealthPreferenceToServer(); + } + } + + // keep local spawn health dic updated + [HarmonyPatch(typeof(Client), "RegisterHandlers")] + class MPSpawnHealth_Client_RegisterHandlers + { + static void Postfix() + { + Client.GetClient()?.RegisterHandler(MessageTypes.MsgSpawnHealth, OnClientReceivedSpawnHealth); + } + + static void OnClientReceivedSpawnHealth(NetworkMessage rawMessage) + { + var message = rawMessage.ReadMessage(); + MPSpawnHealth.SpawnHealthByConnectionId[message.lobby_id] = message.health; + } + } + + + //////////////////////////////////////// + // SERVER + //////////////////////////////////////// + + // send spawn health values of the players to clients that support this feature + [HarmonyPatch(typeof(Server), "SendLoadoutDataToClients")] + class MPSpawnHealth_Server_SendLoadoutDataToClients + { + static void Postfix() + { + foreach (var entry in MPSpawnHealth.SpawnHealthByConnectionId) + MPSpawnHealth.SendToSupportingClients( + new MPSpawnHealth.SpawnHealthMessage { lobby_id = entry.Key, health = entry.Value }); + } + } + + // apply changed health after ship data setup + [HarmonyPatch(typeof(Player), "RestorePlayerShipDataAfterRespawn")] + class MPSpawnHealth_Player_RestorePlayerShipDataAfterRespawn + { + static void Postfix(Player __instance) => MPSpawnHealth.ApplyHealthOnServer(__instance); + } + + // the initial spawn does not trigger RestorePlayerShipDataAfterRespawn + [HarmonyPatch(typeof(Server), "RespawnPlayer")] + class MPSpawnHealth_Server_RespawnPlayer + { + static void Postfix(Player player){ + MPSpawnHealth.ApplyHealthOnServer(player); + } + } + + // potentionally remove old connection_id, health tuples + [HarmonyPatch(typeof(Server), "OnConnect")] + class MPSpawnHealth_Server_OnConnect + { + static void Postfix(NetworkMessage msg) + { + int connectionId = msg.conn.connectionId; + if (MPSpawnHealth.SpawnHealthByConnectionId.Remove(connectionId)) + MPSpawnHealth.SendToSupportingClients(new MPSpawnHealth.SpawnHealthMessage + { + lobby_id = connectionId, + health = MPSpawnHealth.MAX_HEALTH + }); + } + } + + // decode and apply received spawn health packets from clients + [HarmonyPatch(typeof(Server), "RegisterHandlers")] + class MPSpawnHealth_Server_RegisterHandlers + { + static void Postfix(){ + NetworkServer.RegisterHandler(MessageTypes.MsgSpawnHealth, OnServerReceivedSpawnHealth); + } + + static void OnServerReceivedSpawnHealth(NetworkMessage rawMessage) + { + var message = rawMessage.ReadMessage(); + message.health = MPSpawnHealth.ClampToValidHealth(message.health); + MPSpawnHealth.SpawnHealthByConnectionId[message.lobby_id] = message.health; + MPSpawnHealth.SendToSupportingClients(message); + } + } + + +} diff --git a/GameMod/MPTweaks.cs b/GameMod/MPTweaks.cs index d189153a..a60d3bd1 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,spawnhealth,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..d664781a 100644 --- a/GameMod/Menus.cs +++ b/GameMod/Menus.cs @@ -298,6 +298,10 @@ public static void SetLagCompensationDefaults() public static int mms_selected_loadout_idx = 0; public static int mms_collision_mesh = 0; public static bool mms_distinct_kill_sound = false; + public static int mms_spawn_health = 100; + public static int mms_mp_general_scroll_offset = 0; + public const int SCROLL_ELEMENT_PAGE_VISIBLE_ROWS = 10; + public static bool mms_super_countdown = false; } @@ -798,6 +802,23 @@ static IEnumerable Transpiler(IEnumerable code [HarmonyPatch(typeof(UIElement), "DrawMpOptions")] class Menus_UIElement_DrawMpOptions { + private static UIElement mp_general_tab_uie; + private static Vector2 mp_general_tab_row_position; + private static readonly List mp_general_tab_rows = new List { + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("TEXT CHAT"), mp_general_tab_row_position, 0, MenuManager.GetMPTextChat(), string.Empty, 1.5f, false), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("AUTO-RESPAWN TIMER"), mp_general_tab_row_position, 2, MenuManager.GetToggleSetting(MenuManager.opt_mp_auto_respawn), string.Empty, 1.5f, false), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("SPAWN HEALTH"), mp_general_tab_row_position, 11, Menus.mms_spawn_health + "%", Loc.LS("SELF-HANDICAP: SPAWN WITH REDUCED HEALTH, VISIBLE ON THE SCOREBOARD")), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("STICKY DEATH SUMMARY"), mp_general_tab_row_position, 3, Menus.mms_sticky_death_summary ? "YES" : "NO", "KEEP DEATH SUMMARY ON THE SCREEN AFTER LETTING GO OF THE TOGGLE"), + () => mp_general_tab_uie.SelectAndDrawSliderItem(Loc.LS("DAMAGE BLUR INTENSITY"), mp_general_tab_row_position, 4, ((float)Menus.mms_damageeffect_drunk_blur_mult) / 100f), + () => mp_general_tab_uie.SelectAndDrawSliderItem(Loc.LS("DAMAGE COLOR INTENSITY"), mp_general_tab_row_position, 5, ((float)Menus.mms_damageeffect_alpha_mult) / 100f), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("SHIP EXPLOSION EFFECTS"), mp_general_tab_row_position, 6, Menus.mms_reduced_ship_explosions ? Loc.LS("REDUCED") : Loc.LS("FULL"), Loc.LS("REDUCED VISUAL CLUTTER DURING DEATH ROLL")), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("INDIVIDUAL PLAYER COLORS"), mp_general_tab_row_position, 7, MPColoredPlayerNames.isActive ? "ON" : "OFF", Loc.LS("MAKES NAMES MORE RECOGNIZABLE AND DISTINCT BY MAKING THEM BIGGER AND COLORING THEM BY PLAYER [ANARCHY ONLY]")), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("PROFANITY FILTER"), mp_general_tab_row_position, 8, DisableProfanityFilter.profanity_filter ? "ON" : "OFF", Loc.LS("")), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("LOADOUT SELECTION HOTKEYS"), mp_general_tab_row_position, 9, Menus.GetMMSLoadoutHotkeys(), Loc.LS("WEAPON SELECTION HOTKEYS WILL QUICK-SWAP BETWEEN LOADOUTS")), + () => mp_general_tab_uie.SelectAndDrawStringOptionItem(Loc.LS("SUPER SPAWN COUNTDOWN"), mp_general_tab_row_position, 10, Menus.mms_super_countdown ? "ON" : "OFF", Loc.LS("FLASH A COUNTDOWN TO THE NEXT SUPER WHEN THE SUPER SIREN SOUNDS")), + () => mp_general_tab_uie.SelectAndDrawItem(Loc.LS("QUICK CHAT"), mp_general_tab_row_position, 1, false, 1f, 0.75f), + }; + static bool Prefix(UIElement __instance) { UIManager.X_SCALE = 0.2f; @@ -817,25 +838,21 @@ static bool Prefix(UIElement __instance) switch (MenuManager.m_menu_micro_state) { case 0: - __instance.SelectAndDrawStringOptionItem(Loc.LS("TEXT CHAT"), position, 0, MenuManager.GetMPTextChat(), string.Empty, 1.5f, false); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("AUTO-RESPAWN TIMER"), position, 2, MenuManager.GetToggleSetting(MenuManager.opt_mp_auto_respawn), string.Empty, 1.5f, false); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("STICKY DEATH SUMMARY"), position, 3, Menus.mms_sticky_death_summary ? "YES" : "NO", "KEEP DEATH SUMMARY ON THE SCREEN AFTER LETTING GO OF THE TOGGLE"); - position.y += 52f; - __instance.SelectAndDrawSliderItem(Loc.LS("DAMAGE BLUR INTENSITY"), position, 4, ((float)Menus.mms_damageeffect_drunk_blur_mult) / 100f); - position.y += 52f; - __instance.SelectAndDrawSliderItem(Loc.LS("DAMAGE COLOR INTENSITY"), position, 5, ((float)Menus.mms_damageeffect_alpha_mult) / 100f); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("SHIP EXPLOSION EFFECTS"), position, 6, Menus.mms_reduced_ship_explosions ? Loc.LS("REDUCED") : Loc.LS("FULL"), Loc.LS("REDUCED VISUAL CLUTTER DURING DEATH ROLL")); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("INDIVIDUAL PLAYER COLORS"), position, 7, MPColoredPlayerNames.isActive ? "ON" : "OFF", Loc.LS("MAKES NAMES MORE RECOGNIZABLE AND DISTINCT BY MAKING THEM BIGGER AND COLORING THEM BY PLAYER [ANARCHY ONLY]")); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("PROFANITY FILTER"), position, 8, DisableProfanityFilter.profanity_filter ? "ON" : "OFF", Loc.LS("")); - position.y += 52f; - __instance.SelectAndDrawStringOptionItem(Loc.LS("LOADOUT SELECTION HOTKEYS"), position, 9, Menus.GetMMSLoadoutHotkeys(), Loc.LS("WEAPON SELECTION HOTKEYS WILL QUICK-SWAP BETWEEN LOADOUTS")); - position.y += 68f; - __instance.SelectAndDrawItem(Loc.LS("QUICK CHAT"), position, 1, false, 1f, 0.75f); + mp_general_tab_uie = __instance; + int general_tab_max_scroll_offset = Math.Max(0, mp_general_tab_rows.Count - Menus.SCROLL_ELEMENT_PAGE_VISIBLE_ROWS); + Menus.mms_mp_general_scroll_offset = Mathf.Clamp(Menus.mms_mp_general_scroll_offset, 0, general_tab_max_scroll_offset); + int general_tab_visible_rows = Math.Min(Menus.SCROLL_ELEMENT_PAGE_VISIBLE_ROWS, mp_general_tab_rows.Count); + float general_tab_first_row_y = position.y; + for (int row_index = Menus.mms_mp_general_scroll_offset; row_index < Menus.mms_mp_general_scroll_offset + general_tab_visible_rows; row_index++) + { + mp_general_tab_row_position = position; + mp_general_tab_rows[row_index](); + position.y += 52f; + } + position.y -= 52f; + + if (mp_general_tab_rows.Count > Menus.SCROLL_ELEMENT_PAGE_VISIBLE_ROWS) + DrawMPGeneralTabScrollbar(__instance, general_tab_first_row_y, general_tab_visible_rows, mp_general_tab_rows.Count, general_tab_max_scroll_offset); break; case 1: __instance.SelectAndDrawStringOptionItem(Loc.LS("TEAMMATE NAMES"), position, 0, MenuManager.GetMPTeammateNames(), string.Empty, 1.5f, false); @@ -996,6 +1013,63 @@ static bool Prefix(UIElement __instance) return false; } + private static bool mp_general_tab_scrollbar_dragging = false; + private static void DrawMPGeneralTabScrollbar(UIElement uie, float first_row_y, int visible_rows, int total_rows, int max_scroll_offset) + { + const float scrollbar_center_x = 440f; + float track_top_y = first_row_y - 10f; + float track_bottom_y = first_row_y + (visible_rows - 1) * 52f + 10f; + float track_height = track_bottom_y - track_top_y; + float thumb_height = track_height * visible_rows / total_rows; + + bool mouse_on_track = Mathf.Abs(UIManager.m_mouse_pos.x - scrollbar_center_x) <= 14f + && UIManager.m_mouse_pos.y >= track_top_y - 6f && UIManager.m_mouse_pos.y <= track_bottom_y + 6f; + if (Input.GetMouseButtonDown(0) && mouse_on_track) + mp_general_tab_scrollbar_dragging = true; + if (!Input.GetMouseButton(0)) + mp_general_tab_scrollbar_dragging = false; + if (mp_general_tab_scrollbar_dragging) + { + float drag_range = track_height - thumb_height; + float drag_fraction = drag_range > 0f ? Mathf.Clamp01((UIManager.m_mouse_pos.y - track_top_y - thumb_height * 0.5f) / drag_range) : 0f; + Menus.mms_mp_general_scroll_offset = Mathf.RoundToInt(drag_fraction * max_scroll_offset); + } + + Vector2 track_center = new Vector2(scrollbar_center_x, (track_top_y + track_bottom_y) * 0.5f); + UIManager.DrawQuadUI(track_center, 6f, track_height * 0.5f + 4f, UIManager.m_col_ub0, uie.m_alpha * 0.1f, 12); + UIManager.DrawQuadUI(track_center, 1.2f, track_height * 0.5f, UIManager.m_col_ui0, uie.m_alpha * 0.5f, 12); + + float thumb_top_y = track_top_y + Menus.mms_mp_general_scroll_offset * (track_height - thumb_height) / max_scroll_offset; + Vector2 thumb_center = new Vector2(scrollbar_center_x, thumb_top_y + thumb_height * 0.5f); + Color thumb_color = (mp_general_tab_scrollbar_dragging || mouse_on_track) + ? Color.Lerp(UIManager.m_col_ui5, UIManager.m_col_ui6, UnityEngine.Random.Range(0f, 0.2f * UIElement.FLICKER)) + : UIManager.m_col_ui2; + UIManager.DrawQuadUIInner(thumb_center, 5f, thumb_height * 0.5f, thumb_color, uie.m_alpha, 11, 0.75f); + + const float arrow_arm_length = 5f; + if (Menus.mms_mp_general_scroll_offset > 0) + { + Vector2 up_arrow_center = new Vector2(scrollbar_center_x, track_top_y - 18f); + uie.TestMouseInRect(up_arrow_center, 14f, 12f, 1900, true); + Color up_arrow_color = UIManager.m_menu_selection == 1900 + ? Color.Lerp(UIManager.m_col_ui5, UIManager.m_col_ui6, UnityEngine.Random.Range(0f, 0.5f * UIElement.FLICKER)) + : UIManager.m_col_ui2; + UIManager.DrawQuadCenterLine(new Vector2(up_arrow_center.x - arrow_arm_length, up_arrow_center.y + arrow_arm_length * 0.5f), new Vector2(up_arrow_center.x, up_arrow_center.y - arrow_arm_length * 0.5f), 1.5f, 1f, up_arrow_color, 13); + UIManager.DrawQuadCenterLine(new Vector2(up_arrow_center.x, up_arrow_center.y - arrow_arm_length * 0.5f), new Vector2(up_arrow_center.x + arrow_arm_length, up_arrow_center.y + arrow_arm_length * 0.5f), 1.5f, 1f, up_arrow_color, 13); + } + + if (Menus.mms_mp_general_scroll_offset < max_scroll_offset) + { + Vector2 down_arrow_center = new Vector2(scrollbar_center_x, track_bottom_y + 18f); + uie.TestMouseInRect(down_arrow_center, 14f, 12f, 1901, true); + Color down_arrow_color = UIManager.m_menu_selection == 1901 + ? Color.Lerp(UIManager.m_col_ui5, UIManager.m_col_ui6, UnityEngine.Random.Range(0f, 0.5f * UIElement.FLICKER)) + : UIManager.m_col_ui2; + UIManager.DrawQuadCenterLine(new Vector2(down_arrow_center.x - arrow_arm_length, down_arrow_center.y - arrow_arm_length * 0.5f), new Vector2(down_arrow_center.x, down_arrow_center.y + arrow_arm_length * 0.5f), 1.5f, 1f, down_arrow_color, 13); + UIManager.DrawQuadCenterLine(new Vector2(down_arrow_center.x, down_arrow_center.y + arrow_arm_length * 0.5f), new Vector2(down_arrow_center.x + arrow_arm_length, down_arrow_center.y - arrow_arm_length * 0.5f), 1.5f, 1f, down_arrow_color, 13); + } + } + public static void DrawTabs(UIElement uie, Vector2 pos, int tab_selected) { float w = 508f; @@ -1203,6 +1277,22 @@ static bool Prefix(ref float ___m_menu_state_timer) } MenuManager.PlaySelectSound(1f); break; + case 10: + Menus.mms_super_countdown = !Menus.mms_super_countdown; + MenuManager.PlaySelectSound(1f); + break; + case 11: + MPSpawnHealth.CycleMenuOption(); + break; + // scrollbar arrows + case 1900: + Menus.mms_mp_general_scroll_offset--; + MenuManager.PlayCycleSound(1f, -1f); + break; + case 1901: + Menus.mms_mp_general_scroll_offset++; + MenuManager.PlayCycleSound(1f, 1f); + break; } break; case 1: @@ -1488,6 +1578,13 @@ static bool Prefix(ref float ___m_menu_state_timer) break; } } + + if (MenuManager.m_menu_micro_state == 0) + { + float scroll_wheel_delta = Input.GetAxis("Mouse ScrollWheel"); + if (scroll_wheel_delta != 0f) + Menus.mms_mp_general_scroll_offset = Math.Max(0, Menus.mms_mp_general_scroll_offset + (scroll_wheel_delta < 0f ? 1 : -1)); + } } else if (menu_sub_state == MenuSubState.GET_INPUT) { @@ -1768,6 +1865,13 @@ static void DrawMpTeamSwitch(UIElement uie, ref Vector2 position) position.y += 62f; } + static void DrawMpSpawnHealth(UIElement uie, ref Vector2 position) + { + uie.SelectAndDrawStringOptionItem(Loc.LS("SPAWN HEALTH"), position, 14, Menus.mms_spawn_health + "%", "", 1f, false); + + position.y += 62f; + } + static IEnumerable Transpiler(IEnumerable codes) { foreach (var code in codes) @@ -1776,7 +1880,10 @@ static IEnumerable Transpiler(IEnumerable code { yield return new CodeInstruction(OpCodes.Ldloca_S, 0); // Vector2 position yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Menus_UIElement_DrawPauseMenu), "DrawMpTeamSwitch")); - yield return new CodeInstruction(OpCodes.Ldarg_0); // We stole 'this' to pass as first arg to DrawMpTeamSwitch, put back on stack + yield return new CodeInstruction(OpCodes.Ldarg_0); // instance for DrawMpSpawnHealth + yield return new CodeInstruction(OpCodes.Ldloca_S, 0); + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(Menus_UIElement_DrawPauseMenu), "DrawMpSpawnHealth")); + yield return new CodeInstruction(OpCodes.Ldarg_0); // replenish the taken instance } yield return code; @@ -1788,13 +1895,26 @@ static IEnumerable Transpiler(IEnumerable code [HarmonyPatch(typeof(MenuManager), "PausedUpdate")] class Menus_MenuManager_PausedUpdate { + static void Prefix() + { + if (MenuManager.m_menu_sub_state != MenuSubState.ACTIVE || MenuManager.m_menu_micro_state != 0) + return; + if (MenuManager.option_dir && UIManager.PushedDir()) + HandleMenuSelection(); + } + static void HandleMenuSelection() { + MenuManager.MaybeReverseOption(); if (UIManager.m_menu_selection == 13) { Menus.mms_team_selection = MPTeams.NextTeam(Menus.mms_team_selection ?? GameManager.m_local_player.m_mp_team); MenuManager.PlaySelectSound(1f); } + else if (UIManager.m_menu_selection == 14) + { + MPSpawnHealth.CycleMenuOption(); + } } static void InitializeMenu() diff --git a/GameMod/MessageTypes.cs b/GameMod/MessageTypes.cs index 235eb552..bf3d8ac6 100644 --- a/GameMod/MessageTypes.cs +++ b/GameMod/MessageTypes.cs @@ -52,6 +52,9 @@ public class MessageTypes public const short MsgEnhancedFirePacket = 154; public const short MsgPlayerPhysics = 155; + + + public const short MsgSpawnHealth = 160; // Do not use 400, it is in use by Mod-Projdata.dll. }