Unstable v0.19.5.0

This commit is contained in:
Juan Pablo Arce
2022-09-14 12:47:17 -03:00
parent 3f2c843247
commit 1fd2a51bbb
158 changed files with 5702 additions and 4813 deletions
@@ -527,7 +527,7 @@ namespace Barotrauma
{
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = false;
foreach (var button in door.Item.GetConnectedComponents<Controller>(true))
foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
{
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{
@@ -675,6 +675,8 @@ namespace Barotrauma
}
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
//heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
{
closestButton = button;
@@ -1634,9 +1634,9 @@ namespace Barotrauma
return id;
}
public static void ApplyHealthData(Character character, XElement healthData)
public static void ApplyHealthData(Character character, XElement healthData, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); }
}
/// <summary>
@@ -12,12 +12,8 @@ namespace Barotrauma
{
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
Character.RemoveByPrefab(this);
}
@@ -11,13 +11,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
public static CorpsePrefab Get(Identifier identifier)
{
@@ -1227,7 +1227,7 @@ namespace Barotrauma
}
}
public void Load(XElement element)
public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
foreach (var subElement in element.Elements())
{
@@ -1260,6 +1260,7 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return;
}
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
@@ -65,13 +65,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
private static readonly Dictionary<Identifier, float> _itemRepairPriorities = new Dictionary<Identifier, float>();
/// <summary>
@@ -56,11 +56,6 @@ namespace Barotrauma
}
}
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
}
public override void Dispose() { }
}
}
@@ -40,7 +40,9 @@ namespace Barotrauma
}
catch
{
prefab.Dispose(); //clean up before rethrowing, since some prefab types might lock resources
//clean up before rethrowing, since some prefab types might lock resources
prefab.Dispose();
Prefabs.Remove(prefab);
throw;
}
}
@@ -29,25 +29,25 @@ namespace Barotrauma
public readonly ImmutableArray<string> AltNames;
public readonly string Path;
public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? "";
public readonly UInt64 SteamWorkshopId;
public readonly Option<ContentPackageId> UgcId;
public readonly Version GameVersion;
public readonly string ModVersion;
public Md5Hash Hash { get; private set; }
public readonly DateTime? InstallTime;
public readonly Option<DateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
public async Task<bool> IsUpToDate()
{
if (SteamWorkshopId != 0 && InstallTime.HasValue)
{
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= InstallTime;
}
return true;
if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
if (!(ugcId is 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;
}
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -66,18 +66,19 @@ namespace Barotrauma
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
UgcId = steamWorkshopId != 0
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
: Option<ContentPackageId>.None();
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
if (rootElement.Attribute("installtime") != null)
{
InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0));
}
else
{
InstallTime = null;
}
UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0);
InstallTime = installTimeUnix != 0
? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
: Option<DateTime>.None();
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))
@@ -0,0 +1,19 @@
#nullable enable
namespace Barotrauma
{
public abstract class ContentPackageId
{
public abstract string StringRepresentation { get; }
public override string ToString()
=> StringRepresentation;
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public static Option<ContentPackageId> Parse(string s)
=> ReflectionUtils.ParseDerived<ContentPackageId, string>(s);
}
}
@@ -0,0 +1,32 @@
#nullable enable
using System;
using System.Globalization;
namespace Barotrauma
{
sealed class SteamWorkshopId : ContentPackageId
{
public readonly UInt64 Value;
public SteamWorkshopId(UInt64 value)
{
Value = value;
}
private const string Prefix = "STEAM_WORKSHOP_";
public override string StringRepresentation => Value.ToString(CultureInfo.InvariantCulture);
public override bool Equals(object? obj)
=> obj is SteamWorkshopId otherWorkshopId && otherWorkshopId.Value == Value;
public override int GetHashCode() => Value.GetHashCode();
public new static Option<SteamWorkshopId> Parse(string s)
{
if (s.StartsWith(Prefix)) { s = s[Prefix.Length..]; }
if (!UInt64.TryParse(s, out var id) || id == 0) { return Option<SteamWorkshopId>.None(); }
return Option<SteamWorkshopId>.Some(new SteamWorkshopId(id));
}
}
}
@@ -181,7 +181,7 @@ namespace Barotrauma
{
if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
{
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ??
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ??
ContentPackageManager.CorePackages.First());
}
@@ -193,7 +193,7 @@ namespace Barotrauma
newRegular.Add(p);
}
else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
=> p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP)
=> p2.UgcId == p.UgcId) is { } newP)
{
newRegular.Add(newP);
}
@@ -43,10 +43,10 @@ namespace Barotrauma
cachedValue = cachedValue
.Replace(ModDirStr, modPath, StringComparison.OrdinalIgnoreCase)
.Replace(string.Format(OtherModDirFmt, ContentPackage.Name), modPath, StringComparison.OrdinalIgnoreCase);
if (ContentPackage.SteamWorkshopId != 0)
if (ContentPackage.UgcId.TryUnwrap(out var ugcId))
{
cachedValue = cachedValue
.Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase);
.Replace(string.Format(OtherModDirFmt, ugcId.StringRepresentation), modPath, StringComparison.OrdinalIgnoreCase);
}
}
var allPackages = ContentPackageManager.AllPackages;
@@ -55,9 +55,9 @@ namespace Barotrauma
#endif
foreach (Identifier otherModName in otherMods)
{
if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; }
Option<ContentPackageId> ugcId = ContentPackageId.Parse(otherModName.Value);
ContentPackage? otherMod =
allPackages.FirstOrDefault(p => workshopId != 0 && p.SteamWorkshopId != 0 && workshopId == p.SteamWorkshopId)
allPackages.FirstOrDefault(p => ugcId == p.UgcId)
?? allPackages.FirstOrDefault(p => p.Name == otherModName)
?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName))
?? throw new MissingContentPackageException(ContentPackage, otherModName.Value);
@@ -278,6 +278,19 @@ namespace Barotrauma
}
}
}
public static void ListCoroutines()
{
lock (Coroutines)
{
DebugConsole.NewMessage("***********");
DebugConsole.NewMessage($"{Coroutines.Count} coroutine(s)");
foreach (var c in Coroutines)
{
DebugConsole.NewMessage($"- {c.Name}");
}
}
}
}
class WaitForSeconds : CoroutineStatus
@@ -1651,6 +1651,8 @@ namespace Barotrauma
}, 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("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); }));
commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
{
@@ -28,10 +28,7 @@ namespace Barotrauma
DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.");
}
static bool IsTargetTagAttribute(XAttribute attribute)
{
return attribute.Name.ToString().Equals("targettag", System.StringComparison.OrdinalIgnoreCase);
}
static bool IsTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() == "targettag";
}
private string GetEventName()
@@ -0,0 +1,65 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Linq;
namespace Barotrauma;
class CheckConnectionAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ConnectionName { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ConnectedItemTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier OtherConnectionName { get; set; }
public CheckConnectionAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
var connectTargets = !ConnectedItemTag.IsEmpty ? ParentEvent.GetTargets(ConnectedItemTag) : Enumerable.Empty<Entity>();
foreach (var target in ParentEvent.GetTargets(ItemTag))
{
if (target is not Item targetItem) { continue; }
if (targetItem.GetComponent<ConnectionPanel>() is not ConnectionPanel panel) { continue; }
if (panel.Connections == null || panel.Connections.None()) { continue; }
foreach (var connection in panel.Connections)
{
if (!IsCorrectConnection(connection, ConnectionName)) { continue; }
if (ConnectedItemTag.IsEmpty && OtherConnectionName.IsEmpty)
{
if (connection.Wires.Any()) { return true; }
continue;
}
foreach (var wire in connection.Wires)
{
if (wire.OtherConnection(connection) is not Connection otherConnection) { continue; }
if (ConnectedItemTag.IsEmpty)
{
if (IsCorrectConnection(otherConnection, OtherConnectionName)) { return true; }
}
else if (OtherConnectionName.IsEmpty)
{
if (IsCorrectItem()) { return true; }
}
else
{
if (!IsCorrectConnection(otherConnection, OtherConnectionName)) { continue; }
if (!IsCorrectItem()) { continue; }
return true;
}
bool IsCorrectItem() => connectTargets.Contains(otherConnection.Item);
}
bool IsCorrectConnection(Connection connection, Identifier id) => connection.Name.ToIdentifier() == id;
}
}
return false;
}
}
@@ -14,6 +14,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public string ItemTags { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RequireEquipped { get; set; }
private readonly Identifier[] itemIdentifierSplit;
private readonly Identifier[] itemTags;
@@ -30,20 +33,24 @@ namespace Barotrauma
if (!targets.Any()) { return null; }
foreach (var target in targets)
{
if (!(target is Character chr)) { continue; }
if (chr.Inventory == null) { continue; }
if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; }
foreach (var identifier in itemIdentifierSplit)
if (target is Character character)
{
if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null)
if (RequireEquipped)
{
return true;
if (itemTags.Any(tag => character.HasEquippedItem(tag))) { return true; }
if (itemIdentifierSplit.Any(identifier => character.HasEquippedItem(identifier))) { return true; }
return false;
}
if (character.Inventory is not CharacterInventory inventory) { continue; }
if (itemTags.Any(tag => inventory.FindItemByTag(tag, recursive: true) is not null)) { return true; }
if (itemIdentifierSplit.Any(identifier => inventory.FindItemByIdentifier(identifier, recursive: true) is not null)) { return true; }
}
else if (target is Item item && item.OwnInventory is ItemInventory inventory)
{
if (itemTags.Any(tag => inventory.FindItemByTag(tag, recursive: true) is not null)) { return true; }
if (itemIdentifierSplit.Any(identifier => inventory.FindItemByIdentifier(identifier, recursive: true) is not null)) { return true; }
}
}
return false;
}
@@ -15,40 +15,36 @@ namespace Barotrauma
protected override bool? DetermineSuccess()
{
ISerializableEntity target = null;
Character targetCharacter = null;
if (!TargetTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
{
if (t is ISerializableEntity e)
if (t is Character c)
{
target = e;
targetCharacter = c;
break;
}
}
}
if (target == null)
if (targetCharacter == null)
{
DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
return true;
}
if (target is Character character)
{
var currentOrderInfo = character.GetCurrentOrderWithTopPriority();
if (currentOrderInfo?.Identifier == OrderIdentifier)
{
if (OrderOption.IsEmpty)
{
return true;
}
else
{
return currentOrderInfo?.Option == OrderOption;
}
}
DebugConsole.ShowError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target character was found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
return true;
var currentOrderInfo = targetCharacter.GetCurrentOrderWithTopPriority();
if (currentOrderInfo?.Identifier == OrderIdentifier)
{
if (OrderOption.IsEmpty)
{
return true;
}
else
{
return currentOrderInfo?.Option == OrderOption;
}
}
return false;
}
private string GetEventName()
@@ -0,0 +1,89 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class CheckSelectedItemAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CharacterTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
public SelectedItemType ItemType { get; set; }
public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character character = null;
if (!CharacterTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(CharacterTag))
{
if (t is Character c)
{
character = c;
break;
}
}
}
if (character == null)
{
DebugConsole.ShowError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
return false;
}
if (!TargetTag.IsEmpty)
{
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (targets.None())
{
DebugConsole.ShowError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
foreach (var target in targets)
{
if (target is not Item targetItem)
{
continue;
}
if (IsSelected(targetItem))
{
return true;
}
}
return false;
bool IsSelected(Item item)
{
return ItemType switch
{
SelectedItemType.Any => character.IsAnySelectedItem(item),
SelectedItemType.Primary => character.SelectedItem == item,
SelectedItemType.Secondary => character.SelectedSecondaryItem == item,
_ => false
};
}
}
else
{
return ItemType switch
{
SelectedItemType.Any => !character.HasSelectedAnyItem,
SelectedItemType.Primary => character.SelectedItem == null,
SelectedItemType.Secondary => character.SelectedSecondaryItem == null,
_ => false
};
}
}
private string GetEventName()
{
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
}
}
}
@@ -0,0 +1,49 @@
#nullable enable
namespace Barotrauma
{
internal sealed class CheckTalentAction : BinaryOptionAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TalentIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
public CheckTalentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
if (TargetTag.IsEmpty)
{
return false;
}
Character? matchingCharacter = null;
foreach (Entity entity in ParentEvent.GetTargets(TargetTag))
{
if (entity is Character character)
{
matchingCharacter = character;
break;
}
}
return matchingCharacter is not null && matchingCharacter.HasTalent(TalentIdentifier);
}
public override string ToDebugString()
{
string subActionStr = "";
if (succeeded.HasValue)
{
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckTalentAction)} -> (Talent: {TalentIdentifier.ColorizeObject()}" +
$" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
}
}
}
@@ -1,10 +1,18 @@
using System;
using System.Linq;
namespace Barotrauma
{
class MessageBoxAction : EventAction
partial class MessageBoxAction : EventAction
{
public enum ActionType { Create, Close }
[Serialize(ActionType.Create, IsPropertySaveable.Yes)]
public ActionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Identifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string Tag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Header { get; set; }
@@ -24,7 +32,7 @@ namespace Barotrauma
public string CloseOnInput { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CloseOnInteractTag { get; set; }
public Identifier CloseOnSelectTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CloseOnPickUpTag { get; set; }
@@ -35,8 +43,11 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CloseOnExitRoomName { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool IsTutorialObjective { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CloseOnInRoomName { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTag { get; set; }
private bool isFinished = false;
@@ -45,72 +56,16 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (isFinished) { return; }
#if CLIENT
CreateMessageBox();
if (IsTutorialObjective && GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
tutorialMode.Tutorial?.TriggerTutorialSegment(new Tutorials.Tutorial.Segment(Text, CreateMessageBox));
}
#endif
UpdateProjSpecific();
isFinished = true;
}
#if CLIENT
public void CreateMessageBox()
{
new GUIMessageBox(
headerText: TextManager.Get(Header),
text: RichString.Rich(TextManager.ParseInputTypes(TextManager.Get(Text).Fallback(Text.ToString()), useColorHighlight: true)),
buttons: Array.Empty<LocalizedString>(),
type: GUIMessageBox.Type.Tutorial,
iconStyle: IconStyle,
autoCloseCondition: GetAutoCloseCondition(),
hideCloseButton: HideCloseButton);
}
#endif
partial void UpdateProjSpecific();
private Func<bool> GetAutoCloseCondition()
{
var character = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Character;
Func<bool> autoCloseCondition = null;
if (!string.IsNullOrEmpty(CloseOnInput) && Enum.TryParse(CloseOnInput, true, out InputType closeOnInput))
{
#if CLIENT
autoCloseCondition = () => PlayerInput.KeyDown(closeOnInput);
#endif
}
else if (!CloseOnInteractTag.IsEmpty)
{
autoCloseCondition = () => character?.SelectedItem != null && character.SelectedItem.HasTag(CloseOnInteractTag);
}
else if (!CloseOnPickUpTag.IsEmpty)
{
autoCloseCondition = () => character?.Inventory != null && character.Inventory.FindItemByTag(CloseOnPickUpTag, recursive: true) != null;
}
else if (!CloseOnEquipTag.IsEmpty)
{
autoCloseCondition = () => character != null && character.HasEquippedItem(CloseOnEquipTag);
}
else if (!CloseOnExitRoomName.IsEmpty)
{
autoCloseCondition = () => character?.CurrentHull != null && character.CurrentHull.RoomName.ToIdentifier() != CloseOnExitRoomName;
}
return autoCloseCondition;
}
public override bool IsFinished(ref string goToLabel) => isFinished;
public override bool IsFinished(ref string goToLabel)
{
return isFinished;
}
public override void Reset() => isFinished = false;
public override void Reset()
{
isFinished = false;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MessageBoxAction)}";
}
public override string ToDebugString() => $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(MessageBoxAction)}";
}
}
@@ -0,0 +1,48 @@
namespace Barotrauma;
class TeleportAction : EventAction
{
public enum TeleportPosition { MainSub, Outpost }
[Serialize(TeleportPosition.MainSub, IsPropertySaveable.Yes)]
public TeleportPosition Position { get; set; }
[Serialize(SpawnType.Human, IsPropertySaveable.Yes)]
public SpawnType SpawnType { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string SpawnPointTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
private bool isFinished;
public TeleportAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
Submarine sub = Position switch
{
TeleportPosition.MainSub => Submarine.MainSub,
TeleportPosition.Outpost => GameMain.GameSession?.Level?.StartOutpost,
_ => null
};
if (WayPoint.GetRandom(spawnType: SpawnType, sub: sub, spawnPointTag: SpawnPointTag) is WayPoint wp)
{
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (target is Character c)
{
c.TeleportTo(wp.WorldPosition);
}
}
}
isFinished = true;
}
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
}
@@ -0,0 +1,27 @@
namespace Barotrauma;
partial class TutorialHighlightAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize(true, IsPropertySaveable.Yes)]
public bool State { get; set; }
private bool isFinished;
public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
UpdateProjSpecific();
isFinished = true;
}
partial void UpdateProjSpecific();
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
}
@@ -0,0 +1,65 @@
using System.Linq;
namespace Barotrauma;
class TutorialIconAction : EventAction
{
public enum ActionType { Add, Remove, RemoveTarget, RemoveIcon, Clear };
[Serialize(ActionType.Add, IsPropertySaveable.Yes)]
public ActionType Type { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public string IconStyle { get; set; }
private bool isFinished;
public TutorialIconAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
if (ParentEvent.GetTargets(TargetTag).FirstOrDefault() is Entity target)
{
if (Type == ActionType.Add)
{
tutorialMode.Tutorial?.Icons.Add((target, IconStyle));
}
else if(Type == ActionType.Remove)
{
tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target && i.iconStyle.Equals(IconStyle, System.StringComparison.OrdinalIgnoreCase));
}
else if (Type == ActionType.RemoveTarget)
{
tutorialMode.Tutorial?.Icons.RemoveAll(i => i.entity == target);
}
else if (Type == ActionType.RemoveIcon)
{
tutorialMode.Tutorial?.Icons.RemoveAll(i => i.iconStyle.Equals(IconStyle, System.StringComparison.OrdinalIgnoreCase));
}
else if (Type == ActionType.Clear)
{
tutorialMode.Tutorial?.Icons.Clear();
}
}
}
#endif
isFinished = true;
}
public override bool IsFinished(ref string goToLabel)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
}
@@ -1,12 +1,8 @@
#if CLIENT
using Barotrauma.Tutorials;
#endif
namespace Barotrauma
{
class TutorialSegmentAction : EventAction
partial class TutorialSegmentAction : EventAction
{
public enum SegmentActionType { Trigger, Complete, Remove };
public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove };
[Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)]
public SegmentActionType Type { get; set; }
@@ -15,7 +11,7 @@ namespace Barotrauma
public Identifier Id { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ObjectiveTextTag { get; set; }
public Identifier ObjectiveTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AutoPlayVideo { get; set; }
@@ -32,64 +28,21 @@ namespace Barotrauma
[Serialize(80, IsPropertySaveable.Yes)]
public int Height { get; set; }
#if CLIENT
private readonly Tutorial.Segment segment;
#endif
private bool isFinished;
public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
#if CLIENT
// Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance)
if (Type == SegmentActionType.Trigger)
{
segment = new Tutorial.Segment(Id, ObjectiveTextTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No,
new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center),
new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height));
}
#endif
}
public TutorialSegmentAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public override void Update(float deltaTime)
{
if (isFinished) { return; }
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
if (tutorialMode.Tutorial is Tutorial tutorial)
{
switch (Type)
{
case SegmentActionType.Trigger:
tutorial.TriggerTutorialSegment(segment);
break;
case SegmentActionType.Complete:
tutorial.CompleteTutorialSegment(Id);
break;
case SegmentActionType.Remove:
tutorial.RemoveTutorialSegment(Id);
break;
}
}
}
else
{
DebugConsole.ShowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!");
}
#endif
UpdateProjSpecific();
isFinished = true;
}
public override bool IsFinished(ref string goToLabel)
{
return isFinished;
}
partial void UpdateProjSpecific();
public override void Reset()
{
isFinished = false;
}
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset() => isFinished = false;
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
}
#endif
class EventSet : Prefab
sealed class EventSet : Prefab
{
internal class EventDebugStats
{
@@ -489,12 +489,6 @@ namespace Barotrauma
}
}
public override void Dispose()
{
foreach (var childSet in ChildSets)
{
childSet.Dispose();
}
}
public override void Dispose() { }
}
}
@@ -298,5 +298,15 @@ namespace Barotrauma.Extensions
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> NotNone<T>(this IEnumerable<Option<T>> source)
=> source
.OfType<Some<T>>()
.Select(some => some.Value);
}
}
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
@@ -7,14 +8,14 @@ namespace Barotrauma
{
public static string FallbackNullOrEmpty(this string s, string fallback) => string.IsNullOrEmpty(s) ? fallback : s;
public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
public static bool IsNullOrWhiteSpace(this string? s) => string.IsNullOrWhiteSpace(s);
public static bool IsNullOrEmpty(this ContentPath? p) => p?.IsNullOrEmpty() ?? true;
public static bool IsNullOrWhiteSpace(this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? true;
public static bool IsNullOrEmpty(this LocalizedString? s) => s is null || string.IsNullOrEmpty(s.Value);
public static bool IsNullOrWhiteSpace(this LocalizedString? s) => s is null || string.IsNullOrWhiteSpace(s.Value);
public static bool IsNullOrEmpty(this RichString? s) => s is null || s.NestedStr.IsNullOrEmpty();
public static bool IsNullOrWhiteSpace(this RichString? s) => s is null || s.NestedStr.IsNullOrWhiteSpace();
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?.IsNullOrEmpty() ?? true;
public static bool IsNullOrWhiteSpace([NotNullWhen(returnValue: false)]this ContentPath? p) => p?.IsNullOrWhiteSpace() ?? 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;
@@ -5,7 +5,13 @@ namespace Barotrauma
{
class TutorialPrefab : Prefab
{
public static readonly PrefabCollection<TutorialPrefab> Prefabs = new PrefabCollection<TutorialPrefab>();
public static readonly PrefabCollection<TutorialPrefab> Prefabs =
#if CLIENT
new PrefabCollection<TutorialPrefab>(onSort: MainMenuScreen.UpdateInstanceTutorialButtons);
#else
new PrefabCollection<TutorialPrefab>();
#endif
public readonly int Order;
public readonly bool DisableBotConversations;
@@ -54,8 +60,11 @@ namespace Barotrauma
return null;
}
Identifier speciesName = tutorialCharacterElement.GetAttributeIdentifier("speciesname", CharacterPrefab.HumanSpeciesName);
string jobPrefabIdentifier = tutorialCharacterElement.GetAttributeString("jobidentifier", "assistant");
var jobPrefab = JobPrefab.Prefabs.FirstOrDefault(p => p.Identifier == jobPrefabIdentifier) ?? JobPrefab.Prefabs.First();
Identifier jobPrefabIdentifier = tutorialCharacterElement.GetAttributeIdentifier("jobidentifier", "assistant");
if (!JobPrefab.Prefabs.TryGet(jobPrefabIdentifier, out var jobPrefab))
{
jobPrefab = JobPrefab.Prefabs.First();
}
int jobVariant = tutorialCharacterElement.GetAttributeInt("variant", 0);
var characterInfo = new CharacterInfo(speciesName, jobOrJobPrefab: jobPrefab, variant: jobVariant);
foreach (var skillElement in tutorialCharacterElement.GetChildElements("skill"))
@@ -64,6 +64,8 @@ namespace Barotrauma.Items.Components
private const float MinZoom = 1.0f, MaxZoom = 4.0f;
private float zoom = 1.0f;
/// <remarks>Accessed through event actions. Do not remove even if there are no references in code.</remarks>
public bool UseDirectionalPing => useDirectionalPing;
private bool useDirectionalPing = false;
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
private bool useMineralScanner;
@@ -82,9 +82,14 @@ namespace Barotrauma.Items.Components
public sealed override void Update(float deltaTime, Camera cam)
{
int receivedInputs = 0;
bool allInputsTimedOut = true;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; }
if (timeSinceReceived[i] <= timeFrame)
{
allInputsTimedOut = false;
receivedInputs += 1;
}
timeSinceReceived[i] += deltaTime;
}
@@ -93,7 +98,7 @@ namespace Barotrauma.Items.Components
if (string.IsNullOrEmpty(signalOut))
{
//deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal)
if (!state) { IsActive = false; }
if (!state && allInputsTimedOut) { IsActive = false; }
return;
}
@@ -58,6 +58,9 @@ namespace Barotrauma.Items.Components
}
}
public bool WaterDetected => isInWater;
public int WaterPercentage => GetWaterPercentage(item.CurrentHull);
public WaterDetector(Item item, ContentXElement element)
: base(item, element)
{
@@ -2179,7 +2179,7 @@ namespace Barotrauma
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// </summary>
public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent
public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true, Func<Connection, bool> connectionFilter = null) where T : ItemComponent
{
List<T> connectedComponents = new List<T>();
@@ -2195,6 +2195,7 @@ namespace Barotrauma
foreach (Connection c in connectionPanel.Connections)
{
if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; }
var recipients = c.Recipients;
foreach (Connection recipient in recipients)
{
@@ -807,9 +807,6 @@ namespace Barotrauma
//only used if the item doesn't have a name/description defined in the currently selected language
string fallbackNameIdentifier = ConfigElement.GetAttributeString("fallbacknameidentifier", "");
//works the same as nameIdentifier, but just replaces the description
Identifier descriptionIdentifier = ConfigElement.GetAttributeIdentifier("descriptionidentifier", "");
name = TextManager.Get(nameIdentifier.IsEmpty
? $"EntityName.{Identifier}"
: $"EntityName.{nameIdentifier}",
@@ -858,18 +855,7 @@ namespace Barotrauma
SerializableProperty.DeserializeProperties(this, ConfigElement);
if (descriptionIdentifier != Identifier.Empty)
{
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}").Fallback(Description);
}
else if (nameIdentifier == Identifier.Empty)
{
Description = TextManager.Get($"EntityDescription.{Identifier}").Fallback(Description);
}
else
{
Description = TextManager.Get($"EntityDescription.{nameIdentifier}").Fallback(Description);
}
LoadDescription(ConfigElement);
var allowDroppingOnSwapWith = ConfigElement.GetAttributeIdentifierArray("allowdroppingonswapwith", Array.Empty<Identifier>());
AllowDroppingOnSwapWith = allowDroppingOnSwapWith.ToImmutableHashSet();
@@ -1320,12 +1306,8 @@ namespace Barotrauma
throw new InvalidOperationException("Can't call ItemPrefab.CreateInstance");
}
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
Item.RemoveByPrefab(this);
}
@@ -84,7 +84,6 @@ namespace Barotrauma
}
}
private bool disposed = false;
public override Sprite Sprite => null;
@@ -102,9 +101,8 @@ namespace Barotrauma
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
throw new InvalidOperationException(
$"{nameof(CoreEntityPrefab)}.{nameof(Dispose)} should never be called");
}
}
}
@@ -157,7 +157,7 @@ namespace Barotrauma
public void Delete()
{
Dispose();
Prefabs.Remove(this);
try
{
if (ContentPackage is { Files: { Length: 1 } }
@@ -238,8 +238,8 @@ namespace Barotrauma
//find the edge at the opposite side of the adjacent cell
foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges)
{
if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) < 0 &&
otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType == CellType.Solid)
if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 &&
otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid)
{
adjacentEdge = otherEdge;
break;
@@ -11,15 +11,7 @@ namespace Barotrauma
{
partial class LinkedSubmarinePrefab : MapEntityPrefab
{
//public static readonly PrefabCollection<LinkedSubmarinePrefab> Prefabs = new PrefabCollection<LinkedSubmarinePrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
//Prefabs.Remove(this);
}
public override void Dispose() { }
public readonly SubmarineInfo subInfo;
@@ -1253,7 +1253,7 @@ namespace Barotrauma
{
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
if (!characters.Any()) { return 0; }
return characters.Max(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
public void Discover(bool checkTalents = true)
@@ -280,5 +280,29 @@ namespace Barotrauma
return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(Identifier)
|| target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t));
}
protected void LoadDescription(ContentXElement element)
{
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
string originalDescription = Description.Value;
if (descriptionIdentifier != Identifier.Empty)
{
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}");
}
else if (nameIdentifier == Identifier.Empty)
{
Description = TextManager.Get($"EntityDescription.{Identifier}");
}
else
{
Description = TextManager.Get($"EntityDescription.{nameIdentifier}");
}
if (!originalDescription.IsNullOrEmpty())
{
Description = Description.Fallback(originalDescription);
}
}
}
}
@@ -142,8 +142,6 @@ namespace Barotrauma
//only used if the item doesn't have a name/description defined in the currently selected language
Identifier fallbackNameIdentifier = element.GetAttributeIdentifier("fallbacknameidentifier", "");
Identifier descriptionIdentifier = element.GetAttributeIdentifier("descriptionidentifier", "");
Name = TextManager.Get(nameIdentifier.IsEmpty
? $"EntityName.{Identifier}"
: $"EntityName.{nameIdentifier}",
@@ -271,18 +269,7 @@ namespace Barotrauma
tags.Add("wall".ToIdentifier());
}
if (!descriptionIdentifier.IsEmpty)
{
Description = TextManager.Get($"EntityDescription.{descriptionIdentifier}").Fallback(Description);
}
else if (nameIdentifier.IsEmpty)
{
Description = TextManager.Get($"EntityDescription.{Identifier}").Fallback(Description);
}
else
{
Description = TextManager.Get($"EntityDescription.{nameIdentifier}").Fallback(Description);
}
LoadDescription(element);
//backwards compatibility
if (element.GetAttribute("size") == null)
@@ -331,12 +318,6 @@ namespace Barotrauma
throw new NotImplementedException();
}
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
}
}
@@ -1548,6 +1548,7 @@ namespace Barotrauma
element.Add(new XAttribute("description", Info.Description ?? ""));
element.Add(new XAttribute("checkval", Rand.Int(int.MaxValue)));
element.Add(new XAttribute("price", Info.Price));
element.Add(new XAttribute("tier", Info.Tier));
element.Add(new XAttribute("initialsuppliesspawned", Info.InitialSuppliesSpawned));
element.Add(new XAttribute("noitems", Info.NoItems));
element.Add(new XAttribute("lowfuel", !CheckFuel()));
@@ -205,15 +205,7 @@ namespace Barotrauma.Networking
{
votes[(int)voteType] = value;
}
public void ResetVotes()
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
}
public bool SessionOrAccountIdMatches(string userId)
=> (AccountId.IsSome() && Networking.AccountId.Parse(userId) == AccountId)
|| (byte.TryParse(userId, out byte sessionId) && SessionId == sessionId);
@@ -131,28 +131,29 @@ namespace Barotrauma.Networking
enum DisconnectReason
{
//do not attempt reconnecting with these reasons
Unknown,
Disconnected,
Banned,
Kicked,
ServerShutdown,
ServerCrashed,
ServerFull,
AuthenticationRequired,
SteamAuthenticationRequired,
SteamAuthenticationFailed,
SessionTaken,
TooManyFailedLogins,
NoName,
InvalidName,
NameTaken,
InvalidVersion,
MissingContentPackage,
IncompatibleContentPackage,
SteamP2PError,
//attempt reconnecting with these reasons
Timeout,
ExcessiveDesyncOldEvent,
ExcessiveDesyncRemovedEvent,
SyncTimeout,
SteamP2PError,
SteamP2PTimeOut,
SteamP2PTimeOut
}
abstract partial class NetworkMember
@@ -163,71 +164,38 @@ namespace Barotrauma.Networking
set;
}
public virtual bool IsServer
{
get { return false; }
}
public abstract bool IsServer { get; }
public virtual bool IsClient
{
get { return false; }
}
public abstract bool IsClient { get; }
public abstract void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData = null);
#if DEBUG
public Dictionary<string, long> messageCount = new Dictionary<string, long>();
#endif
protected ServerSettings serverSettings;
public abstract Voting Voting { get; }
public Voting Voting { get; protected set; }
protected TimeSpan updateInterval;
protected DateTime updateTimer;
protected bool gameStarted;
protected RespawnManager respawnManager;
public bool ShowNetStats;
public float SimulatedRandomLatency, SimulatedMinimumLatency;
public float SimulatedLoss;
public float SimulatedDuplicatesChance;
public int TickRate
{
get { return serverSettings.TickRate; }
set
{
serverSettings.TickRate = MathHelper.Clamp(value, 1, 60);
updateInterval = new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / serverSettings.TickRate, 1, 500));
}
}
public KarmaManager KarmaManager
{
get;
private set;
} = new KarmaManager();
public bool GameStarted
{
get { return gameStarted; }
}
public bool GameStarted { get; protected set; }
public abstract IReadOnlyList<Client> ConnectedClients { get; }
public RespawnManager RespawnManager
{
get { return respawnManager; }
}
public RespawnManager RespawnManager { get; protected set; }
public ServerSettings ServerSettings { get; protected set; }
public TimeSpan UpdateInterval => new TimeSpan(0, 0, 0, 0, MathHelper.Clamp(1000 / ServerSettings.TickRate, 1, 500));
public ServerSettings ServerSettings
{
get { return serverSettings; }
}
public bool CanUseRadio(Character sender)
{
@@ -277,24 +245,6 @@ namespace Barotrauma.Networking
public abstract void UnbanPlayer(Endpoint endpoint);
public virtual void Update(float deltaTime) { }
public virtual void Quit() { }
/// <summary>
/// Check if the two version are compatible (= if they can play together in multiplayer).
/// Returns null if compatibility could not be determined (invalid/unknown version number).
/// </summary>
public static bool? IsCompatible(string myVersion, string remoteVersion)
{
if (string.IsNullOrEmpty(myVersion) || string.IsNullOrEmpty(remoteVersion)) { return null; }
if (!Version.TryParse(myVersion, out Version myVersionNumber)) { return null; }
if (!Version.TryParse(remoteVersion, out Version remoteVersionNumber)) { return null; }
return IsCompatible(myVersionNumber, remoteVersionNumber);
}
/// <summary>
/// Check if the two version are compatible (= if they can play together in multiplayer).
/// </summary>
@@ -7,7 +7,7 @@ namespace Barotrauma.Networking
public abstract string StringRepresentation { get; }
public static Option<AccountId> Parse(string str)
=> ReflectionUtils.ParseDerived<AccountId>(str);
=> ReflectionUtils.ParseDerived<AccountId, string>(str);
public abstract override bool Equals(object? obj);
@@ -7,7 +7,7 @@ namespace Barotrauma.Networking
public abstract string StringRepresentation { get; }
public static Option<Address> Parse(string str)
=> ReflectionUtils.ParseDerived<Address>(str);
=> ReflectionUtils.ParseDerived<Address, string>(str);
public abstract bool IsLocalHost { get; }
@@ -17,31 +17,27 @@ namespace Barotrauma.Networking
public LidgrenAddress(IPAddress netAddress)
{
NetAddress = netAddress;
if (IPAddress.IsLoopback(netAddress))
{
NetAddress = IPAddress.Loopback;
}
else
{
NetAddress = netAddress;
}
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
{
if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint))
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!));
}
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();
}
return Option<LidgrenAddress>.None();
}
public override bool Equals(object? obj)
@@ -22,12 +22,21 @@ namespace Barotrauma.Networking
public override string ToString() => StringRepresentation;
public static Option<Endpoint> Parse(string str)
=> ReflectionUtils.ParseDerived<Endpoint>(str);
=> ReflectionUtils.ParseDerived<Endpoint, string>(str);
public static bool operator ==(Endpoint a, Endpoint b)
=> a.Equals(b);
public static bool operator ==(Endpoint? a, Endpoint? b)
{
if (a is null)
{
return b is null;
}
else
{
return a.Equals(b);
}
}
public static bool operator !=(Endpoint a, Endpoint b)
public static bool operator !=(Endpoint? a, Endpoint? b)
=> !(a == b);
}
}
@@ -24,24 +24,23 @@ namespace Barotrauma.Networking
public new static Option<LidgrenEndpoint> Parse(string endpointStr)
{
if (IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint))
{
return Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(netEndpoint!));
}
string hostName = endpointStr;
int port = NetConfig.DefaultPort;
if (endpointStr.Count(c => c == ':') == 1)
{
string[] split = endpointStr.Split(':');
string hostName = split[0];
if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr)
&& int.TryParse(split[1], out var port))
{
return Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(adr.NetAddress, port));
}
hostName = split[0];
port = int.TryParse(split[1], out var tmpPort) ? tmpPort : port;
}
return LidgrenAddress.Parse(endpointStr)
.Select(adr => new LidgrenEndpoint(adr.NetAddress, NetConfig.DefaultPort));
if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr))
{
return Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(adr.NetAddress, port));
}
return IPEndPoint.TryParse(endpointStr, out IPEndPoint? netEndpoint)
? Option<LidgrenEndpoint>.Some(new LidgrenEndpoint(netEndpoint))
: Option<LidgrenEndpoint>.None();
}
public override bool Equals(object? obj)
@@ -1,8 +1,8 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace Barotrauma.Networking
{
@@ -68,7 +68,7 @@ namespace Barotrauma.Networking
public byte[] Buffer;
public readonly int Length => Buffer.Length;
public readonly IReadMessage GetReadMessage() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false);
public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false);
public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn);
}
@@ -86,9 +86,164 @@ namespace Barotrauma.Networking
}
[NetworkSerialize]
internal struct PeerDisconnectPacket : INetSerializableStruct
internal readonly struct PeerDisconnectPacket : INetSerializableStruct
{
public string Message;
public readonly DisconnectReason DisconnectReason;
public readonly string AdditionalInformation;
private PeerDisconnectPacket(
DisconnectReason disconnectReason,
string additionalInformation = "")
{
DisconnectReason = disconnectReason;
AdditionalInformation = additionalInformation;
}
public LocalizedString ChatMessage(Client c)
=> DisconnectReason switch
{
DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer",
"[client]", c.Name),
_ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason",
("[client]", c.Name),
("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}")))
};
private LocalizedString msgWithReason
=> TextManager.Get($"DisconnectReason.{DisconnectReason}")
+ "\n\n"
+ TextManager.Get("banreason") + " " + AdditionalInformation;
private LocalizedString serverMessage
=> TextManager.Get($"ServerMessage.{DisconnectReason}");
public LocalizedString PopupMessage
=> DisconnectReason switch
{
DisconnectReason.Banned => msgWithReason,
DisconnectReason.Kicked => msgWithReason,
DisconnectReason.InvalidVersion => TextManager.GetWithVariables("DisconnectMessage.InvalidVersion",
("[version]", AdditionalInformation),
("[clientversion]", GameMain.Version.ToString())),
DisconnectReason.ExcessiveDesyncOldEvent => serverMessage,
DisconnectReason.ExcessiveDesyncRemovedEvent => serverMessage,
DisconnectReason.SyncTimeout => serverMessage,
_ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost"))
};
public LocalizedString ReconnectMessage
=> PopupMessage + "\n\n" + TextManager.Get("ConnectionLostReconnecting");
public PlayerConnectionChangeType ConnectionChangeType
=> DisconnectReason switch
{
DisconnectReason.Banned => PlayerConnectionChangeType.Banned,
DisconnectReason.Kicked => PlayerConnectionChangeType.Kicked,
_ => PlayerConnectionChangeType.Disconnected
};
public bool ShouldAttemptReconnect
=> DisconnectReason
is DisconnectReason.ExcessiveDesyncOldEvent
or DisconnectReason.ExcessiveDesyncRemovedEvent
or DisconnectReason.Timeout
or DisconnectReason.SyncTimeout
or DisconnectReason.SteamP2PTimeOut;
public bool IsEventSyncError
=> DisconnectReason
is DisconnectReason.ExcessiveDesyncOldEvent
or DisconnectReason.ExcessiveDesyncRemovedEvent
or DisconnectReason.SyncTimeout;
public bool ShouldCreateAnalyticsEvent
=> DisconnectReason is not (
DisconnectReason.Disconnected
or DisconnectReason.Banned
or DisconnectReason.Kicked
or DisconnectReason.TooManyFailedLogins
or DisconnectReason.InvalidVersion);
public bool ShouldShowMessage
=> DisconnectReason is not DisconnectReason.Disconnected;
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.
/// </summary>
public string ToLidgrenStringRepresentation()
{
static string strToBase64(string str)
=> Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
return DisconnectReason
+ lidgrenSeparator
+ strToBase64(AdditionalInformation);
}
public static Option<PeerDisconnectPacket> FromLidgrenStringRepresentation(string str)
{
// Lidgren has some hardcoded disconnect strings that it uses
// when it detects that a connection has failed. We can handle
// timeouts, so let's look for strings related to that and return
// an appropriate PeerDisconnectPacket.
switch (str)
{
case Lidgren.Network.NetConnection.NoResponseMessage:
case "Connection timed out":
case "Reconnecting":
return Option<PeerDisconnectPacket>.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])));
}
public static PeerDisconnectPacket Custom(string customMessage)
=> new PeerDisconnectPacket(
DisconnectReason.Unknown,
customMessage);
public static PeerDisconnectPacket WithReason(DisconnectReason disconnectReason)
=> new PeerDisconnectPacket(disconnectReason);
public static PeerDisconnectPacket Kicked(string? msg)
=> new PeerDisconnectPacket(DisconnectReason.Kicked, msg ?? "");
public static PeerDisconnectPacket Banned(string? msg)
=> new PeerDisconnectPacket(DisconnectReason.Banned, msg ?? "");
public static PeerDisconnectPacket InvalidVersion()
=> new PeerDisconnectPacket(
DisconnectReason.InvalidVersion,
GameMain.Version.ToString());
public static PeerDisconnectPacket SteamP2PError(Steamworks.P2PSessionError error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamP2PError,
error.ToString());
public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
$"{nameof(Steamworks.BeginAuthResult)}.{error}");
public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
$"{nameof(Steamworks.AuthResponse)}.{error}");
}
// ReSharper disable MemberCanBePrivate.Global, FieldCanBeMadeReadOnly.Global, UnassignedField.Global
@@ -101,11 +256,14 @@ namespace Barotrauma.Networking
public byte[] HashBytes = Array.Empty<byte>();
[NetworkSerialize]
public ulong WorkshopId;
public string UgcId = "";
[NetworkSerialize]
public uint InstallTimeDiffInSeconds;
[NetworkSerialize]
public bool IsMandatory;
private Md5Hash? cachedHash;
private DateTime? cachedDateTime;
@@ -130,9 +288,12 @@ namespace Barotrauma.Networking
{
Name = contentPackage.Name;
Hash = contentPackage.Hash;
WorkshopId = contentPackage.SteamWorkshopId;
UgcId = contentPackage.UgcId.TryUnwrap(out var ugcId)
? ugcId.StringRepresentation
: "";
IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
InstallTimeDiffInSeconds =
contentPackage.InstallTime is { } installTime
contentPackage.InstallTime.TryUnwrap(out var installTime)
? (uint)(installTime - referenceTime).TotalSeconds
: 0;
}
@@ -333,10 +333,14 @@ namespace Barotrauma.Networking
RespawnCharactersProjSpecific(shuttlePos);
}
public static AfflictionPrefab GetRespawnPenaltyAfflictionPrefab()
{
return AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty");
}
public static Affliction GetRespawnPenaltyAffliction()
{
var respawnPenaltyAffliction = AfflictionPrefab.Prefabs.First(a => a.AfflictionType == "respawnpenalty");
return respawnPenaltyAffliction?.Instantiate(10.0f);
return GetRespawnPenaltyAfflictionPrefab()?.Instantiate(10.0f);
}
public static void GiveRespawnPenaltyAffliction(Character character)
@@ -388,11 +388,12 @@ namespace Barotrauma.Networking
public List<SavedClientPermission> ClientPermissions { get; private set; } = new List<SavedClientPermission>();
private int tickRate = 20;
[Serialize(20, IsPropertySaveable.Yes)]
public int TickRate
{
get;
set;
get { return tickRate; }
set { tickRate = MathHelper.Clamp(value, 1, 60); }
}
[Serialize(true, IsPropertySaveable.Yes)]
@@ -56,23 +56,5 @@ namespace Barotrauma
return selected;
}
public void ResetVotes(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
client.ResetVotes();
}
#if CLIENT
foreach (VoteType voteType in Enum.GetValues(typeof(VoteType)))
{
SetVoteCountYes(voteType, 0);
SetVoteCountNo(voteType, 0);
SetVoteCountMax(voteType, 0);
}
UpdateVoteTexts(connectedClients, VoteType.Mode);
UpdateVoteTexts(connectedClients, VoteType.Sub);
#endif
}
}
}
@@ -7,7 +7,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
public abstract class Prefab : IDisposable
public abstract class Prefab
{
public readonly static ImmutableHashSet<Type> Types;
static Prefab()
@@ -37,6 +37,14 @@ namespace Barotrauma
OnRemoveOverrideFile = onRemoveOverrideFile;
}
/// <summary>
/// Constructor with only the OnSort callback provided.
/// </summary>
public PrefabCollection(Action? onSort) : this()
{
OnSort = onSort;
}
/// <summary>
/// Method to be called when calling Add(T prefab, bool override).
/// If provided, the method is called only if Add succeeds.
@@ -4,16 +4,20 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Barotrauma.Threading;
namespace Barotrauma
{
public class PrefabSelector<T> : IEnumerable<T> where T : notnull, Prefab
{
private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
public T? BasePrefab
{
get
{
lock (overrides) { return basePrefabInternal; }
using (new ReadLock(rwl)) { return basePrefabInternal; }
}
}
@@ -21,51 +25,70 @@ namespace Barotrauma
{
get
{
lock (overrides) { return activePrefabInternal; }
using (new ReadLock(rwl)) { return activePrefabInternal; }
}
}
public void Add(T prefab, bool isOverride)
{
lock (overrides) { AddInternal(prefab, isOverride); }
using (new WriteLock(rwl)) { AddInternal(prefab, isOverride); }
}
public void RemoveIfContains(T prefab)
{
lock (overrides) { RemoveIfContainsInternal(prefab); }
using (new WriteLock(rwl)) { RemoveIfContainsInternal(prefab); }
}
public void Remove(T prefab)
{
lock (overrides) { RemoveInternal(prefab); }
using (new WriteLock(rwl)) { RemoveInternal(prefab); }
}
public void RemoveByFile(ContentFile file, Action<T>? callback = null)
{
lock (overrides) { RemoveByFileInternal(file, callback); }
var removed = new List<T>();
using (new WriteLock(rwl))
{
for (int i = overrides.Count-1; i >= 0; i--)
{
var prefab = overrides[i];
if (prefab.ContentFile == file)
{
RemoveInternal(prefab);
removed.Add(prefab);
}
}
if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file)
{
RemoveInternal(basePrefabInternal);
removed.Add(p);
}
}
if (callback != null) { removed.ForEach(callback); }
}
public void Sort()
{
lock (overrides) { SortInternal(); }
using (new WriteLock(rwl)) { SortInternal(); }
}
public bool IsEmpty
{
get
{
lock (overrides) { return isEmptyInternal; }
using (new ReadLock(rwl)) { return isEmptyInternal; }
}
}
public bool Contains(T prefab)
{
lock (overrides) { return ContainsInternal(prefab); }
using (new ReadLock(rwl)) { return ContainsInternal(prefab); }
}
public bool IsOverride(T prefab)
{
lock (overrides) { return IsOverrideInternal(prefab); }
using (new ReadLock(rwl)) { return IsOverrideInternal(prefab); }
}
@@ -73,7 +96,7 @@ namespace Barotrauma
private T? basePrefabInternal;
private readonly List<T> overrides = new List<T>();
private T? activePrefabInternal => overrides.Any() ? overrides.First() : basePrefabInternal;
private T? activePrefabInternal => overrides.Count > 0 ? overrides.First() : basePrefabInternal;
private void AddInternal(T prefab, bool isOverride)
{
@@ -84,7 +107,7 @@ namespace Barotrauma
}
else
{
if (BasePrefab != null)
if (basePrefabInternal != null)
{
string prefabName
= prefab is MapEntityPrefab mapEntityPrefab
@@ -92,7 +115,7 @@ namespace Barotrauma
: $"\"{prefab.Identifier}\"";
throw new InvalidOperationException(
$"Failed to add the prefab {prefabName} ({prefab.GetType()}) from \"{prefab.ContentPackage?.Name ?? "[NULL]"}\" ({prefab.ContentPackage?.Dir ?? ""}): "
+ $"a prefab with the same identifier from \"{ActivePrefab!.ContentPackage?.Name ?? "[NULL]"}\" ({ActivePrefab!.ContentPackage?.Dir ?? ""}) already exists; try overriding");
+ $"a prefab with the same identifier from \"{activePrefabInternal!.ContentPackage?.Name ?? "[NULL]"}\" ({activePrefabInternal!.ContentPackage?.Dir ?? ""}) already exists; try overriding");
}
basePrefabInternal = prefab;
}
@@ -114,31 +137,12 @@ namespace Barotrauma
SortInternal();
}
private void RemoveByFileInternal(ContentFile file, Action<T>? callback)
{
for (int i = overrides.Count-1; i >= 0; i--)
{
var prefab = overrides[i];
if (prefab.ContentFile == file)
{
RemoveInternal(prefab);
callback?.Invoke(prefab);
}
}
if (basePrefabInternal is { ContentFile: var baseFile } p && baseFile == file)
{
RemoveInternal(basePrefabInternal);
callback?.Invoke(p);
}
}
private void SortInternal()
{
overrides.Sort((p1, p2) => (p1.ContentPackage?.Index ?? int.MaxValue) - (p2.ContentPackage?.Index ?? int.MaxValue));
}
private bool isEmptyInternal => basePrefabInternal is null && !overrides.Any();
private bool isEmptyInternal => basePrefabInternal is null && overrides.Count == 0;
private bool ContainsInternal(T prefab) => basePrefabInternal == prefab || overrides.Contains(prefab);
@@ -153,7 +157,7 @@ namespace Barotrauma
{
T? basePrefab;
ImmutableArray<T> overrideClone;
lock (overrides)
using (new ReadLock(rwl))
{
basePrefab = basePrefabInternal;
overrideClone = overrides.ToImmutableArray();
@@ -315,6 +315,25 @@ namespace Barotrauma
return floatValue;
}
public static bool TryGetAttributeInt(this XElement element, string name, out int result)
{
var attribute = element?.GetAttribute(name);
result = default;
if (attribute == null) { return false; }
if (int.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var intVal))
{
result = intVal;
return true;
}
if (float.TryParse(attribute.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var floatVal))
{
result = (int)floatVal;
return true;
}
return false;
}
public static int GetAttributeInt(this XElement element, string name, int defaultValue)
{
var attribute = element?.GetAttribute(name);
@@ -353,7 +353,7 @@ namespace Barotrauma
// Check for duplicate binds when introducing new binds
foreach (var defaultBinding in defaultBindings)
{
if (!savedBindings.ContainsKey(defaultBinding.Key))
if (!IsSetToNone(defaultBinding.Value) && !savedBindings.ContainsKey(defaultBinding.Key))
{
foreach (var savedBinding in savedBindings)
{
@@ -373,6 +373,8 @@ namespace Barotrauma
}
}
}
static bool IsSetToNone(KeyOrMouse keyOrMouse) => keyOrMouse == Keys.None && keyOrMouse == MouseButton.None;
}
// Clear the old chat binds for configs saved before the introduction of the new chat binds
@@ -119,8 +119,6 @@ namespace Barotrauma
if (!HasRequiredConditions(currentTargets)) { return; }
if (Entity.Spawner != null && Entity.Spawner.IsInRemoveQueue(entity)) { return; }
switch (delayType)
{
case DelayTypes.Timer:
@@ -3,6 +3,7 @@ using Barotrauma.IO;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
@@ -20,6 +21,16 @@ namespace Barotrauma.Steam
public const string PreviewImageName = "PreviewImage.png";
public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png";
public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId)
{
workshopId = null;
if (!contentPackage.UgcId.TryUnwrap(out var ugcId)) { return false; }
if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return false; }
workshopId = steamWorkshopId;
return true;
}
public static partial class Workshop
{
private struct ItemEqualityComparer : IEqualityComparer<Steamworks.Ugc.Item>
@@ -110,7 +121,10 @@ namespace Barotrauma.Steam
{
NukeDownload(workshopItem);
var toUninstall
= ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id)
= ContentPackageManager.WorkshopPackages.Where(p =>
p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId { Value: var itemId }
&& itemId == workshopItem.Id)
.ToHashSet();
ContentPackageManager.EnabledPackages.DisableMods(toUninstall);
toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d));
@@ -9,6 +9,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Globalization;
using System.Text.Unicode;
namespace Barotrauma
{
@@ -30,18 +31,22 @@ namespace Barotrauma
public static int LanguageVersion { get; private set; } = 0;
private readonly static Regex isCJK = new Regex(
@"\p{IsHangulJamo}|" +
@"\p{IsHiragana}|" +
@"\p{IsKatakana}|" +
@"\p{IsCJKRadicalsSupplement}|" +
@"\p{IsCJKSymbolsandPunctuation}|" +
@"\p{IsEnclosedCJKLettersandMonths}|" +
@"\p{IsCJKCompatibility}|" +
@"\p{IsCJKUnifiedIdeographsExtensionA}|" +
@"\p{IsCJKUnifiedIdeographs}|" +
@"\p{IsHangulSyllables}|" +
@"\p{IsCJKCompatibilityForms}");
private static readonly ImmutableArray<Range<int>> CjkRanges = new[]
{
UnicodeRanges.HangulJamo,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana,
UnicodeRanges.CjkRadicalsSupplement,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.EnclosedCjkLettersandMonths,
UnicodeRanges.CjkCompatibility,
UnicodeRanges.CjkUnifiedIdeographsExtensionA,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.HangulSyllables,
UnicodeRanges.CjkCompatibilityForms
}.Select(r => new Range<int>(r.FirstCodePoint, r.FirstCodePoint+r.Length-1))
.OrderBy(r => r.Start)
.ToImmutableArray();
/// <summary>
/// Does the string contain symbols from Chinese, Japanese or Korean languages
@@ -54,7 +59,24 @@ namespace Barotrauma
public static bool IsCJK(string text)
{
if (string.IsNullOrEmpty(text)) { return false; }
return isCJK.IsMatch(text);
for (int i = 0; i < text.Length; i++)
{
char chr = text[i];
for (int j = 0; j < CjkRanges.Length; j++)
{
var range = CjkRanges[j];
// If chr < range.Start, we know that it can't
// be in any of the following ranges, so let's
// not even bother checking them
if (chr < range.Start) { break; }
// This character is in a range, return true
if (range.Contains(chr)) { return true; }
}
}
return false;
}
/// <summary>
@@ -50,10 +50,7 @@ namespace Barotrauma
if (level > maxLevel) { maxLevel = level; }
int price = BasePrice;
for (int i = 1; i <= level; i++)
{
price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, i / (float)maxLevel) / 100);
}
price += (int)(price * MathHelper.Lerp(IncreaseLow, IncreaseHigh, level / (float)maxLevel) / 100);
return location?.GetAdjustedMechanicalCost(price) ?? price;
}
}
@@ -331,8 +328,6 @@ namespace Barotrauma
public ContentXElement SourceElement { get; }
private bool disposed;
public bool SuppressWarnings { get; }
public bool HideInMenus { get; }
@@ -525,18 +520,12 @@ namespace Barotrauma
public override void Dispose()
{
if (!disposed)
{
Prefabs.Remove(this);
#if CLIENT
Sprite?.Remove();
Sprite = null;
DecorativeSprites.ForEach(sprite => sprite.Remove());
targetProperties.Clear();
Sprite?.Remove();
Sprite = null;
DecorativeSprites.ForEach(sprite => sprite.Remove());
targetProperties.Clear();
#endif
}
disposed = true;
}
}
}
@@ -16,18 +16,18 @@ namespace Barotrauma
public bool IsNone() => this is None<T>;
public bool IsSome() => this is Some<T>;
public bool TryUnwrap(out T outValue)
public bool TryUnwrap(out T outValue) => TryUnwrap<T>(out outValue);
public bool TryUnwrap<T1>(out T1 outValue) where T1 : T
{
switch (this)
{
case Some<T> { Value: var value }:
case Some<T> { Value: T1 value }:
outValue = value;
return true;
case None<T> _:
default:
outValue = default!;
return false;
default:
throw new ArgumentOutOfRangeException();
}
}
@@ -23,16 +23,16 @@ namespace Barotrauma
return cachedNonAbstractTypes[assembly].Where(t => t.IsSubclassOf(typeof(T)));
}
public static Option<T1> ParseDerived<T1>(string str)
public static Option<TBase> ParseDerived<TBase, TInput>(TInput input) where TInput : notnull
{
static Option<T1> none() => Option<T1>.None();
static Option<TBase> none() => Option<TBase>.None();
var derivedTypes = GetDerivedNonAbstract<T1>();
var derivedTypes = GetDerivedNonAbstract<TBase>();
Option<T1> parseOfType(Type t)
Option<TBase> parseOfType(Type t)
{
//every T1 type is expected to have a method with the following signature:
// public static Option<T> Parse(string str)
//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(); }
@@ -44,14 +44,17 @@ namespace Barotrauma
if (returnType.GetGenericTypeDefinition() != typeof(Option<>)) { return none(); }
if (returnType.GenericTypeArguments[0] != t) { return none(); }
//some hacky business to convert from Option<T2> to Option<T1> when we only know T2 at runtime
static Option<T1> convert<T2>(Option<T2> option) where T2 : T1
=> option.Select(v => (T1)v);
Func<Option<T1>, Option<T1>> f = convert;
var constructedConverter = f.Method.GetGenericMethodDefinition().MakeGenericMethod(typeof(T1), t);
//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 object?[] { parseFunc.Invoke(null, new object[] { str }) })
as Option<T1> ?? none();
return constructedConverter.Invoke(null, new[] { parseFunc.Invoke(null, new object[] { input }) })
as Option<TBase> ?? none();
}
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none();
@@ -0,0 +1,34 @@
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();
}
}
}