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
@@ -0,0 +1,19 @@
<Project>
<!-- Build EosInterface.Implementation for Release/Unstable builds -->
<Target Name="DeployEosPrivateBuildRelease" AfterTargets="CoreCompile" Condition="'$(Configuration)'!='Debug'" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.$(ProjectFileNamePlatformSuffix).csproj -c Release -r $(ManualDeployRuntime) --no-self-contained --nologo -o $(ProjectDir)$(OutputPath)" ContinueOnError="true" />
</Target>
<Target Name="DeployEosPrivatePublishRelease" AfterTargets="PrepareForPublish" Condition="'$(Configuration)'!='Debug'" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.$(ProjectFileNamePlatformSuffix).csproj -c Release -r $(ManualDeployRuntime) --no-self-contained --nologo -o &quot;$(PublishDir)&quot;" ContinueOnError="true" />
</Target>
<!-- Build EosInterface.Implementation for Debug builds -->
<Target Name="DeployEosPrivateBuildDebug" AfterTargets="CoreCompile" Condition="'$(Configuration)'=='Debug'" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.$(ProjectFileNamePlatformSuffix).csproj -c Debug -r $(ManualDeployRuntime) --no-self-contained --nologo -o $(ProjectDir)$(OutputPath)" ContinueOnError="true" />
</Target>
<Target Name="DeployEosPrivatePublishDebug" AfterTargets="PrepareForPublish" Condition="'$(Configuration)'=='Debug'" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/BarotraumaLibs/EosInterfacePrivate/EosInterface.Implementation.$(ProjectFileNamePlatformSuffix).csproj -c Debug -r $(ManualDeployRuntime) --no-self-contained --nologo -o &quot;$(PublishDir)&quot;" ContinueOnError="true" />
</Target>
</Project>
@@ -9,10 +9,10 @@
<IsNotDebug>false</IsNotDebug>
</PropertyGroup>
<Target Name="DeployGameAnalyticsBuild" AfterTargets="CoreCompile" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj -c Release -r $(GARuntime) --no-self-contained --nologo -o $(ProjectDir)$(OutputPath)" ContinueOnError="$(IsDebug)" />
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj -c Release -r $(ManualDeployRuntime) --no-self-contained --nologo -o $(ProjectDir)$(OutputPath)" ContinueOnError="$(IsDebug)" />
</Target>
<Target Name="DeployGameAnalyticsPublish" AfterTargets="PrepareForPublish" >
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj -c Release -r $(GARuntime) --no-self-contained --nologo -o &quot;$(PublishDir)&quot;" ContinueOnError="$(IsDebug)" />
<Exec Command="dotnet publish $(ProjectDir)../../Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj -c Release -r $(ManualDeployRuntime) --no-self-contained --nologo -o &quot;$(PublishDir)&quot;" ContinueOnError="$(IsDebug)" />
</Target>
</Project>
@@ -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(); }
+14
View File
@@ -1,3 +1,17 @@
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.3.0.1
-------------------------------------------------------------------------------------------------------------------------------------------------
- Fixed character face selection not working in the singleplayer campaign setup menu.
- Made the "connection lost" error messages when connecting to a server fails more descriptive.
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.3.0.0
-------------------------------------------------------------------------------------------------------------------------------------------------
- Epic Games Store release.
- Steam players have the option to enable crossplay between Steam and the Epic Store. When you launch Barotrauma for the first time following our EGS release, you will see a prompt asking whether you want to enable crossplay. If you do not wish to do so, you can decline, and that will be all. You can also update your preferences at any time in the game settings.
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.2.8.0
-------------------------------------------------------------------------------------------------------------------------------------------------