v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -40,7 +40,7 @@ namespace Barotrauma
{
matchingData.ApplyPermadeath();
if (GameMain.Server is { ServerSettings.IronmanMode: true })
if (GameMain.Server?.ServerSettings is { IronmanModeActive: true })
{
mpCampaign.SaveSingleCharacter(matchingData);
}
@@ -21,16 +21,16 @@ namespace Barotrauma
CauseOfDeath = null;
}
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel)
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel, bool forceNotification)
{
if (Character == null || Character.Removed) { return; }
if (!prevSentSkill.ContainsKey(skillIdentifier))
{
prevSentSkill[skillIdentifier] = prevLevel;
}
if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.01f)
if (Math.Abs(prevSentSkill[skillIdentifier] - newLevel) > 0.1f || forceNotification)
{
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData());
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.UpdateSkillsEventData(skillIdentifier, forceNotification));
prevSentSkill[skillIdentifier] = newLevel;
}
}
@@ -98,6 +98,8 @@ namespace Barotrauma
msg.WriteInt32(ExperiencePoints);
msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints);
msg.WriteBoolean(PermanentlyDead);
msg.WriteInt32(TalentRefundPoints);
msg.WriteInt32(TalentResetCount);
}
}
}
@@ -298,17 +298,12 @@ namespace Barotrauma
Kill(causeOfDeath.type, causeOfDeath.affliction);
}
break;
case EventType.ConfirmTalentRefund:
if (!CanManageTalents(c)) { return; }
Info?.RefundTalents();
break;
case EventType.UpdateTalents:
if (c.Character != this)
{
if (!IsBot || !c.HasPermission(ClientPermissions.ManageBotTalents))
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
}
if (!CanManageTalents(c)) { return; }
// get the full list of talents from the player, only give the ones
// that are not already given (or otherwise not viable)
@@ -332,6 +327,22 @@ namespace Barotrauma
}
break;
}
bool CanManageTalents(Client client)
{
if (client.Character != this)
{
if (client.TeamID != TeamID || !IsBot || !client.HasPermission(ClientPermissions.ManageBotTalents) || client.Spectating)
{
#if DEBUG
DebugConsole.Log("A client tried to manage talents of a character they don't control or have permission to manage");
#endif
return false;
}
}
return true;
}
}
public void ServerWritePosition(ReadWriteMessage tempBuffer, Client c)
@@ -377,8 +388,15 @@ namespace Barotrauma
tempBuffer.WriteBoolean(shoot);
tempBuffer.WriteBoolean(use);
tempBuffer.WriteBoolean(AnimController is HumanoidAnimController { Crouching: true });
if (AnimController is HumanoidAnimController humanAnim)
{
tempBuffer.WriteBoolean(humanAnim.Crouching);
}
else if (AnimController is FishAnimController fishAnim)
{
tempBuffer.WriteBoolean(fishAnim.Reverse);
}
tempBuffer.WriteBoolean(attack);
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
@@ -464,20 +482,17 @@ namespace Barotrauma
case CharacterStatusEventData statusEventData:
WriteStatus(msg, statusEventData.ForceAfflictionData);
break;
case UpdateSkillsEventData _:
if (Info?.Job == null)
case UpdateSkillsEventData updateSkillsData:
if (Info?.Job is { } job)
{
msg.WriteByte((byte)0);
msg.WriteIdentifier(updateSkillsData.SkillIdentifier);
msg.WriteBoolean(updateSkillsData.ForceNotification);
//don't use Character.GetSkillLevel here, because it applies all the temporary boosts from items and afflictions on the skill level
msg.WriteSingle(job.GetSkillLevel(updateSkillsData.SkillIdentifier));
}
else
{
var skills = Info.Job.GetSkills();
msg.WriteByte((byte)skills.Count());
foreach (Skill skill in skills)
{
msg.WriteIdentifier(skill.Identifier);
msg.WriteSingle(skill.Level);
}
msg.WriteIdentifier(Identifier.Empty);
}
break;
case IAttackEventData attackEventData:
@@ -555,6 +570,7 @@ namespace Barotrauma
break;
case UpdateExperienceEventData _:
msg.WriteInt32(Info.ExperiencePoints);
msg.WriteInt32(info.AdditionalTalentPoints);
break;
case UpdateTalentsEventData _:
msg.WriteUInt16((ushort)characterTalents.Count);
@@ -565,9 +581,16 @@ namespace Barotrauma
}
break;
case UpdateMoneyEventData _:
msg.WriteInt32(GameMain.GameSession.Campaign.GetWallet(c).Balance);
msg.WriteInt32(Wallet?.Balance ?? 0);
break;
case UpdateRefundPointsEventData when Info is { } i:
msg.WriteInt32(i.TalentRefundPoints);
break;
case ConfirmRefundEventData:
// No data
break;
case UpdatePermanentStatsEventData updatePermanentStatsEventData:
StatTypes statType = updatePermanentStatsEventData.StatType;
if (Info == null)
{
@@ -716,9 +716,7 @@ namespace Barotrauma
ShowQuestionPrompt("Console command permissions to revoke from \"" + client.Name + "\"? You may enter multiple commands separated with a space.", (commandsStr) =>
{
Identifier[] splitCommands = commandsStr.Split(' ')
.Select(s => s.Trim())
.ToIdentifiers().ToArray();
Identifier[] splitCommands = commandsStr.ToIdentifiers(separator: " ").ToArray();
List<Command> revokedCommands = new List<Command>();
bool revokeAll = splitCommands.Length > 0 && splitCommands[0] == "all";
if (revokeAll)
@@ -1351,14 +1349,14 @@ namespace Barotrauma
commands.Add(new Command("mission", "mission [name]: Select the mission type for the next round.", (string[] args) =>
{
GameMain.NetLobbyScreen.MissionTypeName = string.Join(" ", args);
NewMessage("Set mission to " + GameMain.NetLobbyScreen.MissionTypeName, Color.Cyan);
GameMain.NetLobbyScreen.MissionTypes = args.ToIdentifiers();
NewMessage("Set mission to " + string.Join(",", GameMain.NetLobbyScreen.MissionTypes), Color.Cyan);
},
() =>
{
return new string[][]
{
Enum.GetNames(typeof(MissionType))
MissionPrefab.GetAllMultiplayerSelectableMissionTypes().Select(id => id.Value).ToArray()
};
}));
@@ -1404,10 +1402,7 @@ namespace Barotrauma
AssignOnExecute("respawnnow", (string[] args) =>
{
if (GameMain.Server?.RespawnManager == null) { return; }
if (GameMain.Server.RespawnManager.CurrentState != RespawnManager.State.Transporting)
{
GameMain.Server.RespawnManager.ForceRespawn();
}
GameMain.Server.RespawnManager.ForceRespawn();
});
commands.Add(new Command("startgame|startround|start", "start/startgame/startround: Start a new round.", (string[] args) =>
@@ -1416,7 +1411,7 @@ namespace Barotrauma
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign &&
GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign)
{
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath, client: null);
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.DataPath, client: null);
}
else
{
@@ -1425,7 +1420,9 @@ namespace Barotrauma
MultiPlayerCampaign.StartCampaignSetup();
return;
}
if (!GameMain.Server.TryStartGame()) { NewMessage("Failed to start a new round", Color.Yellow); }
var result = GameMain.Server.TryStartGame();
if (result != GameServer.TryStartGameResult.Success) { NewMessage($"Failed to start a new round: {TextManager.Get($"TryStartGameError.{result}")}", Color.Yellow); }
}
}));
@@ -1897,23 +1894,28 @@ namespace Barotrauma
}
}
);
AssignOnClientRequestExecute(
"healme",
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
bool healAll = args.Length > 0 && args[0].Equals("all", StringComparison.OrdinalIgnoreCase);
if (client.Character != null)
{
HealCharacter(client.Character, healAll);
}
}
);
AssignOnClientRequestExecute(
"heal",
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase);
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
Character healedCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
if (healedCharacter != null)
{
healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f);
healedCharacter.Oxygen = 100.0f;
healedCharacter.Bloodloss = 0.0f;
healedCharacter.SetStun(0.0f, true);
if (healAll)
{
healedCharacter.CharacterHealth.RemoveAllAfflictions();
}
HealCharacter(healedCharacter, healAll);
}
}
);
@@ -1934,7 +1936,7 @@ namespace Barotrauma
// If killed in ironman mode, the character has been wiped from the save mid-round, so its
// original data needs to be restored to the save file (without making a backup of the dead character)
if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
if (GameMain.Server.ServerSettings is { IronmanModeActive: true } && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
{
@@ -2546,6 +2548,7 @@ namespace Barotrauma
{
foreach (Skill skill in character.Info.Job.GetSkills())
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData(skill.Identifier, forceNotification: true));
character.Info.SetSkillLevel(skill.Identifier, level);
}
GameMain.Server.SendConsoleMessage($"Set all {character.Name}'s skills to {level}", senderClient);
@@ -2553,10 +2556,10 @@ namespace Barotrauma
else
{
character.Info.SetSkillLevel(skillIdentifier, level);
GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData(skillIdentifier, forceNotification: true));
GameMain.Server.SendConsoleMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", senderClient);
}
GameMain.NetworkMember.CreateEntityEvent(character, new Character.UpdateSkillsEventData());
}
else
{
@@ -1,20 +1,51 @@
using System.Collections.Generic;
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
partial class CombatMission
{
class KillCount
{
public readonly Character Victim;
public readonly Client? VictimClient;
public readonly Character? Killer;
public readonly Client? KillerClient;
public KillCount(Character victim, Character? killer)
{
Victim = victim;
VictimClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => victim.IsClientOwner(c));
Killer = killer;
if (killer != null)
{
KillerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => killer.IsClientOwner(c));
}
}
}
const float RoundEndDuration = 5.0f;
private readonly bool[] teamDead = new bool[2];
/// <summary>
/// Lists of characters currently alive in the teams
/// </summary>
private List<Character>[] crews;
/// <summary>
/// List of all kills (of the characters in either team) during the round
/// </summary>
private readonly List<KillCount> kills = new List<KillCount>();
private bool initialized = false;
private float roundEndTimer;
private float timeInTargetSubmarineTimer;
public override LocalizedString Description
{
get
@@ -28,51 +59,16 @@ namespace Barotrauma
protected override void UpdateMissionSpecific(float deltaTime)
{
if (!initialized)
{
crews[0].Clear();
crews[1].Clear();
foreach (Character character in Character.CharacterList)
{
if (character.TeamID == CharacterTeamType.Team1)
{
crews[0].Add(character);
}
else if (character.TeamID == CharacterTeamType.Team2)
{
crews[1].Add(character);
}
}
initialized = true;
}
if (crews[0].Count == 0 || crews[1].Count == 0)
{
//if there are no characters in either crew, end the round
teamDead[0] = teamDead[1] = true;
state = 1;
}
else
{
teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated);
teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated);
if (teamDead[0] && teamDead[1]) { state = 1; }
}
CheckTeamCharacters();
if (state == 0)
{
CheckWinCondition(deltaTime);
for (int i = 0; i < teamDead.Length; i++)
{
if (!teamDead[i] && teamDead[1 - i])
{
//make sure nobody in the other team can be revived because that would be pretty weird
crews[1 - i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeathType.Unknown, null); });
GameMain.GameSession.WinningTeam = i == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
//state 1 = team 1 won, 2 = team 2 won
State = i + 1;
SetWinningTeam(i);
break;
}
}
@@ -85,7 +81,7 @@ namespace Barotrauma
if (teamDead[0] && teamDead[1])
{
GameMain.GameSession.WinningTeam = CharacterTeamType.None;
if (GameMain.Server != null) { GameMain.Server.EndGame(); }
GameMain.Server?.EndGame();
}
else if (GameMain.GameSession.WinningTeam != CharacterTeamType.None)
{
@@ -93,5 +89,168 @@ namespace Barotrauma
}
}
}
private void CheckTeamCharacters()
{
if (!allowRespawning && initialized)
{
//if no respawns are allowed, we only need to check the characters once
return;
}
for (int i = 0; i < crews.Length; i++)
{
foreach (var character in crews[i])
{
if (character.IsDead)
{
AddKill(character);
}
}
}
crews[0].Clear();
crews[1].Clear();
foreach (Character character in Character.CharacterList)
{
if (character.IsDead) { continue; }
if (character.TeamID == CharacterTeamType.Team1)
{
crews[0].Add(character);
}
else if (character.TeamID == CharacterTeamType.Team2)
{
crews[1].Add(character);
}
if (character.IsBot && character.AIController is HumanAIController humanAi)
{
if (!humanAi.ObjectiveManager.HasOrder<AIObjectiveFightIntruders>(o => o.TargetCharactersInOtherSubs) &&
OrderPrefab.Prefabs.TryGet(Tags.AssaultEnemyOrder, out OrderPrefab? assaultOrder))
{
character.SetOrder(assaultOrder.CreateInstance(
OrderPrefab.OrderTargetType.Entity, orderGiver: null).WithManualPriority(CharacterInfo.HighestManualOrderPriority),
isNewOrder: true, speak: false);
}
}
}
initialized = true;
}
private void CheckWinCondition(float deltaTime)
{
switch (winCondition)
{
case WinCondition.LastManStanding:
if (crews[0].Count == 0 || crews[1].Count == 0)
{
//if there are no characters in either crew, end the round
teamDead[0] = teamDead[1] = true;
state = 1;
}
else
{
teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated);
teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated);
if (teamDead[0] && teamDead[1]) { state = 1; }
}
break;
case WinCondition.KillCount:
//no need to do anything, kills are counted in AddKill
break;
case WinCondition.ControlSubmarine:
CheckTargetSubmarineControl(deltaTime);
break;
}
CheckScore();
}
private void CheckScore()
{
for (int i = 0; i < crews.Length; i++)
{
if (Scores[i] >= WinScore)
{
SetWinningTeam(i);
break;
}
}
}
private void CheckTargetSubmarineControl(float deltaTime)
{
if (targetSubmarine == null) { return; }
//score updates at 1 second intervals, so the score represents the time in seconds
timeInTargetSubmarineTimer += deltaTime;
if (timeInTargetSubmarineTimer < 1.0f)
{
return;
}
timeInTargetSubmarineTimer = 0.0f;
bool crew1InSubmarine = crews[0].Any(c => c.Submarine == targetSubmarine);
bool crew2InSubmarine = crews[1].Any(c => c.Submarine == targetSubmarine);
for (int i = 0; i < crews.Length; i++)
{
if (crews[i].Any(c => c.Submarine == targetSubmarine) &&
crews[1 - i].None(c => c.Submarine == targetSubmarine))
{
Scores[i]++;
GameMain.Server?.UpdateMissionState(this);
}
}
}
private void AddKill(Character character)
{
kills.Add(new KillCount(character, character.CauseOfDeath?.Killer));
if (winCondition == WinCondition.KillCount)
{
Scores[character.TeamID == CharacterTeamType.Team1 ? 1 : 0] += PointsPerKill;
}
GameMain.Server?.UpdateMissionState(this);
}
private void SetWinningTeam(int teamIndex)
{
//state 1 = team 1 won, 2 = team 2 won
State = teamIndex + 1;
GameMain.GameSession.WinningTeam = teamIndex == 0 ? CharacterTeamType.Team1 : CharacterTeamType.Team2;
}
public override void ServerWrite(IWriteMessage msg)
{
base.ServerWrite(msg);
msg.WriteUInt16((ushort)Scores[0]);
msg.WriteUInt16((ushort)Scores[1]);
IEnumerable<Client> uniqueClients = kills
.Select(k => k.VictimClient)
.Union(kills.Select(k => k.KillerClient))
.NotNull();
msg.WriteVariableUInt32((uint)uniqueClients.Count());
foreach (Client client in uniqueClients)
{
msg.WriteByte(client.SessionId);
msg.WriteVariableUInt32((uint)kills.Count(k => k.VictimClient == client));
msg.WriteVariableUInt32((uint)kills.Count(k => k.KillerClient == client));
}
IEnumerable<CharacterInfo> uniqueBots = kills
.Select(k => k.Killer)
.Union(kills.Select(k => k.Victim))
.NotNull()
.Where(c => c.Info != null && c.IsBot)
.Select(c => c.Info);
msg.WriteVariableUInt32((uint)uniqueBots.Count());
foreach (CharacterInfo botInfo in uniqueBots)
{
msg.WriteUInt16(botInfo.ID);
msg.WriteVariableUInt32((uint)kills.Count(k => k.Victim?.Info == botInfo));
msg.WriteVariableUInt32((uint)kills.Count(k => k.Killer?.Info == botInfo));
}
}
}
}
@@ -241,7 +241,7 @@ namespace Barotrauma
maxPlayers,
ownerKey,
ownerEndpoint);
Server.StartServer();
Server.StartServer(registerToServerList: true);
for (int i = 0; i < CommandLineArgs.Length; i++)
{
@@ -143,6 +143,7 @@ namespace Barotrauma
{
Reset();
CharacterInfo.PermanentlyDead = true;
GameMain.GameSession?.IncrementPermadeath(AccountId);
DebugConsole.NewMessage($"Permadeath applied on {Name}'s CharacterCampaignData.CharacterInfo.");
}
@@ -121,21 +121,21 @@ namespace Barotrauma
{
if (string.IsNullOrWhiteSpace(savePath)) { return; }
GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), savePath, GameModePreset.MultiPlayerCampaign, startingSettings, seed);
GameMain.GameSession = new GameSession(new SubmarineInfo(subPath), Option.None, CampaignDataPath.CreateRegular(savePath), GameModePreset.MultiPlayerCampaign, startingSettings, seed);
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
DebugConsole.NewMessage("Campaign started!", Color.Cyan);
DebugConsole.NewMessage("Current location: " + GameMain.GameSession.Map.CurrentLocation.DisplayName, Color.Cyan);
((MultiPlayerCampaign)GameMain.GameSession.GameMode).LoadInitialLevel();
}
public static void LoadCampaign(string selectedSave, Client client)
public static void LoadCampaign(CampaignDataPath path, Client client)
{
GameMain.NetLobbyScreen.ToggleCampaignMode(true);
try
{
SaveUtil.LoadGame(selectedSave);
SaveUtil.LoadGame(path);
if (GameMain.GameSession.GameMode is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastSaveID++;
@@ -148,7 +148,7 @@ namespace Barotrauma
}
catch (Exception e)
{
string errorMsg = $"Error while loading the save {selectedSave}";
string errorMsg = $"Error while loading the save {path.LoadPath}";
if (client != null)
{
GameMain.Server?.SendDirectChatMessage($"{errorMsg}: {e.Message}\n{e.StackTrace}", client, ChatMessageType.Error);
@@ -209,7 +209,7 @@ namespace Barotrauma
{
try
{
LoadCampaign(saveFiles[saveIndex].FilePath, client: null);
LoadCampaign(CampaignDataPath.CreateRegular(saveFiles[saveIndex].FilePath), client: null);
}
catch (Exception ex)
{
@@ -289,7 +289,8 @@ namespace Barotrauma
data.Refresh(character, refreshHealthData: character.CauseOfDeath?.Type != CauseOfDeathType.Disconnected);
characterData.Add(data);
}
else
//check the cause of death in the CharacterInfo too (the character instance may have despawned, so we can't just rely on that)
else if (data.CharacterInfo.CauseOfDeath is not { Type: CauseOfDeathType.Disconnected })
{
//character dead or removed -> reduce skills, remove items, health data, etc
data.CharacterInfo.ApplyDeathEffects();
@@ -406,13 +407,13 @@ namespace Barotrauma
LeaveUnconnectedSubs(leavingSub);
NextLevel = newLevel;
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
SaveUtil.SaveGame(GameMain.GameSession.DataPath);
}
else
{
PendingSubmarineSwitch = null;
GameMain.Server.EndGame(TransitionType.None, wasSaved: false);
LoadCampaign(GameMain.GameSession.SavePath, client: null);
LoadCampaign(GameMain.GameSession.DataPath, client: null);
LastSaveID++;
IncrementAllLastUpdateIds();
yield return CoroutineStatus.Success;
@@ -1229,7 +1230,7 @@ namespace Barotrauma
if (renameCharacter)
{
renamedIdentifier = msg.ReadUInt16();
newName = msg.ReadString();
newName = Client.SanitizeName(msg.ReadString());
existingCrewMember = msg.ReadBoolean();
if (!GameMain.Server.IsNameValid(sender, newName))
{
@@ -1466,7 +1467,16 @@ namespace Barotrauma
return wallet.Balance + Bank.Balance;
}
public override void Save(XElement element)
/// <summary>
/// Serializes the campaign and character data to XML.
/// </summary>
/// <param name="element">Game session element to save the campaign data to.</param>
/// <param name="isSavingOnLoading">
/// Whether the save is being done during loading to ensure the campaign ID matches the one in the save file.
/// Used to work around some quirks with the backup save system.
/// See: <see cref="SaveUtil.SaveGame(CampaignDataPath,bool)"/>
/// </param>
public override void Save(XElement element, bool isSavingOnLoading)
{
element.Add(new XAttribute("campaignid", CampaignID));
XElement modeElement = new XElement("MultiPlayerCampaign",
@@ -1516,8 +1526,16 @@ namespace Barotrauma
element.Add(modeElement);
//save character data to a separate file
string characterDataPath = GetCharacterDataSavePath();
// save character data to a separate file
// When loading a campaign in multiplayer, we save the campaign to ensure the campaign ID that gets assigned
// matches the one in the save file, this is a problem with the backup save system since this causes the
// character data to save too, and we don't want to overwrite the main save file's character data.
// So we instead save over the load path in this case, which in backup saves is the backup file
// which we don't mind getting overriden since the data should be the same
string characterDataPath = isSavingOnLoading
? GetCharacterDataPathForLoading()
: GetCharacterDataPathForSaving();
XDocument characterDataDoc = new XDocument(new XElement("CharacterData"));
foreach (CharacterCampaignData cd in characterData)
{
@@ -1525,6 +1543,7 @@ namespace Barotrauma
}
try
{
SaveUtil.DeleteIfExists(characterDataPath);
characterDataDoc.SaveSafe(characterDataPath);
}
catch (Exception e)
@@ -1544,7 +1563,7 @@ namespace Barotrauma
/// eg. when using this method to save a character itself restored from the backup.</param>
public void SaveSingleCharacter(CharacterCampaignData newData, bool skipBackup = false)
{
string characterDataPath = GetCharacterDataSavePath();
string characterDataPath = GetCharacterDataPathForSaving();
if (!File.Exists(characterDataPath))
{
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
@@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components
private string lastSentText;
private float sendStateTimer;
[Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(100)]
[Serialize("", IsPropertySaveable.Yes, description: "The text to display on the label.", alwaysUseInstanceValues: true), Editable(MaxLength = 100)]
public string Text
{
get;
@@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components
if (!AutoPilot)
{
steeringInput = newSteeringInput;
steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel("helm") / 100.0f);
steeringAdjustSpeed = MathHelper.Lerp(0.2f, 1.0f, c.Character.GetSkillLevel(Tags.HelmSkill) / 100.0f);
}
else
{
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
@@ -310,7 +310,13 @@ namespace Barotrauma.Items.Components
return wire.BackingWire.TryUnwrap(out var backingWire) ? backingWire.Name : "a wire";
}
bool CanAccessAndUnlocked(Client client) => item.CanClientAccess(client) && !Locked;
bool CanAccessAndUnlocked(Client client) =>
!IsLocked() &&
item.CanClientAccess(client) &&
ClientHasRequiredItems(client);
bool ClientHasRequiredItems(Client client) =>
client.Character is { } chara && HasRequiredItems(chara, addMessage: false);
}
/// <summary>
@@ -11,13 +11,17 @@ namespace Barotrauma.Items.Components
string[] elementValues = new string[customInterfaceElementList.Count];
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
if (customInterfaceElementList[i].HasPropertyName)
var element = customInterfaceElementList[i];
switch (element.InputType)
{
elementValues[i] = msg.ReadString();
}
else
{
elementStates[i] = msg.ReadBoolean();
case CustomInterfaceElement.InputTypeOption.Number:
case CustomInterfaceElement.InputTypeOption.Text:
elementValues[i] = msg.ReadString();
break;
case CustomInterfaceElement.InputTypeOption.Button:
case CustomInterfaceElement.InputTypeOption.TickBox:
elementStates[i] = msg.ReadBoolean();
break;
}
}
@@ -26,15 +30,10 @@ namespace Barotrauma.Items.Components
{
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
if (element.HasPropertyName)
var element = customInterfaceElementList[i];
switch (element.InputType)
{
if (!element.IsNumberInput)
{
TextChanged(element, elementValues[i]);
}
else
{
case CustomInterfaceElement.InputTypeOption.Number:
switch (element.NumberType)
{
case NumberType.Int when int.TryParse(elementValues[i], out int value):
@@ -44,16 +43,20 @@ namespace Barotrauma.Items.Components
ValueChanged(element, value);
break;
}
}
}
else if (element.ContinuousSignal)
{
TickBoxToggled(element, elementStates[i]);
}
else if (elementStates[i])
{
clickedButton = element;
ButtonClicked(element);
break;
case CustomInterfaceElement.InputTypeOption.Text:
TextChanged(element, elementValues[i]);
break;
case CustomInterfaceElement.InputTypeOption.TickBox:
TickBoxToggled(element, elementStates[i]);
break;
case CustomInterfaceElement.InputTypeOption.Button:
if (elementStates[i])
{
clickedButton = element;
ButtonClicked(element);
}
break;
}
}
}
@@ -70,17 +73,19 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
var element = customInterfaceElementList[i];
if (element.HasPropertyName)
switch (element.InputType)
{
msg.WriteString(element.Signal);
}
else if(element.ContinuousSignal)
{
msg.WriteBoolean(element.State);
}
else
{
msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]);
case CustomInterfaceElement.InputTypeOption.Number:
case CustomInterfaceElement.InputTypeOption.Text:
msg.WriteString(element.Signal);
break;
case CustomInterfaceElement.InputTypeOption.TickBox:
msg.WriteBoolean(element.State);
break;
case CustomInterfaceElement.InputTypeOption.Button:
msg.WriteBoolean(extraData is Item.ComponentStateEventData { ComponentData: EventData eventData } && eventData.BtnElement == customInterfaceElementList[i]);
break;
}
}
}
@@ -175,6 +175,10 @@ namespace Barotrauma
}
}
break;
case SwapItemEventData swapItemEventData:
msg.WriteUInt16(swapItemEventData.NewId);
msg.WriteUInt32(swapItemEventData.NewItem.UintIdentifier);
break;
default:
throw error($"Unsupported event type {itemEventData.GetType().Name}");
}
@@ -317,7 +321,7 @@ namespace Barotrauma
msg.WriteBoolean(tagsChanged);
if (tagsChanged)
{
IEnumerable<Identifier> splitTags = Tags.Split(',').ToIdentifiers();
IEnumerable<Identifier> splitTags = Tags.ToIdentifiers();
msg.WriteString(string.Join(',', splitTags.Where(t => !base.Prefab.Tags.Contains(t))));
msg.WriteString(string.Join(',', base.Prefab.Tags.Where(t => !splitTags.Contains(t))));
}
@@ -49,7 +49,7 @@ namespace Barotrauma.Networking
string[] lines;
try
{
lines = File.ReadAllLines(LegacySavePath);
lines = File.ReadAllLines(LegacySavePath, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -125,7 +125,7 @@ namespace Barotrauma.Networking
}
else
{
GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode);
GameMain.Server.SendChatMessage(txt, senderClient: c, chatMode: chatMode, type: type == ChatMessageType.Team ? type : null);
}
}
@@ -102,13 +102,19 @@ namespace Barotrauma.Networking
{
get
{
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return 100.0f; }
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode)
{
return 100.0f;
}
if (HasPermission(ClientPermissions.KarmaImmunity)) { return 100.0f; }
return karma;
}
set
{
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return; }
if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled || GameMain.GameSession?.GameMode is PvPMode)
{
return;
}
karma = Math.Min(Math.Max(value, 0.0f), 100.0f);
if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f))
{
@@ -94,7 +94,7 @@ namespace Barotrauma.Networking
{
try
{
Data = File.ReadAllBytes(filePath);
Data = File.ReadAllBytes(filePath, catchUnauthorizedAccessExceptions: false);
}
catch (System.IO.IOException e)
{
@@ -400,7 +400,7 @@ namespace Barotrauma.Networking
if (GameMain.GameSession != null &&
!ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave))
{
StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.SavePath);
StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.LoadPath);
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
{
client.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now);
File diff suppressed because it is too large Load Diff
@@ -269,10 +269,15 @@ namespace Barotrauma.Networking
{
if (netServer == null) { return; }
switch (inc.SenderConnection.Status)
NetConnectionStatus status = inc.ReadHeader<NetConnectionStatus>();
switch (status)
{
case NetConnectionStatus.Disconnected:
LidgrenConnection? conn = connectedClients.Select(c => c.Connection).FirstOrDefault(c => c.NetConnection == inc.SenderConnection);
string disconnectMsg = inc.ReadString();
var peerDisconnectPacket =
PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg).Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown));
if (conn != null)
{
if (conn == OwnerConnection)
@@ -283,7 +288,7 @@ namespace Barotrauma.Networking
}
else
{
Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
Disconnect(conn, peerDisconnectPacket);
}
}
else
@@ -291,7 +296,7 @@ namespace Barotrauma.Networking
PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection);
if (pendingClient != null)
{
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
RemovePendingClient(pendingClient, peerDisconnectPacket);
}
}
@@ -332,7 +337,9 @@ namespace Barotrauma.Networking
if (status == Steamworks.AuthResponse.OK)
{
pendingClient.Connection.SetAccountInfo(new AccountInfo(new SteamId(steamId), new SteamId(ownerId)));
pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder;
pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection)
? ConnectionInitialization.Password
: ConnectionInitialization.ContentPackageOrder;
pendingClient.UpdateTime = Timing.TotalTime;
}
else
@@ -450,7 +457,9 @@ namespace Barotrauma.Networking
pendingClient.Connection.SetAccountInfo(accountInfo);
pendingClient.Name = packet.Name;
pendingClient.OwnerKey = packet.OwnerKey;
pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder;
pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection)
? ConnectionInitialization.Password
: ConnectionInitialization.ContentPackageOrder;
}
void rejectClient()
@@ -470,7 +479,7 @@ namespace Barotrauma.Networking
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(new UnauthenticatedAccountId(packet.Name)));
@@ -344,7 +344,9 @@ namespace Barotrauma.Networking
{
// Do nothing with the auth ticket because that should be handled by the owner peer,
// just assume that authentication succeeded
pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder;
pendingClient.InitializationStep = ShouldAskForPassword(serverSettings, pendingClient.Connection)
? ConnectionInitialization.Password
: ConnectionInitialization.ContentPackageOrder;
pendingClient.Name = packet.Name;
pendingClient.AuthSessionStarted = true;
}
@@ -359,5 +359,18 @@ namespace Barotrauma.Networking
}
protected static void LogMalformedMessage()
=> DebugConsole.ThrowError("Received malformed message from remote peer.");
protected bool ShouldAskForPassword(ServerSettings serverSettings, NetworkConnection connection)
{
if (!serverSettings.HasPassword) { return false; }
if (GameMain.Server is { } server && server.FindAndRemoveRecentlyDisconnectedConnection(connection))
{
// do not ask passwords from clients that have recently disconnected
return false;
}
return true;
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -8,16 +9,11 @@ namespace Barotrauma.Networking
{
partial class RespawnManager : Entity, IServerSerializable
{
private DateTime despawnTime;
private float shuttleEmptyTimer;
private int pendingRespawnCount, requiredRespawnCount;
private int prevPendingRespawnCount, prevRequiredRespawnCount;
public bool IsShuttleInsideLevel => RespawnShuttles.Any(s => s.WorldPosition.Y < Level.Loaded.Size.Y);
public bool IsShuttleInsideLevel => RespawnShuttle != null && RespawnShuttle.WorldPosition.Y < Level.Loaded.Size.Y;
private IEnumerable<Client> GetClientsToRespawn()
private IEnumerable<Client> GetClientsToRespawn(CharacterTeamType teamId)
{
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
foreach (Client c in networkMember.ConnectedClients)
@@ -25,6 +21,10 @@ namespace Barotrauma.Networking
if (!c.InGame) { continue; }
if (c.SpectateOnly && (GameMain.Server.ServerSettings.AllowSpectating || GameMain.Server.OwnerConnection == c.Connection)) { continue; }
if (c.Character != null && !c.Character.IsDead) { continue; }
if (c.TeamID != CharacterTeamType.None && c.TeamID != teamId)
{
continue;
}
var matchingData = campaign?.GetClientCharacterData(c);
@@ -80,36 +80,42 @@ namespace Barotrauma.Networking
return false;
}
private static List<CharacterInfo> GetBotsToRespawn()
private static List<CharacterInfo> GetBotsToRespawn(CharacterTeamType teamId)
{
//this works under the assumption that GetCharacterInfos only returns bots in MP
var botInfos = GameMain.GameSession.CrewManager.GetCharacterInfos()
.Where(botInfo => botInfo.TeamID == teamId)
//filter out players in case a player has been given control of a bot using console commands
.Where(botInfo => GameMain.Server.ConnectedClients.None(c => c.CharacterInfo == botInfo))
.ToList();
if (GameMain.Server.ServerSettings.BotSpawnMode == BotSpawnMode.Normal)
{
return Character.CharacterList
.FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null && c.IsDead)
.Select(c => c.Info)
.ToList();
return botInfos.Where(ci => ci.Character == null || ci.Character.IsDead).ToList();
}
int currPlayerCount = GameMain.Server.ConnectedClients.Count(c =>
c.InGame &&
(!c.SpectateOnly || (!GameMain.Server.ServerSettings.AllowSpectating && GameMain.Server.OwnerConnection != c.Connection)));
var existingBots = Character.CharacterList
.FindAll(c => c.TeamID == CharacterTeamType.Team1 && c.AIController != null && c.Info != null);
var existingBots = Character.CharacterList.FindAll(c => c.IsBot && !c.IsDead && c.TeamID == teamId);
int requiredBots = GameMain.Server.ServerSettings.BotCount - currPlayerCount;
requiredBots -= existingBots.Count(b => !b.IsDead);
List<CharacterInfo> botsToRespawn = new List<CharacterInfo>();
for (int i = 0; i < requiredBots; i++)
{
CharacterInfo botToRespawn = existingBots.Find(b => b.IsDead)?.Info;
CharacterInfo botToRespawn = botInfos.FirstOrDefault(b => b.Character == null || b.Character.IsDead);
if (botToRespawn == null)
{
botToRespawn = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
botToRespawn = new CharacterInfo(CharacterPrefab.HumanSpeciesName)
{
TeamID = teamId
};
}
else
{
botInfos.Remove(botToRespawn);
existingBots.Remove(botToRespawn.Character);
}
botsToRespawn.Add(botToRespawn);
@@ -117,9 +123,27 @@ namespace Barotrauma.Networking
return botsToRespawn;
}
private bool ShouldStartRespawnCountdown()
private string GetRespawnShuttleText(CharacterTeamType team)
{
int characterToRespawnCount = GetClientsToRespawn().Count();
if (teamSpecificStates.Count == 1)
{
return "respawn shuttle";
}
return team == CharacterTeamType.Team1 ? "respawn shuttle (team 1)" : "respawn shuttle (team 2)";
}
private string GetTeamNameText(CharacterTeamType team)
{
if (teamSpecificStates.Count == 1)
{
return "everyone";
}
return team == CharacterTeamType.Team1 ? "team 1" : "team 2";
}
private bool ShouldStartRespawnCountdown(TeamSpecificState teamSpecificState)
{
int characterToRespawnCount = GetClientsToRespawn(teamSpecificState.TeamID).Count();
return ShouldStartRespawnCountdown(characterToRespawnCount);
}
@@ -133,108 +157,91 @@ namespace Barotrauma.Networking
return characterToRespawnCount >= GetMinCharactersToRespawn();
}
partial void UpdateWaiting(float _)
partial void UpdateWaiting(TeamSpecificState teamSpecificState)
{
if (RespawnShuttle != null)
//no respawns in the first minute of the round - otherwise it can be that bots
//are respawned to "fill" the spots of players who are taking a long time to load in
if (GameMain.GameSession is { RoundDuration: < 60 })
{
RespawnShuttle.Velocity = Vector2.Zero;
return;
}
pendingRespawnCount = GetClientsToRespawn().Count();
requiredRespawnCount = GetMinCharactersToRespawn();
if (pendingRespawnCount != prevPendingRespawnCount ||
requiredRespawnCount != prevRequiredRespawnCount)
var teamId = teamSpecificState.TeamID;
var respawnShuttle = GetShuttle(teamId);
if (respawnShuttle != null)
{
prevPendingRespawnCount = pendingRespawnCount;
prevRequiredRespawnCount = requiredRespawnCount;
respawnShuttle.Velocity = Vector2.Zero;
}
teamSpecificState.PendingRespawnCount = GetClientsToRespawn(teamId).Count();
if (GameMain.GameSession?.Campaign == null)
{
teamSpecificState.PendingRespawnCount += GetBotsToRespawn(teamId).Count;
}
teamSpecificState.RequiredRespawnCount = GetMinCharactersToRespawn();
if (teamSpecificState.PendingRespawnCount != teamSpecificState.PrevPendingRespawnCount ||
teamSpecificState.RequiredRespawnCount != teamSpecificState.PrevRequiredRespawnCount)
{
teamSpecificState.PrevPendingRespawnCount = teamSpecificState.PendingRespawnCount;
teamSpecificState.PrevRequiredRespawnCount = teamSpecificState.RequiredRespawnCount;
GameMain.Server.CreateEntityEvent(this);
}
if (RespawnCountdownStarted)
if (teamSpecificState.RespawnCountdownStarted)
{
if (pendingRespawnCount == 0)
if (teamSpecificState.PendingRespawnCount == 0)
{
RespawnCountdownStarted = false;
teamSpecificState.RespawnCountdownStarted = false;
GameMain.Server.CreateEntityEvent(this);
}
}
else
{
bool shouldStartCountdown = ShouldStartRespawnCountdown(pendingRespawnCount);
bool shouldStartCountdown = ShouldStartRespawnCountdown(teamSpecificState.PendingRespawnCount);
if (shouldStartCountdown)
{
RespawnCountdownStarted = true;
if (RespawnTime < DateTime.Now)
teamSpecificState.RespawnCountdownStarted = true;
if (teamSpecificState.RespawnTime < DateTime.Now)
{
RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f));
teamSpecificState.RespawnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, (int)(GameMain.Server.ServerSettings.RespawnInterval * 1000.0f));
}
GameMain.Server.CreateEntityEvent(this);
}
}
}
if (RespawnCountdownStarted && DateTime.Now > RespawnTime)
if (teamSpecificState.RespawnCountdownStarted && DateTime.Now > teamSpecificState.RespawnTime)
{
DispatchShuttle();
RespawnCountdownStarted = false;
DispatchShuttle(teamSpecificState);
teamSpecificState.RespawnCountdownStarted = false;
}
}
private void DispatchShuttle()
private void DispatchShuttle(TeamSpecificState teamSpecificState)
{
if (RespawnShuttle != null)
if (RespawnShuttles.Any())
{
CurrentState = State.Transporting;
ResetShuttle(teamSpecificState);
teamSpecificState.CurrentState = State.Transporting;
GameMain.Server.CreateEntityEvent(this);
ResetShuttle();
if (shuttleSteering != null)
{
shuttleSteering.TargetVelocity = Vector2.Zero;
}
Vector2 spawnPos = FindSpawnPos();
RespawnCharacters(spawnPos, out bool anyCharacterSpawnedInShuttle);
if (anyCharacterSpawnedInShuttle)
{
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
{
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
}
}
else
{
CurrentState = State.Waiting;
GameServer.Log("Respawning everyone in main sub.", ServerLog.MessageType.Spawning);
teamSpecificState.CurrentState = State.Waiting;
GameServer.Log($"Respawning {GetTeamNameText(teamSpecificState.TeamID)} in the main sub.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCharacters(shuttlePos: null, out _);
}
RespawnCharacters(teamSpecificState);
}
partial void UpdateReturningProjSpecific(float deltaTime)
partial void UpdateReturningProjSpecific(TeamSpecificState teamSpecificState, float deltaTime)
{
//speed up despawning if there's no-one inside the shuttle
if (despawnTime > DateTime.Now + new TimeSpan(0, 0, seconds: 30) && CheckShuttleEmpty(deltaTime))
if (teamSpecificState.DespawnTime > DateTime.Now + new TimeSpan(0, 0, seconds: 30) && CheckShuttleEmpty(deltaTime))
{
despawnTime = DateTime.Now + new TimeSpan(0, 0, seconds: 30);
teamSpecificState.DespawnTime = DateTime.Now + new TimeSpan(0, 0, seconds: 30);
}
foreach (Door door in shuttleDoors)
foreach (Door door in shuttleDoors[teamSpecificState.TeamID])
{
if (door.IsOpen)
{
@@ -242,87 +249,71 @@ namespace Barotrauma.Networking
}
}
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null);
var shuttleGaps = Gap.GapList.FindAll(g => RespawnShuttles.Contains(g.Submarine) && g.ConnectedWall != null);
shuttleGaps.ForEach(g => Spawner.AddEntityToRemoveQueue(g));
var dockingPorts = Item.ItemList.FindAll(i => i.Submarine == RespawnShuttle && i.GetComponent<DockingPort>() != null);
var dockingPorts = Item.ItemList.FindAll(i => RespawnShuttles.Contains(i.Submarine) && i.GetComponent<DockingPort>() != null);
dockingPorts.ForEach(d => d.GetComponent<DockingPort>().Undock());
//shuttle has returned if the path has been traversed or the shuttle is close enough to the exit
if (!CoroutineManager.IsCoroutineRunning("forcepos"))
if (!IsShuttleInsideLevel || DateTime.Now > teamSpecificState.DespawnTime)
{
if ((shuttleSteering?.SteeringPath != null && shuttleSteering.SteeringPath.Finished)
|| (RespawnShuttle.WorldPosition.Y + RespawnShuttle.Borders.Y > Level.Loaded.StartPosition.Y - Level.ShaftHeight &&
Math.Abs(Level.Loaded.StartPosition.X - RespawnShuttle.WorldPosition.X) < 1000.0f))
{
CoroutineManager.StopCoroutines("forcepos");
CoroutineManager.StartCoroutine(
ForceShuttleToPos(new Vector2(Level.Loaded.StartPosition.X, Level.Loaded.Size.Y + 1000.0f), 100.0f), "forcepos");
ResetShuttle(teamSpecificState);
}
}
if (!IsShuttleInsideLevel || DateTime.Now > despawnTime)
{
CoroutineManager.StopCoroutines("forcepos");
ResetShuttle();
CurrentState = State.Waiting;
GameServer.Log("The respawn shuttle has left.", ServerLog.MessageType.Spawning);
teamSpecificState.CurrentState = State.Waiting;
GameServer.Log($"The {GetRespawnShuttleText(teamSpecificState.TeamID)} has left.", ServerLog.MessageType.Spawning);
GameMain.Server.CreateEntityEvent(this);
RespawnCountdownStarted = false;
ReturnCountdownStarted = false;
teamSpecificState.RespawnCountdownStarted = false;
teamSpecificState.ReturnCountdownStarted = false;
}
}
partial void UpdateTransportingProjSpecific(float deltaTime)
partial void UpdateTransportingProjSpecific(TeamSpecificState teamSpecificState, float deltaTime)
{
if (!ReturnCountdownStarted)
if (!teamSpecificState.ReturnCountdownStarted)
{
//if there are no living chracters inside, transporting can be stopped immediately
if (CheckShuttleEmpty(deltaTime))
{
ReturnTime = DateTime.Now;
ReturnCountdownStarted = true;
teamSpecificState.ReturnTime = DateTime.Now;
teamSpecificState.ReturnCountdownStarted = true;
}
else if (!ShouldStartRespawnCountdown())
else if (!ShouldStartRespawnCountdown(teamSpecificState))
{
//don't start counting down until someone else needs to respawn
ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000));
despawnTime = ReturnTime + new TimeSpan(0, 0, seconds: 30);
teamSpecificState.ReturnTime = DateTime.Now + new TimeSpan(0, 0, 0, 0, milliseconds: (int)(maxTransportTime * 1000));
teamSpecificState.DespawnTime = teamSpecificState.ReturnTime + new TimeSpan(0, 0, seconds: 30);
return;
}
else
{
ReturnCountdownStarted = true;
teamSpecificState.ReturnCountdownStarted = true;
GameMain.Server.CreateEntityEvent(this);
}
}
else if (CheckShuttleEmpty(deltaTime))
{
ReturnTime = DateTime.Now;
teamSpecificState.ReturnTime = DateTime.Now;
}
if (DateTime.Now > ReturnTime)
if (DateTime.Now > teamSpecificState.ReturnTime)
{
if (IsShuttleInsideLevel)
{
GameServer.Log("The respawn shuttle is leaving.", ServerLog.MessageType.ServerMessage);
GameServer.Log($"The {GetRespawnShuttleText(teamSpecificState.TeamID)} is leaving.", ServerLog.MessageType.ServerMessage);
}
CurrentState = State.Returning;
teamSpecificState.CurrentState = State.Returning;
GameMain.Server.CreateEntityEvent(this);
RespawnCountdownStarted = false;
teamSpecificState.RespawnCountdownStarted = false;
maxTransportTime = GameMain.Server.ServerSettings.MaxTransportTime;
}
}
private bool CheckShuttleEmpty(float deltaTime)
{
if (!Character.CharacterList.Any(c => c.Submarine == RespawnShuttle && !c.IsDead))
if (RespawnShuttles.All(respawnShuttle => Character.CharacterList.None(c => c.Submarine == respawnShuttle && !c.IsDead)))
{
shuttleEmptyTimer += deltaTime;
}
@@ -333,15 +324,18 @@ namespace Barotrauma.Networking
return shuttleEmptyTimer > 1.0f;
}
private void RespawnCharacters(Vector2? shuttlePos, out bool anyCharacterSpawnedInShuttle)
private void RespawnCharacters(TeamSpecificState teamSpecificState)
{
respawnedCharacters.Clear();
var respawnSub = RespawnShuttle ?? Submarine.MainSub;
var teamID = teamSpecificState.TeamID;
int teamIndex = teamID == CharacterTeamType.Team1 ? 0 : 1;
bool anyCharacterSpawnedInShuttle = false;
teamSpecificState.RespawnedCharacters.Clear();
MultiPlayerCampaign campaign = GameMain.GameSession.GameMode as MultiPlayerCampaign;
bool isPvPMode = GameMain.GameSession.GameMode is PvPMode;
int teamCount = isPvPMode ? 2 : 1;
var clients = GetClientsToRespawn().ToList();
var clients = GetClientsToRespawn(teamID).ToList();
foreach (Client c in clients)
{
// Get rid of the existing character
@@ -355,18 +349,31 @@ namespace Barotrauma.Networking
c.CharacterInfo = matchingData.CharacterInfo;
}
//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;
c.CharacterInfo ??= new CharacterInfo(CharacterPrefab.HumanSpeciesName, c.Name);
//force everyone to team 1 if there's just one team
if (teamCount == 1)
{
c.TeamID = teamID;
}
else if (isPvPMode && c.TeamID == CharacterTeamType.None)
{
GameMain.Server.AssignClientToPvpTeamMidgame(c);
}
c.CharacterInfo.TeamID = c.TeamID;
}
List<CharacterInfo> characterInfos = clients.Select(c => c.CharacterInfo).ToList();
//bots don't respawn in the campaign
var botsToSpawn = GetBotsToRespawn(teamID);
if (campaign == null)
{
var botsToSpawn = GetBotsToRespawn();
characterInfos.AddRange(botsToSpawn);
foreach (var bot in botsToSpawn)
{
// Get rid of the existing bots' corpses
if (bot.Character is Character character) { character.DespawnNow(); }
}
}
GameMain.Server.AssignJobs(clients);
@@ -374,38 +381,61 @@ namespace Barotrauma.Networking
{
if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null)
{
c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, Rand.RandSync.Unsynced, c.AssignedJob.Variant);
c.CharacterInfo.Job = new Job(c.AssignedJob.Prefab, isPvPMode, Rand.RandSync.Unsynced, c.AssignedJob.Variant);
}
}
//the spawnpoints where the characters will spawn
var shuttleSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub);
//the spawnpoints where they would spawn if they were spawned inside the main sub
//(in order to give them appropriate ID card tags)
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub);
System.Diagnostics.Debug.Assert(characterInfos.All(c => c.TeamID == teamID),
"List of characters to respawn contained characters from the wrong team.");
Submarine mainSub = Submarine.MainSubs[teamIndex];
Submarine respawnSub = null;
Submarine respawnShuttle = GetShuttle(teamID);
Vector2? shuttlePos = null;
if (respawnShuttle != null)
{
respawnSub = respawnShuttle;
shuttlePos = FindSpawnPos(respawnShuttle, mainSub);
}
respawnSub ??= mainSub ?? Level.Loaded.StartOutpost;
ItemPrefab divingSuitPrefab = null;
if ((shuttlePos != null && Level.Loaded.GetRealWorldDepth(shuttlePos.Value.Y) > Level.DefaultRealWorldCrushDepth) ||
Level.Loaded.GetRealWorldDepth(Submarine.MainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth)
(mainSub != null && Level.Loaded.GetRealWorldDepth(mainSub.WorldPosition.Y) > Level.DefaultRealWorldCrushDepth))
{
divingSuitPrefab = ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuitdeep"));
}
divingSuitPrefab ??=
divingSuitPrefab ??=
ItemPrefab.Prefabs.FirstOrDefault(it => it.Tags.Any(t => t == "respawnsuit")) ??
ItemPrefab.Find(null, "divingsuit".ToIdentifier());
ItemPrefab oxyPrefab = ItemPrefab.Find(null, "oxygentank".ToIdentifier());
ItemPrefab scooterPrefab = ItemPrefab.Find(null, "underwaterscooter".ToIdentifier());
ItemPrefab batteryPrefab = ItemPrefab.Find(null, "batterycell".ToIdentifier());
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
//the spawnpoints where the characters will spawn
var selectedSpawnPoints = WayPoint.SelectCrewSpawnPoints(characterInfos, respawnSub);
if (isPvPMode && Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
{
var spawnWaypoints = WayPoint.GetOutpostSpawnPoints(teamID);
for (int i = 0; i < characterInfos.Count; i++)
{
selectedSpawnPoints[i] = spawnWaypoints.GetRandomUnsynced();
}
}
anyCharacterSpawnedInShuttle = false;
//the spawnpoints where they would spawn if they were spawned inside the main sub
//(in order to give them appropriate ID card tags)
var mainSubSpawnPoints = mainSub != null ? WayPoint.SelectCrewSpawnPoints(characterInfos, mainSub) : null;
var cargoSp = WayPoint.WayPointList.Find(wp => wp.Submarine == respawnSub && wp.SpawnType == SpawnType.Cargo);
for (int i = 0; i < characterInfos.Count; i++)
{
bool bot = i >= clients.Count;
var characterInfo = characterInfos[i];
characterInfos[i].ClearCurrentOrders();
bool bot = botsToSpawn.Contains(characterInfo);
characterInfo.ClearCurrentOrders();
CharacterCampaignData characterCampaignData = null;
bool forceSpawnInMainSub = false;
@@ -413,7 +443,7 @@ namespace Barotrauma.Networking
{
//the client has opted to change the name of their new character
//when the character spawns, set the client's name to match
if (clients[i].PendingName == characterInfos[i].Name)
if (clients[i].PendingName == characterInfo.Name)
{
GameMain.Server?.TryChangeClientName(clients[i], clients[i].PendingName);
clients[i].PendingName = null;
@@ -428,32 +458,31 @@ namespace Barotrauma.Networking
}
else
{
ReduceCharacterSkillsOnDeath(characterInfos[i]);
characterInfos[i].RemoveSavedStatValuesOnDeath();
characterInfos[i].CauseOfDeath = null;
ReduceCharacterSkillsOnDeath(characterInfo);
characterInfo.RemoveSavedStatValuesOnDeath();
characterInfo.CauseOfDeath = null;
}
}
}
if (!forceSpawnInMainSub)
if (!forceSpawnInMainSub && respawnShuttle != null)
{
anyCharacterSpawnedInShuttle = true;
anyCharacterSpawnedInShuttle = true;
}
var character = Character.Create(characterInfos[i], (forceSpawnInMainSub ? mainSubSpawnPoints[i] : shuttleSpawnPoints[i]).WorldPosition, characterInfos[i].Name, isRemotePlayer: !bot, hasAi: bot);
var character = Character.Create(characterInfo, (forceSpawnInMainSub ? mainSubSpawnPoints[i] : selectedSpawnPoints[i]).WorldPosition, characterInfo.Name, isRemotePlayer: !bot, hasAi: bot);
characterCampaignData?.ApplyWalletData(character);
character.TeamID = CharacterTeamType.Team1;
character.LoadTalents();
if (characterInfos[i].LastRewardDistribution.TryUnwrap(out int salary))
if (characterInfo.LastRewardDistribution.TryUnwrap(out int salary))
{
character.Wallet.SetRewardDistribution(salary);
}
respawnedCharacters.Add(character);
teamSpecificState.RespawnedCharacters.Add(character);
if (bot)
{
GameServer.Log(string.Format("Respawning bot {0} as {1}", character.Info.Name, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning);
GameServer.Log(string.Format("Respawning bot {0} as {1}.", character.Info.Name, characterInfo.Job.Name), ServerLog.MessageType.Spawning);
}
else
{
@@ -475,11 +504,18 @@ namespace Barotrauma.Networking
clients[i].Character = character;
character.SetOwnerClient(clients[i]);
GameServer.Log(
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfos[i].Job.Name}", ServerLog.MessageType.Spawning);
$"Respawning {GameServer.ClientLogName(clients[i])} ({clients[i].Connection.Endpoint}) as {characterInfo.Job.Name}.", ServerLog.MessageType.Spawning);
}
if (RespawnShuttle != null && anyCharacterSpawnedInShuttle)
if (respawnShuttle != null && anyCharacterSpawnedInShuttle)
{
GameServer.Log($"Dispatching the {GetRespawnShuttleText(teamID)}.", ServerLog.MessageType.Spawning);
respawnShuttle.SetPosition(shuttlePos.Value);
respawnShuttle.Velocity = Vector2.Zero;
respawnShuttle.NeutralizeBallast();
respawnShuttle.EnableMaintainPosition();
shuttleSteering[teamID].ForEach(s => s.TargetVelocity = Vector2.Zero);
List<Item> newRespawnItems = new List<Item>();
Vector2 pos = cargoSp?.Position ?? character.Position;
if (divingSuitPrefab != null)
@@ -513,25 +549,29 @@ namespace Barotrauma.Networking
}
}
}
if (respawnContainer != null)
{
AutoItemPlacer.RegenerateLoot(RespawnShuttle, respawnContainer);
}
//try to put the items in containers in the shuttle
foreach (var respawnItem in newRespawnItems)
{
System.Diagnostics.Debug.Assert(!respawnItem.Removed);
foreach (Item shuttleItem in RespawnShuttle.GetItems(alsoFromConnectedSubs: false))
//already in a container (a battery we just placed in a scooter?) -> don't move to a cabinet
if (respawnItem.Container == null)
{
if (shuttleItem.NonInteractable || shuttleItem.NonPlayerTeamInteractable) { continue; }
var container = shuttleItem.GetComponent<ItemContainer>();
if (container != null && container.Inventory.TryPutItem(respawnItem, user: null))
foreach (Item shuttleItem in respawnShuttle.GetItems(alsoFromConnectedSubs: false))
{
break;
if (shuttleItem.NonInteractable || shuttleItem.NonPlayerTeamInteractable) { continue; }
var container = shuttleItem.GetComponent<ItemContainer>();
if (container != null && container.Inventory.TryPutItem(respawnItem, user: null))
{
break;
}
}
}
respawnItems.Add(respawnItem);
teamSpecificState.RespawnItems.Add(respawnItem);
}
foreach (var respawnContainer in respawnContainers[teamID])
{
teamSpecificState.RespawnItems.AddRange(AutoItemPlacer.RegenerateLoot(respawnShuttle, respawnContainer));
}
}
@@ -541,10 +581,11 @@ namespace Barotrauma.Networking
{
ReduceCharacterSkillsOnDeath(characterInfos[i], applyExtraSkillLoss: true);
}
WayPoint jobItemSpawnPoint = mainSubSpawnPoints != null ? mainSubSpawnPoints[i] : selectedSpawnPoints[i];
if (characterData == null || characterData.HasSpawned)
{
//give the character the items they would've gotten if they had spawned in the main sub
character.GiveJobItems(mainSubSpawnPoints[i]);
character.GiveJobItems(isPvPMode, jobItemSpawnPoint);
if (campaign != null)
{
characterData = campaign.SetClientCharacterData(clients[i]);
@@ -559,16 +600,17 @@ namespace Barotrauma.Networking
}
else
{
character.GiveJobItems(mainSubSpawnPoints[i]);
character.GiveJobItems(isPvPMode, jobItemSpawnPoint);
}
characterData.ApplyHealthData(character);
character.GiveIdCardTags(mainSubSpawnPoints[i]);
character.GiveIdCardTags(jobItemSpawnPoint);
characterData.HasSpawned = true;
}
//add the ID card tags they should've gotten when spawning in the shuttle
character.GiveIdCardTags(shuttleSpawnPoints[i], createNetworkEvent: true);
character.GiveIdCardTags(selectedSpawnPoints[i], createNetworkEvent: true);
}
}
/// <summary>
@@ -605,24 +647,29 @@ namespace Barotrauma.Networking
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.WriteRangedInteger((int)CurrentState, 0, Enum.GetNames(typeof(State)).Length);
switch (CurrentState)
msg.WriteByte((byte)c.TeamID);
foreach (var teamSpecificState in teamSpecificStates.Values)
{
case State.Transporting:
msg.WriteBoolean(ReturnCountdownStarted);
msg.WriteSingle(GameMain.Server.ServerSettings.MaxTransportTime);
msg.WriteSingle((float)(ReturnTime - DateTime.Now).TotalSeconds);
break;
case State.Waiting:
msg.WriteUInt16((ushort)pendingRespawnCount);
msg.WriteUInt16((ushort)requiredRespawnCount);
msg.WriteBoolean(IsRespawnDecisionPendingForClient(c));
msg.WriteBoolean(RespawnCountdownStarted);
msg.WriteSingle((float)(RespawnTime - DateTime.Now).TotalSeconds);
break;
case State.Returning:
break;
msg.WriteByte((byte)teamSpecificState.TeamID);
msg.WriteRangedInteger((int)teamSpecificState.CurrentState, 0, Enum.GetNames(typeof(State)).Length);
switch (teamSpecificState.CurrentState)
{
case State.Transporting:
msg.WriteBoolean(teamSpecificState.ReturnCountdownStarted);
msg.WriteSingle(GameMain.Server.ServerSettings.MaxTransportTime);
msg.WriteSingle((float)(teamSpecificState.ReturnTime - DateTime.Now).TotalSeconds);
break;
case State.Waiting:
msg.WriteUInt16((ushort)teamSpecificState.PendingRespawnCount);
msg.WriteUInt16((ushort)teamSpecificState.RequiredRespawnCount);
msg.WriteBoolean(IsRespawnDecisionPendingForClient(c));
msg.WriteBoolean(teamSpecificState.RespawnCountdownStarted);
msg.WriteSingle((float)(teamSpecificState.RespawnTime - DateTime.Now).TotalSeconds);
break;
case State.Returning:
break;
}
}
msg.WritePadBits();
@@ -2,9 +2,9 @@
using Barotrauma.IO;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Steam;
namespace Barotrauma.Networking
{
@@ -106,6 +106,7 @@ namespace Barotrauma.Networking
if (requiredFlags.HasFlag(NetFlags.Properties))
{
WriteExtraCargo(outMsg);
WritePerks(outMsg);
}
if (requiredFlags.HasFlag(NetFlags.HiddenSubs))
@@ -129,6 +130,40 @@ namespace Barotrauma.Networking
}
}
public void ReadPerks(IReadMessage incMsg, Client c)
{
if (!HasPermissionToChangePerks(c)) return;
bool changed = ReadPerks(incMsg);
if (!changed) { return; }
UpdateFlag(NetFlags.Properties);
SaveSettings();
GameMain.NetLobbyScreen.LastUpdateID++;
static bool HasPermissionToChangePerks(Client client)
{
if (client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return true; }
bool isPvP = GameMain.NetLobbyScreen?.SelectedMode == GameModePreset.PvP;
bool hasSelectedTeam = client.PreferredTeam is CharacterTeamType.Team1 or CharacterTeamType.Team2;
var otherClients = GameMain.NetworkMember?.ConnectedClients?.Where(c => c != client).ToImmutableArray() ?? ImmutableArray<Client>.Empty;
if (isPvP)
{
if (!hasSelectedTeam) { return false; }
return !otherClients
.Where(c => c.PreferredTeam == client.PreferredTeam)
.Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings));
}
else
{
return !otherClients.Any(static c => c.HasPermission(Networking.ClientPermissions.ManageSettings));
}
}
}
public void ServerRead(IReadMessage incMsg, Client c)
{
if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return;
@@ -136,7 +171,7 @@ namespace Barotrauma.Networking
NetFlags flags = (NetFlags)incMsg.ReadByte();
bool changed = false;
if (flags.HasFlag(NetFlags.Properties))
{
bool propertiesChanged = ReadExtraCargo(incMsg);
@@ -176,6 +211,7 @@ namespace Barotrauma.Networking
if (propertiesChanged)
{
UpdateFlag(NetFlags.Properties);
GameMain.Server.RefreshPvpTeamAssignments(); // the changed settings might be relevant to team logic, so refresh
}
changed |= propertiesChanged;
}
@@ -189,9 +225,18 @@ namespace Barotrauma.Networking
if (flags.HasFlag(NetFlags.Misc))
{
int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits);
List<Identifier> missionTypes = new List<Identifier>(GameMain.NetLobbyScreen.MissionTypes);
Identifier addedMissionType = incMsg.ReadIdentifier();
Identifier removedMissionType = incMsg.ReadIdentifier();
if (!addedMissionType.IsEmpty)
{
missionTypes.Add(addedMissionType);
}
if (!removedMissionType.IsEmpty)
{
missionTypes.Remove(removedMissionType);
}
GameMain.NetLobbyScreen.MissionTypes = missionTypes;
//the byte indicates the direction we're changing the value, subtract one to get negative values from a byte
TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1;
@@ -361,28 +406,20 @@ namespace Barotrauma.Networking
if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Range<int>(min, max)); }
}
AllowedRandomMissionTypes = new List<MissionType>();
string[] allowedMissionTypeNames = doc.Root.GetAttributeStringArray(
"AllowedRandomMissionTypes", Enum.GetValues(typeof(MissionType)).Cast<MissionType>().Select(m => m.ToString()).ToArray());
foreach (string missionTypeName in allowedMissionTypeNames)
{
if (Enum.TryParse(missionTypeName, out MissionType missionType))
{
if (missionType == Barotrauma.MissionType.None) { continue; }
if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) { continue; }
AllowedRandomMissionTypes.Add(missionType);
}
}
AllowedRandomMissionTypes = doc.Root.GetAttributeIdentifierArray(
"AllowedRandomMissionTypes", MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToArray()).ToList();
ServerName = doc.Root.GetAttributeString("name", "");
if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); }
ServerMessageText = doc.Root.GetAttributeString("ServerMessage", "");
GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier;
//handle Random as the mission type, which is no longer a valid setting
//MissionType.All offers equivalent functionality
if (MissionType == "Random") { MissionType = "All"; }
GameMain.NetLobbyScreen.MissionTypeName = MissionType;
if (AllowedRandomMissionTypes.Contains(Tags.MissionTypeAll))
{
AllowedRandomMissionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList();
}
AllowedRandomMissionTypes = AllowedRandomMissionTypes.Distinct().ToList();
ValidateMissionTypes();
GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode);
GameMain.NetLobbyScreen.SetBotCount(BotCount);
@@ -404,6 +441,20 @@ namespace Barotrauma.Networking
CampaignSettings = new CampaignSettings(element);
}
}
HashSet<Identifier> selectedCoalitionPerks = SelectedCoalitionPerks.ToHashSet();
HashSet<Identifier> selectedSeparatistsPerks = SelectedSeparatistsPerks.ToHashSet();
foreach (DisembarkPerkPrefab prefab in DisembarkPerkPrefab.Prefabs)
{
if (prefab.Cost == 0)
{
selectedSeparatistsPerks.Add(prefab.Identifier);
selectedCoalitionPerks.Add(prefab.Identifier);
}
}
SelectedCoalitionPerks = selectedCoalitionPerks.ToArray();
SelectedSeparatistsPerks = selectedSeparatistsPerks.ToArray();
}
public string SelectNonHiddenSubmarine(string current = null)
@@ -374,13 +374,32 @@ namespace Barotrauma
msg.WriteBoolean(GameMain.Server.ServerSettings.AllowSubVoting);
if (GameMain.Server.ServerSettings.AllowSubVoting)
{
IReadOnlyDictionary<SubmarineInfo, int> voteList = GetVoteCounts<SubmarineInfo>(VoteType.Sub, GameMain.Server.ConnectedClients);
bool isMultiSub = GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP;
msg.WriteBoolean(isMultiSub);
var subVoters = isMultiSub ?
GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team1) :
GameMain.Server.ConnectedClients;
IReadOnlyDictionary<SubmarineInfo, int> voteList = GetVoteCounts<SubmarineInfo>(VoteType.Sub, subVoters);
msg.WriteByte((byte)voteList.Count);
foreach (KeyValuePair<SubmarineInfo, int> vote in voteList)
{
msg.WriteByte((byte)vote.Value);
msg.WriteString(vote.Key.Name);
}
if (isMultiSub)
{
var separatistsVotes = GetVoteCounts<SubmarineInfo>(VoteType.Sub, GameMain.Server.ConnectedClients.Where(static c => c.PreferredTeam is CharacterTeamType.Team2));
msg.WriteByte((byte)separatistsVotes.Count);
foreach (var (info, amount) in separatistsVotes)
{
msg.WriteByte((byte)amount);
msg.WriteString(info.Name);
}
}
}
msg.WriteBoolean(GameMain.Server.ServerSettings.AllowModeVoting);
if (GameMain.Server.ServerSettings.AllowModeVoting)
@@ -1,7 +1,9 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
@@ -9,6 +11,7 @@ namespace Barotrauma
partial class NetLobbyScreen : Screen
{
private SubmarineInfo selectedSub;
private SubmarineInfo selectedEnemySub;
private SubmarineInfo selectedShuttle;
public bool RadiationEnabled = true;
@@ -26,6 +29,18 @@ namespace Barotrauma
}
}
}
[MaybeNull, AllowNull]
public SubmarineInfo SelectedEnemySub
{
get => selectedEnemySub;
set
{
selectedEnemySub = value;
lastUpdateID++;
}
}
public SubmarineInfo SelectedShuttle
{
get { return selectedShuttle; }
@@ -81,31 +96,19 @@ namespace Barotrauma
get { return GameModes[SelectedModeIndex]; }
}
private MissionType missionType;
public MissionType MissionType
public IEnumerable<Identifier> MissionTypes
{
get { return missionType; }
get { return GameMain.NetworkMember.ServerSettings.AllowedRandomMissionTypes; }
set
{
lastUpdateID++;
missionType = value;
if (GameMain.NetworkMember?.ServerSettings != null)
{
GameMain.NetworkMember.ServerSettings.MissionType = missionType.ToString();
GameMain.NetworkMember.ServerSettings.MissionTypes = string.Join(",", value.Select(t => t.ToIdentifier()));
}
}
}
public string MissionTypeName
{
get { return missionType.ToString(); }
set
{
Enum.TryParse(value, out MissionType type);
MissionType = type;
}
}
public NetLobbyScreen()
{
LevelSeed = ToolBox.RandomSeed(8);
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Networking;
@@ -273,8 +273,11 @@ namespace Barotrauma
for (int i = 0; i < amountToChoose; i++)
{
var traitor = viableTraitors.GetRandomUnsynced();
viableTraitors.Remove(traitor);
traitors.Add(traitor);
if (traitor != null)
{
viableTraitors.Remove(traitor);
traitors.Add(traitor);
}
}
return traitors;
}
@@ -388,7 +391,7 @@ namespace Barotrauma
{
if (level?.LevelData is { Type: LevelData.LevelType.LocationConnection })
{
if (Submarine.MainSub.WorldPosition.X > level.Size.X / 2)
if (Submarine.MainSub != null && Submarine.MainSub.WorldPosition.X > level.Size.X / 2)
{
//try starting ASAP if the submarine is already half-way through the level
//(brief delay regardless, because otherwise we might retry every frame if finding a suitable event fails below)