diff --git a/Plugin.cs b/Plugin.cs index a87edf46..246da255 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -71,6 +71,7 @@ public void Init() // initialize world components World.World.Load(); // C# sucks Movement.Load(); + CyberGrind.Load(); // initialize ui components WidescreenFix.Load(); diff --git a/content/PacketType.cs b/content/PacketType.cs index 63d7b9c3..f6b604a6 100644 --- a/content/PacketType.cs +++ b/content/PacketType.cs @@ -33,5 +33,7 @@ public enum PacketType /// Need to activate a certain object. It can be anything, because there are a lot of different stuff in the game. ActivateObject, /// Any action with the cinema, like starting a video, pausing or rewinding. - CinemaAction + CinemaAction, + /// Any action with CyberGrind, like pattern and wave. + CybergrindAction } \ No newline at end of file diff --git a/harmony-patches/CyberGrindPatch.cs b/harmony-patches/CyberGrindPatch.cs new file mode 100644 index 00000000..87cefd70 --- /dev/null +++ b/harmony-patches/CyberGrindPatch.cs @@ -0,0 +1,114 @@ +namespace Jaket.HarmonyPatches; + +using HarmonyLib; +using UnityEngine; +using UnityEngine.UI; + +using Jaket.Net; +using Jaket.World; + +#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable RCS1213 // Remove unused member declaration. + +[HarmonyPatch(typeof(EndlessGrid), "LoadPattern")] +public class EndlessGridLoadPatternPatch +{ + static void Prefix(ref ArenaPattern pattern) + { + // load current pattern in client + if (LobbyController.Lobby != null) + { + // send pattern if the player is the owner of the lobby + if (LobbyController.IsOwner) CyberGrind.Instance.SendPattern(pattern); + // replacing client pattern with server pattern on client + else pattern = CyberGrind.Instance.CurrentPattern; + } + } +} + +[HarmonyPatch(typeof(EndlessGrid), "Start")] +public class EndlessGridStartPatch +{ + static bool Prefix() + { + // getting cybergrind grid deathzone to change it later + foreach (var deathZone in Resources.FindObjectsOfTypeAll()) if (deathZone.name == "Cube") CyberGrind.Instance.GridDeathZoneInstance = deathZone; + + // don't skip original method + return true; + } + + // use postfix to wait object to initialize + static void Postfix() + { + var cg = CyberGrind.Instance; + // check when the player in a lobby + if (LobbyController.Lobby != null || LobbyController.IsOwner) + { + // sets as first time + cg.LoadTimes = 0; + // check if current pattern is loaded and the player is the client + if (cg.CurrentPattern != null && !LobbyController.IsOwner) + // loads current pattern from the server + cg.LoadCurrentPattern(); + // send empty pattern when game starts and the player is the owner to prevent load previous cybergrind pattern + else cg.SendPattern(new ArenaPattern()); + } + else + { + // resetting values if the player is not in a lobby. + cg.LoadTimes = 0; + cg.CurrentWave = 0; + cg.CurrentPattern = null; + } + } +} + +[HarmonyPatch(typeof(EndlessGrid), "OnTriggerEnter")] +public class EndlessGridOnTriggerEnterPatch +{ + // dont allow to launch CyberGrind to client + static bool Prefix(ref Text ___waveNumberText) + { + // for some reason, the wave number text is not shown on the client + ___waveNumberText.transform.parent.parent.gameObject.SetActive(value: true); + // don't activate trigger if the player is not the owner of the lobby + if (LobbyController.Lobby != null && !LobbyController.IsOwner) return false; + return true; + } +} + +[HarmonyPatch(typeof(EndlessGrid), "Update")] +public class EndlessGridUpdatePatch +{ + static bool Prefix(ref ActivateNextWave ___anw, EndlessGrid __instance) + { + // check if the player is not the owner of the lobby (client) + if (LobbyController.Lobby != null && !LobbyController.IsOwner) + { + // set the current wave on the client to original cybergrind singleton to sync with the server + __instance.currentWave = CyberGrind.Instance.CurrentWave; + // set death enemies to prevent start new wave on the client to sync it + ___anw.deadEnemies = -999; + } + return true; + } + + // use postfix to change object after original object is changed + static void Postfix(ref Text ___enemiesLeftText) + { + // check if the player is not the owner of the lobby (client) + if (LobbyController.Lobby != null && !LobbyController.IsOwner) + { + // we broke original death enemies count, so we need to create new counter based on EnemyTracker + var enemies = EnemyTracker.Instance.enemies; + // remove dead enemies or null enemies from the list + enemies.RemoveAll(e => e.dead || e is null); + // set enemies left text on the client, replacing original + ___enemiesLeftText.text = EnemyTracker.Instance.enemies.Count.ToString(); + } + // change y position of cybergrind grid deathzone when lobby created or not to prevent enemies randomly dying + var dz = CyberGrind.Instance.GridDeathZoneInstance; + if (dz != null) dz.transform.position = dz.transform.position with { y = LobbyController.Lobby != null ? -10 : 0.5f }; + } +} \ No newline at end of file diff --git a/net/end-points/Client.cs b/net/end-points/Client.cs index 2b4e94d7..9c7e071b 100644 --- a/net/end-points/Client.cs +++ b/net/end-points/Client.cs @@ -86,6 +86,8 @@ public override void Load() Listen(PacketType.ActivateObject, r => World.Instance.ActivateObject(r.Int())); Listen(PacketType.CinemaAction, r => Cinema.Play(r.String())); + + Listen(PacketType.CybergrindAction, r => CyberGrind.Instance.LoadPattern(r.Int(), r.String())); } public override void Update() diff --git a/world/CyberGrind.cs b/world/CyberGrind.cs new file mode 100644 index 00000000..60b5b5e4 --- /dev/null +++ b/world/CyberGrind.cs @@ -0,0 +1,110 @@ +namespace Jaket.World; + +using HarmonyLib; +using Jaket.Content; +using Jaket.IO; +using Jaket.Net; +using Jaket.UI; +using UnityEngine.UI; + +/// Class responsible for Cybergrind synchronization +public class CyberGrind : MonoSingleton +{ + /// UI Text, used for displaying current wave number. + public Text WaveNumberTextInstance; + /// UI Text, used for displaying current enemies left. + public Text EnemiesLeftTextInstance; + public DeathZone GridDeathZoneInstance; + + /// Current wave count used for sync. + public int CurrentWave; + /// How many times pattern is loaded. Can't be bigger that 1. + public int LoadTimes; + + /// Current pattern used for sync. + public ArenaPattern CurrentPattern; + public int LoadCount; + + /// Sends the current arena pattern to all clients. + /// Pattern to send to clients. + public void SendPattern(ArenaPattern pattern) + { + // serialize pattern to send to clients + var data = SerializePattern(pattern); + // sending to clients + Networking.Redirect(Writer.Write(w => + { + // wave number + w.Int(EndlessGrid.Instance.currentWave); + // pattern + w.String(data); + }), PacketType.CybergrindAction); + } + + /// Load pattern serialized pattern and wave from server. + /// String type represented in + public void LoadPattern(int currentWave, string data) + { + // sets current pattern to give it to LoadPattern method of original class + CurrentPattern = DeserializePattern(data); + // set current wave to synced one + CurrentWave = currentWave; + LoadPattern(CurrentPattern); + } + + /// Loads pattern and invoking next wave. + /// to load. + public void LoadPattern(ArenaPattern pattern) + { + // sets current pattern to give it to LoadPattern method of original class + CurrentPattern = pattern; + // start a new wave with server pattern + AccessTools.Method(typeof(EndlessGrid), "NextWave").Invoke(EndlessGrid.Instance, new object[] { }); + + // Do not make new wave if it is the first time + if (LoadTimes < 1) + { + LoadTimes++; + return; + } + + // Resetting weapon charges (for example, railgun will be charged and etc.) + WeaponCharges.Instance.MaxCharges(); + + // play cheering sound effect + var cr = CrowdReactions.Instance; + cr.React(cr.cheerLong); + + // resetting values after each wave + var nmov = NewMovement.Instance; + if (nmov.hp > 0) + { + nmov.ResetHardDamage(); + nmov.exploded = false; + nmov.GetHealth(999, silent: true); + nmov.FullStamina(); + } + } + + /// Loads current pattern. + public void LoadCurrentPattern() => LoadPattern(CurrentPattern); + + /// Deserialize pattern to load it to client. + /// String to deserialize to . + public ArenaPattern DeserializePattern(string data) + { + // split a pattern into heights and prefabs. + string[] parts = data.Split('|'); + return new ArenaPattern { heights = parts[0], prefabs = parts[1] }; + } + + /// Serializes pattern to string to send it to clients. + /// to serialize to string. + public string SerializePattern(ArenaPattern arena) => $"{arena.heights}|{arena.prefabs}"; + + public static void Load() + { + // initialize the singleton + UI.Object("CyberGrind").AddComponent(); + } +} \ No newline at end of file