Add project files.
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user