Build 1.1.4.0
This commit is contained in:
@@ -71,6 +71,11 @@ namespace Barotrauma
|
||||
|
||||
msg.WriteString(ragdollFileName);
|
||||
msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier);
|
||||
msg.WriteIdentifier(MinReputationToHire.factionId);
|
||||
if (MinReputationToHire.factionId != default)
|
||||
{
|
||||
msg.WriteSingle(MinReputationToHire.reputation);
|
||||
}
|
||||
if (Job != null)
|
||||
{
|
||||
msg.WriteUInt32(Job.Prefab.UintIdentifier);
|
||||
@@ -86,7 +91,7 @@ namespace Barotrauma
|
||||
msg.WriteByte((byte)0);
|
||||
}
|
||||
|
||||
msg.WriteUInt16((ushort)ExperiencePoints);
|
||||
msg.WriteInt32(ExperiencePoints);
|
||||
msg.WriteRangedInteger(AdditionalTalentPoints, 0, MaxAdditionalTalentPoints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,6 +692,7 @@ namespace Barotrauma
|
||||
{
|
||||
msg.WriteIdentifier(MerchantIdentifier);
|
||||
}
|
||||
msg.WriteIdentifier(Faction);
|
||||
|
||||
int msgLengthBeforeOrders = msg.LengthBytes;
|
||||
// Current orders
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace Barotrauma
|
||||
{
|
||||
static partial class DebugConsole
|
||||
{
|
||||
private static readonly RateLimiter rateLimiter = new(
|
||||
maxRequests: 10,
|
||||
expiryInSeconds: 5,
|
||||
punishmentRules: new[]
|
||||
{
|
||||
(RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
|
||||
(RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
|
||||
});
|
||||
|
||||
public partial class Command
|
||||
{
|
||||
/// <summary>
|
||||
@@ -608,12 +617,12 @@ namespace Barotrauma
|
||||
NewMessage("Valid ranks are:", Color.White);
|
||||
foreach (PermissionPreset permissionPreset in PermissionPreset.List)
|
||||
{
|
||||
NewMessage(" - " + permissionPreset.Name, Color.White);
|
||||
NewMessage(" - " + permissionPreset.DisplayName, Color.White);
|
||||
}
|
||||
|
||||
ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) =>
|
||||
{
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase));
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase));
|
||||
if (preset == null)
|
||||
{
|
||||
ThrowError("Rank \"" + rank + "\" not found.");
|
||||
@@ -622,7 +631,7 @@ namespace Barotrauma
|
||||
|
||||
client.SetPermissions(preset.Permissions, preset.PermittedCommands);
|
||||
GameMain.Server.UpdateClientPermissions(client);
|
||||
NewMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White);
|
||||
NewMessage("Assigned the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White);
|
||||
}, args, 1);
|
||||
});
|
||||
|
||||
@@ -1401,6 +1410,44 @@ namespace Barotrauma
|
||||
}));
|
||||
|
||||
|
||||
commands.Add(new Command("forcelocationtypechange", "", (string[] args) =>
|
||||
{
|
||||
if (GameMain.Server == null || GameMain.GameSession?.Campaign == null) { return; }
|
||||
|
||||
if (args.Length < 2)
|
||||
{
|
||||
ThrowError("Invalid parameters. The command should be formatted as \"forcelocationtypechange [locationname] [locationtype]\". If the names consist of multiple words, you should surround them with quotation marks.");
|
||||
return;
|
||||
}
|
||||
|
||||
var location = GameMain.GameSession.Campaign.Map.Locations.FirstOrDefault(l => l.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase));
|
||||
if (location == null)
|
||||
{
|
||||
ThrowError($"Could not find a location with the name {args[0]}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var locationType = LocationType.Prefabs.FirstOrDefault(lt =>
|
||||
lt.Name.Equals(args[1], StringComparison.OrdinalIgnoreCase) || lt.Identifier == args[1]);
|
||||
if (location == null)
|
||||
{
|
||||
ThrowError($"Could not find the location type {args[1]}.");
|
||||
return;
|
||||
}
|
||||
|
||||
location.ChangeType(GameMain.GameSession.Campaign, locationType);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign == null) { return null; }
|
||||
|
||||
return new string[][]
|
||||
{
|
||||
GameMain.GameSession.Campaign.Map.Locations.Select(l => l.Name).ToArray(),
|
||||
LocationType.Prefabs.Select(lt => lt.Name.Value).ToArray()
|
||||
};
|
||||
}));
|
||||
|
||||
AssignOnExecute("resetcharacternetstate", (string[] args) =>
|
||||
{
|
||||
if (GameMain.Server == null) { return; }
|
||||
@@ -1672,13 +1719,7 @@ namespace Barotrauma
|
||||
"teleportsub",
|
||||
(Client client, Vector2 cursorWorldPos, string[] args) =>
|
||||
{
|
||||
if (Submarine.MainSub == null || Level.Loaded == null) return;
|
||||
if (Level.Loaded.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage("The teleportsub command is unavailable in outpost levels!", client, Color.Red);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Submarine.MainSub == null || Level.Loaded == null) { return; }
|
||||
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Submarine.MainSub.SetPosition(cursorWorldPos);
|
||||
@@ -1934,7 +1975,7 @@ namespace Barotrauma
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage("Could not find the specified character.", client, Color.Red);
|
||||
}
|
||||
killedCharacter?.SetAllDamage(200.0f, 0.0f, 0.0f);
|
||||
killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1960,6 +2001,7 @@ namespace Barotrauma
|
||||
"freecam",
|
||||
(Client client, Vector2 cursorWorldPos, string[] args) =>
|
||||
{
|
||||
client.UsingFreeCam = true;
|
||||
GameMain.Server.SetClientCharacter(client, null);
|
||||
client.SpectateOnly = true;
|
||||
}
|
||||
@@ -2073,7 +2115,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
string rank = string.Join("", args.Skip(1));
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase));
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase));
|
||||
if (preset == null)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red);
|
||||
@@ -2082,8 +2124,8 @@ namespace Barotrauma
|
||||
|
||||
client.SetPermissions(preset.Permissions, preset.PermittedCommands);
|
||||
GameMain.Server.UpdateClientPermissions(client);
|
||||
GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient);
|
||||
NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White);
|
||||
GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient);
|
||||
NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2477,7 +2519,7 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
item.TryCreateServerEventSpam();
|
||||
item.CreateStatusEvent();
|
||||
item.CreateStatusEvent(loadingRound: false);
|
||||
}
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
@@ -2497,6 +2539,17 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void ServerRead(IReadMessage inc, Client sender)
|
||||
{
|
||||
string consoleCommand = inc.ReadString();
|
||||
float cursorX = inc.ReadSingle();
|
||||
float cursorY = inc.ReadSingle();
|
||||
|
||||
if (rateLimiter.IsLimitReached(sender)) { return; }
|
||||
|
||||
ExecuteClientCommand(sender, new Vector2(cursorX, cursorY), consoleCommand);
|
||||
}
|
||||
|
||||
public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command)
|
||||
{
|
||||
if (GameMain.Server == null) return;
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace Barotrauma
|
||||
clientsToRemove.Add(k);
|
||||
}
|
||||
}
|
||||
if (!(clientsToRemove is null))
|
||||
if (clientsToRemove is not null)
|
||||
{
|
||||
foreach (var k in clientsToRemove)
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Entity e in targets)
|
||||
{
|
||||
if (!(e is Character character) || !character.IsRemotePlayer) { continue; }
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
if (targetClient != null)
|
||||
{
|
||||
@@ -85,7 +85,7 @@ namespace Barotrauma
|
||||
IEnumerable<Entity> entities = ParentEvent.GetTargets(TargetTag);
|
||||
foreach (Entity e in entities)
|
||||
{
|
||||
if (!(e is Character character) || !character.IsRemotePlayer) { continue; }
|
||||
if (e is not Character character || !character.IsRemotePlayer) { continue; }
|
||||
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == character);
|
||||
if (targetClient != null)
|
||||
{
|
||||
@@ -149,5 +149,15 @@ namespace Barotrauma
|
||||
}
|
||||
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
|
||||
public void ServerWriteSelectedOption(Client client)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.CONVERSATION_SELECTED_OPTION);
|
||||
outmsg.WriteUInt16(Identifier);
|
||||
outmsg.WriteByte((byte)(selectedOption + 1));
|
||||
GameMain.Server?.ServerPeer?.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class MissionAction : EventAction
|
||||
{
|
||||
private static readonly HashSet<Mission> missionsUnlockedThisRound = new HashSet<Mission>();
|
||||
|
||||
public static void ResetMissionsUnlockedThisRound()
|
||||
{
|
||||
missionsUnlockedThisRound.Clear();
|
||||
}
|
||||
|
||||
public static void NotifyMissionsUnlockedThisRound(Client client)
|
||||
{
|
||||
foreach (Mission mission in missionsUnlockedThisRound)
|
||||
{
|
||||
NotifyMissionUnlock(mission, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyMissionUnlock(Mission mission)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
NotifyMissionUnlock(mission, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyMissionUnlock(Mission mission, Client client)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.MISSION);
|
||||
outmsg.WriteIdentifier(mission.Prefab.Identifier);
|
||||
outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[0]) ?? -1);
|
||||
outmsg.WriteInt32(GameMain.GameSession?.Map?.Locations.IndexOf(mission.Locations[1]) ?? -1);
|
||||
outmsg.WriteString(mission.Name.Value);
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ namespace Barotrauma
|
||||
|
||||
foreach (Event ev in activeEvents)
|
||||
{
|
||||
if (!(ev is ScriptedEvent scriptedEvent)) { continue; }
|
||||
if (ev is not ScriptedEvent scriptedEvent) { continue; }
|
||||
|
||||
var actions = FindActions(scriptedEvent);
|
||||
foreach (EventAction action in actions.Select(a => a.Item2))
|
||||
{
|
||||
if (!(action is ConversationAction convAction) || convAction.Identifier != actionId) { continue; }
|
||||
if (action is not ConversationAction convAction || convAction.Identifier != actionId) { continue; }
|
||||
if (!convAction.TargetClients.Contains(sender))
|
||||
{
|
||||
#if DEBUG || UNSTABLE
|
||||
@@ -42,6 +42,14 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
convAction.SelectedOption = selectedOption;
|
||||
if (convAction.Options.Any() && !convAction.GetEndingOptions().Contains(selectedOption))
|
||||
{
|
||||
foreach (Client c in convAction.TargetClients)
|
||||
{
|
||||
if (c == sender) { continue; }
|
||||
convAction.ServerWriteSelectedOption(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EndMission : Mission
|
||||
{
|
||||
public override void ServerWriteInitial(IWriteMessage msg, Client c)
|
||||
{
|
||||
base.ServerWriteInitial(msg, c);
|
||||
|
||||
boss.WriteSpawnData(msg, boss.ID, restrictMessageSize: false);
|
||||
msg.WriteByte((byte)minions.Length);
|
||||
foreach (Character minion in minions)
|
||||
{
|
||||
minion.WriteSpawnData(msg, minion.ID, restrictMessageSize: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -16,11 +17,10 @@ namespace Barotrauma
|
||||
foreach (var kvp in spawnedResources)
|
||||
{
|
||||
msg.WriteByte((byte)kvp.Value.Count);
|
||||
var rotation = resourceClusters[kvp.Key].Rotation;
|
||||
msg.WriteSingle(rotation);
|
||||
foreach (var r in kvp.Value)
|
||||
msg.WriteSingle(kvp.Value.FirstOrDefault()?.Rotation ?? 0.0f);
|
||||
foreach (var item in kvp.Value)
|
||||
{
|
||||
r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1);
|
||||
item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ namespace Barotrauma
|
||||
{
|
||||
msg.WriteIdentifier(kvp.Key);
|
||||
msg.WriteByte((byte)kvp.Value.Length);
|
||||
foreach (var i in kvp.Value)
|
||||
foreach (var item in kvp.Value)
|
||||
{
|
||||
msg.WriteUInt16(i.ID);
|
||||
msg.WriteUInt16(item.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,33 +6,66 @@ namespace Barotrauma
|
||||
{
|
||||
partial class SalvageMission : Mission
|
||||
{
|
||||
private bool usedExistingItem;
|
||||
struct SpawnInfo
|
||||
{
|
||||
public readonly bool UsedExistingItem;
|
||||
public readonly UInt16 OriginalInventoryID;
|
||||
public readonly byte OriginalItemContainerIndex;
|
||||
public readonly int OriginalSlotIndex;
|
||||
public readonly List<(int listIndex, int effectIndex)> ExecutedEffectIndices;
|
||||
|
||||
private UInt16 originalInventoryID;
|
||||
private byte originalItemContainerIndex;
|
||||
private int originalSlotIndex;
|
||||
public SpawnInfo(bool usedExistingItem, UInt16 originalInventoryID, byte originalItemContainerIndex, int originalSlotIndex, List<(int listIndex, int effectIndex)> executedEffectIndices)
|
||||
{
|
||||
UsedExistingItem = usedExistingItem;
|
||||
OriginalInventoryID = originalInventoryID;
|
||||
OriginalItemContainerIndex = originalItemContainerIndex;
|
||||
OriginalSlotIndex = originalSlotIndex;
|
||||
ExecutedEffectIndices = executedEffectIndices;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<Pair<int, int>> executedEffectIndices = new List<Pair<int, int>>();
|
||||
private readonly Dictionary<Target, SpawnInfo> spawnInfo = new Dictionary<Target, SpawnInfo>();
|
||||
|
||||
public override void ServerWriteInitial(IWriteMessage msg, Client c)
|
||||
{
|
||||
base.ServerWriteInitial(msg, c);
|
||||
|
||||
msg.WriteBoolean(usedExistingItem);
|
||||
if (usedExistingItem)
|
||||
foreach (var target in targets)
|
||||
{
|
||||
msg.WriteUInt16(item.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.WriteSpawnData(msg, item.ID, originalInventoryID, originalItemContainerIndex, originalSlotIndex);
|
||||
}
|
||||
bool targetFound = spawnInfo.ContainsKey(target) && target.Item != null;
|
||||
msg.WriteBoolean(targetFound);
|
||||
if (!targetFound) { continue; }
|
||||
|
||||
msg.WriteByte((byte)executedEffectIndices.Count);
|
||||
foreach (Pair<int, int> effectIndex in executedEffectIndices)
|
||||
msg.WriteBoolean(spawnInfo[target].UsedExistingItem);
|
||||
if (spawnInfo[target].UsedExistingItem)
|
||||
{
|
||||
msg.WriteUInt16(target.Item.ID);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.Item.WriteSpawnData(msg,
|
||||
target.Item.ID,
|
||||
spawnInfo[target].OriginalInventoryID,
|
||||
spawnInfo[target].OriginalItemContainerIndex,
|
||||
spawnInfo[target].OriginalSlotIndex);
|
||||
}
|
||||
|
||||
msg.WriteByte((byte)spawnInfo[target].ExecutedEffectIndices.Count);
|
||||
foreach ((int listIndex, int effectIndex) in spawnInfo[target].ExecutedEffectIndices)
|
||||
{
|
||||
msg.WriteByte((byte)listIndex);
|
||||
msg.WriteByte((byte)effectIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void ServerWrite(IWriteMessage msg)
|
||||
{
|
||||
base.ServerWrite(msg);
|
||||
msg.WriteByte((byte)targets.Count);
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
msg.WriteByte((byte)effectIndex.First);
|
||||
msg.WriteByte((byte)effectIndex.Second);
|
||||
msg.WriteByte((byte)targets[i].State);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,6 @@ namespace Barotrauma
|
||||
Console.WriteLine("Loading game settings");
|
||||
GameSettings.Init();
|
||||
|
||||
Console.WriteLine("Loading MD5 hash cache");
|
||||
Md5Hash.Cache.Load();
|
||||
|
||||
Console.WriteLine("Initializing SteamManager");
|
||||
SteamManager.Initialize();
|
||||
|
||||
@@ -182,7 +179,7 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < CommandLineArgs.Length; i++)
|
||||
{
|
||||
switch (CommandLineArgs[i].Trim())
|
||||
switch (CommandLineArgs[i].Trim().ToLowerInvariant())
|
||||
{
|
||||
case "-name":
|
||||
name = CommandLineArgs[i + 1];
|
||||
@@ -248,7 +245,7 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < CommandLineArgs.Length; i++)
|
||||
{
|
||||
switch (CommandLineArgs[i].Trim())
|
||||
switch (CommandLineArgs[i].Trim().ToLowerInvariant())
|
||||
{
|
||||
case "-playstyle":
|
||||
Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle);
|
||||
@@ -270,6 +267,14 @@ namespace Barotrauma
|
||||
Server.ServerSettings.KarmaPreset = karmaPresetName;
|
||||
i++;
|
||||
break;
|
||||
case "-language":
|
||||
LanguageIdentifier language = CommandLineArgs[i + 1].ToLanguageIdentifier();
|
||||
if (ServerLanguageOptions.Options.Any(o => o.Identifier == language))
|
||||
{
|
||||
Server.ServerSettings.Language = language;
|
||||
}
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+74
-77
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Steam;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -85,6 +84,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (purchasedHullRepairs == value) { return; }
|
||||
purchasedHullRepairs = value;
|
||||
PurchasedHullRepairsInLatestSave |= value;
|
||||
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (purchasedLostShuttles == value) { return; }
|
||||
purchasedLostShuttles = value;
|
||||
PurchasedLostShuttlesInLatestSave |= value;
|
||||
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +106,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (purchasedItemRepairs == value) { return; }
|
||||
purchasedItemRepairs = value;
|
||||
PurchasedItemRepairsInLatestSave |= value;
|
||||
IncrementLastUpdateIdForFlag(NetFlags.Misc);
|
||||
}
|
||||
}
|
||||
@@ -337,11 +339,12 @@ namespace Barotrauma
|
||||
IsFirstRound = true;
|
||||
break;
|
||||
case TransitionType.ProgressToNextEmptyLocation:
|
||||
Map.Visit(Map.CurrentLocation);
|
||||
TotalPassedLevels++;
|
||||
break;
|
||||
}
|
||||
|
||||
Map.ProgressWorld(transitionType, GameMain.GameSession.RoundDuration);
|
||||
Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration);
|
||||
|
||||
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
||||
if (success)
|
||||
@@ -391,17 +394,14 @@ namespace Barotrauma
|
||||
NextLevel = newLevel;
|
||||
MirrorLevel = mirror;
|
||||
|
||||
//give clients time to play the end cinematic before starting the next round
|
||||
if (transitionType == TransitionType.End)
|
||||
{
|
||||
yield return new WaitForSeconds(EndCinematicDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitForSeconds(EndTransitionDuration * 0.5f);
|
||||
}
|
||||
|
||||
GameMain.Server.TryStartGame();
|
||||
yield return new WaitForSeconds(EndTransitionDuration * 0.5f);
|
||||
|
||||
//don't start the next round automatically if we just finished the campaign
|
||||
if (transitionType != TransitionType.End)
|
||||
{
|
||||
GameMain.Server.TryStartGame();
|
||||
}
|
||||
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
@@ -424,7 +424,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public bool CanPurchaseSub(SubmarineInfo info, Client client)
|
||||
=> CanAfford(info.Price, client) && GetCampaignSubs().Contains(info);
|
||||
=> CanAfford(info.GetPrice(), client) && GetCampaignSubs().Contains(info);
|
||||
|
||||
private readonly List<CharacterCampaignData> discardedCharacters = new List<CharacterCampaignData>();
|
||||
public void DiscardClientCharacterData(Client client)
|
||||
@@ -493,7 +493,8 @@ namespace Barotrauma
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
|
||||
{
|
||||
var transitionType = GetAvailableTransition(out _, out Submarine leavingSub);
|
||||
if (transitionType == TransitionType.End)
|
||||
if (transitionType == TransitionType.End ||
|
||||
(Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation))
|
||||
{
|
||||
LoadNewLevel();
|
||||
}
|
||||
@@ -509,6 +510,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded.IsEndBiome)
|
||||
{
|
||||
var transitionType = GetAvailableTransition(out _, out Submarine leavingSub);
|
||||
if (transitionType == TransitionType.ProgressToNextLocation)
|
||||
{
|
||||
LoadNewLevel();
|
||||
}
|
||||
}
|
||||
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
KeepCharactersCloseToOutpost(deltaTime);
|
||||
@@ -704,10 +713,6 @@ namespace Barotrauma
|
||||
if (requiredFlags.HasFlag(NetFlags.Reputation))
|
||||
{
|
||||
msg.WriteUInt16(GetLastUpdateIdForFlag(NetFlags.Reputation));
|
||||
Reputation reputation = Map?.CurrentLocation?.Reputation;
|
||||
msg.WriteBoolean(reputation != null);
|
||||
if (reputation != null) { msg.WriteSingle(reputation.Value); }
|
||||
|
||||
// hopefully we'll never have more than 128 factions
|
||||
msg.WriteByte((byte)Factions.Count);
|
||||
foreach (Faction faction in Factions)
|
||||
@@ -823,54 +828,37 @@ namespace Barotrauma
|
||||
Bank.ForceUpdate();
|
||||
}
|
||||
|
||||
if (purchasedHullRepairs != PurchasedHullRepairs)
|
||||
if (purchasedHullRepairs && !PurchasedHullRepairs)
|
||||
{
|
||||
switch (purchasedHullRepairs)
|
||||
if (GetBalance(sender) >= hullRepairCost)
|
||||
{
|
||||
case true when GetBalance(sender) >= hullRepairCost:
|
||||
TryPurchase(sender, hullRepairCost);
|
||||
PurchasedHullRepairs = true;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs");
|
||||
break;
|
||||
case false:
|
||||
PurchasedHullRepairs = false;
|
||||
personalWallet.Refund(hullRepairCost);
|
||||
break;
|
||||
TryPurchase(sender, hullRepairCost);
|
||||
PurchasedHullRepairs = true;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs");
|
||||
}
|
||||
}
|
||||
|
||||
if (purchasedItemRepairs != PurchasedItemRepairs)
|
||||
if (purchasedItemRepairs && !PurchasedItemRepairs)
|
||||
{
|
||||
switch (purchasedItemRepairs)
|
||||
if (GetBalance(sender) >= itemRepairCost)
|
||||
{
|
||||
case true when GetBalance(sender) >= itemRepairCost:
|
||||
TryPurchase(sender, itemRepairCost);
|
||||
PurchasedItemRepairs = true;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs");
|
||||
break;
|
||||
case false:
|
||||
PurchasedItemRepairs = false;
|
||||
personalWallet.Refund(itemRepairCost);
|
||||
break;
|
||||
TryPurchase(sender, itemRepairCost);
|
||||
PurchasedItemRepairs = true;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(itemRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs");
|
||||
}
|
||||
}
|
||||
|
||||
if (purchasedLostShuttles != PurchasedLostShuttles)
|
||||
if (purchasedLostShuttles && !PurchasedLostShuttles)
|
||||
{
|
||||
if (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied)
|
||||
{
|
||||
GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox);
|
||||
}
|
||||
else if (purchasedLostShuttles && TryPurchase(sender, shuttleRetrieveCost))
|
||||
else if (TryPurchase(sender, shuttleRetrieveCost))
|
||||
{
|
||||
PurchasedLostShuttles = true;
|
||||
GameAnalyticsManager.AddMoneySpentEvent(shuttleRetrieveCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle");
|
||||
}
|
||||
else if (!purchasedItemRepairs)
|
||||
{
|
||||
PurchasedLostShuttles = false;
|
||||
personalWallet.Refund(shuttleRetrieveCost);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLocIndex < Map.Locations.Count && Map.AllowDebugTeleport)
|
||||
@@ -1021,12 +1009,13 @@ namespace Barotrauma
|
||||
bool predicate(SoldItem i) => allowedToSellInventoryItems != (i.Origin == SoldItem.SellOrigin.Character);
|
||||
}
|
||||
|
||||
var characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
foreach (var (prefab, category, _) in purchasedUpgrades)
|
||||
{
|
||||
UpgradeManager.PurchaseUpgrade(prefab, category, client: sender);
|
||||
|
||||
// unstable logging
|
||||
int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation);
|
||||
int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation, characterList);
|
||||
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
|
||||
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
|
||||
}
|
||||
@@ -1057,49 +1046,47 @@ namespace Barotrauma
|
||||
|
||||
if (GameMain.Server is null) { return; }
|
||||
|
||||
switch (transfer.Sender)
|
||||
if (transfer.Sender.TryUnwrap(out var id))
|
||||
{
|
||||
case Some<ushort> { Value: var id }:
|
||||
if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; }
|
||||
if (id != sender.CharacterID && !AllowedToManageWallets(sender)) { return; }
|
||||
|
||||
Wallet wallet = GetWalletByID(id);
|
||||
if (wallet is InvalidWallet) { return; }
|
||||
Wallet wallet = GetWalletByID(id);
|
||||
if (wallet is InvalidWallet) { return; }
|
||||
|
||||
TransferMoney(wallet);
|
||||
break;
|
||||
case None<ushort> _:
|
||||
if (!AllowedToManageWallets(sender))
|
||||
TransferMoney(wallet);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!AllowedToManageWallets(sender))
|
||||
{
|
||||
if (transfer.Receiver.TryUnwrap(out var receiverId) && receiverId == sender.CharacterID)
|
||||
{
|
||||
if (transfer.Receiver is Some<ushort> { Value: var receiverId } && receiverId == sender.CharacterID)
|
||||
{
|
||||
if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; }
|
||||
GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender);
|
||||
GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money);
|
||||
}
|
||||
return;
|
||||
if (transfer.Amount > GameMain.Server.ServerSettings.MaximumMoneyTransferRequest) { return; }
|
||||
GameMain.Server.Voting.StartTransferVote(sender, null, transfer.Amount, sender);
|
||||
GameServer.Log($"{sender.Name} started a vote to transfer {transfer.Amount} mk from the bank.", ServerLog.MessageType.Money);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
TransferMoney(Bank);
|
||||
break;
|
||||
TransferMoney(Bank);
|
||||
}
|
||||
|
||||
void TransferMoney(Wallet from)
|
||||
{
|
||||
if (!from.TryDeduct(transfer.Amount)) { return; }
|
||||
|
||||
switch (transfer.Receiver)
|
||||
if (transfer.Receiver.TryUnwrap(out var id))
|
||||
{
|
||||
case Some<ushort> { Value: var id }:
|
||||
Wallet wallet = GetWalletByID(id);
|
||||
if (wallet is InvalidWallet) { return; }
|
||||
Wallet wallet = GetWalletByID(id);
|
||||
if (wallet is InvalidWallet) { return; }
|
||||
|
||||
wallet.Give(transfer.Amount);
|
||||
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
||||
break;
|
||||
case None<ushort> _:
|
||||
Bank.Give(transfer.Amount);
|
||||
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
||||
break;
|
||||
wallet.Give(transfer.Amount);
|
||||
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {wallet.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
||||
}
|
||||
else
|
||||
{
|
||||
Bank.Give(transfer.Amount);
|
||||
GameServer.Log($"{sender.Name} transferred {transfer.Amount} mk to {Bank.GetOwnerLogName()} from {from.GetOwnerLogName()}.", ServerLog.MessageType.Money);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1322,6 +1309,10 @@ namespace Barotrauma
|
||||
|
||||
public override bool TryPurchase(Client client, int price)
|
||||
{
|
||||
//disconnected clients can never purchase anything
|
||||
//(can happen e.g. if someone starts a vote to buy something and then disconnects)
|
||||
if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; }
|
||||
|
||||
Wallet wallet = GetWallet(client);
|
||||
if (!AllowedToManageWallets(client))
|
||||
{
|
||||
@@ -1371,6 +1362,12 @@ namespace Barotrauma
|
||||
modeElement.Add(Settings.Save());
|
||||
modeElement.Add(SaveStats());
|
||||
modeElement.Add(Bank.Save());
|
||||
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
{
|
||||
modeElement.Add(GameMain.GameSession?.EventManager.Save());
|
||||
}
|
||||
|
||||
CampaignMetadata?.Save(modeElement);
|
||||
Map.Save(modeElement);
|
||||
CargoManager?.SavePurchasedItems(modeElement);
|
||||
|
||||
@@ -11,20 +11,15 @@ namespace Barotrauma
|
||||
{
|
||||
internal partial class MedicalClinic
|
||||
{
|
||||
private enum RateLimitResult
|
||||
{
|
||||
OK,
|
||||
LimitReached
|
||||
}
|
||||
// allow 10 requests per 5 seconds, announce to chat if the limit is reached
|
||||
private readonly RateLimiter rateLimiter = new(
|
||||
maxRequests: 10,
|
||||
expiryInSeconds: 5,
|
||||
punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce));
|
||||
|
||||
private struct RateLimitInfo
|
||||
{
|
||||
public int Requests;
|
||||
public const int MaxRequests = 10;
|
||||
public DateTimeOffset Expiry;
|
||||
}
|
||||
private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry);
|
||||
|
||||
private readonly Dictionary<Client, RateLimitInfo> rateLimits = new Dictionary<Client, RateLimitInfo>();
|
||||
private readonly List<AfflictionSubscriber> afflictionSubscribers = new();
|
||||
|
||||
public void ServerRead(IReadMessage inc, Client sender)
|
||||
{
|
||||
@@ -35,6 +30,9 @@ namespace Barotrauma
|
||||
case NetworkHeader.ADD_EVERYTHING_TO_PENDING:
|
||||
ProcessAddEverything(sender);
|
||||
break;
|
||||
case NetworkHeader.UNSUBSCRIBE_ME:
|
||||
RemoveClientSubscription(sender);
|
||||
break;
|
||||
case NetworkHeader.REQUEST_AFFLICTIONS:
|
||||
ProcessRequestedAfflictions(inc, sender);
|
||||
break;
|
||||
@@ -58,7 +56,7 @@ namespace Barotrauma
|
||||
|
||||
private void ProcessNewAddition(IReadMessage inc, Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
NetCrewMember newCrewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
|
||||
InsertPendingCrewMember(newCrewMember);
|
||||
@@ -67,14 +65,25 @@ namespace Barotrauma
|
||||
|
||||
private void ProcessAddEverything(Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
AddEverythingToPending();
|
||||
ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client);
|
||||
}
|
||||
|
||||
private void RemoveClientSubscription(Client client)
|
||||
{
|
||||
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
|
||||
{
|
||||
if (sub.Subscriber == client || sub.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
afflictionSubscribers.Remove(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessNewRemoval(IReadMessage inc, Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc);
|
||||
RemovePendingAffliction(removed.CrewMember, removed.Affliction);
|
||||
@@ -83,14 +92,14 @@ namespace Barotrauma
|
||||
|
||||
private void ProcessRequestedPending(Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client);
|
||||
}
|
||||
|
||||
private void ProcessHealing(Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
HealRequestResult result = HealAllPending(client: client);
|
||||
ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client);
|
||||
@@ -98,7 +107,7 @@ namespace Barotrauma
|
||||
|
||||
private void ProcessClearing(Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
if (!PendingHeals.Any()) { return; }
|
||||
|
||||
@@ -108,7 +117,7 @@ namespace Barotrauma
|
||||
|
||||
private void ProcessRequestedAfflictions(IReadMessage inc, Client client)
|
||||
{
|
||||
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
|
||||
if (rateLimiter.IsLimitReached(client)) { return; }
|
||||
|
||||
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
|
||||
|
||||
@@ -129,35 +138,17 @@ namespace Barotrauma
|
||||
Afflictions = pendingAfflictions
|
||||
};
|
||||
|
||||
if (foundInfo is not null)
|
||||
{
|
||||
RemoveClientSubscription(client);
|
||||
|
||||
// the client subscribes to the afflictions of the crew member for the next minute
|
||||
afflictionSubscribers.Add(new AfflictionSubscriber(client, foundInfo, DateTimeOffset.Now.AddMinutes(1)));
|
||||
}
|
||||
|
||||
ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client);
|
||||
}
|
||||
|
||||
private RateLimitResult CheckRateLimit(Client client)
|
||||
{
|
||||
if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo))
|
||||
{
|
||||
if (rateLimitInfo.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5);
|
||||
rateLimitInfo.Requests = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; }
|
||||
|
||||
rateLimitInfo.Requests++;
|
||||
}
|
||||
|
||||
rateLimits[client] = rateLimitInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) });
|
||||
}
|
||||
|
||||
return RateLimitResult.OK;
|
||||
}
|
||||
|
||||
private IWriteMessage StartSending()
|
||||
{
|
||||
IWriteMessage msg = new WriteOnlyMessage();
|
||||
|
||||
@@ -8,10 +8,12 @@ namespace Barotrauma.Items.Components
|
||||
private readonly struct EventData : IEventData
|
||||
{
|
||||
public readonly bool Launch;
|
||||
public readonly byte SpreadCounter;
|
||||
|
||||
public EventData(bool launch)
|
||||
public EventData(bool launch, byte spreadCounter = 0)
|
||||
{
|
||||
Launch = launch;
|
||||
SpreadCounter = spreadCounter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +34,7 @@ namespace Barotrauma.Items.Components
|
||||
msg.WriteSingle(launchPos.X);
|
||||
msg.WriteSingle(launchPos.Y);
|
||||
msg.WriteSingle(launchRot);
|
||||
msg.WriteByte(eventData.SpreadCounter);
|
||||
}
|
||||
|
||||
bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved();
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components
|
||||
//already connected, no need to do anything
|
||||
if (Connections[i].Wires.Contains(newWire)) { continue; }
|
||||
|
||||
newWire.Connect(Connections[i], true, true);
|
||||
newWire.TryConnect(Connections[i], true, true);
|
||||
Connections[i].TryAddLink(newWire);
|
||||
|
||||
var otherConnection = newWire.OtherConnection(Connections[i]);
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
string newOutputValue = msg.ReadString();
|
||||
|
||||
if (item.CanClientAccess(c))
|
||||
if (item.CanClientAccess(c) && !Readonly)
|
||||
{
|
||||
if (newOutputValue.Length > MaxMessageLength)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -135,6 +136,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
EnsureItemsInBothHands(c.Character);
|
||||
|
||||
CreateNetworkEvent();
|
||||
foreach (Inventory prevInventory in prevItemInventories.Distinct())
|
||||
{
|
||||
@@ -174,6 +177,29 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureItemsInBothHands(Character character)
|
||||
{
|
||||
if (this is not CharacterInventory charInv) { return; }
|
||||
|
||||
int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand),
|
||||
rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand);
|
||||
|
||||
TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot);
|
||||
TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot);
|
||||
|
||||
void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot)
|
||||
{
|
||||
const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand;
|
||||
|
||||
foreach (Item it in slots[originalSlot].Items)
|
||||
{
|
||||
if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; }
|
||||
|
||||
TryPutItem(it, otherHandSlot, true, true, character, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
||||
{
|
||||
SharedWrite(msg, extraData);
|
||||
|
||||
@@ -65,7 +65,8 @@ namespace Barotrauma
|
||||
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
|
||||
itemContainer.Inventory.ServerEventWrite(msg, c);
|
||||
break;
|
||||
case ItemStatusEventData _:
|
||||
case ItemStatusEventData statusEvent:
|
||||
msg.WriteBoolean(statusEvent.LoadingRound);
|
||||
msg.WriteSingle(condition);
|
||||
break;
|
||||
case AssignCampaignInteractionEventData _:
|
||||
@@ -253,6 +254,7 @@ namespace Barotrauma
|
||||
msg.WriteBoolean(hasIdCard);
|
||||
if (hasIdCard)
|
||||
{
|
||||
msg.WriteInt32(idCardComponent.SubmarineSpecificID);
|
||||
msg.WriteString(idCardComponent.OwnerName);
|
||||
msg.WriteString(idCardComponent.OwnerTags);
|
||||
msg.WriteByte((byte)Math.Max(0, idCardComponent.OwnerBeardIndex+1));
|
||||
|
||||
@@ -140,8 +140,7 @@ namespace Barotrauma.Networking
|
||||
return Option<BannedPlayer>.Some(new BannedPlayer(name, addressOrAccountId, reason, expirationTime));
|
||||
}
|
||||
|
||||
bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement)
|
||||
.OfType<Some<BannedPlayer>>().Select(o => o.Value));
|
||||
bannedPlayers.AddRange(doc.Root.Elements().Select(loadFromElement).NotNone());
|
||||
}
|
||||
|
||||
private void RemoveExpired()
|
||||
|
||||
@@ -102,9 +102,9 @@ namespace Barotrauma.Networking
|
||||
similarity *= 0.25f;
|
||||
}
|
||||
|
||||
bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection;
|
||||
bool isSpamExempt = RateLimiter.IsExempt(c);
|
||||
|
||||
if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner)
|
||||
if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
c.ChatSpamSpeed += similarity + 0.5f;
|
||||
|
||||
if (c.ChatSpamTimer > 0.0f && !isOwner)
|
||||
if (c.ChatSpamTimer > 0.0f && !isSpamExempt)
|
||||
{
|
||||
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null);
|
||||
c.ChatSpamTimer = 10.0f;
|
||||
|
||||
@@ -303,7 +303,7 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None");
|
||||
var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None");
|
||||
if (defaultPerms != null)
|
||||
{
|
||||
newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands);
|
||||
@@ -332,9 +332,8 @@ namespace Barotrauma.Networking
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
#if CLIENT
|
||||
if (ShowNetStats) { netStats.Update(deltaTime); }
|
||||
#endif
|
||||
dosProtection.Update(deltaTime);
|
||||
|
||||
if (!started) { return; }
|
||||
|
||||
if (ChildServerRelay.HasShutDown)
|
||||
@@ -387,10 +386,10 @@ namespace Barotrauma.Networking
|
||||
Voting.Update(deltaTime);
|
||||
|
||||
bool isCrewDead =
|
||||
connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated);
|
||||
connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated));
|
||||
|
||||
bool subAtLevelEnd = false;
|
||||
if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode))
|
||||
if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
|
||||
{
|
||||
if (Level.Loaded?.EndOutpost != null)
|
||||
{
|
||||
@@ -692,10 +691,14 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private readonly DoSProtection dosProtection = new();
|
||||
|
||||
private void ReadDataMessage(NetworkConnection sender, IReadMessage inc)
|
||||
{
|
||||
var connectedClient = connectedClients.Find(c => c.Connection == sender);
|
||||
|
||||
using var _ = dosProtection.Start(connectedClient);
|
||||
|
||||
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
|
||||
switch (header)
|
||||
{
|
||||
@@ -784,7 +787,7 @@ namespace Barotrauma.Networking
|
||||
if (GameStarted)
|
||||
{
|
||||
SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
|
||||
{
|
||||
@@ -1085,7 +1088,7 @@ namespace Barotrauma.Networking
|
||||
ChatMessage.ServerRead(inc, c);
|
||||
break;
|
||||
case ClientNetSegment.Vote:
|
||||
Voting.ServerRead(inc, c);
|
||||
Voting.ServerRead(inc, c, dosProtection);
|
||||
break;
|
||||
default:
|
||||
return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
|
||||
@@ -1116,6 +1119,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
//check if midround syncing is needed due to missed unique events
|
||||
if (!midroundSyncingDone) { entityEventManager.InitClientMidRoundSync(c); }
|
||||
MissionAction.NotifyMissionsUnlockedThisRound(c);
|
||||
c.InGame = true;
|
||||
}
|
||||
}
|
||||
@@ -1245,7 +1249,7 @@ namespace Barotrauma.Networking
|
||||
entityEventManager.Read(inc, c);
|
||||
break;
|
||||
case ClientNetSegment.Vote:
|
||||
Voting.ServerRead(inc, c);
|
||||
Voting.ServerRead(inc, c, dosProtection);
|
||||
break;
|
||||
case ClientNetSegment.SpectatingPos:
|
||||
c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
|
||||
@@ -1405,19 +1409,23 @@ namespace Barotrauma.Networking
|
||||
bool quitCampaign = inc.ReadBoolean();
|
||||
if (GameStarted)
|
||||
{
|
||||
Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
|
||||
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
|
||||
using (dosProtection.Pause(sender))
|
||||
{
|
||||
mpCampaign.SavePlayers();
|
||||
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
|
||||
mpCampaign.UpdateStoreStock();
|
||||
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
|
||||
Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
|
||||
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
|
||||
{
|
||||
mpCampaign.SavePlayers();
|
||||
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
|
||||
mpCampaign.UpdateStoreStock();
|
||||
GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
|
||||
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
save = false;
|
||||
}
|
||||
EndGame(wasSaved: save);
|
||||
}
|
||||
else
|
||||
{
|
||||
save = false;
|
||||
}
|
||||
EndGame(wasSaved: save);
|
||||
}
|
||||
else if (mpCampaign != null)
|
||||
{
|
||||
@@ -1441,45 +1449,54 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
|
||||
{
|
||||
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
|
||||
using (dosProtection.Pause(sender))
|
||||
{
|
||||
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!GameStarted && !initiatedStartGame)
|
||||
{
|
||||
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
|
||||
TryStartGame();
|
||||
using (dosProtection.Pause(sender))
|
||||
{
|
||||
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
|
||||
TryStartGame();
|
||||
}
|
||||
}
|
||||
else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
|
||||
{
|
||||
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
|
||||
//don't force location if we've teleported
|
||||
bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
|
||||
switch (availableTransition)
|
||||
using (dosProtection.Pause(sender))
|
||||
{
|
||||
case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
|
||||
if (forceLocation)
|
||||
{
|
||||
mpCampaign.Map.SelectLocation(
|
||||
mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
|
||||
}
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
|
||||
if (forceLocation)
|
||||
{
|
||||
mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
|
||||
}
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
case CampaignMode.TransitionType.None:
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
|
||||
#endif
|
||||
return;
|
||||
default:
|
||||
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
|
||||
//don't force location if we've teleported
|
||||
bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
|
||||
switch (availableTransition)
|
||||
{
|
||||
case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
|
||||
if (forceLocation)
|
||||
{
|
||||
mpCampaign.Map.SelectLocation(
|
||||
mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
|
||||
}
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
|
||||
if (forceLocation)
|
||||
{
|
||||
mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
|
||||
}
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
case CampaignMode.TransitionType.None:
|
||||
#if DEBUG || UNSTABLE
|
||||
DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
|
||||
mpCampaign.LoadNewLevel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1519,11 +1536,7 @@ namespace Barotrauma.Networking
|
||||
mpCampaign?.ServerRead(inc, sender);
|
||||
break;
|
||||
case ClientPermissions.ConsoleCommands:
|
||||
{
|
||||
string consoleCommand = inc.ReadString();
|
||||
Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
|
||||
DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand);
|
||||
}
|
||||
DebugConsole.ServerRead(inc, sender);
|
||||
break;
|
||||
case ClientPermissions.ManagePermissions:
|
||||
byte targetClientID = inc.ReadByte();
|
||||
@@ -2366,10 +2379,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
List<WayPoint> spawnWaypoints = null;
|
||||
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSubs[n]).ToList();
|
||||
if (Level.Loaded?.StartOutpost != null &&
|
||||
Level.Loaded.Type == LevelData.LevelType.Outpost &&
|
||||
(Level.Loaded.StartOutpost.Info.OutpostGenerationParams?.SpawnCrewInsideOutpost ?? false) &&
|
||||
Level.Loaded.StartOutpost.GetConnectedSubs().Any(s => s.Info.Type == SubmarineType.Player))
|
||||
if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
|
||||
{
|
||||
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
|
||||
wp.SpawnType == SpawnType.Human &&
|
||||
@@ -2444,7 +2454,6 @@ namespace Barotrauma.Networking
|
||||
spawnedCharacter.Info.InventoryData = new XElement("inventory");
|
||||
spawnedCharacter.Info.StartItemsGiven = true;
|
||||
spawnedCharacter.SaveInventory();
|
||||
// talents are only avilable for players in online sessions, but modders or someone else might want to have them loaded anyway
|
||||
spawnedCharacter.LoadTalents();
|
||||
}
|
||||
}
|
||||
@@ -2979,7 +2988,7 @@ namespace Barotrauma.Networking
|
||||
client.WaitForNextRoundRespawn = null;
|
||||
client.InGame = false;
|
||||
|
||||
if (client.AccountId is Some<AccountId> { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); }
|
||||
if (client.AccountId.TryUnwrap<SteamId>(out var steamId)) { SteamManager.StopAuthSession(steamId); }
|
||||
|
||||
var previousPlayer = previousPlayers.Find(p => p.MatchesClient(client));
|
||||
if (previousPlayer == null)
|
||||
@@ -3313,12 +3322,13 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (checkActiveVote && Voting.ActiveVote != null)
|
||||
{
|
||||
#warning TODO: this is mostly the same as Voting.Update, deduplicate (if/when refactoring the Voting class?)
|
||||
var inGameClients = GameMain.Server.ConnectedClients.Where(c => c.InGame);
|
||||
if (inGameClients.Count() == 1)
|
||||
if (inGameClients.Count() == 1 && inGameClients.First() == Voting.ActiveVote.VoteStarter)
|
||||
{
|
||||
Voting.ActiveVote.Finish(Voting, passed: true);
|
||||
}
|
||||
else
|
||||
else if (inGameClients.Any())
|
||||
{
|
||||
var eligibleClients = inGameClients.Where(c => c != Voting.ActiveVote.VoteStarter);
|
||||
int yes = eligibleClients.Count(c => c.GetVote<int>(Voting.ActiveVote.VoteType) == 2);
|
||||
@@ -3388,12 +3398,11 @@ namespace Barotrauma.Networking
|
||||
|
||||
public void SwitchSubmarine()
|
||||
{
|
||||
if (!(Voting.ActiveVote is Voting.SubmarineVote subVote)) { return; }
|
||||
if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; }
|
||||
|
||||
SubmarineInfo targetSubmarine = subVote.Sub;
|
||||
VoteType voteType = Voting.ActiveVote.VoteType;
|
||||
Client starter = Voting.ActiveVote.VoteStarter;
|
||||
int deliveryFee = 0;
|
||||
|
||||
switch (voteType)
|
||||
{
|
||||
@@ -3403,7 +3412,6 @@ namespace Barotrauma.Networking
|
||||
GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter);
|
||||
break;
|
||||
case VoteType.SwitchSub:
|
||||
deliveryFee = subVote.DeliveryFee;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@@ -3411,7 +3419,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
if (voteType != VoteType.PurchaseSub)
|
||||
{
|
||||
GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, deliveryFee, starter);
|
||||
GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter);
|
||||
}
|
||||
|
||||
Voting.StopSubmarineVote(true);
|
||||
@@ -3592,15 +3600,28 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private readonly RateLimiter charInfoRateLimiter = new(
|
||||
maxRequests: 5,
|
||||
expiryInSeconds: 10,
|
||||
punishmentRules: new[]
|
||||
{
|
||||
(RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
|
||||
(RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
|
||||
});
|
||||
|
||||
private void UpdateCharacterInfo(IReadMessage message, Client sender)
|
||||
{
|
||||
sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
|
||||
if (sender.SpectateOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool spectateOnly = message.ReadBoolean();
|
||||
message.ReadPadBits();
|
||||
|
||||
string newName = message.ReadString();
|
||||
sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
|
||||
if (sender.SpectateOnly) { return; }
|
||||
|
||||
var netInfo = INetSerializableStruct.Read<NetCharacterInfo>(message);
|
||||
|
||||
if (charInfoRateLimiter.IsLimitReached(sender)) { return; }
|
||||
|
||||
string newName = netInfo.NewName;
|
||||
if (string.IsNullOrEmpty(newName))
|
||||
{
|
||||
newName = sender.Name;
|
||||
@@ -3618,42 +3639,31 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
int tagCount = message.ReadByte();
|
||||
HashSet<Identifier> tagSet = new HashSet<Identifier>();
|
||||
for (int i = 0; i < tagCount; i++)
|
||||
{
|
||||
tagSet.Add(message.ReadIdentifier());
|
||||
}
|
||||
int hairIndex = message.ReadByte();
|
||||
int beardIndex = message.ReadByte();
|
||||
int moustacheIndex = message.ReadByte();
|
||||
int faceAttachmentIndex = message.ReadByte();
|
||||
Color skinColor = message.ReadColorR8G8B8();
|
||||
Color hairColor = message.ReadColorR8G8B8();
|
||||
Color facialHairColor = message.ReadColorR8G8B8();
|
||||
|
||||
List<JobVariant> jobPreferences = new List<JobVariant>();
|
||||
int count = message.ReadByte();
|
||||
for (int i = 0; i < Math.Min(count, 3); i++)
|
||||
{
|
||||
string jobIdentifier = message.ReadString();
|
||||
int variant = message.ReadByte();
|
||||
if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab))
|
||||
{
|
||||
if (jobPrefab.HiddenJob) { continue; }
|
||||
jobPreferences.Add(new JobVariant(jobPrefab, variant));
|
||||
}
|
||||
}
|
||||
|
||||
sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
|
||||
sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
|
||||
sender.CharacterInfo.Head.SkinColor = skinColor;
|
||||
sender.CharacterInfo.Head.HairColor = hairColor;
|
||||
sender.CharacterInfo.Head.FacialHairColor = facialHairColor;
|
||||
|
||||
if (jobPreferences.Count > 0)
|
||||
sender.CharacterInfo.RecreateHead(
|
||||
tags: netInfo.Tags.ToImmutableHashSet(),
|
||||
hairIndex: netInfo.HairIndex,
|
||||
beardIndex: netInfo.BeardIndex,
|
||||
moustacheIndex: netInfo.MoustacheIndex,
|
||||
faceAttachmentIndex: netInfo.FaceAttachmentIndex);
|
||||
|
||||
sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor;
|
||||
sender.CharacterInfo.Head.HairColor = netInfo.HairColor;
|
||||
sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor;
|
||||
|
||||
if (netInfo.JobVariants.Length > 0)
|
||||
{
|
||||
sender.JobPreferences = jobPreferences;
|
||||
List<JobVariant> variants = new List<JobVariant>();
|
||||
foreach (NetJobVariant jv in netInfo.JobVariants)
|
||||
{
|
||||
if (jv.ToJobVariant() is { } variant)
|
||||
{
|
||||
variants.Add(variant);
|
||||
}
|
||||
}
|
||||
|
||||
sender.JobPreferences = variants;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3973,8 +3983,7 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
|
||||
public void Quit()
|
||||
{
|
||||
|
||||
{
|
||||
if (started)
|
||||
{
|
||||
started = false;
|
||||
@@ -3986,7 +3995,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
ServerSettings.SaveSettings();
|
||||
|
||||
ModSender.Dispose();
|
||||
ModSender?.Dispose();
|
||||
|
||||
if (ServerSettings.SaveServerLogs)
|
||||
{
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace Barotrauma
|
||||
else if (client.Karma < 40.0f)
|
||||
herpesStrength = 30.0f;
|
||||
|
||||
var existingAffliction = client.Character.CharacterHealth.GetAffliction<AfflictionSpaceHerpes>("spaceherpes");
|
||||
var existingAffliction = client.Character.CharacterHealth.GetAffliction<AfflictionSpaceHerpes>(AfflictionPrefab.SpaceHerpesType);
|
||||
if (existingAffliction == null && herpesStrength > 0.0f)
|
||||
{
|
||||
client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength));
|
||||
@@ -170,7 +170,7 @@ namespace Barotrauma
|
||||
existingAffliction.Strength = herpesStrength;
|
||||
if (herpesStrength <= 0.0f)
|
||||
{
|
||||
client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs("invertcontrols".ToIdentifier(), 100.0f);
|
||||
client.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(AfflictionPrefab.InvertControlsType, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,8 +358,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool targetIsHusk = target.CharacterHealth?.GetAffliction<AfflictionHusk>("huskinfection")?.State == AfflictionHusk.InfectionState.Active;
|
||||
bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction<AfflictionHusk>("huskinfection")?.State == AfflictionHusk.InfectionState.Active;
|
||||
bool targetIsHusk = target.CharacterHealth?.GetAffliction<AfflictionHusk>(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active;
|
||||
bool attackerIsHusk = attacker.CharacterHealth?.GetAffliction<AfflictionHusk>(AfflictionPrefab.HuskInfectionType)?.State == AfflictionHusk.InfectionState.Active;
|
||||
//huskified characters count as enemies to healthy characters and vice versa
|
||||
if (targetIsHusk != attackerIsHusk) { isEnemy = true; }
|
||||
|
||||
@@ -614,7 +614,7 @@ namespace Barotrauma
|
||||
|
||||
if (amount < 0.0f)
|
||||
{
|
||||
float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes");
|
||||
float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength(AfflictionPrefab.SpaceHerpesType);
|
||||
var clientMemory = GetClientMemory(client);
|
||||
clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime);
|
||||
float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b);
|
||||
|
||||
+3
-3
@@ -298,7 +298,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (netServer == null) { return; }
|
||||
|
||||
PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId is Some<AccountId> { Value: SteamId id } && id.Value == steamId);
|
||||
PendingClient? pendingClient = pendingClients.Find(c => c.AccountInfo.AccountId.TryUnwrap<SteamId>(out var id) && id.Value == steamId);
|
||||
DebugConsole.Log($"{steamId} validation: {status}, {(pendingClient != null)}");
|
||||
|
||||
if (pendingClient is null)
|
||||
@@ -306,7 +306,7 @@ namespace Barotrauma.Networking
|
||||
if (status == Steamworks.AuthResponse.OK) { return; }
|
||||
|
||||
if (connectedClients.Find(c
|
||||
=> c.AccountInfo.AccountId is Some<AccountId> { Value: SteamId id } && id.Value == steamId)
|
||||
=> c.AccountInfo.AccountId.TryUnwrap<SteamId>(out var id) && id.Value == steamId)
|
||||
is LidgrenConnection connection)
|
||||
{
|
||||
Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status));
|
||||
@@ -380,7 +380,7 @@ namespace Barotrauma.Networking
|
||||
lidgrenConn.Status = NetworkConnectionStatus.Disconnected;
|
||||
connectedClients.Remove(lidgrenConn);
|
||||
callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket);
|
||||
if (conn.AccountInfo.AccountId is Some<AccountId> { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); }
|
||||
if (conn.AccountInfo.AccountId.TryUnwrap<SteamId>(out var steamId)) { SteamManager.StopAuthSession(steamId); }
|
||||
}
|
||||
|
||||
lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation());
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ namespace Barotrauma.Networking
|
||||
protected List<NetworkConnection> connectedClients = null!;
|
||||
protected List<PendingClient> pendingClients = null!;
|
||||
protected ServerSettings serverSettings = null!;
|
||||
protected Option<int> ownerKey = null!;
|
||||
protected Option<int> ownerKey = Option.None;
|
||||
protected NetworkConnection? OwnerConnection;
|
||||
|
||||
protected void ReadConnectionInitializationStep(PendingClient pendingClient, IReadMessage inc, ConnectionInitialization initializationStep)
|
||||
@@ -290,7 +290,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
pendingClients.Remove(pendingClient);
|
||||
|
||||
if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId is Some<AccountId> { Value: SteamId steamId })
|
||||
if (pendingClient.AuthSessionStarted && pendingClient.AccountInfo.AccountId.TryUnwrap<SteamId>(out var steamId))
|
||||
{
|
||||
Steam.SteamManager.StopAuthSession(steamId);
|
||||
pendingClient.Connection.SetAccountInfo(AccountInfo.None);
|
||||
|
||||
@@ -218,7 +218,10 @@ namespace Barotrauma.Networking
|
||||
|
||||
foreach (Door door in shuttleDoors)
|
||||
{
|
||||
if (door.IsOpen) door.TrySetState(false, false, true);
|
||||
if (door.IsOpen)
|
||||
{
|
||||
door.TrySetState(open: false, isNetworkMessage: false, sendNetworkMessage: true);
|
||||
}
|
||||
}
|
||||
|
||||
var shuttleGaps = Gap.GapList.FindAll(g => g.Submarine == RespawnShuttle && g.ConnectedWall != null);
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Barotrauma.Networking
|
||||
=> LastUpdateIdForFlag.Keys
|
||||
.Where(k => IsFlagRequired(c, k))
|
||||
.Aggregate(NetFlags.None, (f1, f2) => f1 | f2);
|
||||
|
||||
|
||||
partial void InitProjSpecific()
|
||||
{
|
||||
LoadSettings();
|
||||
@@ -176,7 +176,11 @@ namespace Barotrauma.Networking
|
||||
netProperties[key].Read(incMsg);
|
||||
if (!netProperties[key].PropEquals(prevValue, netProperties[key]))
|
||||
{
|
||||
GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage);
|
||||
GameServer.Log(
|
||||
NetworkMember.ClientLogName(c)
|
||||
+ $" changed {netProperties[key].Name}"
|
||||
+ $" to {netProperties[key].Value}",
|
||||
ServerLog.MessageType.ServerMessage);
|
||||
}
|
||||
propertiesChanged = true;
|
||||
}
|
||||
@@ -330,6 +334,10 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
LosMode = GameSettings.CurrentConfig.Graphics.LosMode;
|
||||
}
|
||||
if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", "")))
|
||||
{
|
||||
Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language);
|
||||
}
|
||||
|
||||
AutoRestart = doc.Root.GetAttributeBool("autorestart", false);
|
||||
|
||||
@@ -512,7 +520,7 @@ namespace Barotrauma.Networking
|
||||
else
|
||||
{
|
||||
string presetName = clientElement.GetAttributeString("preset", "");
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName);
|
||||
PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == presetName);
|
||||
if (preset == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found.");
|
||||
@@ -577,8 +585,7 @@ namespace Barotrauma.Networking
|
||||
foreach (SavedClientPermission clientPermission in ClientPermissions)
|
||||
{
|
||||
var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands));
|
||||
#warning TODO: this is broken because of localization
|
||||
if (matchingPreset != null && matchingPreset.Name == "None")
|
||||
if (matchingPreset != null && matchingPreset.Identifier == "None")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -592,7 +599,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
clientElement.Add(matchingPreset == null
|
||||
? new XAttribute("permissions", clientPermission.Permissions.ToString())
|
||||
: new XAttribute("preset", matchingPreset.Name));
|
||||
: new XAttribute("preset", matchingPreset.DisplayName));
|
||||
|
||||
if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands))
|
||||
{
|
||||
|
||||
@@ -28,13 +28,11 @@ namespace Barotrauma
|
||||
|
||||
public SubmarineInfo Sub;
|
||||
public bool TransferItems;
|
||||
public int DeliveryFee;
|
||||
|
||||
public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, int deliveryFee, VoteType voteType)
|
||||
public SubmarineVote(Client starter, SubmarineInfo subInfo, bool transferItems, VoteType voteType)
|
||||
{
|
||||
Sub = subInfo;
|
||||
TransferItems = transferItems;
|
||||
DeliveryFee = deliveryFee;
|
||||
VoteType = voteType;
|
||||
State = VoteState.Started;
|
||||
VoteStarter = starter;
|
||||
@@ -81,10 +79,10 @@ namespace Barotrauma
|
||||
if (passed)
|
||||
{
|
||||
Wallet fromWallet = From == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : From.Character?.Wallet;
|
||||
if (fromWallet.TryDeduct(TransferAmount))
|
||||
if (fromWallet != null && fromWallet.TryDeduct(TransferAmount))
|
||||
{
|
||||
Wallet toWallet = To == null ? (GameMain.GameSession.GameMode as MultiPlayerCampaign)?.Bank : To.Character?.Wallet;
|
||||
toWallet.Give(TransferAmount);
|
||||
toWallet?.Give(TransferAmount);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -109,7 +107,6 @@ namespace Barotrauma
|
||||
sender,
|
||||
subInfo,
|
||||
transferItems,
|
||||
voteType == VoteType.SwitchSub ? GameMain.GameSession.Map.DistanceToClosestLocationWithOutpost(GameMain.GameSession.Map.CurrentLocation, out Location endLocation) : 0,
|
||||
voteType);
|
||||
StartOrEnqueueVote(subVote);
|
||||
GameMain.Server.UpdateVoteStatus(checkActiveVote: false);
|
||||
@@ -206,12 +203,16 @@ namespace Barotrauma
|
||||
// Do not take unanswered into account for total
|
||||
int yes = eligibleClients.Count(c => c.GetVote<int>(ActiveVote.VoteType) == 2);
|
||||
int no = eligibleClients.Count(c => c.GetVote<int>(ActiveVote.VoteType) == 1);
|
||||
int total = Math.Max(yes + no, 1);
|
||||
|
||||
bool passed =
|
||||
yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio ||
|
||||
inGameClients.Count() == 1;
|
||||
int total = yes + no;
|
||||
|
||||
bool passed = false;
|
||||
//total can be zero if the client who initiated the vote has left
|
||||
if (total > 0)
|
||||
{
|
||||
passed =
|
||||
yes / (float)total >= GameMain.NetworkMember.ServerSettings.VoteRequiredRatio ||
|
||||
inGameClients.Count() == 1;
|
||||
}
|
||||
ActiveVote.Finish(this, passed);
|
||||
}
|
||||
}
|
||||
@@ -224,7 +225,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerRead(IReadMessage inc, Client sender)
|
||||
public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection)
|
||||
{
|
||||
if (GameMain.Server == null || sender == null) { return; }
|
||||
|
||||
@@ -336,7 +337,10 @@ namespace Barotrauma
|
||||
|
||||
inc.ReadPadBits();
|
||||
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
using (dosProtection.Pause(sender))
|
||||
{
|
||||
GameMain.Server.UpdateVoteStatus();
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerWrite(IWriteMessage msg)
|
||||
@@ -436,7 +440,6 @@ namespace Barotrauma
|
||||
var subVote = ActiveVote as SubmarineVote;
|
||||
msg.WriteString(subVote.Sub.Name);
|
||||
msg.WriteBoolean(subVote.TransferItems);
|
||||
msg.WriteInt16((short)subVote.DeliveryFee);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -158,7 +158,10 @@ namespace Barotrauma
|
||||
sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language);
|
||||
if (ContentPackageManager.EnabledPackages.All != null)
|
||||
{
|
||||
sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name))));
|
||||
sb.AppendLine("Selected content packages: " +
|
||||
(!ContentPackageManager.EnabledPackages.All.Any() ?
|
||||
"None" :
|
||||
string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})"))));
|
||||
}
|
||||
sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed));
|
||||
sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")"));
|
||||
|
||||
@@ -204,20 +204,24 @@ namespace Barotrauma
|
||||
|
||||
public void RandomizeSettings()
|
||||
{
|
||||
if (GameMain.Server.ServerSettings.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8);
|
||||
if (GameMain.Server.ServerSettings.RandomizeSeed) { LevelSeed = ToolBox.RandomSeed(8); }
|
||||
|
||||
if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random)
|
||||
//don't touch any of these settings if a campaign is running!
|
||||
if (GameMain.GameSession?.Campaign == null)
|
||||
{
|
||||
var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList();
|
||||
SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)];
|
||||
}
|
||||
if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random)
|
||||
{
|
||||
var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign);
|
||||
SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier;
|
||||
}
|
||||
if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random)
|
||||
{
|
||||
var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus) && c.IsPlayer).ToList();
|
||||
SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)];
|
||||
}
|
||||
if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random)
|
||||
{
|
||||
var allowedGameModes = Array.FindAll(GameModes, m => !m.IsSinglePlayer && m != GameModePreset.MultiPlayerCampaign);
|
||||
SelectedModeIdentifier = allowedGameModes[Rand.Range(0, allowedGameModes.Length)].Identifier;
|
||||
}
|
||||
|
||||
GameMain.Server.ServerSettings.SelectNonHiddenSubmarine();
|
||||
GameMain.Server.ServerSettings.SelectNonHiddenSubmarine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ namespace Barotrauma.Steam
|
||||
{
|
||||
partial class SteamManager
|
||||
{
|
||||
private static void InitializeProjectSpecific() { IsInitialized = true; }
|
||||
private static void InitializeProjectSpecific() { }
|
||||
|
||||
private static bool IsInitializedProjectSpecific
|
||||
=> Steamworks.SteamServer.IsValid;
|
||||
|
||||
public static bool CreateServer(Networking.GameServer server, bool isPublic)
|
||||
{
|
||||
IsInitialized = true;
|
||||
|
||||
Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma")
|
||||
{
|
||||
GamePort = (ushort)server.Port,
|
||||
@@ -56,10 +57,15 @@ namespace Barotrauma.Steam
|
||||
Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText);
|
||||
Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString());
|
||||
Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString());
|
||||
Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
|
||||
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
|
||||
Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp
|
||||
=> cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : "")));
|
||||
int index = 0;
|
||||
foreach (var contentPackage in contentPackages)
|
||||
{
|
||||
string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty;
|
||||
Steamworks.SteamServer.SetKey(
|
||||
$"contentpackage{index}",
|
||||
contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr);
|
||||
index++;
|
||||
}
|
||||
Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString());
|
||||
Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
|
||||
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());
|
||||
@@ -71,6 +77,7 @@ namespace Barotrauma.Steam
|
||||
Steamworks.SteamServer.SetKey("gamestarted", server.GameStarted.ToString());
|
||||
Steamworks.SteamServer.SetKey("gamemode", server.ServerSettings.GameModeIdentifier.Value);
|
||||
Steamworks.SteamServer.SetKey("playstyle", server.ServerSettings.PlayStyle.ToString());
|
||||
Steamworks.SteamServer.SetKey("language", server.ServerSettings.Language.ToString());
|
||||
|
||||
Steamworks.SteamServer.DedicatedServer = true;
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal sealed class DoSProtection
|
||||
{
|
||||
/// <summary>
|
||||
/// A struct that executes an action when it's created and another one when it's disposed.
|
||||
/// </summary>
|
||||
public readonly ref struct DoSAction
|
||||
{
|
||||
private readonly Client sender;
|
||||
private readonly Action<Client> end;
|
||||
|
||||
public DoSAction(Client sender, Action<Client> start, Action<Client> end)
|
||||
{
|
||||
this.sender = sender;
|
||||
this.end = end;
|
||||
start(sender);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
end(sender);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class OffenseData
|
||||
{
|
||||
/// <summary>
|
||||
/// Timer that keeps track of how long it takes to process a packet.
|
||||
/// </summary>
|
||||
public readonly Stopwatch Stopwatch = new();
|
||||
|
||||
/// <summary>
|
||||
/// Amount of strikes the client has received for causing the server to slow down.
|
||||
/// </summary>
|
||||
public int Strikes;
|
||||
|
||||
/// <summary>
|
||||
/// How many packets have been sent in the last minute.
|
||||
/// </summary>
|
||||
public int PacketCount;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the strikes and packet count.
|
||||
/// </summary>
|
||||
public void ResetStrikes()
|
||||
{
|
||||
Strikes = 0;
|
||||
PacketCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the timer.
|
||||
/// </summary>
|
||||
public void ResetTimer() => Stopwatch.Reset();
|
||||
}
|
||||
|
||||
private readonly Dictionary<Client, OffenseData> clients = new();
|
||||
|
||||
private float stopwatchResetTimer,
|
||||
strikesResetTimer;
|
||||
|
||||
private const int StopwatchResetInterval = 1,
|
||||
StrikesResetInterval = 60,
|
||||
StrikeThreshold = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server receives a packet to start logging how much time it takes to process.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to start a timer for.</param>
|
||||
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
|
||||
/// <remarks>
|
||||
/// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public void ServerRead(IReadMessage msg, Client c)
|
||||
/// {
|
||||
/// // start the timer
|
||||
/// using var _ = dosProtection.Start(connectedClient);
|
||||
///
|
||||
/// if (condition)
|
||||
/// {
|
||||
/// // the timer will be stopped here.
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// ProcessMessage(msg);
|
||||
/// // the timer will be stopped here.
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor);
|
||||
|
||||
/// <summary>
|
||||
/// Temporary pauses the timer for the client.
|
||||
/// Used when we know a packet is going to slow down the server but we don't want to count it as a strike.
|
||||
/// For example when a client is starting a round.
|
||||
/// </summary>
|
||||
/// <param name="client">The client to pause the timer for.</param>
|
||||
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
|
||||
/// <remarks>
|
||||
/// Calling resume is not required, the timer will be resumed automatically when the using block ends.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// using (dos.Pause(client))
|
||||
/// {
|
||||
/// // do something that will slow down the server
|
||||
/// }
|
||||
/// // the timer will be resumed here
|
||||
/// </code>
|
||||
/// </example>
|
||||
public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor);
|
||||
|
||||
private void StartFor(Client client)
|
||||
{
|
||||
if (!clients.ContainsKey(client))
|
||||
{
|
||||
clients.Add(client, new OffenseData());
|
||||
}
|
||||
|
||||
clients[client].Stopwatch.Start();
|
||||
}
|
||||
|
||||
private void EndFor(Client client)
|
||||
{
|
||||
if (GetData(client) is not { } data) { return; }
|
||||
|
||||
data.PacketCount++;
|
||||
data.Stopwatch.Stop();
|
||||
UpdateOffense(client, data);
|
||||
}
|
||||
|
||||
// stops the clock but doesn't update offenses
|
||||
private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop();
|
||||
|
||||
private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start();
|
||||
|
||||
private void UpdateOffense(Client client, OffenseData data)
|
||||
{
|
||||
if (GameMain.Server?.ServerSettings is not { } settings) { return; }
|
||||
|
||||
// client is sending too many packets, kick them
|
||||
if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin)
|
||||
{
|
||||
AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
|
||||
clients.Remove(client);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong
|
||||
if (data.Stopwatch.ElapsedMilliseconds < 100) { return; }
|
||||
|
||||
data.Strikes++;
|
||||
data.ResetTimer();
|
||||
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection);
|
||||
|
||||
// too many strikes, get them out of here
|
||||
if (data.Strikes < StrikeThreshold) { return; }
|
||||
|
||||
if (settings.EnableDoSProtection)
|
||||
{
|
||||
AttemptKickClient(client, TextManager.Get("DoSProtectionKicked"));
|
||||
}
|
||||
|
||||
clients.Remove(client);
|
||||
|
||||
static void AttemptKickClient(Client client, LocalizedString reason)
|
||||
{
|
||||
// ReSharper disable once ConvertToConstant.Local
|
||||
bool doesRateLimitAffectClient =
|
||||
#if DEBUG
|
||||
true; // for testing
|
||||
#else
|
||||
!RateLimiter.IsExempt(client);
|
||||
#endif
|
||||
|
||||
if (!doesRateLimitAffectClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameMain.Server?.KickClient(client, reason.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
stopwatchResetTimer += deltaTime;
|
||||
strikesResetTimer += deltaTime;
|
||||
|
||||
// reset the stopwatch every second
|
||||
if (stopwatchResetTimer > StopwatchResetInterval)
|
||||
{
|
||||
stopwatchResetTimer = 0;
|
||||
foreach (OffenseData data in clients.Values)
|
||||
{
|
||||
data.ResetTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// reset the strikes every minute
|
||||
if (strikesResetTimer > StrikesResetInterval)
|
||||
{
|
||||
strikesResetTimer = 0;
|
||||
foreach (var (client, data) in clients)
|
||||
{
|
||||
if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings)
|
||||
{
|
||||
if (data.PacketCount > settings.MaxPacketAmount * 0.9f)
|
||||
{
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection);
|
||||
}
|
||||
}
|
||||
|
||||
data.ResetStrikes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum RateLimitAction
|
||||
{
|
||||
Invalid,
|
||||
OnLimitReached,
|
||||
OnLimitDoubled,
|
||||
}
|
||||
|
||||
public enum RateLimitPunishment
|
||||
{
|
||||
None, // just ignore
|
||||
Announce, // announce to the server
|
||||
Kick, // kick the player
|
||||
Ban // ban the player
|
||||
}
|
||||
|
||||
internal sealed class RateLimiter
|
||||
{
|
||||
private sealed record RateLimit(DateTimeOffset Expiry)
|
||||
{
|
||||
public int RequestAmount;
|
||||
}
|
||||
|
||||
private readonly Dictionary<Client, RateLimit> rateLimits = new();
|
||||
private readonly HashSet<Client> expiredRateLimits = new();
|
||||
private readonly Dictionary<Client, DateTimeOffset> recentlyAnnouncedOffenders = new();
|
||||
|
||||
private readonly int maxRequests, expiryInSeconds;
|
||||
|
||||
private readonly ImmutableDictionary<RateLimitAction, RateLimitPunishment> punishments;
|
||||
|
||||
public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules)
|
||||
{
|
||||
this.maxRequests = maxRequests;
|
||||
this.expiryInSeconds = expiryInSeconds;
|
||||
|
||||
punishments = punishmentRules.ToImmutableDictionary(
|
||||
static pair => pair.Action,
|
||||
static pair => pair.Punishment);
|
||||
}
|
||||
|
||||
public bool IsLimitReached(Client client)
|
||||
{
|
||||
#if !DEBUG
|
||||
if (IsExempt(client)) { return false; }
|
||||
#endif
|
||||
expiredRateLimits.Clear();
|
||||
|
||||
foreach (var (c, limit) in rateLimits)
|
||||
{
|
||||
if (limit.Expiry < DateTimeOffset.Now)
|
||||
{
|
||||
expiredRateLimits.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Client c in expiredRateLimits)
|
||||
{
|
||||
rateLimits.Remove(c);
|
||||
}
|
||||
|
||||
if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit))
|
||||
{
|
||||
rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds));
|
||||
rateLimits.Add(client, rateLimit);
|
||||
}
|
||||
|
||||
rateLimit.RequestAmount++;
|
||||
|
||||
if (rateLimit.RequestAmount > maxRequests)
|
||||
{
|
||||
ProcessPunishment(client, rateLimit.RequestAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ProcessPunishment(Client client, int requests)
|
||||
{
|
||||
bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true };
|
||||
|
||||
foreach (var (action, punishment) in punishments)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case RateLimitAction.Invalid:
|
||||
continue;
|
||||
case RateLimitAction.OnLimitReached when requests >= maxRequests:
|
||||
case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2:
|
||||
switch (punishment)
|
||||
{
|
||||
case RateLimitPunishment.None:
|
||||
continue;
|
||||
case RateLimitPunishment.Announce:
|
||||
AnnounceOffender(client);
|
||||
break;
|
||||
case RateLimitPunishment.Ban when isDosProtectionEnabled:
|
||||
GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value);
|
||||
break;
|
||||
case RateLimitPunishment.Kick when isDosProtectionEnabled:
|
||||
GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnnounceOffender(Client client)
|
||||
{
|
||||
if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry))
|
||||
{
|
||||
if (expiry > DateTimeOffset.Now) { return; }
|
||||
|
||||
recentlyAnnouncedOffenders.Remove(client);
|
||||
}
|
||||
|
||||
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection);
|
||||
recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds));
|
||||
}
|
||||
|
||||
public static bool IsExempt(Client client) =>
|
||||
(GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection)
|
||||
|| client.HasPermission(ClientPermissions.SpamImmunity);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user