Release 1.10.5.0 - Autumn Update 2025
This commit is contained in:
@@ -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());
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+11
-8
@@ -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))
|
||||
|
||||
+7
@@ -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)); }
|
||||
|
||||
+2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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);
|
||||
}
|
||||
|
||||
+17
-6
@@ -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))
|
||||
|
||||
+21
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user