Merge remote-tracking branch 'upstream/master' into develop
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -26,6 +27,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.Server is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
|
||||
GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign &&
|
||||
causeOfDeath != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
ownerClient.SpectateOnly = true;
|
||||
CharacterCampaignData matchingData = mpCampaign.GetClientCharacterData(ownerClient);
|
||||
if (matchingData != null)
|
||||
{
|
||||
matchingData.ApplyPermadeath();
|
||||
|
||||
if (GameMain.Server is { ServerSettings.IronmanMode: true })
|
||||
{
|
||||
mpCampaign.SaveSingleCharacter(matchingData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (HasAbilityFlag(AbilityFlags.RetainExperienceForNewCharacter))
|
||||
{
|
||||
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
|
||||
@@ -96,6 +96,7 @@ namespace Barotrauma
|
||||
|
||||
msg.WriteInt32(ExperiencePoints);
|
||||
msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints);
|
||||
msg.WriteBoolean(PermanentlyDead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,7 +964,7 @@ namespace Barotrauma
|
||||
}
|
||||
client.Muted = true;
|
||||
GameMain.Server.SendDirectChatMessage(TextManager.Get("MutedByServer").Value, client, ChatMessageType.MessageBox);
|
||||
},
|
||||
},
|
||||
() =>
|
||||
{
|
||||
if (GameMain.Server == null) return null;
|
||||
@@ -1635,6 +1635,19 @@ namespace Barotrauma
|
||||
NewMessage("Disabled RequireClientNameMatch");
|
||||
}
|
||||
}));
|
||||
|
||||
AssignOnExecute("debugvoip", _ =>
|
||||
{
|
||||
VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip;
|
||||
NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), Color.White);
|
||||
});
|
||||
|
||||
AssignOnClientRequestExecute("debugvoip", (client, _, _) =>
|
||||
{
|
||||
VoipServerDecoder.DebugVoip = !VoipServerDecoder.DebugVoip;
|
||||
NewMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled") + " by " + client.Name, Color.White);
|
||||
GameMain.Server.SendConsoleMessage("Debugging voice chat is now " + (VoipServerDecoder.DebugVoip ? "enabled" : "disabled"), client);
|
||||
});
|
||||
#endif
|
||||
|
||||
AssignOnClientRequestExecute(
|
||||
@@ -1797,11 +1810,7 @@ namespace Barotrauma
|
||||
"teleportcharacter|teleport",
|
||||
(Client client, Vector2 cursorWorldPos, string[] args) =>
|
||||
{
|
||||
Character tpCharacter = (args.Length == 0) ? client.Character : FindMatchingCharacter(args, false);
|
||||
if (tpCharacter != null)
|
||||
{
|
||||
tpCharacter.TeleportTo(cursorWorldPos);
|
||||
}
|
||||
TeleportCharacter(cursorWorldPos, client.Character, args);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1958,6 +1967,17 @@ namespace Barotrauma
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.Character != revivedCharacter) { continue; }
|
||||
|
||||
// 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 (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
|
||||
{
|
||||
characterToRestore.CharacterInfo.PermanentlyDead = false;
|
||||
mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
|
||||
}
|
||||
}
|
||||
|
||||
//clients stop controlling the character when it dies, force control back
|
||||
GameMain.Server.SetClientCharacter(c, revivedCharacter);
|
||||
@@ -2581,6 +2601,91 @@ namespace Barotrauma
|
||||
}
|
||||
);
|
||||
|
||||
commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) =>
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
NewMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
NewMessage("No campaign active.", Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out int amount))
|
||||
{
|
||||
NewMessage($"{args[0]} is not a valid amount.", Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mpCampaign.Bank.SetRewardDistribution(amount);
|
||||
NewMessage($"Set the default salary to {amount}%", Color.White);
|
||||
return;
|
||||
}
|
||||
|
||||
Character character = FindMatchingCharacter(args.Skip(1).ToArray());
|
||||
if (character is null)
|
||||
{
|
||||
NewMessage($"Character not found \"{args[1]}\".", Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
character.Wallet.SetRewardDistribution(amount);
|
||||
NewMessage($"Set {character.Name}'s salary to {amount}%", Color.White);
|
||||
}));
|
||||
|
||||
AssignOnClientRequestExecute(
|
||||
"setsalary",
|
||||
(senderClient, cursorWorldPos, args) =>
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage($"Missing arguments. Expected at least 2 but got {args.Length} (amount, character)", senderClient, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CampaignMode.AllowedToManageWallets(senderClient))
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage("You are not allowed to manage wallets.", senderClient, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage("No campaign active.", senderClient, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out int amount))
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage($"{args[0]} is not a valid amount.", senderClient, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args[1].Equals("default", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mpCampaign.Bank.SetRewardDistribution(amount);
|
||||
GameMain.Server.SendConsoleMessage($"Set the default salary to {amount}%", senderClient);
|
||||
return;
|
||||
}
|
||||
|
||||
Character character = FindMatchingCharacter(args.Skip(1).ToArray());
|
||||
if (character is null)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage($"Character not found \"{args[1]}\".", senderClient, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
character.Wallet.SetRewardDistribution(amount);
|
||||
GameMain.Server.SendConsoleMessage($"Set {character.Name}'s salary to {amount}%.", senderClient);
|
||||
}
|
||||
);
|
||||
|
||||
commands.Add(new Command("readycheck", "Commence a ready check.", (string[] args) =>
|
||||
{
|
||||
if (Screen.Selected == GameMain.GameScreen && GameMain.NetworkMember != null)
|
||||
@@ -2643,7 +2748,7 @@ namespace Barotrauma
|
||||
}));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
public static void ServerRead(IReadMessage inc, Client sender)
|
||||
{
|
||||
string consoleCommand = inc.ReadString();
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class AlienRuinMission : Mission
|
||||
partial class EliminateTargetsMission : Mission
|
||||
{
|
||||
public override void ServerWriteInitial(IWriteMessage msg, Client c)
|
||||
{
|
||||
+14
-2
@@ -123,6 +123,12 @@ namespace Barotrauma
|
||||
|
||||
public bool IsDuplicate(CharacterCampaignData other)
|
||||
{
|
||||
#if DEBUG
|
||||
if (RequireClientNameMatch)
|
||||
{
|
||||
return AccountId == other.AccountId && other.ClientAddress == ClientAddress && Name == other.Name;
|
||||
}
|
||||
#endif
|
||||
return AccountId == other.AccountId && other.ClientAddress == ClientAddress;
|
||||
}
|
||||
|
||||
@@ -133,6 +139,13 @@ namespace Barotrauma
|
||||
WalletData = null;
|
||||
}
|
||||
|
||||
public void ApplyPermadeath()
|
||||
{
|
||||
Reset();
|
||||
CharacterInfo.PermanentlyDead = true;
|
||||
DebugConsole.NewMessage($"Permadeath applied on {Name}'s CharacterCampaignData.CharacterInfo.");
|
||||
}
|
||||
|
||||
public void SpawnInventoryItems(Character character, Inventory inventory)
|
||||
{
|
||||
if (character == null)
|
||||
@@ -158,7 +171,7 @@ namespace Barotrauma
|
||||
|
||||
public void ApplyWalletData(Character character)
|
||||
{
|
||||
character.Wallet = new Wallet(Option<Character>.Some(character), WalletData);
|
||||
character.Wallet = new Wallet(Option.Some(character), WalletData);
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
@@ -167,7 +180,6 @@ namespace Barotrauma
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("address", ClientAddress),
|
||||
new XAttribute("accountid", AccountId.TryUnwrap(out var accountId) ? accountId.StringRepresentation : ""));
|
||||
|
||||
CharacterInfo?.Save(element);
|
||||
if (itemData != null) { element.Add(itemData); }
|
||||
if (healthData != null) { element.Add(healthData); }
|
||||
|
||||
+109
-20
@@ -16,6 +16,12 @@ namespace Barotrauma
|
||||
private readonly HashSet<NetWalletTransaction> transactions = new HashSet<NetWalletTransaction>();
|
||||
private const float clientCheckInterval = 10;
|
||||
private float clientCheckTimer = clientCheckInterval;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary backup storage for characters that have been overwritten by SaveSingleCharacter, this will be gone
|
||||
/// once the round ends or the server closes. Currently needed to enable the console command "revive" in ironman mode.
|
||||
/// </summary>
|
||||
public List<CharacterCampaignData> replacedCharacterDataBackup = new List<CharacterCampaignData>();
|
||||
|
||||
public override Wallet GetWallet(Client client = null)
|
||||
{
|
||||
@@ -295,6 +301,12 @@ namespace Barotrauma
|
||||
MoveDiscardedCharacterBalancesToBank();
|
||||
|
||||
characterData.ForEach(cd => cd.HasSpawned = false);
|
||||
foreach (var cd in characterData)
|
||||
{
|
||||
//remove from crewmanager - we don't need to save the data there if it's been saved as CharacterCampaignData
|
||||
//(e.g. if a client has taken over a bot, we need to do this to prevent it being saved twice)
|
||||
CrewManager.RemoveCharacterInfo(cd.CharacterInfo);
|
||||
}
|
||||
|
||||
SavePets();
|
||||
|
||||
@@ -380,6 +392,9 @@ namespace Barotrauma
|
||||
GameMain.GameSession.EventManager.RegisterEventHistory();
|
||||
}
|
||||
|
||||
//store the currently active missions at this point so we can communicate their states to clients, they're cleared in EndRound
|
||||
List<Mission> missions = GameMain.GameSession.Missions.ToList();
|
||||
|
||||
GameMain.GameSession.EndRound("", transitionType);
|
||||
|
||||
//--------------------------------------
|
||||
@@ -407,7 +422,7 @@ namespace Barotrauma
|
||||
|
||||
//--------------------------------------
|
||||
|
||||
GameMain.Server.EndGame(transitionType, wasSaved: true);
|
||||
GameMain.Server.EndGame(transitionType, wasSaved: true, missions);
|
||||
|
||||
ForceMapUI = false;
|
||||
|
||||
@@ -513,6 +528,9 @@ namespace Barotrauma
|
||||
Map?.Radiation?.UpdateRadiation(deltaTime);
|
||||
|
||||
base.Update(deltaTime);
|
||||
|
||||
MedicalClinic?.Update(deltaTime);
|
||||
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
|
||||
@@ -1165,42 +1183,59 @@ namespace Barotrauma
|
||||
|
||||
if (!AllowedToManageWallets(sender)) { return; }
|
||||
|
||||
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == update.Target);
|
||||
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution);
|
||||
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name ?? "the bank"} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
|
||||
if (update.Target.TryUnwrap(out ushort id))
|
||||
{
|
||||
Character targetCharacter = Character.CharacterList.FirstOrDefault(c => c.ID == id);
|
||||
targetCharacter?.Wallet.SetRewardDistribution(update.NewRewardDistribution);
|
||||
GameServer.Log($"{sender.Name} changed the salary of {targetCharacter?.Name} to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
|
||||
return;
|
||||
}
|
||||
|
||||
Bank.SetRewardDistribution(update.NewRewardDistribution);
|
||||
GameServer.Log($"{sender.Name} changed the default salary to {update.NewRewardDistribution}%.", ServerLog.MessageType.Money);
|
||||
}
|
||||
|
||||
public void ResetSalaries(Client sender)
|
||||
{
|
||||
if (!AllowedToManageWallets(sender)) { return; }
|
||||
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Player))
|
||||
{
|
||||
character.Wallet.SetRewardDistribution(Bank.RewardDistribution);
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerReadCrew(IReadMessage msg, Client sender)
|
||||
{
|
||||
int[] pendingHires = null;
|
||||
UInt16[] pendingHires = null;
|
||||
|
||||
bool updatePending = msg.ReadBoolean();
|
||||
if (updatePending)
|
||||
{
|
||||
ushort pendingHireLength = msg.ReadUInt16();
|
||||
pendingHires = new int[pendingHireLength];
|
||||
pendingHires = new UInt16[pendingHireLength];
|
||||
for (int i = 0; i < pendingHireLength; i++)
|
||||
{
|
||||
pendingHires[i] = msg.ReadInt32();
|
||||
pendingHires[i] = msg.ReadUInt16();
|
||||
}
|
||||
}
|
||||
|
||||
bool validateHires = msg.ReadBoolean();
|
||||
|
||||
bool renameCharacter = msg.ReadBoolean();
|
||||
int renamedIdentifier = -1;
|
||||
UInt16 renamedIdentifier = 0;
|
||||
string newName = null;
|
||||
bool existingCrewMember = false;
|
||||
if (renameCharacter)
|
||||
{
|
||||
renamedIdentifier = msg.ReadInt32();
|
||||
renamedIdentifier = msg.ReadUInt16();
|
||||
newName = msg.ReadString();
|
||||
existingCrewMember = msg.ReadBoolean();
|
||||
}
|
||||
|
||||
bool fireCharacter = msg.ReadBoolean();
|
||||
int firedIdentifier = -1;
|
||||
if (fireCharacter) { firedIdentifier = msg.ReadInt32(); }
|
||||
if (fireCharacter) { firedIdentifier = msg.ReadUInt16(); }
|
||||
|
||||
Location location = map?.CurrentLocation;
|
||||
CharacterInfo firedCharacter = null;
|
||||
@@ -1209,7 +1244,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (fireCharacter)
|
||||
{
|
||||
firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifier() == firedIdentifier);
|
||||
firedCharacter = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == firedIdentifier);
|
||||
if (firedCharacter != null && (firedCharacter.Character?.IsBot ?? true))
|
||||
{
|
||||
CrewManager.FireCharacter(firedCharacter);
|
||||
@@ -1225,11 +1260,11 @@ namespace Barotrauma
|
||||
CharacterInfo characterInfo = null;
|
||||
if (existingCrewMember && CrewManager != null)
|
||||
{
|
||||
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier);
|
||||
characterInfo = CrewManager.GetCharacterInfos().FirstOrDefault(info => info.ID == renamedIdentifier);
|
||||
}
|
||||
else if(!existingCrewMember && location.HireManager != null)
|
||||
{
|
||||
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == renamedIdentifier);
|
||||
characterInfo = location.HireManager.AvailableCharacters.FirstOrDefault(info => info.ID == renamedIdentifier);
|
||||
}
|
||||
|
||||
if (characterInfo != null && (characterInfo.Character?.IsBot ?? true))
|
||||
@@ -1262,9 +1297,9 @@ namespace Barotrauma
|
||||
if (updatePending)
|
||||
{
|
||||
List<CharacterInfo> pendingHireInfos = new List<CharacterInfo>();
|
||||
foreach (int identifier in pendingHires)
|
||||
foreach (UInt16 identifier in pendingHires)
|
||||
{
|
||||
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.GetIdentifierUsingOriginalName() == identifier);
|
||||
CharacterInfo match = location.GetHireableCharacters().FirstOrDefault(info => info.ID == identifier);
|
||||
if (match == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Tried to add a character that doesn't exist ({identifier}) to pending hires");
|
||||
@@ -1311,7 +1346,7 @@ namespace Barotrauma
|
||||
/// the client and the server when there's only one person on the server but when a second person joins both of
|
||||
/// their available hires are different from the server.
|
||||
/// </remarks>
|
||||
public void SendCrewState((int id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null)
|
||||
public void SendCrewState((ushort id, string newName) renamedCrewMember = default, CharacterInfo firedCharacter = null, bool createNotification = true)
|
||||
{
|
||||
List<CharacterInfo> availableHires = new List<CharacterInfo>();
|
||||
List<CharacterInfo> pendingHires = new List<CharacterInfo>();
|
||||
@@ -1327,6 +1362,8 @@ namespace Barotrauma
|
||||
IWriteMessage msg = new WriteOnlyMessage();
|
||||
msg.WriteByte((byte)ServerPacketHeader.CREW);
|
||||
|
||||
msg.WriteBoolean(createNotification);
|
||||
|
||||
msg.WriteUInt16((ushort)availableHires.Count);
|
||||
foreach (CharacterInfo hire in availableHires)
|
||||
{
|
||||
@@ -1337,7 +1374,7 @@ namespace Barotrauma
|
||||
msg.WriteUInt16((ushort)pendingHires.Count);
|
||||
foreach (CharacterInfo pendingHire in pendingHires)
|
||||
{
|
||||
msg.WriteInt32(pendingHire.GetIdentifierUsingOriginalName());
|
||||
msg.WriteUInt16(pendingHire.ID);
|
||||
}
|
||||
|
||||
var hiredCharacters = CrewManager.GetCharacterInfos().Where(ci => ci.IsNewHire);
|
||||
@@ -1348,16 +1385,16 @@ namespace Barotrauma
|
||||
msg.WriteInt32(info.Salary);
|
||||
}
|
||||
|
||||
bool validRenaming = renamedCrewMember.id > -1 && !string.IsNullOrEmpty(renamedCrewMember.newName);
|
||||
bool validRenaming = renamedCrewMember.id > 0 && !string.IsNullOrEmpty(renamedCrewMember.newName);
|
||||
msg.WriteBoolean(validRenaming);
|
||||
if (validRenaming)
|
||||
{
|
||||
msg.WriteInt32(renamedCrewMember.id);
|
||||
msg.WriteUInt16(renamedCrewMember.id);
|
||||
msg.WriteString(renamedCrewMember.newName);
|
||||
}
|
||||
|
||||
msg.WriteBoolean(firedCharacter != null);
|
||||
if (firedCharacter != null) { msg.WriteInt32(firedCharacter.GetIdentifier()); }
|
||||
if (firedCharacter != null) { msg.WriteUInt16(firedCharacter.ID); }
|
||||
|
||||
GameMain.Server.ServerPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
@@ -1475,5 +1512,57 @@ namespace Barotrauma
|
||||
lastSaveID++;
|
||||
DebugConsole.Log("Campaign saved, save ID " + lastSaveID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load the current character save file and add/replace a single character's data with a new version immediately.
|
||||
/// </summary>
|
||||
/// <param name="newData">New character to insert. If it matches one existing in the save, that will get replaced.</param>
|
||||
/// <param name="skipBackup">By default, replaced characters will be temporarily backed up, but that might be unwanted
|
||||
/// 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();
|
||||
if (!File.Exists(characterDataPath))
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
|
||||
}
|
||||
else
|
||||
{
|
||||
var loadedCharacterData = XMLExtensions.TryLoadXml(characterDataPath);
|
||||
if (loadedCharacterData?.Root == null) { return; }
|
||||
var oldData = loadedCharacterData.Root.Elements()
|
||||
.FirstOrDefault(subElement => new CharacterCampaignData(subElement).IsDuplicate(newData));
|
||||
|
||||
if (oldData != null)
|
||||
{
|
||||
if (!skipBackup)
|
||||
{
|
||||
replacedCharacterDataBackup.Add(new CharacterCampaignData(oldData));
|
||||
}
|
||||
oldData.Remove();
|
||||
}
|
||||
loadedCharacterData.Root.Add(newData.Save());
|
||||
|
||||
try
|
||||
{
|
||||
loadedCharacterData.SaveSafe(characterDataPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterCampaignData RestoreSingleCharacterFromBackup(Client client)
|
||||
{
|
||||
if (replacedCharacterDataBackup.Find(cd => cd.MatchesClient(client)) is CharacterCampaignData characterToRestore)
|
||||
{
|
||||
replacedCharacterDataBackup.Remove(characterToRestore);
|
||||
return characterToRestore;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -22,6 +22,36 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<AfflictionSubscriber> afflictionSubscribers = new();
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
processAfflictionChangesTimer -= deltaTime;
|
||||
if (processAfflictionChangesTimer <= 0.0f)
|
||||
{
|
||||
foreach (var character in charactersWithAfflictionChanges)
|
||||
{
|
||||
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(character.CharacterHealth);
|
||||
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
|
||||
{
|
||||
if (sub.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
afflictionSubscribers.Remove(sub);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sub.Target == character.Info)
|
||||
{
|
||||
ServerSend(new NetCrewMember(character.Info, afflictions),
|
||||
header: NetworkHeader.AFFLICTION_UPDATE,
|
||||
deliveryMethod: DeliveryMethod.Unreliable,
|
||||
targetClient: sub.Subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
charactersWithAfflictionChanges.Clear();
|
||||
processAfflictionChangesTimer = ProcessAfflictionChangesInterval;
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerRead(IReadMessage inc, Client sender)
|
||||
{
|
||||
NetworkHeader header = (NetworkHeader)inc.ReadByte();
|
||||
@@ -141,7 +171,7 @@ namespace Barotrauma
|
||||
if (foundInfo is { Character.CharacterHealth: { } health })
|
||||
{
|
||||
pendingAfflictions = GetAllAfflictions(health);
|
||||
infoId = foundInfo.GetIdentifierUsingOriginalName();
|
||||
infoId = foundInfo.ID;
|
||||
}
|
||||
|
||||
INetSerializableStruct writeCrewMember = new NetCrewMember
|
||||
|
||||
@@ -18,10 +18,13 @@ namespace Barotrauma.Items.Components
|
||||
private bool needsServerInitialization;
|
||||
|
||||
/// <summary>
|
||||
/// When in multiplayer and the circuit box is loaded from the players inventory,
|
||||
/// We only load the components from XML on server side since only the server has access to CharacterCampaignData
|
||||
/// and then send a network event syncing the loaded properties. But circuit box properties are too complex to
|
||||
/// sync using the existing syncing logic so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
|
||||
/// When in multiplayer and the circuit box are loaded from the player inventory,
|
||||
/// We only load the components from XML on the server side
|
||||
/// since only the server has access to CharacterCampaignData
|
||||
/// and then send a network event syncing the loaded properties.
|
||||
/// But circuit box properties are too complex to
|
||||
/// sync using the existing syncing logic,
|
||||
/// so we instead send the state using <see cref="CircuitBoxInitializeStateFromServerEvent"/>.
|
||||
/// </summary>
|
||||
public void MarkServerRequiredInitialization()
|
||||
=> needsServerInitialization = true;
|
||||
@@ -280,6 +283,15 @@ namespace Barotrauma.Items.Components
|
||||
CreateServerEvent(data with { Size = Vector2.Max(data.Size, CircuitBoxLabelNode.MinSize) });
|
||||
break;
|
||||
}
|
||||
case CircuitBoxOpcode.RenameConnections:
|
||||
{
|
||||
var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
|
||||
if (!CanAccessAndUnlocked(c)) { break; }
|
||||
|
||||
RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
|
||||
CreateServerEvent(data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
|
||||
}
|
||||
@@ -327,6 +339,7 @@ namespace Barotrauma.Items.Components
|
||||
Components: Components.Select(EventFromComponent).ToImmutableArray(),
|
||||
Wires: Wires.Select(EventFromWire).ToImmutableArray(),
|
||||
Labels: Labels.Select(EventFromLabel).ToImmutableArray(),
|
||||
LabelOverrides: InputOutputNodes.Select(EventFromLabelOverride).ToImmutableArray(),
|
||||
InputPos: inputPos,
|
||||
OutputPos: outputPos);
|
||||
|
||||
@@ -347,6 +360,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
static CircuitBoxServerAddLabelEvent EventFromLabel(CircuitBoxLabelNode label)
|
||||
=> new(label.ID, label.Position, label.Size, label.Color, label.HeaderText, label.BodyText);
|
||||
|
||||
static CircuitBoxRenameConnectionLabelsEvent EventFromLabelOverride(CircuitBoxInputOutputNode node)
|
||||
=> new(node.NodeType, node.ConnectionLabelOverrides.ToNetDictionary());
|
||||
}
|
||||
|
||||
// we don't care about updating the view on server
|
||||
|
||||
+13
-13
@@ -98,7 +98,7 @@ namespace Barotrauma.Items.Components
|
||||
//existing wire not in the list of new wires -> disconnect it
|
||||
if (!wires[i].Contains(existingWire))
|
||||
{
|
||||
if (existingWire.Locked)
|
||||
if (existingWire.Locked || existingWire.Item.IsLayerHidden)
|
||||
{
|
||||
//this should not be possible unless the client is running a modified version of the game
|
||||
GameServer.Log(GameServer.CharacterLogName(c.Character) + " attempted to disconnect a locked wire from " +
|
||||
@@ -166,18 +166,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
|
||||
{
|
||||
if (disconnectedWire.Connections[0] == null &&
|
||||
disconnectedWire.Connections[1] == null &&
|
||||
!clientSideDisconnectedWires.Contains(disconnectedWire) &&
|
||||
disconnectedWire.Item.ParentInventory == null)
|
||||
{
|
||||
disconnectedWire.Item.Drop(c.Character);
|
||||
GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory);
|
||||
}
|
||||
}
|
||||
|
||||
//go through new wires
|
||||
for (int i = 0; i < Connections.Count; i++)
|
||||
{
|
||||
@@ -205,6 +193,18 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Wire disconnectedWire in DisconnectedWires.ToList())
|
||||
{
|
||||
if (disconnectedWire.Connections[0] == null &&
|
||||
disconnectedWire.Connections[1] == null &&
|
||||
!clientSideDisconnectedWires.Contains(disconnectedWire) &&
|
||||
disconnectedWire.Item.ParentInventory == null)
|
||||
{
|
||||
disconnectedWire.Item.Drop(c.Character);
|
||||
GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
||||
|
||||
@@ -11,153 +11,42 @@ namespace Barotrauma
|
||||
{
|
||||
private readonly Dictionary<Client, List<ushort>[]> receivedItemIds = new Dictionary<Client, List<ushort>[]>();
|
||||
|
||||
public void ServerEventRead(IReadMessage msg, Client c)
|
||||
public void ServerEventRead(IReadMessage msg, Client sender)
|
||||
{
|
||||
if (!receivedItemIds.TryGetValue(c, out List<ushort>[] receivedItemIdsFromClient))
|
||||
// if the dictionary doesn't contain the client entry, create a new one
|
||||
if (!receivedItemIds.TryGetValue(sender, out List<ushort>[] receivedItemIdsFromClient))
|
||||
{
|
||||
receivedItemIdsFromClient = new List<ushort>[capacity];
|
||||
receivedItemIds.Add(c, receivedItemIdsFromClient);
|
||||
receivedItemIds.Add(sender, receivedItemIdsFromClient);
|
||||
}
|
||||
|
||||
// Read some item ids from the message. readyToApply waits for all the data from possible multiple messages.
|
||||
SharedRead(msg, receivedItemIdsFromClient, out bool readyToApply);
|
||||
if (!readyToApply) { return; }
|
||||
|
||||
if (c == null || c.Character == null) { return; }
|
||||
|
||||
bool accessible = c.Character.CanAccessInventory(this);
|
||||
if (this is CharacterInventory characterInventory && accessible)
|
||||
if (sender == null || sender.Character == null) { return; }
|
||||
|
||||
if (!IsInventoryAccessible())
|
||||
{
|
||||
if (Owner == null || Owner is not Character ownerCharacter)
|
||||
{
|
||||
accessible = false;
|
||||
}
|
||||
else if (!characterInventory.AccessibleWhenAlive && !ownerCharacter.IsDead && !characterInventory.AccessibleByOwner)
|
||||
{
|
||||
accessible = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessible)
|
||||
{
|
||||
//create a network event to correct the client's inventory state
|
||||
//otherwise they may have an item in their inventory they shouldn't have been able to pick up,
|
||||
//and receiving an event for that inventory later will cause the item to be dropped
|
||||
CreateNetworkEvent();
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (ushort id in receivedItemIdsFromClient[i])
|
||||
{
|
||||
if (Entity.FindEntityByID(id) is not Item item) { continue; }
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
if (item.ParentInventory != null && item.ParentInventory != this)
|
||||
{
|
||||
item.ParentInventory.CreateNetworkEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateCorrectiveNetworkEvent();
|
||||
return;
|
||||
}
|
||||
|
||||
//we need to check which of the items the client can access at this point, before we start shuffling things around
|
||||
//otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding,
|
||||
//you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet.
|
||||
Dictionary<Item, bool> canAccessItem = new Dictionary<Item, bool>();
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (ushort id in receivedItemIdsFromClient[i])
|
||||
{
|
||||
if (Entity.FindEntityByID(id) is not Item item) { continue; }
|
||||
canAccessItem[item] = item.CanClientAccess(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<Item> prevItems = new List<Item>(AllItems.Distinct());
|
||||
List<Inventory> prevItemInventories = new List<Inventory>() { this };
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (Item item in slots[i].Items.ToList())
|
||||
{
|
||||
if (!receivedItemIdsFromClient[i].Contains(item.ID) && item.IsInteractable(c.Character))
|
||||
{
|
||||
Item droppedItem = item;
|
||||
Entity prevOwner = Owner;
|
||||
Inventory previousInventory = droppedItem.ParentInventory;
|
||||
droppedItem.Drop(null);
|
||||
droppedItem.PreviousParentInventory = previousInventory;
|
||||
|
||||
var previousCharacterInventory = prevOwner switch
|
||||
{
|
||||
Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory,
|
||||
Character character => character.Inventory,
|
||||
_ => null
|
||||
};
|
||||
//we need to check which of the items the client (sender) can access at this point, before we start shuffling things around
|
||||
//otherwise if you're e.g. holding an item to access a cabinet, and picking up an item from the cabinet unequips the item you're holding,
|
||||
//you would fail to pick up the item because it gets unequipped before checking whether you can access the cabinet.
|
||||
var itemAccessibility = GetItemAccessibility();
|
||||
|
||||
HandleRemovedItems();
|
||||
|
||||
if (previousCharacterInventory != null && previousCharacterInventory != c.Character?.Inventory)
|
||||
{
|
||||
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, c, droppedItem);
|
||||
}
|
||||
|
||||
if (droppedItem.body != null && prevOwner != null)
|
||||
{
|
||||
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
HandleAddedItems();
|
||||
|
||||
foreach (ushort id in receivedItemIdsFromClient[i])
|
||||
{
|
||||
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
|
||||
prevItemInventories.Add(newItem?.ParentInventory);
|
||||
}
|
||||
}
|
||||
EnsureItemsInBothHands(sender.Character);
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
foreach (ushort id in receivedItemIdsFromClient[i])
|
||||
{
|
||||
if (Entity.FindEntityByID(id) is not Item item || slots[i].Contains(item)) { continue; }
|
||||
|
||||
if (item.GetComponent<Pickable>() is not Pickable pickable ||
|
||||
(pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(c.Character))
|
||||
{
|
||||
DebugConsole.AddWarning($"Client {c.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})",
|
||||
item.Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
|
||||
|
||||
if (!prevItems.Contains(item) && !canAccessItem[item] &&
|
||||
(c.Character == null || item.PreviousParentInventory == null || !c.Character.CanAccessInventory(item.PreviousParentInventory)))
|
||||
{
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.NewMessage($"Client {c.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
|
||||
#endif
|
||||
if (item.body != null && !c.PendingPositionUpdates.Contains(item))
|
||||
{
|
||||
c.PendingPositionUpdates.Enqueue(item);
|
||||
}
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
TryPutItem(item, i, true, true, c.Character, false);
|
||||
for (int j = 0; j < capacity; j++)
|
||||
{
|
||||
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
|
||||
{
|
||||
slots[j].RemoveItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EnsureItemsInBothHands(c.Character);
|
||||
|
||||
receivedItemIds.Remove(c);
|
||||
receivedItemIds.Remove(sender);
|
||||
|
||||
CreateNetworkEvent();
|
||||
foreach (Inventory prevInventory in prevItemInventories.Distinct())
|
||||
@@ -165,43 +54,211 @@ namespace Barotrauma
|
||||
if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); }
|
||||
}
|
||||
|
||||
foreach (Item item in AllItems.DistinctBy(it => it.Prefab))
|
||||
ServerLogAddedItems();
|
||||
|
||||
ServerLogRemovedItems();
|
||||
|
||||
#region local functions
|
||||
bool IsInventoryAccessible() => sender.Character.CanAccessInventory(this, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited);
|
||||
|
||||
void CreateCorrectiveNetworkEvent()
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (!prevItems.Contains(item))
|
||||
// create a network event to correct the client's inventory state.
|
||||
// Otherwise they may have an item in their inventory they shouldn't have been able to pick up,
|
||||
// and receiving an event for that inventory later will cause the item to be dropped
|
||||
CreateNetworkEvent();
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it));
|
||||
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
|
||||
if (Owner == c.Character)
|
||||
foreach (ushort itemId in receivedItemIdsFromClient[i])
|
||||
{
|
||||
HumanAIController.ItemTaken(item, c.Character);
|
||||
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory);
|
||||
if (Entity.FindEntityByID(itemId) is not Item item) { continue; }
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
if (item.ParentInventory != null && item.ParentInventory != this)
|
||||
{
|
||||
item.ParentInventory.CreateNetworkEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<Item, bool> GetItemAccessibility()
|
||||
{
|
||||
Dictionary<Item, bool> itemAccessibility = new Dictionary<Item, bool>();
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
// for every item that the new inventory state contains
|
||||
foreach (ushort itemId in receivedItemIdsFromClient[i])
|
||||
{
|
||||
// if there is no such item, skip
|
||||
if (Entity.FindEntityByID(itemId) is not Item item) { continue; }
|
||||
// add entry: can the sender access the item?
|
||||
itemAccessibility[item] = item.CanClientAccess(sender);
|
||||
}
|
||||
}
|
||||
|
||||
// we now have accessibility for every item in the new inventory state
|
||||
// but not for the items that were in the inventory before and perhaps dropped, so let's add those as well
|
||||
foreach (var item in prevItems)
|
||||
{
|
||||
if (!itemAccessibility.ContainsKey(item))
|
||||
{
|
||||
itemAccessibility[item] = item.CanClientAccess(sender);
|
||||
}
|
||||
}
|
||||
|
||||
return itemAccessibility;
|
||||
}
|
||||
|
||||
void HandleRemovedItems()
|
||||
{
|
||||
for (int slotIndex = 0; slotIndex < capacity; slotIndex++)
|
||||
{
|
||||
foreach (Item item in slots[slotIndex].Items.ToList())
|
||||
{
|
||||
bool shouldBeRemoved = !receivedItemIdsFromClient[slotIndex].Contains(item.ID) &&
|
||||
item.IsInteractable(sender.Character); // item is interactable to sender: not hidden and player team
|
||||
if (shouldBeRemoved)
|
||||
{
|
||||
bool itemAccessDenied = prevItems.Contains(item) && // if the item was in the inventory before
|
||||
!itemAccessibility[item] && // and the sender is not allowed to access it
|
||||
(item.PreviousParentInventory == null || // and either the item has no previous inventory
|
||||
!sender.Character.CanAccessInventory(item.PreviousParentInventory)); // or the sender can't access the previous inventory
|
||||
|
||||
if (itemAccessDenied)
|
||||
{
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.NewMessage($"Client {sender.Name} failed to drop item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
Item droppedItem = item;
|
||||
Entity prevOwner = Owner;
|
||||
Inventory previousInventory = droppedItem.ParentInventory;
|
||||
droppedItem.Drop(null);
|
||||
droppedItem.PreviousParentInventory = previousInventory;
|
||||
|
||||
var previousCharacterInventory = prevOwner switch
|
||||
{
|
||||
Item itemInventory => itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory,
|
||||
Character character => character.Inventory,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (previousCharacterInventory != null && previousCharacterInventory != sender.Character?.Inventory)
|
||||
{
|
||||
GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousCharacterInventory, sender, droppedItem);
|
||||
}
|
||||
|
||||
if (droppedItem.body != null && prevOwner != null)
|
||||
{
|
||||
droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ushort id in receivedItemIdsFromClient[slotIndex])
|
||||
{
|
||||
Item newItem = id == 0 ? null : Entity.FindEntityByID(id) as Item;
|
||||
prevItemInventories.Add(newItem?.ParentInventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HandleAddedItems()
|
||||
{
|
||||
for (int slotIndex = 0; slotIndex < capacity; slotIndex++)
|
||||
{
|
||||
foreach (ushort id in receivedItemIdsFromClient[slotIndex])
|
||||
{
|
||||
if (Entity.FindEntityByID(id) is not Item item || slots[slotIndex].Contains(item)) { continue; }
|
||||
|
||||
if (item.GetComponent<Pickable>() is not Pickable pickable ||
|
||||
(pickable.IsAttached && !pickable.PickingDone) || item.AllowedSlots.None() || !item.IsInteractable(sender.Character))
|
||||
{
|
||||
DebugConsole.AddWarning($"Client {sender.Name} tried to pick up a non-pickable item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"})",
|
||||
item.Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null && !holdable.CanBeDeattached()) { continue; }
|
||||
|
||||
bool itemAccessDenied = !prevItems.Contains(item) && !itemAccessibility[item] &&
|
||||
(sender.Character == null || item.PreviousParentInventory == null || !sender.Character.CanAccessInventory(item.PreviousParentInventory));
|
||||
|
||||
if (itemAccessDenied)
|
||||
{
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.NewMessage($"Client {sender.Name} failed to pick up item \"{item}\" (parent inventory: {item.ParentInventory?.Owner.ToString() ?? "null"}). No access.", Color.Yellow);
|
||||
#endif
|
||||
if (item.body != null && !sender.PendingPositionUpdates.Contains(item))
|
||||
{
|
||||
sender.PendingPositionUpdates.Enqueue(item);
|
||||
}
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
TryPutItem(item, slotIndex, true, true, sender.Character, false);
|
||||
for (int j = 0; j < capacity; j++)
|
||||
{
|
||||
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))
|
||||
{
|
||||
slots[j].RemoveItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerLogAddedItems()
|
||||
{
|
||||
foreach (Item item in AllItems.DistinctBy(it => it.Prefab))
|
||||
{
|
||||
if (item == null) { continue; }
|
||||
if (!prevItems.Contains(item))
|
||||
{
|
||||
int amount = AllItems.Count(it => it.Prefab == item.Prefab && !prevItems.Contains(it));
|
||||
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
|
||||
if (Owner == sender.Character)
|
||||
{
|
||||
HumanAIController.ItemTaken(item, sender.Character);
|
||||
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} picked up {amountText}{item.Name}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} placed {amountText}{item.Name} in the inventory of {Owner}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ServerLogRemovedItems()
|
||||
{
|
||||
var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it));
|
||||
foreach (Item item in droppedItems.DistinctBy(it => it.Prefab))
|
||||
{
|
||||
var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it));
|
||||
int amount = matchingItems.Count();
|
||||
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
|
||||
if (Owner == sender.Character)
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} placed {amountText}{item.Name} in {Owner}", ServerLog.MessageType.Inventory);
|
||||
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} removed {amountText}{item.Name} from the inventory of {Owner}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
item.CreateDroppedStack(matchingItems, allowClientExecute: true);
|
||||
}
|
||||
}
|
||||
|
||||
var droppedItems = prevItems.Where(it => it != null && !AllItems.Contains(it));
|
||||
foreach (Item item in droppedItems.DistinctBy(it => it.Prefab))
|
||||
{
|
||||
var matchingItems = prevItems.Where(it => it.Prefab == item.Prefab && !AllItems.Contains(it));
|
||||
int amount = matchingItems.Count();
|
||||
string amountText = amount > 1 ? $"x{amount} " : string.Empty;
|
||||
if (Owner == c.Character)
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} dropped {amountText}{item.Name}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameServer.Log($"{GameServer.CharacterLogName(c.Character)} removed {amountText}{item.Name} from {Owner}", ServerLog.MessageType.Inventory);
|
||||
}
|
||||
item.CreateDroppedStack(matchingItems, allowClientExecute: true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
private void EnsureItemsInBothHands(Character character)
|
||||
{
|
||||
if (this is not CharacterInventory charInv) { return; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
+24
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,13 +69,13 @@ namespace Barotrauma.Steam
|
||||
foreach (var contentPackage in contentPackages)
|
||||
{
|
||||
Steamworks.SteamServer.SetKey(
|
||||
$"contentpackage{index}",
|
||||
$"contentpackage{index}",
|
||||
new ServerListContentPackageInfo(contentPackage).ToString());
|
||||
index++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Steamworks.SteamServer.SetKey(key.Value.ToLowerInvariant(), value.ToString());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user