Unstable 0.17.1.0

This commit is contained in:
Markus Isberg
2022-03-17 01:25:04 +09:00
parent 3974067915
commit 6d410cc1b7
302 changed files with 5878 additions and 3317 deletions
@@ -388,7 +388,7 @@ namespace Barotrauma
break;
}
}
if (targetingTag == null)
if (targetingTag.IsNullOrEmpty())
{
if (targetItem.GetComponent<Sonar>() != null)
{
@@ -2100,15 +2100,11 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new object[]
{
Networking.NetEntityEvent.Type.SetAttackTarget,
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character character && targetLimb != null ? Array.IndexOf(character.AnimController.Limbs, targetLimb) : 0,
SimPosition.X,
SimPosition.Y
});
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
@@ -2696,7 +2692,7 @@ namespace Barotrauma
float target = targetParams.Threshold;
if (targetParams.ThresholdMin > 0 && targetParams.ThresholdMax > 0)
{
target = selectedTargetingParams == targetParams ? targetParams.ThresholdMax : targetParams.ThresholdMin;
target = selectedTargetingParams == targetParams && State == AIState.FleeTo ? targetParams.ThresholdMax : targetParams.ThresholdMin;
}
if (Character.HealthPercentage > target)
{
@@ -569,7 +569,8 @@ namespace Barotrauma
(Character.Submarine.TeamID != Character.TeamID && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10;
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
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))
@@ -1265,15 +1266,15 @@ namespace Barotrauma
{
if (!IsFriendly(attacker))
{
if (Character.Submarine == null)
if (c.Submarine == null)
{
// Outside
return attacker.Submarine == null ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
if (!Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
if (!c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Attacked from an unconnected submarine.
return Character.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
return c.SelectedConstruction?.GetComponent<Turret>() != null ? AIObjectiveCombat.CombatMode.None : AIObjectiveCombat.CombatMode.Retreat;
}
return c.AIController is HumanAIController humanAI &&
(humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders))
@@ -1285,18 +1286,22 @@ namespace Barotrauma
{
cumulativeDamage = 100;
}
if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID)
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
{
// Bots in the player team never act aggressively in single player when attacked by the player
return cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
if (GameMain.IsSingleplayer || Character.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
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
}
}
if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
{
// Outside or attacked from an unconnected submarine -> don't react.
return AIObjectiveCombat.CombatMode.None;
}
// If there are any enemies around, just ignore the friendly fire
if (Character.CharacterList.Any(ch => ch.Submarine == Character.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
if (Character.CharacterList.Any(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && !IsFriendly(ch) && VisibleHulls.Contains(ch.CurrentHull)))
{
isAttackerFightingEnemy = true;
return AIObjectiveCombat.CombatMode.None;
@@ -1352,18 +1357,19 @@ namespace Barotrauma
Character FindInstigator()
{
if (Character.IsInstigator)
if (attacker.IsInstigator)
{
return Character;
return attacker;
}
else if (c.AIController is HumanAIController humanAi)
if (c.IsInstigator)
{
return c;
}
if (c.AIController is HumanAIController humanAi)
{
return Character.CharacterList.FirstOrDefault(ch => ch.Submarine == c.Submarine && !ch.Removed && !ch.IsIncapacitated && ch.IsInstigator && humanAi.VisibleHulls.Contains(ch.CurrentHull));
}
else
{
return null;
}
return null;
}
}
}
@@ -1497,7 +1503,7 @@ namespace Barotrauma
if (hull == null ||
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
return true;
@@ -30,6 +30,7 @@ namespace Barotrauma
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
if (item.ConditionPercentage <= 0) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(battery)) { return false; }
return true;
@@ -83,7 +83,8 @@ namespace Barotrauma
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character);
IsItemInsideValidSubmarine(container, character) &&
!container.IsClaimedByBallastFlora;
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
@@ -100,6 +101,7 @@ namespace Barotrauma
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (pickable is Holdable h && h.Attachable && h.Attached) { return false; }
@@ -777,7 +777,7 @@ namespace Barotrauma
}
if (retreatTarget != null && character.CurrentHull != retreatTarget)
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true)
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{
UsePathingOutside = false
},
@@ -162,7 +162,6 @@ namespace Barotrauma
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false,
requiredCondition = () => Leak.Submarine == character.Submarine,
// 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()
@@ -1,7 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Barotrauma.Extensions;
@@ -11,6 +10,8 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "go to".ToIdentifier();
public override bool KeepDivingGearOn => GetTargetHull() == null;
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
@@ -74,14 +75,6 @@ namespace Barotrauma
_closeEnough = Math.Max(minDistance, value);
}
}
// TODO: Currently we never check the visibility (to the end node), which is actually unintentional.
// I don't think it has caused any issues so far, so let's keep defaulting to false for now, because the less we do raycasts the better.
// However, if there are cases where the bots attempt to go through walls (select the end node that is behind an obstacle), we should set this true.
// NOTE: This seemes to have caused an issue now Regalis11/Barotrauma#8067: namely, the bot was trying to use a waypoint that was obstructed by a shuttle
// because obstruction was only checked when checking visibility in PathFinder. Changed that so that obstructed nodes are no longer used.
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -268,15 +261,15 @@ namespace Barotrauma
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit)) && character.NeedsAir;
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit));
if (Mimic)
{
if (HumanAIController.HasDivingSuit(followTarget) && character.NeedsAir)
if (HumanAIController.HasDivingSuit(followTarget))
{
needsDivingGear = true;
needsDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.NeedsAir)
else if (HumanAIController.HasDivingMask(followTarget))
{
needsDivingGear = true;
}
@@ -505,7 +498,7 @@ namespace Barotrauma
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: CheckVisibility);
checkVisiblity: Target is Item || Target is Character);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
@@ -60,6 +60,7 @@ namespace Barotrauma
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
if (!item.HasAccess(character)) { return false; }
// Ignore items that require power but don't have it
if (item.GetComponent<Powered>() is Powered powered && powered.PowerConsumption > 0 && powered.Voltage < powered.MinVoltage) { return false; }
@@ -10,6 +10,16 @@ namespace Barotrauma
{
class AIObjectiveManager
{
public enum ObjectiveType
{
None = 0,
Order = 1,
Objective = 2,
MinValue = 0,
MaxValue = 2
}
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
@@ -184,28 +194,20 @@ namespace Barotrauma
{
var previousObjective = CurrentObjective;
var firstObjective = Objectives.FirstOrDefault();
bool currentObjectiveIsOrder = CurrentOrder != null && firstObjective != null && CurrentOrder.Priority > firstObjective.Priority;
if (currentObjectiveIsOrder)
CurrentObjective = currentObjectiveIsOrder ? CurrentOrder : firstObjective;
if (previousObjective == CurrentObjective) { return CurrentObjective; }
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (GameMain.NetworkMember is { IsServer: true })
{
CurrentObjective = CurrentOrder;
}
else
{
CurrentObjective = firstObjective;
}
if (previousObjective != CurrentObjective)
{
previousObjective?.OnDeselected();
CurrentObjective?.OnSelected();
GetObjective<AIObjectiveIdle>().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0));
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ObjectiveManagerState,
currentObjectiveIsOrder ? "order" : "objective"
});
}
GameMain.NetworkMember.CreateEntityEvent(character,
new Character.ObjectiveManagerStateEventData(currentObjectiveIsOrder ? ObjectiveType.Order : ObjectiveType.Objective));
}
return CurrentObjective;
}
@@ -67,6 +67,11 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
else if (targetItem.IsClaimedByBallastFlora)
{
Priority = 0;
return Priority;
}
var reactor = component?.Item.GetComponent<Reactor>();
if (reactor != null)
{
@@ -41,6 +41,7 @@ namespace Barotrauma
if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (pump.Item.IsClaimedByBallastFlora) { return false; }
if (IsReady(pump)) { return false; }
return true;
}
@@ -1,7 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
@@ -12,6 +11,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInAnySub => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public Item Item { get; private set; }
@@ -52,6 +52,10 @@ namespace Barotrauma
Priority = 0;
IsCompleted = true;
}
else if (Item.IsClaimedByBallastFlora)
{
Priority = 0;
}
else
{
float distanceFactor = 1;
@@ -151,6 +151,7 @@ namespace Barotrauma
if (!item.IsInteractable(character)) { return false; }
if (item.IsFullCondition) { return false; }
if (item.Submarine == null || character.Submarine == null) { return false; }
if (item.IsClaimedByBallastFlora) { return false; }
//player crew ignores items in outposts
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
@@ -30,6 +30,7 @@ namespace Barotrauma
private float findHullTimer;
private bool ignoreOxygen;
private readonly float findHullInterval = 1.0f;
private bool performedCpr;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -220,12 +221,12 @@ namespace Barotrauma
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
@@ -401,6 +402,7 @@ namespace Barotrauma
{
character.SelectCharacter(targetCharacter);
character.AnimController.Anim = AnimController.Animation.CPR;
performedCpr = true;
}
else
{
@@ -436,9 +438,10 @@ namespace Barotrauma
{
bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) >= AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager, character, targetCharacter);
if (isCompleted && targetCharacter != character && character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name).Value,
null, 1.0f, $"targethealed{targetCharacter.Name}".ToIdentifier(), 60.0f);
{
string textTag = performedCpr ? "DialogTargetResuscitated" : "DialogTargetHealed";
string message = TextManager.GetWithVariable(textTag, "[targetname]", targetCharacter.Name)?.Value;
character.Speak(message, delay: 1.0f, identifier: $"targethealed{targetCharacter.Name}".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
return isCompleted;
}
@@ -392,7 +392,6 @@ namespace Barotrauma
return option;
}
public ImmutableArray<Identifier> GetTargetItems(Identifier option = default)
{
if (option.IsEmpty || !OptionTargetItems.TryGetValue(option, out ImmutableArray<Identifier> optionTargetItems))
@@ -418,6 +417,28 @@ namespace Barotrauma
}
public override void Dispose() { }
/// <summary>
/// Create an Order instance with a null target
/// </summary>
public Order CreateInstance(OrderTargetType targetType, Character orderGiver = null, bool isAutonomous = false)
{
try
{
return targetType switch
{
OrderTargetType.Entity => new Order(this, targetEntity: null, targetItem: null, orderGiver, isAutonomous),
OrderTargetType.Position => new Order(this, target: null, orderGiver),
OrderTargetType.WallSection => new Order(this, wall: null, sectionIndex: null, orderGiver),
_ => throw new NotImplementedException()
};
}
catch (NotImplementedException e)
{
DebugConsole.ShowError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}");
return null;
}
}
}
class Order
@@ -510,28 +531,45 @@ namespace Barotrauma
public readonly bool UseController;
/// <summary>
/// Constructor for order instances
/// Constructor for orders with the target type OrderTargetType.Entity
/// </summary>
public Order(OrderPrefab prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
: this(prefab, Identifier.Empty, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { }
/// <summary>
/// Constructor for orders with the target type OrderTargetType.Entity
/// </summary>
public Order(OrderPrefab prefab, Identifier option, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
: this(prefab, option, 0, OrderType.Current, null, targetEntity, targetItem, orderGiver, isAutonomous) { }
/// <summary>
/// Constructor for orders with the target type OrderTargetType.Position
/// </summary>
public Order(OrderPrefab prefab, OrderTarget target, Character orderGiver = null)
: this(prefab, prefab.Options.FirstOrDefault(), 0, OrderType.Current, null, target, orderGiver) { }
/// <summary>
/// Constructor for orders with the target type OrderTargetType.Position
/// </summary>
public Order(OrderPrefab prefab, Identifier option, OrderTarget target, Character orderGiver = null)
: this(prefab, option, 0, OrderType.Current, null, target, orderGiver) { }
/// <summary>
/// Constructor for orders with the target type OrderTargetType.WallSection
/// </summary>
public Order(OrderPrefab prefab, Structure wall, int? sectionIndex, Character orderGiver = null)
: this(prefab, Identifier.Empty, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { }
/// <summary>
/// Constructor for orders with the target type OrderTargetType.WallSection
/// </summary>
public Order(OrderPrefab prefab, Identifier option, Structure wall, int? sectionIndex, Character orderGiver = null)
: this(prefab, option, 0, OrderType.Current, null, wall, sectionIndex, orderGiver) { }
public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
/// <summary>
/// Constructor for orders with the target type OrderTargetType.Entity
/// </summary>
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false)
{
Prefab = prefab;
Option = option;
@@ -561,14 +599,20 @@ namespace Barotrauma
TargetType = OrderTargetType.Entity;
}
public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null)
/// <summary>
/// Constructor for orders with the target type OrderTargetType.Position
/// </summary>
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, OrderTarget target, Character orderGiver = null)
: this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: null, targetItem: null, orderGiver)
{
TargetPosition = target;
TargetType = OrderTargetType.Position;
}
public Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null)
/// <summary>
/// Constructor for orders with the target type OrderTargetType.WallSection
/// </summary>
private Order(OrderPrefab prefab, Identifier option, int manualPriority, OrderType orderType, AIObjective aiObjective, Structure wall, int? sectionIndex, Character orderGiver = null)
: this(prefab, option, manualPriority, orderType, aiObjective, targetEntity: wall, null, orderGiver: orderGiver)
{
WallSectionIndex = sectionIndex;
@@ -633,7 +677,7 @@ namespace Barotrauma
public Order WithTargetEntity(Entity entity)
{
return new Order(this, targetEntity: entity);
return new Order(this, targetEntity: entity, targetType: OrderTargetType.Entity);
}
public Order WithTargetSpatialEntity(ISpatialEntity spatialEntity)
@@ -673,7 +717,7 @@ namespace Barotrauma
public Order WithTargetPosition(OrderTarget targetPosition)
{
return new Order(this, targetPosition: targetPosition);
return new Order(this, targetPosition: targetPosition, targetType: OrderTargetType.Position);
}
public Order Clone()
@@ -313,8 +313,7 @@ namespace Barotrauma
ShipCommandLog("Dismissing " + shipIssueWorker + " for character " + shipIssueWorker.OrderedCharacter);
#endif
var order = new Order(OrderPrefab.Dismissal, null).WithManualPriority(3).WithOrderGiver(character);
//character.Speak(orderPrefab.GetChatMessage(shipIssueWorker.OrderedCharacter.Name, "", givingOrderToSelf: false));
shipIssueWorker.OrderedCharacter.SetOrder(order);
shipIssueWorker.OrderedCharacter.SetOrder(order, isNewOrder: true);
shipIssueWorker.RemoveOrder();
break;
}
@@ -368,7 +367,7 @@ namespace Barotrauma
ShipGlobalIssueFixLeaks shipGlobalIssueFixLeaks = new ShipGlobalIssueFixLeaks(this);
for (int i = 0; i < crewSizeModifier; i++)
{
var order = new Order(OrderPrefab.Prefabs["fixleaks"], null);
var order = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
ShipIssueWorkers.Add(new ShipIssueWorkerFixLeaks(this, order, shipGlobalIssueFixLeaks));
}
shipGlobalIssues.Add(shipGlobalIssueFixLeaks);
@@ -376,7 +375,7 @@ namespace Barotrauma
ShipGlobalIssueRepairSystems shipGlobalIssueRepairSystems = new ShipGlobalIssueRepairSystems(this);
for (int i = 0; i < crewSizeModifier; i++)
{
var order = new Order(OrderPrefab.Prefabs["repairsystems"], null);
var order = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
ShipIssueWorkers.Add(new ShipIssueWorkerRepairSystems(this, order, shipGlobalIssueRepairSystems));
}
shipGlobalIssues.Add(shipGlobalIssueRepairSystems);
@@ -442,7 +442,7 @@ namespace Barotrauma
}
#if SERVER
public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client client, NetEntityEvent.IData extraData = null)
{
msg.Write(IsAlive);
}
@@ -1017,24 +1017,29 @@ namespace Barotrauma
{
if (l.IsSevered) { continue; }
float rotation = l.body.Rotation;
if (l.DoesFlip)
{
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle
rotation = -(l.body.Rotation + MathHelper.Pi);
}
else
{
//vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards)
rotation = -l.body.Rotation;
}
}
TrySetLimbPosition(l,
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
rotation,
lerp);
l.body.PositionSmoothingFactor = 0.8f;
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle
l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi));
}
else
{
//vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards)
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
}
}
if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter))
{
@@ -1846,11 +1846,9 @@ namespace Barotrauma
}
float angle = flipAngle ? -limb.body.Rotation : limb.body.Rotation;
if (wrapAngle) angle = MathUtils.WrapAnglePi(angle);
if (wrapAngle) { angle = MathUtils.WrapAnglePi(angle); }
TrySetLimbPosition(limb, Collider.SimPosition, position);
limb.body.SetTransform(limb.body.SimPosition, angle);
TrySetLimbPosition(limb, Collider.SimPosition, position, angle);
}
}
@@ -803,9 +803,9 @@ namespace Barotrauma
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
GameMain.NetworkMember.CreateEntityEvent(character, new Character.StatusEventData());
}
return true;
}
@@ -1693,7 +1693,7 @@ namespace Barotrauma
if (limb.IsSevered) { continue; }
//check visibility from the new position of the collider to the new position of this limb
Vector2 movePos = limb.SimPosition + limbMoveAmount;
TrySetLimbPosition(limb, simPosition, movePos, lerp, ignorePlatforms);
TrySetLimbPosition(limb, simPosition, movePos, limb.Rotation, lerp, ignorePlatforms);
}
}
}
@@ -1708,7 +1708,7 @@ namespace Barotrauma
IsHanging = true;
}
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, bool lerp = false, bool ignorePlatforms = true)
protected void TrySetLimbPosition(Limb limb, Vector2 original, Vector2 simPosition, float rotation, bool lerp = false, bool ignorePlatforms = true)
{
Vector2 movePos = simPosition;
@@ -1730,11 +1730,12 @@ namespace Barotrauma
if (lerp)
{
limb.body.TargetPosition = movePos;
limb.body.MoveToTargetPosition(true);
limb.body.TargetRotation = rotation;
limb.body.MoveToTargetPosition(true);
}
else
{
limb.body.SetTransform(movePos, limb.Rotation);
limb.body.SetTransform(movePos, rotation);
limb.PullJointWorldAnchorB = limb.PullJointWorldAnchorA;
limb.PullJointEnabled = false;
}
@@ -487,6 +487,10 @@ namespace Barotrauma
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (targetCharacter != null)
{
if (effect.HasTargetType(StatusEffect.TargetType.Character))
@@ -551,6 +555,10 @@ namespace Barotrauma
{
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
@@ -25,7 +25,7 @@ namespace Barotrauma
FriendlyNPC = 3
}
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
public readonly static List<Character> CharacterList = new List<Character>();
@@ -130,6 +130,22 @@ namespace Barotrauma
}
}
private Wallet wallet = new Wallet();
public Wallet Wallet
{
get
{
ThrowIfAccessingWalletsInSingleplayer();
return wallet;
}
set
{
ThrowIfAccessingWalletsInSingleplayer();
wallet = value;
}
}
public readonly HashSet<LatchOntoAI> Latchers = new HashSet<LatchOntoAI>();
public readonly HashSet<Projectile> AttachedProjectiles = new HashSet<Projectile>();
@@ -137,6 +153,17 @@ namespace Barotrauma
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
public static void ThrowIfAccessingWalletsInSingleplayer()
{
#if CLIENT && DEBUG
if (Screen.Selected is TestScreen) { return; }
#endif
if (GameMain.NetworkMember is null || GameMain.IsSingleplayer)
{
throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead.");
}
}
public void SetOriginalTeam(CharacterTeamType newTeam)
{
TryRemoveTeamChange(OriginalTeamIdentifier);
@@ -158,12 +185,11 @@ namespace Barotrauma
return;
}
// clear up any duties the character might have had from its old team (autonomous objectives are automatically recreated)
var order = new Order(OrderPrefab.Dismissal, Identifier.Empty,
manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this);
SetOrder(order, speak: false);
var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
SetOrder(order, isNewOrder: true, speak: false);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.TeamChange });
GameMain.NetworkMember.CreateEntityEvent(this, new TeamChangeEventData());
#endif
}
@@ -225,9 +251,8 @@ namespace Barotrauma
if (bestTeamChange.AggressiveBehavior) // this seemed like the least disruptive way to induce aggressive behavior
{
var order = new Order(OrderPrefab.Prefabs["fightintruders"], Identifier.Empty,
manualPriority: 3, orderType: Order.OrderType.Current, aiObjective: null, target: null, orderGiver: this);
SetOrder(order, speak: false);
var order = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: this).WithManualPriority(CharacterInfo.HighestManualOrderPriority);
SetOrder(order, isNewOrder: true, speak: false);
}
}
}
@@ -1023,7 +1048,7 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null && Spawner != null && createNetworkEvent)
{
Spawner.CreateNetworkEvent(newCharacter, false);
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newCharacter));
}
#endif
return newCharacter;
@@ -1178,6 +1203,10 @@ namespace Barotrauma
info = new CharacterInfo(nonHuskedSpeciesName);
}
}
else if (Params.HasInfo && info == null)
{
info = new CharacterInfo(speciesName);
}
if (IsHumanoid)
{
@@ -1418,9 +1447,9 @@ namespace Barotrauma
{
item.AddTag(s);
}
if (createNetworkEvent && (GameMain.NetworkMember?.IsServer ?? false))
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, item.SerializableProperties[nameof(item.Tags).ToIdentifier()] });
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()]));
}
}
}
@@ -3199,7 +3228,7 @@ namespace Barotrauma
}
/// <param name="force">Force an order to be set for the character, bypassing hearing checks</param>
public void SetOrder(Order order, bool speak = true, bool force = false)
public void SetOrder(Order order, bool isNewOrder, bool speak = true, bool force = false)
{
var orderGiver = order?.OrderGiver;
//set the character order only if the character is close enough to hear the message
@@ -3226,7 +3255,7 @@ namespace Barotrauma
if (currentOrder.Identifier != order.Identifier) { continue; }
if (currentOrder.TargetEntity != order.TargetEntity) { continue; }
if (!currentOrder.AutoDismiss) { continue; }
character.SetOrder(currentOrder.GetDismissal(), speak: speak, force: force);
character.SetOrder(currentOrder.GetDismissal(), isNewOrder, speak: speak, force: force);
break;
}
}
@@ -3243,7 +3272,7 @@ namespace Barotrauma
}
if (orderToReplace is { AutoDismiss: true })
{
SetOrder(orderToReplace.GetDismissal(), speak: speak, force: force);
SetOrder(orderToReplace.GetDismissal(), isNewOrder, speak: speak, force: force);
}
break;
}
@@ -3251,10 +3280,10 @@ namespace Barotrauma
}
// Prevent adding duplicate orders
bool wasDuplicate = RemoveDuplicateOrders(order);
RemoveDuplicateOrders(order);
AddCurrentOrder(order);
if (orderGiver != null && order.Identifier != "dismissed" && !wasDuplicate)
if (orderGiver != null && order.Identifier != "dismissed" && isNewOrder)
{
var abilityOrderedCharacter = new AbilityOrderedCharacter(this);
orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter);
@@ -3976,14 +4005,14 @@ namespace Barotrauma
HealthUpdateInterval = 0.0f;
//clients aren't allowed to kill characters unless they receive a network message
if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
if (!isNetworkMessage && GameMain.NetworkMember is { IsClient: true })
{
return;
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData());
}
isDead = true;
@@ -4072,15 +4101,7 @@ namespace Barotrauma
}
}
if (GameMain.GameSession != null)
{
if (GameMain.GameSession.Campaign != null && TeamID == CharacterTeamType.Team1 && !IsAssistant)
{
GameMain.GameSession.Campaign.CrewHasDied = true;
}
GameMain.GameSession.KillCharacter(this);
}
GameMain.GameSession?.KillCharacter(this);
}
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
@@ -4245,7 +4266,7 @@ namespace Barotrauma
if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status });
GameMain.NetworkMember.CreateEntityEvent(newItem, new StatusEventData());
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
@@ -4334,7 +4355,7 @@ namespace Barotrauma
hull?.Submarine ?? Submarine);
extraDuffelBags.Add(newDuffelBag);
#if SERVER
Spawner.CreateNetworkEvent(newDuffelBag, false);
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(newDuffelBag));
#endif
}
@@ -4547,7 +4568,7 @@ namespace Barotrauma
info.UnlockedTalents.Add(talentPrefab.Identifier);
if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; }
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents });
GameMain.NetworkMember.CreateEntityEvent(this, new UpdateTalentsEventData());
#endif
CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
characterTalents.Add(characterTalent);
@@ -4626,23 +4647,44 @@ namespace Barotrauma
/// </summary>
public void GiveMoney(int amount)
{
if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; }
if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
if (amount <= 0) { return; }
int prevAmount = campaign.Money;
campaign.Money += amount;
OnMoneyChanged(prevAmount, campaign.Money);
Wallet wallet;
#if SERVER
if (!(campaign is MultiPlayerCampaign mpCampaign)) { throw new InvalidOperationException("Campaign on a server is not a multiplayer campaign"); }
Client targetClient = null;
foreach (Client client in GameMain.Server.ConnectedClients)
{
if (client.Character == this)
{
targetClient = client;
break;
}
}
wallet = targetClient is null ? mpCampaign.Bank : mpCampaign.GetWallet(targetClient);
#else
wallet = campaign.Wallet;
#endif
int prevAmount = wallet.Balance;
wallet.Give(amount);
OnMoneyChanged(prevAmount, wallet.Balance);
}
#if CLIENT
public void SetMoney(int amount)
{
if (!(GameMain.GameSession?.Campaign is CampaignMode campaign)) { return; }
if (amount == campaign.Money) { return; }
if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
if (amount == campaign.Wallet.Balance) { return; }
int prevAmount = campaign.Money;
campaign.Money = amount;
OnMoneyChanged(prevAmount, campaign.Money);
int prevAmount = campaign.Wallet.Balance;
campaign.Wallet.Balance = amount;
OnMoneyChanged(prevAmount, campaign.Wallet.Balance);
}
#endif
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(TalentPrefab talentPrefab);
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class Character
{
public enum EventType
{
InventoryState = 0,
Control = 1,
Status = 2,
Treatment = 3,
SetAttackTarget = 4,
ExecuteAttack = 5,
AssignCampaignInteraction = 6,
ObjectiveManagerState = 7,
TeamChange = 8,
AddToCrew = 9,
UpdateExperience = 10,
UpdateTalents = 11,
UpdateSkills = 12,
UpdateMoney = 13,
UpdatePermanentStats = 14,
MinValue = 0,
MaxValue = 14
}
private interface IEventData : NetEntityEvent.IData
{
public EventType EventType { get; }
}
public struct InventoryStateEventData : IEventData
{
public EventType EventType => EventType.InventoryState;
}
public struct ControlEventData : IEventData
{
public EventType EventType => EventType.Control;
public readonly Client Owner;
public ControlEventData(Client owner)
{
Owner = owner;
}
}
public struct StatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
public struct TreatmentEventData : IEventData
{
public EventType EventType => EventType.Treatment;
}
private interface IAttackEventData : IEventData
{
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
}
public struct SetAttackTargetEventData : IAttackEventData
{
public EventType EventType => EventType.SetAttackTarget;
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
public SetAttackTargetEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos)
{
AttackLimb = attackLimb;
TargetEntity = targetEntity;
TargetLimb = targetLimb;
TargetSimPos = targetSimPos;
}
}
public struct ExecuteAttackEventData : IAttackEventData
{
public EventType EventType => EventType.ExecuteAttack;
public Limb AttackLimb { get; }
public IDamageable TargetEntity { get; }
public Limb TargetLimb { get; }
public Vector2 TargetSimPos { get; }
public ExecuteAttackEventData(Limb attackLimb, IDamageable targetEntity, Limb targetLimb, Vector2 targetSimPos)
{
AttackLimb = attackLimb;
TargetEntity = targetEntity;
TargetLimb = targetLimb;
TargetSimPos = targetSimPos;
}
}
public struct AssignCampaignInteractionEventData : IEventData
{
public EventType EventType => EventType.AssignCampaignInteraction;
}
public struct ObjectiveManagerStateEventData : IEventData
{
public EventType EventType => EventType.ObjectiveManagerState;
public readonly AIObjectiveManager.ObjectiveType ObjectiveType;
public ObjectiveManagerStateEventData(AIObjectiveManager.ObjectiveType objectiveType)
{
ObjectiveType = objectiveType;
}
}
private struct TeamChangeEventData : IEventData
{
public EventType EventType => EventType.TeamChange;
}
public struct AddToCrewEventData : IEventData
{
public EventType EventType => EventType.AddToCrew;
public readonly CharacterTeamType TeamType;
public readonly ImmutableArray<Item> InventoryItems;
public AddToCrewEventData(CharacterTeamType teamType, IEnumerable<Item> inventoryItems)
{
TeamType = teamType;
InventoryItems = inventoryItems.ToImmutableArray();
}
}
public struct UpdateExperienceEventData : IEventData
{
public EventType EventType => EventType.UpdateExperience;
}
public struct UpdateTalentsEventData : IEventData
{
public EventType EventType => EventType.UpdateTalents;
}
public struct UpdateSkillsEventData : IEventData
{
public EventType EventType => EventType.UpdateSkills;
}
private struct UpdateMoneyEventData : IEventData
{
public EventType EventType => EventType.UpdateMoney;
}
public struct UpdatePermanentStatsEventData : IEventData
{
public EventType EventType => EventType.UpdatePermanentStats;
public readonly StatTypes StatType;
public UpdatePermanentStatsEventData(StatTypes statType)
{
StatType = statType;
}
}
}
}
@@ -299,7 +299,11 @@ namespace Barotrauma
{
if (handleBuff)
{
Character.CharacterHealth.ApplyAffliction(Character.AnimController.GetLimb(LimbType.Head), AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f));
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head != null)
{
Character.CharacterHealth.ApplyAffliction(head, AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == "disguised").Instantiate(100f));
}
}
idCard ??= Character.Inventory?.GetItemInLimbSlot(InvSlotType.Card)?.GetComponent<IdCard>();
@@ -319,7 +323,11 @@ namespace Barotrauma
if (handleBuff)
{
Character.CharacterHealth.ReduceAfflictionOnLimb(Character.AnimController.GetLimb(LimbType.Head), "disguised".ToIdentifier(), 100f);
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head != null)
{
Character.CharacterHealth.ReduceAfflictionOnLimb(head, "disguised".ToIdentifier(), 100f);
}
}
}
@@ -572,15 +580,15 @@ namespace Barotrauma
private void CheckColors()
{
if (IsColorValid(Head.HairColor))
if (!IsColorValid(Head.HairColor))
{
Head.HairColor = SelectRandomColor(HairColors, Rand.RandSync.Unsynced);
}
if (IsColorValid(Head.FacialHairColor))
if (!IsColorValid(Head.FacialHairColor))
{
Head.FacialHairColor = SelectRandomColor(FacialHairColors, Rand.RandSync.Unsynced);
}
if (IsColorValid(Head.SkinColor))
if (!IsColorValid(Head.SkinColor))
{
Head.SkinColor = SelectRandomColor(SkinColors, Rand.RandSync.Unsynced);
}
@@ -736,7 +744,7 @@ namespace Barotrauma
private int GetIdentifier(string name)
{
int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet));
int id = ToolBox.StringToInt(name + string.Join("", Head.Preset.TagSet.OrderBy(s => s)));
id ^= Head.HairIndex << 12;
id ^= Head.BeardIndex << 18;
id ^= Head.MoustacheIndex << 24;
@@ -822,12 +830,12 @@ namespace Barotrauma
{
foreach (var limbElement in Ragdoll.MainElement.Elements())
{
if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
if (!limbElement.GetAttributeString("type", string.Empty).Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; }
ContentXElement spriteElement = limbElement.GetChildElement("sprite");
if (spriteElement == null) { continue; }
string spritePath = spriteElement.Attribute("texture").Value;
string spritePath = spriteElement.GetAttributeContentPath("texture")?.Value;
if (string.IsNullOrEmpty(spritePath)) { continue; }
spritePath = ReplaceVars(spritePath);
@@ -1298,7 +1306,7 @@ namespace Barotrauma
var orders = LoadOrders(orderData);
foreach (var order in orders)
{
character.SetOrder(order, speak: false, force: true);
character.SetOrder(order, isNewOrder: true, speak: false, force: true);
}
}
@@ -41,9 +41,11 @@ namespace Barotrauma
if (previousValue > 0.0f && value <= 0.0f)
{
DeactivateHusk();
highestStrength = 0;
}
}
}
private float highestStrength;
public InfectionState State
{
@@ -75,6 +77,7 @@ namespace Barotrauma
{
if (HuskPrefab == null) { return; }
base.Update(characterHealth, targetLimb, deltaTime);
highestStrength = Math.Max(_strength, highestStrength);
character = characterHealth.Character;
if (character == null) { return; }
@@ -98,7 +101,7 @@ namespace Barotrauma
DeactivateHusk();
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
character.SpeechImpediment = 30;
}
State = InfectionState.Transition;
}
@@ -108,6 +111,10 @@ namespace Barotrauma
{
character.SetStun(Rand.Range(2f, 3f));
}
if (Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true })
{
character.SpeechImpediment = 100;
}
State = InfectionState.Active;
ActivateHusk();
}
@@ -120,7 +127,57 @@ namespace Barotrauma
}
}
partial void UpdateMessages();
private InfectionState? prevDisplayedMessage;
private void UpdateMessages()
{
if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; }
if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; }
if (highestStrength > Strength) { return; }
switch (State)
{
case InfectionState.Dormant:
if (Strength < DormantThreshold * 0.5f)
{
return;
}
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskDormant"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskdormant").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskdormant".ToIdentifier());
}
break;
case InfectionState.Transition:
if (character == Character.Controlled)
{
#if CLIENT
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), GUIStyle.Red);
#endif
}
else if (character.IsBot)
{
character.Speak(TextManager.Get("dialoghuskcantspeak").Value, delay: Rand.Range(0.5f, 5.0f), identifier: "huskcantspeak".ToIdentifier());
}
break;
case InfectionState.Active:
#if CLIENT
if (character == Character.Controlled && character.Params.UseHuskAppendage)
{
GUI.AddMessage(TextManager.GetWithVariable("HuskActivate", "[Attack]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Attack)), GUIStyle.Red);
}
#endif
break;
case InfectionState.Final:
default:
break;
}
prevDisplayedMessage = State;
}
private void ApplyDamage(float deltaTime, bool applyForce)
{
@@ -209,7 +266,9 @@ namespace Barotrauma
{
yield return CoroutineStatus.Success;
}
#if SERVER
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
#endif
character.Enabled = false;
Entity.Spawner.AddEntityToRemoveQueue(character);
UnsubscribeFromDeathEvent();
@@ -246,7 +305,6 @@ namespace Barotrauma
if (huskPrefab.ControlHusk)
{
#if SERVER
var client = GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == character);
if (client != null)
{
GameMain.Server.SetClientCharacter(client, husk);
@@ -657,6 +657,7 @@ namespace Barotrauma
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
{
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
@@ -731,48 +732,47 @@ namespace Barotrauma
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
if (Character.GodMode) { return; }
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
if (!Character.GodMode)
{
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
afflictionsToUpdate.Add(kvp);
}
afflictionsToUpdate.Add(kvp);
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
{
var affliction = kvp.Key;
Limb targetLimb = null;
if (kvp.Value != null)
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
{
int healthIndex = limbHealths.IndexOf(kvp.Value);
targetLimb =
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
Character.AnimController.MainLimb;
var affliction = kvp.Key;
Limb targetLimb = null;
if (kvp.Value != null)
{
int healthIndex = limbHealths.IndexOf(kvp.Value);
targetLimb =
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
Character.AnimController.MainLimb;
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
foreach (var affliction in afflictionsToRemove)
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
afflictions.Remove(affliction);
}
}
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
if (Character.InWater)
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
@@ -782,13 +782,16 @@ namespace Barotrauma
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed));
}
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
if (!Character.GodMode)
{
Kill();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
}
}
@@ -190,7 +190,7 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
if (itemElement.GetAttributeBool("equip", false))
@@ -152,7 +152,7 @@ namespace Barotrauma
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
@@ -1011,15 +1011,9 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new object[]
{
NetEntityEvent.Type.ExecuteAttack,
this,
(damageTarget as Entity)?.ID ?? Entity.NullEntityID,
damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0,
attackSimPos.X,
attackSimPos.Y
});
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
#endif
}
@@ -1055,7 +1049,10 @@ namespace Barotrauma
if (!attack.IsRunning)
{
// Set the main collider where the body lands after the attack
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
if (Vector2.DistanceSquared(character.AnimController.Collider.SimPosition, character.AnimController.MainLimb.body.SimPosition) > 0.1f * 0.1f)
{
character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation);
}
}
return wasHit;
}
@@ -162,7 +162,7 @@ namespace Barotrauma
Identifier variantOf = MainElement.VariantOf();
if (!variantOf.IsEmpty)
{
VariantFile = doc;
VariantFile = new XDocument(doc);
#warning TODO: determine that CreateVariantXML is equipped to do this
XElement newRoot = CreateVariantXml(MainElement, CharacterPrefab.FindBySpeciesName(variantOf).ConfigElement);
var oldElement = MainElement;
@@ -477,12 +477,15 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ConstantHealthRegeneration { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)]
public float HealthRegenerationWhenEating { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool StunImmunity { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
@@ -956,7 +956,7 @@ namespace Barotrauma
Deformations = new Dictionary<SpriteDeformationParams, XElement>();
foreach (var deformationElement in element.GetChildElements("spritedeformation"))
{
string typeName = deformationElement.GetAttributeString("typename", null) ?? deformationElement.GetAttributeString("type", "");
string typeName = deformationElement.GetAttributeString("type", null) ?? deformationElement.GetAttributeString("typename", string.Empty);
SpriteDeformationParams deformation = null;
switch (typeName.ToLowerInvariant())
{
@@ -982,7 +982,7 @@ namespace Barotrauma
}
if (deformation != null)
{
deformation.TypeName = typeName;
deformation.Type = typeName;
}
Deformations.Add(deformation, deformationElement);
}
@@ -1,20 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionNoCrewDied : AbilityConditionDataless
{
public bool assistantsDontCount;
public AbilityConditionNoCrewDied(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
assistantsDontCount = conditionElement.GetAttributeBool(nameof(assistantsDontCount), true);
}
protected override bool MatchesConditionSpecific()
{
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
if (GameMain.GameSession == null) { return false; }
foreach (Character character in GameMain.GameSession.Casualties)
{
return !campaign.CrewHasDied;
if (assistantsDontCount && character.Info?.Job?.Prefab.Identifier == "assistant")
{
continue;
}
if (character.CauseOfDeath != null && character.CauseOfDeath.Type != CauseOfDeathType.Disconnected)
{
return false;
}
}
return true;
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
@@ -63,7 +64,7 @@ namespace Barotrauma
public static Result<ContentFile, string> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
Result<ContentFile, string> fail(string error)
static Result<ContentFile, string> fail(string error)
=> Result<ContentFile, string>.Failure(error);
Identifier elemName = element.NameAsIdentifier();
@@ -73,13 +74,16 @@ namespace Barotrauma
{
return fail($"Invalid content type \"{elemName}\"");
}
if (filePath is null)
{
return fail($"No content path defined for file of type \"{elemName}\"");
}
try
{
if (!File.Exists(filePath.FullPath))
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
}
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
@@ -1,4 +1,3 @@
using System;
using System.Linq;
using System.Xml.Linq;
@@ -54,8 +54,10 @@ namespace Barotrauma
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
#warning TODO: remove this, unless we truly believe that determining "multiplayer-incompatible content" is something we should do
public readonly bool HasMultiplayerIncompatibleContent;
/// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary>
public readonly bool HasMultiplayerSyncedContent;
protected ContentPackage(XDocument doc, string path)
{
@@ -93,7 +95,7 @@ namespace Barotrauma
.Select(f => f.Error)
.ToImmutableArray();
HasMultiplayerIncompatibleContent = Files.Any(f => !f.NotSyncedInMultiplayer);
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
Hash = CalculateHash();
var expectedHash = rootElement.GetAttributeString("expectedhash", "");
@@ -392,6 +392,10 @@ namespace Barotrauma
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
foreach (string error in VanillaCorePackage.Errors)
{
DebugConsole.ThrowError(error);
}
}
public static IEnumerable<LoadProgress> Init()
@@ -101,23 +101,27 @@ namespace Barotrauma
}
private static bool StringEquality(string? a, string? b)
=> (a.IsNullOrEmpty() && b.IsNullOrEmpty()) ||
string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""),
Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""),
StringComparison.OrdinalIgnoreCase);
{
if (a.IsNullOrEmpty() || b.IsNullOrEmpty())
{
return a.IsNullOrEmpty() == b.IsNullOrEmpty();
}
return string.Equals(Path.GetFullPath(a.CleanUpPathCrossPlatform(false) ?? ""),
Path.GetFullPath(b.CleanUpPathCrossPlatform(false) ?? ""), StringComparison.OrdinalIgnoreCase);
}
public static bool operator==(ContentPath a, ContentPath b)
=> StringEquality(a.Value, b.Value);
=> StringEquality(a?.Value, b?.Value);
public static bool operator!=(ContentPath a, ContentPath b) => !(a == b);
public static bool operator==(ContentPath a, string? b)
=> StringEquality(a.Value, b);
=> StringEquality(a?.Value, b);
public static bool operator!=(ContentPath a, string? b) => !(a == b);
public static bool operator==(string? a, ContentPath b)
=> StringEquality(a, b.Value);
=> StringEquality(a, b?.Value);
public static bool operator!=(string? a, ContentPath b) => !(a == b);
@@ -67,8 +67,13 @@ namespace Barotrauma
public void Execute(string[] args)
{
if (OnExecute == null) return;
if (!CheatsEnabled && IsCheat)
if (OnExecute == null) { return; }
bool allowCheats = false;
#if CLIENT
allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen);
#endif
if (!allowCheats && !CheatsEnabled && IsCheat)
{
NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + names[0] + "\".", Color.Red);
#if USE_STEAM
@@ -603,28 +608,27 @@ namespace Barotrauma
commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) =>
{
if (args.Length < 2) { return; }
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a =>
a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
a.Identifier == args[0]);
string affliction = args[0];
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction);
if (afflictionPrefab == null)
{
ThrowError("Affliction \"" + args[0] + "\" not found.");
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase));
}
if (afflictionPrefab == null)
{
ThrowError("Affliction \"" + affliction + "\" not found.");
return;
}
if (!float.TryParse(args[1], out float afflictionStrength))
{
ThrowError("\"" + args[1] + "\" is not a valid affliction strength.");
return;
}
bool relativeStrength = false;
if (args.Length > 4)
{
bool.TryParse(args[4], out relativeStrength);
}
Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] });
if (targetCharacter != null)
{
@@ -671,7 +675,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -699,7 +703,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -720,7 +724,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -825,7 +829,7 @@ namespace Barotrauma
{
Character.Controlled?.Info?.Job?.Skills?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty<string>(),
new[]{ "max" },
Character.CharacterList.Select(c => c.Name).Distinct().ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(),
};
}));
@@ -864,7 +868,7 @@ namespace Barotrauma
return new string[][]
{
talentNames.Select(id => id).ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -916,7 +920,7 @@ namespace Barotrauma
return new string[][]
{
availableArgs.ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -951,7 +955,7 @@ namespace Barotrauma
return new[]
{
new string[] { "100" },
Character.CharacterList.Select(c => c.Name).Distinct().ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(),
};
}));
@@ -1066,7 +1070,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -1413,7 +1417,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -1455,7 +1459,7 @@ namespace Barotrauma
{
return new string[][]
{
Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}, isCheat: true));
@@ -1467,7 +1471,7 @@ namespace Barotrauma
return new string[][]
{
GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
};
}));
@@ -1538,14 +1542,14 @@ namespace Barotrauma
NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White);
}, isCheat: true));
commands.Add(new Command("money", "money [amount]: Gives the specified amount of money to the crew when a campaign is active.", args =>
commands.Add(new Command("money", "money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args =>
{
if (args.Length == 0) { return; }
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
if (int.TryParse(args[0], out int money))
{
campaign.Money += money;
campaign.Bank.Give(money);
GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console");
}
else
@@ -1553,8 +1557,23 @@ namespace Barotrauma
ThrowError($"\"{args[0]}\" is not a valid numeric value.");
}
}
}, isCheat: true, getValidArgs: () => new []
{
new []{ string.Empty },
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
}));
commands.Add(new Command("showmoney", "showmoney: Shows the amount of money in everyones wallet.", args =>
{
if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
{
ThrowError("No campaign active!");
return;
}
NewMessage($"Bank: {campaign.Bank.Balance}");
}, isCheat: true));
commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args =>
{
GameMain.GameSession?.EventManager?.SkipEventCooldown();
@@ -1968,7 +1987,7 @@ namespace Barotrauma
}
}
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray();
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray();
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null)
{
@@ -87,7 +87,7 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
}
@@ -1,4 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -7,15 +10,36 @@ namespace Barotrauma
[Serialize(0, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
public CheckMoneyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
}
protected override bool? DetermineSuccess()
{
Client matchingClient = null;
bool hasTag = !TargetTag.IsEmpty;
#if SERVER
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (hasTag)
{
foreach (Entity entity in targets)
{
if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter)
{
matchingClient = matchingCharacter;
break;
}
}
}
#endif
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
return campaign.Money >= Amount;
return !hasTag ? campaign.Bank.CanAfford(Amount) : campaign.GetWallet(matchingClient).CanAfford(Amount);
}
return false;
}
@@ -182,7 +182,7 @@ namespace Barotrauma
speaker.ActiveConversation = null;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
#endif
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null && !speaker.IsDead && !speaker.Removed)
@@ -259,7 +259,7 @@ namespace Barotrauma
speaker.SetCustomInteract(
TryStartConversation,
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
#endif
}
return;
@@ -369,7 +369,7 @@ namespace Barotrauma
speaker.CampaignInteractionType = CampaignMode.InteractionType.None;
speaker.SetCustomInteract(null, null);
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
GameMain.NetworkMember.CreateEntityEvent(speaker, new Character.AssignCampaignInteractionEventData());
#endif
}
}
@@ -1,5 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -10,6 +14,9 @@ namespace Barotrauma
[Serialize(0, IsPropertySaveable.Yes)]
public int Amount { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
private bool isFinished;
public override bool IsFinished(ref string goTo)
@@ -25,13 +32,44 @@ namespace Barotrauma
{
if (isFinished) { return; }
#if SERVER
bool hasTag = !TargetTag.IsEmpty;
List<Client> matchingClients = new List<Client>();
if (hasTag)
{
IEnumerable targets = ParentEvent.GetTargets(TargetTag);
foreach (Entity entity in targets)
{
if (entity is Character && GameMain.Server?.ConnectedClients.FirstOrDefault(c => c.Character == entity) is { } matchingCharacter)
{
matchingClients.Add(matchingCharacter);
break;
}
}
}
#endif
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
campaign.Money += Amount;
GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value);
#if SERVER
(campaign as MultiPlayerCampaign).LastUpdateID++;
if (!hasTag)
{
campaign.Bank.Give(Amount);
}
else
{
foreach (Client client in matchingClients)
{
campaign.GetWallet(client).Give(Amount);
}
}
((MultiPlayerCampaign)campaign).LastUpdateID++;
#else
campaign.Wallet.Give(Amount);
#endif
GameAnalyticsManager.AddMoneyGainedEvent(Amount, GameAnalyticsManager.MoneySource.Event, ParentEvent.Prefab.Identifier.Value);
}
isFinished = true;
@@ -61,7 +61,7 @@ namespace Barotrauma
npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AddToCrew, TeamTag, npc.Inventory.AllItems.Select(it => it.ID).ToArray() });
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamTag, npc.Inventory.AllItems));
#endif
}
}
@@ -145,7 +145,7 @@ namespace Barotrauma
npc.SetCustomInteract(
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
TextManager.Get("CampaignInteraction.Talk"));
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData());
#endif
}
if (!AllowMultipleTargets) { return; }
@@ -194,7 +194,7 @@ namespace Barotrauma
npc.SetCustomInteract(null, null);
npc.RequireConsciousnessForCustomInteract = true;
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(npc, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData());
#endif
}
else if (npcOrItem.TryGet(out Item item))
@@ -460,9 +460,12 @@ namespace Barotrauma
selectedEvents[eventSet].Add(newEvent);
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
foreach (EventSet childEventSet in eventSet.ChildSets)
{
CreateEvents(childEventSet, rand);
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (location != null && !IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet, rand);
}
}
}
@@ -474,10 +477,7 @@ namespace Barotrauma
Random rand = random ?? new MTRandom(ToolBox.StringToInt(level.Seed));
var allowedEventSets =
eventSets.Where(es =>
level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty &&
level.LevelData.Type == es.LevelType &&
(es.BiomeIdentifier.IsEmpty || es.BiomeIdentifier == level.LevelData.Biome.Identifier));
eventSets.Where(set => IsValidForLevel(set, level));
if (requireCampaignSet.HasValue)
{
@@ -501,13 +501,9 @@ namespace Barotrauma
}
Location location = (GameMain.GameSession?.GameMode as CampaignMode)?.Map?.CurrentLocation ?? level?.StartLocation;
LocationType locationType = location?.GetLocationType();
if (location != null)
{
allowedEventSets = allowedEventSets.Where(set =>
set.LocationTypeIdentifiers == null ||
set.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier));
allowedEventSets = allowedEventSets.Where(set => IsValidForLocation(set, location));
}
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
@@ -526,6 +522,20 @@ namespace Barotrauma
return null;
}
private bool IsValidForLevel(EventSet eventSet, Level level)
{
return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.LevelData.Type == eventSet.LevelType &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
}
private bool IsValidForLocation(EventSet eventSet, Location location)
{
return eventSet.LocationTypeIdentifiers == null ||
eventSet.LocationTypeIdentifiers.Any(identifier => identifier == location.GetLocationType().Identifier);
}
private bool CanStartEventSet(EventSet eventSet)
{
ISpatialEntity refEntity = GetRefEntity();
@@ -210,7 +210,7 @@ namespace Barotrauma
Additive = element.GetAttributeBool("additive", false);
string levelTypeStr = element.GetAttributeString("leveltype", "LocationConnection");
string levelTypeStr = element.GetAttributeString("leveltype", parentSet?.LevelType.ToString() ?? "LocationConnection");
if (!Enum.TryParse(levelTypeStr, true, out LevelType))
{
DebugConsole.ThrowError($"Error in event set \"{Identifier}\". \"{levelTypeStr}\" is not a valid level type.");
@@ -212,7 +212,7 @@ namespace Barotrauma
{
return null;
}
int probabilitySum = allowedMissions.Sum(m => m.Commonness);
int randomNumber = rand.NextInt32() % probabilitySum;
foreach (MissionPrefab missionPrefab in allowedMissions)
@@ -377,10 +377,16 @@ namespace Barotrauma
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
int totalReward = (int)(reward * missionMoneyGainMultiplier.Value);
campaign.Money += totalReward;
GameAnalyticsManager.AddMoneyGainedEvent(totalReward, GameAnalyticsManager.MoneySource.MissionReward, Prefab.Identifier.Value);
#if SERVER
totalReward = DistributeRewardsToCrew(GetSalaryEligibleCrew(), totalReward);
#endif
if (totalReward > 0)
{
campaign.Bank.Give(totalReward);
}
foreach (Character character in crewCharacters)
{
character.Info.MissionsCompletedSinceDeath++;
@@ -409,6 +415,57 @@ namespace Barotrauma
}
}
#if SERVER
public static int DistributeRewardsToCrew(IEnumerable<Character> crew, int totalReward)
{
int remainingRewards = totalReward;
HashSet<Character> nonBotCrew = crew.Where(c => !c.IsBot).ToHashSet();
float sum = nonBotCrew.Sum(c => c.Wallet.RewardDistribution);
if (sum == 0) { return remainingRewards; }
foreach (Character character in nonBotCrew)
{
float rewardWeight = character.Wallet.RewardDistribution / sum;
int reward = (int)Math.Floor(totalReward * rewardWeight);
reward = Math.Max(remainingRewards, reward);
character.Wallet.Give(reward);
remainingRewards -= reward;
if (0 >= remainingRewards) { break; }
}
return remainingRewards;
}
#endif
public static IEnumerable<Character> GetSalaryEligibleCrew()
{
if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return Array.Empty<Character>(); }
IEnumerable<Character> characters = crewManager.GetCharacters();
#if SERVER
return GameMain.Server.ConnectedClients.Select(c => c.Character).Where(IsAlive).Concat(characters);
#elif CLIENT
return characters;
#endif
static bool IsAlive(Character c) { return c.Info != null && !c.IsDead; }
}
public static (int Amount, int Percentage) GetRewardShare(int rewardDistribution, IEnumerable<Character> crew, Option<int> reward)
{
float sum = crew.Sum(c => c.Wallet.RewardDistribution) + rewardDistribution;
if (sum == 0) { return (0, 0); }
float rewardWeight = rewardDistribution / sum;
int rewardPercentage = (int)(rewardWeight * 100);
return reward switch
{
Some<int> { Value: var amount } => ((int)(amount * rewardWeight), rewardPercentage),
None<int> _ => (0, rewardPercentage),
_ => throw new ArgumentOutOfRangeException()
};
}
protected void ChangeLocationType(LocationTypeChange change)
{
if (change == null) { throw new ArgumentException(); }
@@ -361,7 +361,7 @@ namespace Barotrauma
if (!SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
@@ -168,7 +168,7 @@ namespace Barotrauma
foreach (Item spawnedItem in spawnedItems)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(spawnedItem, remove: false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem));
#endif
foreach (ItemComponent ic in spawnedItem.Components)
{
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
#if SERVER
using Barotrauma.Networking;
#endif
@@ -18,12 +19,34 @@ namespace Barotrauma
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
public readonly int BuyerCharacterInfoId;
public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyerCharacterInfoId;
}
#if CLIENT
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer = null)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0;
}
#elif SERVER
public PurchasedItem(ItemPrefab itemPrefab, int quantity, Client buyer)
{
ItemPrefab = itemPrefab;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? 0;
}
#endif
}
class SoldItem
@@ -156,7 +179,7 @@ namespace Barotrauma
OnPurchasedItemsChanged?.Invoke();
}
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity)
public void ModifyItemQuantityInBuyCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInBuyCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
@@ -167,15 +190,15 @@ namespace Barotrauma
ItemsInBuyCrate.Remove(itemInCrate);
}
}
else if(changeInQuantity > 0)
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInBuyCrate.Add(itemInCrate);
}
OnItemsInBuyCrateChanged?.Invoke();
}
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity)
public void ModifyItemQuantityInSubSellCrate(ItemPrefab itemPrefab, int changeInQuantity, Client client = null)
{
var itemInCrate = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab);
if (itemInCrate != null)
@@ -188,13 +211,13 @@ namespace Barotrauma
}
else if (changeInQuantity > 0)
{
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity);
itemInCrate = new PurchasedItem(itemPrefab, changeInQuantity, client);
ItemsInSellFromSubCrate.Add(itemInCrate);
}
OnItemsInSellFromSubCrateChanged?.Invoke();
}
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate)
public void PurchaseItems(List<PurchasedItem> itemsToPurchase, bool removeFromCrate, Client client = null)
{
// Check all the prices before starting the transaction
// to make sure the modifiers stay the same for the whole transaction
@@ -210,13 +233,13 @@ namespace Barotrauma
}
else
{
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity);
purchasedItem = new PurchasedItem(item.ItemPrefab, item.Quantity, client);
PurchasedItems.Add(purchasedItem);
}
// Exchange money
var itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.Money -= itemValue;
campaign.GetWallet(client).TryDeduct(itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
Location.StoreCurrentBalance += itemValue;
@@ -427,7 +450,7 @@ namespace Barotrauma
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(itemContainer.Item, false);
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
@@ -438,7 +461,7 @@ namespace Barotrauma
itemSpawned(item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(item, false);
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
@@ -491,7 +514,8 @@ namespace Barotrauma
if (item?.ItemPrefab == null) { continue; }
itemsElement.Add(new XElement("item",
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
new XAttribute("qty", item.Quantity),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
}
parentElement.Add(itemsElement);
}
@@ -503,12 +527,15 @@ namespace Barotrauma
{
foreach (XElement itemElement in element.GetChildElements("item"))
{
var id = itemElement.GetAttributeString("id", null);
string id = itemElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = itemElement.GetAttributeInt("qty", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty));
int qty = itemElement.GetAttributeInt("qty", 0);
int buyerId = itemElement.GetAttributeInt("buyer", 0);
purchasedItems.Add(new PurchasedItem(prefab, qty, buyerId));
}
}
SetPurchasedItems(purchasedItems);
@@ -20,6 +20,16 @@ namespace Barotrauma
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
private readonly List<Character> characters = new List<Character>();
public IEnumerable<Character> GetCharacters()
{
return characters;
}
public IEnumerable<CharacterInfo> GetCharacterInfos()
{
return characterInfos;
}
private Character welcomeMessageNPC;
public List<CharacterInfo> CharacterInfos => characterInfos;
@@ -1,8 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
@@ -27,6 +25,8 @@ namespace Barotrauma
public LocalizedString Description { get; }
public LocalizedString ShortDescription { get; }
public int MenuOrder { get; }
/// <summary>
/// How low the reputation can drop on this faction
/// </summary>
@@ -52,6 +52,7 @@ namespace Barotrauma
public FactionPrefab(ContentXElement element, FactionsFile file) : base(file, element.GetAttributeIdentifier("identifier", string.Empty))
{
MenuOrder = element.GetAttributeInt("menuorder", 0);
MinReputation = element.GetAttributeInt("minreputation", -100);
MaxReputation = element.GetAttributeInt("maxreputation", 100);
InitialReputation = element.GetAttributeInt("initialreputation", 0);
@@ -164,7 +164,7 @@ namespace Barotrauma
("[reputationvalue]", ((int)Math.Round(value)).ToString()));
if (addColorTags)
{
formattedReputation = $"‖color:{XMLExtensions.ColorToString(GetReputationColor(normalizedValue))}‖"+ formattedReputation+"‖end‖";
formattedReputation = $"‖color:{XMLExtensions.ToStringHex(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖";
}
return formattedReputation;
}
@@ -0,0 +1,193 @@
using System;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
internal readonly struct WalletChangedEvent
{
public readonly Wallet Wallet;
public readonly WalletInfo Info;
public readonly WalletChangedData ChangedData;
public WalletChangedEvent(Wallet wallet, WalletChangedData changedData, WalletInfo info)
{
Wallet = wallet;
Info = info;
ChangedData = changedData;
}
}
[NetworkSerialize]
internal struct WalletInfo : INetSerializableStruct
{
public int RewardDistribution;
public int Balance;
}
internal struct NetWalletUpdate : INetSerializableStruct
{
[NetworkSerialize(ArrayMaxSize = NetConfig.MaxPlayers + 1)]
public NetWalletTransaction[] Transactions;
}
[NetworkSerialize]
internal struct NetWalletTransfer : INetSerializableStruct
{
public Option<ushort> Sender;
public Option<ushort> Receiver;
public int Amount;
}
internal struct NetWalletSalaryUpdate : INetSerializableStruct
{
[NetworkSerialize]
public ushort Target;
[NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)]
public int NewRewardDistribution;
}
[NetworkSerialize]
internal struct WalletChangedData : INetSerializableStruct
{
public Option<int> RewardDistributionChanged;
public Option<int> BalanceChanged;
public WalletChangedData MergeInto(WalletChangedData other)
{
other.BalanceChanged = AddOptionalInt(other.BalanceChanged, BalanceChanged);
other.RewardDistributionChanged = AddOptionalInt(other.RewardDistributionChanged, RewardDistributionChanged);
return other;
static Option<int> AddOptionalInt(Option<int> a, Option<int> b)
{
return a switch
{
Some<int> some1 => b switch
{
Some<int> some2 => Option<int>.Some(some1.Value + some2.Value),
None<int> _ => Option<int>.Some(some1.Value),
_ => throw new ArgumentOutOfRangeException(nameof(b))
},
None<int> _ => b switch
{
Some<int> some1 => Option<int>.Some(some1.Value),
None<int> _ => Option<int>.None(),
_ => throw new ArgumentOutOfRangeException(nameof(b))
},
_ => throw new ArgumentOutOfRangeException(nameof(a))
};
}
}
}
[NetworkSerialize]
internal struct NetWalletTransaction : INetSerializableStruct
{
public Option<ushort> CharacterID;
public WalletChangedData ChangedData;
public WalletInfo Info;
}
// ReSharper disable ValueParameterNotUsed
internal sealed class InvalidWallet : Wallet
{
public override int Balance
{
get => 0;
set => new InvalidOperationException("Tried to set the balance on an invalid wallet");
}
public override int RewardDistribution
{
get => 0;
set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
}
}
internal partial class Wallet
{
public static readonly Wallet Invalid = new InvalidWallet();
public const string LowerCaseSaveElementName = "wallet";
private const string AttributeNameBalance = "balance",
AttrubuteNameRewardDistribution = "rewarddistribution",
SaveElementName = "Wallet";
private int balance;
public virtual int Balance
{
get => balance;
set => balance = ClampBalance(value);
}
private int rewardDistribution;
public virtual int RewardDistribution
{
get => rewardDistribution;
set => rewardDistribution = ClampRewardDistribution(value);
}
public Wallet() { }
public Wallet(XElement element)
{
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
}
public XElement Save()
{
XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution));
return element;
}
public bool TryDeduct(int price)
{
if (!CanAfford(price)) { return false; }
Deduct(price);
return true;
}
public bool CanAfford(int price) => Balance >= price;
public void Refund(int price) => Give(price);
public void Give(int amount)
{
Balance += amount;
SettingsChanged(balanceChanged: Option<int>.Some(amount), rewardChanged: Option<int>.None());
}
public void Deduct(int price)
{
Balance -= price;
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
}
public void SetRewardDistrubiton(int value)
{
int oldValue = RewardDistribution;
RewardDistribution = value;
SettingsChanged(balanceChanged: Option<int>.None(), rewardChanged: Option<int>.Some(RewardDistribution - oldValue));
}
public WalletInfo CreateWalletInfo()
{
return new WalletInfo
{
Balance = Balance,
RewardDistribution = RewardDistribution
};
}
partial void SettingsChanged(Option<int> balanceChanged, Option<int> rewardChanged);
private static int ClampBalance(int value) => Math.Clamp(value, 0, CampaignMode.MaxMoney);
private static int ClampRewardDistribution(int value) => Math.Clamp(value, 0, 100);
}
}
@@ -68,7 +68,7 @@ namespace Barotrauma
abstract partial class CampaignMode : GameMode
{
const int MaxMoney = int.MaxValue / 2; //about 1 billion
public const int MaxMoney = int.MaxValue / 2; //about 1 billion
public const int InitialMoney = 8500;
//duration of the cinematic + credits at the end of the campaign
@@ -102,6 +102,8 @@ namespace Barotrauma
private readonly List<Mission> extraMissions = new List<Mission>();
public readonly NamedEvent<WalletChangedEvent> OnMoneyChanged = new NamedEvent<WalletChangedEvent>();
public enum TransitionType
{
None,
@@ -167,12 +169,7 @@ namespace Barotrauma
}
}
private int money;
public int Money
{
get { return money; }
set { money = MathHelper.Clamp(value, 0, MaxMoney); }
}
public Wallet Bank;
public LevelData NextLevel
{
@@ -183,11 +180,20 @@ namespace Barotrauma
protected CampaignMode(GameModePreset preset)
: base(preset)
{
Money = InitialMoney;
Bank = new Wallet
{
Balance = InitialMoney
};
CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this);
}
public virtual Wallet GetWallet(Client client = null)
{
return Bank;
}
/// <summary>
/// The location that's displayed as the "current one" in the map screen. Normally the current outpost or the location at the start of the level,
/// but when selecting the next destination at the end of the level at an uninhabited location we use the location at the end
@@ -200,7 +206,7 @@ namespace Barotrauma
{
return Level.Loaded.EndLocation;
}
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
return Level.Loaded?.StartLocation ?? Map.CurrentLocation;
}
public List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
@@ -255,8 +261,6 @@ namespace Barotrauma
PurchasedLostShuttles = false;
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
ResetTalentData();
}
public void InitCampaignData()
@@ -702,21 +706,20 @@ namespace Barotrauma
string eventId = "FinishCampaign:";
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0));
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Money);
GameAnalyticsManager.AddDesignEvent(eventId + "Money", Bank.Balance);
GameAnalyticsManager.AddDesignEvent(eventId + "Playtime", TotalPlayTime);
GameAnalyticsManager.AddDesignEvent(eventId + "PassedLevels", TotalPassedLevels);
}
protected virtual void EndCampaignProjSpecific() { }
public bool TryHireCharacter(Location location, CharacterInfo characterInfo)
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null)
{
if (characterInfo == null) { return false; }
if (Money < characterInfo.Salary) { return false; }
if (!GetWallet(client).TryDeduct(characterInfo.Salary)) { return false; }
characterInfo.IsNewHire = true;
location.RemoveHireableCharacter(characterInfo);
CrewManager.AddCharacterInfo(characterInfo);
Money -= characterInfo.Salary;
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown");
return true;
}
@@ -740,8 +743,7 @@ namespace Barotrauma
HumanAIController humanAI = npc.AIController as HumanAIController;
if (humanAI == null) { yield return CoroutineStatus.Success; }
var waitOrderPrefab = OrderPrefab.Prefabs["wait"];
var waitOrder = new Order(waitOrderPrefab, Identifier.Empty, null, orderGiver: null);
var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
humanAI.SetForcedOrder(waitOrder);
var waitObjective = humanAI.ObjectiveManager.ForcedOrder;
humanAI.FaceTarget(interactor);
@@ -856,7 +858,7 @@ namespace Barotrauma
{
GameMain.Server.SendDirectChatMessage(Networking.ChatMessage.Create(
TextManager.Get("RadioAnnouncerName").Value,
TextManager.Get("RadioAnnouncerName").Value,
TextManager.Get("TooFarFromOutpostWarning").Value, Networking.ChatMessageType.Default, null), c);
}
#endif
@@ -906,7 +908,7 @@ namespace Barotrauma
public void LogState()
{
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
DebugConsole.NewMessage(" Money: " + Money, Color.White);
DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White);
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White);
DebugConsole.NewMessage(" Available destinations: ", Color.White);
@@ -960,13 +962,5 @@ namespace Barotrauma
}
}
// Talent relevant data, only stored for the duration of the mission
private void ResetTalentData()
{
CrewHasDied = false;
}
public bool CrewHasDied { get; set; }
}
}
@@ -26,33 +26,6 @@ namespace Barotrauma
private XElement itemData;
private XElement healthData;
public XElement OrderData { get; private set; }
public void Refresh(Character character)
{
healthData = new XElement("health");
character.CharacterHealth.Save(healthData);
if (character.Inventory != null)
{
itemData = new XElement("inventory");
Character.SaveInventory(character.Inventory, itemData);
}
OrderData = new XElement("orders");
CharacterInfo.SaveOrderData(character.Info, OrderData);
}
public XElement Save()
{
XElement element = new XElement("CharacterCampaignData",
new XAttribute("name", Name),
new XAttribute("endpoint", ClientEndPoint),
new XAttribute("steamid", SteamID));
CharacterInfo?.Save(element);
if (itemData != null) { element.Add(itemData); }
if (healthData != null) { element.Add(healthData); }
if (OrderData != null) { element.Add(OrderData); }
return element;
}
public XElement WalletData;
}
}
@@ -99,7 +99,6 @@ namespace Barotrauma
/// </summary>
private void Load(XElement element)
{
Money = element.GetAttributeInt("money", 0);
PurchasedLostShuttles = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairs = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairs = element.GetAttributeBool("purchaseditemrepairs", false);
@@ -166,6 +165,9 @@ namespace Barotrauma
case "stats":
LoadStats(subElement);
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(subElement);
break;
#if SERVER
case "savedexperiencepoints":
foreach (XElement savedExp in subElement.Elements())
@@ -177,6 +179,15 @@ namespace Barotrauma
}
}
int oldMoney = element.GetAttributeInt("money", 0);
if (oldMoney > 0)
{
Bank = new Wallet
{
Balance = oldMoney
};
}
CampaignMetadata ??= new CampaignMetadata(this);
UpgradeManager ??= new UpgradeManager(this);
@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -30,10 +31,14 @@ namespace Barotrauma
private readonly List<Mission> missions = new List<Mission>();
public IEnumerable<Mission> Missions { get { return missions; } }
private readonly HashSet<Character> casualties = new HashSet<Character>();
public IEnumerable<Character> Casualties { get { return casualties; } }
public CharacterTeamType? WinningTeam;
public bool IsRunning { get; private set; }
public bool RoundEnding { get; private set; }
public Level? Level { get; private set; }
@@ -201,7 +206,8 @@ namespace Barotrauma
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (selectedSub != null)
{
campaign.Money = Math.Max(MultiPlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
campaign.Bank.TryDeduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney);
}
return campaign;
}
@@ -211,7 +217,8 @@ namespace Barotrauma
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
if (selectedSub != null)
{
campaign.Money = Math.Max(SinglePlayerCampaign.MinimumInitialMoney, campaign.Money - selectedSub.Price);
campaign.Bank.TryDeduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, MultiPlayerCampaign.MinimumInitialMoney);
}
return campaign;
}
@@ -264,7 +271,7 @@ namespace Barotrauma
/// <summary>
/// Switch to another submarine. The sub is loaded when the next round starts.
/// </summary>
public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost)
public SubmarineInfo SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -283,19 +290,21 @@ namespace Barotrauma
}
}
Campaign!.Money -= cost;
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0)
{
Campaign!.GetWallet(client).TryDeduct(cost);
}
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
return newSubmarine;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine)
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
{
if (Campaign is null) { return; }
if (Campaign.Money < newSubmarine.Price) { return; }
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.GetWallet(client).TryDeduct(newSubmarine.Price)) { return; }
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
Campaign.Money -= newSubmarine.Price;
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
OwnedSubmarines.Add(newSubmarine);
}
@@ -346,7 +355,7 @@ namespace Barotrauma
public void StartRound(LevelData? levelData, bool mirrorLevel = false, SubmarineInfo? startOutpost = null, SubmarineInfo? endOutpost = null)
{
AfflictionPrefab.LoadAllEffects();
MirrorLevel = mirrorLevel;
if (SubmarineInfo == null)
{
@@ -411,9 +420,6 @@ namespace Barotrauma
}
}
//Clear out the stored grids
Powered.Grids.Clear();
Level? level = null;
if (levelData != null)
{
@@ -422,6 +428,11 @@ namespace Barotrauma
InitializeLevel(level);
//Clear out the cached grids and force update
Powered.Grids.Clear();
casualties.Clear();
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Start,
GameMode?.Preset?.Identifier.Value ?? "none");
@@ -480,7 +491,7 @@ namespace Barotrauma
existingRoundSummary.ContinueButton.Visible = true;
}
RoundSummary = new RoundSummary(Submarine.Info, GameMode, Missions, StartLocation, EndLocation);
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
{
@@ -723,7 +734,7 @@ namespace Barotrauma
}
partial void UpdateProjSpecific(float deltaTime);
public static IEnumerable<Character> GetSessionCrewCharacters()
{
#if SERVER
@@ -745,7 +756,7 @@ namespace Barotrauma
{
IEnumerable<Character> crewCharacters = GetSessionCrewCharacters();
int prevMoney = (GameMode as CampaignMode)?.Money ?? 0;
int prevMoney = (GameMode as CampaignMode)?.Bank.Balance ?? 0; // FIXME personal wallets - reward distribution
foreach (Mission mission in missions)
{
@@ -759,14 +770,13 @@ namespace Barotrauma
if (missions.Any())
{
if (missions.Any())
if (missions.Any(m => m.Completed))
{
foreach (Character character in crewCharacters)
{
character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
}
}
if (missions.All(m => m.Completed))
{
foreach (Character character in crewCharacters)
@@ -818,7 +828,7 @@ namespace Barotrauma
LogEndRoundStats(eventId);
if (GameMode is CampaignMode campaignMode)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Money - prevMoney);
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", campaignMode.Bank.Balance - prevMoney); // FIXME personal wallets - reward distrubiton
campaignMode.TotalPlayTime += roundDuration;
}
#if CLIENT
@@ -910,6 +920,10 @@ namespace Barotrauma
public void KillCharacter(Character character)
{
if (CrewManager != null && CrewManager.GetCharacters().Contains(character))
{
casualties.Add(character);
}
#if CLIENT
CrewManager?.KillCharacter(character);
#endif
@@ -917,6 +931,7 @@ namespace Barotrauma
public void ReviveCharacter(Character character)
{
casualties.Remove(character);
#if CLIENT
CrewManager?.ReviveCharacter(character);
#endif
@@ -939,7 +954,7 @@ namespace Barotrauma
List<string> excessPackages = new List<string>();
foreach (ContentPackage cp in ContentPackageManager.EnabledPackages.All)
{
//if (!cp.HasMultiplayerIncompatibleContent) { continue; }
if (!cp.HasMultiplayerSyncedContent) { continue; }
if (!contentPackagePaths.Any(p => p == cp.Path))
{
excessPackages.Add(cp.Name);
@@ -949,7 +964,7 @@ namespace Barotrauma
bool orderMismatch = false;
if (missingPackages.Count == 0 && missingPackages.Count == 0)
{
var enabledPackages = ContentPackageManager.EnabledPackages.All/*.Where(cp => cp.HasMultiplayerIncompatibleContent)*/.ToImmutableArray();
var enabledPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).ToImmutableArray();
for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Length; i++)
{
if (contentPackagePaths[i] != enabledPackages[i].Path)
@@ -1015,7 +1030,7 @@ namespace Barotrauma
}
if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
rootElement.Add(new XAttribute("selectedcontentpackages",
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Path))));
((CampaignMode)GameMode).Save(doc.Root);
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -177,17 +178,20 @@ namespace Barotrauma
}
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private readonly CampaignMode? campaign;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
#if CLIENT
campaign.OnMoneyChanged.RegisterOverwriteExisting(nameof(MedicalClinic).ToIdentifier(), OnMoneyChanged);
#endif
}
public readonly List<NetCrewMember> PendingHeals = new List<NetCrewMember>();
public Action? OnUpdate;
private static bool IsOutpostInCombat()
{
if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; }
@@ -203,14 +207,13 @@ namespace Barotrauma
return false;
}
private HealRequestResult HealAllPending(bool force = false)
private HealRequestResult HealAllPending(bool force = false, Client? client = null)
{
int totalCost = GetTotalCost();
if (!force)
{
if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; }
if (IsOutpostInCombat()) { return HealRequestResult.Refused; }
if (!GetWallet(client).TryDeduct(totalCost)) { return HealRequestResult.InsufficientFunds; }
}
ImmutableArray<CharacterInfo> crew = GetCrewCharacters();
@@ -225,11 +228,6 @@ namespace Barotrauma
}
}
if (campaign != null)
{
campaign.Money -= totalCost;
}
ClearPendingHeals();
return HealRequestResult.Success;
@@ -316,7 +314,10 @@ namespace Barotrauma
private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue;
public int GetMoney() => campaign?.Money ?? 0;
public Wallet GetWallet(Client? c = null)
{
return campaign?.GetWallet(c) ?? Wallet.Invalid;
}
public static ImmutableArray<CharacterInfo> GetCrewCharacters()
{
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
@@ -178,7 +179,7 @@ namespace Barotrauma
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
/// </remarks>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false)
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -215,7 +216,7 @@ namespace Barotrauma
price = 0;
}
if (Campaign.Money >= price)
if (Campaign.GetWallet(client).TryDeduct(price)) // FIXME personal wallets
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
@@ -227,7 +228,6 @@ namespace Barotrauma
}
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value);
PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
@@ -253,14 +253,14 @@ namespace Barotrauma
else
{
DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}");
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
}
}
/// <summary>
/// Purchases an item swap and handles logic for deducting the credit.
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false)
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -313,7 +313,7 @@ namespace Barotrauma
price = 0;
}
if (Campaign.Money >= price)
if (Campaign.GetWallet(client).TryDeduct(price))
{
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -326,7 +326,6 @@ namespace Barotrauma
}
}
Campaign.Money -= price;
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value);
foreach (Item itemToSwap in linkedItems)
@@ -355,7 +354,7 @@ namespace Barotrauma
else
{
DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" +
$"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}");
$"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
}
}
@@ -58,7 +58,7 @@ namespace Barotrauma
IsEquipped = new bool[capacity];
SlotTypes = new InvSlotType[capacity];
AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true);
AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", false);
AccessibleByOwner = element.GetAttributeBool("accessiblebyowner", AccessibleWhenAlive);
string[] slotTypeNames = ParseSlotTypes(element);
@@ -159,14 +159,14 @@ namespace Barotrauma
{
return
base.CanBePutInSlot(item, i, ignoreCondition) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
(SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1);
}
public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null)
{
return
base.CanBePutInSlot(itemPrefab, i, condition, quality) &&
(SlotTypes[i] == InvSlotType.Any || slots[i].ItemCount < 1);
(SlotTypes[i] == InvSlotType.Any || slots[i].Items.Count < 1);
}
public bool CanBeAutoMovedToCorrectSlots(Item item)
@@ -507,7 +507,7 @@ namespace Barotrauma.Items.Components
list.Remove(this);
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
//no further data needed, the event just triggers the discharge
}
@@ -229,7 +229,6 @@ namespace Barotrauma.Items.Components
int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
Vector2 offset = SpawnAreaOffset;
offset.Y = -offset.Y;
switch (SpawnAreaShape)
{
@@ -707,7 +707,7 @@ namespace Barotrauma.Items.Components
#if SERVER
for (int i = 0; i < Vines.Count; i += VineChunkSize)
{
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i });
item.CreateServerEvent(this, new EventData(offset: i));
}
#elif CLIENT
ResetPlanterSize();
@@ -12,6 +12,16 @@ namespace Barotrauma.Items.Components
{
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
{
private readonly struct EventData : IEventData
{
public readonly Vector2 AttachPos;
public EventData(Vector2 attachPos)
{
AttachPos = attachPos;
}
}
const float MaxAttachDistance = 150.0f;
//the position(s) in the item that the Character grabs
@@ -155,8 +165,8 @@ namespace Barotrauma.Items.Components
[Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")]
public bool SwingWhenUsing { get; set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.85f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is attached to a wall.")]
public float SpriteDepthWhenAttached
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
public float SpriteDepthWhenDropped
{
get;
set;
@@ -244,12 +254,12 @@ namespace Barotrauma.Items.Components
}
}
private bool loadedFromXml;
private bool loadedFromInstance;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
loadedFromXml = true;
loadedFromInstance = true;
if (usePrefabValues)
{
@@ -583,7 +593,7 @@ namespace Barotrauma.Items.Components
Attached = true;
#if CLIENT
item.DrawDepthOffset = SpriteDepthWhenAttached - item.SpriteDepth;
item.DrawDepthOffset = 0.0f;
#endif
}
@@ -600,6 +610,9 @@ namespace Barotrauma.Items.Components
requiredItems.Clear();
DisplayMsg = "";
PickKey = InputType.Select;
#if CLIENT
item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth;
#endif
}
public override void ParseMsg()
@@ -663,12 +676,7 @@ namespace Barotrauma.Items.Components
{
#if CLIENT
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
attachPos
});
item.CreateClientEvent(this, new EventData(attachPos));
#endif
}
return false;
@@ -867,8 +875,8 @@ namespace Barotrauma.Items.Components
{
if (!attachable) { return; }
//a mod has overridden the item, and the base item didn't have a Holdable component = a mod made the item movable/detachable
if (item.Prefab.IsOverride && !loadedFromXml)
//the Holdable component didn't get loaded from an instance of the item, just the prefab xml = a mod or update must've made the item movable/detachable
if (!loadedFromInstance)
{
if (attachedByDefault)
{
@@ -67,7 +67,6 @@ namespace Barotrauma.Items.Components
[Serialize("#ffffff", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Color OwnerSkinColor { get; set; }
#warning TODO: figure out how to set Vector2.Zero as the default here
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Vector2 OwnerSheetIndex { get; set; }
@@ -436,13 +436,10 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
{
GameMain.Server.CreateEntityEvent(item, new object[]
{
Networking.NetEntityEvent.Type.ApplyStatusEffect,
GameMain.Server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(
success ? ActionType.OnUse : ActionType.OnFailure,
null, //itemcomponent
targetCharacter.ID, targetLimb
});
targetItemComponent: null,
targetCharacter, targetLimb));
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
@@ -282,12 +282,12 @@ namespace Barotrauma.Items.Components
}
}
public virtual void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public virtual void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.Write(activePicker == null ? (ushort)0 : activePicker.ID);
msg.Write(activePicker?.ID ?? (ushort)0);
}
public virtual void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
public virtual void ClientEventRead(IReadMessage msg, float sendingTime)
{
ushort pickerID = msg.ReadUInt16();
if (pickerID == 0)
@@ -178,11 +178,11 @@ namespace Barotrauma.Items.Components
throwDone = true;
IsActive = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnSecondaryUse, this, CurrentThrower.ID });
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnSecondaryUse, this, CurrentThrower));
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
if (!(GameMain.NetworkMember is { IsClient: true }))
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
@@ -6,6 +6,7 @@ using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Sounds;
@@ -1036,6 +1037,30 @@ namespace Barotrauma.Items.Components
}
}
public interface IEventData { }
public virtual bool ValidateEventData(NetEntityEvent.IData data)
=> true;
protected T ExtractEventData<T>(NetEntityEvent.IData data) where T : IEventData
=> TryExtractEventData(data, out T componentData)
? componentData
: throw new Exception($"Malformed item component state event for {item.Name} " +
$"(item ID {item.ID}, component type {GetType().Name}): " +
$"could not extract ComponentData of type {typeof(T).Name}");
protected bool TryExtractEventData<T>(NetEntityEvent.IData data, out T componentData)
{
componentData = default;
if (data is Item.ComponentStateEventData { ComponentData: T nestedData })
{
componentData = nestedData;
return true;
}
return false;
}
#region AI related
protected const float AIUpdateInterval = 0.2f;
protected float aiUpdateTimer;
@@ -370,7 +370,10 @@ namespace Barotrauma.Items.Components
public Item GetFocusTarget()
{
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out");
var positionOut = item.Connections.Find(c => c.Name == "position_out");
if (positionOut == null) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
@@ -380,7 +383,16 @@ namespace Barotrauma.Items.Components
return item.LastSentSignalRecipients[i].Item;
}
}
foreach (var recipientPanel in item.GetConnectedComponentsRecursive<ConnectionPanel>(positionOut, allowTraversingBackwards: false))
{
if (recipientPanel.Item.Condition <= 0.0f) { continue; }
if (recipientPanel.Item.Prefab.FocusOnSelected)
{
return recipientPanel.Item;
}
}
return null;
}
@@ -221,7 +221,7 @@ namespace Barotrauma.Items.Components
}
float condition = deconstructProduct.CopyCondition ?
percentageHealth * itemPrefab.Health :
percentageHealth * itemPrefab.Health * deconstructProduct.OutConditionMax :
itemPrefab.Health * Rand.Range(deconstructProduct.OutConditionMin, deconstructProduct.OutConditionMax);
if (DeconstructItemsSimultaneously && deconstructProduct.RequiredOtherItem.Length > 0)
@@ -354,7 +354,14 @@ namespace Barotrauma.Items.Components
{
foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod)
{
containedItem.Drop(dropper: null);
if (availableItem.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
{
Entity.Spawner.AddItemToRemoveQueue(containedItem);
}
else
{
containedItem.Drop(dropper: null);
}
}
}
@@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components
hull.BallastFlora = new BallastFloraBehavior(hull, ballastFloraPrefab, offset, firstGrowth: true);
#if SERVER
hull.BallastFlora.SendNetworkMessage(hull.BallastFlora, BallastFloraBehavior.NetworkHeader.Spawn);
hull.BallastFlora.SendNetworkMessage(new BallastFloraBehavior.SpawnEventData());
#endif
}
@@ -1,11 +1,9 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System.Globalization;
using System.Linq;
namespace Barotrauma.Items.Components
{
@@ -287,7 +285,7 @@ namespace Barotrauma.Items.Components
}
else if (autoTemp)
{
UpdateAutoTemp(10.0f, deltaTime * 2f);
UpdateAutoTemp(2.0f, deltaTime);
}
@@ -300,7 +298,14 @@ namespace Barotrauma.Items.Components
if (!item.HasTag("reactorfuel")) { continue; }
if (fissionRate > 0.0f)
{
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
Item.Submarine?.TeamID == CharacterTeamType.Team1 &&
Item.Submarine.GetConnectedSubs().Any(s => s.Info.IsOutpost && s.TeamID == CharacterTeamType.FriendlyNPC);
if (!isConnectedToFriendlyOutpost)
{
item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime;
}
}
fuelLeft += item.ConditionPercentage;
}
@@ -418,7 +423,7 @@ namespace Barotrauma.Items.Components
{
float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft;
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100);
newLoad = MathHelper.Clamp(loadLeft - (expectedPower + output) + loadAdjust, 0, loadLeft);
newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft);
}
if (float.IsNegative(newLoad))
@@ -498,7 +503,6 @@ namespace Barotrauma.Items.Components
if (temperature > optimalTemperature.Y)
{
float prevFireTimer = fireTimer;
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
#if SERVER
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedConstruction == item)
@@ -506,9 +510,10 @@ namespace Barotrauma.Items.Components
GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
}
#endif
if (fireTimer >= FireDelay && prevFireTimer < fireDelay)
if (fireTimer >= FireDelay)
{
new FireSource(item.WorldPosition);
fireTimer = 0.0f;
}
}
else
@@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components
}
}
public void ServerRead(ClientNetObject type, IReadMessage msg, Client c)
public void ServerEventRead(IReadMessage msg, Client c)
{
bool isActive = msg.ReadBoolean();
bool directionalPing = useDirectionalPing;
@@ -394,7 +394,7 @@ namespace Barotrauma.Items.Components
#endif
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.Write(currentMode == Mode.Active);
if (currentMode == Mode.Active)
@@ -225,7 +225,7 @@ namespace Barotrauma.Items.Components
var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking");
if (dockingConnection != null)
{
var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection);
var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection, allowTraversingBackwards: false);
DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost));
}
}
@@ -271,13 +271,9 @@ namespace Barotrauma.Items.Components
item.CreateClientEvent(this);
correctionTimer = CorrectionDelay;
}
else
#endif
#if SERVER
if (GameMain.Server != null)
{
item.CreateServerEvent(this);
}
item.CreateServerEvent(this);
#endif
networkUpdateTimer = 0.1f;
@@ -2,7 +2,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -11,7 +10,7 @@ namespace Barotrauma.Items.Components
//[power/min]
private float capacity;
private float charge;
private float charge, prevCharge;
//how fast the battery can be recharged
private float maxRechargeSpeed;
@@ -34,7 +33,7 @@ namespace Barotrauma.Items.Components
get { return currPowerOutput; }
private set
{
System.Diagnostics.Debug.Assert(value >= 0.0f);
System.Diagnostics.Debug.Assert(value >= 0.0f, $"Tried to set PowerContainer's output to a negative value ({value})");
currPowerOutput = Math.Max(0, value);
}
}
@@ -140,6 +139,7 @@ namespace Barotrauma.Items.Components
{
IsActive = true;
InitProjSpecific();
prevCharge = Charge;
}
partial void InitProjSpecific();
@@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components
loadReading = powerOut.Grid.Load;
}
item.SendSignal(((int)Math.Round(-CurrPowerOutput)).ToString(), "power_value_out");
item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out");
item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out");
item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge");
item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%");
@@ -194,6 +194,8 @@ namespace Barotrauma.Items.Components
}
else
{
if (item.Condition <= 0.0f) { return 0.0f; }
float missingCharge = capacity - charge;
float targetRechargeSpeed = rechargeSpeed;
@@ -231,7 +233,7 @@ namespace Barotrauma.Items.Components
if (connection == powerOut)
{
float maxOutput;
float chargeRatio = charge / capacity;
float chargeRatio = prevCharge / capacity;
if (chargeRatio < 0.1f)
{
maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut;
@@ -242,7 +244,7 @@ namespace Barotrauma.Items.Components
}
//Limit max power out to not exceed the charge of the container
maxOutput = Math.Min(maxOutput, charge * 60 / UpdateInterval);
maxOutput = Math.Min(maxOutput, prevCharge * 60 / UpdateInterval);
return new PowerRange(0.0f, maxOutput);
}
@@ -261,18 +263,11 @@ namespace Barotrauma.Items.Components
/// <returns></returns>
public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load)
{
if (connection == powerOut)
//Only power out connection can provide power and Max poweroutput can't be negative
if (connection == powerOut && minMaxPower.Max > 0)
{
//Calculate the max power the container can output
float maxPowerOutput = MaxOutPut;
float chargeRatio = charge / capacity;
if (chargeRatio < 0.1f)
{
maxPowerOutput *= Math.Max(chargeRatio * 10.0f, 0.0f);
}
//Set power output based on the relative max power output capabilities and load demand
CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * maxPowerOutput;
CurrPowerOutput = MathHelper.Clamp((load - power) / minMaxPower.Max, 0, 1) * MinMaxPowerOut(connection, load).Max;
return CurrPowerOutput;
}
return 0.0f;
@@ -292,6 +287,7 @@ namespace Barotrauma.Items.Components
{
//Decrease charge based on how much power is leaving the device
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity);
prevCharge = Charge;
}
}
@@ -176,23 +176,6 @@ namespace Barotrauma.Items.Components
{
RefreshConnections();
float powerReadingOut = 0;
float loadReadingOut = ExtraLoad;
if (powerLoad < 0)
{
powerReadingOut = -powerLoad;
loadReadingOut = 0;
}
if (powerOut != null && powerOut.Grid != null)
{
powerReadingOut = powerOut.Grid.Power;
loadReadingOut = powerOut.Grid.Load;
}
item.SendSignal(((int)Math.Round(powerReadingOut)).ToString(), "power_value_out");
item.SendSignal(((int)Math.Round(loadReadingOut)).ToString(), "load_value_out");
if (Timing.TotalTime > extraLoadSetTime + 1.0)
{
//Decay the extra load to 0 from either positive or negative
@@ -216,22 +199,36 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
if (prevSentPowerValue != (int)-CurrPowerConsumption || powerSignal == null)
float powerReadingOut = 0;
float loadReadingOut = ExtraLoad;
if (powerLoad < 0)
{
prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption);
powerReadingOut = -powerLoad;
loadReadingOut = 0;
}
if (powerOut != null && powerOut.Grid != null)
{
powerReadingOut = powerOut.Grid.Power;
loadReadingOut = powerOut.Grid.Load;
}
if (prevSentPowerValue != (int)powerReadingOut || powerSignal == null)
{
prevSentPowerValue = (int)Math.Round(powerReadingOut);
powerSignal = prevSentPowerValue.ToString();
}
if (prevSentLoadValue != (int)powerLoad || loadSignal == null)
if (prevSentLoadValue != (int)loadReadingOut || loadSignal == null)
{
prevSentLoadValue = (int)Math.Round(powerLoad);
prevSentLoadValue = (int)Math.Round(loadReadingOut);
loadSignal = prevSentLoadValue.ToString();
}
item.SendSignal(powerSignal, "power_value_out");
item.SendSignal(loadSignal, "load_value_out");
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = Voltage > maxOverVoltage;
@@ -187,14 +187,11 @@ namespace Barotrauma.Items.Components
protected void UpdateOnActiveEffects(float deltaTime)
{
if (currPowerConsumption <= 0.0f)
if (currPowerConsumption <= 0.0f && PowerConsumption <= 0.0f)
{
//if the item consumes no power, ignore the voltage requirement and
//apply OnActive statuseffects as long as this component is active
if (PowerConsumption <= 0.0f)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
return;
}
@@ -216,6 +213,11 @@ namespace Barotrauma.Items.Components
powerOnSoundPlayed = false;
}
#endif
if (powerIn == null)
{
//power down the device here if it has no power connection (= receives power from contained battery cells instead of the "normal" power logic)
Voltage -= deltaTime;
}
}
public override void Update(float deltaTime, Camera cam)
@@ -238,7 +240,11 @@ namespace Barotrauma.Items.Components
else if (c.Name == "power_out")
{
powerOut = c;
powerOut.Priority = Priority;
// Connection takes the lowest priority
if (Priority > powerOut.Priority)
{
powerOut.Priority = Priority;
}
}
else if (c.Name == "power")
{
@@ -258,7 +264,11 @@ namespace Barotrauma.Items.Components
#endif
}
powerOut = c;
powerOut.Priority = Priority;
// Connection takes the lowest priority
if (Priority > powerOut.Priority)
{
powerOut.Priority = Priority;
}
}
else
{
@@ -591,7 +601,7 @@ namespace Barotrauma.Items.Components
foreach (Connection con in grid.Connections)
{
Powered device = con.Item.GetComponent<Powered>();
device.GridResolved(con);
device?.GridResolved(con);
}
}
@@ -196,6 +196,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool FriendlyFire
{
get;
set;
}
private float deactivationTimer;
[Serialize(0f, IsPropertySaveable.No)]
@@ -309,7 +316,7 @@ namespace Barotrauma.Items.Components
{
#if SERVER
launchRot = rotation;
Item.CreateServerEvent(this, new object[] { true }); //true = indicate that this is a launch event
Item.CreateServerEvent(this, new EventData(launch: true));
#endif
}
}
@@ -673,7 +680,7 @@ namespace Barotrauma.Items.Components
{
Unstick();
#if SERVER
item.CreateServerEvent(this);
item.CreateServerEvent(this, new EventData(launch: false));
#endif
}
}
@@ -697,9 +704,9 @@ namespace Barotrauma.Items.Components
return false;
}
if (hits.Contains(target.Body)) { return false; }
if (ShouldIgnoreSubmarineCollision(target, contact))
if (target.Body.UserData is Submarine)
{
return false;
if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; }
}
else if (target.Body.UserData is Limb limb)
{
@@ -709,6 +716,10 @@ namespace Barotrauma.Items.Components
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
return false;
}
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
}
else if (target.Body.UserData is Item item)
{
@@ -893,8 +904,8 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.NetworkMember.IsServer)
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, targetLimb.character.ID, targetLimb, (ushort)0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, targetLimb.character, targetLimb, null, item.WorldPosition));
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, null, item.WorldPosition));
}
#endif
}
@@ -905,8 +916,8 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.NetworkMember.IsServer)
{
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, actionType, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact, this, (ushort)0, null, (target.Body.UserData as Entity)?.ID ?? 0, item.WorldPosition });
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(actionType, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, null, null, target.Body.UserData as Entity, item.WorldPosition));
}
#endif
}
@@ -952,7 +963,7 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
item.CreateServerEvent(this);
item.CreateServerEvent(this, new EventData(launch: false));
}
#endif
item.body.LinearVelocity *= speedMultiplier;
@@ -230,7 +230,7 @@ namespace Barotrauma.Items.Components
{
ApplyStatusEffects(ActionType.OnFailure, 1.0f, CurrentFixer);
#if SERVER
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, CurrentFixer.ID });
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, CurrentFixer));
#endif
}
}
@@ -256,10 +256,10 @@ namespace Barotrauma.Items.Components
if (!CheckCharacterSuccess(character, bestRepairItem))
{
GameServer.Log($"{GameServer.CharacterLogName(character)} failed to {(action == FixActions.Sabotage ? "sabotage" : "repair")} {item.Name}", ServerLog.MessageType.ItemInteraction);
GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID });
GameMain.Server?.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, this, character));
if (bestRepairItem != null && bestRepairItem.GetComponent<Holdable>() is Holdable h)
{
GameMain.Server?.CreateEntityEvent(bestRepairItem, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, h, character.ID });
GameMain.Server?.CreateEntityEvent(bestRepairItem, new Item.ApplyStatusEffectEventData(ActionType.OnFailure, h, character));
}
return false;
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
//the output is sent if both inputs have received a signal within the timeframe
protected float timeFrame;
protected readonly Character[] signalSender = new Character[2];
[Serialize(999999.0f, IsPropertySaveable.Yes, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true),
InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)]
public float ClampMax
@@ -33,7 +35,7 @@ namespace Barotrauma.Items.Components
[InGameEditable(DecimalCount = 2),
Serialize(0.0f, IsPropertySaveable.Yes, description: "The item must have received signals to both inputs within this timeframe to output the result." +
" If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)]
" If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true, translationTextTag: "sp.")]
public float TimeFrame
{
get { return timeFrame; }
@@ -71,7 +73,7 @@ namespace Barotrauma.Items.Components
float output = Calculate(receivedSignal[0], receivedSignal[1]);
if (MathUtils.IsValid(output))
{
item.SendSignal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), "signal_out");
item.SendSignal(new Signal(MathHelper.Clamp(output, ClampMin, ClampMax).ToString("G", CultureInfo.InvariantCulture), sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
}
@@ -83,11 +85,13 @@ namespace Barotrauma.Items.Components
{
case "signal_in1":
float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]);
signalSender[0] = signal.sender;
timeSinceReceived[0] = 0.0f;
IsActive = true;
break;
case "signal_in2":
float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]);
signalSender[1] = signal.sender;
timeSinceReceived[1] = 0.0f;
IsActive = true;
break;
@@ -109,10 +109,23 @@ namespace Barotrauma.Items.Components
return true;
}
private void Write(IWriteMessage msg, object[] extraData)
private readonly struct EventData : IEventData
{
if (extraData == null || extraData.Length < 3) { return; }
msg.WriteRangedInteger((int)extraData[2], 0, Signals.Length - 1);
public readonly int SignalIndex;
public EventData(int signalIndex)
{
SignalIndex = signalIndex;
}
}
public override bool ValidateEventData(NetEntityEvent.IData data)
=> TryExtractEventData<EventData>(data, out _);
private void Write(IWriteMessage msg, NetEntityEvent.IData extraData)
{
var eventData = ExtractEventData<EventData>(extraData);
msg.WriteRangedInteger(eventData.SignalIndex, 0, Signals.Length - 1);
}
}
}
@@ -400,7 +400,7 @@ namespace Barotrauma.Items.Components
}
public void ClientWrite(IWriteMessage msg, object[] extraData = null)
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
#if CLIENT
TriggerRewiringSound();
@@ -8,6 +8,16 @@ namespace Barotrauma.Items.Components
{
partial class CustomInterface : ItemComponent, IClientSerializable, IServerSerializable
{
private readonly struct EventData : IEventData
{
public readonly CustomInterfaceElement BtnElement;
public EventData(CustomInterfaceElement btnElement)
{
BtnElement = btnElement;
}
}
class CustomInterfaceElement : ISerializableEntity
{
public bool ContinuousSignal;
@@ -1,7 +1,6 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -1,13 +1,11 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
namespace Barotrauma.Items.Components
{
class NotComponent : ItemComponent
{
private bool signalReceived;
private bool continuousOutput;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "When enabled, the component continuously outputs \"1\" when it's not receiving a signal.", alwaysUseInstanceValues: true)]
public bool ContinuousOutput
{
get { return continuousOutput; }
@@ -372,12 +372,12 @@ namespace Barotrauma.Items.Components
IsOn = on;
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.Write(isOn);
}
public void ClientRead(ServerNetObject type, IReadMessage msg, float _)
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
SetState(msg.ReadBoolean(), true);
}
@@ -68,7 +68,7 @@ namespace Barotrauma.Items.Components
{
foreach (FireSource fireSource in hull.FireSources)
{
if (fireSource.IsInDamageRange(item.WorldPosition, fireSource.DamageRange * 2.0f)) { return true; }
if (fireSource.IsInDamageRange(item.WorldPosition, Math.Max(fireSource.DamageRange * 2.0f, 500.0f))) { return true; }
}
}
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System;
using System.Globalization;
using System.Xml.Linq;
using System;
namespace Barotrauma.Items.Components
{
@@ -20,6 +20,8 @@ namespace Barotrauma.Items.Components
private readonly float[] receivedSignal = new float[2];
private readonly float[] timeSinceReceived = new float[2];
protected Character signalSender;
[Serialize(FunctionType.Sin, IsPropertySaveable.No, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)]
public FunctionType Function
{
@@ -56,7 +58,7 @@ namespace Barotrauma.Items.Components
{
float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]);
if (!UseRadians) { angle = MathHelper.ToDegrees(angle); }
item.SendSignal(angle.ToString("G", CultureInfo.InvariantCulture), "signal_out");
item.SendSignal(new Signal(angle.ToString("G", CultureInfo.InvariantCulture), sender: signalSender), "signal_out");
}
}
}
@@ -65,6 +67,7 @@ namespace Barotrauma.Items.Components
{
float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float value);
bool sendOutputImmediately = true;
signalSender = signal.sender;
switch (Function)
{
case FunctionType.Sin:
@@ -455,12 +455,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (GameMain.NetworkMember != null)
{
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
nodes.Count
});
item.CreateClientEvent(this, new ClientEventData(nodes.Count));
}
#endif
}
@@ -482,12 +477,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (GameMain.NetworkMember != null)
{
GameMain.Client.CreateEntityEvent(item, new object[]
{
NetEntityEvent.Type.ComponentState,
item.GetComponentIndex(this),
nodes.Count
});
item.CreateClientEvent(this, new ClientEventData(nodes.Count));
}
#endif
}
@@ -49,8 +49,6 @@ namespace Barotrauma.Items.Components
private ChargingState currentChargingState;
private float currentBarrelSpin = 0f;
private readonly List<Item> activeProjectiles = new List<Item>();
public IEnumerable<Item> ActiveProjectiles => activeProjectiles;
@@ -330,10 +328,10 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
base.OnMapLoaded();
FindLightComponent();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
targetRotation = rotation;
FindLightComponent();
UpdateTransformedBarrelPos();
}
@@ -736,6 +734,16 @@ namespace Barotrauma.Items.Components
return true;
}
private readonly struct EventData : IEventData
{
public readonly Item Projectile;
public EventData(Item projectile)
{
Projectile = projectile;
}
}
private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f)
{
reload = reloadTime;
@@ -749,7 +757,7 @@ namespace Barotrauma.Items.Components
if (projectile != null)
{
activeProjectiles.Add(projectile);
projectile.Drop(null);
projectile.Drop(null, setTransform: false);
if (projectile.body != null)
{
projectile.body.Dir = 1.0f;
@@ -787,12 +795,11 @@ namespace Barotrauma.Items.Components
}
}
if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); }
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile });
projectile.Container?.RemoveContained(projectile);
}
#if SERVER
item.CreateServerEvent(this, new EventData(projectile));
#endif
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
LaunchProjSpecific();
@@ -1630,11 +1637,11 @@ namespace Barotrauma.Items.Components
}
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
if (extraData.Length > 2)
if (TryExtractEventData(extraData, out EventData eventData))
{
msg.Write(!(extraData[2] is Item item) ? ushort.MaxValue : item.ID);
msg.Write(eventData.Projectile.ID);
msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16);
}
else
@@ -540,16 +540,16 @@ namespace Barotrauma.Items.Components
Variant = loadedVariant;
}
}
public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
public override void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
msg.Write((byte)Variant);
base.ServerWrite(msg, c, extraData);
base.ServerEventWrite(msg, c, extraData);
}
public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime)
public override void ClientEventRead(IReadMessage msg, float sendingTime)
{
Variant = (int)msg.ReadByte();
base.ClientRead(type, msg, sendingTime);
base.ClientEventRead(msg, sendingTime);
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
{
partial class Inventory : IServerSerializable, IClientSerializable
{
public const int MaxStackSize = 32;
public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
public class ItemSlot
{
@@ -18,15 +18,7 @@ namespace Barotrauma
public bool HideIfEmpty;
public IEnumerable<Item> Items
{
get { return items; }
}
public int ItemCount
{
get { return items.Count; }
}
public IReadOnlyList<Item> Items => items;
public bool CanBePut(Item item, bool ignoreCondition = false)
{
@@ -631,7 +623,7 @@ namespace Barotrauma
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].ItemCount < item.Prefab.MaxStackSize) { return false; }
if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; }
}
}
else
@@ -842,29 +834,30 @@ namespace Barotrauma
public virtual void CreateNetworkEvent()
{
if (GameMain.NetworkMember != null)
if (GameMain.NetworkMember == null) { return; }
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
if (Owner is Character character)
{
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState });
GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData());
}
else if (Owner is Item item)
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData());
}
}
public Item FindItem(Func<Item, bool> predicate, bool recursive)
{
Item match = AllItems.FirstOrDefault(i => predicate(i));
Item match = AllItems.FirstOrDefault(predicate);
if (match == null && recursive)
{
foreach (var item in AllItems)
{
if (item == null) { continue; }
if (item.OwnInventory != null)
{
match = item.OwnInventory.FindItem(predicate, recursive: true);
if (match != null)
{
return match;
}
}
if (item?.OwnInventory == null) { continue; }
match = item.OwnInventory.FindItem(predicate, recursive: true);
if (match != null) { return match; }
}
}
return match;
@@ -946,16 +939,31 @@ namespace Barotrauma
slots[index].RemoveItem(item);
}
public void SharedWrite(IWriteMessage msg, object[] extraData = null)
public void SharedRead(IReadMessage msg, out List<ushort>[] newItemIds)
{
byte slotCount = msg.ReadByte();
newItemIds = new List<ushort>[slotCount];
for (int i = 0; i < slotCount; i++)
{
newItemIds[i] = new List<ushort>();
int itemCount = msg.ReadRangedInteger(0, MaxStackSize);
for (int j = 0; j < itemCount; j++)
{
newItemIds[i].Add(msg.ReadUInt16());
}
}
}
public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
{
msg.Write((byte)capacity);
for (int i = 0; i < capacity; i++)
{
msg.WriteRangedInteger(slots[i].ItemCount, 0, MaxStackSize);
foreach (Item item in slots[i].Items)
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize);
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++)
{
msg.Write((ushort)(item == null ? 0 : item.ID));
var item = slots[i].Items[j];
msg.Write(item?.ID ?? (ushort)0);
}
}
}
@@ -20,7 +20,7 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
{
public static List<Item> ItemList = new List<Item>();
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
@@ -573,7 +573,7 @@ namespace Barotrauma
if (connections == null) { return; }
foreach (Connection c in connections.Values)
{
if (c.IsPower && c.Grid != null)
if (c.IsPower)
{
Powered.ChangedConnections.Add(c);
foreach (Connection conn in c.Recipients)
@@ -827,6 +827,23 @@ namespace Barotrauma
public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam;
public bool OrderedToBeIgnored { get; set; }
public bool HasBallastFloraInHull
{
get
{
return CurrentHull?.BallastFlora != null;
}
}
public bool IsClaimedByBallastFlora
{
get
{
if (CurrentHull?.BallastFlora == null) { return false; }
return CurrentHull.BallastFlora.ClaimedTargets.Contains(this);
}
}
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
: this(new Rectangle(
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
@@ -1417,6 +1434,7 @@ namespace Barotrauma
public bool HasAccess(Character character)
{
if (HiddenInGame) { return false; }
if (character.IsBot && IgnoreByAI(character)) { return false; }
if (!IsInteractable(character)) { return false; }
var itemContainer = GetComponent<ItemContainer>();
@@ -1656,14 +1674,13 @@ namespace Barotrauma
public void SendPendingNetworkUpdates()
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsServer) { return; }
if (conditionUpdatePending)
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
if (!conditionUpdatePending) { return; }
GameMain.NetworkMember.CreateEntityEvent(this, new StatusEventData());
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
private bool isActive = true;
@@ -1919,20 +1936,19 @@ namespace Barotrauma
private void HandleCollision(float impact)
{
OnCollisionProjSpecific(impact);
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance)
{
ApplyStatusEffects(ActionType.OnImpact, 1.0f);
#if SERVER
GameMain.Server?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnImpact });
#endif
}
if (GameMain.NetworkMember is { IsClient: true }) { return; }
foreach (Item contained in ContainedItems)
{
if (contained.body != null) { contained.HandleCollision(impact); }
}
if (ImpactTolerance > 0.0f && condition > 0.0f && Math.Abs(impact) > ImpactTolerance)
{
ApplyStatusEffects(ActionType.OnImpact, 1.0f);
#if SERVER
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
#endif
}
foreach (Item contained in ContainedItems)
{
if (contained.body != null) { contained.HandleCollision(impact); }
}
}
@@ -1995,14 +2011,14 @@ namespace Barotrauma
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// </summary>
public List<T> GetConnectedComponents<T>(bool recursive = false) where T : ItemComponent
public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true) where T : ItemComponent
{
List<T> connectedComponents = new List<T>();
if (recursive)
{
HashSet<Connection> alreadySearched = new HashSet<Connection>();
GetConnectedComponentsRecursive(alreadySearched, connectedComponents);
GetConnectedComponentsRecursive(alreadySearched, connectedComponents, allowTraversingBackwards: allowTraversingBackwards);
return connectedComponents;
}
@@ -2025,7 +2041,7 @@ namespace Barotrauma
return connectedComponents;
}
private void GetConnectedComponentsRecursive<T>(HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays = false) where T : ItemComponent
private void GetConnectedComponentsRecursive<T>(HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
{
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
if (connectionPanel == null) { return; }
@@ -2034,18 +2050,18 @@ namespace Barotrauma
{
if (alreadySearched.Contains(c)) { continue; }
alreadySearched.Add(c);
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays);
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
}
}
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// </summary>
public List<T> GetConnectedComponentsRecursive<T>(Connection c, bool ignoreInactiveRelays = false) where T : ItemComponent
public List<T> GetConnectedComponentsRecursive<T>(Connection c, bool ignoreInactiveRelays = false, bool allowTraversingBackwards = true) where T : ItemComponent
{
List<T> connectedComponents = new List<T>();
HashSet<Connection> alreadySearched = new HashSet<Connection>();
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays);
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
return connectedComponents;
}
@@ -2062,7 +2078,7 @@ namespace Barotrauma
("signal_in2".ToIdentifier(), "signal_out".ToIdentifier())
}.ToImmutableArray();
private void GetConnectedComponentsRecursive<T>(Connection c, HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays) where T : ItemComponent
private void GetConnectedComponentsRecursive<T>(Connection c, HashSet<Connection> alreadySearched, List<T> connectedComponents, bool ignoreInactiveRelays, bool allowTraversingBackwards = true) where T : ItemComponent
{
alreadySearched.Add(c);
@@ -2087,12 +2103,12 @@ namespace Barotrauma
foreach (Connection wifiOutput in receiverConnections)
{
if ((wifiOutput.IsOutput == recipient.IsOutput) || alreadySearched.Contains(wifiOutput)) { continue; }
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays);
GetConnectedComponentsRecursive(wifiOutput, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
}
}
}
recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays);
recipient.Item.GetConnectedComponentsRecursive(recipient, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
}
if (ignoreInactiveRelays)
@@ -2111,12 +2127,12 @@ namespace Barotrauma
if (pairedConnection != null)
{
if (alreadySearched.Contains(pairedConnection)) { return; }
GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays);
GetConnectedComponentsRecursive(pairedConnection, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
}
}
}
searchFromAToB(input, output);
searchFromAToB(output, input);
if (allowTraversingBackwards) { searchFromAToB(output, input); }
}
}
@@ -2490,7 +2506,7 @@ namespace Barotrauma
#if CLIENT
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Treatment, character.ID, targetLimb });
GameMain.Client.CreateEntityEvent(this, new TreatmentEventData(character, targetLimb));
return;
}
#endif
@@ -2523,12 +2539,10 @@ namespace Barotrauma
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[]
{
NetEntityEvent.Type.ApplyStatusEffect, actionType, ic, character.ID, targetLimb
});
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(
actionType, ic, character, targetLimb));
}
if (ic.DeleteOnUse) { remove = true; }
@@ -2553,12 +2567,18 @@ namespace Barotrauma
if (ic.Combine(item, user)) { isCombined = true; }
}
#if CLIENT
if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Combine, item.ID }); }
if (isCombined) { GameMain.Client?.CreateEntityEvent(this, new CombineEventData(item)); }
#endif
return isCombined;
}
public void Drop(Character dropper, bool createNetworkEvent = true)
/// <summary>
///
/// </summary>
/// <param name="dropper">Character who dropped the item</param>
/// <param name="createNetworkEvent">Should clients be notified of the item being dropped</param>
/// <param name="setTransform">Should the transform of the physics body be updated. Only disable this if you're moving the item somewhere else / calling SetTransform manually immediately after dropping!</param>
public void Drop(Character dropper, bool createNetworkEvent = true, bool setTransform = true)
{
if (createNetworkEvent)
{
@@ -2585,7 +2605,7 @@ namespace Barotrauma
"Failed to drop the item \"" + Name + "\" (body has been removed"
+ (Removed ? ", item has been removed)" : ")"));
}
else
else if (setTransform)
{
body.SetTransform(dropper.SimPosition, 0.0f);
}
@@ -2596,7 +2616,10 @@ namespace Barotrauma
if (Container != null)
{
SetTransform(Container.SimPosition, 0.0f);
if (setTransform)
{
SetTransform(Container.SimPosition, 0.0f);
}
Container.RemoveContained(this);
Container = null;
}
@@ -2646,12 +2669,12 @@ namespace Barotrauma
return allProperties;
}
private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly)
private void WritePropertyChange(IWriteMessage msg, ChangePropertyEventData extraData, bool inGameEditableOnly)
{
//ignoreConditions: true = include all ConditionallyEditable properties at this point,
//to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
SerializableProperty property = extraData[1] as SerializableProperty;
SerializableProperty property = extraData.SerializableProperty;
if (property != null)
{
var propertyOwner = allProperties.Find(p => p.Second == property);
@@ -2910,9 +2933,9 @@ namespace Barotrauma
}
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
if (GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.ChangeProperty, property });
GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property));
}
}
@@ -2980,7 +3003,7 @@ namespace Barotrauma
#if SERVER
if (createNetworkEvent)
{
Spawner.CreateNetworkEvent(item, remove: false);
Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
}
#endif
@@ -3016,7 +3039,7 @@ namespace Barotrauma
{
if (!(property.GetValue(item)?.Equals(prevValue) ?? true))
{
GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ChangeProperty, property });
GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property));
}
}
}
@@ -0,0 +1,114 @@
using System;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
partial class Item
{
public enum EventType
{
ComponentState = 0,
InventoryState = 1,
Treatment = 2,
ChangeProperty = 3,
Combine = 4,
Status = 5,
AssignCampaignInteraction = 6,
ApplyStatusEffect = 7,
Upgrade = 8,
MinValue = 0,
MaxValue = 6
}
public interface IEventData : NetEntityEvent.IData
{
public EventType EventType { get; }
}
public struct ComponentStateEventData : IEventData
{
public EventType EventType => EventType.ComponentState;
public readonly ItemComponent Component;
public readonly ItemComponent.IEventData ComponentData;
public ComponentStateEventData(ItemComponent component, ItemComponent.IEventData componentData)
{
Component = component;
ComponentData = componentData;
}
}
public readonly struct InventoryStateEventData : IEventData
{
public EventType EventType => EventType.InventoryState;
public readonly ItemContainer Component;
public InventoryStateEventData(ItemContainer component)
{
Component = component;
}
}
public readonly struct ChangePropertyEventData : IEventData
{
public EventType EventType => EventType.ChangeProperty;
public readonly SerializableProperty SerializableProperty;
public ChangePropertyEventData(SerializableProperty serializableProperty)
{
SerializableProperty = serializableProperty;
}
}
private readonly struct StatusEventData : IEventData
{
public EventType EventType => EventType.Status;
}
private readonly struct AssignCampaignInteractionEventData : IEventData
{
public EventType EventType => EventType.AssignCampaignInteraction;
}
public readonly struct ApplyStatusEffectEventData : IEventData
{
public EventType EventType => EventType.ApplyStatusEffect;
public readonly ActionType ActionType;
public readonly ItemComponent TargetItemComponent;
public readonly Character TargetCharacter;
public readonly Limb TargetLimb;
public readonly Entity UseTarget;
public readonly Vector2? WorldPosition;
public ApplyStatusEffectEventData(
ActionType actionType,
ItemComponent targetItemComponent = null,
Character targetCharacter = null,
Limb targetLimb = null,
Entity useTarget = null,
Vector2? worldPosition = null)
{
ActionType = actionType;
TargetItemComponent = targetItemComponent;
TargetCharacter = targetCharacter;
TargetLimb = targetLimb;
UseTarget = useTarget;
WorldPosition = worldPosition;
}
}
private readonly struct UpgradeEventData : IEventData
{
public EventType EventType => EventType.Upgrade;
public readonly Upgrade Upgrade;
public UpgradeEventData(Upgrade upgrade)
{
Upgrade = upgrade;
}
}
}
}
@@ -48,14 +48,14 @@ namespace Barotrauma
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(item, i)) { return false; }
return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].ItemCount < container.GetMaxStackSize(i);
return item != null && slots[i].CanBePut(item, ignoreCondition) && slots[i].Items.Count < container.GetMaxStackSize(i);
}
public override bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition, int? quality = null)
{
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(itemPrefab, i)) { return false; }
return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].ItemCount < container.GetMaxStackSize(i);
return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i);
}
public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition)
@@ -74,7 +74,7 @@ namespace Barotrauma
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].ItemCount < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; }
if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; }
}
}
else
@@ -145,8 +145,7 @@ namespace Barotrauma
return;
}
int componentIndex = container.Item.GetComponentIndex(container);
if (componentIndex == -1)
if (!container.Item.Components.Contains(container))
{
DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components");
return;
@@ -155,7 +154,7 @@ namespace Barotrauma
if (GameMain.NetworkMember != null)
{
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new object[] { NetEntityEvent.Type.InventoryState, componentIndex });
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container));
}
}
@@ -43,10 +43,6 @@ namespace Barotrauma
OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
CopyCondition = element.GetAttributeBool("copycondition", false);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
if (element.Attribute("copycondition") != null && element.Attribute("outcondition") != null)
{
DebugConsole.AddWarning($"Invalid deconstruction output in \"{parentDebugName}\": the output item \"{ItemIdentifier}\" has the out condition set, but is also set to copy the condition of the deconstructed item. Ignoring the out condition.");
}
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor",
element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]);
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]);

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