EntityEvents and EntitySpawner used to work independently of each other, with separate IDs, and there was no guarantee that spawning and events would happen in the correct order. For example, a client could fail to read events during midround syncing because the entity has been removed, or read an event for an incorrect entity because the entity has been removed and the ID taken by some other entity.
1991 lines
79 KiB
C#
1991 lines
79 KiB
C#
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Lidgren.Network;
|
|
using Microsoft.Xna.Framework;
|
|
using RestSharp;
|
|
using Barotrauma.Items.Components;
|
|
|
|
namespace Barotrauma.Networking
|
|
{
|
|
partial class GameServer : NetworkMember
|
|
{
|
|
private List<Client> connectedClients = new List<Client>();
|
|
|
|
//for keeping track of disconnected clients in case the reconnect shortly after
|
|
private List<Client> disconnectedClients = new List<Client>();
|
|
|
|
private NetStats netStats;
|
|
|
|
private int roundStartSeed;
|
|
|
|
//is the server running
|
|
private bool started;
|
|
|
|
private NetServer server;
|
|
private NetPeerConfiguration config;
|
|
|
|
private int MaxPlayers;
|
|
|
|
private DateTime sparseUpdateTimer;
|
|
private DateTime refreshMasterTimer;
|
|
|
|
private RestClient restClient;
|
|
private bool masterServerResponded;
|
|
private IRestResponse masterServerResponse;
|
|
|
|
private ServerLog log;
|
|
private GUIButton showLogButton;
|
|
|
|
private bool initiatedStartGame;
|
|
private CoroutineHandle startGameCoroutine;
|
|
|
|
private GUIScrollBar clientListScrollBar;
|
|
|
|
public TraitorManager TraitorManager;
|
|
|
|
private ServerEntityEventManager entityEventManager;
|
|
|
|
private FileSender fileSender;
|
|
|
|
public override List<Client> ConnectedClients
|
|
{
|
|
get
|
|
{
|
|
return connectedClients;
|
|
}
|
|
}
|
|
|
|
|
|
public ServerEntityEventManager EntityEventManager
|
|
{
|
|
get { return entityEventManager; }
|
|
}
|
|
|
|
public TimeSpan UpdateInterval
|
|
{
|
|
get { return updateInterval; }
|
|
}
|
|
|
|
public GameServer(string name, int port, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10)
|
|
{
|
|
name = name.Replace(":", "");
|
|
name = name.Replace(";", "");
|
|
|
|
AdminAuthPass = "";
|
|
|
|
this.name = name;
|
|
this.password = "";
|
|
if (password.Length>0)
|
|
{
|
|
this.password = Encoding.UTF8.GetString(NetUtility.ComputeSHAHash(Encoding.UTF8.GetBytes(password)));
|
|
}
|
|
|
|
config = new NetPeerConfiguration("barotrauma");
|
|
|
|
netStats = new NetStats();
|
|
|
|
#if DEBUG
|
|
config.SimulatedLoss = 0.05f;
|
|
config.SimulatedRandomLatency = 0.05f;
|
|
config.SimulatedDuplicatesChance = 0.05f;
|
|
config.SimulatedMinimumLatency = 0.1f;
|
|
|
|
config.ConnectionTimeout = 60.0f;
|
|
|
|
NetIdUtils.Test();
|
|
#endif
|
|
config.Port = port;
|
|
Port = port;
|
|
|
|
if (attemptUPnP)
|
|
{
|
|
config.EnableUPnP = true;
|
|
}
|
|
|
|
config.MaximumConnections = maxPlayers*2; //double the lidgren connections for unauthenticated players
|
|
MaxPlayers = maxPlayers;
|
|
|
|
config.DisableMessageType(NetIncomingMessageType.DebugMessage |
|
|
NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt |
|
|
NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error |
|
|
NetIncomingMessageType.UnconnectedData);
|
|
|
|
config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
|
|
|
|
//----------------------------------------
|
|
|
|
var endRoundButton = new GUIButton(new Rectangle(GameMain.GraphicsWidth - 170, 20, 150, 20), "End round", Alignment.TopLeft, GUI.Style, inGameHUD);
|
|
endRoundButton.OnClicked = (btn, userdata) => { EndGame(); return true; };
|
|
|
|
log = new ServerLog(name);
|
|
showLogButton = new GUIButton(new Rectangle(GameMain.GraphicsWidth - 170 - 170, 20, 150, 20), "Server Log", Alignment.TopLeft, GUI.Style, inGameHUD);
|
|
showLogButton.OnClicked = (GUIButton button, object userData) =>
|
|
{
|
|
if (log.LogFrame == null)
|
|
{
|
|
log.CreateLogFrame();
|
|
}
|
|
else
|
|
{
|
|
log.LogFrame = null;
|
|
GUIComponent.KeyboardDispatcher.Subscriber = null;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
GUIButton settingsButton = new GUIButton(new Rectangle(GameMain.GraphicsWidth - 170 - 170 - 170, 20, 150, 20), "Settings", Alignment.TopLeft, GUI.Style, inGameHUD);
|
|
settingsButton.OnClicked = ToggleSettingsFrame;
|
|
settingsButton.UserData = "settingsButton";
|
|
|
|
entityEventManager = new ServerEntityEventManager(this);
|
|
|
|
whitelist = new WhiteList();
|
|
banList = new BanList();
|
|
|
|
LoadSettings();
|
|
LoadClientPermissions();
|
|
|
|
//----------------------------------------
|
|
|
|
CoroutineManager.StartCoroutine(StartServer(isPublic));
|
|
}
|
|
|
|
private IEnumerable<object> StartServer(bool isPublic)
|
|
{
|
|
bool error = false;
|
|
try
|
|
{
|
|
Log("Starting the server...", Color.Cyan);
|
|
server = new NetServer(config);
|
|
netPeer = server;
|
|
|
|
fileSender = new FileSender(this);
|
|
fileSender.OnEnded += FileTransferChanged;
|
|
fileSender.OnStarted += FileTransferChanged;
|
|
|
|
server.Start();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log("Error while starting the server (" + e.Message + ")", Color.Red);
|
|
|
|
System.Net.Sockets.SocketException socketException = e as System.Net.Sockets.SocketException;
|
|
|
|
if (socketException != null && socketException.SocketErrorCode == System.Net.Sockets.SocketError.AddressAlreadyInUse)
|
|
{
|
|
new GUIMessageBox("Starting the server failed", e.Message + ". Are you trying to run multiple servers on the same port?");
|
|
}
|
|
else
|
|
{
|
|
new GUIMessageBox("Starting the server failed", e.Message);
|
|
}
|
|
|
|
error = true;
|
|
}
|
|
|
|
if (error)
|
|
{
|
|
if (server != null) server.Shutdown("Error while starting the server");
|
|
|
|
GameMain.NetworkMember = null;
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
if (config.EnableUPnP)
|
|
{
|
|
server.UPnP.ForwardPort(config.Port, "barotrauma");
|
|
|
|
GUIMessageBox upnpBox = new GUIMessageBox("Please wait...", "Attempting UPnP port forwarding", new string[] {"Cancel"} );
|
|
upnpBox.Buttons[0].OnClicked = upnpBox.Close;
|
|
|
|
//DateTime upnpTimeout = DateTime.Now + new TimeSpan(0,0,5);
|
|
while (server.UPnP.Status == UPnPStatus.Discovering
|
|
&& GUIMessageBox.VisibleBox == upnpBox)// && upnpTimeout>DateTime.Now)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
upnpBox.Close(null,null);
|
|
|
|
if (server.UPnP.Status == UPnPStatus.NotAvailable)
|
|
{
|
|
new GUIMessageBox("Error", "UPnP not available");
|
|
}
|
|
else if (server.UPnP.Status == UPnPStatus.Discovering)
|
|
{
|
|
new GUIMessageBox("Error", "UPnP discovery timed out");
|
|
}
|
|
}
|
|
|
|
if (isPublic)
|
|
{
|
|
CoroutineManager.StartCoroutine(RegisterToMasterServer());
|
|
}
|
|
|
|
updateInterval = new TimeSpan(0, 0, 0, 0, 150);
|
|
|
|
DebugConsole.NewMessage("Server started", Color.Green);
|
|
|
|
GameMain.NetLobbyScreen.Select();
|
|
started = true;
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
private IEnumerable<object> RegisterToMasterServer()
|
|
{
|
|
if (restClient==null)
|
|
{
|
|
restClient = new RestClient(NetConfig.MasterServerUrl);
|
|
}
|
|
|
|
var request = new RestRequest("masterserver3.php", Method.GET);
|
|
request.AddParameter("action", "addserver");
|
|
request.AddParameter("servername", name);
|
|
request.AddParameter("serverport", Port);
|
|
request.AddParameter("currplayers", connectedClients.Count);
|
|
request.AddParameter("maxplayers", MaxPlayers);
|
|
request.AddParameter("password", string.IsNullOrWhiteSpace(password) ? 0 : 1);
|
|
|
|
masterServerResponded = false;
|
|
masterServerResponse = null;
|
|
var restRequestHandle = restClient.ExecuteAsync(request, response => MasterServerCallBack(response));
|
|
|
|
DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 15);
|
|
while (!masterServerResponded)
|
|
{
|
|
if (DateTime.Now > timeOut)
|
|
{
|
|
restRequestHandle.Abort();
|
|
DebugConsole.NewMessage("Couldn't register to master server (request timed out)", Color.Red);
|
|
Log("Couldn't register to master server (request timed out)", Color.Red);
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
yield return CoroutineStatus.Running;
|
|
}
|
|
|
|
if (masterServerResponse.StatusCode != System.Net.HttpStatusCode.OK)
|
|
{
|
|
DebugConsole.ThrowError("Error while connecting to master server (" + masterServerResponse.StatusCode + ": " + masterServerResponse.StatusDescription + ")");
|
|
}
|
|
else if (masterServerResponse != null && !string.IsNullOrWhiteSpace(masterServerResponse.Content))
|
|
{
|
|
DebugConsole.ThrowError("Error while connecting to master server (" + masterServerResponse.Content + ")");
|
|
}
|
|
else
|
|
{
|
|
registeredToMaster = true;
|
|
refreshMasterTimer = DateTime.Now + refreshMasterInterval;
|
|
}
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
private IEnumerable<object> RefreshMaster()
|
|
{
|
|
if (restClient == null)
|
|
{
|
|
restClient = new RestClient(NetConfig.MasterServerUrl);
|
|
}
|
|
|
|
var request = new RestRequest("masterserver3.php", Method.GET);
|
|
request.AddParameter("action", "refreshserver");
|
|
request.AddParameter("serverport", Port);
|
|
request.AddParameter("gamestarted", gameStarted ? 1 : 0);
|
|
request.AddParameter("currplayers", connectedClients.Count);
|
|
request.AddParameter("maxplayers", MaxPlayers);
|
|
|
|
Log("Refreshing connection with master server...", Color.Cyan);
|
|
|
|
var sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
masterServerResponded = false;
|
|
masterServerResponse = null;
|
|
var restRequestHandle = restClient.ExecuteAsync(request, response => MasterServerCallBack(response));
|
|
|
|
DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 15);
|
|
while (!masterServerResponded)
|
|
{
|
|
if (DateTime.Now > timeOut)
|
|
{
|
|
restRequestHandle.Abort();
|
|
DebugConsole.NewMessage("Couldn't connect to master server (request timed out)", Color.Red);
|
|
Log("Couldn't connect to master server (request timed out)", Color.Red);
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
yield return CoroutineStatus.Running;
|
|
}
|
|
|
|
if (masterServerResponse.Content == "Error: server not found")
|
|
{
|
|
Log("Not registered to master server, re-registering...", Color.Red);
|
|
CoroutineManager.StartCoroutine(RegisterToMasterServer());
|
|
}
|
|
else if (masterServerResponse.ErrorException != null)
|
|
{
|
|
DebugConsole.NewMessage("Error while registering to master server (" + masterServerResponse.ErrorException + ")", Color.Red);
|
|
Log("Error while registering to master server (" + masterServerResponse.ErrorException + ")", Color.Red);
|
|
}
|
|
else if (masterServerResponse.StatusCode != System.Net.HttpStatusCode.OK)
|
|
{
|
|
DebugConsole.NewMessage("Error while reporting to master server (" + masterServerResponse.StatusCode + ": " + masterServerResponse.StatusDescription + ")", Color.Red);
|
|
Log("Error while reporting to master server (" + masterServerResponse.StatusCode + ": " + masterServerResponse.StatusDescription + ")", Color.Red);
|
|
}
|
|
else
|
|
{
|
|
Log("Master server responded", Color.Cyan);
|
|
}
|
|
|
|
System.Diagnostics.Debug.WriteLine("took "+sw.ElapsedMilliseconds+" ms");
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
private void MasterServerCallBack(IRestResponse response)
|
|
{
|
|
masterServerResponse = response;
|
|
masterServerResponded = true;
|
|
}
|
|
|
|
public override void AddToGUIUpdateList()
|
|
{
|
|
if (started) base.AddToGUIUpdateList();
|
|
|
|
if (settingsFrame != null) settingsFrame.AddToGUIUpdateList();
|
|
if (log.LogFrame != null) log.LogFrame.AddToGUIUpdateList();
|
|
}
|
|
|
|
public override void Update(float deltaTime)
|
|
{
|
|
if (ShowNetStats) netStats.Update(deltaTime);
|
|
if (settingsFrame != null) settingsFrame.Update(deltaTime);
|
|
if (log.LogFrame != null) log.LogFrame.Update(deltaTime);
|
|
|
|
if (!started) return;
|
|
|
|
base.Update(deltaTime);
|
|
|
|
foreach (UnauthenticatedClient unauthClient in unauthenticatedClients)
|
|
{
|
|
unauthClient.AuthTimer -= deltaTime;
|
|
if (unauthClient.AuthTimer <= 0.0f)
|
|
{
|
|
unauthClient.Connection.Disconnect("Connection timed out");
|
|
}
|
|
}
|
|
|
|
unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f);
|
|
|
|
fileSender.Update(deltaTime);
|
|
|
|
if (gameStarted)
|
|
{
|
|
if (respawnManager != null) respawnManager.Update(deltaTime);
|
|
|
|
entityEventManager.Update(connectedClients);
|
|
|
|
bool isCrewDead =
|
|
connectedClients.Find(c => c.Character != null && !c.Character.IsDead)==null &&
|
|
(myCharacter == null || myCharacter.IsDead);
|
|
|
|
//restart if all characters are dead or submarine is at the end of the level
|
|
if ((autoRestart && isCrewDead)
|
|
||
|
|
(EndRoundAtLevelEnd && Submarine.MainSub != null && Submarine.MainSub.AtEndPosition && Submarine.MainSubs[1]==null))
|
|
{
|
|
if (AutoRestart && isCrewDead)
|
|
{
|
|
Log("Ending round (entire crew dead)", Color.Cyan);
|
|
}
|
|
else
|
|
{
|
|
Log("Ending round (submarine reached the end of the level)", Color.Cyan);
|
|
}
|
|
|
|
EndGame();
|
|
return;
|
|
}
|
|
}
|
|
else if (initiatedStartGame)
|
|
{
|
|
//tried to start up the game and StartGame coroutine is not running anymore
|
|
// -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer
|
|
if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine))
|
|
{
|
|
if (autoRestart) AutoRestartTimer = Math.Max(AutoRestartInterval, 5.0f);
|
|
GameMain.NetLobbyScreen.StartButton.Enabled = true;
|
|
|
|
GameMain.NetLobbyScreen.LastUpdateID++;
|
|
|
|
startGameCoroutine = null;
|
|
initiatedStartGame = false;
|
|
}
|
|
}
|
|
else if (autoRestart && Screen.Selected == GameMain.NetLobbyScreen && connectedClients.Count>0)
|
|
{
|
|
AutoRestartTimer -= deltaTime;
|
|
if (AutoRestartTimer < 0.0f && GameMain.NetLobbyScreen.StartButton.Enabled)
|
|
{
|
|
StartGameClicked(null,null);
|
|
}
|
|
}
|
|
|
|
for (int i = disconnectedClients.Count - 1; i >= 0; i-- )
|
|
{
|
|
disconnectedClients[i].deleteDisconnectedTimer -= deltaTime;
|
|
if (disconnectedClients[i].deleteDisconnectedTimer > 0.0f) continue;
|
|
|
|
if (gameStarted && disconnectedClients[i].Character!=null)
|
|
{
|
|
disconnectedClients[i].Character.Kill(CauseOfDeath.Damage, true);
|
|
disconnectedClients[i].Character = null;
|
|
}
|
|
|
|
disconnectedClients.RemoveAt(i);
|
|
}
|
|
|
|
foreach (Client c in connectedClients)
|
|
{
|
|
//slowly reset spam timers
|
|
c.ChatSpamTimer = Math.Max(0.0f, c.ChatSpamTimer - deltaTime);
|
|
c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime);
|
|
}
|
|
|
|
NetIncomingMessage inc = null;
|
|
while ((inc = server.ReadMessage()) != null)
|
|
{
|
|
try
|
|
{
|
|
switch (inc.MessageType)
|
|
{
|
|
case NetIncomingMessageType.Data:
|
|
ReadDataMessage(inc);
|
|
break;
|
|
case NetIncomingMessageType.StatusChanged:
|
|
switch (inc.SenderConnection.Status)
|
|
{
|
|
case NetConnectionStatus.Disconnected:
|
|
var connectedClient = connectedClients.Find(c => c.Connection == inc.SenderConnection);
|
|
/*if (connectedClient != null && !disconnectedClients.Contains(connectedClient))
|
|
{
|
|
connectedClient.deleteDisconnectedTimer = NetConfig.DeleteDisconnectedTime;
|
|
disconnectedClients.Add(connectedClient);
|
|
}
|
|
*/
|
|
DisconnectClient(inc.SenderConnection,
|
|
connectedClient != null ? connectedClient.name + " has disconnected" : "");
|
|
break;
|
|
}
|
|
break;
|
|
case NetIncomingMessageType.ConnectionApproval:
|
|
if (banList.IsBanned(inc.SenderEndPoint.Address.ToString()))
|
|
{
|
|
inc.SenderConnection.Deny("You have been banned from the server");
|
|
}
|
|
else if (ConnectedClients.Count >= MaxPlayers)
|
|
{
|
|
inc.SenderConnection.Deny("Server full");
|
|
}
|
|
else
|
|
{
|
|
if ((ClientPacketHeader)inc.SenderConnection.RemoteHailMessage.ReadByte() == ClientPacketHeader.REQUEST_AUTH)
|
|
{
|
|
inc.SenderConnection.Approve();
|
|
ClientAuthRequest(inc.SenderConnection);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
catch (Exception e)
|
|
{
|
|
#if DEBUG
|
|
DebugConsole.ThrowError("Failed to read incoming message", e);
|
|
#endif
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// if 30ms has passed
|
|
if (updateTimer < DateTime.Now)
|
|
{
|
|
if (server.ConnectionsCount > 0)
|
|
{
|
|
if (sparseUpdateTimer < DateTime.Now) SparseUpdate();
|
|
|
|
foreach (Client c in ConnectedClients)
|
|
{
|
|
if (gameStarted && c.inGame)
|
|
{
|
|
ClientWriteIngame(c);
|
|
}
|
|
else
|
|
{
|
|
ClientWriteLobby(c);
|
|
}
|
|
}
|
|
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
item.NeedsPositionUpdate = false;
|
|
}
|
|
}
|
|
|
|
updateTimer = DateTime.Now + updateInterval;
|
|
}
|
|
|
|
if (!registeredToMaster || refreshMasterTimer >= DateTime.Now) return;
|
|
|
|
CoroutineManager.StartCoroutine(RefreshMaster());
|
|
refreshMasterTimer = DateTime.Now + refreshMasterInterval;
|
|
}
|
|
|
|
private void ReadDataMessage(NetIncomingMessage inc)
|
|
{
|
|
if (banList.IsBanned(inc.SenderEndPoint.Address.ToString()))
|
|
{
|
|
KickClient(inc.SenderConnection, true);
|
|
return;
|
|
}
|
|
|
|
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
|
|
switch (header)
|
|
{
|
|
case ClientPacketHeader.REQUEST_AUTH:
|
|
ClientAuthRequest(inc.SenderConnection);
|
|
break;
|
|
case ClientPacketHeader.REQUEST_INIT:
|
|
ClientInitRequest(inc);
|
|
break;
|
|
|
|
case ClientPacketHeader.RESPONSE_STARTGAME:
|
|
var connectedClient = connectedClients.Find(c => c.Connection == inc.SenderConnection);
|
|
if (connectedClient != null)
|
|
{
|
|
connectedClient.ReadyToStart = inc.ReadBoolean();
|
|
UpdateCharacterInfo(inc, connectedClient);
|
|
|
|
//game already started -> send start message immediately
|
|
if (gameStarted)
|
|
{
|
|
SendStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.gameMode.Preset, connectedClient);
|
|
}
|
|
}
|
|
break;
|
|
case ClientPacketHeader.UPDATE_LOBBY:
|
|
ClientReadLobby(inc);
|
|
break;
|
|
case ClientPacketHeader.UPDATE_INGAME:
|
|
if (!gameStarted) return;
|
|
|
|
ClientReadIngame(inc);
|
|
break;
|
|
case ClientPacketHeader.SERVER_COMMAND:
|
|
ClientReadServerCommand(inc);
|
|
break;
|
|
case ClientPacketHeader.FILE_REQUEST:
|
|
if (AllowFileTransfers)
|
|
{
|
|
fileSender.ReadFileRequest(inc);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void SparseUpdate()
|
|
{
|
|
sparseUpdateTimer = DateTime.Now + sparseUpdateInterval;
|
|
}
|
|
|
|
public void CreateEntityEvent(IServerSerializable entity, object[] extraData = null)
|
|
{
|
|
entityEventManager.CreateEvent(entity, extraData);
|
|
}
|
|
|
|
private byte GetNewClientID()
|
|
{
|
|
byte userID = 1;
|
|
while (connectedClients.Any(c => c.ID == userID))
|
|
{
|
|
userID++;
|
|
}
|
|
return userID;
|
|
}
|
|
|
|
private void ClientReadLobby(NetIncomingMessage inc)
|
|
{
|
|
Client c = ConnectedClients.Find(x => x.Connection == inc.SenderConnection);
|
|
if (c == null)
|
|
{
|
|
inc.SenderConnection.Disconnect("You're not a connected client.");
|
|
return;
|
|
}
|
|
|
|
ClientNetObject objHeader;
|
|
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
|
|
{
|
|
switch (objHeader)
|
|
{
|
|
case ClientNetObject.SYNC_IDS:
|
|
//TODO: might want to use a clever class for this
|
|
c.lastRecvGeneralUpdate = NetIdUtils.Clamp(inc.ReadUInt16(), c.lastRecvGeneralUpdate, GameMain.NetLobbyScreen.LastUpdateID);
|
|
c.lastRecvChatMsgID = NetIdUtils.Clamp(inc.ReadUInt16(), c.lastRecvChatMsgID, c.lastChatMsgQueueID);
|
|
break;
|
|
case ClientNetObject.CHAT_MESSAGE:
|
|
ChatMessage.ServerRead(inc, c);
|
|
break;
|
|
case ClientNetObject.VOTE:
|
|
Voting.ServerRead(inc, c);
|
|
break;
|
|
default:
|
|
return;
|
|
//break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClientReadIngame(NetIncomingMessage inc)
|
|
{
|
|
Client c = ConnectedClients.Find(x => x.Connection == inc.SenderConnection);
|
|
if (c == null)
|
|
{
|
|
inc.SenderConnection.Disconnect("You're not a connected client.");
|
|
return;
|
|
}
|
|
|
|
if (gameStarted)
|
|
{
|
|
if (!c.inGame)
|
|
{
|
|
//check if midround syncing is needed due to missed unique events
|
|
entityEventManager.InitClientMidRoundSync(c);
|
|
c.inGame = true;
|
|
}
|
|
}
|
|
|
|
ClientNetObject objHeader;
|
|
while ((objHeader = (ClientNetObject)inc.ReadByte()) != ClientNetObject.END_OF_MESSAGE)
|
|
{
|
|
switch (objHeader)
|
|
{
|
|
case ClientNetObject.SYNC_IDS:
|
|
//TODO: might want to use a clever class for this
|
|
|
|
UInt16 lastRecvChatMsgID = inc.ReadUInt16();
|
|
UInt16 lastRecvEntityEventID = inc.ReadUInt16();
|
|
|
|
//last msgs we've created/sent, the client IDs should never be higher than these
|
|
UInt16 lastEntityEventID = entityEventManager.Events.Count == 0 ? (UInt16)0 : entityEventManager.Events.Last().ID;
|
|
|
|
if (c.NeedsMidRoundSync)
|
|
{
|
|
//received all the old events -> client in sync, we can switch to normal behavior
|
|
if (lastRecvEntityEventID >= c.UnreceivedEntityEventCount - 1 ||
|
|
c.UnreceivedEntityEventCount == 0)
|
|
{
|
|
c.NeedsMidRoundSync = false;
|
|
}
|
|
else
|
|
{
|
|
lastEntityEventID = (UInt16)(c.UnreceivedEntityEventCount - 1);
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
//client thinks they've received a msg we haven't sent yet (corrupted packet, msg read/written incorrectly?)
|
|
if (NetIdUtils.IdMoreRecent(lastRecvChatMsgID, c.lastChatMsgQueueID))
|
|
DebugConsole.ThrowError("client.lastRecvChatMsgID > lastChatMsgQueueID");
|
|
|
|
if (lastRecvEntityEventID > lastEntityEventID)
|
|
DebugConsole.ThrowError("client.lastRecvEntityEventID > lastEntityEventID");
|
|
#endif
|
|
|
|
if (NetIdUtils.IdMoreRecent(lastRecvChatMsgID, c.lastRecvChatMsgID)) c.lastRecvChatMsgID = lastRecvChatMsgID;
|
|
if (NetIdUtils.IdMoreRecent(c.lastRecvChatMsgID, c.lastChatMsgQueueID)) c.lastRecvChatMsgID = c.lastChatMsgQueueID;
|
|
|
|
if (NetIdUtils.IdMoreRecent(lastRecvEntityEventID, c.lastRecvEntityEventID)) c.lastRecvEntityEventID = lastRecvEntityEventID;
|
|
if (NetIdUtils.IdMoreRecent(c.lastRecvEntityEventID, lastEntityEventID)) c.lastRecvEntityEventID = lastEntityEventID;
|
|
|
|
break;
|
|
case ClientNetObject.CHAT_MESSAGE:
|
|
ChatMessage.ServerRead(inc, c);
|
|
break;
|
|
case ClientNetObject.CHARACTER_INPUT:
|
|
if (c.Character != null)
|
|
{
|
|
c.Character.ServerRead(objHeader, inc, c);
|
|
}
|
|
break;
|
|
case ClientNetObject.ENTITY_STATE:
|
|
entityEventManager.Read(inc, c);
|
|
break;
|
|
case ClientNetObject.VOTE:
|
|
Voting.ServerRead(inc, c);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClientReadServerCommand(NetIncomingMessage inc)
|
|
{
|
|
Client c = ConnectedClients.Find(x => x.Connection == inc.SenderConnection);
|
|
if (c == null)
|
|
{
|
|
inc.SenderConnection.Disconnect("You're not a connected client.");
|
|
return;
|
|
}
|
|
|
|
ClientPermissions command = ClientPermissions.None;
|
|
try
|
|
{
|
|
command = (ClientPermissions)inc.ReadByte();
|
|
}
|
|
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!c.HasPermission(command))
|
|
{
|
|
Log("Client \""+c.name+"\" sent a server command \""+command+"\". Permission denied.", Color.Red);
|
|
return;
|
|
}
|
|
|
|
switch (command)
|
|
{
|
|
case ClientPermissions.Kick:
|
|
string kickedName = inc.ReadString();
|
|
var kickedClient = connectedClients.Find(cl => cl != c && cl.name == kickedName);
|
|
if (kickedClient != null)
|
|
{
|
|
Log("Client \"" + c.name + "\" kicked \"" + kickedClient.name + "\".", Color.Red);
|
|
KickClient(kickedClient, false, false);
|
|
}
|
|
break;
|
|
case ClientPermissions.Ban:
|
|
string bannedName = inc.ReadString();
|
|
var bannedClient = connectedClients.Find(cl => cl != c && cl.name == bannedName);
|
|
if (bannedClient != null)
|
|
{
|
|
Log("Client \"" + c.name + "\" banned \"" + bannedClient.name + "\".", Color.Red);
|
|
KickClient(bannedClient, true, false);
|
|
}
|
|
break;
|
|
case ClientPermissions.EndRound:
|
|
if (gameStarted)
|
|
{
|
|
Log("Client \"" + c.name + "\" ended the round.", Color.Cyan);
|
|
EndGame();
|
|
}
|
|
break;
|
|
}
|
|
|
|
inc.ReadPadBits();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write info that the client needs when joining the server
|
|
/// </summary>
|
|
private void ClientWriteInitial(Client c, NetBuffer outmsg)
|
|
{
|
|
outmsg.Write(c.ID);
|
|
|
|
var subList = GameMain.NetLobbyScreen.GetSubList();
|
|
outmsg.Write((UInt16)subList.Count);
|
|
for (int i = 0; i < subList.Count; i++)
|
|
{
|
|
outmsg.Write(subList[i].Name);
|
|
outmsg.Write(subList[i].MD5Hash.ToString());
|
|
}
|
|
|
|
outmsg.Write(GameStarted);
|
|
outmsg.Write(AllowSpectating);
|
|
|
|
outmsg.Write((byte)c.Permissions);
|
|
}
|
|
|
|
private void ClientWriteIngame(Client c)
|
|
{
|
|
NetOutgoingMessage outmsg = server.CreateMessage();
|
|
outmsg.Write((byte)ServerPacketHeader.UPDATE_INGAME);
|
|
|
|
outmsg.Write((float)NetTime.Now);
|
|
|
|
outmsg.Write((byte)ServerNetObject.SYNC_IDS);
|
|
outmsg.Write(c.lastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
|
|
outmsg.Write(c.lastSentEntityEventID);
|
|
|
|
c.chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.lastRecvChatMsgID));
|
|
foreach (ChatMessage cMsg in c.chatMsgQueue)
|
|
{
|
|
cMsg.ServerWrite(outmsg, c);
|
|
}
|
|
|
|
foreach (Character character in Character.CharacterList)
|
|
{
|
|
if (!character.Enabled) continue;
|
|
|
|
if (c.Character != null &&
|
|
Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition) >=
|
|
NetConfig.CharacterIgnoreDistance * NetConfig.CharacterIgnoreDistance)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
outmsg.Write((byte)ServerNetObject.ENTITY_POSITION);
|
|
character.ServerWrite(outmsg, c);
|
|
outmsg.WritePadBits();
|
|
}
|
|
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
//if docked to a sub with a smaller ID, don't send an update
|
|
// (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle)
|
|
if (sub.DockedTo.Any(s => s.ID < sub.ID)) continue;
|
|
|
|
outmsg.Write((byte)ServerNetObject.ENTITY_POSITION);
|
|
sub.ServerWrite(outmsg, c);
|
|
outmsg.WritePadBits();
|
|
}
|
|
|
|
foreach (Item item in Item.ItemList)
|
|
{
|
|
if (!item.NeedsPositionUpdate) continue;
|
|
|
|
outmsg.Write((byte)ServerNetObject.ENTITY_POSITION);
|
|
item.ServerWritePosition(outmsg, c);
|
|
outmsg.WritePadBits();
|
|
}
|
|
|
|
entityEventManager.Write(c, outmsg);
|
|
|
|
outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE);
|
|
server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable);
|
|
}
|
|
|
|
private void ClientWriteLobby(Client c)
|
|
{
|
|
NetOutgoingMessage outmsg = server.CreateMessage();
|
|
outmsg.Write((byte)ServerPacketHeader.UPDATE_LOBBY);
|
|
|
|
outmsg.Write((byte)ServerNetObject.SYNC_IDS);
|
|
|
|
if (NetIdUtils.IdMoreRecent(GameMain.NetLobbyScreen.LastUpdateID, c.lastRecvGeneralUpdate))
|
|
{
|
|
outmsg.Write(true);
|
|
outmsg.WritePadBits();
|
|
|
|
outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
|
|
outmsg.Write(GameMain.NetLobbyScreen.GetServerName());
|
|
outmsg.Write(GameMain.NetLobbyScreen.ServerMessage.Text);
|
|
|
|
outmsg.Write(c.lastRecvGeneralUpdate < 1);
|
|
if (c.lastRecvGeneralUpdate < 1)
|
|
{
|
|
ClientWriteInitial(c, outmsg);
|
|
}
|
|
outmsg.Write((GameMain.NetLobbyScreen.SubList.SelectedData as Submarine).Name);
|
|
outmsg.Write((GameMain.NetLobbyScreen.SubList.SelectedData as Submarine).MD5Hash.ToString());
|
|
outmsg.Write((GameMain.NetLobbyScreen.ShuttleList.SelectedData as Submarine).Name);
|
|
outmsg.Write((GameMain.NetLobbyScreen.ShuttleList.SelectedData as Submarine).MD5Hash.ToString());
|
|
|
|
outmsg.Write(Voting.AllowSubVoting);
|
|
outmsg.Write(Voting.AllowModeVoting);
|
|
|
|
outmsg.WriteRangedInteger(0, 2, (int)TraitorsEnabled);
|
|
|
|
outmsg.WriteRangedInteger(0, Mission.MissionTypes.Count - 1, (GameMain.NetLobbyScreen.MissionTypeIndex));
|
|
|
|
outmsg.Write((byte)GameMain.NetLobbyScreen.ModeList.SelectedIndex);
|
|
outmsg.Write(GameMain.NetLobbyScreen.LevelSeed);
|
|
|
|
outmsg.Write(AutoRestart);
|
|
if (autoRestart)
|
|
{
|
|
outmsg.Write(AutoRestartTimer);
|
|
}
|
|
|
|
outmsg.Write((byte)connectedClients.Count);
|
|
foreach (Client client in connectedClients)
|
|
{
|
|
outmsg.Write(client.ID);
|
|
outmsg.Write(client.name);
|
|
outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
outmsg.Write(false);
|
|
outmsg.WritePadBits();
|
|
}
|
|
|
|
outmsg.Write(c.lastSentChatMsgID); //send this to client so they know which chat messages weren't received by the server
|
|
|
|
c.chatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.lastRecvChatMsgID));
|
|
foreach (ChatMessage cMsg in c.chatMsgQueue)
|
|
{
|
|
cMsg.ServerWrite(outmsg, c);
|
|
}
|
|
|
|
outmsg.Write((byte)ServerNetObject.END_OF_MESSAGE);
|
|
server.SendMessage(outmsg, c.Connection, NetDeliveryMethod.Unreliable);
|
|
}
|
|
|
|
public bool StartGameClicked(GUIButton button, object obj)
|
|
{
|
|
Submarine selectedSub = null;
|
|
Submarine selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle;
|
|
|
|
if (Voting.AllowSubVoting)
|
|
{
|
|
selectedSub = Voting.HighestVoted<Submarine>(VoteType.Sub, connectedClients);
|
|
if (selectedSub == null) selectedSub = GameMain.NetLobbyScreen.SelectedSub;
|
|
}
|
|
else
|
|
{
|
|
selectedSub = GameMain.NetLobbyScreen.SelectedSub;
|
|
}
|
|
|
|
if (selectedSub == null)
|
|
{
|
|
GameMain.NetLobbyScreen.SubList.Flash();
|
|
return false;
|
|
}
|
|
|
|
if (selectedShuttle == null)
|
|
{
|
|
GameMain.NetLobbyScreen.ShuttleList.Flash();
|
|
return false;
|
|
}
|
|
|
|
GameModePreset selectedMode = Voting.HighestVoted<GameModePreset>(VoteType.Mode, connectedClients);
|
|
if (selectedMode == null) selectedMode = GameMain.NetLobbyScreen.SelectedMode;
|
|
|
|
if (selectedMode == null)
|
|
{
|
|
GameMain.NetLobbyScreen.ModeList.Flash();
|
|
return false;
|
|
}
|
|
|
|
CoroutineManager.StartCoroutine(InitiateStartGame(selectedSub, selectedShuttle, selectedMode), "InitiateStartGame");
|
|
|
|
return true;
|
|
}
|
|
|
|
private IEnumerable<object> InitiateStartGame(Submarine selectedSub, Submarine selectedShuttle, GameModePreset selectedMode)
|
|
{
|
|
GameMain.NetLobbyScreen.StartButton.Enabled = false;
|
|
|
|
if (connectedClients.Any())
|
|
{
|
|
NetOutgoingMessage msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.QUERY_STARTGAME);
|
|
|
|
msg.Write(selectedSub.Name);
|
|
msg.Write(selectedSub.MD5Hash.Hash);
|
|
|
|
msg.Write(selectedShuttle.Name);
|
|
msg.Write(selectedShuttle.MD5Hash.Hash);
|
|
|
|
connectedClients.ForEach(c => c.ReadyToStart = false);
|
|
|
|
server.SendMessage(msg, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0);
|
|
|
|
//give the clients a few seconds to request missing sub/shuttle files before starting the round
|
|
float waitForResponseTimer = 5.0f;
|
|
while (connectedClients.Any(c => !c.ReadyToStart) && waitForResponseTimer > 0.0f)
|
|
{
|
|
waitForResponseTimer -= CoroutineManager.UnscaledDeltaTime;
|
|
yield return CoroutineStatus.Running;
|
|
}
|
|
|
|
if (fileSender.ActiveTransfers.Count > 0)
|
|
{
|
|
var msgBox = new GUIMessageBox("", "Waiting for file transfers to finish before starting the round...", new string[] { "Start now" });
|
|
msgBox.Buttons[0].OnClicked += msgBox.Close;
|
|
|
|
float waitForTransfersTimer = 20.0f;
|
|
while (fileSender.ActiveTransfers.Count > 0 && waitForTransfersTimer > 0.0f)
|
|
{
|
|
waitForTransfersTimer -= CoroutineManager.UnscaledDeltaTime;
|
|
|
|
//message box close, break and start the round immediately
|
|
if (!GUIMessageBox.MessageBoxes.Contains(msgBox))
|
|
{
|
|
break;
|
|
}
|
|
|
|
yield return CoroutineStatus.Running;
|
|
}
|
|
}
|
|
}
|
|
|
|
startGameCoroutine = GameMain.ShowLoading(StartGame(selectedSub, selectedShuttle, selectedMode), false);
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
private IEnumerable<object> StartGame(Submarine selectedSub, Submarine selectedShuttle, GameModePreset selectedMode)
|
|
{
|
|
initiatedStartGame = true;
|
|
|
|
entityEventManager.Clear();
|
|
|
|
GameMain.NetLobbyScreen.StartButton.Enabled = false;
|
|
|
|
GUIMessageBox.CloseAll();
|
|
|
|
roundStartSeed = DateTime.Now.Millisecond;
|
|
Rand.SetSyncedSeed(roundStartSeed);
|
|
|
|
int teamCount = 1;
|
|
int hostTeam = 1;
|
|
|
|
GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, Mission.MissionTypes[GameMain.NetLobbyScreen.MissionTypeIndex]);
|
|
|
|
if (GameMain.GameSession.gameMode.Mission != null &&
|
|
GameMain.GameSession.gameMode.Mission.AssignTeamIDs(connectedClients,out hostTeam))
|
|
{
|
|
teamCount = 2;
|
|
}
|
|
|
|
GameMain.GameSession.StartShift(GameMain.NetLobbyScreen.LevelSeed, teamCount > 1);
|
|
|
|
GameServer.Log("Starting a new round...", Color.Cyan);
|
|
GameServer.Log("Submarine: " + selectedSub.Name, Color.Cyan);
|
|
GameServer.Log("Game mode: " + selectedMode.Name, Color.Cyan);
|
|
GameServer.Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, Color.Cyan);
|
|
|
|
bool missionAllowRespawn =
|
|
!(GameMain.GameSession.gameMode is MissionMode) ||
|
|
((MissionMode)GameMain.GameSession.gameMode).Mission.AllowRespawn;
|
|
|
|
if (AllowRespawn && missionAllowRespawn) respawnManager = new RespawnManager(this, selectedShuttle);
|
|
|
|
//assign jobs and spawnpoints separately for each team
|
|
for (int teamID = 1; teamID <= teamCount; teamID++)
|
|
{
|
|
//find the clients in this team
|
|
List<Client> teamClients = teamCount == 1 ? connectedClients : connectedClients.FindAll(c => c.TeamID == teamID);
|
|
|
|
if (!teamClients.Any() && teamID > 1) continue;
|
|
|
|
AssignJobs(teamClients, teamID == hostTeam);
|
|
|
|
List<CharacterInfo> characterInfos = new List<CharacterInfo>();
|
|
foreach (Client client in teamClients)
|
|
{
|
|
client.NeedsMidRoundSync = false;
|
|
|
|
client.entityEventLastSent.Clear();
|
|
client.lastSentEntityEventID = 0;
|
|
client.lastRecvEntityEventID = 0;
|
|
client.UnreceivedEntityEventCount = 0;
|
|
|
|
if (client.characterInfo == null)
|
|
{
|
|
client.characterInfo = new CharacterInfo(Character.HumanConfigFile, client.name);
|
|
}
|
|
characterInfos.Add(client.characterInfo);
|
|
client.characterInfo.Job = new Job(client.assignedJob);
|
|
}
|
|
|
|
//host's character
|
|
if (characterInfo != null && hostTeam == teamID)
|
|
{
|
|
characterInfo.Job = new Job(GameMain.NetLobbyScreen.JobPreferences[0]);
|
|
characterInfos.Add(characterInfo);
|
|
}
|
|
|
|
WayPoint[] assignedWayPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[teamID - 1]);
|
|
for (int i = 0; i < teamClients.Count; i++)
|
|
{
|
|
Character spawnedCharacter = Character.Create(teamClients[i].characterInfo, assignedWayPoints[i].WorldPosition, true, false);
|
|
spawnedCharacter.AnimController.Frozen = true;
|
|
spawnedCharacter.GiveJobItems(assignedWayPoints[i]);
|
|
spawnedCharacter.TeamID = (byte)teamID;
|
|
|
|
teamClients[i].Character = spawnedCharacter;
|
|
|
|
GameMain.GameSession.CrewManager.characters.Add(spawnedCharacter);
|
|
}
|
|
|
|
if (characterInfo != null && hostTeam == teamID)
|
|
{
|
|
myCharacter = Character.Create(characterInfo, assignedWayPoints[assignedWayPoints.Length - 1].WorldPosition, false, false);
|
|
myCharacter.GiveJobItems(assignedWayPoints.Last());
|
|
myCharacter.TeamID = (byte)teamID;
|
|
|
|
Character.Controlled = myCharacter;
|
|
GameMain.GameSession.CrewManager.characters.Add(myCharacter);
|
|
}
|
|
}
|
|
|
|
TraitorManager = null;
|
|
if (TraitorsEnabled == YesNoMaybe.Yes ||
|
|
(TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f))
|
|
{
|
|
TraitorManager = new TraitorManager(this);
|
|
|
|
if (TraitorManager.TraitorCharacter!=null && TraitorManager.TargetCharacter != null)
|
|
{
|
|
Log(TraitorManager.TraitorCharacter.Name + " is the traitor and the target is " + TraitorManager.TargetCharacter.Name, Color.Cyan);
|
|
}
|
|
}
|
|
|
|
SendStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.gameMode.Preset, connectedClients);
|
|
//var startMessage = CreateStartMessage(roundStartSeed, Submarine.MainSub, GameMain.GameSession.gameMode.Preset);
|
|
//server.SendMessage(startMessage, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0);
|
|
|
|
yield return CoroutineStatus.Running;
|
|
|
|
//UpdateCrewFrame();
|
|
|
|
//TraitorManager = null;
|
|
//if (TraitorsEnabled == YesNoMaybe.Yes ||
|
|
// (TraitorsEnabled == YesNoMaybe.Maybe && Rand.Range(0.0f, 1.0f) < 0.5f))
|
|
//{
|
|
// TraitorManager = new TraitorManager(this);
|
|
//}
|
|
|
|
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
|
|
GameMain.GameScreen.Select();
|
|
|
|
AddChatMessage("Press TAB to chat. Use ''r;'' to talk through the radio.", ChatMessageType.Server);
|
|
|
|
GameMain.NetLobbyScreen.StartButton.Enabled = true;
|
|
|
|
gameStarted = true;
|
|
initiatedStartGame = false;
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
private void SendStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode, List<Client> clients)
|
|
{
|
|
foreach (Client client in clients)
|
|
{
|
|
SendStartMessage(seed, selectedSub, selectedMode, client);
|
|
}
|
|
}
|
|
|
|
private void SendStartMessage(int seed, Submarine selectedSub, GameModePreset selectedMode, Client client)
|
|
{
|
|
NetOutgoingMessage msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.STARTGAME);
|
|
|
|
msg.Write(seed);
|
|
|
|
msg.Write(GameMain.NetLobbyScreen.LevelSeed);
|
|
|
|
msg.Write((byte)GameMain.NetLobbyScreen.MissionTypeIndex);
|
|
|
|
msg.Write(selectedSub.Name);
|
|
msg.Write(selectedSub.MD5Hash.Hash);
|
|
|
|
msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name);
|
|
msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash);
|
|
|
|
msg.Write(selectedMode.Name);
|
|
|
|
bool missionAllowRespawn =
|
|
!(GameMain.GameSession.gameMode is MissionMode) ||
|
|
((MissionMode)GameMain.GameSession.gameMode).Mission.AllowRespawn;
|
|
|
|
msg.Write(AllowRespawn && missionAllowRespawn);
|
|
msg.Write(Submarine.MainSubs[1] != null); //loadSecondSub
|
|
|
|
if (TraitorManager != null &&
|
|
TraitorManager.TraitorCharacter != null &&
|
|
TraitorManager.TargetCharacter != null &&
|
|
TraitorManager.TraitorCharacter == client.Character)
|
|
{
|
|
msg.Write(true);
|
|
msg.Write(TraitorManager.TargetCharacter.Name);
|
|
}
|
|
else
|
|
{
|
|
msg.Write(false);
|
|
}
|
|
|
|
server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered);
|
|
}
|
|
|
|
public void EndGame()
|
|
{
|
|
if (!gameStarted) return;
|
|
|
|
string endMessage = "The round has ended." + '\n';
|
|
|
|
if (TraitorManager != null)
|
|
{
|
|
endMessage += TraitorManager.GetEndMessage();
|
|
}
|
|
|
|
Mission mission = GameMain.GameSession.Mission;
|
|
GameMain.GameSession.gameMode.End(endMessage);
|
|
|
|
if (autoRestart) AutoRestartTimer = AutoRestartInterval;
|
|
|
|
if (SaveServerLogs) log.Save();
|
|
|
|
Character.Controlled = null;
|
|
myCharacter = null;
|
|
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
|
|
GameMain.LightManager.LosEnabled = false;
|
|
|
|
entityEventManager.Clear();
|
|
foreach (Client c in connectedClients)
|
|
{
|
|
c.entityEventLastSent.Clear();
|
|
}
|
|
|
|
#if DEBUG
|
|
messageCount.Clear();
|
|
#endif
|
|
|
|
respawnManager = null;
|
|
gameStarted = false;
|
|
|
|
if (connectedClients.Count > 0)
|
|
{
|
|
NetOutgoingMessage msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.ENDGAME);
|
|
msg.Write(endMessage);
|
|
msg.Write(mission != null && mission.Completed);
|
|
if (server.ConnectionsCount > 0)
|
|
{
|
|
server.SendMessage(msg, server.Connections, NetDeliveryMethod.ReliableOrdered, 0);
|
|
}
|
|
|
|
foreach (Client client in connectedClients)
|
|
{
|
|
client.Character = null;
|
|
client.inGame = false;
|
|
}
|
|
}
|
|
|
|
CoroutineManager.StartCoroutine(EndCinematic(),"EndCinematic");
|
|
}
|
|
|
|
public IEnumerable<object> EndCinematic()
|
|
{
|
|
float endPreviewLength = 10.0f;
|
|
|
|
var cinematic = new TransitionCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, endPreviewLength);
|
|
|
|
new TransitionCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, endPreviewLength);
|
|
float secondsLeft = endPreviewLength;
|
|
|
|
do
|
|
{
|
|
secondsLeft -= CoroutineManager.UnscaledDeltaTime;
|
|
|
|
yield return CoroutineStatus.Running;
|
|
} while (secondsLeft > 0.0f);
|
|
|
|
Submarine.Unload();
|
|
entityEventManager.Clear();
|
|
|
|
GameMain.NetLobbyScreen.Select();
|
|
|
|
yield return CoroutineStatus.Success;
|
|
}
|
|
|
|
public override void KickPlayer(string playerName, bool ban, bool range=false)
|
|
{
|
|
playerName = playerName.ToLowerInvariant();
|
|
|
|
Client client = connectedClients.Find(c =>
|
|
c.name.ToLowerInvariant() == playerName ||
|
|
(c.Character != null && c.Character.Name.ToLowerInvariant() == playerName));
|
|
|
|
KickClient(client, ban, range);
|
|
}
|
|
|
|
public void KickClient(NetConnection conn, bool ban = false, bool range = false)
|
|
{
|
|
Client client = connectedClients.Find(c => c.Connection == conn);
|
|
if (client == null)
|
|
{
|
|
conn.Disconnect(ban ? "You have been banned from the server" : "You have been kicked from the server");
|
|
if (ban)
|
|
{
|
|
if (!banList.IsBanned(conn.RemoteEndPoint.Address.ToString()))
|
|
{
|
|
banList.BanPlayer("Unnamed", conn.RemoteEndPoint.Address.ToString());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
KickClient(client, ban, range);
|
|
}
|
|
}
|
|
|
|
public void KickClient(Client client, bool ban = false, bool range = false)
|
|
{
|
|
if (client == null) return;
|
|
|
|
if (ban)
|
|
{
|
|
DisconnectClient(client, client.name + " has been banned from the server", "You have been banned from the server");
|
|
string ip = client.Connection.RemoteEndPoint.Address.ToString();
|
|
if (range) { ip = banList.ToRange(ip); }
|
|
banList.BanPlayer(client.name, ip);
|
|
}
|
|
else
|
|
{
|
|
DisconnectClient(client, client.name + " has been kicked from the server", "You have been kicked from the server");
|
|
}
|
|
}
|
|
|
|
public void DisconnectClient(NetConnection senderConnection, string msg = "", string targetmsg = "")
|
|
{
|
|
Client client = connectedClients.Find(x => x.Connection == senderConnection);
|
|
if (client == null) return;
|
|
|
|
DisconnectClient(client, msg, targetmsg);
|
|
}
|
|
|
|
public void DisconnectClient(Client client, string msg = "", string targetmsg = "")
|
|
{
|
|
if (client == null) return;
|
|
|
|
if (gameStarted && client.Character != null)
|
|
{
|
|
client.Character.ClearInputs();
|
|
client.Character.Kill(CauseOfDeath.Disconnected, true);
|
|
}
|
|
|
|
client.Character = null;
|
|
client.inGame = false;
|
|
|
|
if (string.IsNullOrWhiteSpace(msg)) msg = client.name + " has left the server";
|
|
if (string.IsNullOrWhiteSpace(targetmsg)) targetmsg = "You have left the server";
|
|
|
|
Log(msg, ChatMessage.MessageColor[(int)ChatMessageType.Server]);
|
|
|
|
client.Connection.Disconnect(targetmsg);
|
|
|
|
GameMain.NetLobbyScreen.RemovePlayer(client.name);
|
|
connectedClients.Remove(client);
|
|
|
|
UpdateVoteStatus();
|
|
|
|
SendChatMessage(msg, ChatMessageType.Server);
|
|
|
|
UpdateCrewFrame();
|
|
|
|
refreshMasterTimer = DateTime.Now;
|
|
}
|
|
|
|
private void UpdateCrewFrame()
|
|
{
|
|
foreach (Client c in connectedClients)
|
|
{
|
|
if (c.Character == null || !c.inGame) continue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add the message to the chatbox and pass it to all clients who can receive it
|
|
/// </summary>
|
|
public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null)
|
|
{
|
|
Character senderCharacter = null;
|
|
string senderName = "";
|
|
|
|
Client targetClient = null;
|
|
|
|
if (type==null)
|
|
{
|
|
string tempStr;
|
|
string command = ChatMessage.GetChatMessageCommand(message, out tempStr);
|
|
switch (command.ToLowerInvariant())
|
|
{
|
|
case "r":
|
|
case "radio":
|
|
type = ChatMessageType.Radio;
|
|
break;
|
|
case "d":
|
|
case "dead":
|
|
type = ChatMessageType.Dead;
|
|
break;
|
|
default:
|
|
if (command != "")
|
|
{
|
|
if (command == name.ToLowerInvariant())
|
|
{
|
|
//a private message to the host
|
|
}
|
|
else
|
|
{
|
|
targetClient = connectedClients.Find(c =>
|
|
command == c.name.ToLowerInvariant() ||
|
|
(c.Character != null && command == c.Character.Name.ToLowerInvariant()));
|
|
|
|
if (targetClient == null)
|
|
{
|
|
AddChatMessage("Player \"" + command + "\" not found!", ChatMessageType.Error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
type = ChatMessageType.Private;
|
|
}
|
|
else
|
|
{
|
|
type = ChatMessageType.Default;
|
|
}
|
|
break;
|
|
}
|
|
|
|
message = tempStr;
|
|
}
|
|
|
|
if (gameStarted)
|
|
{
|
|
//msg sent by the server
|
|
if (senderClient == null)
|
|
{
|
|
senderCharacter = myCharacter;
|
|
senderName = myCharacter == null ? name : myCharacter.Name;
|
|
}
|
|
else //msg sent by a client
|
|
{
|
|
senderCharacter = senderClient.Character;
|
|
senderName = senderCharacter == null ? senderClient.name : senderCharacter.Name;
|
|
|
|
//sender doesn't have an alive character -> only ChatMessageType.Dead allowed
|
|
if (senderCharacter == null || senderCharacter.IsDead)
|
|
{
|
|
type = ChatMessageType.Dead;
|
|
}
|
|
else if (type == ChatMessageType.Private)
|
|
{
|
|
//sender has an alive character, sending private messages not allowed
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//msg sent by the server
|
|
if (senderClient == null)
|
|
{
|
|
senderName = name;
|
|
}
|
|
else //msg sent by a client
|
|
{
|
|
//game not started -> clients can only send normal and private chatmessages
|
|
if (type != ChatMessageType.Private) type = ChatMessageType.Default;
|
|
senderName = senderClient.name;
|
|
}
|
|
}
|
|
|
|
//check if the client is allowed to send the message
|
|
WifiComponent senderRadio = null;
|
|
switch (type)
|
|
{
|
|
case ChatMessageType.Radio:
|
|
if (senderCharacter == null) return;
|
|
|
|
//return if senderCharacter doesn't have a working radio
|
|
var radio = senderCharacter.Inventory.Items.First(i => i != null && i.GetComponent<WifiComponent>() != null);
|
|
if (radio == null) return;
|
|
|
|
senderRadio = radio.GetComponent<WifiComponent>();
|
|
if (!senderRadio.CanTransmit()) return;
|
|
break;
|
|
case ChatMessageType.Dead:
|
|
//character still alive -> not allowed
|
|
if (senderClient != null && senderCharacter != null && !senderCharacter.IsDead)
|
|
{
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (type == ChatMessageType.Server)
|
|
{
|
|
senderName = null;
|
|
senderCharacter = null;
|
|
}
|
|
|
|
//check which clients can receive the message and apply distance effects
|
|
foreach (Client client in ConnectedClients)
|
|
{
|
|
string modifiedMessage = message;
|
|
|
|
switch (type)
|
|
{
|
|
case ChatMessageType.Default:
|
|
case ChatMessageType.Radio:
|
|
if (senderCharacter != null &&
|
|
client.Character != null && !client.Character.IsDead)
|
|
{
|
|
modifiedMessage = ApplyChatMsgDistanceEffects(message, (ChatMessageType)type, senderCharacter, client.Character);
|
|
|
|
//too far to hear the msg -> don't send
|
|
if (string.IsNullOrWhiteSpace(modifiedMessage)) continue;
|
|
}
|
|
break;
|
|
case ChatMessageType.Dead:
|
|
//character still alive -> don't send
|
|
if (client.Character != null && !client.Character.IsDead) continue;
|
|
break;
|
|
case ChatMessageType.Private:
|
|
//private msg sent to someone else than this client -> don't send
|
|
if (client != targetClient && client != senderClient) continue;
|
|
break;
|
|
}
|
|
|
|
var chatMsg = ChatMessage.Create(
|
|
senderName,
|
|
modifiedMessage,
|
|
(ChatMessageType)type,
|
|
senderCharacter);
|
|
|
|
chatMsg.NetStateID = client.chatMsgQueue.Count > 0 ?
|
|
(ushort)(client.chatMsgQueue.Last().NetStateID + 1) :
|
|
(ushort)(client.lastRecvChatMsgID + 1);
|
|
|
|
client.chatMsgQueue.Add(chatMsg);
|
|
client.lastChatMsgQueueID = chatMsg.NetStateID;
|
|
}
|
|
|
|
string myReceivedMessage = message;
|
|
|
|
if (gameStarted && myCharacter != null)
|
|
{
|
|
myReceivedMessage = ApplyChatMsgDistanceEffects(message, (ChatMessageType)type, senderCharacter, myCharacter);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(myReceivedMessage) &&
|
|
(targetClient == null || senderClient == null))
|
|
{
|
|
AddChatMessage(myReceivedMessage, (ChatMessageType)type, senderName, senderCharacter);
|
|
}
|
|
}
|
|
|
|
private string ApplyChatMsgDistanceEffects(string message, ChatMessageType type, Character sender, Character receiver)
|
|
{
|
|
if (sender == null) return "";
|
|
|
|
switch (type)
|
|
{
|
|
case ChatMessageType.Default:
|
|
if (!receiver.IsDead)
|
|
{
|
|
return ChatMessage.ApplyDistanceEffect(receiver, sender, message, ChatMessage.SpeakRange);
|
|
}
|
|
break;
|
|
case ChatMessageType.Radio:
|
|
if (!receiver.IsDead)
|
|
{
|
|
var receiverItem = receiver.Inventory.Items.First(i => i != null && i.GetComponent<WifiComponent>() != null);
|
|
//client doesn't have a radio -> don't send
|
|
if (receiverItem == null) return "";
|
|
|
|
var senderItem = sender.Inventory.Items.First(i => i != null && i.GetComponent<WifiComponent>() != null);
|
|
if (senderItem == null) return "";
|
|
|
|
var receiverRadio = receiverItem.GetComponent<WifiComponent>();
|
|
var senderRadio = senderItem.GetComponent<WifiComponent>();
|
|
|
|
if (!receiverRadio.CanReceive(senderRadio)) return "";
|
|
|
|
return ChatMessage.ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
public override void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
|
|
{
|
|
base.Draw(spriteBatch);
|
|
|
|
if (settingsFrame != null)
|
|
{
|
|
settingsFrame.Draw(spriteBatch);
|
|
}
|
|
else if (log.LogFrame!=null)
|
|
{
|
|
log.LogFrame.Draw(spriteBatch);
|
|
}
|
|
|
|
if (!ShowNetStats) return;
|
|
|
|
int width = 200, height = 300;
|
|
int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f);
|
|
|
|
|
|
if (clientListScrollBar == null)
|
|
{
|
|
clientListScrollBar = new GUIScrollBar(new Rectangle(x + width - 10, y, 10, height), GUI.Style, 1.0f);
|
|
}
|
|
|
|
|
|
GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true);
|
|
GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White);
|
|
|
|
GUI.SmallFont.DrawString(spriteBatch, "Connections: "+server.ConnectionsCount, new Vector2(x + 10, y + 30), Color.White);
|
|
GUI.SmallFont.DrawString(spriteBatch, "Received bytes: " + MathUtils.GetBytesReadable(server.Statistics.ReceivedBytes), new Vector2(x + 10, y + 45), Color.White);
|
|
GUI.SmallFont.DrawString(spriteBatch, "Received packets: " + server.Statistics.ReceivedPackets, new Vector2(x + 10, y + 60), Color.White);
|
|
|
|
GUI.SmallFont.DrawString(spriteBatch, "Sent bytes: " + MathUtils.GetBytesReadable(server.Statistics.SentBytes), new Vector2(x + 10, y + 75), Color.White);
|
|
GUI.SmallFont.DrawString(spriteBatch, "Sent packets: " + server.Statistics.SentPackets, new Vector2(x + 10, y + 90), Color.White);
|
|
|
|
int resentMessages = 0;
|
|
|
|
int clientListHeight = connectedClients.Count * 40;
|
|
float scrollBarHeight = (height - 110) / (float)Math.Max(clientListHeight, 110);
|
|
|
|
if (clientListScrollBar.BarSize != scrollBarHeight)
|
|
{
|
|
clientListScrollBar.BarSize = scrollBarHeight;
|
|
}
|
|
|
|
int startY = y + 110;
|
|
y = (startY - (int)(clientListScrollBar.BarScroll * (clientListHeight-(height - 110))));
|
|
foreach (Client c in connectedClients)
|
|
{
|
|
Color clientColor = c.Connection.AverageRoundtripTime > 0.3f ? Color.Red : Color.White;
|
|
|
|
if (y >= startY && y < startY + height - 120)
|
|
{
|
|
GUI.SmallFont.DrawString(spriteBatch, c.name + " ("+c.Connection.RemoteEndPoint.Address.ToString()+")", new Vector2(x + 10, y), clientColor);
|
|
GUI.SmallFont.DrawString(spriteBatch, "Ping: " + (int)(c.Connection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x+20, y+10), clientColor);
|
|
}
|
|
if (y + 25 >= startY && y < startY + height - 130) GUI.SmallFont.DrawString(spriteBatch, "Resent messages: " + c.Connection.Statistics.ResentMessages, new Vector2(x + 20, y + 20), clientColor);
|
|
|
|
resentMessages += (int)c.Connection.Statistics.ResentMessages;
|
|
|
|
y += 40;
|
|
}
|
|
|
|
clientListScrollBar.Update(1.0f / 60.0f);
|
|
clientListScrollBar.Draw(spriteBatch);
|
|
|
|
netStats.AddValue(NetStats.NetStatType.ResentMessages, Math.Max(resentMessages, 0));
|
|
netStats.AddValue(NetStats.NetStatType.SentBytes, server.Statistics.SentBytes);
|
|
netStats.AddValue(NetStats.NetStatType.ReceivedBytes, server.Statistics.ReceivedBytes);
|
|
|
|
netStats.Draw(spriteBatch, new Rectangle(200,0,800,200), this);
|
|
}
|
|
|
|
private void FileTransferChanged(FileSender.FileTransferOut transfer)
|
|
{
|
|
Client recipient = connectedClients.Find(c => c.Connection == transfer.Connection);
|
|
UpdateFileTransferIndicator(recipient);
|
|
}
|
|
|
|
public void SendCancelTransferMsg(FileSender.FileTransferOut transfer)
|
|
{
|
|
NetOutgoingMessage msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.FILE_TRANSFER);
|
|
msg.Write((byte)FileTransferMessageType.Cancel);
|
|
msg.Write((byte)transfer.SequenceChannel);
|
|
server.SendMessage(msg, transfer.Connection, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel);
|
|
}
|
|
|
|
private void UpdateFileTransferIndicator(Client client)
|
|
{
|
|
var transfers = fileSender.ActiveTransfers.FindAll(t => t.Connection == client.Connection);
|
|
|
|
var clientNameBox = GameMain.NetLobbyScreen.PlayerList.FindChild(client.name);
|
|
|
|
var clientInfo = clientNameBox.FindChild("filetransfer");
|
|
if (clientInfo == null)
|
|
{
|
|
clientNameBox.ClearChildren();
|
|
clientInfo = new GUIFrame(new Rectangle(0, 0, 180, 0), Color.Transparent, Alignment.TopRight, null, clientNameBox);
|
|
clientInfo.UserData = "filetransfer";
|
|
}
|
|
else if (transfers.Count == 0)
|
|
{
|
|
clientInfo.Parent.RemoveChild(clientInfo);
|
|
}
|
|
|
|
clientInfo.ClearChildren();
|
|
|
|
var progressBar = new GUIProgressBar(new Rectangle(0, 4, 160, clientInfo.Rect.Height - 8), Color.Green, GUI.Style, 0.0f, Alignment.Left, clientInfo);
|
|
progressBar.IsHorizontal = true;
|
|
progressBar.ProgressGetter = () => { return transfers.Sum(t => t.Progress) / transfers.Count; };
|
|
|
|
var textBlock = new GUITextBlock(new Rectangle(0, 2, 160, 0), "", GUI.Style, Alignment.TopLeft, Alignment.Left | Alignment.CenterY, clientInfo, true, GUI.SmallFont);
|
|
textBlock.TextGetter = () =>
|
|
{ return MathUtils.GetBytesReadable(transfers.Sum(t => t.SentOffset)) + " / " + MathUtils.GetBytesReadable(transfers.Sum(t => t.Data.Length)); };
|
|
|
|
var cancelButton = new GUIButton(new Rectangle(-5, 0, 14, 0), "X", Alignment.Right, GUI.Style, clientInfo);
|
|
cancelButton.OnClicked = (GUIButton button, object userdata) =>
|
|
{
|
|
transfers.ForEach(t => fileSender.CancelTransfer(t));
|
|
return true;
|
|
};
|
|
}
|
|
|
|
public void UpdateVoteStatus()
|
|
{
|
|
if (server.Connections.Count == 0) return;
|
|
if (connectedClients.Count == 0) return;
|
|
|
|
var clientsToKick = connectedClients.FindAll(c => c.KickVoteCount >= connectedClients.Count * KickVoteRequiredRatio);
|
|
foreach (Client c in clientsToKick)
|
|
{
|
|
SendChatMessage(c.name+" has been kicked from the server.", ChatMessageType.Server, null);
|
|
KickClient(c);
|
|
}
|
|
|
|
GameMain.NetLobbyScreen.LastUpdateID++;
|
|
|
|
NetOutgoingMessage msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.UPDATE_LOBBY);
|
|
msg.Write((byte)ServerNetObject.VOTE);
|
|
Voting.ServerWrite(msg);
|
|
msg.Write((byte)ServerNetObject.END_OF_MESSAGE);
|
|
|
|
server.SendMessage(msg, connectedClients.Select(c => c.Connection).ToList(), NetDeliveryMethod.ReliableUnordered, 0);
|
|
|
|
if (Voting.AllowEndVoting && EndVoteMax > 0 &&
|
|
((float)EndVoteCount / (float)EndVoteMax) >= EndVoteRequiredRatio)
|
|
{
|
|
Log("Ending round by votes (" + EndVoteCount + "/" + (EndVoteMax - EndVoteCount) + ")", Color.Cyan);
|
|
EndGame();
|
|
}
|
|
}
|
|
|
|
public void UpdateClientPermissions(Client client)
|
|
{
|
|
clientPermissions.RemoveAll(cp => cp.IP == client.Connection.RemoteEndPoint.Address.ToString());
|
|
|
|
if (client.Permissions != ClientPermissions.None)
|
|
{
|
|
clientPermissions.Add(new SavedClientPermission(
|
|
client.name,
|
|
client.Connection.RemoteEndPoint.Address.ToString(),
|
|
client.Permissions));
|
|
}
|
|
|
|
var msg = server.CreateMessage();
|
|
msg.Write((byte)ServerPacketHeader.PERMISSIONS);
|
|
msg.Write((byte)client.Permissions);
|
|
server.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableUnordered);
|
|
|
|
SaveClientPermissions();
|
|
}
|
|
|
|
public override bool SelectCrewCharacter(GUIComponent component, object obj)
|
|
{
|
|
base.SelectCrewCharacter(component, obj);
|
|
|
|
var characterFrame = component.Parent.Parent.FindChild("selectedcharacter");
|
|
|
|
Character character = obj as Character;
|
|
if (character == null) return false;
|
|
|
|
if (character != myCharacter)
|
|
{
|
|
var banButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Ban", Alignment.BottomRight, GUI.Style, characterFrame);
|
|
banButton.UserData = character.Name;
|
|
banButton.OnClicked += GameMain.NetLobbyScreen.BanPlayer;
|
|
|
|
var rangebanButton = new GUIButton(new Rectangle(0, -25, 100, 20), "Ban range", Alignment.BottomRight, GUI.Style, characterFrame);
|
|
rangebanButton.UserData = character.Name;
|
|
rangebanButton.OnClicked += GameMain.NetLobbyScreen.BanPlayerRange;
|
|
|
|
var kickButton = new GUIButton(new Rectangle(0, 0, 100, 20), "Kick", Alignment.BottomLeft, GUI.Style, characterFrame);
|
|
kickButton.UserData = character.Name;
|
|
kickButton.OnClicked += GameMain.NetLobbyScreen.KickPlayer;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void UpdateCharacterInfo(NetIncomingMessage message, Client sender)
|
|
{
|
|
Gender gender = Gender.Male;
|
|
int headSpriteId = 0;
|
|
try
|
|
{
|
|
gender = message.ReadBoolean() ? Gender.Male : Gender.Female;
|
|
headSpriteId = message.ReadByte();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
gender = Gender.Male;
|
|
headSpriteId = 0;
|
|
|
|
DebugConsole.Log("Received invalid characterinfo from \"" +sender.name+"\"! { "+e.Message+" }");
|
|
}
|
|
|
|
List<JobPrefab> jobPreferences = new List<JobPrefab>();
|
|
int count = message.ReadByte();
|
|
for (int i = 0; i < Math.Min(count, 3); i++)
|
|
{
|
|
string jobName = message.ReadString();
|
|
|
|
JobPrefab jobPrefab = JobPrefab.List.Find(jp => jp.Name == jobName);
|
|
if (jobPrefab != null) jobPreferences.Add(jobPrefab);
|
|
}
|
|
|
|
sender.characterInfo = new CharacterInfo(Character.HumanConfigFile, sender.name, gender);
|
|
sender.characterInfo.HeadSpriteId = headSpriteId;
|
|
sender.jobPreferences = jobPreferences;
|
|
}
|
|
|
|
public void AssignJobs(List<Client> unassigned, bool assignHost)
|
|
{
|
|
unassigned = new List<Client>(unassigned);
|
|
|
|
int[] assignedClientCount = new int[JobPrefab.List.Count];
|
|
|
|
if (assignHost)
|
|
{
|
|
if (characterInfo != null)
|
|
{
|
|
assignedClientCount[JobPrefab.List.FindIndex(jp => jp == GameMain.NetLobbyScreen.JobPreferences[0])] = 1;
|
|
}
|
|
else if (myCharacter != null && !myCharacter.IsDead)
|
|
{
|
|
assignedClientCount[JobPrefab.List.IndexOf(myCharacter.Info.Job.Prefab)] = 1;
|
|
}
|
|
}
|
|
else if (myCharacter != null && !myCharacter.IsDead)
|
|
{
|
|
assignedClientCount[JobPrefab.List.IndexOf(myCharacter.Info.Job.Prefab)]++;
|
|
}
|
|
|
|
//count the clients who already have characters with an assigned job
|
|
foreach (Client c in connectedClients)
|
|
{
|
|
if (unassigned.Contains(c)) continue;
|
|
if (c.Character != null && !c.Character.IsDead)
|
|
{
|
|
assignedClientCount[JobPrefab.List.IndexOf(c.Character.Info.Job.Prefab)]++;
|
|
}
|
|
}
|
|
|
|
//if any of the players has chosen a job that is Always Allowed, give them that job
|
|
for (int i = unassigned.Count - 1; i >= 0; i--)
|
|
{
|
|
if (!unassigned[i].jobPreferences[0].AllowAlways) continue;
|
|
unassigned[i].assignedJob = unassigned[i].jobPreferences[0];
|
|
unassigned.RemoveAt(i);
|
|
}
|
|
|
|
//go throught the jobs whose MinNumber>0 (i.e. at least one crew member has to have the job)
|
|
bool unassignedJobsFound = true;
|
|
while (unassignedJobsFound && unassigned.Count > 0)
|
|
{
|
|
unassignedJobsFound = false;
|
|
for (int i = 0; i < JobPrefab.List.Count; i++)
|
|
{
|
|
if (unassigned.Count == 0) break;
|
|
if (JobPrefab.List[i].MinNumber < 1 || assignedClientCount[i] >= JobPrefab.List[i].MinNumber) continue;
|
|
|
|
//find the client that wants the job the most, or force it to random client if none of them want it
|
|
Client assignedClient = FindClientWithJobPreference(unassigned, JobPrefab.List[i], true);
|
|
|
|
assignedClient.assignedJob = JobPrefab.List[i];
|
|
|
|
assignedClientCount[i]++;
|
|
unassigned.Remove(assignedClient);
|
|
|
|
//the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running
|
|
if (assignedClientCount[i] < JobPrefab.List[i].MinNumber) unassignedJobsFound = true;
|
|
}
|
|
}
|
|
|
|
//find a suitable job for the rest of the players
|
|
for (int i = unassigned.Count - 1; i >= 0; i--)
|
|
{
|
|
for (int preferenceIndex = 0; preferenceIndex < 3; preferenceIndex++)
|
|
{
|
|
int jobIndex = JobPrefab.List.FindIndex(jp => jp == unassigned[i].jobPreferences[preferenceIndex]);
|
|
|
|
//if there's enough crew members assigned to the job already, continue
|
|
if (assignedClientCount[jobIndex] >= JobPrefab.List[jobIndex].MaxNumber) continue;
|
|
|
|
unassigned[i].assignedJob = JobPrefab.List[jobIndex];
|
|
|
|
assignedClientCount[jobIndex]++;
|
|
unassigned.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Client FindClientWithJobPreference(List<Client> clients, JobPrefab job, bool forceAssign = false)
|
|
{
|
|
int bestPreference = 0;
|
|
Client preferredClient = null;
|
|
foreach (Client c in clients)
|
|
{
|
|
int index = c.jobPreferences.IndexOf(job);
|
|
if (index == -1) index = 1000;
|
|
|
|
if (preferredClient == null || index < bestPreference)
|
|
{
|
|
bestPreference = index;
|
|
preferredClient = c;
|
|
}
|
|
}
|
|
|
|
//none of the clients wants the job, assign it to random client
|
|
if (forceAssign && preferredClient == null)
|
|
{
|
|
preferredClient = clients[Rand.Int(clients.Count)];
|
|
}
|
|
|
|
return preferredClient;
|
|
}
|
|
|
|
public static void Log(string line, Color? color)
|
|
{
|
|
if (GameMain.Server == null || !GameMain.Server.SaveServerLogs) return;
|
|
|
|
GameMain.Server.log.WriteLine(line, color);
|
|
}
|
|
|
|
public override void Disconnect()
|
|
{
|
|
banList.Save();
|
|
SaveSettings();
|
|
|
|
if (registeredToMaster && restClient != null)
|
|
{
|
|
var request = new RestRequest("masterserver2.php", Method.GET);
|
|
request.AddParameter("action", "removeserver");
|
|
request.AddParameter("serverport", Port);
|
|
|
|
restClient.Execute(request);
|
|
restClient = null;
|
|
}
|
|
|
|
if (SaveServerLogs)
|
|
{
|
|
Log("Shutting down server...", Color.Cyan);
|
|
log.Save();
|
|
}
|
|
|
|
server.Shutdown("The server has been shut down");
|
|
}
|
|
}
|
|
}
|