Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GameMod/GameMod.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
<Compile Include="MPSniperPackets.cs" />
<Compile Include="MPSoundExt.cs" />
<Compile Include="MPSoundOcclusion.cs" />
<Compile Include="MPSpawnHealth.cs" />
<Compile Include="MPSpawnInitialization.cs" />
<Compile Include="MPSpawnInvuln.cs" />
<Compile Include="MPSpew.cs" />
Expand Down
21 changes: 21 additions & 0 deletions GameMod/MPScoreboards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions GameMod/MPSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
216 changes: 216 additions & 0 deletions GameMod/MPSpawnHealth.cs
Original file line number Diff line number Diff line change
@@ -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<int, int> SpawnHealthByConnectionId = new Dictionary<int, int>();
public static readonly Dictionary<Player, int> LastSpawnHealthByPlayer = new Dictionary<Player, int>();

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<RespawnMessage>();
GameObject playerObject = ClientScene.FindLocalObject(respawnMessage.m_net_id);
Player player = playerObject == null ? null : playerObject.GetComponent<Player>();

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.SpawnHealthMessage>();
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<MPSpawnHealth.SpawnHealthMessage>();
message.health = MPSpawnHealth.ClampToValidHealth(message.health);
MPSpawnHealth.SpawnHealthByConnectionId[message.lobby_id] = message.health;
MPSpawnHealth.SendToSupportingClients(message);
}
}


}
2 changes: 1 addition & 1 deletion GameMod/MPTweaks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading