Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -0,0 +1,58 @@
using System.Collections.Immutable;
using Barotrauma.PerkBehaviors;
namespace Barotrauma
{
internal sealed class DisembarkPerkPrefab : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<DisembarkPerkPrefab> Prefabs = new PrefabCollection<DisembarkPerkPrefab>();
public LocalizedString Name { get; }
public LocalizedString Description { get; }
public Identifier SortCategory { get; }
/// <summary>
/// After the perks have been sorted by category and cost, they are sorted using this key.
/// Use if you want the perks to be arranged in specific order when their cost are the same.
/// </summary>
public int SortKey { get; }
/// <summary>
/// When set to an identifier of another perk, this perk cannot be selected unless the prerequisite perk is selected.
/// </summary>
public Identifier Prerequisite { get; }
/// <summary>
/// When this perk is selected, the perks in this set cannot be selected at the same time.
/// </summary>
public ImmutableHashSet<Identifier> MutuallyExclusivePerks { get; }
public int Cost { get; }
public ImmutableArray<PerkBase> PerkBehaviors { get; }
public DisembarkPerkPrefab(ContentXElement element, DisembarkPerkFile prefabFile) : base(prefabFile, element.GetAttributeIdentifier("identifier", ""))
{
Name = TextManager.Get($"disembarkperk.{Identifier}").Fallback(Identifier.ToString());
Description = TextManager.Get($"disembarkperkdescription.{Identifier}").Fallback("");
Cost = element.GetAttributeInt("cost", 0);
SortCategory = element.GetAttributeIdentifier("sortcategory", Identifier);
Prerequisite = element.GetAttributeIdentifier("prerequisite", Identifier.Empty);
MutuallyExclusivePerks = element.GetAttributeIdentifierImmutableHashSet("mutuallyexclusiveperks", ImmutableHashSet<Identifier>.Empty);
SortKey = element.GetAttributeInt("sortkey", 0);
var builder = ImmutableArray.CreateBuilder<PerkBase>();
foreach (var child in element.Elements())
{
if (PerkBase.TryLoadFromXml(child, this, out var perk))
{
builder.Add(perk);
}
}
PerkBehaviors = builder.ToImmutable();
}
public override void Dispose() { }
}
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace Barotrauma.PerkBehaviors
{
internal class GiveTalentPointPerk : PerkBase
{
[Serialize(0, IsPropertySaveable.Yes)]
public int Amount { get; set; }
public GiveTalentPointPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
{
foreach (Character character in teamCharacters)
{
character.Info.AdditionalTalentPoints += Amount;
}
}
}
}
@@ -0,0 +1,84 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma.PerkBehaviors
{
internal enum PerkSimulation
{
/// <summary>
/// Perk is only run on the server,
/// other parts of the game handle the client-side effects.
/// Like serializable properties and affliction syncing.
/// </summary>
ServerOnly,
/// <summary>
/// Both the server and clients run the perk.
/// </summary>
ServerAndClients
}
internal abstract class PerkBase : ISerializableEntity
{
public string Name { get; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
public virtual PerkSimulation Simulation => PerkSimulation.ServerOnly;
public readonly DisembarkPerkPrefab Prefab;
protected PerkBase(ContentXElement element, DisembarkPerkPrefab prefab)
{
Name = element.Name.ToString();
Prefab = prefab;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool CanApply(SubmarineInfo submarine)
{
return true;
}
/// <summary>
/// You might notice that this function is not virtual.
/// It was at first, but there was a misunderstanding in design,
/// so I turned it into a kill switch for all perks for now.
/// If we ever want to add perks that do work when a submarine is present,
/// this function can be made virtual again and set to true in the appropriate perks.
/// </summary>
public bool CanApplyWithoutSubmarine()
=> false;
public abstract void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine? teamSubmarine);
public static bool TryLoadFromXml(ContentXElement element, DisembarkPerkPrefab prefab, [NotNullWhen(true)] out PerkBase? perk)
{
Type? type = ReflectionUtils.GetTypeWithBackwardsCompatibility(ToolBox.BarotraumaAssembly, "Barotrauma.PerkBehaviors", element.Name.ToString(), throwOnError: false, ignoreCase: true);
if (type is null)
{
DebugConsole.ThrowError($"Could not find a perk behavior of the type \"{element.Name}\".", contentPackage: element.ContentPackage);
perk = null;
return false;
}
try
{
object? instance = Activator.CreateInstance(type, element, prefab);
if (instance is PerkBase perkInstance)
{
perk = perkInstance;
return true;
}
throw new InvalidCastException($"Could not cast the instance of type \"{type}\" to a {nameof(PerkBase)}.");
}
catch (Exception e)
{
DebugConsole.ThrowError(e.InnerException != null ? e.InnerException.ToString() : e.ToString(), contentPackage: element.ContentPackage);
perk = null;
return false;
}
}
}
}
@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma.PerkBehaviors
{
internal class SpawnItemPerk : PerkBase
{
public SpawnItemPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
public override PerkSimulation Simulation
=> PerkSimulation.ServerOnly;
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Identifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Tag { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int MinAmount { get; set; }
[Serialize(0f, IsPropertySaveable.Yes)]
public float PerPlayer { get; set; }
/// <summary>
/// When set to non-empty value, the perk will prioritize spawning items in containers
/// with this tag or identifier over the item's primary and secondary preferred containers.
/// </summary>
[Serialize("", IsPropertySaveable.Yes)]
public Identifier PriorityContainerTag { get; set; }
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
{
if (teamSubmarine is null) { return; }
if (Entity.Spawner is null)
{
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because EntitySpawner is null.");
return;
}
int amount = Math.Max(MinAmount, (int)MathF.Ceiling(PerPlayer * teamCharacters.Count));
if (Identifier.IsEmpty)
{
if (Tag.IsEmpty)
{
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: neither identifier or tag is set.",
contentPackage: Prefab.ContentPackage);
return;
}
var matchingItems = ItemPrefab.Prefabs.Where(ip => ip.Tags.Contains(Tag));
if (matchingItems.None())
{
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items: no items found with the tag \"{Tag}\".",
contentPackage: Prefab.ContentPackage);
return;
}
for (int i = 0; i < amount; i++)
{
SpawnItem(matchingItems.GetRandomUnsynced(), amount: 1);
}
}
else
{
ItemPrefab prefab = ItemPrefab.Find(null, Identifier);
if (prefab is null)
{
DebugConsole.ThrowError($"{nameof(SpawnItemPerk)} ({Prefab.Identifier}) failed to spawn items because the ItemPrefab \"{Identifier}\" was not found.",
contentPackage: Prefab.ContentPackage);
return;
}
SpawnItem(prefab, amount);
}
void SpawnItem(ItemPrefab prefab, int amount)
{
SuitableContainers suitableContainers = FindSuitableContainers(prefab, teamSubmarine);
if (!suitableContainers.Any())
{
SpawnItemInCrate(prefab, teamSubmarine, amount);
return;
}
SpawnInContainer(prefab, amount, suitableContainers, teamSubmarine);
}
}
private readonly record struct SuitableContainers(
ICollection<ItemContainer> PriorityContainers,
ICollection<ItemContainer> PreferredContainers,
ICollection<ItemContainer> SecondaryContainers)
{
public bool Any()
=> PriorityContainers.Count > 0
|| PreferredContainers.Count > 0
|| SecondaryContainers.Count > 0;
}
private SuitableContainers FindSuitableContainers(ItemPrefab prefab, Submarine submarine)
{
HashSet<ItemContainer> priorityContainers = new();
HashSet<ItemContainer> primaryContainers = new();
HashSet<ItemContainer> secondaryContainers = new();
foreach (Item item in submarine.GetItems(alsoFromConnectedSubs: true))
{
if (item.GetComponent<Fabricator>() != null || item.GetComponent<Deconstructor>() != null) { continue; }
if (item.NonInteractable || item.NonPlayerTeamInteractable || item.IsHidden) { continue; }
if (item.GetComponent<ItemContainer>() is { } container)
{
if (!container.CanBeContained(prefab)) { continue; }
var tags = item.GetTags();
if (!PriorityContainerTag.IsEmpty && (tags.Contains(PriorityContainerTag) || item.Prefab.Identifier == PriorityContainerTag))
{
priorityContainers.Add(container);
continue;
}
if (prefab.PreferredContainers.Any(pc => pc.Primary.Any(tags.Contains)))
{
primaryContainers.Add(container);
continue;
}
if (prefab.PreferredContainers.Any(pc => pc.Secondary.Any(tags.Contains)))
{
secondaryContainers.Add(container);
}
}
}
return new SuitableContainers(priorityContainers, primaryContainers, secondaryContainers);
}
private static void SpawnItemInCrate(ItemPrefab prefab, Submarine submarine, int amount)
{
var purchasedItem = new PurchasedItem(prefab, amount, buyer: null);
CargoManager.DeliverItemsToSub(new []{ purchasedItem }, submarine, cargoManager: null, showNotification: false);
}
private static void SpawnInContainer(ItemPrefab prefab, int amount, SuitableContainers containers, Submarine submarine)
{
Dictionary<ItemContainer, int> containerAllocation = new();
int remaining = amount;
TryAllocate(containers.PriorityContainers);
if (remaining > 0)
{
TryAllocate(containers.PreferredContainers);
if (remaining > 0)
{
TryAllocate(containers.SecondaryContainers);
}
}
void TryAllocate(ICollection<ItemContainer> targetContainers)
=> AllocateContainers(prefab, targetContainers, ref remaining, ref containerAllocation);
foreach (var (container, howManyToPut) in containerAllocation)
{
for (int i = 0; i < howManyToPut; i++)
{
SpawnItem(prefab, container);
}
}
if (remaining > 0)
{
SpawnItemInCrate(prefab, submarine, remaining);
}
static void AllocateContainers(ItemPrefab prefab, ICollection<ItemContainer> containers, ref int remaining, ref Dictionary<ItemContainer, int> containerAllocation)
{
foreach (ItemContainer ic in containers)
{
int fit = ic.Inventory.HowManyCanBePut(prefab);
if (fit <= 0) { continue; }
fit = Math.Min(fit, remaining);
containerAllocation.Add(ic, fit);
remaining -= fit;
if (remaining <= 0) { break; }
}
}
static void SpawnItem(ItemPrefab itemPrefab, ItemContainer container)
{
if (container?.Item is null) { return; }
Item item = new Item(itemPrefab, container.Item.Position, container.Item.Submarine);
container.Inventory.TryPutItem(item, user: null);
CargoManager.ItemSpawned(item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
}
}
}
}
@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.PerkBehaviors
{
internal class SubItemSwapPerk : PerkBase
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetItem { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ReplacementItem { get; set; }
public override PerkSimulation Simulation
=> PerkSimulation.ServerOnly;
public SubItemSwapPerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
public override bool CanApply(SubmarineInfo submarine)
{
XElement subElement = submarine.SubmarineElement;
foreach (XElement element in subElement.Elements())
{
if (!element.Name.ToString().Equals(nameof(Item), StringComparison.OrdinalIgnoreCase)) { continue; }
Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier == TargetItem)
{
return true;
}
}
return false;
}
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
{
if (teamSubmarine is null) { return; }
List<Item> items = teamSubmarine.GetItems(true);
ItemPrefab itemToInstall = ItemPrefab.Find(null, ReplacementItem);
if (itemToInstall is null)
{
DebugConsole.ThrowError($"Could not find item \"{ReplacementItem}\" to swap with \"{TargetItem}\".");
return;
}
foreach (Item item in items)
{
if (item.Prefab.Identifier == TargetItem)
{
item.ReplaceWithLinkedItems(itemToInstall);
return;
}
}
}
}
}
@@ -0,0 +1,57 @@
using System.Collections.Generic;
namespace Barotrauma.PerkBehaviors
{
internal class UpgradeSubmarinePerk : PerkBase
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier UpgradeIdentifier { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CategoryIdentifier { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int Level { get; set; }
public override PerkSimulation Simulation
=> PerkSimulation.ServerAndClients;
public UpgradeSubmarinePerk(ContentXElement element, DisembarkPerkPrefab prefab) : base(element, prefab) { }
public override void ApplyOnRoundStart(IReadOnlyCollection<Character> teamCharacters, Submarine teamSubmarine)
{
if (teamSubmarine is null) { return; }
bool prefabFound = UpgradePrefab.Prefabs.TryGet(UpgradeIdentifier, out UpgradePrefab upgradePrefab);
bool categoryFound = UpgradeCategory.Categories.TryGet(CategoryIdentifier, out UpgradeCategory upgradeCategory);
if (!prefabFound)
{
DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade prefab not found");
return;
}
if (upgradePrefab.IsWallUpgrade)
{
foreach (Structure structure in teamSubmarine.GetWalls(UpgradeManager.UpgradeAlsoConnectedSubs))
{
structure.AddUpgrade(new Upgrade(structure, upgradePrefab, Level), createNetworkEvent: true);
}
}
else if (categoryFound)
{
foreach (Item item in teamSubmarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs))
{
if (upgradeCategory.CanBeApplied(item, upgradePrefab))
{
item.AddUpgrade(new Upgrade(item, upgradePrefab, Level), createNetworkEvent: true);
}
}
}
else
{
DebugConsole.ThrowError($"{nameof(UpgradeSubmarinePerk)}: Upgrade category not found");
}
}
}
}