Build 0.21.6.0

This commit is contained in:
Markus Isberg
2023-01-31 18:01:29 +02:00
parent 697ec52120
commit 25fa5a9552
145 changed files with 2317 additions and 1145 deletions
@@ -351,21 +351,6 @@ namespace Barotrauma
}
}
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
@@ -206,13 +206,19 @@ namespace Barotrauma
private set;
} = new HashSet<Submarine>();
public bool IsTargetingPlayerTeam => IsTargetInPlayerTeam(SelectedAiTarget);
public static bool IsTargetBeingChasedBy(Character target, Character character)
=> character?.AIController is EnemyAIController enemyAI && enemyAI.SelectedAiTarget?.Entity == target && (enemyAI.State == AIState.Attack || enemyAI.State == AIState.Aggressive);
public bool IsBeingChasedBy(Character c) => IsTargetBeingChasedBy(Character, c);
private bool IsBeingChased => IsBeingChasedBy(SelectedAiTarget?.Entity as Character);
private bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private static bool IsTargetInPlayerTeam(AITarget target) => target?.Entity?.Submarine != null && target.Entity.Submarine.Info.IsPlayer || target?.Entity is Character targetCharacter && targetCharacter.IsOnPlayerTeam;
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
private bool reverse;
public bool Reverse
@@ -355,7 +361,7 @@ namespace Barotrauma
{
targetingTag = "owner";
}
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
else if (PetBehavior != null && (!Character.IsOnFriendlyTeam(targetCharacter) || IsAttackingOwner(targetCharacter)))
{
targetingTag = "hostile";
}
@@ -681,19 +687,22 @@ namespace Barotrauma
{
if (SelectedAiTarget.Entity is Character targetCharacter)
{
bool IsValid(Character.Attacker a)
bool ShouldRetaliate(Character.Attacker a)
{
Character c = a.Character;
if (c.IsDead || c.Removed) { return false; }
if (!Character.IsFriendly(c)) { return true; }
if (!c.IsPlayer) { return false; }
// Only apply the threshold to players
return a.Damage >= selectedTargetingParams.Threshold;
if (c == null || c.IsUnconscious || c.Removed) { return false; }
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
}
return true;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
Character attacker = targetCharacter.LastAttackers.LastOrDefault(ShouldRetaliate)?.Character;
if (attacker?.AiTarget != null)
{
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
State = AIState.Attack;
@@ -1502,7 +1511,7 @@ namespace Barotrauma
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget) && !IsAttackingOwner(hitTarget))
{
return true;
}
@@ -2316,7 +2325,7 @@ namespace Barotrauma
{
t = limb.character;
}
if (t != null && (t == target || !Character.IsFriendly(t)))
if (t != null && (t == target || (!Character.IsFriendly(t) || IsAttackingOwner(t))))
{
return true;
}
@@ -310,7 +310,7 @@ namespace Barotrauma
UseIndoorSteeringOutside = false;
}
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID))
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -541,7 +541,7 @@ namespace Barotrauma
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (Character.CurrentHull == null) { return; }
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold && Character.NeedsOxygen;
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, AIObjectiveFindDivingGear.OXYGEN_SOURCE, out _, conditionPercentage: 1);
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
@@ -566,17 +566,17 @@ namespace Barotrauma
gotoObjective.Abandon = true;
}
}
if (!oxygenLow)
if (!shouldActOnSuffocation)
{
return;
}
}
// Diving gear
if (oxygenLow || findItemState != FindItemState.OtherItem)
if (shouldActOnSuffocation || findItemState != FindItemState.OtherItem)
{
bool needsGear = NeedsDivingGear(Character.CurrentHull, out _);
if (!needsGear || oxygenLow)
if (!needsGear || shouldActOnSuffocation)
{
bool isCurrentObjectiveFindSafety = ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>();
bool shouldKeepTheGearOn =
@@ -591,14 +591,14 @@ namespace Barotrauma
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (oxygenLow && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
shouldKeepTheGearOn = false;
// Remove the suit before we pass out
removeDivingSuit = true;
}
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
if (!shouldKeepTheGearOn && !shouldActOnSuffocation)
{
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
@@ -647,7 +647,7 @@ namespace Barotrauma
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null && !divingSuit.HasTag(AIObjectiveFindDivingGear.DIVING_GEAR_WEARABLE_INDOORS))
{
if (oxygenLow || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
HandleRelocation(divingSuit);
@@ -982,7 +982,7 @@ namespace Barotrauma
if (target.CurrentHull != hull) { continue; }
if (AIObjectiveRescueAll.IsValidTarget(target, Character))
{
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
{
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -1161,7 +1161,7 @@ namespace Barotrauma
freezeAI = true;
}
}
if (attacker == null || attacker.IsDead || attacker.Removed)
if (attacker == null || attacker.IsUnconscious || attacker.Removed)
{
// Don't react to the damage if there's no attacker.
// We might consider launching the retreat combat objective in some cases, so that the bot does not just stand somewhere getting damaged and dying.
@@ -1199,7 +1199,7 @@ namespace Barotrauma
return;
}
float cumulativeDamage = realDamage + Character.GetDamageDoneByAttacker(attacker);
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && Character.CombatAction == null;
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
@@ -1279,7 +1279,7 @@ namespace Barotrauma
if (otherCharacter.Submarine != attacker.Submarine) { continue; }
if (otherCharacter.Info?.Job == null || otherCharacter.IsInstigator) { continue; }
if (otherCharacter.IsPlayer) { continue; }
if (!(otherCharacter.AIController is HumanAIController otherHumanAI)) { continue; }
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
if (!otherHumanAI.IsFriendly(Character)) { continue; }
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (!isWitnessing)
@@ -1299,7 +1299,7 @@ namespace Barotrauma
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage = 0, bool isWitnessing = false)
{
if (!(c.AIController is HumanAIController humanAI)) { return AIObjectiveCombat.CombatMode.None; }
if (c.AIController is not HumanAIController humanAI) { return AIObjectiveCombat.CombatMode.None; }
if (!IsFriendly(attacker))
{
if (c.Submarine == null)
@@ -1327,7 +1327,7 @@ namespace Barotrauma
}
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{
if (GameMain.IsSingleplayer || Character.TeamID != attacker.TeamID)
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
// In multiplayer, they react only to players attacking them or other crew members
@@ -1345,11 +1345,11 @@ namespace Barotrauma
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
}
if (isWitnessing && Character.CombatAction != null && !c.IsSecurity)
if (isWitnessing && c.CombatAction != null && !c.IsSecurity)
{
return Character.CombatAction.WitnessReaction;
return c.CombatAction.WitnessReaction;
}
if (attacker.IsPlayer && FindInstigator() is Character instigator)
if (!attacker.IsInstigator && c.IsOnFriendlyTeam(attacker) && FindInstigator() is Character instigator)
{
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
@@ -1359,11 +1359,11 @@ namespace Barotrauma
{
if (c.IsSecurity)
{
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None;
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None;
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
}
}
else
@@ -1567,20 +1567,20 @@ namespace Barotrauma
return false;
}
public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage);
public static bool HasDivingGear(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) => HasDivingSuit(character, conditionPercentage, requireOxygenTank) || HasDivingMask(character, conditionPercentage, requireOxygenTank);
/// <summary>
/// Check whether the character has a diving suit in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true,
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
public static bool HasDivingMask(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true);
private static List<Item> matchingItems = new List<Item>();
@@ -2045,7 +2045,7 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || !onlySameTeam && me.IsOnFriendlyTeam(other);
if (!teamGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
@@ -352,7 +352,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (WeaponComponent.IsLoaded(character))
if (WeaponComponent.IsNotEmpty(character))
{
// All good, the weapon is loaded
break;
@@ -470,7 +470,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!WeaponComponent.IsLoaded(character))
if (!WeaponComponent.IsNotEmpty(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -541,7 +541,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (!weapon.IsLoaded(character))
if (!weapon.IsNotEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
@@ -554,7 +554,15 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.IsKnockedDown)
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
{
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -244,7 +244,8 @@ namespace Barotrauma
public bool IsInTargetSlot(Item item)
{
if (container?.Inventory is ItemInventory inventory && TargetSlot is not null)
if (TargetSlot == null) { return true; }
if (container?.Inventory is ItemInventory inventory)
{
return inventory.IsInSlot(item, (int)TargetSlot);
}
@@ -63,15 +63,16 @@ namespace Barotrauma
}
else
{
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character))))
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
{
Priority = 100;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
{
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear
@@ -137,12 +138,14 @@ namespace Barotrauma
private float retryTimer;
protected override void Act(float deltaTime)
{
if (resetPriority) { return; }
var currentHull = character.CurrentHull;
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull))
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
bool needsEquipment = shouldActOnSuffocation;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.GetMinOxygen(character));
@@ -178,7 +178,7 @@ namespace Barotrauma
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && (character.CurrentHull == h || h.linkedTo.Contains(character.CurrentHull))),
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull),
endNodeFilter = IsSuitableEndNode,
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
@@ -197,6 +197,14 @@ namespace Barotrauma
}
},
onCompleted: () => RemoveSubObjective(ref gotoObjective));
bool IsSuitableEndNode(PathNode n)
{
if (n.Waypoint.CurrentHull is null) { return false; }
if (n.Waypoint.CurrentHull.ConnectedGaps.Contains(Leak)) { return true; }
// Accept also nodes located in the linked hulls (multi-hull rooms)
return Leak.linkedTo.Any(e => e is Hull h && h.linkedTo.Contains(n.Waypoint.CurrentHull));
}
}
}
@@ -54,7 +54,7 @@ namespace Barotrauma
public bool AllowVariants { get; set; }
public bool Equip { get; set; }
public bool Wear { get; set; }
public bool RequireLoaded { get; set; }
public bool RequireNonEmpty { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool SpeakIfFails { get; set; }
@@ -391,10 +391,10 @@ namespace Barotrauma
{
if (!itemInventory.Container.HasRequiredItems(character, addMessage: false)) { continue; }
}
float itemPriority = 1;
float itemPriority = item.Prefab.BotPriority;
if (GetItemPriority != null)
{
itemPriority = GetItemPriority(item);
itemPriority *= GetItemPriority(item);
}
Entity rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Item ownerItem)
@@ -513,7 +513,7 @@ namespace Barotrauma
float lowestCost = float.MaxValue;
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
if (!(prefab is ItemPrefab itemPrefab)) { continue; }
if (prefab is not ItemPrefab itemPrefab) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
@@ -561,7 +561,7 @@ namespace Barotrauma
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireLoaded && item.Components.Any(i => !i.IsLoaded(character))) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
}
@@ -21,7 +21,7 @@ namespace Barotrauma
public bool CheckInventory { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool CheckPathForEachItem { get; set; }
public bool RequireLoaded { get; set; }
public bool RequireNonEmpty { get; set; }
public bool RequireAllItems { get; set; }
private readonly ImmutableArray<Identifier> gearTags;
@@ -61,7 +61,7 @@ namespace Barotrauma
AllowStealing = AllowStealing,
ignoredIdentifiersOrTags = ignoredTags,
CheckPathForEachItem = CheckPathForEachItem,
RequireLoaded = RequireLoaded,
RequireNonEmpty = RequireNonEmpty,
ItemCount = count,
SpeakIfFails = RequireAllItems
},
@@ -364,8 +364,7 @@ namespace Barotrauma
CurrentOrders.RemoveAt(i);
continue;
}
var currentOrderInfo = character.GetCurrentOrder(currentOrder);
if (currentOrderInfo is Order)
if (character.GetCurrentOrder(currentOrder) is Order currentOrderInfo)
{
int currentPriority = currentOrderInfo.ManualPriority;
if (currentOrder.ManualPriority != currentPriority)
@@ -539,7 +538,8 @@ namespace Barotrauma
KeepActiveWhenReady = true,
CheckInventory = true,
Equip = false,
FindAllItems = true
FindAllItems = true,
RequireNonEmpty = false
};
break;
case "findweapon":
@@ -555,7 +555,8 @@ namespace Barotrauma
KeepActiveWhenReady = false,
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false
FindAllItems = false,
RequireNonEmpty = true
};
}
prepareObjective.KeepActiveWhenReady = false;
@@ -600,9 +601,9 @@ namespace Barotrauma
Order dismissOrder = currentOrder.GetDismissal();
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
if (GameMain.GameSession?.CrewManager is CrewManager cm && cm.IsSinglePlayer)
{
GameMain.GameSession.CrewManager.SetCharacterOrder(character, dismissOrder);
character.SetOrder(dismissOrder, isNewOrder: true, speak: false);
}
#else
GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(dismissOrder, character, character));
@@ -27,6 +27,7 @@ namespace Barotrauma
public bool FindAllItems { get; set; }
public bool Equip { get; set; }
public bool EvaluateCombatPriority { get; set; }
public bool RequireNonEmpty { get; set; }
private AIObjective GetSubObjective()
{
@@ -74,7 +75,7 @@ namespace Barotrauma
Abandon = true;
}
else if (items.Any(i => i.Components.Any(i => !i.IsLoaded(character))))
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
{
Reset();
}
@@ -106,7 +107,7 @@ namespace Barotrauma
CheckInventory = CheckInventory,
Equip = Equip,
EvaluateCombatPriority = EvaluateCombatPriority,
RequireLoaded = true,
RequireNonEmpty = RequireNonEmpty,
RequireAllItems = requireAll
},
onCompleted: () =>
@@ -157,7 +158,7 @@ namespace Barotrauma
{
EvaluateCombatPriority = EvaluateCombatPriority,
SpeakIfFails = true,
RequireLoaded = true
RequireNonEmpty = RequireNonEmpty
};
}
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
@@ -320,10 +320,10 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
if (ItemPrefab.Prefabs.TryGet(treatmentSuitability.Key, out ItemPrefab itemPrefab))
{
if (!Item.ItemList.Any(it => ((MapEntity)it).Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
if (Item.ItemList.None(it => it.Prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(itemPrefab.Identifier);
//only list the first 4 items
if (itemNameList.Count < 4)
{
@@ -482,18 +482,6 @@ namespace Barotrauma
public static IEnumerable<Affliction> GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs);
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (!affliction.Prefab.TreatmentSuitability.Any(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
public override void Reset()
{
base.Reset();
@@ -26,7 +26,7 @@ namespace Barotrauma
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
}
}
@@ -67,15 +67,34 @@ namespace Barotrauma
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis");
foreach (Affliction affliction in AIObjectiveRescue.GetTreatableAfflictions(character))
foreach (Affliction affliction in GetTreatableAfflictions(character))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
if (affliction.Prefab.AfflictionType == "paralysis")
{
vitality -= affliction.Strength;
}
else if (affliction.Prefab.AfflictionType == "poison")
{
vitality -= affliction.Strength;
}
}
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
}
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier);
@@ -454,7 +454,7 @@ namespace Barotrauma
aiming = false;
wasAimingMelee = aimingMelee;
aimingMelee = false;
IsHanging = false;
IsHanging = IsHanging && character.IsRagdolled;
}
void UpdateStanding()
@@ -489,7 +489,7 @@ namespace Barotrauma
LocalizedString displayName = Params.DisplayName;
if (displayName.IsNullOrWhiteSpace())
{
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
if (Params.SpeciesTranslationOverride.IsEmpty)
{
displayName = TextManager.Get($"Character.{SpeciesName}");
}
@@ -752,7 +752,7 @@ namespace Barotrauma
get
{
if (IsUnconscious) { return true; }
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
return CharacterHealth.GetAllAfflictions().Any(a => a.Prefab.Identifier == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
}
}
@@ -822,6 +822,7 @@ namespace Barotrauma
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
public float EmpVulnerability => Params.Health.EmpVulnerability;
public float PoisonVulnerability => Params.Health.PoisonVulnerability;
public float Bloodloss
{
@@ -1040,6 +1041,8 @@ namespace Barotrauma
public bool InWater => AnimController is AnimController { InWater: true };
public bool IsLowInOxygen => NeedsOxygen && OxygenAvailable < CharacterHealth.LowOxygenThreshold;
public bool GodMode = false;
public CampaignMode.InteractionType CampaignInteractionType;
@@ -2871,6 +2874,23 @@ namespace Barotrauma
}
else
{
#if CLIENT
if (Controlled == this)
{
HealingCooldown.PutOnCooldown();
}
#elif SERVER
if (GameMain.Server?.ConnectedClients is { } clients)
{
foreach (Client c in clients)
{
if (c.Character != this) { continue; }
HealingCooldown.SetCooldown(c);
break;
}
}
#endif
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
@@ -3791,9 +3811,10 @@ namespace Barotrauma
message.SendDelay -= deltaTime;
if (message.SendDelay > 0.0f) { continue; }
bool canUseRadio = ChatMessage.CanUseRadio(this, out WifiComponent radio);
if (message.MessageType == null)
{
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default;
message.MessageType = canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
}
#if CLIENT
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
@@ -3803,6 +3824,11 @@ namespace Barotrauma
{
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(Name, modifiedMessage, message.MessageType.Value, this);
}
if (canUseRadio)
{
Signal s = new Signal(modifiedMessage, sender: this, source: radio.Item);
radio.TransmitSignal(s, sentFromChat: true);
}
}
#endif
#if SERVER
@@ -4122,13 +4148,13 @@ namespace Barotrauma
OnAttackedProjSpecific(attacker, attackResult, stun);
if (!wasDead)
{
TryAdjustAttackerSkill(attacker, CharacterHealth.Vitality - prevVitality);
TryAdjustAttackerSkill(attacker, attackResult);
}
};
}
if (attackResult.Damage > 0)
{
LastDamage = attackResult;
if (attacker != null)
if (attacker != null && attacker != this && !attacker.Removed)
{
AddAttacker(attacker, attackResult.Damage);
AddEncounter(attacker);
@@ -4143,26 +4169,84 @@ namespace Barotrauma
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
public void TryAdjustAttackerSkill(Character attacker, AttackResult attackResult)
{
if (attacker == null) { return; }
if (!attacker.IsOnPlayerTeam) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
if (isEnemy)
if (!isEnemy) { return; }
float weaponDamage = 0;
float medicalDamage = 0;
foreach (var affliction in attackResult.Afflictions)
{
if (healthChange < 0.0f)
if (affliction.Prefab.IsBuff) { continue; }
if (Params.IsMachine && !affliction.Prefab.AffectMachines) { continue; }
if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
{
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
attacker.Info?.IncreaseSkillLevel("weapons".ToIdentifier(),
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f));
if (!Params.Health.PoisonImmunity)
{
float relativeVitality = MaxVitality / 100f;
// Undo the applied modifiers to get the base value. Poison damage is multiplied by max vitality when it's applied.
float dmg = affliction.Strength;
if (relativeVitality > 0)
{
dmg /= relativeVitality;
}
if (PoisonVulnerability > 0)
{
dmg /= PoisonVulnerability;
}
float strength = MaxVitality;
if (Params.AI != null)
{
strength = Params.AI.CombatStrength;
}
// Adjust the skill gain by the strength of the target. Combat strength >= 1000 gives 2x bonus, combat strength < 333 less than 1x.
float vitalityFactor = MathHelper.Lerp(0.5f, 2f, MathUtils.InverseLerp(0, 1000, strength));
dmg *= vitalityFactor;
medicalDamage += dmg * affliction.Prefab.MedicalSkillGain;
}
}
else
{
medicalDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.MedicalSkillGain;
}
weaponDamage += affliction.GetVitalityDecrease(null) * affliction.Prefab.WeaponsSkillGain;
}
else if (healthChange > 0.0f)
if (medicalDamage > 0)
{
float attackerSkillLevel = attacker.GetSkillLevel("medical");
attacker.Info?.IncreaseSkillLevel("medical".ToIdentifier(),
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f));
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
}
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), 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));
}
}
public void TryAdjustHealerSkill(Character healer, float healthChange = 0, Affliction affliction = null)
{
if (healer == null) { return; }
bool isEnemy = AIController is EnemyAIController || TeamID != healer.TeamID;
if (isEnemy) { return; }
float medicalGain = healthChange;
if (affliction?.Prefab is { IsBuff: true } && (!Params.IsMachine || affliction.Prefab.AffectMachines))
{
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));
}
/// <summary>
@@ -5240,7 +5324,24 @@ namespace Barotrauma
public bool IsFriendly(Character other) => IsFriendly(this, other);
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public static bool IsFriendly(Character me, Character other) => IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public bool IsOnFriendlyTeam(Character other) => IsOnFriendlyTeam(TeamID, other.TeamID);
public bool IsOnFriendlyTeam(CharacterTeamType otherTeam) => IsOnFriendlyTeam(TeamID, otherTeam);
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
@@ -321,6 +321,7 @@ namespace Barotrauma
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
public readonly float MinStrength, MaxStrength;
public PeriodicEffect(ContentXElement element, string parentDebugName)
{
@@ -335,8 +336,10 @@ namespace Barotrauma
}
else
{
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval);
MinInterval = Math.Max(element.GetAttributeFloat(nameof(MinInterval), 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat(nameof(MaxInterval), 1.0f), MinInterval);
MinStrength = Math.Max(element.GetAttributeFloat(nameof(MinStrength), 0f), 0f);
MaxStrength = Math.Max(element.GetAttributeFloat(nameof(MaxStrength), MinStrength), MinStrength);
}
}
}
@@ -415,8 +418,8 @@ namespace Barotrauma
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
public float BurnOverlayAlpha;
public float DamageOverlayAlpha;
public readonly float BurnOverlayAlpha;
public readonly float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
public readonly Identifier AchievementOnRemoved;
@@ -427,6 +430,20 @@ namespace Barotrauma
public readonly Sprite AfflictionOverlay;
public readonly bool AfflictionOverlayAlphaIsLinear;
public readonly bool DamageParticles;
/// <summary>
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
/// </summary>
public readonly float MedicalSkillGain;
/// <summary>
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
/// The skill is increased only when the target is hostile.
/// </summary>
public readonly float WeaponsSkillGain;
private readonly List<Effect> effects = new List<Effect>();
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
@@ -528,6 +545,10 @@ namespace Barotrauma
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
DamageParticles = element.GetAttributeBool(nameof(DamageParticles), true);
WeaponsSkillGain = element.GetAttributeFloat(nameof(WeaponsSkillGain), 0.0f);
MedicalSkillGain = element.GetAttributeFloat(nameof(MedicalSkillGain), 0.0f);
List<Description> descriptions = new List<Description>();
foreach (var subElement in element.Elements())
{
@@ -708,7 +708,7 @@ namespace Barotrauma
return;
}
}
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (Character.Params.Health.PoisonImmunity && (newAffliction.Prefab.AfflictionType == "poison" || newAffliction.Prefab.AfflictionType == "paralysis")) { return; }
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
@@ -490,13 +490,9 @@ namespace Barotrauma
public int RefJointIndex => Params.RefJoint;
private List<WearableSprite> wearingItems;
public List<WearableSprite> WearingItems
{
get { return wearingItems; }
}
public readonly List<WearableSprite> WearingItems = new List<WearableSprite>();
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
public readonly List<WearableSprite> OtherWearables = new List<WearableSprite>();
public bool PullJointEnabled
{
@@ -640,7 +636,6 @@ namespace Barotrauma
this.ragdoll = ragdoll;
this.character = character;
this.Params = limbParams;
wearingItems = new List<WearableSprite>();
dir = Direction.Right;
body = new PhysicsBody(limbParams);
type = limbParams.Type;
@@ -772,7 +767,7 @@ namespace Barotrauma
tempModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
foreach (WearableSprite wearable in WearingItems)
{
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
@@ -791,10 +786,14 @@ namespace Barotrauma
}
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
float finalDamageModifier = damageMultiplier;
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
if (character.EmpVulnerability > 0 && affliction.Prefab.AfflictionType == "emp")
{
finalDamageModifier *= character.EmpVulnerability;
}
if (!character.Params.Health.PoisonImmunity && (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis"))
{
finalDamageModifier *= character.PoisonVulnerability;
}
foreach (DamageModifier damageModifier in tempModifiers)
{
float damageModifierValue = damageModifier.DamageMultiplier;
@@ -21,7 +21,7 @@ namespace Barotrauma
public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; }
public Identifier SpeciesTranslationOverride { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
@@ -501,6 +501,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
public float PoisonVulnerability { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
public float EmpVulnerability { get; set; }
@@ -42,7 +42,7 @@ namespace Barotrauma
public readonly Version GameVersion;
public readonly string ModVersion;
public Md5Hash Hash { get; private set; }
public readonly Option<DateTime> InstallTime;
public readonly Option<SerializableDateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; }
@@ -73,7 +73,7 @@ namespace Barotrauma
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= installTime;
return item.Value.LatestUpdateTime <= installTime.ToUtcValue();
}
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -106,10 +106,7 @@ namespace Barotrauma
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0);
InstallTime = installTimeUnix != 0
? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
: Option<DateTime>.None();
InstallTime = rootElement.GetAttributeDateTime("installtime");
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))
@@ -1868,6 +1868,7 @@ namespace Barotrauma
commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
InitProjectSpecific();
@@ -19,7 +19,9 @@ partial class UIHighlightAction : EventAction
TurbineOutputSlider,
DeconstructButton,
RechargeSpeedSlider,
CPRButton
CPRButton,
CloseButton,
MessageBoxCloseButton
}
[Serialize(ElementId.None, IsPropertySaveable.Yes)]
@@ -21,7 +21,7 @@ namespace Barotrauma
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; }
if (sub == null || sub.Info.InitialSuppliesSpawned || sub.Info.IsManuallyOutfitted || !sub.Info.IsPlayer) { continue; }
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
SpawnStartItems(sub, startItemSet);
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
@@ -5,6 +5,7 @@ using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -13,13 +14,11 @@ namespace Barotrauma
abstract partial class CampaignMode : GameMode
{
[NetworkSerialize]
public struct SaveInfo : INetSerializableStruct
{
public string FilePath;
public int SaveTime;
public string SubmarineName;
public string[] EnabledContentPackageNames;
}
public readonly record struct SaveInfo(
string FilePath,
Option<SerializableDateTime> SaveTime,
string SubmarineName,
ImmutableArray<string> EnabledContentPackageNames) : INetSerializableStruct;
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
public const int InitialMoney = 8500;
@@ -1114,7 +1113,6 @@ namespace Barotrauma
if (item.Components.None(c => c is Pickable)) { continue; }
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
itemsToTransfer.Add((item, item.Container));
item.Submarine = null;
}
@@ -546,9 +546,7 @@ namespace Barotrauma
StatusEffect.StopAll();
#if CLIENT
#if !DEBUG
GameMain.LightManager.LosEnabled = GameMain.Client == null || GameMain.Client.CharacterInfo != null;
#endif
GameMain.LightManager.LosEnabled = (GameMain.Client == null || GameMain.Client.CharacterInfo != null) && !GameMain.DevMode;
if (GameMain.LightManager.LosEnabled) { GameMain.LightManager.LosAlpha = 1f; }
if (GameMain.Client == null) { GameMain.LightManager.LosMode = GameSettings.CurrentConfig.Graphics.LosMode; }
#endif
@@ -1074,7 +1072,10 @@ namespace Barotrauma
XDocument doc = new XDocument(new XElement("Gamesession"));
XElement rootElement = doc.Root ?? throw new NullReferenceException("Game session XML element is invalid: document is null.");
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
rootElement.Add(new XAttribute("savetime", SerializableDateTime.UtcNow.ToUnixTime()));
#warning TODO: after this gets on main, replace savetime with the commented line
//rootElement.Add(new XAttribute("savetime", SerializableDateTime.LocalNow));
rootElement.Add(new XAttribute("version", GameMain.Version));
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
{
@@ -692,11 +692,13 @@ namespace Barotrauma
/// Gets the progress that is shown on the store interface.
/// Includes values stored in the metadata and <see cref="PendingUpgrades"/>, and takes submarine tier and class restrictions into account
/// </summary>
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
/// <param name="info">Submarine used to determine the upgrade limit. If not defined, will default to the current sub.</param>
public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info = null)
{
if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub());
int maxLevel = info is null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info);
return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), maxLevel);
int GetPendingLevel()
{
@@ -713,6 +715,14 @@ namespace Barotrauma
return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
}
/// <summary>
/// Gets the level of the upgrade that is stored in the metadata. Takes into account the limits of the provided submarine.
/// </summary>
public int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info)
{
return Math.Min(GetRealUpgradeLevel(prefab, category), prefab.GetMaxLevel(info));
}
/// <summary>
/// Stores the target upgrade level in the campaign metadata.
/// </summary>
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -308,6 +309,8 @@ namespace Barotrauma
#endif
}
if (item.GetComponent<Pickable>() == null || item.AllowedSlots.None()) { return false; }
bool inSuitableSlot = false;
bool inWrongSlot = false;
int currentSlot = -1;
@@ -289,7 +289,16 @@ namespace Barotrauma.Items.Components
return;
}
if (!(joint is WeldJoint))
if (joint == null)
{
string errorMsg = "Error while locking a docking port (joint between submarines doesn't exist)." +
" Submarine: " + (item.Submarine?.Info.Name ?? "null") +
", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null");
GameAnalyticsManager.AddErrorEventOnce("DockingPort.Lock:JointNotCreated", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return;
}
if (joint is not WeldJoint)
{
DockingDir = GetDir(DockingTarget);
DockingTarget.DockingDir = -DockingDir;
@@ -1,12 +1,9 @@
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Lights;
#endif
@@ -206,6 +203,8 @@ namespace Barotrauma.Items.Components
IsHorizontal = element.GetAttributeBool("horizontal", false);
canBePicked = element.GetAttributeBool("canbepicked", false);
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
allowedSlots.Clear();
foreach (var subElement in element.Elements())
{
@@ -359,7 +358,10 @@ namespace Barotrauma.Items.Components
{
lastBrokenTime = Timing.TotalTime;
//the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
//multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
//otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
IsBroken = false;
@@ -58,9 +58,6 @@ namespace Barotrauma.Items.Components
set;
}
//the angle in which the Character holds the item
protected float holdAngle;
public PhysicsBody Body
{
get { return item.body ?? body; }
@@ -143,6 +140,7 @@ namespace Barotrauma.Items.Components
set { aimPos = ConvertUnits.ToSimUnits(value); }
}
protected float holdAngle;
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")]
#else
@@ -154,6 +152,18 @@ namespace Barotrauma.Items.Components
set { holdAngle = MathHelper.ToRadians(value); }
}
protected float aimAngle;
#if DEBUG
[Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item while aiming (in degrees, relative to the rotation of the character's hand).")]
#else
[Serialize(0.0f, IsPropertySaveable.No)]
#endif
public float AimAngle
{
get { return MathHelper.ToDegrees(aimAngle); }
set { aimAngle = MathHelper.ToRadians(value); }
}
private Vector2 swingAmount;
#if DEBUG
[Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")]
@@ -552,6 +562,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
bool wasAttached = IsAttached;
if (base.OnPicked(picker))
{
DeattachFromWall();
@@ -560,7 +571,7 @@ namespace Barotrauma.Items.Components
if (GameMain.Server != null && attachable)
{
item.CreateServerEvent(this);
if (picker != null)
if (picker != null && wasAttached)
{
GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
}
@@ -688,16 +699,22 @@ namespace Barotrauma.Items.Components
if (maxAttachableCount == 0)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
if (character == Character.Controlled)
{
GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red);
}
#endif
return false;
}
else if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
if (character == Character.Controlled)
{
GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red);
}
#endif
return false;
return false;
}
}
@@ -875,9 +892,13 @@ namespace Barotrauma.Items.Components
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
if (!aim)
if (aim)
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
}
else
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle);
var rope = GetRope();
if (rope != null && rope.SnapWhenNotAimed && rope.Item.ParentInventory == null)
{
@@ -223,7 +223,7 @@ namespace Barotrauma.Items.Components
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
if (ac.InWater)
{
ac.LockFlipping();
@@ -472,8 +472,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
serverLogger ??= new System.Text.StringBuilder();
serverLogger.Clear();
serverLogger.Append($"{picker?.LogName} used {item.Name}");
@@ -26,6 +26,8 @@ namespace Barotrauma.Items.Components
get { return allowedSlots; }
}
public bool PickingDone => pickTimer >= PickingTime;
public Character Picker
{
get
@@ -205,12 +205,12 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower));
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, targetCharacter: CurrentThrower));
}
if (!(GameMain.NetworkMember is { IsClient: true }))
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower);
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: CurrentThrower, user: CurrentThrower);
}
throwState = ThrowState.None;
}
@@ -125,8 +125,8 @@ namespace Barotrauma.Items.Components
get { return drawable; }
set
{
if (value == drawable) return;
if (!(this is IDrawableComponent))
if (value == drawable) { return; }
if (this is not IDrawableComponent)
{
DebugConsole.ThrowError("Couldn't make \"" + this + "\" drawable (the component doesn't implement the IDrawableComponent interface)");
return;
@@ -236,10 +236,7 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).
/// </summary>
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced).")]
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
public float CombatPriority { get; private set; }
/// <summary>
@@ -697,7 +694,7 @@ namespace Barotrauma.Items.Components
public virtual void FlipY(bool relativeToSub) { }
public bool IsLoaded(Character user, bool checkContainedItems = true) =>
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
HasRequiredContainedItems(user, addMessage: false) &&
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
@@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
public bool DrawInventory
{
get;
@@ -142,6 +142,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; }
@@ -534,12 +537,12 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
}
public override bool Select(Character character)
{
if (!DrawInventory) { return false; }
if (!AllowAccess) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
@@ -575,7 +578,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!DrawInventory) { return false; }
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
@@ -112,27 +112,34 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the character it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")]
public bool StickToCharacters
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the structure it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")]
public bool StickToStructures
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick to the item it hits.")]
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")]
public bool StickToItems
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")]
public bool StickToDoors
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")]
public bool StickToDeflective
{
@@ -457,36 +464,36 @@ namespace Barotrauma.Items.Components
Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
List<HitscanResult> hits = new List<HitscanResult>();
hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
if (item.Submarine != null)
{
//shooting indoors, do a hitscan outside as well
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
//also in the coordinate space of docked subs
foreach (Submarine dockedSub in item.Submarine.DockedTo)
{
if (dockedSub == item.Submarine) { continue; }
hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition, dockedSub));
}
//do a hitscan in other subs' coordinate spaces
RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition);
}
else
{
RayCastInOtherSubs(rayStart, rayEnd);
}
void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd)
{
//shooting outdoors, see if we can hit anything inside a sub
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine == item.Submarine) { continue; }
var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
//transform back to world coordinates
for (int i = 0; i < inSubHits.Count; i++)
{
inSubHits[i] = new HitscanResult(
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fraction);
}
hits.AddRange(inSubHits);
}
}
@@ -993,8 +1000,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
}
}
else
@@ -1003,8 +1010,8 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
}
}
}
@@ -1012,13 +1019,12 @@ namespace Barotrauma.Items.Components
target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
if (hits.Count() >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
{
DisableProjectileCollisions();
}
if (attackResult.AppliedDamageModifiers != null &&
(attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective))
if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)
{
item.body.LinearVelocity *= deflectedSpeedMultiplier;
}
@@ -1028,7 +1034,7 @@ namespace Barotrauma.Items.Components
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(StickToItems && target.Body.UserData is Item))))
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
{
Vector2 dir = new Vector2(
(float)Math.Cos(item.body.Rotation),
@@ -302,33 +302,16 @@ namespace Barotrauma.Items.Components
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null && targetBody.UserData is not Character)
if (user != null && user.InWater)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
float forceMultiplier = 1;
if (user != null)
{
user.AnimController.Hang();
if (user.InWater)
if (user.IsRagdolled)
{
if (user.IsRagdolled)
{
forceMultiplier = 0;
}
}
else
{
forceMultiplier = user.IsRagdolled ? 0.1f : 0.4f;
// Prevents too easy smashing to the walls
forceDir.X /= 4;
// Prevents rubberbanding up and down
if (forceDir.Y < 0)
{
forceDir.Y = 0;
}
// Reel in towards the target.
user.AnimController.Hang();
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
// Take the target velocity into account.
if (targetCharacter != null)
{
var myCollider = user.AnimController.Collider;
@@ -341,9 +324,15 @@ namespace Barotrauma.Items.Components
}
}
}
else
{
var targetBody = GetBodyToPull(target);
if (targetBody != null)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) * forceMultiplier : SourcePullForce * forceMultiplier;
sourceBody.ApplyForce(forceDir * force);
}
}
}
@@ -187,6 +187,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Should the light sprite be drawn on the item using alpha blending, in addition to being rendered in the light map? Can be used to make the light sprite stand out more.")]
public bool AlphaBlend
{
get;
set;
}
public float TemporaryFlickerTimer;
public override void Move(Vector2 amount, bool ignoreContacts = false)
@@ -241,6 +248,7 @@ namespace Barotrauma.Items.Components
SetLightSourceState(IsActive);
turret = item.GetComponent<Turret>();
#if CLIENT
Drawable = AlphaBlend && Light.LightSprite != null;
if (Screen.Selected.IsEditor)
{
OnMapLoaded();
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
private string prevSignal;
private readonly int[] channelMemory = new int[ChannelMemorySize];
private int[] channelMemory = new int[ChannelMemorySize];
private Connection signalInConnection;
private Connection signalOutConnection;
@@ -94,7 +94,17 @@ namespace Barotrauma.Items.Components
{
list.Add(this);
IsActive = true;
channelMemory = element.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
channelMemory = componentElement.GetAttributeIntArray("channelmemory", new int[ChannelMemorySize]);
if (channelMemory.Length != ChannelMemorySize)
{
DebugConsole.AddWarning($"Error when loading item {item.Prefab.Identifier}: the size of the channel memory doesn't match the default value of {ChannelMemorySize}. Resizing...");
Array.Resize(ref channelMemory, ChannelMemorySize);
}
}
public override void OnItemLoaded()
@@ -44,7 +44,16 @@ namespace Barotrauma
}
public LimbType Limb { get; private set; }
public bool HideLimb { get; private set; }
public bool HideOtherWearables { get; private set; }
public enum ObscuringMode
{
None,
Hide,
AlphaClip
}
public ObscuringMode ObscureOtherWearables { get; private set; }
public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide;
public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip;
public bool CanBeHiddenByOtherWearables { get; private set; }
public List<WearableType> HideWearablesOfType { get; private set; }
public bool InheritLimbDepth { get; private set; }
@@ -130,7 +139,7 @@ namespace Barotrauma
case WearableType.Husk:
case WearableType.Herpes:
Limb = LimbType.Head;
HideOtherWearables = false;
ObscureOtherWearables = ObscuringMode.None;
InheritLimbDepth = true;
InheritScale = true;
InheritOrigin = true;
@@ -202,7 +211,16 @@ namespace Barotrauma
Sprite = new Sprite(SourceElement, file: SpritePath);
Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true);
HideLimb = SourceElement.GetAttributeBool("hidelimb", false);
HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false);
foreach (var mode in Enum.GetValues<ObscuringMode>())
{
if (mode == ObscuringMode.None) { continue; }
if (SourceElement.GetAttributeBool($"{mode}OtherWearables", false))
{
ObscureOtherWearables = mode;
}
}
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
var scale = SourceElement.GetAttribute("inheritscale");
@@ -585,6 +585,8 @@ namespace Barotrauma
item.body.Enabled = false;
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
//update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies)
item.body.Update();
}
#if SERVER
@@ -897,7 +897,7 @@ namespace Barotrauma
defaultRect = newRect;
rect = newRect;
condition = MaxCondition = Prefab.Health;
condition = MaxCondition = prevCondition = Prefab.Health;
ConditionPercentage = 100.0f;
lastSentCondition = condition;
@@ -999,13 +999,6 @@ namespace Barotrauma
if (ic == null) break;
AddComponent(ic);
if (ic is IDrawableComponent && ic.Drawable)
{
drawableComponents.Add(ic as IDrawableComponent);
hasComponentsToDraw = true;
}
if (ic is Repairable) repairables.Add((Repairable)ic);
break;
}
}
@@ -1020,6 +1013,14 @@ namespace Barotrauma
}
}
if (ic is Repairable repairable) { repairables.Add(repairable); }
if (ic is IDrawableComponent && ic.Drawable)
{
drawableComponents.Add(ic as IDrawableComponent);
hasComponentsToDraw = true;
}
if (ic.statusEffectLists == null) { continue; }
if (ic.InheritStatusEffects)
{
@@ -1751,6 +1752,7 @@ namespace Barotrauma
RecalculateConditionValues();
bool wasPreviousConditionChanged = false;
if (condition == 0.0f && prevCondition > 0.0f)
{
//Flag connections to be updated as device is broken
@@ -1763,6 +1765,8 @@ namespace Barotrauma
}
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
#endif
// Have to set the previous condition here or OnBroken status effects that reduce the condition will keep triggering the status effects, resulting in a stack overflow.
SetPreviousCondition();
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
}
else if (condition > 0.0f && prevCondition <= 0.0f)
@@ -1793,9 +1797,18 @@ namespace Barotrauma
}
}
LastConditionChange = condition - prevCondition;
ConditionLastUpdated = Timing.TotalTime;
prevCondition = condition;
if (!wasPreviousConditionChanged)
{
SetPreviousCondition();
}
void SetPreviousCondition()
{
LastConditionChange = condition - prevCondition;
ConditionLastUpdated = Timing.TotalTime;
prevCondition = condition;
wasPreviousConditionChanged = true;
}
static void flagChangedConnections(Dictionary<string, Connection> connections)
{
@@ -2696,7 +2709,7 @@ namespace Barotrauma
return;
}
if (condition == 0.0f) { return; }
if (condition <= 0.0f) { return; }
bool remove = false;
@@ -2713,7 +2726,7 @@ namespace Barotrauma
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character);
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character);
if (ic.DeleteOnUse) { remove = true; }
}
@@ -2727,7 +2740,7 @@ namespace Barotrauma
public void SecondaryUse(float deltaTime, Character character = null)
{
if (condition == 0.0f) { return; }
if (condition <= 0.0f) { return; }
bool remove = false;
@@ -2763,6 +2776,13 @@ namespace Barotrauma
if (!UseInHealthInterface) { return; }
#if CLIENT
if (user == Character.Controlled)
{
if (HealingCooldown.IsOnCooldown) { return; }
HealingCooldown.PutOnCooldown();
}
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb));
@@ -2783,13 +2803,13 @@ namespace Barotrauma
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: user);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: character, user: user);
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb, useTarget: character));
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb, useTarget: character));
}
if (ic.DeleteOnUse) { remove = true; }
@@ -769,6 +769,9 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.No)]
public bool ShowHealthBar { get; private set; }
[Serialize(1f, IsPropertySaveable.No, description: "How much the bots prioritize this item when they seek for items. For example, bots prioritize less exosuit than the other diving suits. Defaults to 1. Note that there's also a specific CombatPriority for items that can be used as weapons.")]
public float BotPriority { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -938,14 +941,20 @@ namespace Barotrauma
AllowDeconstruct = true;
RandomDeconstructionOutput = subElement.GetAttributeBool("chooserandom", false);
RandomDeconstructionOutputAmount = subElement.GetAttributeInt("amount", 1);
foreach (XElement deconstructItem in subElement.Elements())
foreach (XElement itemElement in subElement.Elements())
{
if (deconstructItem.Attribute("name") != null)
if (itemElement.Attribute("name") != null)
{
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - use item identifiers instead of names to configure the deconstruct items.");
continue;
}
deconstructItems.Add(new DeconstructItem(deconstructItem, Identifier));
var deconstructItem = new DeconstructItem(itemElement, Identifier);
if (deconstructItem.ItemIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in item config \"{ToString()}\" - deconstruction output contains an item with no identifier.");
continue;
}
deconstructItems.Add(deconstructItem);
}
RandomDeconstructionOutputAmount = Math.Min(RandomDeconstructionOutputAmount, deconstructItems.Count);
break;
@@ -18,9 +18,9 @@ namespace Barotrauma
public const ushort ReservedIDStart = ushort.MaxValue - 3;
public const ushort MaxEntityCount = ushort.MaxValue - 2; //ushort.MaxValue - 2 because 0 and ushort.MaxValue are reserved values
public const ushort MaxEntityCount = ushort.MaxValue - 4; //ushort.MaxValue - 4 because the 4 values above are reserved values
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
private static readonly Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IReadOnlyCollection<Entity> GetEntities()
{
return dictionary.Values;
@@ -85,6 +85,28 @@ namespace Barotrauma
this.Submarine = submarine;
spawnTime = Timing.TotalTime;
if (dictionary.Count >= MaxEntityCount)
{
Dictionary<Identifier, int> entityCounts = new Dictionary<Identifier, int>();
foreach (var entity in dictionary)
{
if (entity.Value is MapEntity me)
{
if (entityCounts.ContainsKey(me.Prefab.Identifier))
{
entityCounts[me.Prefab.Identifier]++;
}
else
{
entityCounts[me.Prefab.Identifier] = 1;
}
}
}
string errorMsg = $"Maximum amount of entities ({MaxEntityCount}) exceeded! Largest numbers of entities: " +
string.Join(", ", entityCounts.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
throw new Exception(errorMsg);
}
//give a unique ID
ID = DetermineID(id, submarine);
@@ -50,6 +50,12 @@ namespace Barotrauma
Description = TextManager.Get($"EntityDescription.{Identifier}");
Tags = Enumerable.Empty<Identifier>().ToImmutableHashSet();
string description = element.GetAttributeString("description", string.Empty);
if (!description.IsNullOrEmpty())
{
Description = Description.Fallback(description);
}
List<ushort> containedItemIDs = new List<ushort>();
foreach (XElement entityElement in element.Elements())
{
@@ -112,10 +112,9 @@ namespace Barotrauma
(int)MathUtils.Round(generationParams.Height, Level.GridCellSize));
}
public LevelData(XElement element, float? forceDifficulty = null)
public LevelData(XElement element, float? forceDifficulty = null, bool clampDifficultyToBiome = false)
{
Seed = element.GetAttributeString("seed", "");
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
Size = element.GetAttributePoint("size", new Point(1000));
Enum.TryParse(element.GetAttributeString("type", "LocationConnection"), out Type);
@@ -131,10 +130,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Error while loading a level. Could not find level generation params with the ID \"{generationParamsId}\".");
GenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(l => l.Type == Type);
if (GenerationParams == null)
{
GenerationParams = LevelGenerationParams.LevelParams.First();
}
GenerationParams ??= LevelGenerationParams.LevelParams.First();
}
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
@@ -147,10 +143,16 @@ namespace Barotrauma
Biome = Biome.Prefabs.First();
}
string[] prefabNames = element.GetAttributeStringArray("eventhistory", new string[] { });
Difficulty = forceDifficulty ?? element.GetAttributeFloat("difficulty", 0.0f);
if (clampDifficultyToBiome)
{
Difficulty = MathHelper.Clamp(Difficulty, Biome.MinDifficulty, Biome.AdjustedMaxDifficulty);
}
string[] prefabNames = element.GetAttributeStringArray("eventhistory", Array.Empty<string>());
EventHistory.AddRange(EventPrefab.Prefabs.Where(p => prefabNames.Any(n => p.Identifier == n)));
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", Array.Empty<string>());
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
@@ -559,12 +559,9 @@ namespace Barotrauma
killedCharacterIdentifiers = element.GetAttributeIntArray("killedcharacters", Array.Empty<int>()).ToHashSet();
System.Diagnostics.Debug.Assert(Type != null, $"Could not find the location type \"{locationTypeId}\"!");
if (Type == null)
{
Type = LocationType.Prefabs.First();
}
Type ??= LocationType.Prefabs.First();
LevelData = new LevelData(element.Element("Level"));
LevelData = new LevelData(element.Element("Level"), clampDifficultyToBiome: true);
PortraitId = ToolBox.StringToInt(Name);
@@ -531,11 +531,15 @@ namespace Barotrauma
/// <summary>
/// Calculated from <see cref="SubmarineElement"/>. Can be used when the sub hasn't been loaded and we can't access <see cref="Submarine.RealWorldCrushDepth"/>.
/// </summary>
public float GetRealWorldCrushDepth()
public bool IsCrushDepthDefinedInStructures(out float realWorldCrushDepth)
{
if (SubmarineElement == null) { return Level.DefaultRealWorldCrushDepth; }
if (SubmarineElement == null)
{
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
return false;
}
bool structureCrushDepthsDefined = false;
float realWorldCrushDepth = float.PositiveInfinity;
realWorldCrushDepth = float.PositiveInfinity;
foreach (var structureElement in SubmarineElement.GetChildElements("structure"))
{
string name = structureElement.Attribute("name")?.Value ?? "";
@@ -553,7 +557,7 @@ namespace Barotrauma
{
realWorldCrushDepth = Level.DefaultRealWorldCrushDepth;
}
return realWorldCrushDepth;
return structureCrushDepthsDefined;
}
//saving/loading ----------------------------------------------------
@@ -12,7 +12,7 @@ namespace Barotrauma.Networking
public readonly Either<Address, AccountId> AddressOrAccountId;
public readonly string Reason;
public DateTime? ExpirationTime;
public Option<SerializableDateTime> ExpirationTime;
public readonly UInt32 UniqueIdentifier;
}
@@ -160,7 +160,8 @@ namespace Barotrauma
{ typeof(Identifier), new ReadWriteBehavior<Identifier>(ReadIdentifier, WriteIdentifier) },
{ typeof(AccountId), new ReadWriteBehavior<AccountId>(ReadAccountId, WriteAccountId) },
{ typeof(Color), new ReadWriteBehavior<Color>(ReadColor, WriteColor) },
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) }
{ typeof(Vector2), new ReadWriteBehavior<Vector2>(ReadVector2, WriteVector2) },
{ typeof(SerializableDateTime), new ReadWriteBehavior<SerializableDateTime>(ReadSerializableDateTime, WriteSerializableDateTime) }
};
private static readonly ImmutableDictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>> BehaviorFactories = new Dictionary<Predicate<Type>, Func<Type, IReadWriteBehavior>>
@@ -512,6 +513,41 @@ namespace Barotrauma
WriteSingle(y, attribute, msg, bitField);
}
private static readonly Range<Int64> ValidTickRange
= new Range<Int64>(
start: DateTime.MinValue.Ticks,
end: DateTime.MaxValue.Ticks);
private static readonly Range<Int16> ValidTimeZoneMinuteRange
= new Range<Int16>(
start: (Int16)TimeSpan.FromHours(-12).TotalMinutes,
end: (Int16)TimeSpan.FromHours(14).TotalMinutes);
private static SerializableDateTime ReadSerializableDateTime(
IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField)
{
var ticks = inc.ReadInt64();
var timezone = inc.ReadInt16();
if (!ValidTickRange.Contains(ticks))
{
throw new Exception($"Incoming SerializableDateTime ticks out of range (ticks: {ticks}, timezone: {timezone})");
}
if (!ValidTimeZoneMinuteRange.Contains(timezone))
{
throw new Exception($"Incoming SerializableDateTime timezone out of range (ticks: {ticks}, timezone: {timezone})");
}
return new SerializableDateTime(new DateTime(ticks),
new SerializableTimeZone(TimeSpan.FromMinutes(timezone)));
}
private static void WriteSerializableDateTime(
SerializableDateTime dateTime, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField)
{
msg.WriteInt64(dateTime.Ticks);
msg.WriteInt16((Int16)(dateTime.TimeZone.Value.Ticks / TimeSpan.TicksPerMinute));
}
private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue;
private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue;
@@ -302,7 +302,7 @@ namespace Barotrauma.Networking
public ServerContentPackage() { }
public ServerContentPackage(ContentPackage contentPackage, DateTime referenceTime)
public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime)
{
Name = contentPackage.Name;
Hash = contentPackage.Hash;
@@ -1,15 +1,14 @@
using System;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using File = Barotrauma.IO.File;
using FileStream = Barotrauma.IO.FileStream;
using Path = Barotrauma.IO.Path;
@@ -315,7 +314,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
}
@@ -357,7 +356,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
return val;
@@ -376,7 +375,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
return val;
@@ -395,12 +394,22 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
return val;
}
public static Option<SerializableDateTime> GetAttributeDateTime(
this XElement element, string name)
{
var attribute = element?.GetAttribute(name);
if (attribute == null) { return Option<SerializableDateTime>.None(); }
string attrVal = attribute.Value;
return SerializableDateTime.Parse(attrVal);
}
public static Version GetAttributeVersion(this XElement element, string name, Version defaultValue)
{
var attribute = element?.GetAttribute(name);
@@ -414,7 +423,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
return val;
@@ -439,7 +448,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
}
@@ -464,7 +473,7 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
}
@@ -566,13 +575,26 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error when reading attribute \"{name}\" from {element}!", e);
LogAttributeError(attribute, element, e);
}
}
return colorValue;
}
private static void LogAttributeError(XAttribute attribute, XElement element, Exception e)
{
string elementStr = element.ToString();
if (elementStr.Length > 500)
{
DebugConsole.ThrowError($"Error when reading attribute \"{attribute}\"!", e);
}
else
{
DebugConsole.ThrowError($"Error when reading attribute \"{attribute.Name}\" from {elementStr}!", e);
}
}
#if CLIENT
public static KeyOrMouse GetAttributeKeyOrMouse(this XElement element, string name, KeyOrMouse defaultValue)
{
@@ -1455,7 +1455,7 @@ namespace Barotrauma
if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; }
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue);
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
RegisterTreatmentResults(entity, limb, affliction, result);
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
//only apply non-limb-specific afflictions to the first limb
if (!affliction.Prefab.LimbSpecific) { break; }
}
@@ -1467,7 +1467,7 @@ namespace Barotrauma
newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality);
AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue);
limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source);
RegisterTreatmentResults(entity, limb, affliction, result);
RegisterTreatmentResults(user, entity as Item, limb, affliction, result);
}
}
@@ -1498,17 +1498,18 @@ namespace Barotrauma
{
targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType);
}
targetCharacter.AIController?.OnHealed(healer: user, targetCharacter.Vitality - prevVitality);
if (user != null && user != targetCharacter)
if (!targetCharacter.IsDead)
{
if (!targetCharacter.IsDead)
float healthChange = targetCharacter.Vitality - prevVitality;
targetCharacter.AIController?.OnHealed(healer: user, healthChange);
if (user != null)
{
targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality);
}
};
targetCharacter.TryAdjustHealerSkill(user, healthChange);
#if SERVER
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f);
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, healthChange, 0.0f);
#endif
}
}
}
}
@@ -2073,14 +2074,14 @@ namespace Barotrauma
if (character.Removed) { continue; }
newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
var result = character.AddDamage(character.WorldPosition, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attacker: element.User);
element.Parent.RegisterTreatmentResults(element.Entity, result.HitLimb, affliction, result);
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, result.HitLimb, affliction, result);
}
else if (target is Limb limb)
{
if (limb.character.Removed || limb.Removed) { continue; }
newAffliction = element.Parent.GetMultipliedAffliction(affliction, element.Entity, limb.character, deltaTime, element.Parent.multiplyAfflictionsByMaxVitality);
var result = limb.character.DamageLimb(limb.WorldPosition, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: element.User);
element.Parent.RegisterTreatmentResults(element.Entity, limb, affliction, result);
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result);
}
}
@@ -2111,17 +2112,18 @@ namespace Barotrauma
{
targetCharacter.CharacterHealth.ReduceAfflictionOnAllLimbs(affliction, reduceAmount, treatmentAction: actionType);
}
if (element.User != null && element.User != targetCharacter)
if (!targetCharacter.IsDead)
{
targetCharacter.AIController?.OnHealed(healer: element.User, targetCharacter.Vitality - prevVitality);
if (!targetCharacter.IsDead)
float healthChange = targetCharacter.Vitality - prevVitality;
targetCharacter.AIController?.OnHealed(healer: element.User, healthChange);
if (element.User != null)
{
targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality);
}
};
targetCharacter.TryAdjustHealerSkill(element.User, healthChange);
#if SERVER
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f);
GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, healthChange, 0.0f);
#endif
}
}
}
}
}
@@ -2170,7 +2172,7 @@ namespace Barotrauma
{
afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier);
}
else if (affliction.Prefab.AfflictionType == "poison")
else if (affliction.Prefab.AfflictionType == "poison" || affliction.Prefab.AfflictionType == "paralysis")
{
afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier);
}
@@ -2183,23 +2185,25 @@ namespace Barotrauma
return affliction;
}
private void RegisterTreatmentResults(Entity entity, Limb limb, Affliction affliction, AttackResult result)
private void RegisterTreatmentResults(Character user, Item item, Limb limb, Affliction affliction, AttackResult result)
{
if (entity is Item item && item.UseInHealthInterface && limb != null)
if (item == null) { return; }
if (!item.UseInHealthInterface) { return; }
if (limb == null) { return; }
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
{
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
{
if (result.Afflictions != null && result.Afflictions.Any(a => a.Prefab == limbAffliction.Prefab) &&
(!affliction.Prefab.LimbSpecific || limb.character.CharacterHealth.GetAfflictionLimb(affliction) == limb))
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
{
if (type == ActionType.OnUse || type == ActionType.OnSuccess)
{
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
else if (type == ActionType.OnFailure)
{
limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
}
limbAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
}
else if (type == ActionType.OnFailure)
{
limbAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
limb.character.TryAdjustHealerSkill(user, affliction: affliction);
}
}
}
@@ -495,7 +495,8 @@ namespace Barotrauma.Steam
new XAttribute("corepackage", isCorePackage),
new XAttribute("modversion", modVersion),
new XAttribute("gameversion", gameVersion),
new XAttribute("installtime", ToolBox.Epoch.FromDateTime(updateTime)));
#warning TODO: stop writing Unix time after this gets on main
new XAttribute("installtime", new SerializableDateTime(updateTime).ToUnixTime()));
if ((modPathDirName ?? modName).ToIdentifier() != itemTitle)
{
root.Add(new XAttribute("altnames", modPathDirName ?? modName));
@@ -302,6 +302,7 @@ namespace Barotrauma
UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
}
// TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
if (character.CharacterHealth?.GetAffliction("morbusinepoisoning") != null)
{
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
@@ -315,6 +316,7 @@ namespace Barotrauma
}
else
{
// TODO: should we change this? Morbusine used to be the strongest poison. Now Cyanide is strongest.
if (item.Prefab.Identifier == "morbusine")
{
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
@@ -46,7 +46,7 @@ namespace Barotrauma
case int _:
case double _:
{
var value = (float) OriginalValue;
var value = Convert.ToSingle(OriginalValue);
return level == 0 ? value : CalculateUpgrade(value, level, Multiplier);
}
case bool _ when bool.TryParse(Multiplier, out bool result):
@@ -546,7 +546,7 @@ namespace Barotrauma
foreach (Item item in itemsToRemove)
{
item.Remove();
Entity.Spawner.AddItemToRemoveQueue(item);
}
if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
@@ -0,0 +1,26 @@
using Microsoft.Xna.Framework;
namespace Barotrauma.Utils;
public struct CoordinateSpace2D
{
public static readonly CoordinateSpace2D CanonicalSpace = new CoordinateSpace2D
{
Origin = Vector2.Zero,
I = Vector2.UnitX,
J = Vector2.UnitY
};
public Vector2 Origin;
public Vector2 I;
public Vector2 J;
public Matrix LocalToCanonical
=> new Matrix(
m11: I.X, m12: I.Y, m13: 0f, m14: 0f,
m21: J.X, m22: J.Y, m23: 0f, m24: 0f,
m31: 0f, m32: 0f, m33: 1f, m34: 0f,
m41: 0f, m42: 0f, m43: 0f, m44: 1f)
* Matrix.CreateTranslation(Origin.X, Origin.Y, 0f);
public Matrix CanonicalToLocal => Matrix.Invert(LocalToCanonical);
}
@@ -19,6 +19,9 @@ namespace Barotrauma
static class MathUtils
{
public static Vector2 DiscardZ(this Vector3 vector)
=> new Vector2(vector.X, vector.Y);
public static float Percentage(float portion, float total)
{
return portion / total * 100;
@@ -8,6 +8,7 @@ using System.Xml.Linq;
using System.Text.RegularExpressions;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -292,10 +293,11 @@ namespace Barotrauma
}
if (doc?.Root == null)
{
saveInfos.Add(new CampaignMode.SaveInfo()
{
FilePath = file
});
saveInfos.Add(new CampaignMode.SaveInfo(
FilePath: file,
SaveTime: Option.None,
SubmarineName: "",
EnabledContentPackageNames: ImmutableArray<string>.Empty));
}
else
{
@@ -326,13 +328,11 @@ namespace Barotrauma
enabledContentPackageNames.Add(packageName.Replace(@"\|", "|"));
}
saveInfos.Add(new CampaignMode.SaveInfo()
{
FilePath = file,
SubmarineName = doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
SaveTime = doc.Root.GetAttributeInt("savetime", 0),
EnabledContentPackageNames = enabledContentPackageNames.ToArray(),
});
saveInfos.Add(new CampaignMode.SaveInfo(
FilePath: file,
SaveTime: doc.Root.GetAttributeDateTime("savetime"),
SubmarineName: doc?.Root?.GetAttributeStringUnrestricted("submarine", ""),
EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
}
}
@@ -0,0 +1,266 @@
#nullable enable
using System;
using System.Globalization;
using System.Linq;
namespace Barotrauma
{
public readonly struct SerializableTimeZone
{
/// <summary>
/// Diff from UTC
/// </summary>
public readonly TimeSpan Value;
private readonly int hours;
private readonly int minutes;
private readonly char sign;
public SerializableTimeZone(TimeSpan value)
{
Value = new TimeSpan(
hours: value.Hours,
minutes: value.Minutes,
seconds: 0);
hours = Math.Abs(value.Hours);
minutes = Math.Abs(value.Minutes);
sign = Value.Ticks < 0 ? '-' : '+';
}
public override string ToString()
=> (hours, minutes) switch
{
(0, 0) => "UTC",
(_, 0) => $"UTC{sign}{hours}",
(_, < 10) => $"UTC{sign}{hours}:0{minutes}",
_ => $"UTC{sign}{hours}:{minutes}"
};
public override int GetHashCode()
=> HashCode.Combine(Value.Ticks < 0, hours, minutes);
public static SerializableTimeZone FromDateTime(DateTime dateTime)
{
if (dateTime.Kind == DateTimeKind.Unspecified)
{
throw new InvalidOperationException(
$"Cannot determine timezone for {nameof(DateTime)} " +
$"of unspecified kind");
}
var utcDateTime = dateTime.ToUniversalTime();
return new SerializableTimeZone(dateTime - utcDateTime);
}
public static SerializableTimeZone LocalTimeZone
=> FromDateTime(DateTime.Now);
public static Option<SerializableTimeZone> Parse(string str)
{
if (!str.StartsWith("UTC", StringComparison.OrdinalIgnoreCase))
{
return Option<SerializableTimeZone>.None();
}
string timeZoneStr = str[3..];
bool negative = timeZoneStr.StartsWith("-");
bool valid = negative || timeZoneStr.StartsWith("+");
if (!valid) { return Option<SerializableTimeZone>.None(); }
timeZoneStr = str[4..];
TimeSpan makeTimeSpan(int hours, int minutes)
=> new TimeSpan(
ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute)
* (negative ? -1L : 1L));
if (timeZoneStr.IndexOf(':') is var hrMinSeparator && hrMinSeparator > 0)
{
if (int.TryParse(timeZoneStr[..hrMinSeparator], out int timeZoneHours)
&& int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out int timeZoneMinutes))
{
return Option<SerializableTimeZone>.Some(
new SerializableTimeZone(makeTimeSpan(timeZoneHours, timeZoneMinutes)));
}
}
else if (int.TryParse(timeZoneStr, out int timeZoneHours))
{
return Option<SerializableTimeZone>.Some(
new SerializableTimeZone(makeTimeSpan(timeZoneHours, 0)));
}
return Option<SerializableTimeZone>.None();
}
}
/// <summary>
/// DateTime wrapper that tries to offer a reliable
/// string representation that's also human-friendly
/// </summary>
public readonly struct SerializableDateTime : IComparable<SerializableDateTime>
{
public bool Equals(SerializableDateTime other)
=> ToUtc().value.Equals(other.ToUtc().value);
public override bool Equals(object? obj)
=> obj is SerializableDateTime other && Equals(other);
private static DateTime UnixEpoch(DateTimeKind kind)
=> new DateTime(1970, 1, 1, 0, 0, 0, kind);
private readonly DateTime value;
public readonly SerializableTimeZone TimeZone;
public SerializableDateTime(DateTime value) : this(value, default)
{
if (value.Kind == DateTimeKind.Unspecified)
{
throw new Exception($"Timezone required when constructing {nameof(SerializableDateTime)} " +
$"from {nameof(DateTime)} of unspecified kind");
}
TimeZone = SerializableTimeZone.FromDateTime(value);
}
public SerializableDateTime(DateTime value, SerializableTimeZone timeZone)
{
this.value = new DateTime(
value.Year, value.Month, value.Day,
value.Hour, value.Minute, value.Second,
DateTimeKind.Unspecified);
TimeZone = timeZone;
}
public static SerializableDateTime LocalNow
=> new SerializableDateTime(DateTime.Now);
public static SerializableDateTime UtcNow
=> new SerializableDateTime(DateTime.UtcNow);
public SerializableDateTime ToUtc()
=> new SerializableDateTime(
DateTime.SpecifyKind(value - TimeZone.Value, DateTimeKind.Utc));
public SerializableDateTime ToLocal()
=> new SerializableDateTime(
DateTime.SpecifyKind(
value - TimeZone.Value + SerializableTimeZone.LocalTimeZone.Value,
DateTimeKind.Local));
public long Ticks => value.Ticks;
public DateTime ToUtcValue() => ToUtc().value;
public DateTime ToLocalValue() => ToLocal().value;
public static SerializableDateTime FromLocalUnixTime(long unixTime)
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Local) + TimeSpan.FromSeconds(unixTime));
public static SerializableDateTime FromUtcUnixTime(long unixTime)
=> new SerializableDateTime(UnixEpoch(DateTimeKind.Utc) + TimeSpan.FromSeconds(unixTime));
public long ToUnixTime()
=> (value - UnixEpoch(value.Kind)).Ticks / TimeSpan.TicksPerSecond;
private static string MakeString(params (long Value, string Suffix)[] parts)
=> string.Join(' ',
parts.Select(p => $"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}"));
public override string ToString()
=> MakeString(
// Let's go out of our way to tag
// the year, month and day so nobody
// gets confused about the meaning of
// each number
(value.Year, "Y"),
(value.Month, "M"),
(value.Day, "D"),
(value.Hour, "HR"),
(value.Minute, "MIN"),
(value.Second, "SEC"))
+ $" {TimeZone}";
public string ToLocalUserString()
=> ToLocalValue().ToString(CultureInfo.InvariantCulture);
public override int GetHashCode()
=> HashCode.Combine(
value.Year, value.Month, value.Day,
value.Hour, value.Minute, value.Second,
TimeZone.GetHashCode());
public static Option<SerializableDateTime> Parse(string str)
{
if (long.TryParse(str, out long unixTime)
&& unixTime > 0
&& unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds)
{
return Option<SerializableDateTime>.Some(FromUtcUnixTime(unixTime));
}
string[] split = str.Split(' ');
int year = 0; int month = 0; int day = 0;
int hour = 0; int minute = 0; int second = 0;
SerializableTimeZone timeZone = default;
foreach (var part in split)
{
if (SerializableTimeZone.Parse(part).TryUnwrap(out var parsedTimeZone))
{
timeZone = parsedTimeZone;
continue;
}
Identifier suffix = string.Join("", part.Where(char.IsLetter)).ToIdentifier();
if (!part.EndsWith(suffix.Value)) { continue; }
if (!int.TryParse(
part[..^suffix.Value.Length],
NumberStyles.Integer,
CultureInfo.InvariantCulture,
out int value))
{
continue;
}
if (suffix == "Y") { year = value; }
else if (suffix == "M") { month = value; }
else if (suffix == "D") { day = value; }
else if (suffix == "HR") { hour = value; }
else if (suffix == "MIN") { minute = value; }
else if (suffix == "SEC") { second = value; }
}
if (year > 0 && month > 0 && day > 0)
{
return Option<SerializableDateTime>.Some(
new SerializableDateTime(
new DateTime(year, month, day, hour, minute, second),
timeZone));
}
return Option<SerializableDateTime>.None();
}
public int CompareTo(SerializableDateTime other)
=> ToUtc().value.CompareTo(other.ToUtc().value);
public static bool operator <(in SerializableDateTime a, in SerializableDateTime b)
=> a.CompareTo(b) < 0;
public static bool operator >(in SerializableDateTime a, in SerializableDateTime b)
=> a.CompareTo(b) > 0;
public static bool operator ==(in SerializableDateTime a, in SerializableDateTime b)
=> a.CompareTo(b) == 0;
public static bool operator !=(in SerializableDateTime a, in SerializableDateTime b)
=> !(a == b);
public static SerializableDateTime operator +(in SerializableDateTime dt, in TimeSpan ts)
=> new SerializableDateTime(dt.value + ts, dt.TimeZone);
public static SerializableDateTime operator -(in SerializableDateTime dt, in TimeSpan ts)
=> new SerializableDateTime(dt.value - ts, dt.TimeZone);
public static TimeSpan operator -(in SerializableDateTime a, in SerializableDateTime b)
=> a.ToUtc().value - b.ToUtc().value;
}
}
@@ -29,49 +29,6 @@ namespace Barotrauma
static partial class ToolBox
{
internal static class Epoch
{
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Returns the current Unix Epoch (Coordinated Universal Time)
/// </summary>
public static int NowUTC
{
get
{
return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds);
}
}
/// <summary>
/// Returns the current Unix Epoch (user's current time)
/// </summary>
public static int NowLocal
{
get
{
return (int)(DateTime.Now.Subtract(epoch).TotalSeconds);
}
}
/// <summary>
/// Convert an epoch to a datetime
/// </summary>
public static DateTime ToDateTime(decimal unixTime)
{
return epoch.AddSeconds((long)unixTime);
}
/// <summary>
/// Convert a DateTime to a unix time
/// </summary>
public static uint FromDateTime(DateTime dt)
{
return (uint)(dt.Subtract(epoch).TotalSeconds);
}
}
public static bool IsProperFilenameCase(string filename)
{
//File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.