Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2024-03-28 14:26:18 -03:00
271 changed files with 13174 additions and 3021 deletions
@@ -5,11 +5,15 @@ using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
static class SteamAchievementManager
[NetworkSerialize]
internal readonly record struct NetIncrementedStat(AchievementStat Stat, float Amount) : INetSerializableStruct;
static class AchievementManager
{
private const float UpdateInterval = 1.0f;
@@ -22,7 +26,7 @@ namespace Barotrauma
/// <summary>
/// Keeps track of things that have happened during the round
/// </summary>
class RoundData
private sealed class RoundData
{
public readonly List<Reactor> Reactors = new List<Reactor>();
@@ -61,12 +65,15 @@ namespace Barotrauma
updateTimer -= deltaTime;
if (updateTimer > 0.0f) { return; }
updateTimer = UpdateInterval;
if (Level.Loaded != null && roundData != null && Screen.Selected == GameMain.GameScreen)
{
if (GameMain.GameSession.EventManager.CurrentIntensity > 0.99f)
{
UnlockAchievement("maxintensity".ToIdentifier(), true, c => c != null && !c.IsDead && !c.IsUnconscious);
UnlockAchievement(
identifier: "maxintensity".ToIdentifier(),
unlockClients: true,
conditions: static c => c is { IsDead: false, IsUnconscious: false });
}
foreach (Character c in Character.CharacterList)
@@ -273,12 +280,12 @@ namespace Barotrauma
causeOfDeath.Killer != null &&
causeOfDeath.Killer == Character.Controlled)
{
IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1);
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
}
#elif SERVER
if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
{
IncrementStat(causeOfDeath.Killer, (character.IsHuman ? "humanskilled" : "monsterskilled").ToIdentifier(), 1);
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
}
#endif
@@ -375,14 +382,14 @@ namespace Barotrauma
!myCharacter.IsDead &&
(myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost)))
{
IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers);
IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
}
#endif
}
else
{
//in sp making it to the end is enough
IncrementStat("kmstraveled".ToIdentifier(), levelLengthKilometers);
IncrementStat(AchievementStat.KMsTraveled, levelLengthKilometers);
}
}
@@ -497,31 +504,19 @@ namespace Barotrauma
#endif
}
private static void IncrementStat(Character recipient, Identifier identifier, int amount)
private static void IncrementStat(Character recipient, AchievementStat stat, int amount)
{
if (CheatsEnabled || recipient == null) { return; }
#if CLIENT
if (recipient == Character.Controlled)
{
SteamManager.IncrementStat(identifier, amount);
IncrementStat(stat, amount);
}
#elif SERVER
GameMain.Server?.IncrementStat(recipient, identifier, amount);
GameMain.Server?.IncrementStat(recipient, stat, amount);
#endif
}
public static void IncrementStat(Identifier identifier, int amount)
{
if (CheatsEnabled) { return; }
SteamManager.IncrementStat(identifier, amount);
}
public static void IncrementStat(Identifier identifier, float amount)
{
if (CheatsEnabled) { return; }
SteamManager.IncrementStat(identifier, amount);
}
public static void UnlockAchievement(Identifier identifier, bool unlockClients = false, Func<Character, bool> conditions = null)
{
if (CheatsEnabled) { return; }
@@ -539,15 +534,150 @@ namespace Barotrauma
}
}
#endif
//already unlocked, no need to do anything
if (unlockedAchievements.Contains(identifier)) { return; }
unlockedAchievements.Add(identifier);
#if CLIENT
if (conditions != null && !conditions(Character.Controlled)) { return; }
#endif
SteamManager.UnlockAchievement(identifier);
UnlockAchievementsOnPlatforms(identifier);
}
private static void UnlockAchievementsOnPlatforms(Identifier identifier)
{
if (unlockedAchievements.Contains(identifier)) { return; }
if (SteamManager.IsInitialized)
{
if (SteamManager.UnlockAchievement(identifier))
{
unlockedAchievements.Add(identifier);
}
}
if (EosInterface.Core.IsInitialized)
{
TaskPool.Add("Eos.UnlockAchievementsOnPlatforms", EosInterface.Achievements.UnlockAchievements(identifier), t =>
{
if (!t.TryGetResult(out Result<uint, EosInterface.AchievementUnlockError> result)) { return; }
if (result.IsSuccess) { unlockedAchievements.Add(identifier); }
});
}
}
public static void IncrementStat(AchievementStat stat, float amount)
{
if (CheatsEnabled) { return; }
IncrementStatOnPlatforms(stat, amount);
}
private static void IncrementStatOnPlatforms(AchievementStat stat, float amount)
{
if (SteamManager.IsInitialized)
{
SteamManager.IncrementStats(stat.ToSteam(amount));
}
if (EosInterface.Core.IsInitialized)
{
TaskPool.Add("Eos.IncrementStat", EosInterface.Achievements.IngestStats(stat.ToEos(amount)), TaskPool.IgnoredCallback);
}
}
public static void SyncBetweenPlatforms()
{
if (!SteamManager.IsInitialized || !EosInterface.Core.IsInitialized) { return; }
var steamStats = SteamManager.GetAllStats();
TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryStats", EosInterface.Achievements.QueryStats(AchievementStatExtension.EosStats), result =>
{
result.Match(
success: stats => SyncStats(stats, steamStats),
failure: static error => DebugConsole.ThrowError($"Failed to query stats from EOS: {error}"));
});
static void SyncStats(ImmutableDictionary<AchievementStat, int> eosStats,
ImmutableDictionary<AchievementStat, float> steamStats)
{
var steamStatsConverted = steamStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
var eosStatsConverted = eosStats.Select(static s => s.Key.ToEos(s.Value)).ToImmutableDictionary(static s => s.Stat, static s => s.Value);
static int GetStatValue(AchievementStat stat, ImmutableDictionary<AchievementStat, int> stats) => stats.TryGetValue(stat, out int value) ? value : 0;
var highestStats = AchievementStatExtension.EosStats.ToDictionary(
static key => key,
value =>
Math.Max(
GetStatValue(value, steamStatsConverted),
GetStatValue(value, eosStatsConverted)));
List<(AchievementStat Stat, int Value)> eosStatsToIngest = new(),
steamStatsToIncrement = new();
foreach (var (stat, value) in highestStats)
{
int steamDiff = value - GetStatValue(stat, steamStatsConverted),
eosDiff = value - GetStatValue(stat, eosStatsConverted);
if (steamDiff > 0) { steamStatsToIncrement.Add((stat, steamDiff)); }
if (eosDiff > 0) { eosStatsToIngest.Add((stat, eosDiff)); }
}
if (steamStatsToIncrement.Any())
{
SteamManager.IncrementStats(steamStatsToIncrement.Select(static s => s.Stat.ToSteam(s.Value)).ToArray());
SteamManager.StoreStats();
}
if (eosStatsToIngest.Any())
{
TaskPool.Add("Eos.SyncBetweenPlatforms.IngestStats", EosInterface.Achievements.IngestStats(eosStatsToIngest.ToArray()), TaskPool.IgnoredCallback);
}
}
if (!SteamManager.TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> steamUnlockedAchievements))
{
DebugConsole.ThrowError("Failed to query unlocked achievements from Steam");
return;
}
TaskPool.AddWithResult("Eos.SyncBetweenPlatforms.QueryPlayerAchievements", EosInterface.Achievements.QueryPlayerAchievements(), t =>
{
t.Match(
success: eosAchievements => SyncAchievements(eosAchievements, steamUnlockedAchievements),
failure: static error => DebugConsole.ThrowError($"Failed to query achievements from EOS: {error}"));
});
static void SyncAchievements(
ImmutableDictionary<Identifier, double> eosAchievements,
List<Steamworks.Data.Achievement> steamUnlockedAchievements)
{
foreach (var (identifier, progress) in eosAchievements)
{
if (!IsUnlocked(progress)) { continue; }
if (steamUnlockedAchievements.Any(a => a.Identifier.ToIdentifier() == identifier)) { continue; }
SteamManager.UnlockAchievement(identifier);
}
List<Identifier> eosAchievementsToUnlock = new();
foreach (var achievement in steamUnlockedAchievements)
{
Identifier identifier = achievement.Identifier.ToIdentifier();
if (eosAchievements.TryGetValue(identifier, out double progress) && IsUnlocked(progress)) { continue; }
eosAchievementsToUnlock.Add(achievement.Identifier.ToIdentifier());
}
if (eosAchievementsToUnlock.Any())
{
TaskPool.Add("Eos.SyncBetweenPlatforms.UnlockAchievements", EosInterface.Achievements.UnlockAchievements(eosAchievementsToUnlock.ToArray()), TaskPool.IgnoredCallback);
}
static bool IsUnlocked(double progress) => progress >= 100.0d;
}
}
}
}
@@ -1566,7 +1566,7 @@ namespace Barotrauma
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
SteamAchievementManager.OnCharacterRevived(target, character);
AchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f), stun: 0.0f);
@@ -1276,14 +1276,11 @@ namespace Barotrauma
return newCharacter;
}
private Character(Submarine submarine, ushort id): base(submarine, id)
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true)
: base(null, id)
{
wallet = new Wallet(Option<Character>.Some(this));
}
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true)
: this(null, id)
{
this.Seed = seed;
this.Prefab = prefab;
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
@@ -2485,7 +2482,7 @@ namespace Barotrauma
return false;
}
public Item GetEquippedItem(Identifier? tagOrIdentifier = null, InvSlotType? slotType = null)
public Item GetEquippedItem(Identifier tagOrIdentifier = default, InvSlotType? slotType = null)
{
if (Inventory == null) { return null; }
for (int i = 0; i < Inventory.Capacity; i++)
@@ -2500,7 +2497,7 @@ namespace Barotrauma
}
var item = Inventory.GetItemAt(i);
if (item == null) { continue; }
if (tagOrIdentifier == null || tagOrIdentifier.Value.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier.Value))
if (tagOrIdentifier.IsEmpty || item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier))
{
return item;
}
@@ -4677,7 +4674,7 @@ namespace Barotrauma
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
{
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
AchievementManager.OnCharacterKilled(this, CauseOfDeath);
}
GameMain.LuaCs.Hook.Call("character.death", this, causeOfDeathAffliction);
@@ -1459,7 +1459,7 @@ namespace Barotrauma
charElement.Add(new XAttribute("missionscompletedsincedeath", MissionsCompletedSinceDeath));
if (MinReputationToHire.factionId != default)
if (!MinReputationToHire.factionId.IsEmpty)
{
charElement.Add(
new XAttribute("factionId", MinReputationToHire.factionId),
@@ -576,7 +576,7 @@ namespace Barotrauma
matchingAfflictions.RemoveAt(i);
if (i == 0) { i = matchingAfflictions.Count; }
if (i > 0) { reduceAmount += surplus / i; }
SteamAchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
AchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
}
else
{
@@ -793,7 +793,7 @@ namespace Barotrauma
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
newAffliction.Source);
afflictions.Add(copyAffliction, limbHealth);
SteamAchievementManager.OnAfflictionReceived(copyAffliction, Character);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character);
Character.HealthUpdateInterval = 0.0f;
@@ -834,7 +834,7 @@ namespace Barotrauma
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
AchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
@@ -18,7 +18,7 @@ namespace Barotrauma.Abilities
{
if (tags.None())
{
return character.GetEquippedItem(null) != null;
return character.GetEquippedItem(Identifier.Empty) != null;
}
if (requireAll)
@@ -16,7 +16,7 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless
{
if (tags.Count is 0)
{
return HasItemInHand(character, null);
return HasItemInHand(character, Identifier.Empty);
}
foreach (Identifier tag in tags)
@@ -26,7 +26,7 @@ internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless
return false;
static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) =>
static bool HasItemInHand(Character character, Identifier tagOrIdentifier) =>
character.GetEquippedItem(tagOrIdentifier, InvSlotType.RightHand) is not null ||
character.GetEquippedItem(tagOrIdentifier, InvSlotType.LeftHand) is not null;
@@ -54,7 +54,7 @@ namespace Barotrauma
public static Color GenerateColor(string name)
{
Random random = new Random(ToolBox.StringToInt(name));
return ToolBox.HSVToRGB(random.NextSingle() * 360f, 1f, 1f);
return ToolBoxCore.HSVToRGB(random.NextSingle() * 360f, 1f, 1f);
}
private const float UpdateTimeout = 5f;
@@ -9,7 +9,7 @@ namespace Barotrauma
{
using var md5 = MD5.Create();
#warning TODO: this doesn't account for collisions, this should probably be using the PrefabCollection class like everything else
UintIdentifier = ToolBox.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5);
UintIdentifier = ToolBoxCore.StringToUInt32Hash(Barotrauma.IO.Path.GetFileNameWithoutExtension(path.Value), md5);
}
public readonly UInt32 UintIdentifier;
@@ -72,9 +72,9 @@ namespace Barotrauma
if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; }
if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= installTime.ToUtcValue();
Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
if (!itemOption.TryUnwrap(out var item)) { return true; }
return item.LatestUpdateTime <= installTime.ToUtcValue();
}
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -306,7 +306,7 @@ namespace Barotrauma
{
onLoadFail?.Invoke(
fileListPath,
result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable"));
result.TryUnwrapFailure(out var exception) ? exception : throw new UnreachableCodeException());
continue;
}
@@ -497,7 +497,7 @@ namespace Barotrauma
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
#if CLIENT
TaskPool.Add("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { });
TaskPool.AddWithResult("EnqueueWorkshopUpdates", EnqueueWorkshopUpdates(), t => { });
#else
#warning TODO: implement Workshop updates for servers at some point
#endif
@@ -20,6 +20,7 @@ namespace Barotrauma
Element = element;
}
[return: NotNullIfNotNull("cxe")]
public static implicit operator XElement?(ContentXElement? cxe) => cxe?.Element;
//public static implicit operator ContentXElement?(XElement? xe) => xe is null ? null : new ContentXElement(null, xe);
@@ -1,169 +0,0 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
{
// Identifier struct to eliminate case-sensitive comparisons
public readonly struct Identifier : IComparable, IEquatable<Identifier>
{
public readonly static Identifier Empty = default;
private readonly static int emptyHash = "".GetHashCode(StringComparison.OrdinalIgnoreCase);
private readonly string? value;
private readonly Lazy<int>? hashCode;
public string Value => value ?? "";
public int HashCode => hashCode?.Value ?? emptyHash;
public Identifier(string? str)
{
value = str;
hashCode = new Lazy<int>(() => (str ?? "").GetHashCode(StringComparison.OrdinalIgnoreCase));
}
public bool IsEmpty => Value.IsNullOrEmpty();
public Identifier IfEmpty(in Identifier id)
=> IsEmpty ? id : this;
public Identifier Replace(in Identifier subStr, in Identifier newStr)
=> Replace(subStr.Value ?? "", newStr.Value ?? "");
public Identifier Replace(string subStr, string newStr)
=> (Value?.Replace(subStr, newStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public Identifier Remove(Identifier subStr)
=> Remove(subStr.Value ?? "");
public Identifier Remove(string subStr)
=> (Value?.Remove(subStr, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public override bool Equals(object? obj) =>
obj switch
{
Identifier i => this == i,
string s => this == s,
_ => base.Equals(obj)
};
public bool StartsWith(string str) => Value?.StartsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool StartsWith(Identifier id) => StartsWith(id.Value ?? "");
public bool EndsWith(string str) => Value?.EndsWith(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool EndsWith(Identifier id) => EndsWith(id.Value ?? "");
public Identifier AppendIfMissing(string suffix)
=> EndsWith(suffix) ? this : $"{this}{suffix}".ToIdentifier();
public Identifier RemoveFromEnd(string suffix)
=> (Value?.RemoveFromEnd(suffix, StringComparison.OrdinalIgnoreCase)).ToIdentifier();
public bool Contains(string str) => Value?.Contains(str, StringComparison.OrdinalIgnoreCase) ?? str.IsNullOrEmpty();
public bool Contains(in Identifier id) => Contains(id.Value ?? "");
public override string ToString() => Value ?? "";
public override int GetHashCode() => HashCode;
public int CompareTo(object? obj)
{
return string.Compare(Value, obj?.ToString() ?? "", StringComparison.InvariantCultureIgnoreCase);
}
public bool Equals([AllowNull] Identifier other)
{
return this == other;
}
private static bool StringEquality(string? a, string? b)
=> (a.IsNullOrEmpty() && b.IsNullOrEmpty()) || string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
public static bool operator ==(in Identifier a, in Identifier b) =>
StringEquality(a.Value, b.Value);
public static bool operator !=(in Identifier a, in Identifier b) =>
!(a == b);
public static bool operator ==(in Identifier identifier, string? str) =>
StringEquality(identifier.Value, str);
public static bool operator !=(in Identifier identifier, string? str) =>
!(identifier == str);
public static bool operator ==(string? str, in Identifier identifier) =>
identifier == str;
public static bool operator !=(string? str, in Identifier identifier) =>
!(identifier == str);
public static bool operator ==(in Identifier? a, in Identifier? b) =>
StringEquality(a?.Value, b?.Value);
public static bool operator !=(in Identifier? a, in Identifier? b) =>
!(a == b);
public static bool operator ==(in Identifier? a, string? b) =>
StringEquality(a?.Value, b);
public static bool operator !=(in Identifier? a, string? b) =>
!(a == b);
public static bool operator ==(string str, in Identifier? identifier) =>
identifier == str;
public static bool operator !=(string str, in Identifier? identifier) =>
!(identifier == str);
public static implicit operator Identifier(string str)
{
return new Identifier(str);
}
internal int IndexOf(char c) => Value.IndexOf(c);
internal Identifier this[Range range] => Value[range].ToIdentifier();
internal Char this[int i] => Value[i];
}
public static class IdentifierExtensions
{
public static IEnumerable<Identifier> ToIdentifiers(this IEnumerable<string> strings)
{
foreach (string s in strings)
{
if (string.IsNullOrEmpty(s)) { continue; }
yield return new Identifier(s);
}
}
public static Identifier[] ToIdentifiers(this string[] strings)
=> ((IEnumerable<string>)strings).ToIdentifiers().ToArray();
public static Identifier ToIdentifier(this string? s)
{
return new Identifier(s);
}
public static Identifier ToIdentifier<T>(this T t) where T: notnull
{
return t.ToString().ToIdentifier();
}
public static bool Contains(this ISet<Identifier> set, string identifier)
{
return set.Contains(identifier.ToIdentifier());
}
public static bool ContainsKey<T>(this IReadOnlyDictionary<Identifier, T> dictionary, string key)
{
return dictionary.ContainsKey(key.ToIdentifier());
}
}
}
@@ -1,8 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Steam;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
@@ -12,8 +10,9 @@ using System.ComponentModel;
using System.Globalization;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Barotrauma.MapCreatures.Behavior;
using System.Text;
namespace Barotrauma
{
@@ -80,9 +79,7 @@ namespace Barotrauma
{
NewMessage(
$"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red);
#if USE_STEAM
NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red);
#endif
return;
}
@@ -183,7 +180,6 @@ namespace Barotrauma
#if DEBUG
CheatsEnabled = true;
#endif
commands.Add(new Command("help", "", (string[] args) =>
{
if (args.Length == 0)
@@ -1816,7 +1812,7 @@ namespace Barotrauma
NewMessage((GameSettings.CurrentConfig.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
}, isCheat: false));
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); }));
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(line => DebugConsole.NewMessage(line)); }));
commands.Add(new Command("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); }));
@@ -0,0 +1,117 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
using Barotrauma.Steam;
namespace Barotrauma.Eos;
static class EosSessionManager
{
public static Option<EosInterface.Sessions.OwnedSession> CurrentOwnedSession;
public static void LeaveSession()
{
if (!CurrentOwnedSession.TryUnwrap(out var ownedSession)) { return; }
ownedSession.Dispose();
CurrentOwnedSession = Option.None;
}
public static void UpdateOwnedSession(Endpoint endpoint, ServerSettings serverSettings)
=> UpdateOwnedSession(Option.Some(endpoint), serverSettings);
public static void UpdateOwnedSession(Option<Endpoint> endpoint, ServerSettings serverSettings)
{
if (!EosInterface.Core.IsInitialized) { return; }
if (!serverSettings.IsPublic)
{
// Sessions can only be public, so if that's not what we want then
// destroy the current one if it exists and do not attempt to create
// or update one
LeaveSession();
return;
}
var selfPuids = EosInterface.IdQueries.GetLoggedInPuids();
if (!CurrentOwnedSession.TryUnwrap(out var ownedSession))
{
if (!TaskPool.IsTaskRunning("CreateOwnedSession"))
{
TaskPool.Add(
"CreateOwnedSession",
EosInterface.Sessions.CreateSession(selfPuids.Any() ? Option.Some(selfPuids.First()) : Option.None, internalId: "OwnedSession".ToIdentifier(), maxPlayers: serverSettings.MaxPlayers),
t =>
{
LeaveSession();
if (!t.TryGetResult(out Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>? result)) { return; }
if (!result.TryUnwrapSuccess(out var newOwnedSession))
{
if (result.TryUnwrapFailure(out var error) &&
error is EosInterface.Sessions.CreateError.SessionAlreadyExists)
{
// If the session already exists then this failure is not a problem
return;
}
DebugConsole.ThrowError($"Failed to create session: {result}");
return;
}
CurrentOwnedSession = Option.Some(newOwnedSession);
UpdateOwnedSession(endpoint, serverSettings);
});
}
return;
}
if (selfPuids.Length > 0)
{
endpoint = Option<Endpoint>.Some(new EosP2PEndpoint(selfPuids.First()));
}
ownedSession.HostAddress = endpoint.Select(e1 => e1.StringRepresentation);
if (endpoint.TryUnwrap(out var e2) && e2 is LidgrenEndpoint { Port: var port })
{
SetAttributeValue("Port".ToIdentifier(), port.ToString());
}
else if (serverSettings.Port != 0)
{
SetAttributeValue("Port".ToIdentifier(), serverSettings.Port.ToString());
}
if (SteamManager.GetSteamId().TryUnwrap(out var steamId))
{
SetAttributeValue("SteamP2PEndpoint".ToIdentifier(), steamId.StringRepresentation);
}
serverSettings.UpdateServerListInfo(SetAttributeValue);
TaskPool.Add(
"UpdateOwnedSessionAttributes",
ownedSession.UpdateAttributes(),
t =>
{
if (!t.TryGetResult(out Result<Unit, EosInterface.Sessions.AttributeUpdateError>? result)) { return; }
DebugConsole.Log($"EOS UpdateOwnedSessionAttributes result: {result}");
});
void SetAttributeValue(Identifier attributeKey, object value)
{
string valueStr = value.ToString() ?? "";
if (attributeKey == "contentpackages" && value is IEnumerable<ContentPackage> contentPackages)
{
int contentPackageIndex = 0;
foreach (var contentPackage in contentPackages)
{
ownedSession.Attributes[$"contentpackage{contentPackageIndex}".ToIdentifier()]
= new ServerListContentPackageInfo(contentPackage).ToString();
contentPackageIndex++;
}
}
else
{
ownedSession.Attributes[attributeKey] = valueStr;
}
}
}
}
@@ -465,17 +465,17 @@ namespace Barotrauma
public float GetCommonness(Level level)
{
if (level.GenerationParams?.Identifier != null &&
if (level.GenerationParams?.Identifier is { IsEmpty: false } &&
OverrideCommonness.TryGetValue(level.GenerationParams.Identifier, out float generationParamsCommonness))
{
return generationParamsCommonness;
}
else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
else if (level.StartOutpost?.Info.OutpostGenerationParams?.Identifier is { IsEmpty: false } &&
OverrideCommonness.TryGetValue(level.StartOutpost.Info.OutpostGenerationParams.Identifier, out float startOutpostParamsCommonness))
{
return startOutpostParamsCommonness;
}
else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier != null &&
else if (level.EndOutpost?.Info.OutpostGenerationParams?.Identifier is { IsEmpty: false } &&
OverrideCommonness.TryGetValue(level.EndOutpost.Info.OutpostGenerationParams.Identifier, out float endOutpostParamsCommonness))
{
return endOutpostParamsCommonness;
@@ -1,54 +0,0 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class ColorExtensions
{
public static Color Multiply(this Color color, float value, bool onlyAlpha = false)
{
return onlyAlpha ?
new Color(color.R, color.G, color.B, (byte)(color.A * value)) :
new Color((byte)(color.R * value), (byte)(color.G * value), (byte)(color.B * value), (byte)(color.A * value));
}
public static Color Multiply(this Color thisColor, Color color)
{
return new Color((byte)(thisColor.R * color.R / 255f), (byte)(thisColor.G * color.G / 255f), (byte)(thisColor.B * color.B / 255f), (byte)(thisColor.A * color.A / 255f));
}
public static Color Opaque(this Color color)
{
return new Color(color.R, color.G, color.B, (byte)255);
}
private static bool IsFirstColorChannelDominant(byte first, byte second, byte third, float minimumRatio = 2)
=> first > second * minimumRatio && first > third * minimumRatio;
/// <summary>
/// Is the value of the red channel at least 'minimumRatio' larger than the blue and green
/// </summary>
public static bool IsRedDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.R,
color.G, color.B, minimumRatio);
/// <summary>
/// Is the value of the green channel at least 'minimumRatio' larger than the red and blue
/// </summary>
public static bool IsGreenDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.G,
color.R, color.B, minimumRatio);
/// <summary>
/// Is the value of the blue channel at least 'minimumRatio' larger than the red and green
/// </summary>
public static bool IsBlueDominant(Color color, float minimumRatio = 2, byte minimumAlpha = 0)
=> color.A > minimumAlpha &&
IsFirstColorChannelDominant(
first: color.B,
color.G, color.R, minimumRatio);
}
}
@@ -185,27 +185,6 @@ namespace Barotrauma.Extensions
if (value != null) { source.Add(value); }
}
public static ImmutableDictionary<TKey, TValue> ToImmutableDictionary<TKey, TValue>(this IEnumerable<(TKey, TValue)> enumerable)
{
return enumerable.ToDictionary().ToImmutableDictionary();
}
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<(TKey, TValue)> enumerable)
{
var dictionary = new Dictionary<TKey, TValue>();
foreach (var (k,v) in enumerable)
{
dictionary.Add(k, v);
}
return dictionary;
}
public static Dictionary<TKey, TValue> ToMutable<TKey, TValue>(this ImmutableDictionary<TKey, TValue> immutableDictionary)
{
if (immutableDictionary == null) { return null; }
return new Dictionary<TKey, TValue>(immutableDictionary);
}
public static NetCollection<T> ToNetCollection<T>(this IEnumerable<T> enumerable) => new NetCollection<T>(enumerable.ToImmutableArray());
/// <summary>
@@ -236,87 +215,5 @@ namespace Barotrauma.Extensions
public static IReadOnlyList<T> ListConcat<T>(this IEnumerable<T> self, IEnumerable<T> other)
=> new ListConcat<T>(self, other);
/// <summary>
/// Returns the maximum element in a given enumerable, or null if there
/// aren't any elements in the input.
/// </summary>
/// <param name="enumerable">Input collection</param>
/// <returns>Maximum element or null</returns>
public static T? MaxOrNull<T>(this IEnumerable<T> enumerable) where T : struct, IComparable<T>
{
T? retVal = null;
foreach (T v in enumerable)
{
if (!retVal.HasValue || v.CompareTo(retVal.Value) > 0) { retVal = v; }
}
return retVal;
}
public static TOut? MaxOrNull<TIn, TOut>(this IEnumerable<TIn> enumerable, Func<TIn, TOut> conversion)
where TOut : struct, IComparable<TOut>
=> enumerable.Select(conversion).MaxOrNull();
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> predicate)
{
for (int i=0; i<list.Count; i++)
{
if (predicate(list[i])) { return i; }
}
return -1;
}
/// <summary>
/// Same as FirstOrDefault but will always return null instead of default(T) when no element is found
/// </summary>
public static T? FirstOrNull<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : struct
{
if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
public static T? FirstOrNull<T>(this IEnumerable<T> source) where T : struct
{
if (source.FirstOrDefault() is var first && !first.Equals(default(T)))
{
return first;
}
return null;
}
public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> source) where T : struct
=> source
.Where(nullable => nullable.HasValue)
.Select(nullable => nullable.Value);
public static IEnumerable<T> NotNull<T>(this IEnumerable<T> source) where T : class
=> source
.Where(nullable => nullable != null)
.Select(nullable => nullable!);
public static IEnumerable<T> NotNone<T>(this IEnumerable<Option<T>> source)
{
foreach (var o in source)
{
if (o.TryUnwrap(out var v)) { yield return v; }
}
}
public static IEnumerable<TSuccess> Successes<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
=> source
.OfType<Success<TSuccess, TFailure>>()
.Select(s => s.Value);
public static IEnumerable<TFailure> Failures<TSuccess, TFailure>(
this IEnumerable<Result<TSuccess, TFailure>> source)
=> source
.OfType<Failure<TSuccess, TFailure>>()
.Select(f => f.Error);
}
}
@@ -1,61 +0,0 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class PointExtensions
{
public static Point Multiply(this Point p, float f)
{
return new Point((int)(p.X * f), (int)(p.Y * f));
}
public static Point Multiply(this Point p, int i)
{
return new Point(p.X * i, p.Y * i);
}
public static Point Multiply(this Point p, Vector2 v)
{
return new Point((int)(p.X * v.X), (int)(p.Y * v.Y));
}
public static Point Divide(this Point p, int i)
{
if (i == 0) { return Point.Zero; }
return new Point(p.X / i, p.Y / i);
}
public static Point Divide(this Point p, float f)
{
if (f == 0) { return Point.Zero; }
return new Point((int)(p.X / f), (int)(p.Y / f));
}
public static Point Divide(this Point p, Vector2 v)
{
if (v.X == 0 || v.Y == 0) { return Point.Zero; }
return new Point((int)(p.X / v.X), (int)(p.Y / v.Y));
}
/// <summary>
/// Negates the X and Y components.
/// </summary>
public static Point Inverse(this Point p)
{
return new Point(-p.X, -p.Y);
}
/// <summary>
/// Flips the X and Y components.
/// </summary>
public static Point Flip(this Point p)
{
return new Point(p.Y, p.X);
}
public static Point Clamp(this Point p, Point min, Point max)
{
return new Point(MathHelper.Clamp(p.X, min.X, max.X), MathHelper.Clamp(p.Y, min.Y, max.Y));
}
}
}
@@ -1,99 +0,0 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class RectangleExtensions
{
public static Rectangle Multiply(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) * f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.MultiplySize(f));
}
public static Rectangle Divide(this Rectangle rect, float f)
{
Vector2 location = new Vector2(rect.X, rect.Y) / f;
return new Rectangle(new Point((int)location.X, (int)location.Y), rect.DivideSize(f));
}
public static Point DivideSize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width / f), (int)(rect.Height / f));
}
public static Point DivideSize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width / f.X), (int)(rect.Height / f.Y));
}
public static Point MultiplySize(this Rectangle rect, float f)
{
return new Point((int)(rect.Width * f), (int)(rect.Height * f));
}
public static Point MultiplySize(this Rectangle rect, Vector2 f)
{
return new Point((int)(rect.Width * f.X), (int)(rect.Height * f.Y));
}
public static Vector2 CalculateRelativeSize(this Rectangle rect, Rectangle relativeRect)
{
return new Vector2(rect.Width, rect.Height) / new Vector2(relativeRect.Width, relativeRect.Height);
}
public static Rectangle ScaleSize(this Rectangle rect, Rectangle relativeTo)
{
return rect.ScaleSize(rect.CalculateRelativeSize(relativeTo));
}
public static Rectangle ScaleSize(this Rectangle rect, Vector2 scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static Rectangle ScaleSize(this Rectangle rect, float scale)
{
var size = rect.MultiplySize(scale);
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
}
public static bool IntersectsWorld(this Rectangle rect, Rectangle value)
{
int bottom = rect.Y - rect.Height;
int otherBottom = value.Y - value.Height;
return value.Left < rect.Right && rect.Left < value.Right &&
value.Top > bottom && rect.Top > otherBottom;
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Rectangle other)
{
return
(rect.X <= other.X) && ((other.X + other.Width) <= (rect.X + rect.Width)) &&
(rect.Y >= other.Y) && ((other.Y - other.Height) >= (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Vector2 point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
/// <summary>
/// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower.
/// </summary>
public static bool ContainsWorld(this Rectangle rect, Point point)
{
return
(rect.X <= point.X) && (point.X < (rect.X + rect.Width)) &&
(rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height));
}
}
}
@@ -6,25 +6,13 @@ namespace Barotrauma
{
static class StringExtensions
{
public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s;
public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrEmpty(s);
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this string? s) => string.IsNullOrWhiteSpace(s);
public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrEmpty() ?? true;
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsPathNullOrWhiteSpace() ?? true;
public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value);
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value);
public static bool IsNullOrEmpty([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty();
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace();
public static string RemoveFromEnd(this string s, string substr, StringComparison stringComparison = StringComparison.Ordinal)
=> s.EndsWith(substr, stringComparison) ? s.Substring(0, s.Length - substr.Length) : s;
public static bool IsTrueString(this string s)
=> s.Length == 4
&& s[0] is 'T' or 't'
&& s[1] is 'R' or 'r'
&& s[2] is 'U' or 'u'
&& s[3] is 'E' or 'e';
}
}
@@ -1,179 +0,0 @@
using System.Globalization;
using Microsoft.Xna.Framework;
using System.Linq;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
public static class StringFormatter
{
public static string Replace(this string s, string replacement, Func<char, bool> predicate)
{
var newString = new string[s.Length];
for (int i = 0; i < s.Length; i++)
{
char letter = s[i];
string newLetter = letter.ToString();
if (predicate(letter))
{
newLetter = replacement;
}
newString[i] = newLetter;
}
return new string(newString.SelectMany(str => str.ToCharArray()).ToArray());
}
public static string Remove(this string s, string substring, StringComparison comparisonType = StringComparison.Ordinal)
{
return s.Replace(substring, string.Empty, comparisonType);
}
public static string Remove(this string s, Func<char, bool> predicate)
{
return new string(s.ToCharArray().Where(c => !predicate(c)).ToArray());
}
public static string RemoveWhitespace(this string s)
{
return s.Remove(c => char.IsWhiteSpace(c));
}
public static string FormatSingleDecimal(this float value)
{
return value.ToString("F1", CultureInfo.InvariantCulture);
}
public static string FormatDoubleDecimal(this float value)
{
return value.ToString("F2", CultureInfo.InvariantCulture);
}
public static string FormatZeroDecimal(this float value)
{
return value.ToString("F0", CultureInfo.InvariantCulture);
}
public static string Format(this float value, int decimalCount)
{
return value.ToString($"F{decimalCount}", CultureInfo.InvariantCulture);
}
public static string FormatSingleDecimal(this Vector2 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector3 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()})";
}
public static string FormatSingleDecimal(this Vector4 value)
{
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()}, {value.Z.FormatSingleDecimal()}, {value.W.FormatSingleDecimal()})";
}
public static string FormatDoubleDecimal(this Vector2 value)
{
return $"({value.X.FormatDoubleDecimal()}, {value.Y.FormatDoubleDecimal()})";
}
public static string FormatZeroDecimal(this Vector2 value)
{
return $"({value.X.FormatZeroDecimal()}, {value.Y.FormatZeroDecimal()})";
}
public static string Format(this Vector2 value, int decimalCount)
{
return $"({value.X.Format(decimalCount)}, {value.Y.Format(decimalCount)})";
}
/// <summary>
/// Capitalises the first letter (invariant) and forces the rest to lower case (invariant).
/// </summary>
public static string CapitaliseFirstInvariant(this string s)
{
if (string.IsNullOrEmpty(s)) { return string.Empty; }
return s.Substring(0, 1).ToUpperInvariant() + s.Substring(1, s.Length - 1).ToLowerInvariant();
}
/// <summary>
/// Adds spaces into a CamelCase string.
/// </summary>
public static string FormatCamelCaseWithSpaces(this string str)
{
return new string(InsertSpacesBeforeCaps(str).ToArray());
IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
{
int i = 0;
int lastChar = input.Count() - 1;
foreach (char c in input)
{
if (char.IsUpper(c) && i > 0)
{
yield return ' ';
}
yield return c;
i++;
}
}
}
public static ICollection<string> ParseCommaSeparatedStringToCollection(string input, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(','))
{
if (string.IsNullOrWhiteSpace(value)) { continue; }
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
public static ICollection<string> ParseSeparatedStringToCollection(string input, string[] separators, ICollection<string> texts = null, bool convertToLowerInvariant = true)
{
if (texts == null)
{
texts = new HashSet<string>();
}
else
{
texts.Clear();
}
if (!string.IsNullOrWhiteSpace(input))
{
foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries))
{
if (convertToLowerInvariant)
{
texts.Add(value.ToLowerInvariant());
}
else
{
texts.Add(value);
}
}
}
return texts;
}
}
}
@@ -1,16 +0,0 @@
namespace Barotrauma.Extensions
{
public static class StructExtensions
{
public static bool TryGetValue<T>(this T? nullableStruct, out T nonNullable) where T : struct
{
if (nullableStruct.HasValue)
{
nonNullable = nullableStruct.Value;
return true;
}
nonNullable = default(T);
return false;
}
}
}
@@ -1,103 +0,0 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Extensions
{
public static class VectorExtensions
{
/// <summary>
/// Unity's Angle implementation without the conversion to degrees.
/// Returns the angle in radians between two vectors.
/// 0 - Pi.
/// </summary>
public static float Angle(this Vector2 from, Vector2 to)
{
return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f));
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Forward(float radians, float length = 1)
{
return new Vector2((float)Math.Cos(radians), (float)Math.Sin(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians).
/// </summary>
public static Vector2 Backward(float radians, float length = 1)
{
return -Forward(radians, length);
}
/// <summary>
/// Creates a forward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 ForwardFlipped(float radians, float length = 1)
{
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length;
}
/// <summary>
/// Creates a backward pointing vector based on the rotation (in radians). TODO: remove when the implications have been neutralized
/// </summary>
public static Vector2 BackwardFlipped(float radians, float length = 1)
{
return -ForwardFlipped(radians, length);
}
/// <summary>
/// Creates a normalized perpendicular vector to the right from a forward vector.
/// </summary>
public static Vector2 Right(this Vector2 forward)
{
var normV = Vector2.Normalize(forward);
return new Vector2(normV.Y, -normV.X);
}
/// <summary>
/// Creates a normalized perpendicular vector to the left from a forward vector.
/// </summary>
public static Vector2 Left(this Vector2 forward)
{
return -forward.Right();
}
/// <summary>
/// Transforms a vector relative to the given up vector.
/// </summary>
public static Vector2 TransformVector(this Vector2 v, Vector2 up)
{
up = Vector2.Normalize(up);
return (up * v.Y) + (up.Right() * v.X);
}
/// <summary>
/// Flips the x and y components.
/// </summary>
public static Vector2 Flip(this Vector2 v) => new Vector2(v.Y, v.X);
/// <summary>
/// Returns the sum of the x and y components.
/// </summary>
public static float Combine(this Vector2 v) => v.X + v.Y;
public static Vector2 Clamp(this Vector2 v, Vector2 min, Vector2 max)
{
return Vector2.Clamp(v, min, max);
}
public static bool NearlyEquals(this Vector2 v, Vector2 other)
{
return MathUtils.NearlyEqual(v.X, other.X) && MathUtils.NearlyEqual(v.Y, other.Y);
}
public static Vector2 Pad(this Vector2 v, Vector4 padding)
{
v.X += padding.X + padding.Z;
v.Y += padding.Y + padding.W;
return v;
}
}
}
@@ -1,7 +1,8 @@
#nullable enable
#nullable enable
using Barotrauma.Steam;
using RestSharp;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
@@ -13,7 +14,7 @@ namespace Barotrauma
/// The protocol used to communicate with the remote consent server may change.
/// This number tells the server which version the game is using so we can implement backwards-compatibility.
/// </summary>
private const string RemoteRequestVersion = "2";
private const string RemoteRequestVersion = "3";
public enum Consent
{
@@ -47,19 +48,66 @@ namespace Barotrauma
public static bool SendUserStatistics => UserConsented == Consent.Yes && loadedImplementation != null;
private static bool consentTextAvailable
private static bool ConsentTextAvailable
=> TextManager.ContainsTag("statisticsconsentheader")
&& TextManager.ContainsTag("statisticsconsenttext");
private const string consentServerUrl = "https://barotraumagame.com/baromaster/";
private const string consentServerFile = "consentserver.php";
private static async Task<string> GetAuthTicket()
enum Platform
{
var ticketOption = await SteamManager.GetAuthTicketForGameAnalyticsConsent();
if (!ticketOption.TryUnwrap(out var authTicket) || authTicket.Data is null) { return ""; }
//convert byte array to hex
return BitConverter.ToString(authTicket.Data).Replace("-", "");
Steam,
EOS,
None
}
private class AuthTicket
{
public readonly string Token;
public readonly Platform Platform;
public AuthTicket(string token, Platform platform)
{
Token = token ?? string.Empty;
Platform = platform;
}
}
private static async Task<AuthTicket> GetAuthTicket()
{
if (SteamManager.IsInitialized)
{
return await GetSteamAuthTicket();
}
else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
{
return await GetEOSAuthTicket();
}
return new AuthTicket(string.Empty, Platform.None);
}
private static async Task<AuthTicket> GetSteamAuthTicket()
{
var authTicket = await SteamManager.GetAuthTicketForGameAnalyticsConsent();
return authTicket.TryUnwrap(out var ticketUnwrapped) && ticketUnwrapped.Data is { Length: > 0 }
? new AuthTicket(ToolBoxCore.ByteArrayToHexString(ticketUnwrapped.Data), Platform.Steam) //convert byte array to hex
: throw new Exception("Could not retrieve Steamworks authentication ticket for GameAnalytics");
}
private static async Task<AuthTicket> GetEOSAuthTicket()
{
var puid = EosInterface.IdQueries.GetLoggedInPuids().First();
var tokenResult = EosInterface.EosIdToken.FromProductUserId(puid);
if (tokenResult.TryUnwrapFailure(out var error))
{
throw new Exception($"Could not retrieve EOS authentication ticket for GameAnalytics. {error}");
}
else if (tokenResult.TryUnwrapSuccess(out var token))
{
return new AuthTicket(token.JsonWebToken.ToString(), Platform.EOS);
}
throw new UnreachableCodeException();
}
/// <summary>
@@ -92,7 +140,9 @@ namespace Barotrauma
if (consent == Consent.Ask)
{
CreateConsentPrompt();
#if CLIENT
GameMain.ExecuteAfterContentFinishedLoading(CreateConsentPrompt);
#endif
}
if (consent != Consent.No && consent != Consent.Yes)
@@ -129,20 +179,24 @@ namespace Barotrauma
/// </summary>
private static async Task<bool> SendAnswerToRemoteDatabase(Consent consent)
{
string authTicketStr;
AuthTicket authTicket;
try
{
authTicketStr = await GetAuthTicket();
authTicket = await GetAuthTicket();
}
catch (Exception e)
{
DebugConsole.ThrowError("Error in GameAnalyticsManager.SetConsent. Could not get a Steam authentication ticket.", e);
DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Could not get an authentication ticket.", e);
return false;
}
if (string.IsNullOrEmpty(authTicketStr))
if (authTicket.Platform == Platform.None)
{
DebugConsole.ThrowError("Error in GameAnalyticsManager.SetContent. Steam authentication ticket was empty.");
DebugConsole.AddWarning($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. Not logged in to any platform.");
return false;
}
if (string.IsNullOrEmpty(authTicket.Token))
{
DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(SendAnswerToRemoteDatabase)}. {authTicket.Platform} authentication ticket was empty.");
return false;
}
@@ -152,10 +206,18 @@ namespace Barotrauma
var client = new RestClient(consentServerUrl);
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", authTicketStr);
request.AddParameter("action", "setconsent");
request.AddParameter("consent", consent == Consent.Yes ? 1 : 0);
request.AddParameter("authticket", authTicket.Token);
if (consent == Consent.Ask)
{
request.AddParameter("action", "resetconsent");
}
else
{
request.AddParameter("action", "setconsent");
request.AddParameter("consent", consent == Consent.Yes ? 1 : 0);
}
request.AddParameter("request_version", RemoteRequestVersion);
request.AddParameter("platform", authTicket.Platform);
response = await client.ExecuteAsync(request, Method.GET);
}
@@ -175,6 +237,18 @@ namespace Barotrauma
return true;
}
public static void ResetConsent()
{
TaskPool.Add(
"GameAnalyticsConsent.ResetConsentInternal",
SendAnswerToRemoteDatabase(Consent.Ask),
t =>
{
if (!t.TryGetResult(out bool success) || !success) { return; }
DebugConsole.NewMessage("Reset GameAnalytics consent.");
});
}
static partial void CreateConsentPrompt();
public static void InitIfConsented()
@@ -183,15 +257,15 @@ namespace Barotrauma
return;
#endif
if (!consentTextAvailable)
if (!ConsentTextAvailable)
{
SetConsent(Consent.Unknown);
return;
}
if (!SteamManager.IsInitialized)
if (!SteamManager.IsInitialized && EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 })
{
DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam).");
DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam or EOS authentication ticket (not connected to Steam or EOS).");
SetConsent(Consent.Error);
return;
}
@@ -208,20 +282,25 @@ namespace Barotrauma
private static async Task<Consent> RequestAnswerFromRemoteDatabase()
{
static void error(string reason, Exception exception)
static void error(string reason, Exception? exception)
{
DebugConsole.ThrowError($"Error in GameAnalyticsManager.GetConsent: {reason}", exception);
DebugConsole.ThrowError($"Error in {nameof(GameAnalyticsManager)}.{nameof(RequestAnswerFromRemoteDatabase)}: {reason}", exception);
SetConsent(Consent.Error);
}
string authTicketStr;
AuthTicket authTicket;
try
{
authTicketStr = await GetAuthTicket();
authTicket = await GetAuthTicket();
}
catch (Exception e)
{
error("Could not get a Steam authentication ticket.", e);
error("Could not get an authentication ticket.", e);
return Consent.Error;
}
if (authTicket.Platform == Platform.None)
{
error($"Could not get an authentication ticket. Not logged in to any platform.", exception: null);
return Consent.Error;
}
@@ -237,9 +316,10 @@ namespace Barotrauma
}
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", authTicketStr);
request.AddParameter("authticket", authTicket.Token);
request.AddParameter("action", "getconsent");
request.AddParameter("request_version", RemoteRequestVersion);
request.AddParameter("platform", authTicket.Platform);
IRestResponse response;
try
@@ -1,5 +1,6 @@
#nullable enable
using Barotrauma.IO;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -51,6 +52,13 @@ namespace Barotrauma
Difficulty90to100,
}
public enum CustomDimensions03
{
UnknownPlatform,
Steam,
EGS
}
public enum ResourceCurrency
{
Money
@@ -149,6 +157,14 @@ namespace Barotrauma
internal void ConfigureAvailableResourceCurrencies(params ResourceCurrency[] customDimensions)
=> configureAvailableResourceCurrencies(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string[]> configureAvailableCustomDimensions03;
internal void ConfigureAvailableCustomDimensions03(params CustomDimensions03[] customDimensions)
=> configureAvailableCustomDimensions03(customDimensions.Select(d => d.ToString()).ToArray());
private readonly Action<string> setCustomDimension03;
internal void SetCustomDimension03(string dimension03)
=> setCustomDimension03(dimension03);
private readonly Action<string[]> configureAvailableResourceItemTypes;
internal void ConfigureAvailableResourceItemTypes(params string[] resourceItemTypes)
=> configureAvailableResourceItemTypes(resourceItemTypes);
@@ -238,9 +254,9 @@ namespace Barotrauma
{
if (resolvingDependency) { return null; }
resolvingDependency = true;
Assembly dep = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
Assembly dependency = context.LoadFromAssemblyPath(GetAssemblyPath(dependencyName.Name ?? throw new Exception("Dependency name was null")));
resolvingDependency = false;
return dep;
return dependency;
}
internal Implementation()
@@ -297,7 +313,10 @@ namespace Barotrauma
new Type[] { typeof(string) }));
configureAvailableCustomDimensions02 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions02),
new Type[] { typeof(string[]) }));
configureAvailableCustomDimensions03 = Call<string[]>(getMethod(nameof(ConfigureAvailableCustomDimensions03),
new Type[] { typeof(string[]) }));
setCustomDimension03 = Call<string>(getMethod(nameof(SetCustomDimension03),
new Type[] { typeof(string) }));
configureAvailableResourceCurrencies = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceCurrencies),
new Type[] { typeof(string[]) }));
configureAvailableResourceItemTypes = Call<string[]>(getMethod(nameof(ConfigureAvailableResourceItemTypes),
@@ -424,6 +443,12 @@ namespace Barotrauma
loadedImplementation?.SetCustomDimension01(dimension.ToString());
}
public static void SetCustomDimension03(CustomDimensions03 dimension)
{
if (!SendUserStatistics) { return; }
loadedImplementation?.SetCustomDimension03(dimension.ToString());
}
public static void SetCurrentLevel(LevelData levelData)
{
if (!SendUserStatistics) { return; }
@@ -509,6 +534,7 @@ namespace Barotrauma
+ buildConfiguration);
loadedImplementation?.ConfigureAvailableCustomDimensions01(Enum.GetValues(typeof(CustomDimensions01)).Cast<CustomDimensions01>().ToArray());
loadedImplementation?.ConfigureAvailableCustomDimensions02(Enum.GetValues(typeof(CustomDimensions02)).Cast<CustomDimensions02>().ToArray());
loadedImplementation?.ConfigureAvailableCustomDimensions03(Enum.GetValues(typeof(CustomDimensions03)).Cast<CustomDimensions03>().ToArray());
loadedImplementation?.ConfigureAvailableResourceCurrencies(Enum.GetValues(typeof(ResourceCurrency)).Cast<ResourceCurrency>().ToArray());
loadedImplementation?.ConfigureAvailableResourceItemTypes(
Enum.GetValues(typeof(MoneySink)).Cast<MoneySink>().Select(s => s.ToString()).Union(Enum.GetValues(typeof(MoneySource)).Cast<MoneySource>().Select(s => s.ToString())).ToArray());
@@ -521,6 +547,19 @@ namespace Barotrauma
+ (exeHash?.ShortRepresentation ?? "Unknown") + ":"
+ AssemblyInfo.GitRevision + ":"
+ buildConfiguration);
SetCustomDimension01(ContentPackageManager.ModsEnabled ? CustomDimensions01.Modded : CustomDimensions01.Vanilla);
CustomDimensions03 platform = CustomDimensions03.UnknownPlatform;
if (SteamManager.IsInitialized)
{
platform = CustomDimensions03.Steam;
}
else if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
{
platform = CustomDimensions03.EGS;
}
SetCustomDimension03(platform);
}
catch (Exception e)
{
@@ -32,7 +32,7 @@ namespace Barotrauma
continue;
}
Type? type = Type.GetType(valueType);
Type? type = ReflectionUtils.GetType(valueType);
if (type == null)
{
@@ -62,7 +62,7 @@ namespace Barotrauma
{
DebugConsole.Log($"Set the value \"{identifier}\" to {value}");
SteamAchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true);
AchievementManager.OnCampaignMetadataSet(identifier, value, unlockClients: true);
if (!data.ContainsKey(identifier))
{
@@ -135,7 +135,7 @@ namespace Barotrauma
element.Add(new XElement("Data",
new XAttribute("key", key),
new XAttribute("value", valueStr),
new XAttribute("type", value.GetType())));
new XAttribute("type", value.GetType().FullName ?? "")));
}
#if DEBUG
DebugConsole.Log(element.ToString());
@@ -156,17 +156,15 @@ namespace Barotrauma
if (CheatsEnabled)
{
DebugConsole.CheatsEnabled = true;
#if USE_STEAM
if (!SteamAchievementManager.CheatsEnabled)
if (!AchievementManager.CheatsEnabled)
{
SteamAchievementManager.CheatsEnabled = true;
AchievementManager.CheatsEnabled = true;
#if CLIENT
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive Steam Achievements until you restart the game.");
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive achievements until you restart the game.");
#else
DebugConsole.NewMessage("Cheat commands have been enabled.", Color.Red);
#endif
}
#endif
}
foreach (var subElement in element.Elements())
@@ -551,7 +551,7 @@ namespace Barotrauma
}
#endif
#if CLIENT
if (campaignMode != null && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
if (campaignMode != null && levelData != null) { AchievementManager.OnBiomeDiscovered(levelData.Biome); }
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
if (existingRoundSummary?.ContinueButton != null)
@@ -655,7 +655,7 @@ namespace Barotrauma
ObjectiveManager.ResetObjectives();
#endif
EventManager?.StartRound(Level.Loaded);
SteamAchievementManager.OnStartRound();
AchievementManager.OnStartRound();
GameMode.ShowStartMessage();
@@ -946,7 +946,7 @@ namespace Barotrauma
ObjectiveManager.ResetUI();
CharacterHUD.ClearBossProgressBars();
#endif
SteamAchievementManager.OnRoundEnded(this);
AchievementManager.OnRoundEnded(this);
#if SERVER
GameMain.Server?.TraitorManager?.EndRound();
@@ -497,7 +497,7 @@ namespace Barotrauma.Items.Components
{
CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair);
}
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
AchievementManager.OnItemRepaired(item, CurrentFixer);
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
}
if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; }
@@ -35,7 +35,7 @@ namespace Barotrauma.Items.Components
if (UseHSV)
{
Color hsvColor = ToolBox.HSVToRGB(signalR, signalG, signalB);
Color hsvColor = ToolBoxCore.HSVToRGB(signalR, signalG, signalB);
signalR = hsvColor.R;
signalG = hsvColor.G;
signalB = hsvColor.B;
@@ -150,7 +150,7 @@ namespace Barotrauma
{
ItemPrefabIdentifier = itemPrefab;
using MD5 md5 = MD5.Create();
UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5);
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(itemPrefab, md5);
}
public override string ToString()
@@ -197,7 +197,7 @@ namespace Barotrauma
{
Tag = tag;
using MD5 md5 = MD5.Create();
UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5);
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(tag, md5);
}
public override string ToString()
@@ -349,7 +349,7 @@ namespace Barotrauma
private uint GenerateHash()
{
using var md5 = MD5.Create();
uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5);
uint outputId = ToolBoxCore.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5);
var requiredItems = string.Join(':', RequiredItems
.Select(static i => $"{i.UintIdentifier}:{i.Amount}")
@@ -358,7 +358,7 @@ namespace Barotrauma
var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}"));
uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5);
uint retVal = ToolBoxCore.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5);
if (retVal == 0) { retVal = 1; }
return retVal;
}
@@ -2046,7 +2046,8 @@ namespace Barotrauma
caveStartPos.ToVector2(), caveEndPos.ToVector2(),
iterations: 3,
offsetAmount: Vector2.Distance(caveStartPos.ToVector2(), caveEndPos.ToVector2()) * 0.75f,
bounds: caveArea);
bounds: caveArea,
rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
if (!caveSegments.Any()) { return; }
@@ -2066,7 +2067,8 @@ namespace Barotrauma
branchStartPos, branchEndPos,
iterations: 3,
offsetAmount: Vector2.Distance(branchStartPos, branchEndPos) * 0.75f,
bounds: caveArea);
bounds: caveArea,
rng: Rand.GetRNG(Rand.RandSync.ServerAndClient));
if (!branchSegments.Any()) { continue; }
var branch = new Tunnel(TunnelType.Cave, SegmentsToNodes(branchSegments), 150, parentBranch);
@@ -1,10 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Barotrauma.Extensions;
#if SERVER
using PipeType = System.IO.Pipes.AnonymousPipeClientStream;
@@ -121,6 +121,7 @@ namespace Barotrauma.Networking
readCancellationToken?.Cancel();
return Option<int>.None();
}
try
{
if (readTask.IsCompleted || readTask.Wait(timeOutMilliseconds, readCancellationToken.Token))
@@ -327,7 +328,36 @@ namespace Barotrauma.Networking
writeManualResetEvent.Set();
}
public static bool Read(out byte[] msg)
private static readonly Stopwatch stopwatch = new Stopwatch();
private const int MaxMilliseconds = 8;
public static IEnumerable<byte[]> Read()
{
stopwatch.Restart();
// To avoid the stopwatch somehow experiencing magical overhead that makes it not even
// start the loop within 8ms, use this bool to force at least one iteration.
bool hasIteratedAtLeastOnce = false;
// If it's taken more than 8 milliseconds to read dequeued messages, take
// a break from reading and allow all of the other logic to run for a tick.
// Otherwise the server may overwhelm the host client with redundant messages
// that are being read too slowly.
while (!hasIteratedAtLeastOnce || stopwatch.ElapsedMilliseconds < MaxMilliseconds)
{
hasIteratedAtLeastOnce = true;
if (!ReadSingleMessage(out var msg))
{
// No more messages available to dequeue, we don't need
// to reach 8 milliseconds to know we're done here
break;
}
yield return msg;
}
stopwatch.Stop();
}
private static bool ReadSingleMessage(out byte[] msg)
{
if (HasShutDown) { msg = null; return false; }
@@ -67,6 +67,7 @@ namespace Barotrauma.Networking
PERMISSIONS, //tell the client which special permissions they have (if any)
ACHIEVEMENT, //give the client a steam achievement
ACHIEVEMENT_STAT, //increment stat for an achievement
CHEATS_ENABLED, //tell the clients whether cheats are on or off
CAMPAIGN_SETUP_INFO,
@@ -151,7 +152,7 @@ namespace Barotrauma.Networking
ServerCrashed,
ServerFull,
AuthenticationRequired,
SteamAuthenticationFailed,
AuthenticationFailed,
SessionTaken,
TooManyFailedLogins,
InvalidName,
@@ -1,24 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class AccountId
{
public abstract string StringRepresentation { get; }
public static Option<AccountId> Parse(string str)
=> ReflectionUtils.ParseDerived<AccountId, string>(str);
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(AccountId a, AccountId b)
=> a.Equals(b);
public static bool operator !=(AccountId a, AccountId b)
=> !(a == b);
}
}
@@ -1,121 +0,0 @@
#nullable enable
using System;
namespace Barotrauma.Networking
{
sealed class SteamId : AccountId
{
public readonly UInt64 Value;
public override string StringRepresentation { get; }
/// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID
/// ------------------------------------------------------------------------------------
/// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows:
///
/// | a | b | c | d |
/// Most significant - | 01 | 1 | 00001 | 0546779D | - Least significant
///
/// a) 8 bits representing the universe the account belongs to.
/// b) 4 bits representing the type of account. Typically 1.
/// c) 20 bits representing the instance of the account. Typically 1.
/// d) 32 bits representing the account number.
///
/// The account number is additionally broken up as follows:
///
/// | e | f |
/// Most significant - | 0000010101000110011101111001110 | 1 | - Least significant
///
/// e) These are the 31 most significant bits of the account number.
/// f) This is the least significant bit of the account number, discriminated under the name Y for some reason.
///
/// Barotrauma supports two textual representations of SteamIDs:
/// 1. STEAM40: Given this name as it represents 40 of the 64 bits in the ID. The account type and instance both
/// have an implied value of 1. The format is "STEAM_{universe}:{Y}:{restOfAccountNumber}".
/// 2. STEAM64: If STEAM40 does not suffice to represent an ID (i.e. the account type or instance were different
/// from 1), we use "STEAM64_{fullId}" where fullId is the 64-bit decimal representation of the full
/// ID.
private const string steam64Prefix = "STEAM64_";
private const string steam40Prefix = "STEAM_";
private const UInt64 usualAccountInstance = 1;
private const UInt64 usualAccountType = 1;
static UInt64 ExtractBits(UInt64 id, int offset, int numberOfBits)
=> (id >> offset) & ((1ul << numberOfBits) - 1ul);
static UInt64 ExtractY(UInt64 id)
=> ExtractBits(id, offset: 0, numberOfBits: 1);
static UInt64 ExtractAccountNumberRemainder(UInt64 id)
=> ExtractBits(id, offset: 1, numberOfBits: 31);
static UInt64 ExtractAccountInstance(UInt64 id)
=> ExtractBits(id, offset: 32, numberOfBits: 20);
static UInt64 ExtractAccountType(UInt64 id)
=> ExtractBits(id, offset: 52, numberOfBits: 4);
static UInt64 ExtractUniverse(UInt64 id)
=> ExtractBits(id, offset: 56, numberOfBits: 8);
public SteamId(UInt64 value)
{
Value = value;
if (ExtractAccountInstance(Value) == usualAccountInstance
&& ExtractAccountType(Value) == usualAccountType)
{
UInt64 y = ExtractY(Value);
UInt64 accountNumberRemainder = ExtractAccountNumberRemainder(Value);
UInt64 universe = ExtractUniverse(Value);
StringRepresentation = $"{steam40Prefix}{universe}:{y}:{accountNumberRemainder}";
}
else
{
StringRepresentation = $"{steam64Prefix}{Value}";
}
}
public override string ToString() => StringRepresentation;
public new static Option<SteamId> Parse(string str)
{
if (str.IsNullOrWhiteSpace()) { return Option<SteamId>.None(); }
if (str.StartsWith(steam64Prefix, StringComparison.InvariantCultureIgnoreCase)) { str = str[steam64Prefix.Length..]; }
if (UInt64.TryParse(str, out UInt64 retVal) && ExtractAccountInstance(retVal) > 0)
{
return Option<SteamId>.Some(new SteamId(retVal));
}
if (!str.StartsWith(steam40Prefix, StringComparison.InvariantCultureIgnoreCase)) { return Option<SteamId>.None(); }
string[] split = str[steam40Prefix.Length..].Split(':');
if (split.Length != 3) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[0], out UInt64 universe)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[1], out UInt64 y)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return Option<SteamId>.None(); }
return Option<SteamId>.Some(
new SteamId((universe << 56)
| usualAccountType << 52
| usualAccountInstance << 32
| (accountNumber << 1)
| y));
}
public override bool Equals(object? obj)
=> obj switch
{
SteamId otherId => this == otherId,
_ => false
};
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(SteamId a, SteamId b)
=> a.Value == b.Value;
public static bool operator !=(SteamId a, SteamId b)
=> !(a == b);
}
}
@@ -18,15 +18,16 @@ namespace Barotrauma.Networking
/// Other user IDs that this user might be closely tied to,
/// such as the owner of the current copy of Barotrauma
/// </summary>
#warning TODO: make ImmutableArray once feature/inetserializablestruct-improvements gets merged to dev
public readonly AccountId[] OtherMatchingIds;
public readonly ImmutableArray<AccountId> OtherMatchingIds;
public bool IsNone => AccountId.IsNone() && OtherMatchingIds.Length == 0;
public AccountInfo(AccountId accountId, params AccountId[] otherIds) : this(Option<AccountId>.Some(accountId), otherIds) { }
public AccountInfo(Option<AccountId> accountId, params AccountId[] otherIds)
{
AccountId = accountId;
OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToArray();
OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToImmutableArray();
}
public bool Matches(AccountId accountId)
@@ -1,26 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class Address
{
public abstract string StringRepresentation { get; }
public static Option<Address> Parse(string str)
=> ReflectionUtils.ParseDerived<Address, string>(str);
public abstract bool IsLocalHost { get; }
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(Address a, Address b)
=> a.Equals(b);
public static bool operator !=(Address a, Address b)
=> !(a == b);
}
}
@@ -1,79 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Barotrauma.Networking
{
sealed class LidgrenAddress : Address
{
public readonly IPAddress NetAddress;
public override string StringRepresentation
=> NetAddress.ToString();
public override bool IsLocalHost => IPAddress.IsLoopback(NetAddress);
public LidgrenAddress(IPAddress netAddress)
{
if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; }
if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); }
NetAddress = netAddress;
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
{
if (endpointStr.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(IPAddress.Loopback));
}
else if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(netEndpoint!));
}
return Option<LidgrenAddress>.None();
}
public static Option<LidgrenAddress> ParseHostName(string endpointStr)
{
try
{
var resolvedAddresses = Dns.GetHostAddresses(endpointStr);
return resolvedAddresses.Any()
? Option<LidgrenAddress>.Some(new LidgrenAddress(resolvedAddresses.First()))
: Option<LidgrenAddress>.None();
}
catch (SocketException)
{
return Option<LidgrenAddress>.None();
}
catch (ArgumentOutOfRangeException)
{
return Option<LidgrenAddress>.None();
}
}
public override bool Equals(object? obj)
=> obj switch
{
LidgrenAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> NetAddress.GetHashCode();
public static bool operator ==(LidgrenAddress a, LidgrenAddress b)
{
var addressA = a.NetAddress.MapToIPv6();
var addressB = b.NetAddress.MapToIPv6();
if (IPAddress.IsLoopback(addressA) && IPAddress.IsLoopback(addressB)) { return true; }
return addressA.Equals(addressB);
}
public static bool operator !=(LidgrenAddress a, LidgrenAddress b)
=> !(a == b);
}
}
@@ -1,22 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class PipeAddress : Address
{
public override string StringRepresentation => "PIPE";
public override bool IsLocalHost => true;
public override bool Equals(object? obj)
=> obj is PipeAddress;
public override int GetHashCode() => 1;
public static bool operator ==(PipeAddress a, PipeAddress b)
=> true;
public static bool operator !=(PipeAddress a, PipeAddress b)
=> !(a == b);
}
}
@@ -1,37 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class SteamP2PAddress : Address
{
public readonly SteamId SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override bool IsLocalHost => false;
public SteamP2PAddress(SteamId steamId)
{
SteamId = steamId;
}
public new static Option<SteamP2PAddress> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PAddress a, SteamP2PAddress b)
=> a.SteamId == b.SteamId;
public static bool operator !=(SteamP2PAddress a, SteamP2PAddress b)
=> !(a == b);
}
}
@@ -1,16 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class UnknownAddress : Address
{
public override string StringRepresentation => "Hidden";
public override bool IsLocalHost => false;
public override bool Equals(object? obj)
=> ReferenceEquals(obj, this);
public override int GetHashCode() => 1;
}
}
@@ -0,0 +1,65 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
public enum AuthenticationTicketKind
{
SteamAuthTicketForSteamHost = 0,
SteamAuthTicketForEosHost = 1,
EgsOwnershipToken = 2
}
[NetworkSerialize(ArrayMaxSize = UInt16.MaxValue)]
readonly record struct AuthenticationTicket(
AuthenticationTicketKind Kind,
ImmutableArray<byte> Data) : INetSerializableStruct
{
public static async Task<Option<AuthenticationTicket>> Create(Endpoint serverEndpoint)
{
if (SteamManager.IsInitialized && SteamManager.GetSteamId().TryUnwrap(out var steamId))
{
if (serverEndpoint is EosP2PEndpoint)
{
var authTicket = await SteamManager.GetAuthTicketForEosHostAuth();
return authTicket
.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None)
.Select(data => new AuthenticationTicket(AuthenticationTicketKind.SteamAuthTicketForEosHost, data.ToImmutableArray()));
}
else
{
var authTicket = SteamManager.GetAuthSessionTicketForSteamHost(serverEndpoint);
var steamIdBytes = BitConverter.GetBytes(steamId.Value);
return authTicket
.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None)
.Select(data => new AuthenticationTicket(
AuthenticationTicketKind.SteamAuthTicketForSteamHost,
steamIdBytes.Concat(data).ToImmutableArray()));
}
}
if (EosInterface.IdQueries.GetLoggedInPuids() is { Length: > 0 } puids)
{
var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]);
if (externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds))
{
var epicAccountIdOption = externalAccountIds.OfType<EpicAccountId>().FirstOrNone();
if (epicAccountIdOption.TryUnwrap(out var epicAccountId))
{
return (await EosInterface.Ownership.GetGameOwnershipToken(epicAccountId))
.Select(t => new AuthenticationTicket(
AuthenticationTicketKind.EgsOwnershipToken,
Encoding.UTF8.GetBytes(t.Jwt.ToString()).ToImmutableArray()));
}
}
}
return Option.None;
}
}
@@ -0,0 +1,38 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
abstract class Authenticator
{
public abstract Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket);
public abstract void EndAuthSession(AccountId accountId);
public static ImmutableDictionary<AuthenticationTicketKind, Authenticator> GetAuthenticatorsForHost(Option<Endpoint> ownerEndpointOption)
{
var authenticators = new Dictionary<AuthenticationTicketKind, Authenticator>();
if (EosInterface.Core.IsInitialized)
{
// Every kind of host should be able to do EOS ID Token authentication if they have EOS enabled
authenticators.Add(AuthenticationTicketKind.EgsOwnershipToken, new EgsOwnershipTokenAuthenticator());
if (ownerEndpointOption.TryUnwrap(out var ownerEndpoint) && ownerEndpoint is EosP2PEndpoint)
{
// EOS P2P hosts do not have access to Steamworks
authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForEosHost, new SteamAuthTicketForEosHostAuthenticator());
}
}
if (!(ownerEndpointOption.TryUnwrap(out var ownerEndpoint2) && ownerEndpoint2 is EosP2PEndpoint) && SteamManager.IsInitialized)
{
// Steam P2P hosts and dedicated servers have access to Steamworks
authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForSteamHost, new SteamAuthTicketForSteamHostAuthenticator());
}
return authenticators.ToImmutableDictionary();
}
}
@@ -0,0 +1,21 @@
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking;
sealed class EgsOwnershipTokenAuthenticator : Authenticator
{
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
var jwtOption = JsonWebToken.Parse(Encoding.UTF8.GetString(ticket.Data.AsSpan()));
if (!jwtOption.TryUnwrap(out var jwt)) { return AccountInfo.None; }
var ownershipToken = new EosInterface.Ownership.Token(jwt);
var accountIdOption = await ownershipToken.Verify();
if (!accountIdOption.TryUnwrap(out var accountId)) { return AccountInfo.None; }
return new AccountInfo(accountId);
}
public override void EndAuthSession(AccountId accountId) { /* do nothing */ }
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using RestSharp;
namespace Barotrauma.Networking;
sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator
{
#warning FIXME change URL back to the non-experimental one once this passes QA
private const string consentServerUrl = "https://barotraumagame.com/baromaster/experimental/";
private const string consentServerFile = "getOwnerSteamId.php";
private const int RemoteRequestVersion = 1;
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data);
var client = new RestClient(consentServerUrl);
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", ticketData);
request.AddParameter("request_version", RemoteRequestVersion);
var response = await client.ExecuteAsync(request, Method.GET);
if (!response.IsSuccessful) { return AccountInfo.None; }
try
{
var jsonDoc = JsonDocument.Parse(response.Content);
Option<SteamId> steamId = Option.None;
Option<SteamId> ownerSteamId = Option.None;
foreach (var property in jsonDoc.RootElement.EnumerateObject())
{
if (!property.Name.ToIdentifier().Contains("SteamId")) { continue; }
var accountIdOption = SteamId.Parse(property.Value.GetString() ?? "");
if (accountIdOption.IsNone()) { continue; }
if (property.Name.ToIdentifier() == "SteamId")
{
steamId = accountIdOption;
}
else if (property.Name.ToIdentifier() == "OwnerSteamId")
{
ownerSteamId = accountIdOption;
}
}
var otherIds = ownerSteamId.TryUnwrap(out var id) ? new AccountId[] { id } : Array.Empty<AccountId>();
return new AccountInfo(steamId.Select(static id => (AccountId)id), otherIds);
}
catch
{
return AccountInfo.None;
}
}
public override void EndAuthSession(AccountId accountId) { /* do nothing */ }
}
@@ -0,0 +1,77 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
#if CLIENT
using SteamAuthSessionInterface = Steamworks.SteamUser;
#else
using SteamAuthSessionInterface = Steamworks.SteamServer;
#endif
sealed class SteamAuthTicketForSteamHostAuthenticator : Authenticator
{
private static Steamworks.BeginAuthResult BeginAuthSession(Steamworks.AuthTicket authTicket, SteamId clientSteamId)
{
if (!SteamManager.IsInitialized) { return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; }
if (authTicket.Data is null) { return Steamworks.BeginAuthResult.InvalidTicket; }
DebugConsole.Log("Authenticating Steam client " + clientSteamId);
Steamworks.BeginAuthResult startResult = SteamAuthSessionInterface.BeginAuthSession(authTicket.Data, clientSteamId.Value);
if (startResult != Steamworks.BeginAuthResult.OK)
{
DebugConsole.Log($"Steam authentication failed: failed to start auth session ({startResult})");
}
return startResult;
}
private static void EndAuthSession(SteamId clientSteamId)
{
if (!SteamManager.IsInitialized) { return; }
DebugConsole.Log($"Ending auth session with Steam client {clientSteamId}");
SteamAuthSessionInterface.EndAuthSession(clientSteamId.Value);
}
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
if (ticket.Data.Length < 8) { return AccountInfo.None; }
var ticketData = ticket.Data.ToArray();
var steamAuthTicket = new Steamworks.AuthTicket { Data = ticketData[8..] };
var steamId = new SteamId(BitConverter.ToUInt64(ticketData.AsSpan()[..8]));
using var janitor = Janitor.Start();
(Steamworks.AuthResponse AuthResponse, SteamId OwnerSteamId)? authResult = null;
void onValidateAuthTicketResponse(Steamworks.SteamId clientId, Steamworks.SteamId ownerClientId, Steamworks.AuthResponse response)
{
if (clientId != steamId.Value) { response = Steamworks.AuthResponse.AuthTicketInvalid; }
authResult = (response, new SteamId(ownerClientId));
}
SteamAuthSessionInterface.OnValidateAuthTicketResponse += onValidateAuthTicketResponse;
janitor.AddAction(() => SteamAuthSessionInterface.OnValidateAuthTicketResponse -= onValidateAuthTicketResponse);
var beginAuthSessionResult = BeginAuthSession(steamAuthTicket, steamId);
if (beginAuthSessionResult != Steamworks.BeginAuthResult.OK) { return AccountInfo.None; }
while (authResult is null)
{
await Task.Delay(32);
}
if (authResult.Value.AuthResponse != Steamworks.AuthResponse.OK) { return AccountInfo.None; }
return new AccountInfo(steamId, authResult.Value.OwnerSteamId);
}
public override void EndAuthSession(AccountId accountId)
{
if (accountId is not SteamId steamId) { return; }
SteamAuthSessionInterface.EndAuthSession(steamId.Value);
}
}
@@ -0,0 +1,32 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class EosP2PEndpoint : P2PEndpoint
{
public EosInterface.ProductUserId ProductUserId => new EosInterface.ProductUserId((Address as EosP2PAddress)!.EosStringRepresentation);
public EosP2PEndpoint(EosInterface.ProductUserId puid) : this(new EosP2PAddress(puid.Value)) { }
public EosP2PEndpoint(EosP2PAddress address) : base(address) { }
public override string StringRepresentation => (Address as EosP2PAddress)!.StringRepresentation;
public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer");
public override int GetHashCode()
=> (Address as EosP2PAddress)!.GetHashCode();
public override bool Equals(object? obj)
=> obj is EosP2PEndpoint otherEndpoint
&& ProductUserId == otherEndpoint.ProductUserId;
public new static Option<EosP2PEndpoint> Parse(string endpointStr)
=> EosP2PAddress.Parse(endpointStr).Select(eosAddress => new EosP2PEndpoint(eosAddress));
public const string SocketName = "Barotrauma.EosP2PSocket";
public override P2PConnection MakeConnectionFromEndpoint()
=> new EosP2PConnection(this);
}
@@ -60,7 +60,7 @@ namespace Barotrauma.Networking
=> NetEndpoint.GetHashCode();
public static bool operator ==(LidgrenEndpoint a, LidgrenEndpoint b)
=> a.Address.Equals(b.Address) && a.Port == b.Port;
=> a.NetEndpoint.EquivalentTo(b.NetEndpoint);
public static bool operator !=(LidgrenEndpoint a, LidgrenEndpoint b)
=> !(a == b);
@@ -0,0 +1,12 @@
#nullable enable
namespace Barotrauma.Networking;
abstract class P2PEndpoint : Endpoint
{
protected P2PEndpoint(P2PAddress address) : base(address) { }
public abstract P2PConnection MakeConnectionFromEndpoint();
public new static Option<P2PEndpoint> Parse(string str)
=> Endpoint.Parse(str).Bind(ep => ep is P2PEndpoint pep ? Option.Some(pep) : Option.None);
}
@@ -2,36 +2,27 @@
namespace Barotrauma.Networking
{
sealed class SteamP2PEndpoint : Endpoint
sealed class SteamP2PEndpoint : P2PEndpoint
{
public readonly SteamId SteamId;
public SteamId SteamId => (Address as SteamP2PAddress)!.SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override LocalizedString ServerTypeString { get; } = TextManager.Get("SteamP2PServer");
public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer");
public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId))
{
SteamId = steamId;
}
public new static Option<SteamP2PEndpoint> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PEndpoint otherEndpoint => this == otherEndpoint,
_ => false
};
public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId)) { }
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> a.SteamId == b.SteamId;
public override bool Equals(object? obj)
=> obj is SteamP2PEndpoint otherEndpoint
&& this.SteamId == otherEndpoint.SteamId;
public static bool operator !=(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> !(a == b);
public new static Option<SteamP2PEndpoint> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId));
public override P2PConnection MakeConnectionFromEndpoint()
=> new SteamP2PConnection(this);
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Networking;
sealed class MessageDefragmenter
{
private readonly Dictionary<ushort, MessageFragment[]> partialMessages = new Dictionary<ushort, MessageFragment[]>();
public Option<ImmutableArray<byte>> ProcessIncomingFragment(MessageFragment fragment)
{
if (!partialMessages.ContainsKey(fragment.FragmentId.MessageId))
{
partialMessages[fragment.FragmentId.MessageId] = new MessageFragment[fragment.FragmentId.FragmentCount];
}
else if (partialMessages[fragment.FragmentId.MessageId].Length != fragment.FragmentId.FragmentCount)
{
DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " +
$"with a mismatched expected fragment count");
return Option.None;
}
var fragmentBuffer = partialMessages[fragment.FragmentId.MessageId];
if (fragment.FragmentId.FragmentIndex >= fragmentBuffer.Length)
{
DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " +
$"with an index greater than or equal to the expected fragment count ({fragment.FragmentId.FragmentIndex} >= {fragmentBuffer.Length})");
return Option.None;
}
fragmentBuffer[fragment.FragmentId.FragmentIndex] = fragment;
if (fragmentBuffer.All(f => !f.Data.IsDefault && f.FragmentId.MessageId == fragment.FragmentId.MessageId))
{
partialMessages.Remove(fragment.FragmentId.MessageId);
return Option.Some(fragmentBuffer.SelectMany(f => f.Data).ToImmutableArray());
}
return Option.None;
}
}
@@ -0,0 +1,17 @@
using System.Collections.Immutable;
namespace Barotrauma.Networking;
[NetworkSerialize(ArrayMaxSize = MaxSize)]
readonly record struct MessageFragment(
MessageFragment.Id FragmentId,
ImmutableArray<byte> Data) : INetSerializableStruct
{
public const int MaxSize = 1100;
[NetworkSerialize]
public readonly record struct Id(
ushort FragmentIndex,
ushort FragmentCount,
ushort MessageId) : INetSerializableStruct;
}
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma.Networking;
sealed class MessageFragmenter
{
private UInt16 nextFragmentedMessageId = 0;
private readonly List<MessageFragment> fragments = new List<MessageFragment>();
public ImmutableArray<MessageFragment> FragmentMessage(ReadOnlySpan<byte> bytes)
{
UInt16 msgId = nextFragmentedMessageId;
nextFragmentedMessageId++;
int roundedByteCount = bytes.Length;
roundedByteCount += (MessageFragment.MaxSize - (roundedByteCount % MessageFragment.MaxSize)) % MessageFragment.MaxSize;
int fragmentCount = roundedByteCount / MessageFragment.MaxSize;
fragments.Clear();
fragments.EnsureCapacity(fragmentCount);
for (int i = 0; i < fragmentCount; i++)
{
var subset = bytes[(i * MessageFragment.MaxSize)..];
if (subset.Length > MessageFragment.MaxSize) { subset = subset[..MessageFragment.MaxSize]; }
fragments.Add(new MessageFragment(
FragmentId: new MessageFragment.Id(
FragmentIndex: (ushort)i,
FragmentCount: (ushort)fragmentCount,
MessageId: msgId),
Data: subset.ToArray().ToImmutableArray()));
}
return fragments.ToImmutableArray();
}
}
@@ -10,7 +10,11 @@ namespace Barotrauma.Networking
{
public static class MsgConstants
{
public const int MTU = 1200; //TODO: determine dynamically
// MTU currently set to the upper limit of what EOS P2P can do
// TODO: determine dynamically so other protocols can use a larger MTU,
// as well as handle a client with a lower MTU set outside of our control
public const int MTU = 1170;
public const int CompressionThreshold = 1000;
public const int InitialBufferSize = 256;
public const int BufferOverAllocateAmount = 4;
@@ -0,0 +1,8 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class EosP2PConnection : P2PConnection<EosP2PEndpoint>
{
public EosP2PConnection(EosP2PEndpoint endpoint) : base(endpoint) { }
}
@@ -2,7 +2,7 @@
namespace Barotrauma.Networking
{
sealed class LidgrenConnection : NetworkConnection
sealed class LidgrenConnection : NetworkConnection<LidgrenEndpoint>
{
public readonly NetConnection NetConnection;
@@ -8,6 +8,13 @@ namespace Barotrauma.Networking
Disconnected = 0x2
}
abstract class NetworkConnection<T> : NetworkConnection where T : Endpoint
{
protected NetworkConnection(T endpoint) : base(endpoint) { }
public new T Endpoint => (base.Endpoint as T)!;
}
abstract class NetworkConnection
{
public const double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while
@@ -23,7 +30,7 @@ namespace Barotrauma.Networking
get; set;
}
public NetworkConnection(Endpoint endpoint)
protected NetworkConnection(Endpoint endpoint)
{
Endpoint = endpoint;
}
@@ -35,9 +42,9 @@ namespace Barotrauma.Networking
public void SetAccountInfo(AccountInfo newInfo)
{
AccountInfo = newInfo;
if (AccountInfo.IsNone) { AccountInfo = newInfo; }
}
public sealed override string ToString()
=> Endpoint.StringRepresentation;
}
@@ -0,0 +1,30 @@
#nullable enable
namespace Barotrauma.Networking;
abstract class P2PConnection<T> : P2PConnection where T : P2PEndpoint
{
protected P2PConnection(T endpoint) : base(endpoint) { }
public new T Endpoint => (base.Endpoint as T)!;
}
abstract class P2PConnection : NetworkConnection<P2PEndpoint>
{
protected P2PConnection(P2PEndpoint endpoint) : base(endpoint)
{
Heartbeat();
}
public double Timeout = 0.0;
public void Decay(float deltaTime)
{
Timeout -= deltaTime;
}
public void Heartbeat()
{
Timeout = TimeoutThreshold;
}
}
@@ -23,11 +23,11 @@ namespace Barotrauma.Networking
=> !(a == b);
}
sealed class PipeConnection : NetworkConnection
sealed class PipeConnection : NetworkConnection<PipeEndpoint>
{
public PipeConnection(AccountId accountId) : base(new PipeEndpoint())
public PipeConnection(Option<AccountId> accountId) : base(new PipeEndpoint())
{
SetAccountInfo(new AccountInfo(Option<AccountId>.Some(accountId)));
SetAccountInfo(new AccountInfo(accountId));
}
}
}
@@ -1,24 +1,9 @@
namespace Barotrauma.Networking
{
sealed class SteamP2PConnection : NetworkConnection
sealed class SteamP2PConnection : P2PConnection<SteamP2PEndpoint>
{
public double Timeout = 0.0;
public SteamP2PConnection(SteamId steamId) : this(new SteamP2PEndpoint(steamId)) { }
public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint)
{
Heartbeat();
}
public void Decay(float deltaTime)
{
Timeout -= deltaTime;
}
public void Heartbeat()
{
Timeout = TimeoutThreshold;
}
public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint) { }
}
}
@@ -1,57 +0,0 @@
using System;
namespace Barotrauma.Networking
{
public enum DeliveryMethod : int
{
Unreliable = 0x0,
Reliable = 0x1,
ReliableOrdered = 0x2
}
public enum ConnectionInitialization : int
{
//used by all peer implementations
SteamTicketAndVersion = 0x1,
ContentPackageOrder = 0x2,
Password = 0x3,
Success = 0x0,
//used only by SteamP2P implementations
ConnectionStarted = 0x4
}
[Flags]
public enum PacketHeader : int
{
//used by all peer implementations
None = 0x0,
IsCompressed = 0x1,
IsConnectionInitializationStep = 0x2,
//used only by SteamP2P implementations
IsDisconnectMessage = 0x4,
IsServerMessage = 0x8,
IsHeartbeatMessage = 0x10
}
public static class NetworkEnumExtensions
{
public static bool IsCompressed(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsCompressed);
#warning TODO: remove?
public static bool IsConnectionInitializationStep(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsConnectionInitializationStep);
public static bool IsDisconnectMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsDisconnectMessage);
public static bool IsServerMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsServerMessage);
public static bool IsHeartbeatMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsHeartbeatMessage);
}
}
@@ -52,8 +52,7 @@ namespace Barotrauma.Networking
deliveryMethod switch
{
DeliveryMethod.Unreliable => NetDeliveryMethod.Unreliable,
DeliveryMethod.Reliable => NetDeliveryMethod.ReliableUnordered,
DeliveryMethod.ReliableOrdered => NetDeliveryMethod.ReliableOrdered,
DeliveryMethod.Reliable => NetDeliveryMethod.ReliableOrdered,
_ => NetDeliveryMethod.Unreliable
};
@@ -61,7 +60,6 @@ namespace Barotrauma.Networking
deliveryMethod switch
{
DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable,
DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Reliable,
_ => Steamworks.P2PSend.Unreliable
};
}
@@ -25,34 +25,42 @@ namespace Barotrauma.Networking
}
[NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
internal struct ClientSteamTicketAndVersionPacket : INetSerializableStruct
internal struct ClientAuthTicketAndVersionPacket : INetSerializableStruct
{
public string Name;
public Option<int> OwnerKey;
#warning TODO: do something about the type of this
// It probably should be Option<SteamId> but we shouldn't build support for
// writing SteamIDs to INetSerializableStruct; we should consider adding
// attributes to give custom behaviors to specific members of a struct
public Option<AccountId> SteamId;
public Option<byte[]> SteamAuthTicket;
public Option<AccountId> AccountId;
public Option<AuthenticationTicket> AuthTicket;
public string GameVersion;
public Identifier Language;
}
[NetworkSerialize]
internal struct SteamP2PInitializationRelayPacket : INetSerializableStruct
internal readonly record struct P2POwnerToServerHeader
(string? EndpointStr, AccountInfo AccountInfo) : INetSerializableStruct
{
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
}
[NetworkSerialize]
internal readonly record struct P2PServerToOwnerHeader
(string? EndpointStr) : INetSerializableStruct
{
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
}
[NetworkSerialize]
internal struct P2PInitializationRelayPacket : INetSerializableStruct
{
public ulong LobbyID;
public PeerPacketMessage Message;
}
[NetworkSerialize]
internal struct SteamP2PInitializationOwnerPacket : INetSerializableStruct
{
public string OwnerName;
}
internal readonly record struct P2PInitializationOwnerPacket(
string Name,
AccountId AccountId)
: INetSerializableStruct;
[NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
@@ -68,7 +76,7 @@ namespace Barotrauma.Networking
public byte[] Buffer;
public readonly int Length => Buffer.Length;
public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false);
public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length * 8, copyBuf: false);
public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn);
}
@@ -140,7 +148,8 @@ namespace Barotrauma.Networking
DisconnectReason.ExcessiveDesyncOldEvent => ServerMessage,
DisconnectReason.ExcessiveDesyncRemovedEvent => ServerMessage,
DisconnectReason.SyncTimeout => ServerMessage,
_ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost"))
DisconnectReason.AuthenticationFailed => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ChatMsg.DisconnectReason.AuthenticationRequired")),
_ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback($"{TextManager.Get("ConnectionLost")} ({DisconnectReason})")
};
public LocalizedString ReconnectMessage
@@ -178,15 +187,12 @@ namespace Barotrauma.Networking
or DisconnectReason.TooManyFailedLogins
or DisconnectReason.InvalidVersion);
private const string lidgrenSeparator = ":hankey:";
/// <summary>
/// This exists because Lidgren is a piece of shit and
/// doesn't readily support sending anything other than
/// a string through a disconnect packet, so this thing
/// needs a sufficiently nasty string representation that
/// can be decoded with some certainty that it won't get
/// mangled by user input.
/// This exists because Lidgren doesn't readily support
/// sending anything other than a string through a disconnect
/// packet, so this thing needs a sufficiently nasty string
/// representation that can be decoded with some certainty
/// that it won't get mangled by user input.
/// </summary>
public string ToLidgrenStringRepresentation()
{
@@ -194,7 +200,7 @@ namespace Barotrauma.Networking
=> Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
return DisconnectReason
+ lidgrenSeparator
+ NetworkMagicStrings.LidgrenDisconnectSeparator
+ strToBase64(AdditionalInformation);
}
@@ -209,16 +215,16 @@ namespace Barotrauma.Networking
case Lidgren.Network.NetConnection.NoResponseMessage:
case "Connection timed out":
case "Reconnecting":
return Option<PeerDisconnectPacket>.Some(WithReason(DisconnectReason.Timeout));
return Option.Some(WithReason(DisconnectReason.Timeout));
}
static string base64ToStr(string base64)
=> Encoding.UTF8.GetString(Convert.FromBase64String(base64));
string[] split = str.Split(lidgrenSeparator);
if (split.Length != 2) { return Option<PeerDisconnectPacket>.None(); }
if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option<PeerDisconnectPacket>.None(); }
return Option<PeerDisconnectPacket>.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1])));
string[] split = str.Split(NetworkMagicStrings.LidgrenDisconnectSeparator);
if (split.Length != 2) { return Option.None; }
if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None; }
return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1])));
}
public static PeerDisconnectPacket Custom(string customMessage)
@@ -247,12 +253,12 @@ namespace Barotrauma.Networking
public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
DisconnectReason.AuthenticationFailed,
$"{nameof(Steamworks.BeginAuthResult)}.{error}");
public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
DisconnectReason.AuthenticationFailed,
$"{nameof(Steamworks.AuthResponse)}.{error}");
}
@@ -0,0 +1,23 @@
namespace Barotrauma.Networking;
public readonly record struct ServerListContentPackageInfo(
string Name, string Hash, Option<ContentPackageId> Id)
{
public ServerListContentPackageInfo(ContentPackage pkg)
: this(pkg.Name, pkg.Hash.StringRepresentation, pkg.UgcId) {}
public static Option<ServerListContentPackageInfo> ParseSingleEntry(string singleEntry)
{
if (singleEntry.SplitEscaped(',') is not { Count: 3 } split) { return Option.None; }
return Option.Some(
new ServerListContentPackageInfo(
split[0],
split[1],
ContentPackageId.Parse(split[2])));
}
public override string ToString()
=> new[] { Name, Hash, Id.Select(id => id.StringRepresentation).Fallback("") }
.JoinEscaped(',');
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
@@ -296,7 +297,7 @@ namespace Barotrauma.Networking
if (typeName != null || property.PropertyType.IsEnum)
{
NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName);
UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5);
UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5);
if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message
if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); }
netProperties.Add(key, netPropertyData);
@@ -313,7 +314,7 @@ namespace Barotrauma.Networking
if (typeName != null || property.PropertyType.IsEnum)
{
NetPropertyData netPropertyData = new NetPropertyData(networkMember.KarmaManager, property, typeName);
UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5);
UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5);
if (netProperties.ContainsKey(key)) { throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); }
netProperties.Add(key, netPropertyData);
}
@@ -1140,5 +1141,47 @@ namespace Barotrauma.Networking
msg.WriteUInt16((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase)));
}
}
public void UpdateServerListInfo(Action<Identifier, object> setter)
{
void set(string key, object obj) => setter(key.ToIdentifier(), obj);
set("ServerName", ServerName);
set("MaxPlayers", MaxPlayers);
set("HasPassword", HasPassword);
set("message", ServerMessageText);
set("version", GameMain.Version);
set("playercount", GameMain.NetworkMember.ConnectedClients.Count);
set("contentpackages", ContentPackageManager.EnabledPackages.All.Where(p => p.HasMultiplayerSyncedContent));
set("modeselectionmode", ModeSelectionMode);
set("subselectionmode", SubSelectionMode);
set("voicechatenabled", VoiceChatEnabled);
set("allowspectating", AllowSpectating);
set("allowrespawn", AllowRespawn);
set("traitors", TraitorProbability.ToString(CultureInfo.InvariantCulture));
set("friendlyfireenabled", AllowFriendlyFire);
set("karmaenabled", KarmaEnabled);
set("gamestarted", GameMain.NetworkMember.GameStarted);
set("gamemode", GameModeIdentifier);
set("playstyle", PlayStyle);
set("language", Language.ToString());
#if SERVER
set("eoscrossplay", EosInterface.Core.IsInitialized);
#else
set("eoscrossplay", EosInterface.IdQueries.IsLoggedIntoEosConnect || Eos.EosSessionManager.CurrentOwnedSession.IsSome());
#endif
if (GameMain.NetLobbyScreen?.SelectedSub != null)
{
set("submarine", GameMain.NetLobbyScreen.SelectedSub.Name);
}
if (Steamworks.SteamClient.IsLoggedOn)
{
string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString();
if (!pingLocation.IsNullOrEmpty())
{
set("steampinglocation", pingLocation);
}
}
}
}
}
@@ -386,7 +386,7 @@ namespace Barotrauma
{
using (MD5 md5 = MD5.Create())
{
prefabWithUintIdentifier.UintIdentifier = ToolBox.IdentifierToUint32Hash(prefab.Identifier, md5);
prefabWithUintIdentifier.UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
T? findCollision()
@@ -5,10 +5,12 @@ using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.Extensions;
using File = Barotrauma.IO.File;
using FileStream = Barotrauma.IO.FileStream;
using Path = Barotrauma.IO.Path;
@@ -841,6 +843,13 @@ namespace Barotrauma
return vector;
}
private static readonly ImmutableDictionary<Identifier, Color> monoGameColors =
typeof(Color)
.GetProperties(BindingFlags.Static | BindingFlags.Public)
.Where(p => p.PropertyType == typeof(Color))
.Select(p => (p.Name.ToIdentifier(), p.GetValueFromStaticProperty<Color>()))
.ToImmutableDictionary();
public static Color ParseColor(string stringColor, bool errorMessages = true)
{
if (stringColor.StartsWith("gui.", StringComparison.OrdinalIgnoreCase))
@@ -864,6 +873,11 @@ namespace Barotrauma
return Color.White;
}
if (monoGameColors.TryGetValue(stringColor.ToIdentifier(), out var monoGameColor))
{
return monoGameColor;
}
string[] strComponents = stringColor.Split(',');
Color color = Color.White;
@@ -84,6 +84,7 @@ namespace Barotrauma
Graphics = GraphicsSettings.GetDefault(),
Audio = AudioSettings.GetDefault(),
#if CLIENT
CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown,
DisableGlobalSpamList = false,
KeyMap = KeyMapping.GetDefault(),
InventoryKeyMap = InventoryKeyMapping.GetDefault()
@@ -91,9 +92,7 @@ namespace Barotrauma
};
#if DEBUG
config.UseSteamMatchmaking = true;
config.QuickStartSub = "Humpback".ToIdentifier();
config.RequireSteamAuthentication = true;
config.AutomaticQuickStartEnabled = false;
config.AutomaticCampaignLoadEnabled = false;
config.TextManagerDebugModeEnabled = false;
@@ -156,20 +155,16 @@ namespace Barotrauma
public Identifier QuickStartSub;
public string RemoteMainMenuContentUrl;
#if CLIENT
public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice;
public XElement SavedCampaignSettings;
public bool DisableGlobalSpamList;
#endif
#if DEBUG
public bool UseSteamMatchmaking;
public bool RequireSteamAuthentication;
public bool AutomaticQuickStartEnabled;
public bool AutomaticCampaignLoadEnabled;
public bool TestScreenEnabled;
public bool TextManagerDebugModeEnabled;
public bool ModBreakerMode;
#else
public bool UseSteamMatchmaking => true;
public bool RequireSteamAuthentication => true;
#endif
public struct GraphicsSettings
@@ -7,19 +7,20 @@ namespace Barotrauma.Steam
{
static partial class SteamManager
{
private static Option<Steamworks.AuthTicket> currentMultiplayerTicket = Option.None;
public static Option<Steamworks.AuthTicket> GetAuthSessionTicketForMultiplayer(Endpoint remoteHostEndpoint)
#region Auth ticket for Steam host
private static Option<Steamworks.AuthTicket> currentSteamHostAuthTicket = Option.None;
public static Option<Steamworks.AuthTicket> GetAuthSessionTicketForSteamHost(Endpoint remoteHostEndpoint)
{
if (!IsInitialized)
{
return Option.None;
}
if (currentMultiplayerTicket.TryUnwrap(out var ticketToCancel))
if (currentSteamHostAuthTicket.TryUnwrap(out var ticketToCancel))
{
ticketToCancel.Cancel();
}
currentMultiplayerTicket = Option.None;
currentSteamHostAuthTicket = Option.None;
var netIdentity = remoteHostEndpoint switch
{
@@ -32,13 +33,42 @@ namespace Barotrauma.Steam
};
var newTicket = Steamworks.SteamUser.GetAuthSessionTicket(netIdentity);
currentMultiplayerTicket = newTicket != null
currentSteamHostAuthTicket = newTicket != null
? Option.Some(newTicket)
: Option.None;
return currentMultiplayerTicket;
return currentSteamHostAuthTicket;
}
#endregion Auth ticket for Steam host
#region Auth ticket for EOS host
private const string EosHostAuthIdentity = "BarotraumaRemotePlayerAuth";
private static Option<Steamworks.AuthTicketForWebApi> currentEosHostAuthTicket = Option.None;
public static async Task<Option<Steamworks.AuthTicketForWebApi>> GetAuthTicketForEosHostAuth()
{
if (!IsInitialized)
{
return Option.None;
}
if (currentEosHostAuthTicket.TryUnwrap(out var ticketToCancel))
{
ticketToCancel.Cancel();
}
currentEosHostAuthTicket = Option.None;
var newTicket = await Steamworks.SteamUser.GetAuthTicketForWebApi(identity: EosHostAuthIdentity);
currentEosHostAuthTicket = newTicket != null
? Option.Some(newTicket)
: Option.None;
return currentEosHostAuthTicket;
}
#endregion Auth ticket for EOS host
#region Auth ticket for GameAnalytics consent server
private const string GameAnalyticsConsentIdentity = "BarotraumaGameAnalyticsConsent";
private static Option<Steamworks.AuthTicketForWebApi> currentGameAnalyticsConsentTicket = Option.None;
@@ -63,5 +93,6 @@ namespace Barotrauma.Steam
return currentGameAnalyticsConsentTicket;
}
#endregion Auth ticket for GameAnalytics consent server
}
}
@@ -1,8 +1,10 @@
using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using Barotrauma.Networking;
using Barotrauma.IO;
namespace Barotrauma.Steam
{
@@ -38,6 +40,15 @@ namespace Barotrauma.Steam
}
}
public static bool SteamworksLibExists
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? File.Exists("steam_api64.dll")
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? File.Exists("libsteam_api64.dylib")
: RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
? File.Exists("libsteam_api64.so")
: false;
public static void Initialize()
{
InitializeProjectSpecific();
@@ -53,6 +64,16 @@ namespace Barotrauma.Steam
return Option<SteamId>.Some(new SteamId(Steamworks.SteamClient.SteamId));
}
public static Option<SteamId> GetOwnerSteamId()
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
{
return Option<SteamId>.None();
}
return Option<SteamId>.Some(new SteamId(Steamworks.SteamClient.SteamId));
}
public static bool IsFamilyShared()
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; }
@@ -112,42 +133,78 @@ namespace Barotrauma.Steam
return unlocked;
}
public static bool IncrementStat(Identifier statName, int increment)
/// <summary>
/// Increment multiple stats in bulk.
/// Make sure to call StoreStats() after calling this method since it doesn't do it automatically.
/// </summary>
/// <param name="stats"></param>
public static void IncrementStats(params (AchievementStat Identifier, float Increment)[] stats)
=> Array.ForEach(stats, static s
=> IncrementStat(s.Identifier, s.Increment, storeStats: false));
public static bool IncrementStat(AchievementStat statName, int increment, bool storeStats = true)
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; }
DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment);
bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment);
bool success = Steamworks.SteamUserStats.AddStatInt(statName.ToIdentifier().Value.ToLowerInvariant(), increment);
if (!success)
{
DebugConsole.Log("Failed to increment stat \"" + statName + "\".");
}
else
else if (storeStats)
{
StoreStats();
}
return success;
}
public static bool IncrementStat(Identifier statName, float increment)
public static bool IncrementStat(AchievementStat statName, float increment, bool storeStats = true)
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return false; }
DebugConsole.Log($"Incremented stat \"{statName}\" by " + increment);
bool success = Steamworks.SteamUserStats.AddStat(statName.Value.ToLowerInvariant(), increment);
bool success = Steamworks.SteamUserStats.AddStatFloat(statName.ToIdentifier().Value.ToLowerInvariant(), increment);
if (!success)
{
DebugConsole.Log("Failed to increment stat \"" + statName + "\".");
}
else
else if (storeStats)
{
StoreStats();
}
return success;
}
public static int GetStatInt(Identifier statName)
public static int GetStatInt(AchievementStat stat)
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0; }
return Steamworks.SteamUserStats.GetStatInt(statName.Value.ToLowerInvariant());
return Steamworks.SteamUserStats.GetStatInt(stat.ToString().ToLowerInvariant());
}
public static float GetStatFloat(AchievementStat stat)
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return 0f; }
return Steamworks.SteamUserStats.GetStatFloat(stat.ToString().ToLowerInvariant());
}
public static ImmutableDictionary<AchievementStat, float> GetAllStats()
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid) { return ImmutableDictionary<AchievementStat, float>.Empty; }
var builder = ImmutableDictionary.CreateBuilder<AchievementStat, float>();
foreach (AchievementStat stat in AchievementStatExtension.SteamStats)
{
if (stat.IsFloatStat())
{
builder.Add(stat, GetStatFloat(stat));
}
else
{
builder.Add(stat, GetStatInt(stat));
}
}
return builder.ToImmutable();
}
public static bool StoreStats()
@@ -177,7 +234,7 @@ namespace Barotrauma.Steam
{
//this should be run even if SteamManager is uninitialized
//servers need to be able to notify clients of unlocked talents even if the server isn't connected to Steam
SteamAchievementManager.Update(deltaTime);
AchievementManager.Update(deltaTime);
if (!IsInitialized) { return; }
@@ -193,22 +250,6 @@ namespace Barotrauma.Steam
if (Steamworks.SteamServer.IsValid) { Steamworks.SteamServer.Shutdown(); }
}
public static IEnumerable<ulong> ParseWorkshopIds(string workshopIdData)
{
string[] workshopIds = workshopIdData.Split(',');
foreach (string id in workshopIds)
{
if (ulong.TryParse(id, out ulong idCast))
{
yield return idCast;
}
else
{
yield return 0;
}
}
}
public static IEnumerable<ulong> WorkshopUrlsToIds(IEnumerable<string> urls)
{
return urls.Select((u) =>
@@ -140,7 +140,7 @@ namespace Barotrauma.Steam
return await queryTask;
}
public static async Task<Steamworks.Ugc.Item?> MakeRequest(UInt64 id)
public static async Task<Option<Steamworks.Ugc.Item>> MakeRequest(UInt64 id)
{
Task<WorkshopItemSet> ourTask;
lock (mutex)
@@ -155,7 +155,7 @@ namespace Barotrauma.Steam
}
var items = await ourTask;
var result = items.FirstOrNull(it => it.Id == id);
var result = items.FirstOrNone(it => it.Id == id);
return result;
}
}
@@ -165,7 +165,7 @@ namespace Barotrauma.Steam
/// The description of the returned item is truncated to save bandwidth.
/// </summary>
/// <param name="itemId">Workshop Item ID</param>
public static Task<Steamworks.Ugc.Item?> GetItem(UInt64 itemId)
public static Task<Option<Steamworks.Ugc.Item>> GetItem(UInt64 itemId)
=> SingleItemRequestPool.MakeRequest(itemId);
/// <summary>
@@ -176,15 +176,17 @@ namespace Barotrauma.Steam
/// <param name="withLongDescription">
/// If true, ask for the item's entire description, otherwise it'll be truncated.
/// </param>
public static async Task<Steamworks.Ugc.Item?> GetItemAsap(UInt64 itemId, bool withLongDescription = false)
public static async Task<Option<Steamworks.Ugc.Item>> GetItemAsap(UInt64 itemId, bool withLongDescription = false)
{
if (!IsInitialized) { return null; }
if (!IsInitialized) { return Option.None; }
var items = await GetWorkshopItems(
Steamworks.Ugc.Query.All
.WithFileId(itemId)
.WithLongDescription(withLongDescription));
return items.Any() ? items.First() : null;
return items.Any()
? Option.Some(items.First())
: Option.None;
}
public static async Task ForceRedownload(UInt64 itemId)
@@ -379,7 +381,7 @@ namespace Barotrauma.Steam
// made private. Players cannot download updates for these, so
// we treat them as if they were deleted.
allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value))))
.NotNull()
.NotNone()
.Where(it => it.ConsumerApp == AppID)
.ToHashSet();
@@ -399,7 +401,7 @@ namespace Barotrauma.Steam
TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t =>
{
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> items)) { return; }
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item>? items)) { return; }
var ids = items.Select(it => it.Id.Value).ToHashSet();
var toUninstall = ContentPackageManager.WorkshopPackages
.Where(pkg
@@ -428,8 +430,8 @@ namespace Barotrauma.Steam
{
using var installCounter = await InstallTaskCounter.Create(id);
var itemNullable = await GetItem(id);
if (!(itemNullable is { } item)) { return; }
var itemOption = await GetItem(id);
if (!itemOption.TryUnwrap(out var item)) { return; }
await Task.Yield();
string itemTitle = item.Title?.Trim() ?? "";
@@ -48,10 +48,23 @@ namespace Barotrauma
public abstract void RetrieveValue();
public static implicit operator LocalizedString(string value) => new RawLString(value);
public static readonly RawLString EmptyString = new RawLString("");
public static implicit operator LocalizedString(string value)
=> !value.IsNullOrEmpty()
? new RawLString(value)
: EmptyString;
public static implicit operator LocalizedString(char value) => new RawLString(value.ToString());
public static LocalizedString operator+(LocalizedString left, LocalizedString right) => new ConcatLString(left, right);
public static LocalizedString operator+(LocalizedString left, LocalizedString right)
{
// If either side of the concatenation is an empty string,
// return the other string instead of creating a new object
if (left is RawLString { Value.Length: 0 }) { return right; }
if (right is RawLString { Value.Length: 0 }) { return left; }
return new ConcatLString(left, right);
}
public static LocalizedString operator+(LocalizedString left, object right) => left + (right.ToString() ?? "");
public static LocalizedString operator+(object left, LocalizedString right) => (left.ToString() ?? "") + right;
@@ -1,11 +1,11 @@
using System;
using System.Linq;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("WindowsTest"),
InternalsVisibleTo("MacTest"),
InternalsVisibleTo("LinuxTest")]
public static class AssemblyInfo
{
public static readonly string GitRevision;
@@ -13,6 +13,38 @@ public static class AssemblyInfo
public static readonly string ProjectDir;
public static readonly string BuildString;
public enum Platform
{
Windows,
MacOS,
Linux
}
public enum Configuration
{
Release,
Unstable,
Debug
}
#if WINDOWS
public const Platform CurrentPlatform = Platform.Windows;
#elif OSX
public const Platform CurrentPlatform = Platform.MacOS;
#elif LINUX
public const Platform CurrentPlatform = Platform.Linux;
#else
#error Unknown platform
#endif
#if DEBUG
public const Configuration CurrentConfiguration = Configuration.Debug;
#elif UNSTABLE
public const Configuration CurrentConfiguration = Configuration.Unstable;
#else
public const Configuration CurrentConfiguration = Configuration.Release;
#endif
static AssemblyInfo()
{
var asm = typeof(AssemblyInfo).Assembly;
@@ -27,22 +59,7 @@ public static class AssemblyInfo
string[] dirSplit = ProjectDir.Split('/', '\\');
ProjectDir = string.Join(ProjectDir.Contains('/') ? '/' : '\\', dirSplit.Take(dirSplit.Length - 2));
BuildString = "Unknown";
#if WINDOWS
BuildString = "Windows";
#elif OSX
BuildString = "Mac";
#elif LINUX
BuildString = "Linux";
#endif
#if DEBUG
BuildString = "Debug" + BuildString;
#elif UNSTABLE
BuildString = "Unstable" + BuildString;
#else
BuildString = "Release" + BuildString;
#endif
BuildString = $"{CurrentConfiguration}{CurrentPlatform}";
}
public static string CleanupStackTrace(this string stackTrace)
@@ -1,124 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
public class CollectionConcat<T> : ICollection<T>
{
protected readonly IEnumerable<T> enumerableA;
protected readonly IEnumerable<T> enumerableB;
public CollectionConcat(IEnumerable<T> a, IEnumerable<T> b)
{
enumerableA = a; enumerableB = b;
}
public int Count => enumerableA.Count()+enumerableB.Count();
public bool IsReadOnly => true;
public void Add(T item) => throw new InvalidOperationException();
public void Clear() => throw new InvalidOperationException();
public bool Remove(T item) => throw new InvalidOperationException();
public bool Contains(T item) => enumerableA.Contains(item) || enumerableB.Contains(item);
public void CopyTo(T[] array, int arrayIndex)
{
void performCopy(IEnumerable<T> enumerable)
{
if (enumerable is ICollection<T> collection)
{
collection.CopyTo(array, arrayIndex);
arrayIndex += collection.Count;
}
else
{
foreach (var item in enumerable)
{
array[arrayIndex] = item;
arrayIndex++;
}
}
}
performCopy(enumerableA);
performCopy(enumerableB);
}
public IEnumerator<T> GetEnumerator()
{
foreach (T item in enumerableA) { yield return item; }
foreach (T item in enumerableB) { yield return item; }
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class ListConcat<T> : CollectionConcat<T>, IList<T>, IReadOnlyList<T>
{
public ListConcat(IEnumerable<T> a, IEnumerable<T> b) : base(a, b) { }
public int IndexOf(T item)
{
int aCount = 0;
if (enumerableA is IList<T> listA)
{
int index = listA.IndexOf(item);
if (index >= 0) { return index; }
aCount = listA.Count;
}
else
{
foreach (var a in enumerableA)
{
if (object.Equals(item, a)) { return aCount; }
aCount++;
}
}
if (enumerableB is IList<T> listB)
{
int index = listB.IndexOf(item);
if (index >= 0) { return index + aCount; }
}
else
{
foreach (var b in enumerableB)
{
if (object.Equals(item, b)) { return aCount; }
aCount++;
}
}
return -1;
}
public void Insert(int index, T item)
{
throw new InvalidOperationException();
}
public void RemoveAt(int index)
{
throw new InvalidOperationException();
}
public T this[int index]
{
get
{
int aCount = enumerableA.Count();
return index < aCount ? enumerableA.ElementAt(index) : enumerableB.ElementAt(index - aCount);
}
set
{
throw new InvalidOperationException();
}
}
}
}
@@ -1,105 +0,0 @@
#nullable enable
using System;
namespace Barotrauma
{
public abstract class Either<T, U> where T : notnull where U : notnull
{
public static implicit operator Either<T, U>(T t) => new EitherT<T, U>(t);
public static implicit operator Either<T, U>(U u) => new EitherU<T, U>(u);
public static explicit operator T(Either<T, U> e) => e.TryGet(out T t) ? t : throw new InvalidCastException($"Contained object is not of type {typeof(T).Name}");
public static explicit operator U(Either<T, U> e) => e.TryGet(out U u) ? u : throw new InvalidCastException($"Contained object is not of type {typeof(U).Name}");
public abstract bool TryGet(out T t);
public abstract bool TryGet(out U u);
public abstract bool TryCast<V>(out V v);
public abstract override string? ToString();
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public static bool operator ==(Either<T, U>? a, Either<T, U>? b)
=> a is null ? b is null : a.Equals(b);
public static bool operator !=(Either<T, U>? a, Either<T, U>? b)
=> !(a == b);
}
public sealed class EitherT<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly T Value;
public EitherT(T value) { Value = value; }
public override string? ToString()
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})";
public override bool TryGet(out T t) { t = Value; return true; }
public override bool TryGet(out U u) { u = default!; return false; }
public override bool TryCast<V>(out V v)
{
if (Value is V result)
{
v = result;
return true;
}
else
{
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherT<T, U> other => Value.Equals(other.Value),
T value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
public sealed class EitherU<T, U> : Either<T, U> where T : notnull where U : notnull
{
public readonly U Value;
public EitherU(U value) { Value = value; }
public override string? ToString()
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})";
public override bool TryGet(out T t) { t = default!; return false; }
public override bool TryGet(out U u) { u = Value; return true; }
public override bool TryCast<V>(out V v)
{
if (Value is V result)
{
v = result;
return true;
}
else
{
v = default!;
return false;
}
}
public override bool Equals(object? obj)
=> obj switch
{
EitherU<T, U> other => Value.Equals(other.Value),
U value => Value.Equals(value),
_ => false
};
public override int GetHashCode() => Value.GetHashCode();
}
}
File diff suppressed because it is too large Load Diff
@@ -44,26 +44,10 @@ namespace Barotrauma
}
private static string ByteRepresentationToStringRepresentation(byte[] byteHash)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < byteHash.Length; i++)
{
sb.Append(byteHash[i].ToString("X2"));
}
return sb.ToString();
}
=> ToolBoxCore.ByteArrayToHexString(byteHash);
private static byte[] StringRepresentationToByteRepresentation(string strHash)
{
var byteRepresentation = new byte[strHash.Length / 2];
for (int i = 0; i < byteRepresentation.Length; i++)
{
byteRepresentation[i] = Convert.ToByte(strHash.Substring(i * 2, 2), 16);
}
return byteRepresentation;
}
=> ToolBoxCore.HexStringToByteArray(strHash);
public static string GetShortHash(string fullHash)
{
@@ -1,56 +0,0 @@
using System;
using System.Collections.Generic;
namespace Barotrauma
{
internal sealed class NamedEvent<T> : IDisposable
{
private readonly Dictionary<Identifier, Action<T>> events = new Dictionary<Identifier, Action<T>>();
public void Register(Identifier identifier, Action<T> action)
{
if (HasEvent(identifier))
{
throw new ArgumentException($"Event with the identifier \"{identifier}\" has already been registered.", nameof(identifier));
}
events.Add(identifier, action);
}
public void RegisterOverwriteExisting(Identifier identifier, Action<T> action)
{
if (HasEvent(identifier))
{
Deregister(identifier);
}
Register(identifier, action);
}
public void Deregister(Identifier identifier)
{
events.Remove(identifier);
}
public void TryDeregister(Identifier identifier)
{
if (!HasEvent(identifier)) { return; }
Deregister(identifier);
}
public bool HasEvent(Identifier identifier) => events.ContainsKey(identifier);
public void Invoke(T data)
{
foreach (var (_, action) in events)
{
action?.Invoke(data);
}
}
public void Dispose()
{
events.Clear();
}
}
}
@@ -1,125 +0,0 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
public readonly struct Option<T> where T : notnull
{
private readonly bool hasValue;
private readonly T? value;
private Option(bool hasValue, T? value)
{
this.hasValue = hasValue;
this.value = value;
}
public bool IsSome() => hasValue;
public bool IsNone() => !IsSome();
public bool TryUnwrap<T1>([NotNullWhen(returnValue: true)] out T1? outValue) where T1 : T
{
bool hasValueOfGivenType = false;
outValue = default;
if (hasValue && value is T1 t1)
{
hasValueOfGivenType = true;
outValue = t1;
}
return hasValueOfGivenType;
}
public bool TryUnwrap([NotNullWhen(returnValue: true)] out T? outValue)
=> TryUnwrap<T>(out outValue);
public Option<TType> Select<TType>(Func<T, TType> selector) where TType : notnull
=> TryUnwrap(out T? selfValue) ? Option.Some(selector(selfValue)) : Option.None;
public Option<TType> Bind<TType>(Func<T, Option<TType>> binder) where TType : notnull
=> TryUnwrap(out T? selfValue) ? binder(selfValue) : Option.None;
public T Match(Func<T, T> some, Func<T> none)
=> TryUnwrap(out T? selfValue) ? some(selfValue) : none();
public void Match(Action<T> some, Action none)
{
if (TryUnwrap(out T? selfValue))
{
some(selfValue);
return;
}
none();
}
public T Fallback(T fallback)
=> TryUnwrap(out var v) ? v : fallback;
public Option<T> Fallback(Option<T> fallback)
=> IsSome() ? this : fallback;
public static Option<T> Some(T value)
=> typeof(T) switch
{
var t when t == typeof(bool)
=> throw new Exception("Option type rejects booleans"),
{IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Option<>)
=> throw new Exception("Option type rejects nested Option"),
{IsConstructedGenericType: true} t when t.GetGenericTypeDefinition() == typeof(Nullable<>)
=> throw new Exception("Option type rejects Nullable"),
_
=> new Option<T>(hasValue: true, value: value ?? throw new Exception("Option type rejects null"))
};
public override bool Equals(object? obj)
=> obj switch
{
Option<T> otherOption when otherOption.IsNone()
=> IsNone(),
Option<T> otherOption when otherOption.TryUnwrap(out var otherValue)
=> ValueEquals(otherValue),
T otherValue
=> ValueEquals(otherValue),
_
=> false
};
public bool ValueEquals(T otherValue)
=> TryUnwrap(out T? selfValue) && selfValue.Equals(otherValue);
public override int GetHashCode()
=> TryUnwrap(out T? selfValue) ? selfValue.GetHashCode() : 0;
public static bool operator ==(Option<T> a, Option<T> b)
=> a.Equals(b);
public static bool operator !=(Option<T> a, Option<T> b)
=> !(a == b);
public static Option<T> None()
=> default;
public static implicit operator Option<T>(in Option.UnspecifiedNone _)
=> None();
public override string ToString()
=> TryUnwrap(out var selfValue)
? $"Some<{typeof(T).Name}>({selfValue})"
: $"None<{typeof(T).Name}>";
}
public static class Option
{
public static Option<T> Some<T>(T value) where T : notnull
=> Option<T>.Some(value);
public static UnspecifiedNone None
=> default;
public readonly ref struct UnspecifiedNone
{
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -30,6 +31,7 @@ namespace Barotrauma
public static Random GetRNG(RandSync randSync)
{
CheckRandThreadSafety(randSync);
return randSync == RandSync.Unsynced ? localRandom : syncedRandom[randSync];
}
@@ -70,16 +72,10 @@ namespace Barotrauma
}
public static float Range(float minimum, float maximum, RandSync sync=RandSync.Unsynced)
{
CheckRandThreadSafety(sync);
return (float)(sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum;
}
=> GetRNG(sync).Range(minimum, maximum);
public static double Range(double minimum, double maximum, RandSync sync = RandSync.Unsynced)
{
CheckRandThreadSafety(sync);
return (sync == RandSync.Unsynced ? localRandom : (syncedRandom[sync])).NextDouble() * (maximum - minimum) + minimum;
}
=> GetRNG(sync).Range(minimum, maximum);
/// <summary>
/// Min inclusive, Max exclusive!
@@ -1,51 +0,0 @@
#nullable enable
using System;
namespace Barotrauma
{
/// <summary>
/// An inclusive range, i.e. [Start, End] where Start <= End
/// </summary>
public struct Range<T> where T : notnull, IComparable<T>
{
private T start; private T end;
public T Start
{
get { return start; }
set
{
start = value;
VerifyStartLessThanEnd();
}
}
public T End
{
get { return end; }
set
{
end = value;
VerifyEndGreaterThanStart();
}
}
public readonly bool Contains(in T v)
=> start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0;
private void VerifyStartLessThanEnd()
{
if (start.CompareTo(end) > 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.Start set to a value greater than End ({start} > {end})"); }
}
private void VerifyEndGreaterThanStart()
{
if (end.CompareTo(start) < 0) { throw new InvalidOperationException($"Range<{typeof(T).Name}>.End set to a value less than Start ({end} < {start})"); }
}
public Range(T start, T end)
{
this.start = start; this.end = end;
VerifyEndGreaterThanStart();
}
}
}
@@ -1,153 +0,0 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
namespace Barotrauma
{
public static class ReflectionUtils
{
private static readonly ConcurrentDictionary<Assembly, ImmutableArray<Type>> CachedNonAbstractTypes = new();
private static readonly ConcurrentDictionary<string, ImmutableArray<Type>> TypeSearchCache = new();
public static IEnumerable<Type> GetDerivedNonAbstract<T>()
{
Type t = typeof(T);
string typeName = t.FullName ?? t.Name;
// search quick lookup cache
if (TypeSearchCache.TryGetValue(typeName, out var value))
{
return value;
}
// doesn't exist so let's add it.
Assembly assembly = typeof(T).Assembly;
if (!CachedNonAbstractTypes.ContainsKey(assembly))
{
AddNonAbstractAssemblyTypes(assembly);
}
// build cache from registered assemblies' types.
var list = CachedNonAbstractTypes.Values
.SelectMany(arr => arr.Where(type => type.IsSubclassOf(t)))
.ToImmutableArray();
if (list.Length == 0)
{
return ImmutableArray<Type>.Empty; // No types, don't add to cache
}
if (!TypeSearchCache.TryAdd(typeName, list))
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | Error while adding to quick lookup cache.");
}
return list;
}
/// <summary>
/// Adds an assembly's Non-Abstract Types to the cache for Barotrauma's Type lookup.
/// </summary>
/// <param name="assembly">Assembly to be added</param>
/// <param name="overwrite">Whether or not to overwrite an entry if the assembly already exists within it.</param>
public static void AddNonAbstractAssemblyTypes(Assembly assembly, bool overwrite = false)
{
if (CachedNonAbstractTypes.ContainsKey(assembly))
{
if (!overwrite)
{
DebugConsole.LogError(
$"ReflectionUtils::AddNonAbstractAssemblyTypes() | The assembly [{assembly.GetName()}] already exists in the cache.");
return;
}
CachedNonAbstractTypes.Remove(assembly, out _);
}
try
{
if (!CachedNonAbstractTypes.TryAdd(assembly, assembly.GetSafeTypes().Where(t => !t.IsAbstract).ToImmutableArray()))
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | Unable to add types from Assembly to cache.");
}
else
{
TypeSearchCache.Clear(); // Needs to be rebuilt to include potential new types
}
}
catch (ReflectionTypeLoadException e)
{
DebugConsole.LogError($"ReflectionUtils::AddNonAbstractAssemblyTypes() | RTFException: Unable to load Assembly Types from {assembly.GetName()}.");
}
}
/// <summary>
/// Removes an assembly from the cache for Barotrauma's Type lookup.
/// </summary>
/// <param name="assembly">Assembly to remove.</param>
public static void RemoveAssemblyFromCache(Assembly assembly)
{
CachedNonAbstractTypes.Remove(assembly, out _);
TypeSearchCache.Clear();
}
/// <summary>
/// Clears all cached assembly data and rebuilds types list only to include base Barotrauma types.
/// </summary>
internal static void ResetCache()
{
CachedNonAbstractTypes.Clear();
CachedNonAbstractTypes.TryAdd(typeof(ReflectionUtils).Assembly, typeof(ReflectionUtils).Assembly.GetSafeTypes().ToImmutableArray());
TypeSearchCache.Clear();
}
public static Option<TBase> ParseDerived<TBase, TInput>(TInput input) where TInput : notnull where TBase : notnull
{
static Option<TBase> none() => Option<TBase>.None();
var derivedTypes = GetDerivedNonAbstract<TBase>();
Option<TBase> parseOfType(Type t)
{
//every TBase type is expected to have a method with the following signature:
// public static Option<T> Parse(TInput str)
var parseFunc = t.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static);
if (parseFunc is null) { return none(); }
var parameters = parseFunc.GetParameters();
if (parameters.Length != 1) { return none(); }
var returnType = parseFunc.ReturnType;
if (!returnType.IsConstructedGenericType) { return none(); }
if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); }
if (returnType.GenericTypeArguments[0] != t) { return none(); }
//some hacky business to convert from Option<T2> to Option<TBase> when we only know T2 at runtime
static Option<TBase> convert<T2>(Option<T2> option) where T2 : TBase
=> option.Select(v => (TBase)v);
Func<Option<TBase>, Option<TBase>> f = convert;
var genericArgs = f.Method.GetGenericArguments();
genericArgs[^1] = t;
var constructedConverter =
f.Method.GetGenericMethodDefinition().MakeGenericMethod(genericArgs);
return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) })
as Option<TBase>? ?? none();
}
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome());
}
public static string NameWithGenerics(this Type t)
{
if (!t.IsGenericType) { return t.Name; }
string result = t.Name[..t.Name.IndexOf('`')];
result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>";
return result;
}
}
}
@@ -1,85 +0,0 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
public abstract class Result<T, TError>
where T: notnull
where TError: notnull
{
public abstract bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public static Result<T, TError> Success(T value)
=> new Success<T, TError>(value);
public static Result<T, TError> Failure(TError error)
=> new Failure<T, TError>(error);
public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value);
public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value);
public abstract override string? ToString();
public static (Func<T, Result<T, TError>> Success, Func<TError, Result<T, TError>> Failure) GetFactoryMethods()
=> (Success, Failure);
}
public sealed class Success<T, TError> : Result<T, TError>
where T: notnull
where TError: notnull
{
public readonly T Value;
public override bool IsSuccess => true;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
{
value = Value;
return true;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
{
value = default;
return false;
}
public override string ToString()
=> $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})";
public Success(T value)
{
Value = value;
}
}
public sealed class Failure<T, TError> : Result<T, TError>
where T: notnull
where TError: notnull
{
public readonly TError Error;
public override bool IsSuccess => false;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
{
value = default;
return false;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
{
value = Error;
return true;
}
public override string ToString()
=> $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})";
public Failure(TError error)
{
Error = error;
}
}
}
@@ -1,20 +1,10 @@
using System.Threading.Tasks;
#nullable enable
using System.Threading.Tasks;
namespace Barotrauma
{
static class TaskExtensions
public static class TaskExtensions
{
public static bool TryGetResult<T>(this Task task, out T result)
{
if (task is Task<T> { IsCompletedSuccessfully: true } castTask)
{
result = castTask.Result;
return true;
}
result = default;
return false;
}
public static async Task<T> WaitForLoadingScreen<T>(this Task<T> task)
{
var result = await task;
@@ -27,4 +17,4 @@ namespace Barotrauma
return result;
}
}
}
}
@@ -1,102 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma
{
public static class TaskPool
{
const int MaxTasks = 5000;
private struct TaskAction
{
public string Name;
public Task Task;
public Action<Task, object> OnCompletion;
public object UserData;
}
private static readonly List<TaskAction> taskActions = new List<TaskAction>();
public static void ListTasks()
{
lock (taskActions)
{
DebugConsole.NewMessage($"Task count: {taskActions.Count}");
for (int i = 0; i < taskActions.Count; i++)
{
DebugConsole.NewMessage($" -{i}: {taskActions[i].Name}, {taskActions[i].Task.Status}");
}
}
}
public static bool IsTaskRunning(string name)
{
lock (taskActions)
{
return taskActions.Any(t => t.Name == name);
}
}
private static void AddInternal(string name, Task task, Action<Task, object> onCompletion, object userdata, bool addIfFound = true)
{
lock (taskActions)
{
if (!addIfFound)
{
if (taskActions.Any(t => t.Name == name)) { return; }
}
if (taskActions.Count >= MaxTasks)
{
throw new Exception(
"Too many tasks in the TaskPool:\n" + string.Join('\n', taskActions.Select(ta => ta.Name))
);
}
taskActions.Add(new TaskAction() { Name = name, Task = task, OnCompletion = onCompletion, UserData = userdata });
DebugConsole.Log($"New task: {name} ({taskActions.Count}/{MaxTasks})");
}
}
public static void Add(string name, Task task, Action<Task> onCompletion)
{
AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null);
}
public static void AddIfNotFound(string name, Task task, Action<Task> onCompletion)
{
AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null, addIfFound: false);
}
public static void Add<U>(string name, Task task, U userdata, Action<Task, U> onCompletion) where U : class
{
AddInternal(name, task, (Task t, object obj) => { onCompletion?.Invoke(t, (U)obj); }, userdata);
}
public static void Update()
{
lock (taskActions)
{
for (int i = 0; i < taskActions.Count; i++)
{
if (taskActions[i].Task.IsCompleted)
{
taskActions[i].OnCompletion?.Invoke(taskActions[i].Task, taskActions[i].UserData);
DebugConsole.Log($"Task {taskActions[i].Name} completed ({taskActions.Count-1}/{MaxTasks})");
taskActions.RemoveAt(i);
i--;
}
}
}
}
public static void PrintTaskExceptions(Task task, string msg)
{
DebugConsole.ThrowError(msg);
foreach (Exception e in task.Exception.InnerExceptions)
{
DebugConsole.ThrowError(e.Message + "\n" + e.StackTrace.CleanupStackTrace());
}
}
}
}
@@ -1,34 +0,0 @@
using System.Threading;
namespace Barotrauma.Threading
{
internal readonly ref struct ReadLock
{
private readonly ReaderWriterLockSlim rwl;
public ReadLock(ReaderWriterLockSlim rwl)
{
this.rwl = rwl;
rwl.EnterReadLock();
}
public void Dispose()
{
rwl.ExitReadLock();
}
}
internal readonly ref struct WriteLock
{
private readonly ReaderWriterLockSlim rwl;
public WriteLock(ReaderWriterLockSlim rwl)
{
this.rwl = rwl;
rwl.EnterWriteLock();
}
public void Dispose()
{
rwl.ExitWriteLock();
}
}
}
@@ -199,38 +199,6 @@ namespace Barotrauma
return inputType;
}
/// <summary>
/// Convert a HSV value into a RGB value.
/// </summary>
/// <param name="hue">Value between 0 and 360</param>
/// <param name="saturation">Value between 0 and 1</param>
/// <param name="value">Value between 0 and 1</param>
/// <see href="https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB">Reference</see>
/// <returns></returns>
public static Color HSVToRGB(float hue, float saturation, float value)
{
float c = value * saturation;
float h = Math.Clamp(hue, 0, 360) / 60f;
float x = c * (1 - Math.Abs(h % 2 - 1));
float r = 0,
g = 0,
b = 0;
if (0 <= h && h <= 1) { r = c; g = x; b = 0; }
else if (1 < h && h <= 2) { r = x; g = c; b = 0; }
else if (2 < h && h <= 3) { r = 0; g = c; b = x; }
else if (3 < h && h <= 4) { r = 0; g = x; b = c; }
else if (4 < h && h <= 5) { r = x; g = 0; b = c; }
else if (5 < h && h <= 6) { r = c; g = 0; b = x; }
float m = value - c;
return new Color(r + m, g + m, b + m);
}
/// <summary>
/// Returns either a green [x] or a red [o]
/// </summary>
@@ -459,24 +427,6 @@ namespace Barotrauma
return default;
}
public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5)
=> StringToUInt32Hash(id.Value.ToLowerInvariant(), md5);
public static UInt32 StringToUInt32Hash(string str, MD5 md5)
{
//calculate key based on MD5 hash instead of string.GetHashCode
//to ensure consistent results across platforms
byte[] inputBytes = Encoding.UTF8.GetBytes(str);
byte[] hash = md5.ComputeHash(inputBytes);
UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead?
key |= (UInt32)(hash[hash.Length - 3] << 16);
key |= (UInt32)(hash[hash.Length - 2] << 8);
key |= (UInt32)(hash[hash.Length - 1]);
return key;
}
/// <summary>
/// Returns a new instance of the class with all properties and fields copied.
/// </summary>
@@ -542,14 +492,6 @@ namespace Barotrauma
}
}
public static string ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
foreach (byte b in ba)
hex.AppendFormat("{0:x2}", b);
return hex.ToString();
}
public static string EscapeCharacters(string str)
{
return str.Replace("\\", "\\\\").Replace("\"", "\\\"");
@@ -692,13 +634,6 @@ namespace Barotrauma
return new Rectangle(topLeft, size);
}
public static Exception GetInnermost(this Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
return e;
}
public static void ThrowIfNull<T>([NotNull] T o)
{
if (o is null) { throw new ArgumentNullException(); }