Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
+157
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (tags.None())
|
||||
{
|
||||
return character.GetEquippedItem(null) != null;
|
||||
return character.GetEquippedItem(Identifier.Empty) != null;
|
||||
}
|
||||
|
||||
if (requireAll)
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+3
-3
@@ -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());
|
||||
|
||||
+3
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
-79
@@ -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);
|
||||
}
|
||||
}
|
||||
-37
@@ -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);
|
||||
}
|
||||
}
|
||||
-16
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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();
|
||||
}
|
||||
}
|
||||
+21
@@ -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 */ }
|
||||
}
|
||||
+58
@@ -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 */ }
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+12
@@ -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);
|
||||
}
|
||||
+12
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
+38
@@ -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;
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Barotrauma.Networking;
|
||||
|
||||
sealed class EosP2PConnection : P2PConnection<EosP2PEndpoint>
|
||||
{
|
||||
public EosP2PConnection(EosP2PEndpoint endpoint) : base(endpoint) { }
|
||||
}
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
sealed class LidgrenConnection : NetworkConnection
|
||||
sealed class LidgrenConnection : NetworkConnection<LidgrenEndpoint>
|
||||
{
|
||||
public readonly NetConnection NetConnection;
|
||||
|
||||
|
||||
+10
-3
@@ -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;
|
||||
}
|
||||
|
||||
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-17
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+38
-32
@@ -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(); }
|
||||
|
||||
Reference in New Issue
Block a user