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);
}
}
}