Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-12-14 11:56:39 -03:00
376 changed files with 7775 additions and 2879 deletions
@@ -91,6 +91,11 @@ namespace Barotrauma
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
/// <summary>
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// </summary>
public IEnumerable<Hull> VisibleHulls
{
get
@@ -353,7 +358,7 @@ namespace Barotrauma
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
if (inventory == null || !inventory.Container.DrawInventory) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
@@ -262,7 +262,8 @@ namespace Barotrauma
if (aiElements.Count == 0)
{
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.");
DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
contentPackage: c.Prefab?.ContentPackage);
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, false);
return;
@@ -311,7 +312,7 @@ namespace Barotrauma
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
@@ -321,6 +322,10 @@ namespace Barotrauma
}
private CharacterParams.AIParams _aiParams;
/// <summary>
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
/// </summary>
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
public CharacterParams.AIParams AIParams
{
get
@@ -330,7 +335,8 @@ namespace Barotrauma
_aiParams = Character.Params.AI;
if (_aiParams == null)
{
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.");
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.",
contentPackage: Character.Prefab.ContentPackage);
Enabled = false;
_aiParams = new CharacterParams.AIParams(null, Character.Params);
}
@@ -563,7 +569,7 @@ namespace Barotrauma
}
}
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -2503,7 +2509,8 @@ namespace Barotrauma
Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head);
if (mouthLimb == null)
{
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)");
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)",
contentPackage: Character.Prefab.ContentPackage);
State = AIState.Idle;
return;
}
@@ -2540,7 +2547,11 @@ namespace Barotrauma
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
item.AddDamage(Character,
item.WorldPosition,
new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed),
impulseDirection: Vector2.Zero,
deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
{
@@ -3090,7 +3101,10 @@ namespace Barotrauma
break;
}
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
valueModifier *=
targetMemory.Priority /
//sqrt = the further the target is, the less the distance matters
MathF.Sqrt(dist);
if (valueModifier > targetValue)
{
@@ -685,8 +685,8 @@ namespace Barotrauma
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
{
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -727,54 +727,51 @@ namespace Barotrauma
}
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(Tags.LightDivingGear))
{
var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
if (mask != null)
{
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
if (mask != null)
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
}
@@ -784,13 +781,11 @@ namespace Barotrauma
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
for (int i = 0; i < 2; i++)
foreach (Item item in Character.HeldItems)
{
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
@@ -1389,7 +1384,10 @@ namespace Barotrauma
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
bool isWitnessing =
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
@@ -1754,11 +1752,11 @@ namespace Barotrauma
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
if (!otherCharacter.CanSeeTarget(character)) { continue; }
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1796,7 +1794,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
{
@@ -1840,13 +1838,13 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (thief.Submarine != null)
@@ -1857,16 +1855,11 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
#if CLIENT
@@ -1877,7 +1870,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
{
if (TriggerSecurity(security.AIController as HumanAIController))
{
@@ -1898,6 +1891,10 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
if (humanAI.ObjectiveManager.GetObjective<AIObjectiveFindThieves>() is { } findThieves)
{
findThieves.InspectEveryone();
}
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
onAbort: () =>
@@ -1915,6 +1912,18 @@ namespace Barotrauma
}
}
public static void ApplyStealingReputationLoss(Item item)
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
}
}
// 0.225 - 0.375
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
@@ -197,17 +197,6 @@ namespace Barotrauma
}
}
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
{
if (objective != null)
@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCheckStolenItems : AIObjective
{
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
public override bool AllowOutsideSubmarine => false;
public override bool AllowInAnySub => false;
public float FindStolenItemsProbability = 1.0f;
enum State
{
GotoTarget,
Inspect,
Warn,
Done
}
private float inspectDelay;
private float warnDelay;
private State currentState;
public readonly Character TargetCharacter;
private AIObjectiveGoTo? goToObjective;
private readonly List<Item> stolenItems = new List<Item>();
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
base(character, objectiveManager, priorityModifier)
{
TargetCharacter = targetCharacter;
inspectDelay = 5.0f;
warnDelay = 5.0f;
}
public override bool IsLoop
{
get => false;
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
protected override bool CheckObjectiveSpecific() => false;
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
return Priority;
}
public void ForceComplete()
{
IsCompleted = true;
}
protected override void Act(float deltaTime)
{
switch (currentState)
{
case State.GotoTarget:
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
{
SpeakIfFails = false
};
},
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
currentState = State.Inspect;
stolenItems.Clear();
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
},
onAbandon: () =>
{
Abandon = true;
});
break;
case State.Inspect:
Inspect(deltaTime);
break;
case State.Warn:
Warn(deltaTime);
break;
}
}
private void Inspect(float deltaTime)
{
if (inspectDelay > 0.0f)
{
character.SelectCharacter(TargetCharacter);
inspectDelay -= deltaTime;
return;
}
if (stolenItems.Any() &&
Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability)
{
character.Speak(TextManager.Get("dialogcheckstolenitems.warn").Value);
currentState = State.Warn;
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
currentState = State.Done;
IsCompleted = true;
}
character.DeselectCharacter();
}
private void Warn(float deltaTime)
{
if (warnDelay > 0.0f)
{
warnDelay -= deltaTime;
return;
}
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
}
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
}
foreach (var item in stolenItems)
{
HumanAIController.ObjectiveManager.AddObjective(new AIObjectiveGetItem(character, item, objectiveManager, equip: false)
{
BasePriority = 10
});
}
currentState = State.Done;
IsCompleted = true;
}
}
}
@@ -1070,7 +1070,8 @@ namespace Barotrauma
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
Item ammunition = character.Inventory.FindItem(i =>
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
@@ -1089,6 +1090,9 @@ namespace Barotrauma
}
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
SeekAmmunition(ammunitionIdentifiers);
}
return false;
@@ -14,7 +14,7 @@ namespace Barotrauma
private int escapeProgress;
private bool isBeingWatched;
private bool shouldSwitchTeams;
private readonly bool shouldSwitchTeams;
const string EscapeTeamChangeIdentifier = "escape";
@@ -88,10 +88,12 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
foreach (var it in character.HeldItems)
{
handcuffs.Drop(character);
if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character))
{
it.Drop(character);
}
}
}
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
@@ -0,0 +1,152 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveFindThieves : AIObjectiveLoop<Character>
{
public override Identifier Identifier { get; set; } = "find thieves".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 1.0f;
const float DefaultInspectDistance = 200.0f;
/// <summary>
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
/// </summary>
public float InspectDistance = DefaultInspectDistance;
private float? overrideInspectProbability;
/// <summary>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
/// regardless if the target is inspected or not.
/// </summary>
public float InspectProbability
{
get
{
if (overrideInspectProbability.HasValue)
{
return overrideInspectProbability.Value;
}
if (GameMain.GameSession?.Campaign is { } campaign)
{
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
reputation.NormalizedValue);
}
}
return 0.2f;
}
}
/// <summary>
/// When did the character last inspect whether some other character has stolen items on them?
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private readonly float inspectionInterval = 120.0f;
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character)) { return false; }
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
{
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
{
return false;
}
}
return true;
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
return subObjectives.Any() ? 50 : 0;
}
public void InspectEveryone()
{
lastInspectionTimes.Clear();
overrideInspectProbability = 1.0f;
InspectDistance = DefaultInspectDistance * 2;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
{
checkStolenItemsObjective.ForceComplete();
lastInspectionTimes[target] = Timing.TotalTime;
}
return checkStolenItemsObjective;
}
private float checkVisibleStolenItemsTimer;
private const float CheckVisibleStolenItemsInterval = 5.0f;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (checkVisibleStolenItemsTimer > 0.0f)
{
checkVisibleStolenItemsTimer -= deltaTime;
return;
}
foreach (var target in Character.CharacterList)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
}
private bool IsValidTarget(Character target, Character character)
{
if (target == null || target.Removed) { return false; }
if (target.IsIncapacitated) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (target.Submarine != character.Submarine) { return false; }
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsArrested) { return false; }
return true;
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
{
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
newTargetTimer = Math.Max(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
@@ -178,7 +178,7 @@ namespace Barotrauma
if (!objectiveManager.IsOrder(this))
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
@@ -151,6 +151,7 @@ namespace Barotrauma
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -558,6 +559,9 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "findthieves":
newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
@@ -441,7 +441,8 @@ namespace Barotrauma
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}");
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}",
contentPackage: ContentPackage);
return null;
}
}
@@ -663,6 +664,13 @@ namespace Barotrauma
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
UseController = useController ?? other.UseController;
#if DEBUG
if (UseController && ConnectedController == null)
{
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
}
#endif
}
public Order WithOption(Identifier option)
@@ -712,7 +720,12 @@ namespace Barotrauma
public Order WithItemComponent(Item item, ItemComponent component = null)
{
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item));
Controller controller = null;
if (UseController)
{
controller = item?.FindController(tags: ControllerTags);
}
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
}
public Order WithWallSection(Structure wall, int? sectionIndex)
@@ -552,7 +552,8 @@ namespace Barotrauma
#if DEBUG
if (handlePos[i].LengthSquared() > ArmLength)
{
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)");
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)",
item.Prefab.ContentPackage);
}
#endif
HandIK(
@@ -10,6 +10,17 @@ namespace Barotrauma
{
class HumanoidAnimController : AnimController
{
private const float SteepestWalkableSlopeAngleDegrees = 50f;
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
private static readonly float SteepestWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
private static readonly float SlowlyWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
private const float MaxSpeedOnStairs = 1.7f;
private const float SteepSlopePushMagnitude = MaxSpeedOnStairs;
public override RagdollParams RagdollParams
{
get { return HumanRagdollParams; }
@@ -150,7 +161,7 @@ namespace Barotrauma
private readonly float movementLerp;
private float cprAnimTimer,cprPump;
private float cprAnimTimer, cprPumpTimer;
private float fallingProneAnimTimer;
const float FallingProneAnimDuration = 1.0f;
@@ -243,14 +254,17 @@ namespace Barotrauma
if (MainLimb == null) { return; }
levitatingCollider = !IsHanging;
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
(character.SelectedSecondaryItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
character.SelectedSecondaryItem?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
if (onGround && character.CanMove)
{
Crouching = false;
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
(character.SelectedSecondaryItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
character.SelectedSecondaryItem?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
{
Crouching = false;
}
ColliderIndex = Crouching && !swimming ? 1 : 0;
}
ColliderIndex = Crouching && !swimming ? 1 : 0;
//stun (= disable the animations) if the ragdoll receives a large enough impact
if (strongestImpact > 0.0f)
@@ -276,7 +290,7 @@ namespace Barotrauma
if (!character.CanMove)
{
if (fallingProneAnimTimer < FallingProneAnimDuration)
if (fallingProneAnimTimer < FallingProneAnimDuration && onGround)
{
fallingProneAnimTimer += deltaTime;
UpdateFallingProne(1.0f);
@@ -285,7 +299,12 @@ namespace Barotrauma
Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
if (Collider.Enabled)
{
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
Collider.Enabled = false;
}
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
@@ -386,6 +405,12 @@ namespace Barotrauma
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (Anim != Animation.CPR)
{
cprAnimTimer = 0.0f;
cprPumpTimer = 0.0f;
}
switch (Anim)
{
case Animation.Climbing:
@@ -487,10 +512,14 @@ namespace Barotrauma
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX;
bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X);
bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX;
float walkCycleMultiplier = 1.0f;
if (Stairs != null)
if (Stairs != null || slowedDownBySlope)
{
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y);
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y);
walkCycleMultiplier *= 1.5f;
}
@@ -572,6 +601,15 @@ namespace Barotrauma
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
if (Stairs == null && onSlopeTooSteepToClimb)
{
if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X))
{
targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude;
movement = targetMovement;
}
}
if (Stairs != null || onSlope)
{
torso.PullJointWorldAnchorB = new Vector2(
@@ -648,14 +686,6 @@ namespace Barotrauma
if (!onGround)
{
Vector2 move = torso.PullJointWorldAnchorB - torso.SimPosition;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
MoveLimb(limb, limb.SimPosition + move, 15.0f, true);
}
return;
}
@@ -758,7 +788,7 @@ namespace Barotrauma
{
footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f);
}
if (Stairs == null)
if (Stairs == null && !onSlopeThatMakesSlow)
{
footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y);
}
@@ -1318,14 +1348,14 @@ namespace Barotrauma
}
}
void UpdateFallingProne(float strength)
void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true)
{
if (strength <= 0.0f) { return; }
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
if (moveHands && head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
{
//if the head is moving, try to protect it with the hands
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -1347,7 +1377,7 @@ namespace Barotrauma
//make the torso tip over
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
if (!InWater)
if (moveTorso && !InWater)
{
//prefer tipping over in the same direction the torso is rotating
//or moving
@@ -1358,27 +1388,30 @@ namespace Barotrauma
}
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++)
if (moveLegs)
{
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float diff = torso.Rotation - thigh.Rotation;
if (MathUtils.IsValid(diff))
for (int i = 0; i < 2; i++)
{
float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
}
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float diff = torso.Rotation - thigh.Rotation;
if (MathUtils.IsValid(diff))
{
float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
}
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
diff = torso.Rotation - leg.Rotation;
if (MathUtils.IsValid(diff))
{
float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
diff = torso.Rotation - leg.Rotation;
if (MathUtils.IsValid(diff))
{
float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
}
}
}
}
@@ -1398,7 +1431,8 @@ namespace Barotrauma
Crouching = true;
Vector2 diff = target.SimPosition - character.SimPosition;
Vector2 offset = Vector2.UnitX * -Dir * 0.75f;
Vector2 diff = (target.SimPosition + offset) - character.SimPosition;
Limb targetHead = target.AnimController.GetLimb(LimbType.Head);
Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso);
if (targetTorso == null)
@@ -1412,7 +1446,23 @@ namespace Barotrauma
Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition;
targetMovement = new Vector2(diff.X, 0.0f);
const float CloseEnough = 0.1f;
if (Math.Abs(targetMovement.X) < CloseEnough)
{
targetMovement.X = 0.0f;
}
TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left;
//if the target's in some weird pose, we may not be able to flip it so it's facing up,
//so let's only try it once so we don't end up constantly flipping it
if (cprAnimTimer <= 0.0f && target.AnimController.Direction == TargetDir)
{
target.AnimController.Flip();
}
(target.AnimController as HumanoidAnimController)?.UpdateFallingProne(strength: 1.0f, moveHands: false, moveTorso: false);
head.Disabled = true;
torso.Disabled = true;
UpdateStanding();
@@ -1443,79 +1493,69 @@ namespace Barotrauma
}
}
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
if (cprAnimTimer > 15.0f && targetHead != null && head != null)
//Serverside code
if (GameMain.NetworkMember is not { IsClient: true })
{
float yPos = (float)Math.Sin(cprAnimTimer) * 0.2f;
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.3f + yPos);
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
if (targetHead != null && head != null)
{
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f);
head.PullJointEnabled = true;
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.2f));
torso.PullJointEnabled = true;
//Serverside code
if (GameMain.NetworkMember is not { IsClient: true })
{
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
}
else
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f));
torso.PullJointEnabled = true;
if (cprPumpTimer >= 1)
{
if (targetHead != null && head != null)
torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
cprPumpTimer = 0;
if (skill < CPRSettings.Active.DamageSkillThreshold)
{
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f);
head.PullJointEnabled = true;
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) },
stun: 0.0f,
playSound: true,
attackImpulse: Vector2.Zero,
attacker: null);
}
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f));
torso.PullJointEnabled = true;
if (cprPump >= 1)
//need to CPR for at least a couple of seconds before the target can be revived
//(reviving the target when the CPR has barely started looks strange)
if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true })
{
torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
cprPump = 0;
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
reviveChance *= 1f + cprBoost;
if (skill < CPRSettings.Active.DamageSkillThreshold)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) },
0.0f, true, 0.0f, attacker: null);
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
GameMain.LuaCs.Hook.Call("human.CPRSuccess", this);
}
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
else
{
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
GameMain.LuaCs.Hook.Call("human.CPRSuccess", this);
}
else
{
GameMain.LuaCs.Hook.Call("human.CPRFailed", this);
}
GameMain.LuaCs.Hook.Call("human.CPRFailed", this);
}
}
cprPump += deltaTime;
}
cprAnimTimer = (cprAnimTimer + deltaTime) % 17;
cprPumpTimer += deltaTime;
cprAnimTimer += deltaTime;
//got the character back into a non-critical state, increase medical skill
//BUT only if it has been more than 10 seconds since the character revived someone
@@ -1525,7 +1565,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive);
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1730,7 +1770,23 @@ namespace Barotrauma
targetAnchor += target.Submarine.SimPosition;
}
}
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f)
{
//there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of
//errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with
//different error messages that all include a different coordinate
string errorMsg =
$"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " +
$"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}.";
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
}
else
{
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
}
}
else
{
@@ -438,7 +438,18 @@ namespace Barotrauma
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this);
Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty);
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) ||
affliction is not AfflictionPrefabHusk matchingAffliction)
{
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!",
contentPackage: huskAppendage.ContentPackage);
}
else
{
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
}
}
}
}
@@ -562,6 +573,10 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
{
return;
}
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.Add(joint.Joint);
for (int i = 0; i < LimbJoints.Length; i++)
@@ -572,6 +587,21 @@ namespace Barotrauma
}
Array.Resize(ref LimbJoints, LimbJoints.Length + 1);
LimbJoints[LimbJoints.Length - 1] = joint;
bool checkLimbIndex(int index, string debugName)
{
if (index < 0 || index >= limbs.Length)
{
string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}.";
DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage);
if (jointParams.Element?.ContentPackage == GameMain.VanillaContent)
{
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
return false;
}
return true;
}
}
protected void AddLimb(LimbParams limbParams)
@@ -658,6 +688,13 @@ namespace Barotrauma
}
}
private enum LimbStairCollisionResponse
{
DontClimbStairs,
ClimbWithoutLimbCollision,
ClimbWithLimbCollision
}
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
@@ -700,37 +737,53 @@ namespace Barotrauma
}
else if (structure.StairDirection != Direction.None)
{
Stairs = null;
//don't collider with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; }
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return false; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return false; }
//---------------
//set stairs to that of the one dragging us
if (character.SelectedBy != null)
{
Stairs = character.SelectedBy.AnimController.Stairs;
}
else
Stairs = structure;
{
var collisionResponse = handleLimbStairCollision();
if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision)
{
Stairs = structure;
}
else
{
if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; }
if (Stairs == null)
return false;
return false;
}
}
LimbStairCollisionResponse handleLimbStairCollision()
{
//don't collide with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f)
{
return Stairs != structure
? LimbStairCollisionResponse.DontClimbStairs
: LimbStairCollisionResponse.ClimbWithoutLimbCollision;
}
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
return LimbStairCollisionResponse.ClimbWithLimbCollision;
}
}
lock (impactQueue)
@@ -1332,17 +1385,28 @@ namespace Barotrauma
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
const float LevitationSpeedMultiplier = 5f;
// If the character is walking down a slope, target a position that moves along it
float slopePull = 0f;
if (floorNormal.Y is > 0f and < 1f
&& Math.Sign(movement.X) == Math.Sign(floorNormal.X))
{
if (Stairs != null)
slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier;
}
if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
{
float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier;
if (Stairs != null && targetY < Collider.SimPosition.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
yVelocity = Math.Sign(yVelocity);
}
yVelocity -= slopePull * LevitationSpeedMultiplier;
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity);
}
}
else
@@ -1590,10 +1654,10 @@ namespace Barotrauma
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
private void RefreshFloorY(float deltaTime, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
PhysicsBody refBody = Collider;
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
@@ -1618,7 +1682,7 @@ namespace Barotrauma
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f);
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
Fixture standOnFloorFixture = null;
@@ -1697,7 +1761,7 @@ namespace Barotrauma
}
}
if (closestFraction == 1) //raycast didn't hit anything
if (closestFraction >= 1) //raycast didn't hit anything
{
floorNormal = Vector2.UnitY;
if (CurrentHull == null)
@@ -1,11 +1,12 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
{
public enum HitDetection
{
Distance,
@@ -391,7 +392,8 @@ namespace Barotrauma
element.GetAttribute("burndamage") != null ||
element.GetAttribute("bleedingdamage") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).",
contentPackage: element.ContentPackage);
}
//if level wall damage is not defined, default to the structure damage
@@ -414,12 +416,14 @@ namespace Barotrauma
AfflictionPrefab afflictionPrefab;
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.",
contentPackage: element.ContentPackage);
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.",
contentPackage: element.ContentPackage);
continue;
}
}
@@ -428,7 +432,8 @@ namespace Barotrauma
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab))
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.",
contentPackage: element.ContentPackage);
continue;
}
}
@@ -441,7 +446,7 @@ namespace Barotrauma
}
partial void InitProjSpecific(ContentXElement element);
public void ReloadAfflictions(XElement element, string parentDebugName)
public void ReloadAfflictions(ContentXElement element, string parentDebugName)
{
Afflictions.Clear();
foreach (var subElement in element.GetChildElements("affliction"))
@@ -450,13 +455,14 @@ namespace Barotrauma
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab))
{
DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".");
DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".",
contentPackage: element.ContentPackage);
continue;
}
affliction = afflictionPrefab.Instantiate(0.0f);
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null)
if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null)
{
affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f);
}
@@ -465,7 +471,7 @@ namespace Barotrauma
}
}
public void Serialize(XElement element)
public void Serialize(ContentXElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (var affliction in Afflictions)
@@ -477,7 +483,7 @@ namespace Barotrauma
}
}
public void Deserialize(XElement element, string parentDebugName)
public void Deserialize(ContentXElement element, string parentDebugName)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ReloadAfflictions(element, parentDebugName);
@@ -497,8 +503,9 @@ namespace Barotrauma
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
Vector2 impulseDirection = GetImpulseDirection(target as ISpatialEntity, worldPosition, SourceItem);
var attackResult = target?.AddDamage(attacker, worldPosition, this, impulseDirection, deltaTime, playSound) ?? new AttackResult();
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
var additionalEffectType = ActionType.OnUse;
if (targetCharacter != null && targetCharacter.IsDead)
@@ -606,7 +613,7 @@ namespace Barotrauma
float penetration = Penetration;
RangedWeapon weapon =
SourceItem?.GetComponent<RangedWeapon>() ??
SourceItem?.GetComponent<RangedWeapon>() ??
SourceItem?.GetComponent<Projectile>()?.Launcher?.GetComponent<RangedWeapon>();
float? penetrationValue = weapon?.Penetration;
if (penetrationValue.HasValue)
@@ -614,7 +621,8 @@ namespace Barotrauma
penetration += penetrationValue.Value;
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
Vector2 impulseDirection = GetImpulseDirection(targetLimb, worldPosition, SourceItem);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, impulseDirection, playSound, targetLimb, penetration);
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -666,6 +674,34 @@ namespace Barotrauma
return attackResult;
}
private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPosition, Item sourceItem)
{
Vector2 impulseDirection = Vector2.Zero;
if (target != null)
{
impulseDirection = target.WorldPosition - sourceWorldPosition;
}
if (sourceItem?.body != null && sourceItem.body.Enabled && sourceItem.body.LinearVelocity.LengthSquared() > 0.0f)
{
impulseDirection = sourceItem.body.LinearVelocity;
}
else
{
var projectileComponent = sourceItem?.GetComponent<Projectile>();
if (projectileComponent != null)
{
impulseDirection = new Vector2(MathF.Cos(SourceItem.Rotation), MathF.Sin(SourceItem.Rotation));
}
}
if (impulseDirection.LengthSquared() > 0.0001f)
{
impulseDirection = Vector2.Normalize(impulseDirection);
}
return impulseDirection;
}
public float AttackTimer { get; private set; }
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
@@ -70,10 +70,18 @@ namespace Barotrauma
}
foreach (var item in HeldItems)
{
if (item.body != null)
if (item.body == null) { continue; }
if (!enabled)
{
item.body.Enabled = enabled;
item.body.Enabled = false;
}
else if (item.GetComponent<Holdable>() is { IsActive: true })
{
//held items includes all items in hand slots
//we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
item.body.Enabled = true;
}
}
AnimController.Collider.Enabled = value;
}
@@ -939,10 +947,16 @@ namespace Barotrauma
{
var prevSelectedItem = _selectedItem;
_selectedItem = value;
if (value is not null)
{
CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value));
}
#if CLIENT
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
if (Controlled == this)
{
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
if (_selectedItem == null)
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
@@ -1101,6 +1115,15 @@ namespace Barotrauma
set { CharacterHealth.Unkillable = value; }
}
/// <summary>
/// Is the health interface available on this character? Can be used by status effects
/// </summary>
public bool UseHealthWindow
{
get { return CharacterHealth.UseHealthWindow; }
set { CharacterHealth.UseHealthWindow = value; }
}
public CampaignMode.InteractionType CampaignInteractionType;
public Identifier MerchantIdentifier;
@@ -1284,7 +1307,8 @@ namespace Barotrauma
{
if (!VariantOf.IsEmpty)
{
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!");
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!",
contentPackage: Prefab.ContentPackage);
}
if (characterInfo == null)
{
@@ -1408,7 +1432,8 @@ namespace Barotrauma
if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
{
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"");
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"",
contentPackage: Prefab.ContentPackage);
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
speciesName = nonHuskedSpeciesName;
@@ -1690,31 +1715,21 @@ namespace Barotrauma
GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
{
GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent);
}
public void GiveIdCardTags(IEnumerable<WayPoint> spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
{
if (info?.Job == null || spawnPoints == null) { return; }
if (info?.Job == null || spawnPoint == null) { return; }
foreach (Item item in Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() is not IdCard idCard) { continue; }
if (requireSpawnPointTagsNotGiven)
var idCard = item?.GetComponent<IdCard>();
if (idCard == null) { continue; }
//if the card belongs to someone else, don't add any tags.
//otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round
if (idCard.OwnerName != info.Name) { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
if (idCard.SpawnPointTagsGiven) { continue; }
item.AddTag(s);
}
foreach (var spawnPoint in spawnPoints)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
idCard.SpawnPointTagsGiven = true;
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
@@ -2299,40 +2314,40 @@ namespace Barotrauma
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false)
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false)
{
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing);
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null || target.Removed) { return false; }
if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; }
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
@@ -2361,13 +2376,13 @@ namespace Barotrauma
continue;
}
}
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; }
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
}
return false;
}
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
@@ -2378,38 +2393,41 @@ namespace Barotrauma
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//we're outside, the other character inside
else if (seeingEntity.Submarine == null)
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
}
return
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
return !IsBlocking(closestBody);
bool IsBlocking(Body body)
bool IsBlocking(Fixture f)
{
var body = f.Body;
if (body == null) { return false; }
if (body.UserData is Structure wall && wall.CastShadow)
if (body.UserData is Structure wall)
{
if (!wall.CastShadow && seeThroughWindows) { return false; }
return wall != target;
}
else if (body.UserData is Item item)
{
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
{
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
}
return item != target;
}
return true;
@@ -2504,9 +2522,21 @@ namespace Barotrauma
if (inventory.Owner is Item item)
{
if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = item.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!CanInteractWith(item))
{
//could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this
foreach (var linkedEntity in item.linkedTo)
{
if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; }
}
return false;
}
ItemContainer container = (inventory as ItemInventory)?.Container;
if (container != null)
{
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!container.DrawInventory) { return false; }
}
}
return true;
}
@@ -2771,9 +2801,17 @@ namespace Barotrauma
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
{
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
if (body != null)
{
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
}
}
}
@@ -2800,7 +2838,12 @@ namespace Barotrauma
public void DeselectCharacter()
{
if (SelectedCharacter == null) { return; }
SelectedCharacter.AnimController?.ResetPullJoints();
if (!SelectedCharacter.AllowInput)
{
//we cannot reset the pull joints if the target is conscious (moving on its own),
//that'd interfere with its animations
SelectedCharacter.AnimController?.ResetPullJoints();
}
SelectedCharacter = null;
}
@@ -3316,10 +3359,7 @@ namespace Barotrauma
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
}
if (IsRagdolled)
{
SetInput(InputType.Ragdoll, false, true);
}
SetInput(InputType.Ragdoll, false, IsRagdolled);
}
if (!wasRagdolled && IsRagdolled)
{
@@ -3577,6 +3617,8 @@ namespace Barotrauma
private void Despawn(bool createNetworkEvents = true)
{
if (!EnableDespawn) { return; }
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
@@ -3658,10 +3700,12 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
private void UpdateSoundRange(float deltaTime)
@@ -3676,6 +3720,7 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -3995,15 +4040,15 @@ namespace Barotrauma
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
}
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
{
return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null);
return ApplyAttack(attacker, worldPosition, attack, deltaTime, impulseDirection, playSound);
}
/// <summary>
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
/// </summary>
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
{
if (Removed)
{
@@ -4015,7 +4060,16 @@ namespace Barotrauma
Limb limbHit = targetLimb;
float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime;
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime;
Vector2 attackImpulse = Vector2.Zero;
if (Math.Abs(impulseMagnitude) > 0.0f)
{
impulseDirection = impulseDirection.LengthSquared() > 0.0001f ?
Vector2.Normalize(impulseDirection) :
Vector2.UnitX;
attackImpulse = impulseDirection * impulseMagnitude;
}
AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker);
IEnumerable<Affliction> attackAfflictions;
@@ -4144,12 +4198,12 @@ namespace Barotrauma
}
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f)
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2? attackImpulse = null, Character attacker = null, float damageMultiplier = 1f)
{
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier);
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse ?? Vector2.Zero, out _, attacker, damageMultiplier: damageMultiplier);
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
{
hitLimb = null;
@@ -4182,7 +4236,7 @@ namespace Barotrauma
CreatureMetrics.RecordKill(target.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
{
if (Removed) { return new AttackResult(); }
@@ -4215,18 +4269,17 @@ namespace Barotrauma
}
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
if (attackImpulse.LengthSquared() > 0.0f)
{
Vector2 diff = dir;
if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); }
Vector2 impulse = Vector2.Normalize(diff) * attackImpulse;
Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff);
hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
hitLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
var mainLimb = hitLimb.character.AnimController.MainLimb;
if (hitLimb != mainLimb)
{
// Always add force to mainlimb
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mainLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
bool wasDead = IsDead;
@@ -4325,19 +4378,16 @@ namespace Barotrauma
}
if (medicalDamage > 0)
{
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage);
}
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage);
}
void IncreaseSkillLevel(Identifier skill, float damage)
{
float attackerSkillLevel = attacker.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f);
}
}
@@ -4351,12 +4401,10 @@ namespace Barotrauma
{
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
}
if (medicalGain <= 0) { return; }
Identifier skill = new Identifier("medical");
float attackerSkillLevel = healer.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
if (medicalGain > 0)
{
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
}
}
/// <summary>
@@ -4991,8 +5039,10 @@ namespace Barotrauma
private readonly List<Hull> visibleHulls = new List<Hull>();
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
/// <summary>
/// Returns hulls that are visible to the player, including the current hull.
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// Can be heavy if used every frame.
/// </summary>
public List<Hull> GetVisibleHulls()
@@ -5006,7 +5056,9 @@ namespace Barotrauma
float maxDistance = 1000f;
foreach (var hull in adjacentHulls)
{
if (hull.ConnectedGaps.Any(g => g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) &&
if (hull.ConnectedGaps.Any(g =>
g.Open > 0.9f &&
g.linkedTo.Contains(CurrentHull) &&
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
{
if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2))
@@ -5047,7 +5099,7 @@ namespace Barotrauma
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer");
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer") || HasJob("outpostsecurityofficer");
public bool IsAssistant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool IsVip => HasJob("prisoner");
@@ -5566,4 +5618,12 @@ namespace Barotrauma
public Character Character { get; set; }
}
class AbilityItemSelected : AbilityObject, IAbilityItem
{
public AbilityItemSelected(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
}
@@ -767,7 +767,7 @@ namespace Barotrauma
}
// Used for loading the data
public CharacterInfo(XElement infoElement, Identifier npcIdentifier = default)
public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default)
{
ID = idCounter;
idCounter++;
@@ -1214,6 +1214,21 @@ namespace Barotrauma
return (int)(salary * Job.Prefab.PriceMultiplier);
}
/// <summary>
/// Increases the characters skill at a rate proportional to their current skill.
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
/// </summary>
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
{
float skillLevel = Job.GetSkillLevel(skillIdentifier);
// The formula is too generous on low skill levels, hence the minimum divider.
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
}
/// <summary>
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
/// </summary>
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1222,9 +1237,7 @@ namespace Barotrauma
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
@@ -1311,12 +1324,12 @@ namespace Barotrauma
OnExperienceChanged(prevAmount, ExperiencePoints);
}
const int BaseExperienceRequired = -50;
const int BaseExperienceRequired = 450;
const int AddedExperienceRequiredPerLevel = 500;
public int GetTotalTalentPoints()
{
return GetCurrentLevel() + AdditionalTalentPoints - 1;
return GetCurrentLevel() + AdditionalTalentPoints;
}
public int GetAvailableTalentPoints()
@@ -1342,16 +1355,19 @@ namespace Barotrauma
return experienceRequired + ExperienceRequiredPerLevel(level);
}
/// <summary>
/// How much more experience does the character need to reach the specified level?
/// </summary>
public int GetExperienceRequiredForLevel(int level)
{
int currentLevel = GetCurrentLevel(out int experienceRequired);
int currentLevel = GetCurrentLevel();
if (currentLevel >= level) { return 0; }
int required = experienceRequired;
for (int i = currentLevel + 1; i <= level; i++)
int required = 0;
for (int i = 0; i < level; i++)
{
required += ExperienceRequiredPerLevel(i);
}
return required;
return required - ExperiencePoints;
}
public int GetCurrentLevel()
@@ -1361,7 +1377,7 @@ namespace Barotrauma
private int GetCurrentLevel(out int experienceRequired)
{
int level = 1;
int level = 0;
experienceRequired = 0;
while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints)
{
@@ -1899,13 +1915,30 @@ namespace Barotrauma
}
}
/// <summary>
/// Get the combined stat value of the identifier "all" and the specified identifier.
/// </summary>
/// <remarks>
/// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but
/// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent
/// that increases the fabrication quality of every single item we could use something like:
/// <CharacterAbilityGivePermanentStat stattype="IncreaseFabricationQuality" statidentifier="all" />
/// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code)
/// </remarks>
public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
GetSavedStatValue(statType, statIdentifier);
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot));
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
foreach (Character bot in bots)
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
@@ -95,7 +95,8 @@ namespace Barotrauma
name = ParseName(mainElement, file);
if (name == Identifier.Empty)
{
DebugConsole.ThrowError($"No species name defined for: {file.Path}");
DebugConsole.ThrowError($"No species name defined for: {file.Path}",
contentPackage: file.ContentPackage);
return false;
}
return true;
@@ -75,7 +75,8 @@ namespace Barotrauma
HuskPrefab = prefab as AfflictionPrefabHusk;
if (HuskPrefab == null)
{
DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!");
DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!",
contentPackage: prefab.ContentPackage);
}
}
@@ -197,7 +198,7 @@ namespace Barotrauma
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
character.LastDamageSource = null;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force));
}
}
@@ -205,7 +206,7 @@ namespace Barotrauma
{
if (huskAppendage == null && character.Params.UseHuskAppendage)
{
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk);
}
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
@@ -285,13 +286,14 @@ namespace Barotrauma
if (prefab == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.",
contentPackage: Prefab.ContentPackage);
yield return CoroutineStatus.Success;
}
XElement parentElement = new XElement("CharacterInfo");
XElement infoElement = character.Info?.Save(parentElement);
CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement);
CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(new ContentXElement(Prefab.ContentPackage, infoElement));
if (huskCharacterInfo != null)
{
@@ -372,31 +374,28 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction))
{
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!");
return appendage;
}
Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!",
contentPackage: matchingAffliction.ContentPackage);
return appendage;
}
var mainElement = huskPrefab.ConfigElement;
var element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier);
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == matchingAffliction.Identifier);
}
if (element == null)
{
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!",
contentPackage: matchingAffliction.ContentPackage);
return appendage;
}
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
@@ -170,11 +170,13 @@ namespace Barotrauma
if (DormantThreshold > ActiveThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})");
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})",
contentPackage: element.ContentPackage);
}
if (ActiveThreshold > TransitionThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})");
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})",
contentPackage: element.ContentPackage);
}
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
@@ -440,13 +442,15 @@ namespace Barotrauma
AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None);
if (flagType is AbilityFlags.None)
{
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".");
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".",
contentPackage: element.ContentPackage);
continue;
}
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.");
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.",
contentPackage: element.ContentPackage);
break;
}
}
@@ -537,14 +541,16 @@ namespace Barotrauma
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.");
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.",
contentPackage: element.ContentPackage);
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.");
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.",
contentPackage: element.ContentPackage);
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
@@ -953,7 +959,8 @@ namespace Barotrauma
AfflictionOverlay = new Sprite(subElement);
break;
case "statvalue":
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.");
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.",
contentPackage: element.ContentPackage);
break;
case "effect":
case "periodiceffect":
@@ -962,7 +969,8 @@ namespace Barotrauma
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})");
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})",
contentPackage: element.ContentPackage);
break;
}
}
@@ -1018,6 +1026,10 @@ namespace Barotrauma
}
}
/// <summary>
/// Removes all the effects of the prefab (including the sounds and other assets defined in them).
/// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again!
/// </summary>
public static void ClearAllEffects()
{
Prefabs.ForEach(p => p.ClearEffects());
@@ -1046,7 +1058,8 @@ namespace Barotrauma
var b = effects[j];
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
{
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.",
ContentPackage);
}
}
}
@@ -54,7 +54,8 @@ namespace Barotrauma
case "vitalitymultiplier":
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.");
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.",
contentPackage: element.ContentPackage);
continue;
}
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
@@ -66,7 +67,8 @@ namespace Barotrauma
VitalityMultipliers.Add(vitalityMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier))
{
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?");
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?",
contentPackage: element.ContentPackage);
}
}
}
@@ -79,13 +81,15 @@ namespace Barotrauma
VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier))
{
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?");
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?",
contentPackage: element.ContentPackage);
}
}
}
if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
{
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!");
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!",
contentPackage: element.ContentPackage);
}
break;
}
@@ -1325,6 +1329,8 @@ namespace Barotrauma
public void Remove()
{
RemoveProjSpecific();
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();
@@ -79,12 +79,13 @@ namespace Barotrauma
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true)
public DamageModifier(ContentXElement element, string parentDebugName, bool checkErrors = true)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
if (element.GetAttribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.",
contentPackage: element.ContentPackage);
}
if (checkErrors)
{
@@ -108,12 +109,12 @@ namespace Barotrauma
}
}
static void createWarningOrError(string msg)
void createWarningOrError(string msg)
{
#if DEBUG
DebugConsole.ThrowError(msg);
DebugConsole.ThrowError(msg, contentPackage: element.ContentPackage);
#else
DebugConsole.AddWarning(msg);
DebugConsole.AddWarning(msg, contentPackage: element.ContentPackage);
#endif
}
}
@@ -117,8 +117,8 @@ namespace Barotrauma
public XElement Element { get; protected set; }
public readonly List<(XElement element, float commonness)> ItemSets = new List<(XElement element, float commonness)>();
public readonly List<(XElement element, float commonness)> CustomCharacterInfos = new List<(XElement element, float commonness)>();
public readonly List<(ContentXElement element, float commonness)> ItemSets = new List<(ContentXElement element, float commonness)>();
public readonly List<(ContentXElement element, float commonness)> CustomCharacterInfos = new List<(ContentXElement element, float commonness)>();
public readonly Identifier NpcSetIdentifier;
@@ -196,7 +196,7 @@ namespace Barotrauma
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element;
if (spawnItems != null)
{
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
foreach (ContentXElement itemElement in spawnItems.GetChildElements("item"))
{
int amount = itemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
@@ -239,14 +239,15 @@ namespace Barotrauma
return characterInfo;
}
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.",
contentPackage: itemElement?.ContentPackage);
return;
}
Item item = new Item(itemPrefab, character.Position, null);
@@ -301,7 +302,7 @@ namespace Barotrauma
wifiComponent.TeamID = character.TeamID;
}
parentItem?.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
foreach (ContentXElement childItemElement in itemElement.Elements())
{
int amount = childItemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
@@ -47,13 +47,14 @@ namespace Barotrauma
}
}
public Job(XElement element)
public Job(ContentXElement element)
{
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
JobPrefab p;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.",
contentPackage: element.ContentPackage);
p = JobPrefab.Random(Rand.RandSync.Unsynced);
}
else
@@ -43,7 +43,8 @@ namespace Barotrauma
Priority = element.GetAttributeFloat("priority", -1f);
if (Priority < 0)
{
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.");
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.",
ContentPackage);
}
}
@@ -266,7 +266,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}");
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}",
contentPackage: characterPrefab.ContentPackage);
}
return a;
}
@@ -63,7 +63,8 @@ namespace Barotrauma
{
if (!character.AnimController.CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!");
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!",
contentPackage: character.Prefab.ContentPackage);
return false;
}
return true;
@@ -136,6 +136,12 @@ namespace Barotrauma
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
/// <summary>
/// Parameters for EnemyAIController. Not used by HumanAIController.
/// </summary>
/// <returns>
/// AIParams or null. Use <see cref="EnemyAIController.AIParams"/>, if you don't expect nulls.
/// </returns>
public AIParams AI { get; private set; }
public CharacterParams(CharacterFile file)
@@ -559,7 +565,8 @@ namespace Barotrauma
DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+
"Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+
"but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.");
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.",
element.ContentPackage);
CrushDepth = newCrushDepth;
}
}
@@ -602,7 +609,8 @@ namespace Barotrauma
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
if (Element == null) { return; }
identifier ??= "";
var element = CreateElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
@@ -711,7 +719,8 @@ namespace Barotrauma
if (HasTag(tag))
{
target = null;
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!",
targetElement.ContentPackage);
return false;
}
else
@@ -730,6 +739,11 @@ namespace Barotrauma
public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams)
{
if (Element == null)
{
targetParams = null;
return false;
}
var element = TargetParams.CreateNewElement(Character, tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
@@ -84,12 +84,14 @@ namespace Barotrauma
doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.");
DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.",
contentPackage: file.ContentPackage);
return false;
}
if (MainElement == null)
{
DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.");
DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.",
contentPackage: file.ContentPackage);
return false;
}
IsLoaded = Deserialize(MainElement);
@@ -106,7 +106,8 @@ namespace Barotrauma
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'",
contentPackage: contentPackage);
return string.Empty;
}
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
@@ -183,7 +184,8 @@ namespace Barotrauma
}
if (error != null)
{
DebugConsole.ThrowError(error);
DebugConsole.ThrowError(error,
contentPackage: prefab?.ContentPackage);
}
}
if (selectedFile == null)
@@ -444,7 +446,8 @@ namespace Barotrauma
{
if (source.MainElement == null)
{
DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!");
DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!",
contentPackage: source.MainElement?.ContentPackage);
return;
}
Deserialize(source.MainElement, alsoChildren: false);
@@ -453,7 +456,8 @@ namespace Barotrauma
// TODO: cannot currently undo joint/limb deletion.
if (sourceSubParams.Count != subParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.",
contentPackage: source.MainElement?.ContentPackage);
return;
}
for (int i = 0; i < subParams.Count; i++)
@@ -461,7 +465,8 @@ namespace Barotrauma
var subSubParams = subParams[i].SubParams;
if (subSubParams.Count != sourceSubParams[i].SubParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.",
contentPackage: source.MainElement?.ContentPackage);
return;
}
subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false);
@@ -890,14 +895,14 @@ namespace Barotrauma
#if CLIENT
public DecorativeSprite DecorativeSprite { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element);
@@ -985,7 +990,8 @@ namespace Barotrauma
deformation = new PositionalDeformationParams(deformationElement);
break;
default:
DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'");
DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'",
contentPackage: element.ContentPackage);
break;
}
if (deformation != null)
@@ -1000,14 +1006,14 @@ namespace Barotrauma
#if CLIENT
public Dictionary<SpriteDeformationParams, XElement> Deformations { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value));
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value));
@@ -1098,14 +1104,14 @@ namespace Barotrauma
}
#if CLIENT
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
LightSource.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
LightSource.Serialize(element ?? Element);
@@ -1130,14 +1136,14 @@ namespace Barotrauma
Attack = new Attack(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Attack.Serialize(element ?? Element);
@@ -1182,14 +1188,14 @@ namespace Barotrauma
DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DamageModifier.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
DamageModifier.Serialize(element ?? Element);
@@ -1218,7 +1224,7 @@ namespace Barotrauma
public virtual string Name { get; set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public ContentXElement Element { get; set; }
public XElement OriginalElement { get; protected set; }
public ContentXElement OriginalElement { get; protected set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public RagdollParams Ragdoll { get; private set; }
@@ -1230,14 +1236,14 @@ namespace Barotrauma
public SubParam(ContentXElement element, RagdollParams ragdoll)
{
Element = element;
OriginalElement = new XElement(element);
OriginalElement = new ContentXElement(element.ContentPackage, element);
Ragdoll = ragdoll;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool Deserialize(XElement element = null, bool recursive = true)
public virtual bool Deserialize(ContentXElement element = null, bool recursive = true)
{
element = element ?? Element;
element ??= Element;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (recursive)
{
@@ -1246,9 +1252,9 @@ namespace Barotrauma
return SerializableProperties != null;
}
public virtual bool Serialize(XElement element = null, bool recursive = true)
public virtual bool Serialize(ContentXElement element = null, bool recursive = true)
{
element = element ?? Element;
element ??= Element;
SerializableProperty.SerializeProperties(this, element, true);
if (recursive)
{
@@ -100,6 +100,13 @@ namespace Barotrauma
set;
}
[Serialize(1.5f, IsPropertySaveable.Yes)]
public float SkillIncreaseExponent
{
get;
set;
}
public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier())
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -13,7 +13,7 @@ namespace Barotrauma.Abilities
public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement)
{
this.characterTalent = characterTalent;
this.characterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent));
character = characterTalent.Character;
invert = conditionElement.GetAttributeBool("invert", false);
}
@@ -40,7 +40,8 @@ namespace Barotrauma.Abilities
{
if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent.Prefab.ContentPackage);
}
targetTypes.Add(targetType);
}
@@ -1,7 +1,4 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Abilities
{
@@ -34,7 +34,8 @@ namespace Barotrauma.Abilities
string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any");
if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype))
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.",
contentPackage: conditionElement.ContentPackage);
}
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -11,13 +10,19 @@ namespace Barotrauma.Abilities
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
/// <summary>
/// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to true, except in the case of <see cref="AbilityConditionHasPermanentStat"/>, which by default targets the character who has the talent.
/// </summary>
private readonly bool targetAbilityTarget = false;
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements())
foreach (ContentXElement subElement in conditionElement.Elements())
{
if (subElement.NameAsIdentifier() == "conditional")
{
@@ -25,29 +30,47 @@ namespace Barotrauma.Abilities
}
}
if (!targetTypes.Any() && !conditionals.Any())
//don't log this error if this is a subclass of AbilityConditionCharacter
//(in that case not having any conditionals here is ok)
if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter))
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.",
contentPackage: conditionElement.ContentPackage);
}
targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
public sealed override bool MatchesCondition()
{
if (abilityObject is IAbilityCharacter abilityCharacter)
//by default data-reliant conditions don't accept null, but in this case it's ok,
//because we can assume it's the character who has the talent
return MatchesCondition(abilityObject: null);
}
public sealed override bool MatchesCondition(AbilityObject abilityObject)
{
return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
}
protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? character :
character;
if (targetCharacter is null) { return false; }
if (!IsViableTarget(targetTypes, targetCharacter)) { return false; }
foreach (var conditional in conditionals)
{
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
if (!conditional.Matches(character)) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
return false;
if (!conditional.Matches(targetCharacter)) { return false; }
}
return MatchesCharacter(targetCharacter);
}
protected virtual bool MatchesCharacter(Character character)
{
return true;
}
}
}
@@ -1,6 +1,6 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter
{
private readonly Identifier identifier;
@@ -9,11 +9,9 @@ namespace Barotrauma.Abilities
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
return character != null &&!character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -2,15 +2,13 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
return character is { IsUnconscious: true };
}
}
}
@@ -17,13 +17,15 @@ namespace Barotrauma.Abilities
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}");
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}",
contentPackage: characterTalent.Prefab.ContentPackage);
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.");
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.",
contentPackage: characterTalent.Prefab.ContentPackage);
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
@@ -19,7 +19,8 @@ namespace Barotrauma.Abilities
if (identifiers.None() && tags.None() && category == MapEntityCategory.None)
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.",
contentPackage: conditionElement.ContentPackage);
}
}
@@ -0,0 +1,19 @@
using Barotrauma.Items.Components;
namespace Barotrauma.Abilities
{
class AbilityConditionItemIsStatic : AbilityConditionData
{
public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityItem { Item: var item })
{
return item.GetComponent<Holdable>() is null && item.GetComponent<Wearable>() is null;
}
return false;
}
}
}
@@ -22,7 +22,8 @@ namespace Barotrauma.Abilities
{
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.",
contentPackage: conditionElement.ContentPackage);
}
continue;
}
@@ -1,6 +1,8 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
class AbilityConditionHasPermanentStat : AbilityConditionCharacter
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
@@ -12,7 +14,8 @@
statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!",
contentPackage: conditionElement.ContentPackage);
}
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
@@ -20,8 +23,13 @@
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
if (character?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return false;
}
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
@@ -13,7 +13,8 @@ namespace Barotrauma.Abilities
tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty);
if (tag.IsEmpty)
{
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.");
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.",
characterTalent.Prefab.ContentPackage);
}
}
@@ -2,21 +2,18 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter
{
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (crew == character) { continue; }
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
if (otherCharacter == character) { continue; }
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
}
return true;
}
}
@@ -26,7 +26,7 @@ namespace Barotrauma.Abilities
public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement)
{
CharacterAbilityGroup = characterAbilityGroup;
CharacterAbilityGroup = characterAbilityGroup ?? throw new ArgumentNullException(nameof(characterAbilityGroup));
CharacterTalent = characterAbilityGroup.CharacterTalent;
Character = CharacterTalent.Character;
RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true);
@@ -59,7 +59,8 @@ namespace Barotrauma.Abilities
protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
}
public void ApplyAbilityEffect(AbilityObject abilityObject)
@@ -76,17 +77,20 @@ namespace Barotrauma.Abilities
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}",
CharacterTalent.Prefab.ContentPackage);
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}",
CharacterTalent.Prefab.ContentPackage);
}
protected void LogAbilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
}
// XML
@@ -99,13 +103,18 @@ namespace Barotrauma.Abilities
abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (abilityType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")");
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")",
contentPackage: abilityElement.ContentPackage);
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e);
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e,
contentPackage: abilityElement.ContentPackage);
}
return null;
}
@@ -118,7 +127,8 @@ namespace Barotrauma.Abilities
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException);
DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException,
contentPackage: abilityElement.ContentPackage);
return null;
}
@@ -30,7 +30,8 @@ namespace Barotrauma.Abilities
}
else
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.",
contentPackage: abilityElement.ContentPackage);
}
}
}
@@ -21,7 +21,8 @@ namespace Barotrauma.Abilities
JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList);
if (apprenticeJob is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -14,7 +14,8 @@
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -16,7 +16,8 @@
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -27,7 +28,8 @@
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".");
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
float strength = this.strength;
@@ -14,29 +14,35 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
if (amount == 0 && level == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.",
contentPackage: abilityElement.ContentPackage);
}
if (amount > 0 && level > 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.",
contentPackage: abilityElement.ContentPackage);
}
}
private void ApplyEffectSpecific(Character targetCharacter)
{
if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
if (level > 0)
{
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level));
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level) + amount);
}
else if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
if (abilityObject is AbilityCharacterKill { Killer: { } killer })
{
ApplyEffectSpecific(killer);
}
else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -7,12 +7,14 @@ namespace Barotrauma.Abilities
private readonly ItemTalentStats stat;
private readonly float value;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -27,7 +29,7 @@ namespace Barotrauma.Abilities
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -10,6 +10,7 @@ namespace Barotrauma.Abilities
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
@@ -17,6 +18,7 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -44,7 +46,7 @@ namespace Barotrauma.Abilities
if (item.Submarine?.TeamID != Character.TeamID) { continue; }
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -14,7 +14,8 @@
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -1,4 +1,6 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
@@ -19,7 +21,12 @@
private readonly bool setValue;
private readonly PermanentStatPlaceholder placeholder;
//private readonly float maximumValue;
/// <summary>
/// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to false (= targets the character who has the talent).
/// </summary>
private readonly bool targetAbilityTarget = false;
public override bool AllowClientSimulation => true;
public override bool AppliesEffectOnIntervalUpdate => true;
@@ -28,7 +35,8 @@
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.");
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
@@ -39,27 +47,28 @@
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (giveOnAddingFirstTime && addingFirstTime)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
private void ApplyEffectSpecific()
private void ApplyEffectSpecific(AbilityObject abilityObject)
{
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
if (targetAllies)
@@ -71,7 +80,21 @@
}
else
{
Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? Character :
Character;
if (targetCharacter == null)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
if (targetCharacter?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
@@ -82,7 +105,7 @@
switch (placeholder)
{
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
return original.Replace("[placeholder]", location.Name);
return original.Replace("[placeholder]", location.NameIdentifier.Value);
case PermanentStatPlaceholder.LocationIndex:
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
}
@@ -13,11 +13,13 @@ namespace Barotrauma.Abilities
amount = abilityElement.GetAttributeFloat("amount", 0f);
if (factionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -12,11 +12,13 @@
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.");
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -9,7 +9,8 @@
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@ namespace Barotrauma.Abilities
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -16,11 +16,13 @@ namespace Barotrauma.Abilities
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(skillIncrease, 0))
{
DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.");
DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -8,7 +8,8 @@ namespace Barotrauma.Abilities
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -15,11 +15,13 @@
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(multiplier, 1.0f))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.");
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f))
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@
amount = abilityElement.GetAttributeInt("amount", 1);
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -19,14 +20,16 @@
{
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.");
DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier);
if (itemPrefab == null)
{
DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.");
DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
for (int i = 0; i < amount; i++)
@@ -14,7 +14,8 @@ namespace Barotrauma.Abilities
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.");
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -12,7 +12,8 @@ namespace Barotrauma.Abilities
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.",
contentPackage: abilityElement.ContentPackage);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -13,7 +13,8 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeInt("value", 0);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -24,7 +24,8 @@ namespace Barotrauma.Abilities
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup)
{
AbilityEffectType = abilityEffectType;
CharacterTalent = characterTalent;
CharacterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent));
Character = CharacterTalent.Character;
maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue);
foreach (var subElement in abilityElementGroup.Elements())
@@ -44,9 +44,13 @@ namespace Barotrauma.Abilities
case "fallbackabilities":
LoadFallbackAbilities(subElement);
break;
case "condition":
case "conditions":
LoadConditions(subElement);
break;
default:
DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\".");
break;
}
}
@@ -55,7 +59,8 @@ namespace Barotrauma.Abilities
case AbilityEffectType.OnDieToCharacter:
if (characterAbilities.Any(a => a.RequiresAlive))
{
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.",
characterTalent.Prefab.ContentPackage);
}
break;
}
@@ -90,7 +95,8 @@ namespace Barotrauma.Abilities
if (newCondition == null)
{
DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!",
contentPackage: conditionElement.ContentPackage);
return;
}
@@ -107,7 +113,8 @@ namespace Barotrauma.Abilities
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -118,7 +125,8 @@ namespace Barotrauma.Abilities
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -135,13 +143,21 @@ namespace Barotrauma.Abilities
conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (conditionType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")");
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent.Prefab.ContentPackage);
}
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e);
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e,
contentPackage: characterTalent.Prefab.ContentPackage);
}
return null;
}
@@ -154,13 +170,15 @@ namespace Barotrauma.Abilities
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException);
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
if (newCondition == null)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null");
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null",
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -189,7 +207,8 @@ namespace Barotrauma.Abilities
if (newAbility == null)
{
DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!",
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -200,7 +219,8 @@ namespace Barotrauma.Abilities
{
if (statusEffectElements == null)
{
DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier);
DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -233,7 +253,8 @@ namespace Barotrauma.Abilities
{
if (afflictionElements == null)
{
DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier);
DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -248,7 +269,8 @@ namespace Barotrauma.Abilities
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.");
DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.",
contentPackage: characterTalent.Prefab.ContentPackage);
continue;
}
@@ -23,9 +23,8 @@ namespace Barotrauma
public CharacterTalent(TalentPrefab talentPrefab, Character character)
{
Character = character;
Prefab = talentPrefab;
Character = character ?? throw new ArgumentNullException(nameof(character));
Prefab = talentPrefab ?? throw new ArgumentNullException(nameof(talentPrefab));
var element = talentPrefab.ConfigElement;
DebugIdentifier = talentPrefab.OriginalName;
@@ -46,7 +45,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}");
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}",
contentPackage: element.ContentPackage);
}
break;
case "addedstoreitem":
@@ -56,7 +56,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}");
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}",
contentPackage: element.ContentPackage);
}
break;
}
@@ -146,11 +147,13 @@ namespace Barotrauma
{
if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType))
{
DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent?.Prefab?.ContentPackage);
}
if (abilityEffectType == AbilityEffectType.Undefined)
{
DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent?.Prefab?.ContentPackage);
}
return abilityEffectType;
@@ -84,7 +84,8 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e);
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e,
element?.ContentPackage);
}
}
break;
@@ -37,7 +37,8 @@ namespace Barotrauma
if (Identifier.IsEmpty)
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!");
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!",
contentPackage: element.ContentPackage);
return;
}
@@ -304,7 +305,8 @@ namespace Barotrauma
if (RequiredTalents > MaxChosenTalents)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
contentPackage: talentOptionsElement.ContentPackage);
}
HashSet<Identifier> identifiers = new HashSet<Identifier>();
@@ -333,11 +335,13 @@ namespace Barotrauma
if (RequiredTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.",
contentPackage: talentOptionsElement.ContentPackage);
}
if (MaxChosenTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.",
contentPackage: talentOptionsElement.ContentPackage);
}
}
}
@@ -39,6 +39,23 @@ namespace Barotrauma
}
public Item? FindItemInContainer(ItemContainer? container)
=> container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex);
{
var items = container?.Inventory.GetItemsAt(Slot);
if (items != null && StackIndex >= 0 && StackIndex < items.Count())
{
return items.ElementAt(StackIndex);
}
else
{
string errorMsg =
$"Circuit box error: failed to find an item in the container {container?.Item.Name ?? "null"}.";
DebugConsole.ThrowError(
errorMsg +
$" Items: {items?.Count().ToString() ?? "null"}, " +
$" Slot: {Slot}, StackIndex: {StackIndex}");
GameAnalyticsManager.AddErrorEventOnce("ItemSlotIndexPair.FindItemInContainer", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return null;
}
}
}
}
@@ -50,7 +50,8 @@ namespace Barotrauma
if (identifier.IsEmpty)
{
DebugConsole.ThrowError(
$"No identifier defined for the affliction '{elementName}' in file '{Path}'");
$"No identifier defined for the affliction '{elementName}' in file '{Path}'",
contentPackage: element?.ContentPackage);
return;
}
@@ -60,12 +61,13 @@ namespace Barotrauma
{
DebugConsole.NewMessage(
$"Overriding an affliction or a buff with the identifier '{identifier}' using the file '{Path}'",
Color.Yellow);
Color.MediumPurple);
}
else
{
DebugConsole.ThrowError(
$"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'");
$"Duplicate affliction: '{identifier}' defined in {elementName} of '{Path}'",
contentPackage: element?.ContentPackage);
return;
}
}
@@ -17,12 +17,12 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
DebugConsole.ThrowError($"Loading character file failed: {Path}");
DebugConsole.ThrowError($"Loading character file failed: {Path}", contentPackage: ContentPackage);
return;
}
if (CharacterPrefab.Prefabs.AllPrefabs.Any(kvp => kvp.Value.Any(cf => cf?.ContentFile == this)))
{
DebugConsole.ThrowError($"Duplicate path: {Path}");
DebugConsole.ThrowError($"Duplicate path: {Path}", contentPackage: ContentPackage);
return;
}
var mainElement = doc.Root.FromPackage(ContentPackage);
@@ -69,7 +69,8 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e);
DebugConsole.ThrowError($"Failed to preload a ragdoll file for the character \"{characterPrefab.Name}\"", e,
contentPackage: characterPrefab.ContentPackage);
return;
}
@@ -76,7 +76,8 @@ namespace Barotrauma
{
DebugConsole.AddWarning(
$"The content type \"TraitorMission\" in content package \"{package.Name}\" is no longer supported." +
$" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents.");
$" Traitor missions should be implemented using the scripted event system and the content type TraitorEvents.",
package);
}
return true;
}
@@ -55,7 +55,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}", contentPackage: ContentPackage);
}
}
@@ -2,6 +2,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
[NotSyncedInMultiplayer]
sealed class NPCConversationsFile : ContentFile
{
public NPCConversationsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
@@ -42,7 +42,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}",
contentPackage: parentElement?.ContentPackage);
}
}
@@ -65,7 +65,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}",
contentPackage: parentElement.ContentPackage);
}
}
@@ -4,6 +4,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
[NotSyncedInMultiplayer]
public sealed class TextFile : ContentFile
{
public TextFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
@@ -18,13 +19,12 @@ namespace Barotrauma
LanguageIdentifier language = languageName.ToLanguageIdentifier();
if (!TextManager.TextPacks.ContainsKey(language))
{
TextManager.TextPacks.TryAdd(language, ImmutableHashSet<TextPack>.Empty);
TextManager.TextPacks.TryAdd(language, ImmutableList<TextPack>.Empty);
}
var newPack = new TextPack(this, mainElement, language);
var newHashSet = TextManager.TextPacks[language].Add(newPack);
var newList = TextManager.TextPacks[language].Add(newPack);
TextManager.TextPacks.TryRemove(language, out _);
TextManager.TextPacks.TryAdd(language, newHashSet);
TextManager.TextPacks.TryAdd(language, newList);
TextManager.IncrementLanguageVersion();
}
@@ -32,16 +32,27 @@ namespace Barotrauma
{
foreach (var kvp in TextManager.TextPacks.ToArray())
{
var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet();
var newList = kvp.Value.Where(p => p.ContentFile != this).ToImmutableList();
TextManager.TextPacks.TryRemove(kvp.Key, out _);
if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); }
if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); }
}
TextManager.IncrementLanguageVersion();
if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
{
DebugConsole.AddWarning($"The language {GameSettings.CurrentConfig.Language} is no longer available. Switching to {TextManager.DefaultLanguage}...");
var config = GameSettings.CurrentConfig;
config.Language = TextManager.DefaultLanguage;
GameSettings.SetCurrentConfig(config);
}
}
public override void Sort()
{
//Overrides for text packs don't exist! Should we change this?
foreach (var language in TextManager.TextPacks.Keys.ToList())
{
TextManager.TextPacks[language] =
TextManager.TextPacks[language].Sort((t1, t2) => (t1.ContentFile.ContentPackage?.Index ?? int.MaxValue) - (t2.ContentFile.ContentPackage?.Index ?? int.MaxValue));
}
}
}
}
@@ -283,7 +283,7 @@ namespace Barotrauma
catch (Exception e)
{
var innermost = e.GetInnermost();
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}");
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}", contentPackage: this);
exception = e;
}
if (exception != null)
@@ -391,7 +391,8 @@ namespace Barotrauma
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
string.Join('\n', FatalLoadErrors.Select(errorToStr)));
string.Join('\n', FatalLoadErrors.Select(errorToStr)),
this);
static string errorToStr(LoadError error)
=> error.ToString();
@@ -155,6 +155,7 @@ namespace Barotrauma
.Distinct(new TypeComparer<ContentFile>())
.ForEach(f => f.Sort());
MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash));
TextManager.IncrementLanguageVersion();
}
public static int IndexOf(ContentPackage contentPackage)
@@ -70,7 +70,7 @@ namespace Barotrauma
public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return: NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier> GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
[return: NotNullIfNotNull(parameterName: "def")]
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
@@ -90,6 +90,7 @@ namespace Barotrauma
public Color? GetAttributeColor(string key) => Element.GetAttributeColor(key);
public Color[]? GetAttributeColorArray(string key, Color[]? def) => Element.GetAttributeColorArray(key, def);
public Rectangle GetAttributeRect(string key, in Rectangle def) => Element.GetAttributeRect(key, def);
public Version GetAttributeVersion(string key, Version def) => Element.GetAttributeVersion(key, def);
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
@@ -7,6 +7,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using Barotrauma.IO;
@@ -41,8 +42,8 @@ namespace Barotrauma
public partial class Command
{
public readonly string[] names;
public readonly string help;
public readonly ImmutableArray<Identifier> Names;
public readonly string Help;
public Action<string[]> OnExecute;
@@ -58,8 +59,8 @@ namespace Barotrauma
/// </summary>
public Command(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false)
{
names = name.Split('|');
this.help = help;
Names = name.Split('|').ToIdentifiers().ToImmutableArray();
this.Help = help;
this.OnExecute = onExecute;
@@ -77,7 +78,8 @@ namespace Barotrauma
#endif
if (!allowCheats && !CheatsEnabled && IsCheat)
{
NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red);
NewMessage(
$"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red);
#if USE_STEAM
NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red);
#endif
@@ -89,7 +91,7 @@ namespace Barotrauma
public override int GetHashCode()
{
return names[0].GetHashCode();
return Names.First().GetHashCode();
}
}
@@ -165,7 +167,7 @@ namespace Barotrauma
public static void AssignOnExecute(string names, Action<string[]> onExecute)
{
var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0);
var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
if (matchingCommand == null)
{
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
@@ -188,13 +190,13 @@ namespace Barotrauma
{
foreach (Command c in commands)
{
if (string.IsNullOrEmpty(c.help)) continue;
if (string.IsNullOrEmpty(c.Help)) continue;
ShowHelpMessage(c);
}
}
else
{
var matchingCommand = commands.Find(c => c.names.Any(name => name == args[0]));
var matchingCommand = commands.Find(c => c.Names.Any(name => name == args[0]));
if (matchingCommand == null)
{
NewMessage("Command " + args[0] + " not found.", Color.Red);
@@ -209,7 +211,7 @@ namespace Barotrauma
{
return new string[][]
{
commands.SelectMany(c => c.names).ToArray(),
commands.SelectMany(c => c.Names).Select(n => n.Value).ToArray(),
Array.Empty<string>()
};
}));
@@ -404,7 +406,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray()
commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray()
};
}));
@@ -416,7 +418,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
commands.Select(c => c.names[0]).Union(new string[]{ "All" }).ToArray()
commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray()
};
}));
@@ -857,7 +859,13 @@ namespace Barotrauma
commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) =>
{
if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0)
if (args.Length == 0)
{
ThrowError($"Please specify the identifier of the event you want to debug.");
return;
}
if (GameMain.GameSession?.EventManager is EventManager eventManager)
{
var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
if (ev == null)
@@ -876,9 +884,18 @@ namespace Barotrauma
}
}, isCheat: true, getValidArgs: () =>
{
IEnumerable<EventPrefab> eventPrefabs;
if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None())
{
eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty);
}
else
{
eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab);
}
return new[]
{
GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
};
}));
@@ -1134,7 +1151,7 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
{
if (Submarine.MainSub == null) { return; }
@@ -1160,7 +1177,7 @@ namespace Barotrauma
}
Submarine.MainSub.SetPosition(pos);
}
else
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
if (Level.Loaded == null)
{
@@ -1173,13 +1190,29 @@ namespace Barotrauma
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
if (Level.Loaded?.EndOutpost == null)
{
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return;
}
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null)
{
submarineDockingPort.Dock(outpostDockingPort);
}
}
},
() =>
{
return new string[][]
{
new string[] { "start", "end", "cursor" }
new string[] { "start", "end", "endoutpost", "cursor" }
};
}, isCheat: true));
@@ -1596,7 +1629,7 @@ namespace Barotrauma
int i = 0;
foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
{
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White);
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
i++;
}
ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
@@ -1610,7 +1643,7 @@ namespace Barotrauma
}
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
campaign.Map.SelectLocation(location);
NewMessage(location.Name + " selected.", Color.White);
NewMessage(location.DisplayName + " selected.", Color.White);
});
}
else
@@ -1624,7 +1657,7 @@ namespace Barotrauma
}
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
campaign.Map.SelectLocation(location);
NewMessage(location.Name + " selected.", Color.White);
NewMessage(location.DisplayName + " selected.", Color.White);
}
}));
@@ -1896,7 +1929,7 @@ namespace Barotrauma
{
if (location.Stores != null)
{
var msg = "--- Location: " + location.Name + " ---";
var msg = "--- Location: " + location.DisplayName + " ---";
foreach (var store in location.Stores)
{
msg += $"\nStore identifier: {store.Value.Identifier}";
@@ -1960,7 +1993,7 @@ namespace Barotrauma
InitProjectSpecific();
commands.Sort((c1, c2) => c1.names[0].CompareTo(c2.names[0]));
commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First()));
}
public static string AutoComplete(string command, int increment = 1)
@@ -1971,14 +2004,14 @@ namespace Barotrauma
//if an argument is given or the last character is a space, attempt to autocomplete the argument
if (args.Length > 0 || (splitCommand.Length > 0 && command.Last() == ' '))
{
Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0]));
if (matchingCommand == null || matchingCommand.GetValidArgs == null) return command;
Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier()));
if (matchingCommand?.GetValidArgs == null) { return command; }
int autoCompletedArgIndex = args.Length > 0 && command.Last() != ' ' ? args.Length - 1 : args.Length;
//get all valid arguments for the given command
string[][] allArgs = matchingCommand.GetValidArgs();
if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) return command;
if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) { return command; }
if (string.IsNullOrEmpty(currentAutoCompletedCommand))
{
@@ -1990,7 +2023,7 @@ namespace Barotrauma
currentAutoCompletedCommand.Trim().Length <= arg.Length &&
arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray();
if (validArgs.Length == 0) return command;
if (validArgs.Length == 0) { return command; }
currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, validArgs.Length);
string autoCompletedArg = validArgs[currentAutoCompletedIndex];
@@ -2011,13 +2044,13 @@ namespace Barotrauma
currentAutoCompletedCommand = command;
}
List<string> matchingCommands = new List<string>();
List<Identifier> matchingCommands = new List<Identifier>();
foreach (Command c in commands)
{
foreach (string name in c.names)
foreach (var name in c.Names)
{
if (currentAutoCompletedCommand.Length > name.Length) continue;
if (currentAutoCompletedCommand == name.Substring(0, currentAutoCompletedCommand.Length))
if (currentAutoCompletedCommand.Length > name.Value.Length) { continue; }
if (name.StartsWith(currentAutoCompletedCommand))
{
matchingCommands.Add(name);
}
@@ -2027,7 +2060,7 @@ namespace Barotrauma
if (matchingCommands.Count == 0) return command;
currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, matchingCommands.Count);
return matchingCommands[currentAutoCompletedIndex];
return matchingCommands[currentAutoCompletedIndex].Value;
}
}
@@ -2065,9 +2098,9 @@ namespace Barotrauma
return;
}
string firstCommand = splitCommand[0].ToLowerInvariant();
Identifier firstCommand = splitCommand[0].ToIdentifier();
if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase))
if (firstCommand != "admin")
{
NewCommand(command);
}
@@ -2075,7 +2108,7 @@ namespace Barotrauma
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand));
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
if (matchingCommand == null)
{
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
@@ -2096,12 +2129,12 @@ namespace Barotrauma
}
return;
}
if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client))
if (!IsCommandPermitted(firstCommand, GameMain.Client))
{
#if DEBUG
AddWarning($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\". Executing the command anyway because this is a debug build.");
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
#else
ThrowError($"You're not permitted to use the command \"{splitCommand[0].ToLowerInvariant()}\"!");
ThrowError($"You're not permitted to use the command \"{firstCommand}\"!");
return;
#endif
}
@@ -2111,7 +2144,7 @@ namespace Barotrauma
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.names.Contains(firstCommand)) { continue; }
if (!c.Names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
@@ -2398,8 +2431,9 @@ namespace Barotrauma
#endif
}
public static void LogError(string msg, Color? color = null)
public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null)
{
msg = AddContentPackageInfoToMessage(msg, contentPackage);
color ??= Color.Red;
NewMessage(msg, color.Value, isCommand: false, isError: true);
}
@@ -2516,7 +2550,7 @@ namespace Barotrauma
return true;
}
public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase)));
public static Command FindCommand(string commandName) => commands.Find(c => c.Names.Contains(commandName.ToIdentifier()));
public static void Log(LocalizedString message) => Log(message?.Value);
@@ -2533,8 +2567,9 @@ namespace Barotrauma
ThrowError(error.Value, e, createMessageBox, appendStackTrace);
}
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
{
error = AddContentPackageInfoToMessage(error, contentPackage);
if (e != null)
{
error += " {" + e.Message + "}\n";
@@ -2548,7 +2583,7 @@ namespace Barotrauma
error += "\n\nInner exception: " + innermost.Message + "\n";
if (innermost.StackTrace != null)
{
error += innermost.StackTrace.CleanupStackTrace(); ;
error += innermost.StackTrace.CleanupStackTrace();
}
}
}
@@ -2581,10 +2616,22 @@ namespace Barotrauma
errorMsg);
}
public static void AddWarning(string warning)
public static void AddWarning(string warning, ContentPackage contentPackage = null)
{
warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage);
System.Diagnostics.Debug.WriteLine(warning);
NewMessage($"WARNING: {warning}", Color.Yellow);
NewMessage(warning, Color.Yellow);
}
private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage)
{
if (contentPackage == null) { return message; }
#if CLIENT
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}";
#else
return $"[{contentPackage.Name}] {message}";
#endif
}
#if CLIENT
@@ -159,11 +159,13 @@ namespace Barotrauma
OnItemDeconstructedInventory,
OnStopTinkering,
OnItemPicked,
OnItemSelected,
OnGeneticMaterialCombinedOrRefined,
OnCrewGeneticMaterialCombinedOrRefined,
AfterSubmarineAttacked,
OnApplyTreatment,
OnStatusEffectIdentifier,
OnRepairedOutsideLeak
}
/// <summary>
@@ -550,7 +552,27 @@ namespace Barotrauma
/// <summary>
/// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat.
/// </summary>
LockedTalents
LockedTalents,
/// <summary>
/// Used to reduce or increase the cost of hiring certain jobs by a percentage.
/// </summary>
HireCostMultiplier,
/// <summary>
/// Used to increase how much items can stack in the characters inventory.
/// </summary>
InventoryExtraStackSize,
/// <summary>
/// Modifies the range of the sounds emitted by the character (can be used to make the character easier or more difficult for monsters to hear)
/// </summary>
SoundRangeMultiplier,
/// <summary>
/// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see)
/// </summary>
SightRangeMultiplier
}
internal enum ItemTalentStats
@@ -565,7 +587,8 @@ namespace Barotrauma
ReactorMaxOutput,
ReactorFuelConsumption,
DeconstructorSpeed,
FabricationSpeed
FabricationSpeed,
ExtraStackSize
}
/// <summary>
@@ -34,7 +34,8 @@ namespace Barotrauma
{
if (prefab.ConfigElement.GetAttribute("itemname") != null)
{
DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.");
DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.",
contentPackage: prefab?.ContentPackage);
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
if (itemPrefab == null)
@@ -44,11 +45,12 @@ namespace Barotrauma
}
else
{
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
Identifier itemIdentifier = prefab.ConfigElement.GetAttributeIdentifier("itemidentifier", Identifier.Empty);
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier);
DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier,
contentPackage: prefab?.ContentPackage);
}
}
}
@@ -39,7 +39,7 @@ namespace Barotrauma
public Event(EventPrefab prefab)
{
this.prefab = prefab;
this.prefab = prefab ?? throw new ArgumentNullException(nameof(prefab));
}
public virtual IEnumerable<ContentFile> GetFilesToPreload()
@@ -1,3 +1,6 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -8,21 +11,56 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
private PropertyConditional Conditional { get; }
[Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)]
public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; }
private ImmutableArray<PropertyConditional> Conditionals { get; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier ApplyTagToLinkedHulls { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")]
public Identifier ApplyTagToHull { get; set; }
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (TargetTag.IsEmpty)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.");
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
if (Conditional == null)
var conditionalElements = element.GetChildElements("Conditional");
if (conditionalElements.None())
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.");
//backwards compatibility
Conditionals = PropertyConditional.FromXElement(element, IsConditionalAttribute).ToImmutableArray();
}
else
{
var conditionalList = new List<PropertyConditional>();
foreach (ContentXElement subElement in conditionalElements)
{
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
Conditionals = conditionalList.ToImmutableArray();
}
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
if (Conditionals.None())
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
static bool IsConditionalAttribute(XAttribute attribute)
{
var nameAsIdentifier = attribute.NameAsIdentifier();
return
nameAsIdentifier != nameof(TargetTag) &&
nameAsIdentifier != nameof(LogicalOperator) &&
nameAsIdentifier != nameof(ApplyTagToLinkedHulls) &&
nameAsIdentifier != nameof(ApplyTagToHull);
}
}
private string GetEventName()
@@ -32,31 +70,65 @@ namespace Barotrauma
protected override bool? DetermineSuccess()
{
ISerializableEntity target = null;
IEnumerable<ISerializableEntity> targets = null;
if (!TargetTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(TargetTag))
targets = ParentEvent.GetTargets(TargetTag).OfType<ISerializableEntity>();
}
if (targets.None())
{
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
if (targets.None() || Conditionals.None())
{
foreach (var target in targets)
{
if (t is ISerializableEntity e)
{
target = e;
break;
}
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
}
}
if (target == null)
{
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.");
}
if (target == null || Conditional == null)
{
return true;
}
else
{
bool success = false;
foreach (var target in targets)
{
if (ConditionalsMatch(target))
{
success = true;
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
}
}
return success;
}
}
private bool ConditionalsMatch(ISerializableEntity target)
{
if (LogicalOperator == PropertyConditional.LogicalOperatorType.And)
{
return Conditionals.All(c => ConditionalMatches(target, c));
}
else
{
return Conditionals.Any(c => ConditionalMatches(target, c));
}
}
private static bool ConditionalMatches(ISerializableEntity target, PropertyConditional conditional)
{
if (target is Item item)
{
return item.ConditionalMatches(Conditional);
if (!conditional.TargetItemComponent.IsNullOrEmpty() &&
item.Components.None(ic => ic.Name == conditional.TargetItemComponent))
{
return false;
}
return item.ConditionalMatches(conditional);
}
return Conditional.Matches(target);
return conditional.Matches(target);
}
}
}
@@ -30,7 +30,8 @@ namespace Barotrauma
Condition = element.GetAttributeString("value", string.Empty)!;
if (string.IsNullOrEmpty(Condition))
{
DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element}).");
DebugConsole.ThrowError($"Error in scripted event \"{parentEvent.Prefab.Identifier}\". CheckDataAction with no condition set ({element}).",
contentPackage: element?.ContentPackage);
}
}
}
@@ -42,7 +43,8 @@ namespace Barotrauma
Condition = element.GetAttributeString("value", string.Empty)!;
if (string.IsNullOrEmpty(Condition))
{
DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element}).");
DebugConsole.ThrowError($"Error in scripted event \"{parentDebugString}\". CheckDataAction with no condition set ({element}).",
contentPackage: element?.ContentPackage);
}
}
}
@@ -59,7 +61,8 @@ namespace Barotrauma
(Operator, string value) = PropertyConditional.ExtractComparisonOperatorFromConditionString(Condition);
if (Operator == PropertyConditional.ComparisonOperatorType.None)
{
DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.");
DebugConsole.ThrowError($"{Condition} is invalid, it should start with an operator followed by a boolean or a floating point value.",
contentPackage: ParentEvent?.Prefab?.ContentPackage);
return false;
}
@@ -77,7 +77,8 @@ namespace Barotrauma
ItemIdentifiers.None() &&
TargetTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.");
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.",
contentPackage: element.ContentPackage);
}
checkPercentage = element.GetAttribute(nameof(RequiredConditionalMatchPercentage)) is not null;
if (checkPercentage && conditionals.None())
@@ -86,7 +87,8 @@ namespace Barotrauma
}
if (Amount != 1 && checkPercentage)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.");
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Cannot define both '{Amount}' and '{RequiredConditionalMatchPercentage}' in {nameof(CheckItemAction)}.",
contentPackage: element.ContentPackage);
}
}
@@ -32,7 +32,8 @@ namespace Barotrauma
var targetCharacters = ParentEvent.GetTargets(TargetTag);
if (targetCharacters.None())
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckOrderAction but no valid target characters were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.",
contentPackage: ParentEvent.Prefab.ContentPackage);
return false;
}
foreach (var t in targetCharacters)
@@ -1,7 +1,5 @@
#nullable enable
using System;
using System.Diagnostics;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -31,7 +29,8 @@ namespace Barotrauma
}
default:
{
DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified.");
DebugConsole.ThrowError("CheckReputationAction requires a \"TargetType\" but none were specified.",
contentPackage: ParentEvent.Prefab.ContentPackage);
break;
}
}
@@ -41,7 +40,8 @@ namespace Barotrauma
protected override bool GetBool(CampaignMode campaignMode)
{
DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations.");
DebugConsole.ThrowError("Boolean comparison cannot be applied to reputations.",
contentPackage: ParentEvent.Prefab.ContentPackage);
return false;
}
@@ -87,13 +87,13 @@ namespace Barotrauma
#if DEBUG
void Error(string errorMsg)
{
DebugConsole.ThrowError(errorMsg);
DebugConsole.ThrowError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage);
}
#else
void Error(string errorMsg)
{
DebugConsole.LogError(errorMsg);
DebugConsole.LogError(errorMsg, contentPackage: ParentEvent.Prefab.ContentPackage);
}
#endif
}
@@ -17,7 +17,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.");
DebugConsole.ThrowError($"Cannot use the action {nameof(CheckTraitorEventStateAction)} in the event \"{parentEvent.Prefab.Identifier}\" because it's not a traitor event.",
contentPackage: element.ContentPackage);
}
}
@@ -16,7 +16,8 @@ namespace Barotrauma
{
if (parentEvent is not TraitorEvent)
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.");
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - {nameof(CheckTraitorVoteAction)} can only be used in traitor events.",
contentPackage: element.ContentPackage);
}
}
@@ -1,5 +1,6 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Linq;
namespace Barotrauma
{
@@ -8,12 +9,18 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
public Identifier EntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Entities that also have this tag are excluded.")]
public Identifier ExcludedEntityTag { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")]
public Identifier TargetTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")]
public bool CheckFacing { get; set; }
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "Maximum distance between the targets.")]
public float MaxDistance { get; set; }
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")]
public Identifier ApplyTagToEntity { get; set; }
@@ -31,11 +38,17 @@ namespace Barotrauma
{
foreach (var entity in ParentEvent.GetTargets(EntityTag))
{
if (!ExcludedEntityTag.IsEmpty)
{
if (ParentEvent.GetTargets(ExcludedEntityTag).Contains(entity)) { continue; }
}
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
if (!AllowSameEntity && entity == target) { continue; }
if (Character.IsTargetVisible(target, entity, CheckFacing))
{
if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; }
if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
{
if (!ApplyTagToEntity.IsEmpty)
{
ParentEvent.AddTarget(ApplyTagToEntity, entity);
@@ -116,7 +116,8 @@ namespace Barotrauma
{
DebugConsole.ThrowError(
$"Error in {nameof(EventObjectiveAction)} in the event \"{parentEvent.Prefab.Identifier}\"" +
$" - unrecognized child element \"Replace\".");
$" - unrecognized child element \"Replace\".",
contentPackage: element.ContentPackage);
}
}
@@ -61,14 +61,16 @@ namespace Barotrauma
}
if (MinAmount > MaxAmount && MaxAmount > -1)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}.");
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {MinAmount} is larger than {MaxAmount} in {nameof(CountTargetsAction)}.",
contentPackage: element.ContentPackage);
}
}
else
{
if (MinPercentageRelativeToTarget < 0.0f && MaxPercentageRelativeToTarget < 0.0f)
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set.");
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". Comparing to another target, but neither {nameof(MinPercentageRelativeToTarget)} or {nameof(MaxPercentageRelativeToTarget)} is set.",
contentPackage: element.ContentPackage);
}
}
}
@@ -36,7 +36,8 @@ namespace Barotrauma
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.");
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action (text: \"{Text}\"). Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: elem.ContentPackage);
continue;
}
var action = Instantiate(scriptedEvent, e);
@@ -140,14 +141,19 @@ namespace Barotrauma
Identifier typeName = element.Name.ToString().ToIdentifier();
if (typeName == "TutorialSegmentAction")
{
typeName = "EventObjectiveAction".ToIdentifier();
typeName = nameof(EventObjectiveAction).ToIdentifier();
}
else if (typeName == "TutorialHighlightAction")
{
typeName = nameof(HighlightAction).ToIdentifier();
}
actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true);
if (actionType == null) { throw new NullReferenceException(); }
}
catch
{
DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".");
DebugConsole.ThrowError($"Could not find an {nameof(EventAction)} class of the type \"{element.Name}\".",
contentPackage: element.ContentPackage);
return null;
}
@@ -162,11 +168,36 @@ namespace Barotrauma
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString(),
contentPackage: element.ContentPackage);
return null;
}
}
protected void ApplyTagsToHulls(Entity entity, Identifier hullTag, Identifier linkedHullTag)
{
var currentHull = entity switch
{
Item item => item.CurrentHull,
Character character => character.CurrentHull,
_ => null,
};
if (currentHull == null) { return; }
if (!hullTag.IsEmpty)
{
ParentEvent.AddTarget(hullTag, currentHull);
}
if (!linkedHullTag.IsEmpty)
{
ParentEvent.AddTarget(linkedHullTag, currentHull);
foreach (var linkedHull in currentHull.GetLinkedEntities<Hull>())
{
ParentEvent.AddTarget(linkedHullTag, linkedHull);
}
}
}
/// <summary>
/// Rich test to display in debugdraw
/// </summary>

Some files were not shown because too many files have changed in this diff Show More