using System.Collections.Generic; using System.IO; using HutongGames.PlayMaker; using MSCLoader; using UnityEngine; namespace MSCO { /// /// Lebt über Szenenwechsel hinweg (DontDestroyOnLoad): Lobby im Hauptmenü, /// Spielstand-Übertragung beim Start und Positions-Sync im Spiel. /// public class MultiplayerManager : MonoBehaviour { public static MultiplayerManager Instance; public const int DefaultPort = 7777; const float SendInterval = 0.05f; // 20 Updates pro Sekunde NetSession session; bool peerReady; // Handshake abgeschlossen bool saveSentThisLoad; // Host: Spielstand für diesen Start schon verschickt bool receivingSave; // Client: Spielstand wird gerade empfangen string status = ""; string ipInput = "127.0.0.1"; string portInput = DefaultPort.ToString(); PlayMakerFSM continueFsm; FsmState continueState; float nextContinueSearch; Transform localPlayer; RemotePlayer remotePlayer; float sendTimer; float nextPlayerSearch; public static void Ensure() { if (Instance != null) return; var go = new GameObject("MSCO_Multiplayer"); DontDestroyOnLoad(go); Instance = go.AddComponent(); } void Awake() { Application.runInBackground = true; // Falls das Spiel beim letzten Mal abgestürzt ist: eigenen Spielstand zurückholen TryRestoreBackup(); } // ---------------------------------------------------------------- Verbindung public void Host(int port) { Disconnect(); try { session = NetSession.StartHost(port); SetStatus("Hosting on port " + port + " - waiting for player..."); } catch (System.Exception e) { SetStatus("Could not host: " + e.Message); } } public void Join(string ip, int port) { if (!MenuHelper.InMenu) { SetStatus("Join from the main menu!"); return; } Disconnect(); session = NetSession.StartClient(ip, port); SetStatus("Connecting to " + ip + ":" + port + "..."); } public void Disconnect() { if (session != null) session.Close(); OnSessionEnded("Disconnected"); } void OnSessionEnded(string reason) { session = null; peerReady = false; DestroyRemotePlayer(); if (receivingSave) { // Abbruch mitten in der Übertragung: eigenen Spielstand zurück receivingSave = false; TryRestoreBackup(); } SetStatus(reason); } // ---------------------------------------------------------------- Update void Update() { if (session == null) return; List messages = session.Poll(); if (session.ConsumeConnected()) OnConnected(); foreach (Message m in messages) Handle(m); if (session.IsClosed) { OnSessionEnded(session.CloseReason); return; } if (session.IsHost && peerReady && MenuHelper.InMenu && !saveSentThisLoad) WatchHostContinue(); if (MenuHelper.InGame) UpdateInGame(); } void OnConnected() { if (session.IsHost) { SetStatus("Player connecting..."); } else { session.Send(MsgType.Hello, NetSession.Build(w => w.Write(MSCO.ModVersion))); SetStatus("Connected, waiting for host..."); } } void Handle(Message m) { BinaryReader r = m.Reader(); switch (m.Type) { case MsgType.Hello: if (!session.IsHost) return; string version = r.ReadString(); if (version != MSCO.ModVersion) { session.Send(MsgType.Reject, NetSession.Build(w => w.Write("Version mismatch (host " + MSCO.ModVersion + ", you " + version + ")"))); session.Close("Player rejected: wrong mod version " + version); return; } session.Send(MsgType.Welcome); peerReady = true; if (MenuHelper.InGame) { // Host spielt schon: letzten gespeicherten Stand schicken SendSaveAndStart(); SetStatus("Player joined (sent last saved game)"); } else { SetStatus("Player connected - press CONTINUE to start!"); } break; case MsgType.Welcome: peerReady = true; SetStatus("Connected - waiting for host to press CONTINUE..."); break; case MsgType.Reject: session.Close("Rejected by host: " + r.ReadString()); break; case MsgType.SaveBegin: if (session.IsHost) return; if (!MenuHelper.InMenu) { SetStatus("Host started - go back to the main menu to join"); return; } SaveSync.BeginReceive(); receivingSave = true; SetStatus("Receiving host's save..."); break; case MsgType.SaveFile: if (!receivingSave) return; string name = r.ReadString(); byte[] data = r.ReadBytes(r.ReadInt32()); SaveSync.WriteFile(name, data); break; case MsgType.StartGame: if (!receivingSave) return; receivingSave = false; SetStatus("Loading host's game..."); MenuHelper.StartGame(); break; case MsgType.Player: if (MenuHelper.InGame) ApplyRemoteState(PlayerState.Deserialize(r)); break; } } // ---------------------------------------------------------------- Host: Spielstart void WatchHostContinue() { if (continueFsm == null) { if (Time.realtimeSinceStartup < nextContinueSearch) return; nextContinueSearch = Time.realtimeSinceStartup + 1f; if (!MenuHelper.FindContinueLoadState(out continueFsm, out continueState)) return; } // Host hat CONTINUE gedrückt und die FSM ist im Lade-Zustand if (continueFsm.ActiveStateName == continueState.Name) SendSaveAndStart(); } void SendSaveAndStart() { saveSentThisLoad = true; List> files = SaveSync.ReadLocalSave(); session.Send(MsgType.SaveBegin); foreach (KeyValuePair file in files) { session.Send(MsgType.SaveFile, NetSession.Build(w => { w.Write(file.Key); w.Write(file.Value.Length); w.Write(file.Value); })); } session.Send(MsgType.StartGame); ModConsole.Print("[MSCO] Sent save (" + files.Count + " files) to player"); } void OnLevelWasLoaded(int level) { if (MenuHelper.InGame) { // Falls der CONTINUE-Klick nicht erkannt wurde: jetzt nachholen if (session != null && session.IsHost && peerReady && !saveSentThisLoad) SendSaveAndStart(); localPlayer = null; } else if (MenuHelper.InMenu) { saveSentThisLoad = false; continueFsm = null; localPlayer = null; DestroyRemotePlayer(); if (!receivingSave) TryRestoreBackup(); if (session != null && peerReady && !session.IsHost) SetStatus("Connected - waiting for host to press CONTINUE..."); } } void TryRestoreBackup() { try { SaveSync.RestoreBackup(); } catch (System.Exception e) { ModConsole.Error("[MSCO] Could not restore own save: " + e.Message); } } // ---------------------------------------------------------------- Im Spiel void UpdateInGame() { if (localPlayer == null) { if (Time.realtimeSinceStartup < nextPlayerSearch) return; nextPlayerSearch = Time.realtimeSinceStartup + 1f; GameObject player = GameObject.Find("PLAYER"); if (player == null) return; localPlayer = player.transform; } if (peerReady) { sendTimer += Time.deltaTime; if (sendTimer >= SendInterval) { sendTimer = 0f; session.Send(MsgType.Player, new PlayerState { Position = localPlayer.position, RotationY = localPlayer.eulerAngles.y }.Serialize()); } } if (remotePlayer != null) remotePlayer.Tick(Time.deltaTime); } void ApplyRemoteState(PlayerState state) { if (remotePlayer == null) { // Platzhalter-Modell: eine Kapsel GameObject go = GameObject.CreatePrimitive(PrimitiveType.Capsule); go.name = "MSCO_RemotePlayer"; Destroy(go.GetComponent()); // stört sonst Physik/Trigger go.GetComponent().material.color = Color.cyan; go.transform.position = state.Position; remotePlayer = go.AddComponent(); ModConsole.Print("[MSCO] Other player is in the game"); } remotePlayer.SetTarget(state); } void DestroyRemotePlayer() { if (remotePlayer != null) Destroy(remotePlayer.gameObject); remotePlayer = null; } // ---------------------------------------------------------------- GUI void OnGUI() { if (MenuHelper.InMenu) DrawLobby(); else if (session != null) GUI.Label(new Rect(10, 10, 500, 25), "[MSCO] " + status); } void DrawLobby() { GUILayout.BeginArea(new Rect(10, 10, 280, 200), GUI.skin.box); GUILayout.Label("MSCO Multiplayer"); if (session == null) { GUILayout.BeginHorizontal(); GUILayout.Label("IP", GUILayout.Width(40)); ipInput = GUILayout.TextField(ipInput); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label("Port", GUILayout.Width(40)); portInput = GUILayout.TextField(portInput); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Host")) Host(ParsePort()); if (GUILayout.Button("Join")) Join(ipInput.Trim(), ParsePort()); GUILayout.EndHorizontal(); } else { GUILayout.Label(session.IsHost ? "You are the HOST" : "You are a CLIENT"); if (GUILayout.Button("Disconnect")) Disconnect(); } GUILayout.Label(status); GUILayout.EndArea(); } int ParsePort() { int port; return int.TryParse(portInput, out port) ? port : DefaultPort; } void SetStatus(string text) { status = text; ModConsole.Print("[MSCO] " + text); } } }