Release 1.10.5.0 - Autumn Update 2025

This commit is contained in:
Regalis11
2025-09-17 13:44:21 +03:00
parent d13836ce87
commit caa0326cf8
120 changed files with 2584 additions and 635 deletions
@@ -312,9 +312,10 @@ namespace Barotrauma
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.GetItemAt(i);
//slot free, continue
if (otherItem == null) { continue; }
if (!otherItem.IsInteractable(Character)) { return false; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{
@@ -1092,6 +1092,8 @@ namespace Barotrauma
}
if (!isFleeing)
{
CheckForDraggedCorpses();
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull) { continue; }
@@ -1209,6 +1211,34 @@ namespace Barotrauma
}
}
}
private void CheckForDraggedCorpses()
{
if (Character.IsOnPlayerTeam) { return; }
if (Character.Submarine is not { Info.IsOutpost: true }) { return; }
//find corpses in the same team
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter.SelectedCharacter == null ||
!otherCharacter.SelectedCharacter.IsDead ||
otherCharacter.SelectedCharacter.TeamID != Character.TeamID ||
otherCharacter.IsInstigator)
{
continue;
}
if (!Character.CanSeeTarget(otherCharacter)) { continue; }
// Player is dragging a corpse from our team
string dialogTag = Character.IsSecurity ? "dialogdraggingcorpsereactionsecurity" : "dialogdraggingcorpsereaction";
Character.Speak(TextManager.Get(dialogTag).Value, messageType: null,
delay: Rand.Range(0.5f, 1.0f), identifier: "dialogdraggingcorpsereaction".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
AddCombatObjective(Character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat, otherCharacter);
break; // Only react to one at a time
}
}
public override void OnHealed(Character healer, float healAmount)
{
@@ -1660,6 +1690,10 @@ namespace Barotrauma
public AIObjective SetForcedOrder(Order order)
{
var objective = ObjectiveManager.CreateObjective(order);
if (order != null && !order.IsDismissal)
{
System.Diagnostics.Debug.Assert(objective != null);
}
ObjectiveManager.SetForcedOrder(objective);
return objective;
}
@@ -71,7 +71,7 @@ namespace Barotrauma
private static List<Identifier> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<Identifier>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); }
if (Submarine.MainSub != null && Submarine.MainSub.AtCosmeticDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); }
if (GameMain.GameSession != null && Level.Loaded != null)
{
@@ -84,7 +84,6 @@ namespace Barotrauma
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost".ToIdentifier());
@@ -101,6 +101,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true)
{
if (item == null) { return false; }
if (item.GetComponents<Pickable>().None(c => c is not Door && c.CanBePicked)) { return false; }
if (item.DontCleanUp) { return false; }
if (item.Illegitimate == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
@@ -65,7 +65,7 @@ namespace Barotrauma
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private readonly HashSet<ItemComponent> ignoredWeapons = new HashSet<ItemComponent>();
private AIObjectiveContainItem seekAmmunitionObjective;
private AIObjectiveGoTo retreatObjective;
@@ -503,7 +503,8 @@ namespace Barotrauma
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
Weapon = GetWeapon(allWeapons, out ItemComponent newWeaponComponent);
_weaponComponent = newWeaponComponent;
if (Weapon == null)
{
// No weapons
@@ -512,7 +513,7 @@ namespace Barotrauma
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
allWeapons.RemoveWhere(weaponComponent => weaponComponent.Item == Weapon);
Weapon = null;
continue;
}
@@ -540,7 +541,7 @@ namespace Barotrauma
else
{
// No ammo and should not try to seek ammo.
allWeapons.Remove(WeaponComponent);
allWeapons.RemoveWhere(weaponComponent => weaponComponent.Item == Weapon);
Weapon = null;
}
}
@@ -980,7 +981,6 @@ namespace Barotrauma
weapons.Clear();
foreach (var item in character.Inventory.AllItems)
{
if (ignoredWeapons.Contains(item)) { continue; }
GetWeapons(item, weapons);
if (item.OwnInventory != null)
{
@@ -990,11 +990,12 @@ namespace Barotrauma
return weapons;
}
private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (ignoredWeapons.Contains(component)) { continue; }
if (component.CombatPriority > 0)
{
weaponList.Add(component);
@@ -1332,19 +1333,21 @@ namespace Barotrauma
{
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunitionObjective);
ignoredWeapons.Add(Weapon);
ignoredWeapons.Add(WeaponComponent);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
/// <returns>True if the weapon was reloaded successfully.</returns>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
if (!Weapon.IsInteractable(character)) { return false; }
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
@@ -443,6 +443,10 @@ namespace Barotrauma
newCurrentObjective.Abandoned += () => DismissSelf(order);
CurrentOrders.Add(order.WithObjective(newCurrentObjective));
}
else if (!order.IsDismissal)
{
DebugConsole.ThrowError($"Failed to create an objective for the order: {order.Name}");
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
@@ -458,6 +462,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Creates an AI objective based on the order. Note that the method can return null in the case of e.g. Dismissal orders or orders that erroneously target something non-interactable.
/// </summary>
public AIObjective CreateObjective(Order order, float priorityModifier = 1)
{
if (order == null || order.IsDismissal) { return null; }
@@ -341,6 +341,8 @@ namespace Barotrauma
{
if (component?.GetType() is Type componentType)
{
// Items used via a controller (i.e. turrets) are not selectable but should still be valid targets.
if (!UseController && !component.CanBeSelected) { continue; }
if (componentType == ItemComponentType) { return component; }
if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; }
}
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using static Barotrauma.CharacterParams;
@@ -434,6 +435,21 @@ namespace Barotrauma
var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior == null) { continue; }
//never save hostile pets or pets left outside
if (c.TeamID == CharacterTeamType.None ||
c.TeamID == CharacterTeamType.Team2 ||
c.Submarine == null)
{
continue;
}
//pets must be in a player sub or owned by someone to be persistent
if (c.Submarine is not { Info.IsPlayer: true } &&
petBehavior.Owner is not { IsOnPlayerTeam: true })
{
continue;
}
XElement petElement = new XElement("pet",
new XAttribute("speciesname", c.SpeciesName),
new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0),
@@ -42,7 +42,7 @@ namespace Barotrauma
{
enemyAi.PetBehavior?.Update(deltaTime);
}
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
@@ -8,8 +8,15 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AnimController : Ragdoll
abstract class AnimController : Ragdoll, ISerializableEntity
{
/// <summary>
/// Most of the properties in this class are read-only, but can be useful for conditionals
/// </summary>
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public string Name => nameof(AnimController);
public Vector2 RightHandIKPos { get; protected set; }
public Vector2 LeftHandIKPos { get; protected set; }
@@ -200,7 +207,10 @@ namespace Barotrauma
public float WalkPos { get; protected set; }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams)
{
SerializableProperties = SerializableProperty.GetProperties(this);
}
public void UpdateAnimations(float deltaTime)
{
@@ -2411,7 +2411,9 @@ namespace Barotrauma
if (Inventory != null)
{
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false })
//this doesn't need to be run by the server, clients sync the contents of their inventory with the server instead of the inputs used to manipulate the inventory
#if CLIENT
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false } && CharacterHUD.ShouldDrawInventory(this))
{
foreach (Item item in HeldItems)
{
@@ -2429,6 +2431,7 @@ namespace Barotrauma
break;
}
}
#endif
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
@@ -2632,6 +2635,7 @@ namespace Barotrauma
public bool Unequip(Item item)
{
if (!HasEquippedItem(item)) { return false; }
if (!item.IsInteractable(this)) { return false; }
if (!TryPutItemInAnySlot(item))
{
if (!TryPutItemInBag(item))
@@ -2642,7 +2646,7 @@ namespace Barotrauma
return true;
}
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.AllowBotsAndPets)
{
if (!CanInteract || inventory.Locked) { return false; }
@@ -2686,7 +2690,7 @@ namespace Barotrauma
/// <summary>
/// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc).
/// </summary>
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.AllowBotsAndPets)
{
if (Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive && !IsDead)
@@ -2701,9 +2705,9 @@ namespace Barotrauma
if (IsKnockedDownOrRagdolled || LockHands) { return true; }
return accessLevel switch
{
CharacterInventory.AccessLevel.Restricted => false,
CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
CharacterInventory.AccessLevel.OnlyIfIncapacitated => false,
CharacterInventory.AccessLevel.AllowBotsAndPets => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.AllowFriendly => IsOnSameTeam() || IsFriendlyPet(),
_ => throw new NotImplementedException()
};
@@ -5714,7 +5718,7 @@ namespace Barotrauma
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
if (GameMain.GameSession != null && GameMain.GameSession.UnlockedRecipes.Contains(recipeIdentifier)) { return true; }
if (GameMain.GameSession != null && GameMain.GameSession.HasUnlockedRecipe(this, recipeIdentifier)) { return true; }
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
@@ -135,6 +135,9 @@ namespace Barotrauma
private Affliction stunAffliction;
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
@@ -106,6 +106,8 @@ namespace Barotrauma
void AddTexturePath(string path)
{
if (string.IsNullOrEmpty(path)) { return; }
//if the path contains a gender variable, we can't load it yet because we don't know which gender we need
if (path.Contains("[GENDER]")) { return; }
texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture));
}
}
@@ -656,13 +656,17 @@ namespace Barotrauma
NewMessage("***************", Color.Cyan);
}));
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
commands.Add(new Command("godmode", "godmode [character name] [remove afflictions (true/false)]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
(string[] args) =>
{
bool? godmodeStateOnFirstCharacter = null;
HandleCommandForCrewOrSingleCharacter(args, ToggleGodMode);
void ToggleGodMode(Character targetCharacter)
{
if (args.Length > 1 && bool.TryParse(args[1], out bool removeafflictions))
{
if (removeafflictions) { targetCharacter.CharacterHealth.RemoveAllAfflictions(); }
}
targetCharacter.GodMode = godmodeStateOnFirstCharacter ?? !targetCharacter.GodMode;
godmodeStateOnFirstCharacter = targetCharacter.GodMode;
NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name,
@@ -21,6 +21,9 @@
[Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")]
public int State { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")]
public bool ForceFailure { get; set; }
private bool isFinished;
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
@@ -31,7 +34,7 @@
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
contentPackage: element.ContentPackage);
}
if (Operation == OperationType.Add && State == 0)
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
{
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.",
contentPackage: element.ContentPackage);
@@ -54,6 +57,11 @@
foreach (Mission mission in GameMain.GameSession.Missions)
{
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
if (ForceFailure)
{
mission.ForceFailure = true;
}
switch (Operation)
{
case OperationType.Set:
@@ -410,16 +410,39 @@ namespace Barotrauma
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true)
{
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
IEnumerable<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
potentialSpawnPoints = potentialSpawnPoints.Where(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
//find spawnpoints with the desired type, or any random spawnpoints if not specified
IEnumerable<WayPoint> spawnPointsWithCorrectType;
if (spawnPointType.HasValue)
{
spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp =>
spawnPointType.Value.HasFlag(wp.SpawnType) &&
//need to handle zero (SpawnType.Path) separately, because spawnPointType will always have the flag 0
(wp.SpawnType != 0 || spawnPointType.Value == 0));
}
else
{
spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
}
if (spawnPointsWithCorrectType.Any())
{
potentialSpawnPoints = spawnPointsWithCorrectType;
}
//with correct module flags, if there are any
if (moduleFlags != null && moduleFlags.Any())
{
var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
if (spawnPoints.Any())
var spawnPointsWithCorrectFlags = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
if (spawnPointsWithCorrectFlags.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
potentialSpawnPoints = spawnPointsWithCorrectFlags.ToList();
}
}
//with correct spawn point tags, if there are any
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPointsWithTag = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
@@ -461,16 +484,8 @@ namespace Barotrauma
return null;
}
IEnumerable<WayPoint> validSpawnPoints;
if (spawnPointType.HasValue)
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
}
else
{
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType != SpawnType.Path);
if (!validSpawnPoints.Any()) { validSpawnPoints = potentialSpawnPoints; }
}
//spawnpoints that match the desired criteria found, choose the best one next
IEnumerable<WayPoint> validSpawnPoints = potentialSpawnPoints;
//don't spawn in an airlock module if there are other options
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false);
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -23,10 +24,12 @@ namespace Barotrauma
private bool swarmSpawned;
private readonly List<MonsterSet> monsterSets = new List<MonsterSet>();
private readonly LocalizedString sonarLabel;
private readonly ImmutableArray<Identifier> beaconTags;
public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
swarmSpawned = false;
beaconTags = prefab.ConfigElement.GetAttributeIdentifierArray("beacontags", []).ToImmutableArray();
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
@@ -185,6 +188,29 @@ namespace Barotrauma
{
levelData.HasBeaconStation = true;
levelData.IsBeaconActive = false;
if (beaconTags.Length > 0)
{
var selectedBeacon = GetRandomBeaconByTags(beaconTags, levelData);
if (selectedBeacon != null)
{
levelData.ForceBeaconStation = selectedBeacon;
}
else
{
DebugConsole.ThrowError($"Beacon mission \"{Prefab.Identifier}\" could not find a suitable beacon station with beacontags \"{string.Join(", ", beaconTags)}\" for level difficulty {levelData.Difficulty:F1}.",
contentPackage: Prefab.ContentPackage);
}
}
}
private static SubmarineInfo GetRandomBeaconByTags(ImmutableArray<Identifier> tags, LevelData levelData)
{
return GetRandomSubmarineByTagsAndDifficulty(
tags,
levelData,
s => s.IsBeacon,
"beacon station");
}
}
}
@@ -48,7 +48,7 @@ namespace Barotrauma
protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
private readonly CheckDataAction completeCheckDataAction;
protected readonly CheckDataAction completeCheckDataAction;
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
@@ -110,9 +110,11 @@ namespace Barotrauma
public bool Failed
{
get { return failed; }
get { return failed || ForceFailure; }
}
public bool ForceFailure;
public virtual bool AllowRespawning
{
get { return true; }
@@ -541,9 +543,10 @@ namespace Barotrauma
{
if (GameMain.NetworkMember is not { IsClient: true })
{
completed =
completed =
!ForceFailure &&
DetermineCompleted() &&
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
(completeCheckDataAction == null || completeCheckDataAction.GetSuccess());
}
if (completed)
{
@@ -569,6 +572,10 @@ namespace Barotrauma
TimesAttempted++;
EndMissionSpecific(completed);
if (ForceFailure)
{
failed = true;
}
}
protected abstract bool DetermineCompleted();
@@ -829,6 +836,51 @@ namespace Barotrauma
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.ServerAndClient),
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
}
/// <summary>
/// Gets a random submarine by tags, filtered by difficulty. Used by missions that force specific submarines (wrecks, beacons, etc.)
/// </summary>
/// <param name="tags">Mission tags to match against</param>
/// <param name="seed">Random seed for selection</param>
/// <param name="submarineSelector">Function to filter submarines by type (e.g., s => s.IsWreck)</param>
/// <param name="submarineTypeName">Name of submarine type for error messages (e.g., "wreck", "beacon station")</param>
/// <returns>Selected submarine, or null if none found</returns>
protected static SubmarineInfo GetRandomSubmarineByTagsAndDifficulty(
IEnumerable<Identifier> tags,
LevelData levelData,
Func<SubmarineInfo, bool> submarineSelector,
string submarineTypeName)
{
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
float levelDifficulty = levelData.Difficulty;
var submarinesWithTags = SubmarineInfo.SavedSubmarines
.Where(submarineSelector)
.Where(s =>
{
return s.GetExtraSubmarineInfo is { } extraInfo && (tags.None() || tags.Any(t => extraInfo.MissionTags.Contains(t)));
})
.ToList();
var matchingSubmarines = submarinesWithTags
.Where(s =>
{
return s.GetExtraSubmarineInfo is { } extraInfo &&
levelDifficulty >= extraInfo.MinLevelDifficulty &&
levelDifficulty <= extraInfo.MaxLevelDifficulty;
})
.ToList();
if (matchingSubmarines.Count == 0)
{
if (submarinesWithTags.Count > 0)
{
DebugConsole.ThrowError($"Found {submarinesWithTags.Count} {submarineTypeName}(s) with matching tags \"{string.Join(", ", tags)}\", but none are suitable for level difficulty {levelDifficulty:F1}.");
}
return null;
}
return matchingSubmarines[rand.Next(matchingSubmarines.Count)];
}
}
class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
@@ -4,6 +4,7 @@ using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
@@ -240,6 +241,8 @@ namespace Barotrauma
/// </summary>
private readonly float requiredDeliveryAmount;
private readonly ImmutableArray<Identifier> wreckTags;
private LocalizedString pickedUpMessage;
/// <summary>
@@ -311,12 +314,13 @@ namespace Barotrauma
: base(prefab, locations, sub)
{
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeFloat(nameof(requiredDeliveryAmount), 0.98f);
//LevelData may not be instantiated at this point, in that case use the name identifier of the location
rng = new MTRandom(ToolBox.StringToInt(
locations[0].LevelData?.Seed ?? locations[0].NameIdentifier.Value +
locations[1].LevelData?.Seed ?? locations[1].NameIdentifier.Value));
wreckTags = prefab.ConfigElement.GetAttributeIdentifierArray("wrecktags", []).ToImmutableArray();
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
@@ -756,5 +760,31 @@ namespace Barotrauma
target.Reset();
}
}
public override void AdjustLevelData(LevelData levelData)
{
if (wreckTags.Length > 0)
{
var selectedWreck = GetRandomWreckByTags(wreckTags, levelData);
if (selectedWreck != null)
{
levelData.ForceWreck = selectedWreck;
}
else
{
DebugConsole.ThrowError($"Salvage mission \"{Prefab.Identifier}\" could not find a suitable wreck with wrecktags \"{string.Join(", ", wreckTags)}\" for level difficulty {levelData.Difficulty:F1}.",
contentPackage: Prefab.ContentPackage);
}
}
}
private static SubmarineInfo GetRandomWreckByTags(ImmutableArray<Identifier> tags, LevelData levelData)
{
return GetRandomSubmarineByTagsAndDifficulty(
tags,
levelData,
s => s.IsWreck,
"wreck");
}
}
}
@@ -304,12 +304,17 @@ namespace Barotrauma
// Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab));
var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true);
foreach (PurchasedItem item in itemsToPurchase)
//handle checking which items can be purchased and deducting money first
foreach (PurchasedItem item in itemsToPurchase.ToList())
{
if (item.Quantity <= 0) { continue; }
// Exchange money
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
if (!campaign.TryPurchase(client, itemValue)) { continue; }
if (!campaign.TryPurchase(client, itemValue))
{
itemsToPurchase.Remove(item);
continue;
}
// Add to the purchased items
var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab && pi.DeliverImmediately == item.DeliverImmediately);
@@ -329,6 +334,7 @@ namespace Barotrauma
}
store.Balance += itemValue;
}
//actually spawn the items at this point
if (GameMain.NetworkMember is not { IsClient: true })
{
Character targetCharacter;
@@ -1724,7 +1724,10 @@ namespace Barotrauma
GameMain.GameSession.EventManager.Load(subElement);
break;
case "unlockedrecipe":
GameMain.GameSession.UnlockRecipe(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), showNotifications: false);
GameMain.GameSession.UnlockRecipe(
subElement.GetAttributeEnum("team", CharacterTeamType.Team1),
subElement.GetAttributeIdentifier("identifier", Identifier.Empty),
showNotifications: false);
break;
}
}
@@ -170,8 +170,8 @@ namespace Barotrauma
public Submarine? Submarine { get; set; }
private readonly HashSet<Identifier> unlockedRecipes = new HashSet<Identifier>();
public IEnumerable<Identifier> UnlockedRecipes => unlockedRecipes;
private readonly HashSet<(CharacterTeamType team, Identifier identifier)> unlockedRecipes = new HashSet<(CharacterTeamType, Identifier)>();
public IEnumerable<(CharacterTeamType, Identifier)> UnlockedRecipes => unlockedRecipes;
public CampaignDataPath DataPath { get; set; }
@@ -1499,25 +1499,32 @@ namespace Barotrauma
#endif
}
public void UnlockRecipe(Identifier identifier, bool showNotifications)
public void UnlockRecipe(CharacterTeamType team, Identifier identifier, bool showNotifications)
{
if (unlockedRecipes.Add(identifier))
if (unlockedRecipes.Add((team, identifier)))
{
#if CLIENT
if (showNotifications)
{
foreach (var character in GetSessionCrewCharacters(CharacterType.Both))
{
if (character.TeamID != team) { continue; }
LocalizedString recipeName = TextManager.Get($"entityname.{identifier}").Fallback(identifier.Value);
character.AddMessage(TextManager.GetWithVariable("recipeunlockednotification", "[name]", recipeName).Value, GUIStyle.Yellow, playSound: true);
}
}
#else
GameMain.Server.UnlockRecipe(identifier);
GameMain.Server.UnlockRecipe(team, identifier);
#endif
}
}
public bool HasUnlockedRecipe(Character character, Identifier itemIdentifier)
{
if (character == null) { return false; }
return unlockedRecipes.Contains((character.TeamID, itemIdentifier));
}
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
{
errorMsg = "";
@@ -17,15 +17,21 @@ namespace Barotrauma
{
/// <summary>
/// How much access other characters have to the inventory?
/// <see cref="Restricted"/> = Only accessible when character is knocked down or handcuffed.
/// <see cref="Limited"/> = Can also access inventories of bots on the same team and friendly pets.
/// <see cref="Allowed"/> = Can also access other players in the same team (used for drag and drop give).
/// </summary>
public enum AccessLevel
{
Restricted,
Limited,
Allowed
/// <summary>
/// Only accessible when character is knocked down or handcuffed.
/// </summary>
OnlyIfIncapacitated,
/// <summary>
/// Can also access inventories of bots on the same team and friendly pets.
/// </summary>
AllowBotsAndPets,
/// <summary>
/// Can also access other players in the same team (used for drag and drop give).
/// </summary>
AllowFriendly
}
private readonly Character character;
@@ -342,8 +348,9 @@ namespace Barotrauma
{
foreach (Item existingItem in slots[slot].Items.ToList())
{
if (!existingItem.IsInteractable(character)) { continue; }
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
existingItem.ParentInventory?.RemoveItem(existingItem);
}
}
}
@@ -1,5 +1,6 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma.Items.Components
@@ -197,7 +198,16 @@ namespace Barotrauma.Items.Components
item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
item.WaterDragCoefficient = WaterDragCoefficient;
item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
float throwForce = ThrowForce;
//Reduce force when aiming down
float downwardsDotProduct = Vector2.Dot(-Vector2.UnitY, throwVector); //1 when pointing directly down, 0 when sideways, -1 when up
if (downwardsDotProduct > 0)
{
throwForce *= (1.0f - downwardsDotProduct * 0.7f);
}
item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
//disable platform collisions until the item comes back to rest again
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
@@ -483,6 +483,9 @@ namespace Barotrauma.Items.Components
public virtual bool UpdateWhenInactive => false;
[Serialize(false, IsPropertySaveable.No, "If true, the component will retain its normal functionality when the item reaches 0 condition.")]
public bool UpdateWhenBroken { get; set; }
//called when isActive is true and condition > 0.0f
public virtual void Update(float deltaTime, Camera cam)
{
@@ -132,6 +132,12 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should a button that allows sorting the items alphabetically be shown in the container's UI panel?")]
public bool ShowSortButton { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should a button that merges items into stacks be shown in the container's UI panel?")]
public bool ShowMergeButton { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
public bool QuickUseMovesItemsInside { get; set; }
@@ -406,6 +406,7 @@ namespace Barotrauma.Items.Components
if (IsOutOfPower()) { return false; }
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -421,8 +422,7 @@ namespace Barotrauma.Items.Components
item.SendSignal(new Signal(output, sender: user), "trigger_out");
}
lastUsed = Timing.TotalTime;
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
lastUsed = Timing.TotalTime;
return true;
}
@@ -541,6 +541,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
PlaySound(ActionType.OnUse, picker);
#endif
ApplyStatusEffects(ActionType.OnUse, 1f, picker);
return true;
}
@@ -84,7 +84,10 @@ namespace Barotrauma.Items.Components
CurrFlow = 0.0f;
}
private void GetVents()
/// <summary>
/// Finds all the linked vents and calculates how much oxygen should be distributed to each of them based on the hull volumes.
/// </summary>
public void GetVents()
{
totalHullVolume = 0.0f;
ventList ??= new List<(Vent vent, float hullVolume)>();
@@ -2,6 +2,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -35,7 +36,7 @@ namespace Barotrauma.Items.Components
{
get
{
if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; }
if (item.ConditionPercentage > 10.0f || !IsActive || Disabled) { return 0.0f; }
return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f;
}
}
@@ -61,6 +62,23 @@ namespace Barotrauma.Items.Components
set => maxFlow = value;
}
private bool disabled;
[Serialize(false, IsPropertySaveable.Yes, description: "If true, the pump is unable to pump water.", alwaysUseInstanceValues: true)]
public bool Disabled
{
get => disabled;
set
{
if (disabled == value) { return; }
disabled = value;
#if SERVER
//send a network update soon
//don't force to 0 though so this doesn't lead to spam if the property is toggled rapidly
networkUpdateTimer = Math.Min(networkUpdateTimer, 0.5f);
#endif
}
}
[Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool IsOn
{
@@ -68,15 +86,13 @@ namespace Barotrauma.Items.Components
set { IsActive = value; }
}
[Serialize(false, IsPropertySaveable.No)]
public bool CanCauseLethalPressure { get; set; }
private float currFlow;
public float CurrFlow
{
get
{
if (!IsActive) { return 0.0f; }
return Math.Abs(currFlow);
}
}
public float CurrFlow => IsActive ? Math.Abs(currFlow) : 0.0f;
public bool IsHullFull => item.CurrentHull != null && item.CurrentHull.WaterVolume >= item.CurrentHull.Volume * Hull.MaxCompress;
public override bool HasPower => IsActive && Voltage >= MinVoltage;
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
@@ -85,7 +101,7 @@ namespace Barotrauma.Items.Components
public override bool UpdateWhenInactive => true;
public float CurrentStress => Math.Abs(flowPercentage / 100.0f);
public float CurrentStress => IsActive ? Math.Abs(flowPercentage / 100.0f) : 0.0f;
public Pump(Item item, ContentXElement element)
: base(item, element)
@@ -95,48 +111,42 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(ContentXElement element);
private readonly List<Hull> linkedHulls = [];
public override void Update(float deltaTime, Camera cam)
{
pumpSpeedLockTimer -= deltaTime;
isActiveLockTimer -= deltaTime;
if (!IsActive)
currFlow = 0f;
if (item.CurrentHull == null)
{
if (TargetLevel != null) { FlowPercentage = 0f; }
return;
}
currFlow = 0.0f;
if (TargetLevel != null)
{
float hullPercentage = 0.0f;
if (item.CurrentHull != null)
float hullWaterVolume = item.CurrentHull.WaterVolume;
float totalHullVolume = item.CurrentHull.Volume;
linkedHulls.Clear();
//hidden hulls still affect buoyancy, include them here
item.CurrentHull.GetLinkedHulls(linkedHulls, includeHiddenHulls: true);
foreach (var linkedHull in linkedHulls)
{
float hullWaterVolume = item.CurrentHull.WaterVolume;
float totalHullVolume = item.CurrentHull.Volume;
foreach (var linked in item.CurrentHull.linkedTo)
{
if ((linked is Hull linkedHull))
{
hullWaterVolume += linkedHull.WaterVolume;
totalHullVolume += linkedHull.Volume;
}
}
hullPercentage = hullWaterVolume / totalHullVolume * 100.0f;
hullWaterVolume += linkedHull.WaterVolume;
totalHullVolume += linkedHull.Volume;
}
float hullPercentage = hullWaterVolume / totalHullVolume * 100.0f;
FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f;
}
if (!HasPower)
{
return;
}
UpdateNetworking(deltaTime);
UpdateProjSpecific(deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (item.CurrentHull == null) { return; }
if (!IsActive || Disabled) { return; }
if (flowPercentage <= 0f && item.CurrentHull.WaterVolume <= 0f) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
@@ -150,8 +160,22 @@ namespace Barotrauma.Items.Components
//less effective when in a bad condition
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate;
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; }
if (MathUtils.NearlyEqual(currFlow, 0f, epsilon: 0.01f))
{
currFlow = 0f; // Set to 0 for conditionals.
return;
}
item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate;
if (flowPercentage > 0f && item.CurrentHull.WaterVolume > item.CurrentHull.Volume)
{
item.CurrentHull.Pressure += 30f * deltaTime;
if (CanCauseLethalPressure) { item.CurrentHull.LethalPressure += Hull.PressureBuildUpSpeed * deltaTime; }
}
ApplyStatusEffects(ActionType.OnActive, deltaTime);
UpdateProjSpecific(deltaTime);
}
public void InfectBallast(Identifier identifier, bool allowMultiplePerShip = false)
@@ -188,7 +212,7 @@ namespace Barotrauma.Items.Components
public override float GetCurrentPowerConsumption(Connection connection = null)
{
//There shouldn't be other power connections to this
if (connection != this.powerIn || !IsActive)
if (connection != this.powerIn || !IsActive || Disabled)
{
return 0;
}
@@ -202,6 +226,8 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime);
partial void UpdateNetworking(float deltaTime);
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (Hijacked) { return; }
@@ -276,5 +302,11 @@ namespace Barotrauma.Items.Components
}
return true;
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
linkedHulls.Clear();
}
}
}
@@ -82,6 +82,11 @@ namespace Barotrauma.Items.Components
#if CLIENT
CreateGUI();
if (Screen.Selected is not { IsEditor: true })
{
//set text via the property to refresh the UI
Name = name;
}
#endif
}
@@ -13,10 +13,24 @@ namespace Barotrauma.Items.Components
{
partial class TriggerComponent : ItemComponent
{
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
[Editable, Serialize(0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public float Force { get; set; }
[Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The maximum amount of directional force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public Vector2 DirectionalForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, $"If true, {nameof(DirectionalForce)} is relative to the angle between the target and the item, Similar to {nameof(Force)}.\nIf false, it always pushes in the same direction, with respect to the item's rotation.", alwaysUseInstanceValues: true)]
public bool RelativeDirectionalForce { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no vertical force will be applied.", alwaysUseInstanceValues: true)]
public bool VerticalForce { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no horizontal force will be applied.", alwaysUseInstanceValues: true)]
public bool HorizontalForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)]
public bool DistanceBasedForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
public bool ForceFluctuation { get; set; }
@@ -141,12 +155,29 @@ namespace Barotrauma.Items.Components
get => base.IsActive;
set
{
bool wasActive = base.IsActive;
base.IsActive = value;
if (!IsActive)
{
TriggerActive = false;
triggerers.Clear();
}
else if (!wasActive && PhysicsBody?.FarseerBody != null)
{
//when the trigger becomes active, we need to check which entities are inside it
ContactEdge ce = PhysicsBody.FarseerBody.ContactList;
while (ce != null && ce.Contact != null)
{
if (ce.Contact.Enabled)
{
var thisFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureA : ce.Contact.FixtureB;
var otherFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureB : ce.Contact.FixtureA;
OnCollision(thisFixture, otherFixture, ce.Contact);
}
ce = ce.Next;
}
}
}
}
@@ -374,7 +405,9 @@ namespace Barotrauma.Items.Components
float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount);
ForceFluctuationTimer = 0.0f;
GameMain.NetworkMember?.CreateEntityEvent(this);
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
@@ -398,7 +431,7 @@ namespace Barotrauma.Items.Components
}
}
if (Math.Abs(Force) < 0.01f)
if (Force < 0.01f && DirectionalForce.LengthSquared() < 0.0001f)
{
// Just ignore very minimal forces
continue;
@@ -436,7 +469,25 @@ namespace Barotrauma.Items.Components
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
Vector2 radialForce = Force * Vector2.Normalize(diff);
Vector2 directionalForce;
if (RelativeDirectionalForce)
{
directionalForce = DirectionalForce * new Vector2(Math.Sign(diff.X), Math.Sign(diff.Y));
}
else
{
Vector2 flippedForce = DirectionalForce;
if (item.FlippedX) { flippedForce.X = -flippedForce.X; }
if (item.FlippedY) { flippedForce.Y = -flippedForce.Y; }
directionalForce = MathUtils.RotatePoint(flippedForce, -item.RotationRad);
}
Vector2 force = (radialForce + directionalForce) * CurrentForceFluctuation * distanceFactor * multiplier;
if (!HorizontalForce) { force.Y = 0.0f; }
if (!VerticalForce) { force.Y = 0.0f; }
if (force.LengthSquared() < 0.01f) { return; }
if (body.Mass < 1)
{
@@ -461,20 +461,15 @@ namespace Barotrauma
}
}
public float ImpactTolerance
{
get { return Prefab.ImpactTolerance; }
}
public float InteractDistance
{
get { return Prefab.InteractDistance; }
}
public float ImpactTolerance => Prefab.ImpactTolerance;
public float InteractPriority
{
get { return Prefab.InteractPriority; }
}
public float ImpactDamage => Prefab.ImpactDamage;
public float ImpactDamageProbability => Prefab.ImpactDamageProbability;
public float InteractDistance => Prefab.InteractDistance;
public float InteractPriority => Prefab.InteractPriority;
public override Vector2 Position
{
@@ -1767,7 +1762,7 @@ namespace Barotrauma
ic.Move(amount, ignoreContacts);
}
if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); }
if (body != null && (Submarine == null || !Submarine.Loading) || Screen.Selected is { IsEditor: true }) { FindHull(); }
}
public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
@@ -2387,7 +2382,7 @@ namespace Barotrauma
{
while (impactQueue.TryDequeue(out float impact))
{
HandleCollision(impact);
ReceiveImpact(impact);
}
}
if (isDroppedStackOwner && body != null)
@@ -2461,7 +2456,7 @@ namespace Barotrauma
if (ic.IsActive || ic.UpdateWhenInactive)
{
if (condition <= 0.0f)
if (!ic.UpdateWhenBroken && condition <= 0.0f)
{
ic.UpdateBroken(deltaTime, cam);
}
@@ -2713,25 +2708,35 @@ namespace Barotrauma
return true;
}
private void HandleCollision(float impact)
public void ReceiveImpact(float impactStrength, bool recursive = true)
{
OnCollisionProjSpecific(impact);
OnCollisionProjSpecific(impactStrength);
if (GameMain.NetworkMember is { IsClient: true }) { return; }
if (ImpactTolerance > 0.0f && Math.Abs(impact) > ImpactTolerance && hasStatusEffectsOfType[(int)ActionType.OnImpact])
if (ImpactTolerance > 0.0f && Math.Abs(impactStrength) > ImpactTolerance && Rand.Range(0.0f, 1.0f) < ImpactDamageProbability)
{
foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact])
if (ImpactDamage != 0.0f)
{
ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f);
Condition -= impactStrength * ImpactDamage;
}
if (hasStatusEffectsOfType[(int)ActionType.OnImpact])
{
foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact])
{
ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f);
}
#if SERVER
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
#endif
}
}
if (!recursive) { return; }
foreach (Item contained in ContainedItems)
{
if (contained.body != null) { contained.HandleCollision(impact); }
if (contained.body != null) { contained.ReceiveImpact(impactStrength, recursive: true); }
}
}
@@ -3698,18 +3703,15 @@ namespace Barotrauma
SerializableProperty property = extraData.SerializableProperty;
ISerializableEntity entity = extraData.Entity;
msg.WriteVariableUInt32((uint)allProperties.Count);
if (property != null)
{
if (allProperties.Count > 1)
{
int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
if (propertyIndex < 0)
if (allProperties.None(p => p.property == property && p.obj == entity))
{
throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
}
msg.WriteVariableUInt32((uint)propertyIndex);
msg.WriteIdentifier(property.Name.ToIdentifier());
}
object value = property.GetValue(entity);
@@ -3814,21 +3816,11 @@ namespace Barotrauma
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
if (allProperties.Count == 0) { return; }
int propertyCount = (int)msg.ReadVariableUInt32();
if (propertyCount != allProperties.Count)
Identifier propertyIdentifier = msg.ReadIdentifier();
int propertyIndex = allProperties.IndexOf(p => p.property.Name == propertyIdentifier);
if (propertyIndex < 0)
{
throw new Exception($"Error in {nameof(ReadPropertyChange)}. The number of properties on the item \"{Prefab.Identifier}\" does not match between the server and the client. Server: {propertyCount}, client: {allProperties.Count}.");
}
int propertyIndex = 0;
if (allProperties.Count > 1)
{
propertyIndex = (int)msg.ReadVariableUInt32();
}
if (propertyIndex >= allProperties.Count || propertyIndex < 0)
{
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Property index out of bounds (item: {Prefab.Identifier}, index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Could not find the property \"{propertyIdentifier}\" in item \"{Prefab.Identifier}\" (property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
}
bool allowEditing = true;
@@ -821,6 +821,15 @@ namespace Barotrauma
set { impactTolerance = Math.Max(value, 0.0f); }
}
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register.")]
public float ImpactDamage { get; set; }
[Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register.")]
public float ImpactDamageProbability { get; set; }
[Serialize(false, IsPropertySaveable.No, "If true, submarine impacts will trigger OnImpact effects. Only applies to items with a null or non-dynamic physics body - items with dynamic bodies always react to impacts.")]
public bool ReceiveSubmarineImpacts { get; set; }
[Serialize(0.0f, IsPropertySaveable.No)]
public float OnDamagedThreshold { get; set; }
@@ -102,12 +102,12 @@ namespace Barotrauma
}
/// <summary>
/// Index of the slot the target must be in when targeting a Contained item
/// Index of the slot the target must be in when targeting a Contained item or a character inventory.
/// </summary>
public int TargetSlot = -1;
/// <summary>
/// The slot type the target must be in when targeting an item contained inside a character's inventory
/// The slot type the target must be in when targeting an item contained inside a character's inventory.
/// </summary>
public InvSlotType CharacterInventorySlotType;
@@ -329,7 +329,6 @@ namespace Barotrauma
IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false);
MatchOnEmpty = element.GetAttributeBool("matchonempty", false);
TargetSlot = element.GetAttributeInt("targetslot", -1);
}
public bool CheckRequirements(Character character, Item parentItem)
@@ -344,22 +343,21 @@ namespace Barotrauma
return CheckItem(parentItem.Container, this);
case RelationType.Equipped:
if (character == null) { return false; }
var heldItems = character.HeldItems;
if (RequireOrMatchOnEmpty && heldItems.None()) { return true; }
foreach (Item equippedItem in heldItems)
foreach (var item in character.Inventory.AllItemsMod)
{
if (equippedItem == null) { continue; }
if (CheckItem(equippedItem, this))
if (character.HasEquippedItem(item) && CheckItem(item, this))
{
if (RequireEmpty && equippedItem.Condition > 0) { return false; }
if (RequireEmpty && item.Condition > 0) { return false; }
return true;
}
}
break;
//got this far -> no matching item was equipped
//return true if we require or want to match "empty" (no matching item), otherwise false
return RequireOrMatchOnEmpty;
case RelationType.Picked:
if (character == null) { return false; }
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
var allItems = character.Inventory.AllItems;
var allItems = TargetSlot == -1 ? character.Inventory.AllItems : character.Inventory.GetItemsAt(TargetSlot);
if (RequireOrMatchOnEmpty && allItems.None()) { return true; }
foreach (Item pickedItem in allItems)
{
@@ -396,7 +396,10 @@ namespace Barotrauma
}
}
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && Attack.Afflictions.None())
if (Attack.Afflictions.None() &&
MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) &&
MathUtils.NearlyEqual(Attack.ItemDamage, 0.0f) &&
MathUtils.NearlyEqual(Attack.StructureDamage, 0.0f))
{
return;
}
@@ -370,18 +370,31 @@ namespace Barotrauma
public override void Update(float deltaTime, Camera cam)
{
Hull hull1 = linkedTo.Count < 1 ? null : linkedTo[0] as Hull;
Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1];
int updateInterval = 4;
float flowMagnitude = flowForce.LengthSquared();
if (flowMagnitude < 1.0f)
//if one hull is at lethal pressure (connected to outside), and the other not yet,
//we need frequent updates to quickly move water into the other hull
if (hull1 != null && hull2 != null &&
hull1.LethalPressure > 0.0f != hull2.LethalPressure > 0.0f)
{
//very sparse updates if there's practically no water moving
updateInterval = 8;
}
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
{
//frequent updates if water is moving between hulls
updateInterval = 1;
}
else
{
float flowMagnitude = flowForce.LengthSquared();
if (flowMagnitude < 1.0f)
{
//very sparse updates if there's practically no water moving
updateInterval = 8;
}
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
{
//frequent updates if water is moving between hulls
updateInterval = 1;
}
}
updateCount++;
if (updateCount < updateInterval) { return; }
@@ -409,8 +422,6 @@ namespace Barotrauma
return;
}
Hull hull1 = (Hull)linkedTo[0];
Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1];
if (hull1 == hull2) { return; }
UpdateOxygen(hull1, hull2, deltaTime);
@@ -469,6 +480,8 @@ namespace Barotrauma
higherSurface = Math.Max(hull1.Surface, hull2.Surface + subOffset.Y);
float delta = 0.0f;
Hull flowSourceHull = null;
//water level is above the lower boundary of the gap
if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - Size)
{
@@ -479,10 +492,9 @@ namespace Barotrauma
{
if (!(hull2.WaterVolume > 0.0f)) { return; }
lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1];
//delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume));
//delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress);
flowTargetHull = hull1;
flowSourceHull = hull2;
//make sure not to move more than what the room contains
delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 300.0f * sizeModifier * deltaTime, Math.Min(hull2.WaterVolume, hull2.Volume));
@@ -504,6 +516,7 @@ namespace Barotrauma
lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1];
flowTargetHull = hull2;
flowSourceHull = hull1;
//make sure not to move more than what the room contains
delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 300.0f * sizeModifier * deltaTime, Math.Min(hull1.WaterVolume, hull1.Volume));
@@ -547,7 +560,6 @@ namespace Barotrauma
if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f)
{
float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier);
//make sure not to place more water to the target room than it can hold
if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress)
{
@@ -623,19 +635,30 @@ namespace Barotrauma
}
}
void UpdateRoomToOut(float deltaTime, Hull hull1)
/// <summary>
/// How much water can flow through the gap to the hull if the gap is connected outside.
/// </summary>
private float GetWaterFlowFromOutside(Hull hull, float deltaTime, bool ignoreCurrentWater = false)
{
//a variable affecting the water flow through the gap
//the larger the gap is, the faster the water flows
float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction);
float delta = 500.0f * sizeModifier * deltaTime;
if (!ignoreCurrentWater)
{
delta = Math.Min(delta, hull.Volume * Hull.MaxCompress - hull.WaterVolume);
}
return delta;
}
void UpdateRoomToOut(float deltaTime, Hull hull1)
{
float delta = GetWaterFlowFromOutside(hull1, deltaTime);
//make sure not to place more water to the target room than it can hold
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
hull1.WaterVolume += delta;
if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 30.0f * deltaTime; }
if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 100.0f * deltaTime; }
flowTargetHull = hull1;
@@ -698,6 +721,65 @@ namespace Barotrauma
hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime;
}
}
if (hull1.LethalPressure > 0)
{
SimulateWaterFlowFromOutsideToConnectedHulls(hull1, maxFlow: GetWaterFlowFromOutside(hull1, deltaTime, ignoreCurrentWater: true), deltaTime: deltaTime);
}
}
private Hull GetOtherLinkedHull(Hull hull1)
{
if (linkedTo.Count != 2 || hull1 == null) { return null; }
return (linkedTo[0] == hull1 ? linkedTo[1] : linkedTo[0]) as Hull;
}
private static readonly HashSet<Hull> checkedHulls = new HashSet<Hull>();
/// <summary>
/// Simulates water flow from the source to all the hulls it's connected to across the sub, as if the water was coming directly from outside.
/// Used to prevent gaps from slowing down flooding when hulls are directly connected outside and highly pressurized.
/// </summary>
void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime)
{
checkedHulls.Clear();
checkedHulls.Add(hull);
foreach (var connectedGap in hull.ConnectedGaps)
{
if (connectedGap == this || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; }
var otherHull = connectedGap.GetOtherLinkedHull(hull);
if (otherHull == null) { continue; }
SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, hull, maxFlow, deltaTime);
}
}
static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, HashSet<Hull> checkedHulls, Hull originHull, float maxFlow, float deltaTime)
{
const float decay = 0.95f;
maxFlow = Math.Min(maxFlow, gap.GetWaterFlowFromOutside(targetHull, deltaTime, ignoreCurrentWater: true)) * decay;
if (maxFlow <= 0.001f) { return; }
checkedHulls.Add(targetHull);
//don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside
targetHull.WaterVolume += maxFlow;
//lerp lethal pressure up very fast
if (targetHull.WaterVolume > targetHull.Volume)
{
targetHull.LethalPressure = Math.Max(targetHull.LethalPressure, MathHelper.Lerp(targetHull.LethalPressure, originHull.LethalPressure, 0.1f));
}
//stop pushing water to the following hulls once we get to a hull that's not at high pressure yet
if (targetHull.LethalPressure <= 0 || targetHull.WaterVolume < targetHull.Volume) { return; }
foreach (var connectedGap in targetHull.ConnectedGaps)
{
if (connectedGap == gap || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; }
var otherHull = connectedGap.GetOtherLinkedHull(targetHull);
if (otherHull == null || checkedHulls.Contains(otherHull)) { continue; }
SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, originHull, maxFlow, deltaTime);
}
}
public bool RefreshOutsideCollider()
@@ -884,6 +966,8 @@ namespace Barotrauma
base.Remove();
GapList.Remove(this);
checkedHulls.Clear();
foreach (Hull hull in Hull.HullList)
{
hull.ConnectedGaps.Remove(this);
@@ -785,7 +785,7 @@ namespace Barotrauma
#region Shared network write
private void SharedStatusWrite(IWriteMessage msg)
{
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
msg.WriteSingle(waterVolume);
System.Diagnostics.Debug.Assert(FireSources.Count <= MaxFireSources, $"Too many fire sources ({FireSources.Count}) in hull {ID} (max {MaxFireSources}).");
msg.WriteRangedInteger(Math.Min(FireSources.Count, MaxFireSources), 0, MaxFireSources);
@@ -833,7 +833,7 @@ namespace Barotrauma
private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources)
{
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
newWaterVolume = msg.ReadSingle();
int fireSourceCount = msg.ReadRangedInteger(0, MaxFireSources);
newFireSources = new NetworkFireSource[fireSourceCount];
@@ -1269,6 +1269,23 @@ namespace Barotrauma
return null;
}
/// <summary>
/// Recursively find all the hulls linked to the specified hull.
/// </summary>
public void GetLinkedHulls(List<Hull> linkedHulls, bool includeHiddenHulls = false)
{
foreach (var linkedEntity in linkedTo)
{
if (linkedEntity is Hull linkedHull)
{
if (linkedHulls.Contains(linkedHull)) { continue; }
if (!includeHiddenHulls && linkedHull.IsHidden) { continue; }
linkedHulls.Add(linkedHull);
linkedHull.GetLinkedHulls(linkedHulls, includeHiddenHulls);
}
}
}
public static void DetectItemVisibility(Character c=null)
{
if (c==null)
@@ -4291,6 +4291,11 @@ namespace Barotrauma
{
placeableWrecks.RemoveAt(i);
}
// Exclude wrecks that have mission tags, those can't show up randomly
else if (wreckInfo.MissionTags.Count != 0)
{
placeableWrecks.RemoveAt(i);
}
}
if (placeableWrecks.None())
{
@@ -4742,6 +4747,11 @@ namespace Barotrauma
{
beaconStationFiles.RemoveAt(i);
}
// Exclude beacons that have mission tags, those can't show up randomly
else if (beaconInfo.MissionTags.Count != 0)
{
beaconStationFiles.RemoveAt(i);
}
}
}
if (beaconStationFiles.None())
@@ -4926,17 +4936,17 @@ namespace Barotrauma
{
int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
var humanSpawnPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Human);
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
pathPoints.Shuffle(Rand.RandSync.ServerAndClient);
if (corpsePoints.None() && humanSpawnPoints.None()) { continue; }
humanSpawnPoints.Shuffle(Rand.RandSync.ServerAndClient);
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
var usedJobs = new HashSet<JobPrefab>();
int spawnCounter = 0;
for (int j = 0; j < corpseCount; j++)
{
WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault();
WayPoint sp = corpsePoints.FirstOrDefault() ?? humanSpawnPoints.FirstOrDefault();
JobPrefab job = sp?.AssignedJob;
CorpsePrefab selectedPrefab;
if (job == null)
@@ -4949,8 +4959,8 @@ namespace Barotrauma
if (selectedPrefab == null)
{
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
humanSpawnPoints.Remove(sp);
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? humanSpawnPoints.FirstOrDefault(sp => sp.AssignedJob == null);
// Deduce the job from the selected prefab
selectedPrefab = GetCorpsePrefab(usedJobs);
if (selectedPrefab != null)
@@ -4972,7 +4982,7 @@ namespace Barotrauma
{
worldPos = sp.WorldPosition;
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
humanSpawnPoints.Remove(sp);
}
job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
@@ -473,7 +473,7 @@ namespace Barotrauma
{
get
{
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry) || m.ForceFailure);
return availableMissions;
}
}
@@ -1091,6 +1091,12 @@ namespace Barotrauma
mission.TimesAttempted = loadedMission.TimesAttempted;
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
var levelData = destination == this ? LevelData : Connections.FirstOrDefault(c => c.OtherLocation(this) == destination)?.LevelData;
if (levelData != null)
{
mission.AdjustLevelData(levelData);
}
}
loadedMissions = null;
}
@@ -39,7 +39,7 @@ namespace Barotrauma
{
get
{
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry) || m.ForceFailure);
return availableMissions;
}
}
@@ -235,15 +235,17 @@ namespace Barotrauma
//backwards compatibility (or support for loading maps created with mods that modify the end biome setup):
//if there's too few end locations, create more
int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count;
Location firstEndLocation = EndLocations[0];
Biome endBiome = firstEndLocation.Biome;
int missingOutpostCount = endBiome.EndBiomeLocationCount - endLocations.Count;
for (int i = 0; i < missingOutpostCount; i++)
{
Vector2 mapPos = new Vector2(
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, firstEndLocation.Biome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, endBiome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
newEndLocation.Biome = endBiome;
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
Locations.Add(newEndLocation);
endLocations.Add(newEndLocation);
@@ -11,6 +11,8 @@ namespace Barotrauma
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
public HashSet<Identifier> MissionTags { get; } = [];
[Serialize(0.0f, IsPropertySaveable.Yes), Editable]
public float MinLevelDifficulty { get; set; }
@@ -21,6 +23,10 @@ namespace Barotrauma
{
Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), []))
{
MissionTags.Add(missionTag);
}
}
public ExtraSubmarineInfo(SubmarineInfo submarineInfo)
@@ -41,11 +47,18 @@ namespace Barotrauma
kvp.Value.TrySetValue(this, kvp.Value.GetValue(original));
}
}
foreach (var missionTag in original.MissionTags)
{
MissionTags.Add(missionTag);
}
}
public virtual void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
// MissionTags is not automatically serialized because HashSet<Identifier> is not a supported type
// We need to manually serialize it as a comma-separated string
element.SetAttributeValue(nameof(MissionTags), string.Join(',', MissionTags));
}
}
@@ -135,18 +148,10 @@ namespace Barotrauma
[Serialize(50.0f, IsPropertySaveable.Yes), Editable]
public float PreferredDifficulty { get; set; }
private readonly HashSet<Identifier> missionTags = new HashSet<Identifier>();
public HashSet<Identifier> MissionTags => missionTags;
public EnemySubmarineInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element)
{
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), Array.Empty<Identifier>()))
{
missionTags.Add(missionTag);
}
}
public EnemySubmarineInfo(SubmarineInfo submarineInfo) : base(submarineInfo)
@@ -156,16 +161,8 @@ namespace Barotrauma
public EnemySubmarineInfo(EnemySubmarineInfo original) : base(original)
{
foreach (var missionTag in original.missionTags)
{
missionTags.Add(missionTag);
}
}
public override void Save(XElement element)
{
base.Save(element);
element.Add(new XAttribute(nameof(MissionTags), string.Join(',', missionTags)));
}
}
}
@@ -159,11 +159,11 @@ namespace Barotrauma
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
//...but only if the outpost is suitable for the mission (or if the mission has no specific requirements for the outpost)
if (outpostInfosSuitableForMission.None() ||
outpostInfosSuitableForMission.Any(outpostInfo => outpostInfo.OutpostTags.Contains(serverSettings.SelectedOutpostName)))
if (outpostInfosSuitableForMission.Contains(matchingOutpost) ||
outpostInfosSuitableForMission.None())
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
return matchingOutpost;
@@ -37,14 +37,14 @@ namespace Barotrauma
public bool RequiresUnlock { get; }
/// <summary>
/// Used when neither <see cref="MinAvailableAmount"/> or <see cref="MaxAvailableAmount"/> are defined.
/// Default minimum amount when no MinAvailableAmount is defined.
/// </summary>
private const int DefaultAmount = 5;
private const int DefaultMinAmount = 1;
/// <summary>
/// How much more the maximum stock is relative to the minimum stock if not defined. Stores will gradually stock up towards the maximum.
/// Default maximum amount when no MaxAvailableAmount is defined.
/// </summary>
private const float DefaultMaxAvailabilityRelativeToMin = 1.2f;
private const int DefaultMaxAmount = 5;
/// <summary>
/// If set, the item is only available in outposts with this faction.
@@ -66,11 +66,12 @@ namespace Barotrauma
public PriceInfo(XElement element)
{
Price = element.GetAttributeInt("buyprice", 0);
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
MinLevelDifficulty = GetMinLevelDifficulty(element, 0);
BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
CanBeBought = true;
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultAmount), CargoManager.MaxQuantity);
MaxAvailableAmount = MathHelper.Clamp(GetMaxAmount(element, defaultValue: (int)(MinAvailableAmount * DefaultMaxAvailabilityRelativeToMin)), MinAvailableAmount, CargoManager.MaxQuantity);
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultMinAmount), CargoManager.MaxQuantity);
int maxAmount = GetMaxAmount(element, defaultValue: DefaultMaxAmount);
MaxAvailableAmount = MathHelper.Clamp(maxAmount, MinAvailableAmount, CargoManager.MaxQuantity);
RequiresUnlock = element.GetAttributeBool("requiresunlock", false);
RequiredFaction = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount);
@@ -112,27 +113,27 @@ namespace Barotrauma
var priceInfos = new List<PriceInfo>();
defaultPrice = null;
int basePrice = element.GetAttributeInt("baseprice", 0);
int minAmount = GetMinAmount(element, defaultValue: DefaultAmount);
int maxAmount = GetMaxAmount(element, defaultValue: (int)(DefaultAmount * DefaultMaxAvailabilityRelativeToMin));
int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
int minAmount = GetMinAmount(element, defaultValue: DefaultMinAmount);
int maxAmount = GetMaxAmount(element, defaultValue: DefaultMaxAmount);
int minLevelDifficulty = GetMinLevelDifficulty(element, 0);
bool canBeSpecial = element.GetAttributeBool("canbespecial", true);
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
bool soldByDefault = GetSold(element, element.GetAttributeBool("soldbydefault", true));
bool requiresUnlock = element.GetAttributeBool("requiresunlock", false);
Identifier requiredFactionByDefault = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
foreach (XElement childElement in element.GetChildElements("price"))
{
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty);
bool sold = GetSold(childElement, soldByDefault);
int storeMinLevelDifficulty = GetMinLevelDifficulty(childElement, minLevelDifficulty);
float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier);
string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", "");
if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier))
{
backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}";
}
string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier);
string storeIdentifier = GetStoreIdentifier(childElement, backwardsCompatibleIdentifier);
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice),
canBeBought: sold,
@@ -167,12 +168,40 @@ namespace Barotrauma
return priceInfos;
}
private static int GetMinAmount(XElement element, int defaultValue) => element != null ?
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) :
defaultValue;
private static int GetMinAmount(XElement element, int defaultValue) =>
element?.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) ?? defaultValue;
private static int GetMaxAmount(XElement element, int defaultValue) => element != null ?
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) :
defaultValue;
private static int GetMaxAmount(XElement element, int defaultValue) =>
element?.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) ?? defaultValue;
public static bool HasMinAmountDefined(XElement element) => element != null &&
(element.GetAttribute("minamount") != null || element.GetAttribute("minavailable") != null);
public static bool HasMaxAmountDefined(XElement element) => element != null &&
(element.GetAttribute("maxamount") != null || element.GetAttribute("maxavailable") != null);
public static bool HasSoldDefined(XElement element) => element != null &&
element.GetAttribute("sold") != null;
public static string GetMinAmountString(XElement element)
{
if (element == null) { return null; }
return element.GetAttributeString("minamount", null) ?? element.GetAttributeString("minavailable", null);
}
public static string GetMaxAmountString(XElement element)
{
if (element == null) { return null; }
return element.GetAttributeString("maxamount", null) ?? element.GetAttributeString("maxavailable", null);
}
public static bool GetSold(XElement element, bool defaultValue = true) =>
element?.GetAttributeBool("sold", defaultValue) ?? defaultValue;
public static int GetMinLevelDifficulty(XElement element, int defaultValue = 0) =>
element?.GetAttributeInt("minleveldifficulty", defaultValue) ?? defaultValue;
public static string GetStoreIdentifier(XElement element, string defaultValue = "unknown") =>
element?.GetAttributeString("storeidentifier", defaultValue) ?? defaultValue;
}
}
@@ -343,6 +343,9 @@ namespace Barotrauma
}
/// <summary>
/// Is the sub at the depth where it starts to take damage to appear due to the pressure?
/// </summary>
public bool AtDamageDepth
{
get
@@ -352,6 +355,18 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the sub at the depth where cosmetic effects (e.g. camera shake) start to appear due to the pressure?
/// </summary>
public bool AtCosmeticDamageDepth
{
get
{
if (Level.Loaded == null || subBody == null) { return false; }
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth + SubmarineBody.CosmeticDamageEffectThreshold && RealWorldDepth > RealWorldCrushDepth + SubmarineBody.CosmeticDamageEffectThreshold;
}
}
public bool IsRespawnShuttle =>
GameMain.NetworkMember?.RespawnManager is { } respawnManager && respawnManager.RespawnShuttles.Contains(this);
@@ -579,13 +579,16 @@ namespace Barotrauma
Body.SetTransform(ConvertUnits.ToSimUnits(position), 0.0f);
}
/// <summary>
/// Camera shake and sounds start playing 500 meters before crush depth
/// </summary>
public const float CosmeticDamageEffectThreshold = -500.0f;
private void UpdateDepthDamage(float deltaTime)
{
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
if (Level.Loaded == null) { return; }
//camera shake and sounds start playing 500 meters before crush depth
const float CosmeticEffectThreshold = -500.0f;
//breaches won't get any more severe 500 meters below crush depth
const float MaxEffectThreshold = 500.0f;
const float MinWallDamageProbability = 0.1f;
@@ -598,7 +601,7 @@ namespace Barotrauma
//(gives you a bit of time to react and return if you start the round in a level that's too deep)
const float MinRoundDuration = 60.0f;
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
if (!Submarine.AtCosmeticDamageDepth)
{
return;
}
@@ -606,9 +609,9 @@ namespace Barotrauma
damageSoundTimer -= deltaTime;
if (damageSoundTimer <= 0.0f)
{
const float PressureSoundRange = -CosmeticEffectThreshold;
const float PressureSoundRange = -CosmeticDamageEffectThreshold;
//Ratio between 0 (where the 'approaching crush depth' indication starts) and 1 (at crush depth or past it)
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)) / PressureSoundRange, 0f, 1f);
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticDamageEffectThreshold)) / PressureSoundRange, 0f, 1f);
#if CLIENT
SoundPlayer.PlayDamageSound("pressure", MathHelper.Lerp(0f, 100f, closenessToCrushDepthRatio), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f, gain: 1f + closenessToCrushDepthRatio * 2);
#endif
@@ -1066,8 +1069,15 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; }
if (item.body.Mass > impulseMagnitude) { continue; }
if (item.Submarine != submarine) { continue; }
if (item.body is not { BodyType: BodyType.Dynamic })
{
if (!item.Prefab.ReceiveSubmarineImpacts) { continue; }
item.ReceiveImpact(impact, recursive: false);
}
if (!item.body.Enabled || item.CurrentHull == null || item.body.Mass > impulseMagnitude) { continue; }
item.body.ApplyLinearImpulse(impulse, 10.0f);
item.PositionUpdateInterval = 0.0f;
@@ -820,16 +820,22 @@ namespace Barotrauma
{
int prevPos = inc.BitPosition;
const int MaxBytesToLog = 500;
StringBuilder hexData = new();
inc.BitPosition = 0;
while (inc.BitPosition < inc.LengthBits)
while (inc.BitPosition < inc.LengthBits &&
inc.BytePosition < MaxBytesToLog)
{
byte b = inc.ReadByte();
hexData.Append($"{b:X2} ");
}
// trim the last space if there is one
if (hexData.Length > 0) { hexData.Length--; }
if (inc.BytePosition >= MaxBytesToLog)
{
hexData.Append($" (data truncated, {inc.LengthBytes} bytes in the full message)");
}
inc.BitPosition = prevPos;
//only log the error once per sender, so this can't be abused by spamming the server with malformed data to fill up the console with errors
@@ -19,7 +19,7 @@ namespace Barotrauma.Networking
return msg;
}
#endif
public static void WriteNetSerializableStruct(this IWriteMessage msg, INetSerializableStruct serializableStruct)
public static void WriteNetSerializableStruct<T>(this IWriteMessage msg, T serializableStruct) where T : INetSerializableStruct
{
serializableStruct.Write(msg);
}
@@ -94,6 +94,12 @@ namespace Barotrauma.Networking
public Option<int> RetriesLeft;
}
[NetworkSerialize]
internal readonly record struct DoSProtectionPacket(string EndpointStr, bool ShouldBan) : INetSerializableStruct
{
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr);
}
[NetworkSerialize]
internal readonly struct PeerDisconnectPacket : INetSerializableStruct
{
@@ -109,19 +115,24 @@ namespace Barotrauma.Networking
AdditionalInformation = additionalInformation;
}
public LocalizedString ChatMessage(Client c)
public LocalizedString ChatMessage(string? name)
{
if (string.IsNullOrEmpty(name))
{
name = TextManager.Get("ServerMessage.UnknownClient").Value;
}
LocalizedString message = DisconnectReason switch
{
DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer",
"[client]", c.Name),
DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", c.Name),
DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", c.Name),
"[client]", name),
DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", name),
DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", name),
_ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason",
("[client]", c.Name),
("[client]", name),
("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}")))
};
if (!string.IsNullOrEmpty(AdditionalInformation) &&
if (!string.IsNullOrEmpty(AdditionalInformation) &&
DisconnectReason is DisconnectReason.Banned or DisconnectReason.Kicked)
{
message += " "+ TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation);
@@ -89,8 +89,6 @@ namespace Barotrauma.Networking
private readonly Queue<LogMessage> lines;
private readonly Queue<LogMessage> unsavedLines;
private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
public int LinesPerFile
{
get { return linesPerFile; }
@@ -79,14 +79,13 @@ namespace Barotrauma
bool cleared = false;
foreach (var subElement in element.Elements())
{
if (elementNamesToRemove.Contains(subElement.NameAsIdentifier()))
{
if (!elementsToRemove.Contains(subElement)) { elementsToRemove.Add(subElement); }
matchingElementFound = true;
continue;
}
if (replacementSubElement.Name.ToString().Equals("clearall", StringComparison.OrdinalIgnoreCase))
{
if (elementNamesToRemove.Contains(subElement.NameAsIdentifier()))
{
if (!elementsToRemove.Contains(subElement)) { elementsToRemove.Add(subElement); }
matchingElementFound = true;
}
continue;
}
else if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase))
@@ -876,7 +876,18 @@ namespace Barotrauma
Dictionary<Identifier, SerializableProperty> dictionary = new Dictionary<Identifier, SerializableProperty>();
foreach (var property in properties)
{
var serializableProperty = new SerializableProperty(property);
//if the getter is private, we must get it from the declaring type to access it and check if it exists
SerializableProperty serializableProperty = null;
try
{
serializableProperty = new SerializableProperty(property);
}
catch (AmbiguousMatchException)
{
//can happen e.g. with AnimController.CurrentGroundedParams, which is of an abstract type -
//let's just ignore these types of properties (you can't really do anything with SerializableProperties that are reference types anyway)
continue;
}
dictionary.Add(serializableProperty.Name.ToIdentifier(), serializableProperty);
}
@@ -1066,6 +1077,15 @@ namespace Barotrauma
}
}
}
else if (attributeName == "unlockrecipe" || attributeName == "unlockrecipes")
{
var recipes = subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipes",
def: subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipe", ImmutableHashSet<Identifier>.Empty));
foreach (var recipe in recipes)
{
GameMain.GameSession?.UnlockRecipe(CharacterTeamType.Team1, recipe, showNotifications: false);
}
}
if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property))
{
@@ -696,7 +696,8 @@ namespace Barotrauma
root.Add(CampaignSettings.CurrentSettings.Save());
#endif
configDoc.SaveSafe(PlayerConfigPath);
//allow retrying a few times because the file may be in use if the player is running multiple instances of the game on the same machine
configDoc.SaveSafe(PlayerConfigPath, maxRetries: 4);
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
{
@@ -425,6 +425,11 @@ namespace Barotrauma
{
return PropertyMatchesRequirement(targetChar, characterProperty);
}
else if (targetChar?.AnimController?.SerializableProperties is { } animControllerProperties
&& animControllerProperties.TryGetValue(AttributeName, out var animControllerProperty))
{
return PropertyMatchesRequirement(targetChar.AnimController, animControllerProperty);
}
return ComparisonOperatorIsNotEquals;
case ConditionType.SkillRequirement:
if (targetChar != null)
@@ -420,7 +420,7 @@ namespace Barotrauma
[Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")]
public int Count { get; private set; }
[Serialize(false, IsPropertySaveable.No, description:
[Serialize(false, IsPropertySaveable.No, description:
"Should the buffs of the character executing the effect be transferred to the spawned character?"+
" Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
public bool TransferBuffs { get; private set; }
@@ -445,11 +445,11 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")]
public Identifier AfflictionOnSpawn { get; private set; }
[Serialize(1, IsPropertySaveable.No, description:
[Serialize(1, IsPropertySaveable.No, description:
$"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")]
public int AfflictionStrength { get; private set; }
[Serialize(false, IsPropertySaveable.No, description:
[Serialize(false, IsPropertySaveable.No, description:
"Should the player controlling the character that executes the effect gain control of the spawned character?" +
" Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
public bool TransferControl { get; private set; }
@@ -470,7 +470,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool InheritEventTags { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the character team be inherited from the entity that owns the status effect?")]
public bool InheritTeam { get; private set; }
@@ -722,9 +722,9 @@ namespace Barotrauma
private readonly List<(Identifier eventIdentifier, Identifier tag)> eventTargetTags;
/// <summary>
/// Can be used to make the effect unlock a fabrication recipe globally for the entire crew.
/// Can be used to make the effect unlock a fabrication recipe (or multiple recipes separated by a comma) globally for the entire crew.
/// </summary>
public readonly Identifier UnlockRecipe;
public readonly ImmutableHashSet<Identifier> UnlockRecipes;
private Character user;
@@ -740,6 +740,8 @@ namespace Barotrauma
/// </summary>
public readonly float SeverLimbsProbability;
private readonly Vector2 randomCondition;
public PhysicsBody sourceBody;
/// <summary>
@@ -802,11 +804,11 @@ namespace Barotrauma
private readonly List<Identifier> talentTriggers;
private readonly List<int> giveExperiences;
private readonly List<GiveSkill> giveSkills;
private HashSet<(Character targetCharacter, AnimLoadInfo anim)> failedAnimations;
public readonly record struct AnimLoadInfo(AnimationType Type, Either<string, ContentPath> File, float Priority, ImmutableArray<Identifier> ExpectedSpeciesNames);
private readonly List<AnimLoadInfo> animationsToTrigger;
/// <summary>
/// How long the effect runs (in seconds). Note that if <see cref="Stackable"/> is true,
/// there can be multiple instances of the effect running at a time.
@@ -903,9 +905,10 @@ namespace Barotrauma
}
SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f);
randomCondition = element.GetAttributeVector2("randomcondition", Vector2.Zero);
string[] targetTypesStr =
element.GetAttributeStringArray("target", null) ??
string[] targetTypesStr =
element.GetAttributeStringArray("target", null) ??
element.GetAttributeStringArray("targettype", Array.Empty<string>());
foreach (string s in targetTypesStr)
{
@@ -939,7 +942,10 @@ namespace Barotrauma
playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false);
#endif
UnlockRecipe = element.GetAttributeIdentifier(nameof(UnlockRecipe), Identifier.Empty);
UnlockRecipes =
element.GetAttributeIdentifierImmutableHashSet(nameof(UnlockRecipes),
//backwards compatibility
def: element.GetAttributeIdentifierImmutableHashSet("UnlockRecipe", ImmutableHashSet<Identifier>.Empty));
List<XAttribute> propertyAttributes = new List<XAttribute>();
propertyConditionals = new List<PropertyConditional>();
@@ -1014,7 +1020,7 @@ namespace Barotrauma
DebugConsole.AddWarning(
$"StatusEffect tags defined using the attribute 'tags' in StatusEffect ({parentDebugName}). "+
"Please use the attribute 'statuseffecttags' or 'settags' instead to make it more explicit whether the 'tags' attribute means the status effect's tags, or tags the effect is supposed to set. " +
"The game now assumes it means the status effect's tags.",
"The game now assumes it means the status effect's tags.",
contentPackage: element.ContentPackage);
#endif
}
@@ -1038,7 +1044,7 @@ namespace Barotrauma
//if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched.
propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase));
}
List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>();
foreach (XAttribute attribute in propertyAttributes)
{
@@ -1074,7 +1080,7 @@ namespace Barotrauma
dropItem = true;
break;
case "removecharacter":
removeCharacter = true;
removeCharacter = true;
containerForItemsOnCharacterRemoval = subElement.GetAttributeIdentifier("moveitemstocontainer", Identifier.Empty);
break;
case "breaklimb":
@@ -1132,7 +1138,7 @@ namespace Barotrauma
continue;
}
}
Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", nameof(afflictionInstance.Strength)));
// Deserializing the object normally might cause some unexpected side effects. At least it clamps the strength of the affliction, which we don't want here.
// Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here.
@@ -1166,10 +1172,10 @@ namespace Barotrauma
break;
case "spawnitem":
var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName);
if (newSpawnItem.ItemPrefab != null)
if (newSpawnItem.ItemPrefab != null)
{
spawnItems ??= new List<ItemSpawnInfo>();
spawnItems.Add(newSpawnItem);
spawnItems.Add(newSpawnItem);
}
break;
case "triggerevent":
@@ -1247,7 +1253,7 @@ namespace Barotrauma
Identifier[] expectedSpeciesNames = subElement.GetAttributeIdentifierArray("expectedspecies", Array.Empty<Identifier>());
animationsToTrigger ??= new List<AnimLoadInfo>();
animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray()));
break;
case "forcesay":
forceSayIdentifier = subElement.GetAttributeIdentifier("message", Identifier.Empty);
@@ -1294,7 +1300,7 @@ namespace Barotrauma
return conditionValue < 0.0f || (setValue && conditionValue <= 0.0f);
}
}
return false;
return randomCondition.X < 0f || randomCondition.Y < 0f;
}
public bool IncreasesItemCondition()
@@ -1306,7 +1312,7 @@ namespace Barotrauma
return conditionValue > 0.0f || (setValue && conditionValue > 0.0f);
}
}
return false;
return randomCondition.X > 0f || randomCondition.Y > 0f;
}
private bool ChangesItemCondition(Identifier propertyName, object value, out float conditionValue)
@@ -1383,7 +1389,7 @@ namespace Barotrauma
if (HasTargetType(TargetType.NearbyItems))
{
//optimization for powered components that can be easily fetched from Powered.PoweredList
if (TargetIdentifiers != null &&
if (TargetIdentifiers != null &&
TargetIdentifiers.Count == 1 &&
(TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent")))
{
@@ -1491,8 +1497,8 @@ namespace Barotrauma
{
owner = ownerItem.ParentInventory?.Owner;
}
if (owner is Item container)
{
if (owner is Item container)
{
if (pc.Type == PropertyConditional.ConditionType.HasTag)
{
//if we're checking for tags, just check the Item object, not the ItemComponents
@@ -1500,8 +1506,8 @@ namespace Barotrauma
}
else
{
if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
}
if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
}
}
if (owner is Character character && shouldShortCircuit(pc.Matches(character), out valueToReturn)) { return valueToReturn; }
}
@@ -1673,7 +1679,7 @@ namespace Barotrauma
PlaySound(entity, GetHull(entity), GetPosition(entity, targets, worldPosition));
}
#endif
return;
return;
}
if (Duration > 0.0f && !Stackable)
@@ -1743,7 +1749,7 @@ namespace Barotrauma
}
}
}
}
position += Offset;
position += Rand.Vector(Rand.Range(0.0f, RandomOffset));
@@ -1784,7 +1790,7 @@ namespace Barotrauma
}
for (int i = 0; i < targets.Count; i++)
{
if (targets[i] is not Item item) { continue; }
if (targets[i] is not Item item) { continue; }
for (int j = 0; j < useItemCount; j++)
{
if (item.Removed) { continue; }
@@ -1807,8 +1813,8 @@ namespace Barotrauma
{
for (int i = 0; i < targets.Count; i++)
{
if (targets[i] is Item item)
{
if (targets[i] is Item item)
{
foreach (var itemContainer in item.GetComponents<ItemContainer>())
{
foreach (var containedItem in itemContainer.Inventory.AllItemsMod)
@@ -1885,6 +1891,11 @@ namespace Barotrauma
if (target is Entity targetEntity)
{
if (targetEntity.Removed) { continue; }
if (targetEntity is Item targetItem && randomCondition != Vector2.Zero)
{
float newCondition = Rand.Range(randomCondition.X, randomCondition.Y);
targetItem.Condition = GetModifiedValue(targetItem.Condition, newCondition, deltaTime);
}
}
else if (target is Limb limb)
{
@@ -1893,10 +1904,7 @@ namespace Barotrauma
}
foreach (var (propertyName, value) in PropertyEffects)
{
if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property))
{
continue;
}
if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
ApplyToProperty(target, property, value, deltaTime);
}
}
@@ -2034,10 +2042,10 @@ namespace Barotrauma
}
}
}
TryTriggerAnimation(target, entity);
if (!forceSayIdentifier.IsEmpty)
if (!forceSayIdentifier.IsEmpty)
{
LocalizedString messageToSay = TextManager.Get(forceSayIdentifier).Fallback(forceSayIdentifier.Value);
@@ -2121,7 +2129,7 @@ namespace Barotrauma
foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos)
{
if (giveTalentInfo.GiveRandom)
{
{
// for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well
IEnumerable<Identifier> viableTalents = giveTalentInfo.TalentIdentifiers.Where(id => !targetCharacter.Info.UnlockedTalents.Contains(id) && !characterTalentTree.AllTalentIdentifiers.Contains(id));
if (viableTalents.None()) { continue; }
@@ -2142,8 +2150,8 @@ namespace Barotrauma
{
foreach ((Identifier eventId, Identifier tag) in eventTargetTags)
{
if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev)
{
if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev)
{
targets.Where(t => t is Entity).ForEach(t => ev.AddTarget(tag, (Entity)t));
}
}
@@ -2157,9 +2165,13 @@ namespace Barotrauma
fire.Size = new Vector2(FireSize, fire.Size.Y);
}
if (isNotClient && !UnlockRecipe.IsEmpty && GameMain.GameSession is { } gameSession)
if (isNotClient && UnlockRecipes.Any() && GameMain.GameSession is { } gameSession)
{
gameSession.UnlockRecipe(UnlockRecipe, showNotifications: true);
foreach (var unlockRecipe in UnlockRecipes)
{
Character targetCharacter = user ?? targets.Select(GetCharacterFromTarget).NotNull().FirstOrDefault();
gameSession.UnlockRecipe(targetCharacter?.TeamID ?? CharacterTeamType.Team1, unlockRecipe, showNotifications: true);
}
}
if (isNotClient && triggeredEvents != null && GameMain.GameSession?.EventManager is { } eventManager)
@@ -2168,7 +2180,7 @@ namespace Barotrauma
{
Event ev = eventPrefab.CreateInstance(eventManager.RandomSeed);
if (ev == null) { continue; }
eventManager.QueuedEvents.Enqueue(ev);
eventManager.QueuedEvents.Enqueue(ev);
if (ev is ScriptedEvent scriptedEvent)
{
if (!triggeredEventTargetTag.IsEmpty)
@@ -2198,7 +2210,7 @@ namespace Barotrauma
foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters)
{
var characters = new List<Character>();
CharacterTeamType? inheritedTeam = null;
if (characterSpawnInfo.InheritTeam)
{
@@ -2211,16 +2223,16 @@ namespace Barotrauma
_ => null
// Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory).
} ?? (isPvP ? CharacterTeamType.None : CharacterTeamType.Team1);
CharacterTeamType? GetTeamFromSubmarine(MapEntity e)
{
if (e.Submarine == null) { return null; }
// Don't allow team FriendlyNPC in outposts, because if you buy a spawner item (such as husk container) from the store and choose to get it immediately, it will be spawned in the outpost.
return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ?
return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ?
CharacterTeamType.Team1 : e.Submarine.TeamID;
}
}
for (int i = 0; i < characterSpawnInfo.Count; i++)
{
Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset,
@@ -2311,11 +2323,11 @@ namespace Barotrauma
Character.Controlled = newCharacter;
}
#elif SERVER
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != target) { continue; }
GameMain.Server.SetClientCharacter(c, newCharacter);
}
foreach (Client c in GameMain.Server.ConnectedClients)
{
if (c.Character != target) { continue; }
GameMain.Server.SetClientCharacter(c, newCharacter);
}
#endif
}
if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); }
@@ -2477,7 +2489,7 @@ namespace Barotrauma
position = entity.WorldPosition;
if (entity is Item it)
{
sourceBody ??=
sourceBody ??=
(entity as Item)?.body ??
(entity as Character)?.AnimController.Collider;
}
@@ -2532,7 +2544,7 @@ namespace Barotrauma
else if (parentItem != null)
{
rotation = PhysicsBody.TransformRotation(
-parentItem.RotationRad + chosenItemSpawnInfo.RotationRad,
-parentItem.RotationRad + chosenItemSpawnInfo.RotationRad,
dir: parentItem.FlippedX ? -1.0f : 1.0f);
}
break;
@@ -2758,29 +2770,17 @@ namespace Barotrauma
private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime)
{
if (disableDeltaTime || setValue) { deltaTime = 1.0f; }
if (value is int || value is float)
if (value is int or float)
{
float propertyValueF = property.GetFloatValue(target);
float newValue = GetModifiedValue(property.GetFloatValue(target), Convert.ToSingle(value), deltaTime);
if (property.PropertyType == typeof(float))
{
float floatValue = value is float single ? single : (int)value;
floatValue *= deltaTime;
if (!setValue)
{
floatValue += propertyValueF;
}
property.TrySetValue(target, floatValue);
property.TrySetValue(target, newValue);
return;
}
else if (property.PropertyType == typeof(int))
{
int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime);
if (!setValue)
{
intValue += (int)propertyValueF;
}
property.TrySetValue(target, intValue);
property.TrySetValue(target, (int)newValue);
return;
}
}
@@ -2792,6 +2792,13 @@ namespace Barotrauma
property.TrySetValue(target, value);
}
private float GetModifiedValue(float currValue, float newValue, float deltaTime)
{
if (setValue) { return newValue; }
if (disableDeltaTime) { deltaTime = 1f; }
return currValue + newValue * deltaTime;
}
public static void UpdateAll(float deltaTime)
{
UpdateAllProjSpecific(deltaTime);
@@ -2848,7 +2855,7 @@ namespace Barotrauma
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result);
}
}
foreach ((Identifier affliction, float amount) in element.Parent.ReduceAffliction)
{
Limb targetLimb = null;
@@ -2894,10 +2901,10 @@ namespace Barotrauma
element.Parent.TryTriggerAnimation(target, element.Entity);
}
element.Parent.ApplyProjSpecific(deltaTime,
element.Entity,
element.Targets,
element.Parent.GetHull(element.Entity),
element.Parent.ApplyProjSpecific(deltaTime,
element.Entity,
element.Targets,
element.Parent.GetHull(element.Entity),
element.Parent.GetPosition(element.Entity, element.Targets),
playSound: element.Timer >= element.Duration);
@@ -2970,7 +2977,7 @@ namespace Barotrauma
if (limb == null) { return; }
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
{
if (result.Afflictions != null &&
if (result.Afflictions != null &&
/* "affliction" is the affliction directly defined in the status effect (e.g. "5 internal damage (per second / per frame / however the effect is defined to run)"),
* "result" is how much we actually applied of that affliction right now (taking into account the elapsed time, resistances and such) */
result.Afflictions.FirstOrDefault(a => a.Prefab == limbAffliction.Prefab) is Affliction resultAffliction &&
@@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Diagnostics;
namespace Barotrauma
{
@@ -30,6 +29,8 @@ namespace Barotrauma
public LocalizedString NestedStr { get; private set; }
public readonly LocalizedString SanitizedString;
public bool Loaded => loaded;
#if CLIENT
private readonly GUIFont? font;
private readonly GUIComponentStyle? componentStyle;
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
#if CLIENT
using Barotrauma.Networking;
using Barotrauma.Steam;
@@ -99,7 +101,8 @@ namespace Barotrauma.IO
this System.Xml.Linq.XDocument doc,
string path,
System.Xml.Linq.SaveOptions saveOptions = System.Xml.Linq.SaveOptions.None,
bool throwExceptions = false)
bool throwExceptions = false,
int maxRetries = 0)
{
if (!Validation.CanWrite(path, false))
{
@@ -114,7 +117,21 @@ namespace Barotrauma.IO
}
return;
}
doc.Save(path, saveOptions);
for (int i = 0; i <= maxRetries; i++)
{
try
{
doc.Save(path, saveOptions);
break;
}
catch (IOException e)
{
if (i >= maxRetries) { throw; }
DebugConsole.NewMessage("Failed save XML document {" + e.Message + "}, retrying in 250 ms...");
Thread.Sleep(250);
}
}
}
public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false)
@@ -116,10 +116,23 @@ namespace Barotrauma
#if SERVER
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); }
#else
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
get
{
string tempFolder = Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp");
#if DEBUG
if (GameClient.MultiClientTestMode && GameMain.Client != null)
{
//append the name of the client to the download folder to avoid multiple clients
//from trying to download a file into the same path at the same time
tempFolder += "_" + GameMain.Client.Name;
}
#endif
return tempFolder;
}
#endif
}
public static void EnsureSaveFolderExists()
{
try