Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions
@@ -98,7 +98,7 @@ namespace Barotrauma.Networking
if (c.Character == null || c.Character.SpeechImpediment >= 100.0f || c.Character.IsDead) { return; }
if (orderMsg.Order.IsReport)
{
HumanAIController.ReportProblem(orderMsg.Sender, orderMsg.Order);
HumanAIController.ReportProblem(orderMsg.Sender as Character, orderMsg.Order);
}
if (order != null)
{
@@ -1,4 +1,5 @@
using System;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,6 +9,8 @@ namespace Barotrauma.Networking
{
public bool VoiceEnabled = true;
public VoipServerDecoder VoipServerDecoder;
public UInt16 LastRecvClientListUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.LastClientListUpdateID);
@@ -15,7 +18,7 @@ namespace Barotrauma.Networking
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvServerSettingsUpdate
= NetIdUtils.GetIdOlderThan(GameMain.Server.ServerSettings.LastUpdateIdForFlag[ServerSettings.NetFlags.Properties]);
public UInt16 LastRecvLobbyUpdate
= NetIdUtils.GetIdOlderThan(GameMain.NetLobbyScreen.LastUpdateID);
@@ -129,6 +132,7 @@ namespace Barotrauma.Networking
JobPreferences = new List<JobVariant>();
VoipQueue = new VoipQueue(SessionId, true, true);
VoipServerDecoder = new VoipServerDecoder(VoipQueue, this);
GameMain.Server.VoipServer.RegisterQueue(VoipQueue);
//initialize to infinity, gets set to a proper value when initializing midround syncing
@@ -277,5 +281,47 @@ namespace Barotrauma.Networking
{
return Permissions.HasFlag(permission);
}
public bool TryTakeOverBot(Character botCharacter)
{
if (GameMain.Server == null)
{
DebugConsole.ThrowError($"TryTakeOverBot: Client {Name} requested to take over a bot but GameMain.Server is null!");
return false;
}
if (GameMain.NetworkMember is not { ServerSettings.RespawnMode: RespawnMode.Permadeath })
{
DebugConsole.ThrowError($"Client {Name} requested to take over a bot but Permadeath is not enabled!");
GameMain.Server.SendConsoleMessage($"Permadeath mode is not enabled, cannot take over a bot.", this, Color.Red);
return false;
}
if (CharacterInfo == null)
{
DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but they don't seem to have a character at all yet.");
GameMain.Server.SendConsoleMessage($"Permadeath: Taking over a bot requires having a character that died first.", this, Color.Red);
return false;
}
if (CharacterInfo is not { PermanentlyDead: true })
{
DebugConsole.ThrowError($"Permadeath: Client {Name} requested to take over a bot, but their character has not been permanently killed.");
GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the bot, previous character not permanently killed.", this, Color.Red);
return false;
}
if (!botCharacter.IsBot)
{
DebugConsole.ThrowError($"Permadeath: {Name} requested to take over a bot character, but the target character is not a bot!");
GameMain.Server.SendConsoleMessage($"Permadeath: Could not take over the target character because it is not a bot.", this, Color.Red);
return false;
}
// Now that the old permanently killed character will be replaced, we can fully discard it
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.DiscardClientCharacterData(this);
}
GameMain.Server.SetClientCharacter(this, botCharacter);
SpectateOnly = false;
return true;
}
}
}
@@ -60,7 +60,10 @@ namespace Barotrauma.Networking
private bool wasReadyToStartAutomatically;
private bool autoRestartTimerRunning;
private float endRoundTimer;
public float EndRoundTimer { get; private set; }
public float EndRoundDelay { get; private set; }
public float EndRoundTimeRemaining => EndRoundTimer > 0 ? EndRoundDelay - EndRoundTimer : 0;
/// <summary>
/// Chat messages that get sent to the owner of the server when the owner is determined
@@ -360,6 +363,10 @@ namespace Barotrauma.Networking
if (ServerSettings.VoiceChatEnabled)
{
VoipServer.SendToClients(connectedClients);
foreach (var c in connectedClients)
{
c.VoipServerDecoder.DebugUpdate(deltaTime);
}
}
if (GameStarted)
@@ -367,6 +374,7 @@ namespace Barotrauma.Networking
RespawnManager?.Update(deltaTime);
entityEventManager.Update(connectedClients);
bool permadeathMode = ServerSettings.RespawnMode == RespawnMode.Permadeath;
//go through the characters backwards to give rejoining clients control of the latest created character
for (int i = Character.CharacterList.Count - 1; i >= 0; i--)
@@ -377,7 +385,8 @@ namespace Barotrauma.Networking
Client owner = connectedClients.Find(c => (c.Character == null || c.Character == character) && character.IsClientOwner(c));
bool canOwnerTakeControl =
owner != null && owner.InGame && !owner.NeedsMidRoundSync &&
(!ServerSettings.AllowSpectating || !owner.SpectateOnly);
(!ServerSettings.AllowSpectating || !owner.SpectateOnly ||
(permadeathMode && (!character.IsDead || character.CauseOfDeath?.Type == CauseOfDeathType.Disconnected)));
if (!character.IsDead)
{
if (!GameMain.LuaCs.Game.disableDisconnectCharacter)
@@ -386,7 +395,8 @@ namespace Barotrauma.Networking
character.SetStun(1.0f);
}
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) && character.KillDisconnectedTimer > ServerSettings.KillDisconnectedTime)
if ((OwnerConnection == null || owner?.Connection != OwnerConnection) &&
character.KillDisconnectedTimer > (permadeathMode ? ServerSettings.DespawnDisconnectedPermadeathTime : ServerSettings.KillDisconnectedTime))
{
character.Kill(CauseOfDeathType.Disconnected, null);
continue;
@@ -411,8 +421,10 @@ namespace Barotrauma.Networking
Voting.Update(deltaTime);
bool isCrewDead =
bool isCrewDown =
connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated));
bool isSomeoneIncapacitatedNotDead =
connectedClients.Any(c => !c.UsingFreeCam && c.Character is { IsDead: false, IsIncapacitated: true });
bool subAtLevelEnd = false;
if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
@@ -441,45 +453,58 @@ namespace Barotrauma.Networking
}
}
float endRoundDelay = 1.0f;
if (ServerSettings.AutoRestart && isCrewDead)
EndRoundDelay = 1.0f;
if (permadeathMode && isCrewDown)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
if (EndRoundTimer <= 0.0f)
{
CreateEntityEvent(RespawnManager);
}
EndRoundDelay = 120.0f;
EndRoundTimer += deltaTime;
}
else if (ServerSettings.AutoRestart && isCrewDown)
{
EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 5.0f;
EndRoundTimer += deltaTime;
}
else if (subAtLevelEnd && GameMain.GameSession?.GameMode is not CampaignMode)
{
endRoundDelay = 5.0f;
endRoundTimer += deltaTime;
EndRoundDelay = 5.0f;
EndRoundTimer += deltaTime;
}
else if (isCrewDead && (RespawnManager == null || !RespawnManager.CanRespawnAgain))
else if (isCrewDown && (RespawnManager == null || !RespawnManager.CanRespawnAgain))
{
#if !DEBUG
if (endRoundTimer <= 0.0f)
if (EndRoundTimer <= 0.0f)
{
SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "60").Value, ChatMessageType.Server);
SendChatMessage(TextManager.GetWithVariable("CrewDeadNoRespawns", "[time]", "120").Value, ChatMessageType.Server);
}
endRoundDelay = 60.0f;
endRoundTimer += deltaTime;
EndRoundDelay = 120.0f;
EndRoundTimer += deltaTime;
#endif
}
else if (isCrewDead && (GameMain.GameSession?.GameMode is CampaignMode))
else if (isCrewDown && (GameMain.GameSession?.GameMode is CampaignMode))
{
#if !DEBUG
endRoundDelay = 2.0f;
endRoundTimer += deltaTime;
EndRoundDelay = isSomeoneIncapacitatedNotDead ? 120.0f : 2.0f;
EndRoundTimer += deltaTime;
#endif
}
else
{
endRoundTimer = 0.0f;
EndRoundTimer = 0.0f;
}
if (endRoundTimer >= endRoundDelay)
if (EndRoundTimer >= EndRoundDelay)
{
if (ServerSettings.AutoRestart && isCrewDead)
if (permadeathMode && isCrewDown)
{
Log("Ending round (entire crew dead)", ServerLog.MessageType.ServerMessage);
Log("Ending round (entire crew dead or down and did not acquire new characters in time)", ServerLog.MessageType.ServerMessage);
}
else if (ServerSettings.AutoRestart && isCrewDown)
{
Log("Ending round (entire crew down)", ServerLog.MessageType.ServerMessage);
}
else if (subAtLevelEnd)
{
@@ -487,11 +512,11 @@ namespace Barotrauma.Networking
}
else if (RespawnManager == null)
{
Log("Ending round (no living players left and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage);
Log("Ending round (no players left standing and respawning is not enabled during this round)", ServerLog.MessageType.ServerMessage);
}
else
{
Log("Ending round (no living players left)", ServerLog.MessageType.ServerMessage);
Log("Ending round (no players left standing)", ServerLog.MessageType.ServerMessage);
}
EndGame(wasSaved: false);
return;
@@ -836,7 +861,7 @@ namespace Barotrauma.Networking
#endif
return;
}
connectedClient.VoipQueue.Read(inc);
VoipServer.Read(inc, connectedClient);
}
break;
case ClientPacketHeader.SERVER_SETTINGS:
@@ -854,6 +879,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.REWARD_DISTRIBUTION:
ReadRewardDistributionMessage(inc, connectedClient);
break;
case ClientPacketHeader.RESET_REWARD_DISTRIBUTION:
ResetRewardDistribution(connectedClient);
break;
case ClientPacketHeader.MEDICAL:
ReadMedicalMessage(inc, connectedClient);
break;
@@ -866,6 +894,9 @@ namespace Barotrauma.Networking
case ClientPacketHeader.READY_TO_SPAWN:
ReadReadyToSpawnMessage(inc, connectedClient);
break;
case ClientPacketHeader.TAKEOVERBOT:
ReadTakeOverBotMessage(inc, connectedClient);
break;
case ClientPacketHeader.FILE_REQUEST:
if (ServerSettings.AllowFileTransfers)
{
@@ -1319,6 +1350,14 @@ namespace Barotrauma.Networking
mpCampaign.ServerReadRewardDistribution(inc, sender);
}
}
private void ResetRewardDistribution(Client client)
{
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.ResetSalaries(client);
}
}
private void ReadMedicalMessage(IReadMessage inc, Client sender)
{
@@ -1354,6 +1393,80 @@ namespace Barotrauma.Networking
}
}
private void ReadTakeOverBotMessage(IReadMessage inc, Client sender)
{
UInt16 botId = inc.ReadUInt16();
if (GameMain.GameSession?.GameMode is not MultiPlayerCampaign campaign) { return; }
if (ServerSettings.IronmanMode)
{
DebugConsole.ThrowError($"Client {sender.Name} has requested to take over a bot in Ironman mode!");
return;
}
if (campaign.CurrentLocation.GetHireableCharacters().FirstOrDefault(c => c.ID == botId) is CharacterInfo hireableCharacter)
{
if (campaign.TryHireCharacter(campaign.CurrentLocation, hireableCharacter, takeMoney: true, sender))
{
campaign.CurrentLocation.RemoveHireableCharacter(hireableCharacter);
SpawnAndTakeOverBot(campaign, hireableCharacter, sender);
campaign.SendCrewState(createNotification: false);
}
else
{
SendConsoleMessage($"Could not hire the bot {hireableCharacter.Name}.", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to hire the bot {hireableCharacter.Name}.");
}
}
else
{
CharacterInfo botInfo = GameMain.GameSession.CrewManager?.GetCharacterInfos()?.FirstOrDefault(i => i.ID == botId);
if (botInfo is { IsNewHire: true, Character: null })
{
SpawnAndTakeOverBot(campaign, botInfo, sender);
}
else if (botInfo?.Character == null || !botInfo.Character.IsBot)
{
SendConsoleMessage($"Could not find a bot with the id {botId}.", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (Could not find a bot with the id {botId}).");
return;
}
else if (ServerSettings.AllowBotTakeoverOnPermadeath)
{
sender.TryTakeOverBot(botInfo.Character);
}
else
{
SendConsoleMessage($"Failed to take over a bot (taking control of bots is disallowed).", sender, Color.Red);
DebugConsole.ThrowError($"Client {sender.Name} failed to take over a bot (taking control of bots is disallowed).");
}
}
}
private static void SpawnAndTakeOverBot(CampaignMode campaign, CharacterInfo botInfo, Client client)
{
var mainSubSpawnpoint = WayPoint.SelectCrewSpawnPoints(botInfo.ToEnumerable().ToList(), Submarine.MainSub).FirstOrDefault();
var spawnWaypoint = campaign.CrewManager.GetOutpostSpawnpoints()?.FirstOrDefault() ?? mainSubSpawnpoint;
if (spawnWaypoint == null)
{
DebugConsole.ThrowError("SpawnAndTakeOverBot: Unable to find any spawn waypoints inside the sub");
return;
}
Entity.Spawner.AddCharacterToSpawnQueue(botInfo.SpeciesName, spawnWaypoint.WorldPosition, botInfo, onSpawn: newCharacter =>
{
if (newCharacter == null)
{
DebugConsole.ThrowError("SpawnAndTakeOverBot: newCharacter is null somehow");
return;
}
campaign.CrewManager.RemoveCharacterInfo(botInfo);
newCharacter.TeamID = CharacterTeamType.Team1;
campaign.CrewManager.InitializeCharacter(newCharacter, mainSubSpawnpoint, spawnWaypoint);
client.TryTakeOverBot(newCharacter);
});
}
private void ClientReadServerCommand(IReadMessage inc)
{
Client sender = ConnectedClients.Find(x => x.Connection == inc.Sender);
@@ -1462,9 +1575,8 @@ namespace Barotrauma.Networking
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{
mpCampaign.SavePlayers();
mpCampaign.HandleSaveAndQuit();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
mpCampaign.UpdateStoreStock();
GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else
@@ -1698,6 +1810,8 @@ namespace Barotrauma.Networking
outmsg.WriteBoolean(GameStarted);
outmsg.WriteBoolean(ServerSettings.AllowSpectating);
outmsg.WriteBoolean(ServerSettings.RespawnMode == RespawnMode.Permadeath);
outmsg.WriteBoolean(ServerSettings.IronmanMode);
c.WritePermissions(outmsg);
}
@@ -1788,6 +1902,7 @@ namespace Barotrauma.Networking
IWriteMessage outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteSingle(EndRoundTimeRemaining);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
@@ -1870,7 +1985,8 @@ namespace Barotrauma.Networking
{
outmsg = new WriteOnlyMessage();
outmsg.WriteByte((byte)ServerPacketHeader.UPDATE_INGAME);
outmsg.WriteSingle((float)Lidgren.Network.NetTime.Now);
outmsg.WriteSingle((float)NetTime.Now);
outmsg.WriteSingle(EndRoundTimeRemaining);
using (var segmentTable = SegmentTableWriter<ServerNetSegment>.StartWriting(outmsg))
{
@@ -2341,17 +2457,18 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Failure;
}
bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn);
bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost;
if (ServerSettings.AllowRespawn && missionAllowRespawn)
if (ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn)
{
RespawnManager = new RespawnManager(this, ServerSettings.UseRespawnShuttle && !isOutpost ? selectedShuttle : null);
}
if (campaign != null)
{
campaign.CargoManager.CreatePurchasedItems();
campaign.SendCrewState();
//midround-joining clients need to be informed of pending/new hires at outposts
if (isOutpost) { campaign.SendCrewState(); }
}
Level.Loaded?.SpawnNPCs();
@@ -2392,6 +2509,8 @@ namespace Barotrauma.Networking
}
//always allow the server owner to spectate even if it's disallowed in server settings
teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly);
// Clients with last character permanently dead spectate regardless of server settings
teamClients.RemoveAll(c => c.CharacterInfo != null && c.CharacterInfo.PermanentlyDead);
//if (!teamClients.Any() && n > 0) { continue; }
@@ -2460,6 +2579,7 @@ namespace Barotrauma.Networking
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull?.OutpostModuleTags != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -2507,13 +2627,17 @@ namespace Barotrauma.Networking
characterData.ApplyWalletData(spawnedCharacter);
spawnedCharacter.GiveIdCardTags(mainSubWaypoints[i]);
spawnedCharacter.LoadTalents();
characterData.HasSpawned = true;
}
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign && spawnedCharacter.Info != null)
{
spawnedCharacter.Info.SetExperience(Math.Max(spawnedCharacter.Info.ExperiencePoints, mpCampaign.GetSavedExperiencePoints(teamClients[i])));
mpCampaign.ClearSavedExperiencePoints(teamClients[i]);
if (spawnedCharacter.Info.LastRewardDistribution.TryUnwrap(out int salary))
{
spawnedCharacter.Wallet.SetRewardDistribution(salary);
}
}
spawnedCharacter.SetOwnerClient(teamClients[i]);
@@ -2614,11 +2738,12 @@ namespace Barotrauma.Networking
msg.WriteInt32(seed);
msg.WriteIdentifier(gameSession.GameMode.Preset.Identifier);
bool missionAllowRespawn = GameMain.GameSession.GameMode is not MissionMode missionMode || !missionMode.Missions.Any(m => !m.AllowRespawn);
msg.WriteBoolean(ServerSettings.AllowRespawn && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.RespawnMode != RespawnMode.BetweenRounds && missionAllowRespawn);
msg.WriteBoolean(ServerSettings.AllowDisguises);
msg.WriteBoolean(ServerSettings.AllowRewiring);
msg.WriteBoolean(ServerSettings.AllowImmediateItemDelivery);
msg.WriteBoolean(ServerSettings.AllowFriendlyFire);
msg.WriteBoolean(ServerSettings.AllowDragAndDropGive);
msg.WriteBoolean(ServerSettings.LockAllDefaultWires);
msg.WriteBoolean(ServerSettings.AllowLinkingWifiToChat);
msg.WriteInt32(ServerSettings.MaximumMoneyTransferRequest);
@@ -2726,7 +2851,7 @@ namespace Barotrauma.Networking
GameMain.GameSession.CrewManager?.ServerWriteActiveOrders(msg);
}
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false)
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false, IEnumerable<Mission> missions = null)
{
if (GameStarted)
{
@@ -2742,7 +2867,7 @@ namespace Barotrauma.Networking
}
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
List<Mission> missions = GameMain.GameSession.Missions.ToList();
missions ??= GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession is { IsRunning: true })
{
GameMain.GameSession.EndRound(endMessage);
@@ -2758,7 +2883,7 @@ namespace Barotrauma.Networking
}
}
endRoundTimer = 0.0f;
EndRoundTimer = 0.0f;
if (ServerSettings.AutoRestart)
{
@@ -2794,7 +2919,7 @@ namespace Barotrauma.Networking
msg.WriteByte((byte)transitionType);
msg.WriteBoolean(wasSaved);
msg.WriteString(endMessage);
msg.WriteByte((byte)missions.Count);
msg.WriteByte((byte)missions.Count());
foreach (Mission mission in missions)
{
msg.WriteBoolean(mission.Completed);
@@ -2838,6 +2963,12 @@ namespace Barotrauma.Networking
{
logMsg = message.TextWithSender;
}
if (message.Sender is Character sender)
{
sender.TextChatVolume = 1f;
}
Log(logMsg, ServerLog.MessageType.Chat);
}
@@ -3377,24 +3508,24 @@ namespace Barotrauma.Networking
public void SendOrderChatMessage(OrderChatMessage message)
{
if (message.Sender == null || message.Sender.SpeechImpediment >= 100.0f) { return; }
if (message.SenderCharacter == null || message.SenderCharacter.SpeechImpediment >= 100.0f) { return; }
//check which clients can receive the message and apply distance effects
foreach (Client client in ConnectedClients)
{
if (message.Sender != null && client.Character != null && !client.Character.IsDead)
if (message.SenderCharacter != null && client.Character != null && !client.Character.IsDead)
{
//too far to hear the msg -> don't send
if (!client.Character.CanHearCharacter(message.Sender)) { continue; }
if (!client.Character.CanHearCharacter(message.SenderCharacter)) { continue; }
}
SendDirectChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder), client);
}
if (!string.IsNullOrWhiteSpace(message.Text))
{
AddChatMessage(new OrderChatMessage(message.Order, message.Text, message.TargetCharacter, message.Sender, isNewOrder: message.IsNewOrder));
if (ChatMessage.CanUseRadio(message.Sender, out var senderRadio))
if (ChatMessage.CanUseRadio(message.SenderCharacter, out var senderRadio))
{
//send to chat-linked wifi components
Signal s = new Signal(message.Text, sender: message.Sender, source: senderRadio.Item);
Signal s = new Signal(message.Text, sender: message.SenderCharacter, source: senderRadio.Item);
senderRadio.TransmitSignal(s, sentFromChat: true);
}
}
@@ -3738,6 +3869,14 @@ namespace Barotrauma.Networking
}
}
// If a CharacterInfo for this Client already exists on the server, make sure it is used, and prevent the Client from replacing it
var existingCampaignData = (GameMain.GameSession?.Campaign as MultiPlayerCampaign)?.GetClientCharacterData(sender);
if (existingCampaignData != null)
{
sender.CharacterInfo = existingCampaignData.CharacterInfo;
return;
}
sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
sender.CharacterInfo.RecreateHead(
@@ -3900,7 +4039,7 @@ namespace Barotrauma.Networking
foreach (Client c in unassigned)
{
//find all jobs that are still available
var remainingJobs = jobList.FindAll(jp => assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma);
var remainingJobs = jobList.FindAll(jp => !jp.HiddenJob && assignedClientCount[jp] < jp.MaxNumber && c.Karma >= jp.MinKarma);
//all jobs taken, give a random job
if (remainingJobs.Count == 0)
@@ -3945,9 +4084,15 @@ namespace Barotrauma.Networking
public void AssignBotJobs(List<CharacterInfo> bots, CharacterTeamType teamID)
{
//shuffle first so the parts where we go through the prefabs
//and find ones there's too few of don't always pick the same job
List<JobPrefab> shuffledPrefabs = JobPrefab.Prefabs.Where(static jp => !jp.HiddenJob).ToList();
shuffledPrefabs.Shuffle();
Dictionary<JobPrefab, int> assignedPlayerCount = new Dictionary<JobPrefab, int>();
foreach (JobPrefab jp in JobPrefab.Prefabs)
foreach (JobPrefab jp in shuffledPrefabs)
{
if (jp.HiddenJob) { continue; }
assignedPlayerCount.Add(jp, 0);
}
@@ -3966,53 +4111,55 @@ namespace Barotrauma.Networking
}
List<CharacterInfo> unassignedBots = new List<CharacterInfo>(bots);
List<WayPoint> spawnPoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine != null && wp.Submarine.TeamID == teamID)
.OrderBy(sp => Rand.Int(int.MaxValue))
.OrderBy(sp => sp.AssignedJob == null ? 0 : 1)
.ToList();
bool canAssign = false;
do
while (unassignedBots.Count > 0)
{
canAssign = false;
foreach (WayPoint spawnPoint in spawnPoints)
//if there's any jobs left that must be included in the crew, assign those
var jobsBelowMinNumber = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MinNumber);
if (jobsBelowMinNumber.Any())
{
if (unassignedBots.Count == 0) { break; }
JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.Prefabs.GetRandomUnsynced();
if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; }
var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.ServerAndClient);
unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.ServerAndClient, variant);
assignedPlayerCount[jobPrefab]++;
unassignedBots.Remove(unassignedBots[0]);
canAssign = true;
AssignJob(unassignedBots[0], jobsBelowMinNumber.GetRandomUnsynced());
}
} while (unassignedBots.Count > 0 && canAssign);
else
{
//if there's any jobs left that are below the normal number of bots initially in the crew, assign those
var jobsBelowInitialCount = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.InitialCount);
if (jobsBelowInitialCount.Any())
{
AssignJob(unassignedBots[0], jobsBelowInitialCount.GetRandomUnsynced());
}
else
{
//no "must-have-jobs" left, break and start assigning randomly
break;
}
}
}
//find a suitable job for the rest of the bots
foreach (CharacterInfo c in unassignedBots)
foreach (CharacterInfo c in unassignedBots.ToList())
{
//find all jobs that are still available
var remainingJobs = JobPrefab.Prefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber);
var remainingJobs = shuffledPrefabs.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber);
//all jobs taken, give a random job
if (remainingJobs.None())
{
DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job...");
c.Job = Job.Random(Rand.RandSync.ServerAndClient);
assignedPlayerCount[c.Job.Prefab]++;
AssignJob(c, shuffledPrefabs.GetRandomUnsynced());
}
else //some jobs still left, choose one of them by random
{
var job = remainingJobs.GetRandomUnsynced();
var variant = Rand.Range(0, job.Variants);
c.Job = new Job(job, Rand.RandSync.Unsynced, variant);
assignedPlayerCount[c.Job.Prefab]++;
else
{
//some jobs still left, choose one of them by random (preferring ones there's the least of in the crew)
var selectedJob = remainingJobs.GetRandomByWeight(jp => 1.0f / Math.Max(assignedPlayerCount[jp], 0.01f), Rand.RandSync.Unsynced);
AssignJob(c, selectedJob);
}
}
void AssignJob(CharacterInfo bot, JobPrefab job)
{
int variant = Rand.Range(0, job.Variants);
bot.Job = new Job(job, Rand.RandSync.Unsynced, variant);
assignedPlayerCount[bot.Job.Prefab]++;
unassignedBots.Remove(bot);
}
}
private Client FindClientWithJobPreference(List<Client> clients, JobPrefab job, bool forceAssign = false)
@@ -468,15 +468,35 @@ namespace Barotrauma.Networking
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
}
if (authenticators is null &&
GameMain.Server.ServerSettings.RequireAuthentication)
{
DebugConsole.NewMessage(
"The server is configured to require authentication from clients, but there are no authenticators available. " +
$"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.",
Microsoft.Xna.Framework.Color.Yellow);
}
if (authenticators is null
|| !packet.AuthTicket.TryUnwrap(out var authTicket)
|| !authenticators.TryGetValue(authTicket.Kind, out var authenticator))
{
{
#if DEBUG
DebugConsole.NewMessage($"Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow);
acceptClient(new AccountInfo(packet.AccountId));
DebugConsole.NewMessage("Debug server accepts unauthenticated connections", Microsoft.Xna.Framework.Color.Yellow);
acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name)));
#else
rejectClient();
if (GameMain.Server.ServerSettings.RequireAuthentication)
{
DebugConsole.NewMessage(
"A client attempted to join without an authentication ticket, but the server is configured to require authentication. " +
$"If you're for example trying to host a server in a local network without being connected to Steam or Epic Online Services, please set {nameof(GameMain.Server.ServerSettings.RequireAuthentication)} to false in the server settings.",
Microsoft.Xna.Framework.Color.Yellow);
rejectClient();
}
else
{
acceptClient(new AccountInfo(new UnauthenticatedAccountId(packet.Name)));
}
#endif
return;
}
@@ -15,6 +15,8 @@ namespace Barotrauma.Networking
private int pendingRespawnCount, requiredRespawnCount;
private int prevPendingRespawnCount, prevRequiredRespawnCount;
public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y;
private IEnumerable<Client> GetClientsToRespawn()
{
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
@@ -27,18 +29,27 @@ namespace Barotrauma.Networking
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; }
if (c.Character != null && !c.Character.IsDead) { continue; }
//don't allow respawn if the client already has a character (they'll regain control once they're in sync)
var matchingData = campaign?.GetClientCharacterData(c);
//don't allow respawn if the client already has a character (they'll regain control once they're in sync)
if (matchingData != null && matchingData.HasSpawned &&
Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead))
{
continue;
}
if (UseRespawnPrompt)
// Respawning might also be needed in permadeath mode for disconnected characters, but never for permanently dead ones
if (GameMain.NetworkMember?.ServerSettings is { RespawnMode: RespawnMode.Permadeath } &&
(matchingData?.CharacterInfo is { PermanentlyDead: true } || c.Character is { IsDead: true }))
{
continue;
}
if (campaign != null)
{
if (matchingData != null && matchingData.HasSpawned)
{
//in the campaign mode, wait for the client to choose whether they want to spawn
if (!c.WaitForNextRoundRespawn.HasValue || c.WaitForNextRoundRespawn.Value) { continue; }
}
}
@@ -47,9 +58,9 @@ namespace Barotrauma.Networking
}
}
private static bool IsRespawnPromptPendingForClient(Client c)
private static bool IsRespawnDecisionPendingForClient(Client c)
{
if (!UseRespawnPrompt || !(GameMain.GameSession.GameMode is MultiPlayerCampaign campaign)) { return false; }
if (Level.Loaded == null || GameMain.GameSession.GameMode is not MultiPlayerCampaign campaign) { return false; }
if (!c.InGame) { return false; }
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { return false; }
@@ -58,7 +69,9 @@ namespace Barotrauma.Networking
var matchingData = campaign.GetClientCharacterData(c);
if (matchingData != null && matchingData.HasSpawned)
{
if (Character.CharacterList.Any(c => c.Info == matchingData.CharacterInfo && !c.IsDead))
if (Character.CharacterList.Any(c =>
c.Info == matchingData.CharacterInfo &&
(!c.IsDead || c.CauseOfDeath is { Type: CauseOfDeathType.Disconnected })))
{
return false;
}
@@ -197,26 +210,30 @@ namespace Barotrauma.Networking
shuttleSteering.TargetVelocity = Vector2.Zero;
}
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
Vector2 spawnPos = FindSpawnPos();
if (!GameMain.LuaCs.Game.overrideRespawnSub)
{
RespawnCharacters(spawnPos);
RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle);
}
CoroutineManager.StopCoroutines("forcepos");
if (spawnPos.Y > Level.Loaded.Size.Y)
if (anyCharacterSpawnedInShuttle)
{
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
GameServer.Log("Dispatching the respawn shuttle.", ServerLog.MessageType.Spawning);
CoroutineManager.StopCoroutines("forcepos");
if (spawnPos.Y > Level.Loaded.Size.Y)
{
CoroutineManager.StartCoroutine(ForceShuttleToPos(Level.Loaded.StartPosition - Vector2.UnitY * Level.ShaftHeight, 100.0f), "forcepos");
}
else
{
RespawnShuttle.SetPosition(spawnPos);
RespawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.NeutralizeBallast();
RespawnShuttle.EnableMaintainPosition();
}
}
else
{
RespawnShuttle.SetPosition(spawnPos);
RespawnShuttle.Velocity = Vector2.Zero;
RespawnShuttle.NeutralizeBallast();
RespawnShuttle.EnableMaintainPosition();
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
}
}
else
@@ -225,7 +242,7 @@ namespace Barotrauma.Networking
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCharacters(null);
RespawnCharacters(shuttlePos: null, out _);
}
}
@@ -265,7 +282,7 @@ namespace Barotrauma.Networking
}
}
if (RespawnShuttle.WorldPosition.Y > Level.Loaded.Size.Y || DateTime.Now > despawnTime)
if (!IsShuttleInsideLevel || DateTime.Now > despawnTime)
{
CoroutineManager.StopCoroutines("forcepos");
@@ -310,7 +327,10 @@ namespace Barotrauma.Networking
if (DateTime.Now > ReturnTime)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
if (IsShuttleInsideLevel)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
}
CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this);
@@ -332,8 +352,8 @@ namespace Barotrauma.Networking
}
return shuttleEmptyTimer > 1.0f;
}
partial void RespawnCharactersProjSpecific(Vector2? shuttlePos)
private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle)
{
respawnedCharacters.Clear();
@@ -358,7 +378,7 @@ namespace Barotrauma.Networking
//all characters are in Team 1 in game modes/missions with only one team.
//if at some point we add a game mode with multiple teams where respawning is possible, this needs to be reworked
c.TeamID = CharacterTeamType.Team1;
if (c.CharacterInfo == null) { c.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name); }
c.CharacterInfo ??= new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name);
}
List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
@@ -399,6 +419,8 @@ namespace Barotrauma.Networking
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
anyCharacterSpawnedInShuttle = false;
for (int i = 0; i < characterInfos.Count; i++)
{
bool bot = i >= clients.Count;
@@ -433,10 +455,19 @@ namespace Barotrauma.Networking
}
}
if (!forceSpawnInMainSub)
{
anyCharacterSpawnedInShuttle = true;
}
var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot);
characterCampaignData?.ApplyWalletData(character);
character.TeamID = CharacterTeamType.Team1;
character.LoadTalents();
if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary))
{
character.Wallet.SetRewardDistribution(salary);
}
respawnedCharacters.Add(character);
@@ -467,7 +498,7 @@ namespace Barotrauma.Networking
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning);
}
if (RespawnShuttle != null)
if (RespawnShuttle != null && anyCharacterSpawnedInShuttle)
{
List<Item> newRespawnItems = new List<Item>();
Vector2 pos = cargoSp?.Position ?? character.Position;
@@ -588,9 +619,7 @@ namespace Barotrauma.Networking
foreach (Skill skill in characterInfo.Job.GetSkills())
{
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier);
if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; }
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, skillLossPercentage / 100.0f);
skill.Level = GetReducedSkill(characterInfo, skill, skillLossPercentage);
}
}
@@ -606,14 +635,10 @@ namespace Barotrauma.Networking
msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds);
break;
case State.Waiting:
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
var matchingData = campaign?.GetClientCharacterData(c);
bool forceSpawnInMainSub = matchingData != null && !matchingData.HasSpawned;
msg.WriteUInt16((ushort)pendingRespawnCount);
msg.WriteUInt16((ushort)requiredRespawnCount);
msg.WriteBoolean(IsRespawnPromptPendingForClient(c));
msg.WriteBoolean(IsRespawnDecisionPendingForClient(c));
msg.WriteBoolean(RespawnCountdownStarted);
msg.WriteBoolean(forceSpawnInMainSub);
msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds);
break;
case State.Returning:
@@ -132,5 +132,21 @@ namespace Barotrauma.Networking
return garbleAmount < range;
}
}
public static void Read(IReadMessage inc, Client connectedClient)
{
var queue = connectedClient.VoipQueue;
if (queue.Read(inc, discardData: false))
{
connectedClient.VoipServerDecoder.OnNewVoiceReceived();
}
#if DEBUG
var msg = new WriteOnlyMessage().WithHeader(ServerPacketHeader.VOICE_AMPLITUDE_DEBUG);
msg.WriteRangedSingle(connectedClient.VoipServerDecoder.Amplitude, min: 0, max: 1, bitCount: 8);
GameMain.Server?.ServerPeer?.Send(msg, connectedClient.Connection, DeliveryMethod.Unreliable);
#endif
}
}
}
@@ -0,0 +1,230 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.IO;
using System.Text;
using Barotrauma.Networking;
using Concentus.Structs;
namespace Barotrauma
{
internal sealed class VoipServerDecoder
{
private readonly OpusDecoder decoder;
private readonly VoipQueue queue;
private int lastRetrievedBufferID;
public float Amplitude { get; private set; }
private readonly Client ownerClient;
public VoipServerDecoder(VoipQueue q, Client owner)
{
ownerClient = owner;
decoder = VoipConfig.CreateDecoder();
queue = q;
lastRetrievedBufferID = q.LatestBufferID;
}
private static bool debugVoip;
/// <summary>
/// When set to true the server will write VOIP into an audio file for debugging purposes.
/// Useful if you're modifying this part of the code and want to be able to hear what the server "hears"
/// </summary>
public static bool DebugVoip
{
get => debugVoip;
set
{
#if !DEBUG
debugVoip = false;
if (value)
{
DebugConsole.ThrowError("DebugVoip is only available in debug builds of the game");
}
#else
debugVoip = value;
if (!value)
{
if (GameMain.Server is null) { return; }
foreach (var c in GameMain.Server.ConnectedClients)
{
c.VoipServerDecoder.ClearStoredDebugSamples();
}
}
#endif
}
}
private readonly List<short[]> debugStoredSamples = new();
private float debugWriteTimerBacking;
private float DebugWriteTimer
{
get => debugWriteTimerBacking;
set => debugWriteTimerBacking = Math.Clamp(value, min: 0, max: DebugWriteTimeout);
}
private bool shouldWriteDebugFile;
private const float DebugWriteTimeout = 3f; // 3 seconds of no data before writing to file
public void OnNewVoiceReceived()
{
float amplitude = 0.0f;
for (int i = lastRetrievedBufferID + 1; i <= queue.LatestBufferID; i++)
{
queue.RetrieveBuffer(i, out int compressedSize, out byte[] compressedBuffer);
if (compressedSize <= 0) { continue; }
short[] buffer = new short[VoipConfig.BUFFER_SIZE];
decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE);
amplitude = Math.Max(amplitude, GetAmplitude(buffer));
lastRetrievedBufferID = i;
if (!DebugVoip) { continue; }
lock (debugStoredSamples) { debugStoredSamples.Add(buffer); }
}
Amplitude = amplitude;
if (DebugVoip)
{
DebugWriteTimer = DebugWriteTimeout;
}
}
public void DebugUpdate(float deltaTime)
{
if (!DebugVoip) { return; }
if (DebugWriteTimer > 0)
{
DebugWriteTimer -= deltaTime;
if (DebugWriteTimer <= 0)
{
shouldWriteDebugFile = true;
}
return;
}
if (!shouldWriteDebugFile) { return; }
lock (debugStoredSamples)
{
#if DEBUG
WriteSamplesToWaveFile(debugStoredSamples,
filename: $"voip_{ownerClient.Name}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.wav",
sampleRate: VoipConfig.FREQUENCY,
channels: 1);
#endif
debugStoredSamples.Clear();
shouldWriteDebugFile = false;
}
}
private static float GetAmplitude(short[] values)
{
float max = 0;
foreach (short v in values)
{
max = Math.Max(max, ToolBox.ShortAudioSampleToFloat(v));
}
return max;
}
/// <summary>
/// Writes the given audio samples to a wave file.
/// </summary>
/// <param name="samples">The audio samples to write.</param>
/// <param name="filename">The name of the wave file to create.</param>
/// <param name="sampleRate">The sample rate of the audio.</param>
/// <param name="channels">The number of channels in the audio.</param>
private static void WriteSamplesToWaveFile(IReadOnlyList<short[]> samples, string filename, int sampleRate, short channels)
{
if (!samples.Any()) { return; }
var path = Path.Combine(Path.GetFullPath("AudioDebug"));
if (!Directory.Exists(path))
{
var dir = Directory.CreateDirectory(path);
if (dir is not { Exists: true }) { return; }
}
using var outFile = File.Create(Path.Combine(path, ToolBox.RemoveInvalidFileNameChars(filename)));
if (outFile is null)
{
DebugConsole.ThrowError("Failed to create audio debug file");
return;
}
// wave file format: https://docs.fileformat.com/audio/wav/
using var writer = new System.IO.BinaryWriter(outFile);
const short pcmFormat = 1; // PCM
const short bitsPerSample = 16; // 16 bits in a short
int byteRate = sampleRate * bitsPerSample * channels / 8;
short blockAlign = (short)(bitsPerSample * channels / 8);
// === FILE INFO === //
writer.Write(Encoding.ASCII.GetBytes("RIFF"));
long sizePos = outFile.Position;
writer.Write(0); // size of file, will be written later
writer.Write(Encoding.ASCII.GetBytes("WAVE"));
writer.Write(Encoding.ASCII.GetBytes("fmt ")); // trailing space is required, not a typo
writer.Write(16); // length of format header
// === AUDIO FORMAT === //
writer.Write(pcmFormat);
writer.Write(channels);
writer.Write(sampleRate);
writer.Write(byteRate);
writer.Write(blockAlign);
writer.Write(bitsPerSample);
// === SAMPLE DATA === //
writer.Write(Encoding.ASCII.GetBytes("data"));
writer.Flush();
long dataPos = outFile.Position;
writer.Write(0); // temporary data size
foreach (var sample in samples)
{
foreach (var s in sample)
{
writer.Write(s);
}
}
writer.Flush();
// write the file size
writer.Seek((int)sizePos, System.IO.SeekOrigin.Begin);
writer.Write((int)(outFile.Length - 8)); // spec says to subtract 8 bytes from the file size
// write the data size
writer.Seek((int)dataPos, System.IO.SeekOrigin.Begin);
writer.Write((int)(outFile.Length - dataPos)); // size of the data only
writer.Flush();
}
private void ClearStoredDebugSamples()
{
lock (debugStoredSamples)
{
debugStoredSamples.Clear();
}
DebugWriteTimer = 0;
shouldWriteDebugFile = false;
}
}
}