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();
}
}
}
+84 -1
View File
@@ -1,3 +1,87 @@
---------------------------------------------------------------------------------------------------------
v0.19.5.0
---------------------------------------------------------------------------------------------------------
Unstable only:
- Progress on the new tutorials, the Roles tutorials in particular. Still a work in progress, but feedback is again more than welcome!
- Cleaned up networking and server list code to make them less error-prone and easier to work with in the future. Should not cause any functional differences, but please let us know if you notice any issues or oddities!
- Fixes and improvements to Camel.
- Fixed skill texts not being colored according to the user's skills in the repair UI.
- Fixed outpost service NPCs' titles (which are also used as the tooltips on the icons) not showing up in the multiplayer campaign.
- Further fixes to dragging animations.
- Fixed warning about duplicate keybinds being displayed for things set to None.
- Fixed clients who've gotten vote kicked getting vote kicked again when they join.
- Fixed favorited localhost servers causing a crash.
- Fixed respawn shuttle setting toggling on and off when adjusting other settings.
- Fixed skills increasing on death if the skill level is under the maximum initial skill.
- Fixes duplicate Captain Hognoses occasionally appearing in the crew list when selecting the Hognose mission in the multiplayer campaign.
- Fixed wrecked coilgun and railgun launch impulses.
- Fixed delayed status effects not working after the parent entity has been removed (breaking many medical items, e.g. ethanol).
- Fixed inability to buy anything from other stores when you've bought something from one of the stores in an outpost in the multiplayer campaign.
Changes:
- Made crates deconstruct much faster to make them easier to get rid of.
- Sonar disruptions hide minerals.
- Gray out RangedWeapon's crosshair when reloading (similar to turret crosshairs).
Fixes:
- Fixed clients downloading submarines they already have from the server if the mods those submarines are in are not currently enabled.
- Fixed submarines always saving in the root folder of a local mod, instead of the subfolder they were originally in.
- Fixed Reaper's Tax not stacking.
- Fixed turrets linked to the same loader messing up the upgrade store UI and causing item swaps to cost more than they should.
- Fixed status monitor calculating linked hulls' water amounts incorrectly (displaying the average of their water percentages, which isn't correct if the hulls aren't the same size).
- Fixes to messed up ruin decals in a bunch of ruin modules.
- Fixed a waypoint issue in the Alien_Entrance3 ruin module.
- Removed oxygen tanks from DockingModule_01_Colony.
- Tweaked the hulls and waypoints around Herja's top docking hatch to make it easier for bots to reach and weld.
- Fixed a waypoint/hull issue in Typhon's stowage compartment (waypoint in such a tight space the bots couldn't reach it).
- Fixed inactive components (components not currently sending any signal) not reactivating if their output is set to a non-empty value.
- Fixed duct block's misaligned broken sprite.
Submarine editor:
- Fixed door gaps not appearing in the sub editor until you select the door
Modding:
- Added CheckTalentAction, which can be used in events to check whether a target has unlocked a specific talent.
- Fixed ExtraLoad working the wrong way around on PowerTransfer components that generate/consume power (the extra load would supply power to the grid). Does not affect the vanilla game, because neither junction boxes or relays generate or consume power.
---------------------------------------------------------------------------------------------------------
v0.19.4.0
---------------------------------------------------------------------------------------------------------
Unstable only:
- Fixed networking issues that prevented joining SteamP2P servers.
- Fixed crashing on startup if GameAnalytics is enabled.
- Fixed sub editor background images not saving if you leave the editor and quit the game without exiting the image editing mode first.
- Camel hull fixes.
- Fixed a draw order issue in unit load device.
- Fixed unit load device's inventory slot layout.
- Fixed submarine tier set in the sub editor not being saved, causing all subs to use the default tier determined by the price.
Changes:
- Made extra sales from "traveling tradesman" talent stack.
Fixes:
- Made bots better at figuring out which button controls a door when there's some complex circuit involved. Previously the bots would try to find a button connected to any of the door's connections via wires/circuits, now only the toggle and set_state inputs are considered.
- Bots now heavily prefer using buttons linked to the door in the sub editor. Can be used as another way to help the bots figure out which button they should press in situations with multiple buttons and complex door control logic.
- Fixed bots failing to find a path to a couple of spots in Herja.
- Fixed alien materials (physicorium, incendium, fulgurium, dementonite, paralyxis) not being shown on the mineral scanner.
- Another fix to cave generation to prevent it from creating impassable paths.
- Fixed inability to use manual assignment for bot orders with options.
- Fixed all boolean components (And, Or, Xor) using the And Component's tooltip for the "timeframe" property.
- Fixed boolean operator component (And, Or, Xor) timeframes not working correctly in some situations (non-zero timeframe, empty false output). The component would deactivate as soon as it stops sending an output, which could prevent some inputs from timing out (meaning that the component could send a signal again as soon as it receives signal A, even if signal B hasn't been received within the timeframe).
- Fixed PUCS consuming the medical item inside it when a welding fuel or incendium tank is inserted.
Multiplayer:
- Fixed inventory and wallet resetting if a campaign round ends when a client's character has spawned, but the client is not currently controlling it (e.g. due to getting kicked to the lobby).
- Fixed spectator checkbox overlapping with the character info if you get kicked to the lobby mid-round.
Modding:
- Fixed items/structures now falling back to the description defined in the xml even if it's empty, if the description is not defined for the selected language (instead of using English instead).
- Fixed using the "reloadwearables" and "loadwearable" console commands outside the character editor crashing the game.
- Fixed character editor crash if you first reload textures and then recreate the ragdoll.
- Changed how submarine upgrades are calculated, now no longer adds previous levels' costs to the price, but rather relies on higher increasehigh values
---------------------------------------------------------------------------------------------------------
v0.19.3.0
---------------------------------------------------------------------------------------------------------
@@ -44,7 +128,6 @@ Fixes:
- Fixed chaingun rotation speed not being affected by the weapons skill.
- Fixed crashing when using ':' in item assembly names on Linux platforms.
- Fixed ImmuneToPressure ability flag being ignored on characters who don't need air (in practice meaning that you can get killed by pressure if you get huskified even if you have a talent that makes you immune to pressure).
- Disallow using the "reloadwearables" and "loadwearable" when a gamesession is active because it leads to a crash. These commands are intended to be used in the character editor.
- Fixed geneticmaterialcrawler_unresearched3 producing mudraptor genes.
Submarine editor: