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.
}