370 lines
12 KiB
C#
370 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using HutongGames.PlayMaker;
|
|
using MSCLoader;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// Lebt über Szenenwechsel hinweg (DontDestroyOnLoad): Lobby im Hauptmenü,
|
|
/// Spielstand-Übertragung beim Start und Positions-Sync im Spiel.
|
|
/// </summary>
|
|
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<MultiplayerManager>();
|
|
}
|
|
|
|
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<Message> 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<KeyValuePair<string, byte[]>> files = SaveSync.ReadLocalSave();
|
|
session.Send(MsgType.SaveBegin);
|
|
foreach (KeyValuePair<string, byte[]> 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<Collider>()); // stört sonst Physik/Trigger
|
|
go.GetComponent<Renderer>().material.color = Color.cyan;
|
|
go.transform.position = state.Position;
|
|
remotePlayer = go.AddComponent<RemotePlayer>();
|
|
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);
|
|
}
|
|
}
|
|
}
|