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