Build 0.18.0.0

This commit is contained in:
Markus Isberg
2022-05-13 00:55:52 +09:00
parent 15d18e6ff6
commit 7547a9b78a
218 changed files with 3881 additions and 2192 deletions
@@ -31,6 +31,17 @@ namespace Barotrauma
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
if (_selectedAiTarget != null)
{
if (_selectedAiTarget.Entity is Item i && _previousAiTarget.Entity is Character c)
{
if (i.IsOwnedBy(c)) { return; }
}
else if (_previousAiTarget.Entity is Item it && _selectedAiTarget.Entity is Character ch)
{
if (it.IsOwnedBy(ch)) { return; }
}
}
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
@@ -1055,6 +1055,9 @@ namespace Barotrauma
private Vector2 attackWorldPos;
private Vector2 attackSimPos;
private float reachTimer;
// How long the monster tries to reach out for the target when it's close to it before ignoring it.
private const float reachTimeOut = 10;
private void UpdateAttack(float deltaTime)
{
@@ -1427,6 +1430,22 @@ namespace Barotrauma
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackLimb.attack.Range;
if (canAttack)
{
reachTimer = 0;
}
else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5)
{
reachTimer += deltaTime;
if (reachTimer > reachTimeOut)
{
reachTimer = 0;
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
return;
}
}
// Crouch if the target is down (only humanoids), so that we can reach it.
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
@@ -1958,9 +1977,8 @@ namespace Barotrauma
}
if (!isFriendly && attackResult.Damage > 0.0f)
{
ignoredTargets.Remove(attacker.AiTarget);
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
if (AIParams.AttackWhenProvoked && canAttack)
if (AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.AiTarget))
{
if (attacker.IsHusk)
{
@@ -3476,6 +3494,7 @@ namespace Barotrauma
{
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
reachTimer = 0;
}
protected override void OnStateChanged(AIState from, AIState to)
@@ -3496,6 +3515,7 @@ namespace Barotrauma
SetStateResetTimer();
}
blockCheckTimer = 0;
reachTimer = 0;
}
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
@@ -59,7 +59,11 @@ namespace Barotrauma
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800;
private readonly float enemySpotDistanceInside = 1000;
private float enemycheckTimer;
private float enemyCheckTimer;
private readonly float reportProblemsInterval = 1.0f;
private float reportProblemsTimer;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
@@ -166,6 +170,7 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
reportProblemsTimer = Rand.Range(0f, reportProblemsInterval);
}
public override void Update(float deltaTime)
@@ -309,10 +314,10 @@ namespace Barotrauma
{
// Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
enemycheckTimer -= deltaTime;
if (enemycheckTimer < 0)
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
@@ -407,19 +412,29 @@ namespace Barotrauma
{
if (Character.IsOnPlayerTeam)
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
}
}
else
{
// Outpost npcs don't inform each other about threats, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
}
}
}
if (Character.SpeechImpediment < 100.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
reportProblemsTimer -= deltaTime;
if (reportProblemsTimer <= 0.0f)
{
ReportProblems();
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
}
@@ -785,9 +800,10 @@ namespace Barotrauma
if (item == null || item.Removed) { return; }
if (!itemsToRelocate.Contains(item)) { return; }
var mainSub = Submarine.MainSub;
if (item.ParentInventory != null)
Entity owner = item.GetRootInventoryOwner();
if (owner != null)
{
if (item.ParentInventory.Owner is Character c)
if (owner is Character c)
{
if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2)
{
@@ -795,24 +811,37 @@ namespace Barotrauma
return;
}
}
else if (item.ParentInventory.Owner.Submarine == mainSub)
else if (owner.Submarine == mainSub)
{
// Placed inside an inventory that's already in the main sub.
return;
}
}
// Laying on ground inside the main sub.
// Laying on the ground inside the main sub.
if (item.Submarine == mainSub)
{
return;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub);
if (wp != null)
if (owner != null && owner != item)
{
item.Submarine = mainSub;
item.SetTransform(wp.SimPosition, 0.0f);
item.Drop(null);
}
item.Submarine = mainSub;
Item newContainer = mainSub.FindContainerFor(item, onlyPrimary: false);
if (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null))
{
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub) ?? WayPoint.GetRandom(SpawnType.Path, null, mainSub);
if (wp != null)
{
item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false);
}
else
{
DebugConsole.AddWarning($"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
}
}
itemsToRelocate.Remove(item);
DebugConsole.Log($"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub.");
}
}
@@ -1149,7 +1178,7 @@ namespace Barotrauma
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID)
if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
@@ -1356,6 +1385,10 @@ namespace Barotrauma
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
if (attacker.IsInstigator)
{
return attacker;
@@ -1545,7 +1578,7 @@ namespace Barotrauma
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
}
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
@@ -1889,7 +1922,7 @@ namespace Barotrauma
float fireFactor = 1;
if (!ignoreFire)
{
float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
@@ -1897,10 +1930,22 @@ namespace Barotrauma
float enemyFactor = 1;
if (!ignoreEnemies)
{
bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested;
int enemyCount = visibleHulls == null ?
Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) :
Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull));
int enemyCount = 0;
foreach (Character c in Character.CharacterList)
{
if (visibleHulls == null)
{
if (c.CurrentHull != hull) { continue; }
}
else
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
{
enemyCount++;
}
}
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
}
@@ -1911,6 +1956,7 @@ namespace Barotrauma
if (item.Prefab != null && item.Prefab.IsDangerous)
{
dangerousItemsFactor = 0;
break;
}
}
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
@@ -113,7 +113,7 @@ namespace Barotrauma
else
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null)))
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Count > 0))
{
return false;
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
using static Barotrauma.AIObjectiveFindSafety;
namespace Barotrauma
{
@@ -775,7 +776,13 @@ namespace Barotrauma
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
if (hullSearchStatus != HullSearchStatus.Finished)
{
findSafety.UpdateSimpleEscape(deltaTime);
return;
}
retreatTarget = potentialSafeHull;
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -785,21 +792,21 @@ namespace Barotrauma
{
UsePathingOutside = false
},
onAbandon: () =>
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -192,9 +193,17 @@ namespace Barotrauma
}
else
{
HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
if (hullSearchStatus != HullSearchStatus.Finished)
{
UpdateSimpleEscape(deltaTime);
return;
}
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
@@ -250,58 +259,122 @@ namespace Barotrauma
}
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
UpdateSimpleEscape(deltaTime);
}
}
public void UpdateSimpleEscape(float deltaTime)
{
Vector2 escapeVel = Vector2.Zero;
if (character.CurrentHull != null)
{
foreach (Hull hull in HumanAIController.VisibleHulls)
{
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
foreach (Hull hull in HumanAIController.VisibleHulls)
foreach (FireSource fireSource in hull.FireSources)
{
foreach (FireSource fireSource in hull.FireSources)
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
}
if (escapeVel != Vector2.Zero)
{
float left = character.CurrentHull.Rect.X + 50;
float right = character.CurrentHull.Rect.Right - 50;
//only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
public enum HullSearchStatus
{
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float EstimateHullSuitability(Hull hull)
Running,
Finished
}
private readonly List<Hull> hulls = new List<Hull>();
private int hullSearchIndex = -1;
float bestHullValue = 0;
bool bestHullIsAirlock = false;
Hull potentialBestHull;
/// <summary>
/// Tries to find the best (safe, nearby) hull the character can find a path to.
/// Checks one hull at a time, and returns HullSearchStatus.Finished when all potential hulls have been checked.
/// </summary>
public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable<Hull> ignoredHulls = null, bool allowChangingSubmarine = true)
{
if (hullSearchIndex == -1)
{
bestHullValue = 0;
potentialBestHull = null;
bestHullIsAirlock = false;
hulls.Clear();
var connectedSubs = character.Submarine?.GetConnectedSubs();
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
if (hull.Submarine.Info.IsRuin) { continue; }
if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (!hulls.Any())
{
hulls.Add(hull);
}
else
{
for (int i = 0; i < hulls.Count; i++)
{
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
{
hulls.Insert(i, hull);
break;
}
}
}
}
if (hulls.None())
{
bestHull = null;
return HullSearchStatus.Finished;
}
hullSearchIndex = 0;
}
static float EstimateHullSuitability(Character character, Hull hull)
{
float dist =
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
@@ -314,86 +387,91 @@ namespace Barotrauma
return suitability;
}
Hull bestHull = null;
float bestValue = 0;
bool bestIsAirlock = false;
foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h)))
Hull potentialHull = hulls[hullSearchIndex];
float hullSafety = 0;
bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
if (hull.Submarine.Info.IsRuin) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety > bestHullValue)
{
//avoid airlock modules if not allowed to change the sub
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock"))
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
{
continue;
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
hullSafety = 0;
HumanAIController.UnreachableHulls.Add(potentialHull);
}
else
{
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
{
hullSafety /= 10;
}
}
}
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
else
{
HumanAIController.UnreachableHulls.Add(hull);
continue;
hullSafety = 0;
}
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
else
{
// TODO: could also target gaps that get us inside?
if (hull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestIsAirlock && hull.LeadsOutside(character))
{
hullSafety = 100;
}
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock))
{
bestHull = hull;
bestValue = hullSafety;
bestIsAirlock = hullIsAirlock;
}
}
return bestHull;
else
{
// TODO: could also target gaps that get us inside?
if (potentialHull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
{
hullSafety = 100;
}
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock))
{
potentialBestHull = potentialHull;
bestHullValue = hullSafety;
bestHullIsAirlock = hullIsAirlock;
}
bestHull = potentialBestHull;
hullSearchIndex++;
if (hullSearchIndex >= hulls.Count)
{
hullSearchIndex = -1;
return HullSearchStatus.Finished;
}
return HullSearchStatus.Running;
}
public override void Reset()
@@ -165,6 +165,7 @@ namespace Barotrauma
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h),
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull),
// 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()
},
@@ -471,7 +471,8 @@ namespace Barotrauma
{
if (spawnItemIfNotFound)
{
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
@@ -501,6 +502,33 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns the "best" item to spawn when using <see cref="spawnItemIfNotFound"/> and there's multiple suitable items.
/// Best in this context is the one that's sold at the lowest price in stores (usually the most "basic" item)
/// </summary>
/// <returns></returns>
private ItemPrefab FindItemToSpawn()
{
ItemPrefab bestItem = null;
float lowestCost = float.MaxValue;
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
if (!(prefab is ItemPrefab itemPrefab)) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
itemPrefab.DefaultPrice.Price :
float.MaxValue;
if (cost < lowestCost || bestItem == null)
{
bestItem = itemPrefab;
lowestCost = cost;
}
}
}
return bestItem;
}
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using static Barotrauma.AIObjectiveFindSafety;
namespace Barotrauma
{
@@ -186,7 +187,9 @@ namespace Barotrauma
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
HullSearchStatus hullSearchStatus = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls);
if (hullSearchStatus != HullSearchStatus.Finished) { return; }
safeHull = potentialSafeHull;
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
protected bool Aiming => aiming || aimingMelee;
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
public float ArmLength => upperArmLength + forearmLength;
@@ -275,6 +275,8 @@ namespace Barotrauma
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public float LockFlippingUntil;
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
@@ -380,18 +382,10 @@ namespace Barotrauma
{
//if holding two items that should control the characters' pose, let the item in the right hand do it
bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent<Holdable>()?.ControlPose ?? false);
if (!anotherItemControlsPose)
if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater)
{
var head = GetLimb(LimbType.Head);
if (head != null)
{
head.body.SmoothRotate(itemAngle, force: 30 * head.Mass);
}
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
aiming = true;
}
@@ -164,8 +164,6 @@ namespace Barotrauma
public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale;
public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale;
public float LockFlippingUntil;
public override Vector2 AimSourceSimPos
{
get
@@ -841,7 +839,7 @@ namespace Barotrauma
rotation += 360;
}
float targetSpeed = TargetMovement.Length();
if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim))
if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !Aiming)
{
if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
@@ -153,12 +153,12 @@ namespace Barotrauma
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
public static void ThrowIfAccessingWalletsInSingleplayer()
private void ThrowIfAccessingWalletsInSingleplayer()
{
#if CLIENT && DEBUG
if (Screen.Selected is TestScreen) { return; }
#endif
if (GameMain.NetworkMember is null || GameMain.IsSingleplayer)
if ((GameMain.NetworkMember is null || GameMain.IsSingleplayer) && IsPlayer)
{
throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead.");
}
@@ -560,18 +560,35 @@ namespace Barotrauma
#if CLIENT
CharacterHealth.SetHealthBarVisibility(value == null);
#elif SERVER
if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
{
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign)
#if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
{
mpCampaign.Bank.Give(balance);
switch (settings.LootedMoneyDestination)
{
case LootedMoneyDestination.Wallet when IsPlayer:
Wallet.Give(balance);
break;
default:
mpCampaign.Bank.Give(balance);
break;
}
}
grabbedWallet.Deduct(balance);
GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money);
}
#elif CLIENT
if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign)
{
spCampaign.Bank.Give(balance);
}
#endif
grabbedWallet.Deduct(balance);
}
}
}
@@ -1443,7 +1460,7 @@ namespace Barotrauma
foreach (Item item in Inventory.AllItems)
{
if (item?.Prefab.Identifier != "idcard") { continue; }
if (item?.GetComponent<IdCard>() == null) { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
@@ -3954,7 +3971,10 @@ namespace Barotrauma
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
foreach (Limb limb in AnimController.Limbs)
{
limb.ApplyStatusEffects(actionType, deltaTime);
}
}
//OnActive effects are handled by the afflictions themselves
if (actionType != ActionType.OnActive)
@@ -252,12 +252,11 @@ namespace Barotrauma
}
/// <summary>
/// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them
/// Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e.g. with an endocrine booster)
/// </summary>
public IEnumerable<Identifier> GetEndocrineTalents()
public IEnumerable<Identifier> GetUnlockedTalentsOutsideTree()
{
if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t));
}
@@ -1182,7 +1181,7 @@ namespace Barotrauma
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; }
if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; }
foreach (var tag in item.Tags.Split(','))
{
var splitTag = tag.Split(":");
@@ -300,6 +300,7 @@ namespace Barotrauma
}
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs["bleeding"];
public static AfflictionPrefab Burn => Prefabs["burn"];
@@ -124,7 +124,18 @@ namespace Barotrauma
public float PressureKillDelay { get; private set; } = 5.0f;
public float Vitality { get; private set; }
private float vitality;
public float Vitality
{
get
{
return Character.IsDead ? minVitality : vitality;
}
private set
{
vitality = value;
}
}
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
@@ -725,6 +736,8 @@ namespace Barotrauma
AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
}
partial void UpdateSkinTint();
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
@@ -788,7 +801,7 @@ namespace Barotrauma
if (!Character.GodMode)
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
@@ -798,23 +811,6 @@ namespace Barotrauma
}
}
private void UpdateSkinTint()
{
FaceTint = DefaultFaceTint;
BodyTint = Color.TransparentBlack;
if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
}
}
private void UpdateDamageReductions(float deltaTime)
{
float healthRegen = Character.Params.Health.ConstantHealthRegeneration;
@@ -905,6 +901,7 @@ namespace Barotrauma
if (Unkillable || Character.GodMode) { return; }
var (type, affliction) = GetCauseOfDeath();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
Character.Kill(type, affliction);
#if CLIENT
@@ -105,9 +105,9 @@ namespace Barotrauma
return spawnPointTags;
}
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced)
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced, Func<JobPrefab, bool> predicate = null)
{
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate);
}
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
@@ -209,11 +209,8 @@ namespace Barotrauma
}
}
if (item.Prefab.Identifier == "idcard")
{
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(spawnPoint, character);
}
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(spawnPoint, character);
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
@@ -293,6 +293,6 @@ namespace Barotrauma
//ClothingElement = element.GetChildElement("PortraitClothing");
}
public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
public static JobPrefab Random(Rand.RandSync sync, Func<JobPrefab, bool> predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync);
}
}
@@ -331,7 +331,7 @@ namespace Barotrauma
#if CLIENT
if (isSevered)
{
damageOverlayStrength = 100.0f;
damageOverlayStrength = 1.0f;
}
#endif
}
@@ -597,18 +597,7 @@ namespace Barotrauma
dir = Direction.Right;
body = new PhysicsBody(limbParams);
type = limbParams.Type;
if (limbParams.IgnoreCollisions)
{
body.CollisionCategories = Category.None;
body.CollidesWith = Category.None;
IgnoreCollisions = true;
}
else
{
//limbs don't collide with each other
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
}
IgnoreCollisions = limbParams.IgnoreCollisions;
body.UserData = this;
pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale))
{
@@ -646,10 +635,9 @@ namespace Barotrauma
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (!character.VariantOf.IsEmpty)
if (character is { VariantOf: { IsEmpty: false } })
{
var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab)
? basePrefab.ConfigElement.GetChildElement("attack") : null;
var attackElement = character.Params.VariantFile.Root.GetChildElement("attack");
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior";
protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors";
protected override PrefabCollection<BallastFloraPrefab> prefabs => BallastFloraPrefab.Prefabs;
protected override PrefabCollection<BallastFloraPrefab> Prefabs => BallastFloraPrefab.Prefabs;
protected override BallastFloraPrefab CreatePrefab(ContentXElement element)
{
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "cave";
protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters";
protected override PrefabCollection<CaveGenerationParams> prefabs => CaveGenerationParams.CaveParams;
protected override PrefabCollection<CaveGenerationParams> Prefabs => CaveGenerationParams.CaveParams;
protected override CaveGenerationParams CreatePrefab(ContentXElement element)
{
return new CaveGenerationParams(element, this);
@@ -77,7 +77,7 @@ namespace Barotrauma
{
HashSet<string> texturePaths = new HashSet<string>
{
ragdollParams.Texture
ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value
};
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
{
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse";
protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses";
protected override PrefabCollection<CorpsePrefab> prefabs => CorpsePrefab.Prefabs;
protected override PrefabCollection<CorpsePrefab> Prefabs => CorpsePrefab.Prefabs;
protected override CorpsePrefab CreatePrefab(ContentXElement element)
{
return new CorpsePrefab(element, this);
@@ -8,7 +8,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings";
protected override PrefabCollection<EventManagerSettings> prefabs => EventManagerSettings.Prefabs;
protected override PrefabCollection<EventManagerSettings> Prefabs => EventManagerSettings.Prefabs;
protected override EventManagerSettings CreatePrefab(ContentXElement element)
{
return new EventManagerSettings(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "faction";
protected override bool MatchesPlural(Identifier identifier) => identifier == "factions";
protected override PrefabCollection<FactionPrefab> prefabs => FactionPrefab.Prefabs;
protected override PrefabCollection<FactionPrefab> Prefabs => FactionPrefab.Prefabs;
protected override FactionPrefab CreatePrefab(ContentXElement element)
{
return new FactionPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected abstract bool MatchesSingular(Identifier identifier);
protected abstract bool MatchesPlural(Identifier identifier);
protected abstract PrefabCollection<T> prefabs { get; }
protected abstract PrefabCollection<T> Prefabs { get; }
protected abstract T CreatePrefab(ContentXElement element);
private void LoadFromXElement(ContentXElement parentElement, bool overriding)
@@ -29,14 +29,14 @@ namespace Barotrauma
}
else if (elemName == "clear")
{
prefabs.AddOverrideFile(this);
Prefabs.AddOverrideFile(this);
}
else if (MatchesSingular(elemName))
{
T prefab = CreatePrefab(parentElement);
try
{
prefabs.Add(prefab, overriding);
Prefabs.Add(prefab, overriding);
}
catch
{
@@ -53,7 +53,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -68,12 +68,12 @@ namespace Barotrauma
public override sealed void UnloadFile()
{
prefabs.RemoveByFile(this);
Prefabs.RemoveByFile(this);
}
public sealed override void Sort()
{
prefabs.SortAll();
Prefabs.SortAll();
}
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly";
protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies";
protected override PrefabCollection<ItemAssemblyPrefab> prefabs => ItemAssemblyPrefab.Prefabs;
protected override PrefabCollection<ItemAssemblyPrefab> Prefabs => ItemAssemblyPrefab.Prefabs;
protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element)
{
return new ItemAssemblyPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "items";
protected override PrefabCollection<ItemPrefab> prefabs => ItemPrefab.Prefabs;
protected override PrefabCollection<ItemPrefab> Prefabs => ItemPrefab.Prefabs;
protected override ItemPrefab CreatePrefab(ContentXElement element)
{
return new ItemPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects";
protected override PrefabCollection<LevelObjectPrefab> prefabs => LevelObjectPrefab.Prefabs;
protected override PrefabCollection<LevelObjectPrefab> Prefabs => LevelObjectPrefab.Prefabs;
protected override LevelObjectPrefab CreatePrefab(ContentXElement element)
{
return new LevelObjectPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes";
protected override PrefabCollection<LocationType> prefabs => LocationType.Prefabs;
protected override PrefabCollection<LocationType> Prefabs => LocationType.Prefabs;
protected override LocationType CreatePrefab(ContentXElement element)
{
return new LocationType(element, this);
@@ -23,7 +23,7 @@ namespace Barotrauma
/*missionTypes.Any(t => identifier == t.Name)
|| identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/
protected override bool MatchesPlural(Identifier identifier) => identifier == "missions";
protected override PrefabCollection<MissionPrefab> prefabs => MissionPrefab.Prefabs;
protected override PrefabCollection<MissionPrefab> Prefabs => MissionPrefab.Prefabs;
protected override MissionPrefab CreatePrefab(ContentXElement element)
{
return new MissionPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset";
protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets";
protected override PrefabCollection<NPCSet> prefabs => NPCSet.Sets;
protected override PrefabCollection<NPCSet> Prefabs => NPCSet.Sets;
protected override NPCSet CreatePrefab(ContentXElement element)
{
return new NPCSet(element, this);
@@ -42,7 +42,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters";
protected override PrefabCollection<OutpostGenerationParams> prefabs => OutpostGenerationParams.OutpostParams;
protected override PrefabCollection<OutpostGenerationParams> Prefabs => OutpostGenerationParams.OutpostParams;
protected override OutpostGenerationParams CreatePrefab(ContentXElement element)
{
return new OutpostGenerationParams(element, this);
@@ -14,7 +14,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles";
protected override PrefabCollection<ParticlePrefab> prefabs => ParticlePrefab.Prefabs;
protected override PrefabCollection<ParticlePrefab> Prefabs => ParticlePrefab.Prefabs;
protected override ParticlePrefab CreatePrefab(ContentXElement element)
{
return new ParticlePrefab(element, this);
@@ -57,7 +57,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters";
protected override PrefabCollection<RuinGenerationParams> prefabs => RuinGenerationParams.RuinParams;
protected override PrefabCollection<RuinGenerationParams> Prefabs => RuinGenerationParams.RuinParams;
protected override RuinGenerationParams CreatePrefab(ContentXElement element)
{
return new RuinGenerationParams(element, this);
@@ -11,7 +11,7 @@ namespace Barotrauma
{
public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override PrefabCollection<SoundPrefab> prefabs => SoundPrefab.Prefabs;
protected override PrefabCollection<SoundPrefab> Prefabs => SoundPrefab.Prefabs;
protected override SoundPrefab CreatePrefab(ContentXElement element)
{
@@ -0,0 +1,12 @@
namespace Barotrauma
{
sealed class StartItemsFile : GenericPrefabFile<StartItemSet>
{
public StartItemsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "itemset";
protected override bool MatchesPlural(Identifier identifier) => identifier == "startitems";
protected override PrefabCollection<StartItemSet> Prefabs => StartItemSet.Sets;
protected override StartItemSet CreatePrefab(ContentXElement element) => new StartItemSet(element, this);
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures";
protected override PrefabCollection<StructurePrefab> prefabs => StructurePrefab.Prefabs;
protected override PrefabCollection<StructurePrefab> Prefabs => StructurePrefab.Prefabs;
protected override StructurePrefab CreatePrefab(ContentXElement element)
{
return new StructurePrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees";
protected override PrefabCollection<TalentTree> prefabs => TalentTree.JobTalentTrees;
protected override PrefabCollection<TalentTree> Prefabs => TalentTree.JobTalentTrees;
protected override TalentTree CreatePrefab(ContentXElement element)
{
return new TalentTree(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "talent";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talents";
protected override PrefabCollection<TalentPrefab> prefabs => TalentPrefab.TalentPrefabs;
protected override PrefabCollection<TalentPrefab> Prefabs => TalentPrefab.TalentPrefabs;
protected override TalentPrefab CreatePrefab(ContentXElement element)
{
return new TalentPrefab(element, this);
@@ -17,7 +17,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission";
protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions";
protected override PrefabCollection<PrefabType> prefabs => PrefabType.Prefabs;
protected override PrefabCollection<PrefabType> Prefabs => PrefabType.Prefabs;
protected override PrefabType CreatePrefab(ContentXElement element)
{
return new PrefabType(element, this);
@@ -14,7 +14,7 @@ namespace Barotrauma
protected override bool MatchesPlural(Identifier identifier) =>
identifier == "upgrademodules";
protected override PrefabCollection<UpgradeContentPrefab> prefabs => UpgradeContentPrefab.PrefabsAndCategories;
protected override PrefabCollection<UpgradeContentPrefab> Prefabs => UpgradeContentPrefab.PrefabsAndCategories;
protected override UpgradeContentPrefab CreatePrefab(ContentXElement element)
{
Identifier elemName = element.NameAsIdentifier();
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs";
protected override PrefabCollection<WreckAIConfig> prefabs => WreckAIConfig.Prefabs;
protected override PrefabCollection<WreckAIConfig> Prefabs => WreckAIConfig.Prefabs;
protected override WreckAIConfig CreatePrefab(ContentXElement element)
{
return new WreckAIConfig(element, this);
@@ -14,8 +14,7 @@ namespace Barotrauma
{
public abstract class ContentPackage
{
#warning TODO: make this independent of the current version
public static readonly Version MinimumHashCompatibleVersion = GameMain.Version;
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 17, 16, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -49,7 +49,10 @@ namespace Barotrauma
.Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase);
}
}
var allPackages = ContentPackageManager.EnabledPackages.All;
var allPackages = ContentPackageManager.AllPackages;
#if CLIENT
if (GameMain.ModDownloadScreen?.DownloadedPackages != null) { allPackages = allPackages.Concat(GameMain.ModDownloadScreen.DownloadedPackages); }
#endif
foreach (Identifier otherModName in otherMods)
{
if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; }
@@ -51,7 +51,7 @@ namespace Barotrauma
=> Element.Descendants().Select(e => new ContentXElement(ContentPackage, e));
public IEnumerable<ContentXElement> GetChildElements(string name)
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.CurrentCultureIgnoreCase));
=> Elements().Where(e => string.Equals(name, e.Name.LocalName, StringComparison.InvariantCultureIgnoreCase));
public XAttribute? GetAttribute(string name) => Element.GetAttribute(name);
@@ -11,7 +11,7 @@ namespace Barotrauma
{
Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " +
$"with name or ID \"{missingPackage ?? "[NULL]"}\" " +
$"that is not currently enabled.";
$"that is not currently installed.";
}
}
}
@@ -780,7 +780,7 @@ namespace Barotrauma
return;
}
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
newEvent.Init();
NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua);
return;
}
@@ -1829,6 +1829,17 @@ namespace Barotrauma
}));
#endif
commands.Add(new Command("startitems|startitemset", "start item set identifier", (string[] args) =>
{
if (args.Length == 0)
{
ThrowError($"No start item set identifier defined!");
return;
}
AutoItemPlacer.StartItemSet = args[0].ToIdentifier();
NewMessage($"Start item set changed to \"{AutoItemPlacer.StartItemSet}\"");
}, isCheat: false));
//"dummy commands" that only exist so that the server can give clients permissions to use them
//TODO: alphabetical order?
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
@@ -53,8 +53,9 @@ namespace Barotrauma
}
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
spawnPos = Level.Loaded.GetRandomItemPos(
(Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ?
Level.PositionType.MainPath | Level.PositionType.SidePath :
@@ -111,7 +112,7 @@ namespace Barotrauma
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return;
Finished();
Finish();
state = 2;
break;
}
@@ -5,13 +5,16 @@ using System.Collections.Generic;
namespace Barotrauma
{
class Event
{
{
public event Action Finished;
protected bool isFinished;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
public EventSet ParentSet { get; private set; }
public Func<Level.InterestingPosition, bool> SpawnPosFilter;
public bool IsFinished
@@ -42,23 +45,20 @@ namespace Barotrauma
yield break;
}
public virtual void Init(bool affectSubImmediately)
public virtual void Init(EventSet parentSet = null)
{
ParentSet = parentSet;
}
public virtual void Update(float deltaTime)
{
}
public virtual void Finished()
public virtual void Finish()
{
isFinished = true;
}
public virtual bool CanAffectSubImmediately(Level level)
{
return true;
}
Finished?.Invoke();
}
public virtual bool LevelMeetsRequirements()
{
@@ -117,6 +117,8 @@ namespace Barotrauma
public bool Enabled = true;
private MTRandom rand;
public void StartRound(Level level)
{
this.level = level;
@@ -147,7 +149,7 @@ namespace Barotrauma
seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier);
}
}
MTRandom rand = new MTRandom(seed);
rand = new MTRandom(seed);
EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
EventSet additiveSet = null;
@@ -159,12 +161,12 @@ namespace Barotrauma
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet, rand);
CreateEvents(initialEventSet);
}
if (additiveSet != null)
{
pendingEventSets.Add(additiveSet);
CreateEvents(additiveSet, rand);
CreateEvents(additiveSet);
}
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
@@ -183,7 +185,7 @@ namespace Barotrauma
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
newEvent.Init(true);
newEvent.Init();
ActiveEvents.Add(newEvent);
}
else
@@ -362,8 +364,9 @@ namespace Barotrauma
return retVal;
}
private void CreateEvents(EventSet eventSet, Random rand)
private void CreateEvents(EventSet eventSet)
{
selectedEvents.Remove(eventSet);
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
@@ -421,7 +424,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
newEvent.Init(eventSet);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -438,7 +441,7 @@ namespace Barotrauma
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
if (newEventSet != null)
{
CreateEvents(newEventSet, rand);
CreateEvents(newEventSet);
}
}
}
@@ -451,7 +454,7 @@ namespace Barotrauma
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
newEvent.Init(eventSet);
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
@@ -465,7 +468,7 @@ namespace Barotrauma
{
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (location != null && !IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet, rand);
CreateEvents(childEventSet);
}
}
}
@@ -666,6 +669,14 @@ namespace Barotrauma
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
}
}
@@ -58,7 +58,7 @@ namespace Barotrauma
}
#endif
public static List<EventPrefab> GetAllEventPrefabs()
public static List<EventPrefab> GetAllEventPrefabs()
{
List<EventPrefab> eventPrefabs = EventPrefab.Prefabs.ToList();
foreach (var eventSet in Prefabs)
@@ -118,6 +118,8 @@ namespace Barotrauma
public readonly float DefaultCommonness;
public readonly ImmutableDictionary<Identifier, float> OverrideCommonness;
public readonly float ResetTime;
public readonly struct SubEventPrefab
{
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability)
@@ -244,6 +246,7 @@ namespace Barotrauma
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
DefaultCommonness = 1.0f;
foreach (var subElement in element.Elements())
@@ -454,7 +457,6 @@ namespace Barotrauma
{
childSet.Dispose();
}
}
}
}
@@ -39,13 +39,9 @@ namespace Barotrauma
targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty<Identifier>());
}
public override bool CanAffectSubImmediately(Level level)
{
return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount;
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
for (int i = 0; i < itemAmount; i++)
@@ -60,7 +56,7 @@ namespace Barotrauma
if (isFinished) return;
if (targetItems.Count == 0 || timer >= duration)
{
Finished();
Finish();
return;
}
@@ -323,7 +323,7 @@ namespace Barotrauma
{
var newEvent = eventPrefab.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
newEvent.Init();
}
}
@@ -382,7 +382,8 @@ namespace Barotrauma
#if SERVER
totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward);
#endif
if (totalReward > 0)
bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isSingleplayerOrServer && totalReward > 0)
{
campaign.Bank.Give(totalReward);
}
@@ -146,8 +146,15 @@ namespace Barotrauma
tags = element.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", ""));
Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", ""));
Name =
TextManager.Get($"MissionName.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("name", "")))
.Fallback(element.GetAttributeString("name", ""));
Description =
TextManager.Get($"MissionDescription.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("description", "")))
.Fallback(element.GetAttributeString("description", ""));
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
@@ -160,10 +167,15 @@ namespace Barotrauma
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}").Fallback(element.GetAttributeString("successmessage", "Mission completed successfully"));
FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}").Fallback(
TextManager.Get("missionfailed")).Fallback(
GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : "");
SuccessMessage =
TextManager.Get($"MissionSuccess.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("successmessage", "")))
.Fallback(element.GetAttributeString("successmessage", "Mission completed successfully"));
FailureMessage =
TextManager.Get($"MissionFailure.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("missionfailed", "")))
.Fallback(TextManager.Get("missionfailed"))
.Fallback(GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : "");
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
@@ -208,8 +220,14 @@ namespace Barotrauma
headers.Add(string.Empty);
messages.Add(string.Empty);
}
headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", ""));
headers[messageIndex] =
TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("header", "")))
.Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] =
TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("text", "")))
.Fallback(subElement.GetAttributeString("text", ""));
messageIndex++;
break;
case "locationtype":
@@ -270,7 +270,7 @@ namespace Barotrauma
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.Prefab.Identifier == "idcard")
if (item?.GetComponent<IdCard>() != null)
{
item.AddTag("id_pirate");
}
@@ -16,6 +16,8 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly float delayBetweenSpawns;
private float resetTime;
private float resetTimer;
private Vector2? spawnPos;
@@ -24,7 +26,7 @@ namespace Barotrauma
public readonly Level.PositionType SpawnPosType;
private readonly string spawnPointTag;
private bool spawnPending;
private bool spawnPending, spawnReady;
public readonly int MaxAmountPerLevel = int.MaxValue;
@@ -96,6 +98,7 @@ namespace Barotrauma
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0);
if (GameMain.NetworkMember != null)
{
@@ -131,14 +134,14 @@ namespace Barotrauma
}
}
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Sonar.DefaultSonarRange * 0.8f;
return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), GetReferenceSub().WorldPosition) < maxRange * maxRange);
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
if (parentSet != null && resetTime == 0)
{
// Use the parent reset time only if there's no reset time defined for the event.
resetTime = parentSet.ResetTime;
}
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White);
@@ -199,7 +202,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
Submarine refSub = GetReferenceSub();
@@ -267,22 +270,17 @@ namespace Barotrauma
if (!isRuinOrWreck)
{
float minDistance = 20000;
var refSub = GetReferenceSub();
availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
if (Submarine.MainSubs.Length > 1)
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
for (int i = 1; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
}
if (availablePositions.None())
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
chosenPosition = availablePositions.GetRandomUnsynced();
@@ -306,7 +304,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
}
@@ -344,7 +342,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
}
@@ -352,20 +350,42 @@ namespace Barotrauma
}
}
private float GetMinDistanceToSub(Submarine submarine)
private float GetMinDistanceToSub(Submarine submarine)
{
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
if (SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
minDist *= 2;
}
return minDist;
}
public override void Update(float deltaTime)
{
if (disallowed)
{
Finished();
Finish();
return;
}
if (isFinished) { return; }
if (resetTimer > 0)
{
resetTimer -= deltaTime;
if (resetTimer <= 0)
{
if (ParentSet?.ResetTime > 0)
{
// If parent has reset time defined, the set is recreated. Otherwise we'll just reset this event.
Finish();
}
else
{
spawnReady = false;
spawnPos = null;
}
}
return;
}
if (spawnPos == null)
{
@@ -373,7 +393,11 @@ namespace Barotrauma
{
if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel)
{
disallowed = true;
// If the event is set to reset, let's just wait until the old corpse is removed (after being disabled).
if (resetTime == 0)
{
disallowed = true;
}
return;
}
}
@@ -384,9 +408,14 @@ namespace Barotrauma
spawnPending = true;
}
bool spawnReady = false;
if (spawnPending)
{
System.Diagnostics.Debug.Assert(spawnPos.HasValue);
if (spawnPos == null)
{
Finish();
return;
}
//wait until there are no submarines at the spawnpos
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
@@ -554,28 +583,24 @@ namespace Barotrauma
}
}
if (!spawnReady) { return; }
Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
#if CLIENT
if (Character.Controlled != null) { targetEntity = Character.Controlled; }
#endif
bool monstersDead = true;
foreach (Character monster in monsters)
if (spawnReady)
{
if (!monster.IsDead)
if (monsters.None())
{
monstersDead = false;
if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f)
Finish();
}
else if (monsters.All(m => m.IsDead))
{
if (resetTime > 0)
{
break;
resetTimer = resetTime;
}
else
{
Finish();
}
}
}
if (monstersDead) { Finished(); }
}
}
}
@@ -173,14 +173,14 @@ namespace Barotrauma
if (!Actions.Any())
{
Finished();
Finish();
return;
}
var currentAction = Actions[CurrentActionIndex];
if (!currentAction.CanBeFinished())
{
Finished();
Finish();
return;
}
@@ -207,7 +207,7 @@ namespace Barotrauma
if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)
{
Finished();
Finish();
}
}
else
@@ -232,9 +232,9 @@ namespace Barotrauma
return false;
}
public override void Finished()
public override void Finish()
{
base.Finished();
base.Finish();
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
}
}
@@ -3,36 +3,32 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
#warning TODO: This class needs some changes:
// - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods.
// - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism.
// - SpawnItems and SpawnItem are named incorrectly.
static class AutoItemPlacer
{
public static bool OutputDebugInfo = false;
/// <summary>
/// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle"
/// </summary>
public const float DefaultDifficultyModifier = 0f;
public static void PlaceIfNeeded()
public static void SpawnItems()
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
for (int i = 0; i < Submarine.MainSubs.Length; i++)
bool skipMainSubs = GameMain.GameSession.GameMode is CampaignMode { IsFirstRound: false };
if (!skipMainSubs)
{
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned) { continue; }
SpawnStartItems(sub);
var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID);
CreateAndPlace(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
}
float difficultyModifier = GetLevelDifficultyModifier();
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Player ||
@@ -42,33 +38,93 @@ namespace Barotrauma
{
continue;
}
Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier);
if (sub.Info.InitialSuppliesSpawned) { continue; }
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name));
Place(Level.Loaded.StartOutpost.ToEnumerable());
var sub = Level.Loaded.StartOutpost;
if (!sub.Info.InitialSuppliesSpawned)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(sub.Info.Name));
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
}
}
private const float MaxDifficultyModifier = 0.2f;
/// <summary>
/// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty.
/// </summary>
private static float GetLevelDifficultyModifier()
{
return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier);
}
public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer)
{
// Level difficulty currently doesn't affect regenerated loot for the sake of simplicity
Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
}
private static void Place(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier)
public static Identifier StartItemSet = new Identifier("normal");
private static void SpawnStartItems(Submarine sub)
{
if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet))
{
DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{StartItemSet}\"!");
return;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
ISpatialEntity initialSpawnPos;
if (wp?.CurrentHull == null)
{
var spawnHull = Hull.HullList.Where(h => h.Submarine == sub && !h.IsWetRoom).GetRandomUnsynced();
if (spawnHull == null)
{
DebugConsole.AddWarning($"Failed to spawn start items in the sub. No cargo waypoint or dry hulls found to spawn the items in.");
return;
}
initialSpawnPos = spawnHull;
}
else
{
initialSpawnPos = wp;
}
var newItems = new List<Item>();
foreach (var startItem in itemSet.Items)
{
if (!ItemPrefab.Prefabs.TryGet(startItem.Item, out ItemPrefab itemPrefab))
{
DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\"");
continue;
}
for (int i = 0; i < startItem.Amount; i++)
{
var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false);
// Is this necessary?
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
newItems.Add(item);
}
}
var cargoContainers = new List<ItemContainer>();
foreach (var item in newItems)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
var container = sub.FindContainerFor(item, onlyPrimary: true);
if (container == null)
{
var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, initialSpawnPos, ref cargoContainers);
container = cargoContainer?.Item;
}
container?.OwnInventory.TryPutItem(item, user: null);
}
}
private static void CreateAndPlace(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
@@ -76,7 +132,7 @@ namespace Barotrauma
return;
}
List<Item> spawnedItems = new List<Item>(100);
List<Item> itemsToSpawn = new List<Item>(100);
int itemCountApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(70 + 30 * subs.Count());
@@ -100,11 +156,11 @@ namespace Barotrauma
containers.Shuffle(Rand.RandSync.ServerAndClient);
}
foreach (ItemPrefab ip in ItemPrefab.Prefabs)
var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier);
foreach (ItemPrefab ip in itemPrefabs)
{
if (!ip.PreferredContainers.Any()) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) &&
ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
{
prefabsItemsCanSpawnIn.Add(ip);
}
@@ -141,9 +197,9 @@ namespace Barotrauma
{
var subNames = subs.Select(s => s.Info.Name).ToList();
DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:");
foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct())
foreach (string itemName in itemsToSpawn.Select(it => it.Name).Distinct())
{
DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName));
DebugConsole.NewMessage(" - " + itemName + " x" + itemsToSpawn.Count(it => it.Name == itemName));
}
}
@@ -153,24 +209,28 @@ namespace Barotrauma
{
foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems)
{
var matchingItem = spawnedItems.Find(it => takenItem.Matches(it));
var matchingItem = itemsToSpawn.Find(it => takenItem.Matches(it));
if (matchingItem == null) { continue; }
var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem);
if (OutputDebugInfo)
{
DebugConsole.NewMessage($"Removing the stolen item: {matchingItem.Prefab.Identifier} ({matchingItem.ID})");
}
var containedItems = itemsToSpawn.FindAll(it => it.ParentInventory?.Owner == matchingItem);
matchingItem.Remove();
spawnedItems.Remove(matchingItem);
itemsToSpawn.Remove(matchingItem);
foreach (Item containedItem in containedItems)
{
containedItem.Remove();
spawnedItems.Remove(containedItem);
itemsToSpawn.Remove(containedItem);
}
}
}
foreach (Item spawnedItem in spawnedItems)
foreach (Item item in itemsToSpawn)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem));
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in spawnedItem.Components)
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
@@ -186,9 +246,12 @@ namespace Barotrauma
return false;
}
bool success = false;
bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode;
foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers)
{
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; }
if (preferredContainer.CampaignOnly && !isCampaign) { continue; }
if (preferredContainer.NotCampaign && isCampaign) { continue; }
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; }
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true);
if (validContainers.None())
{
@@ -196,10 +259,10 @@ namespace Barotrauma
}
foreach (var validContainer in validContainers)
{
var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier);
var newItems = CreateItems(itemPrefab, containers, validContainer);
if (newItems.Any())
{
spawnedItems.AddRange(newItems);
itemsToSpawn.AddRange(newItems);
success = true;
}
}
@@ -238,16 +301,20 @@ namespace Barotrauma
(3, 0.0f),
};
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
List<Item> spawnedItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
List<Item> newItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability) { return newItems; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return spawnedItems;
return newItems;
}
int amount = validContainer.Value.Amount;
if (amount == 0)
{
amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
}
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true))
@@ -255,14 +322,12 @@ namespace Barotrauma
containers.Remove(validContainer.Key);
break;
}
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab);
int quality =
existingItem?.Quality ??
ToolBox.SelectWeightedRandom(
qualityCommonnesses.Select(q => q.quality).ToList(),
qualityCommonnesses.Select(q => q.commonness).ToList(),
Rand.RandSync.ServerAndClient);
qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient);
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
@@ -277,11 +342,11 @@ namespace Barotrauma
{
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
}
spawnedItems.Add(item);
newItems.Add(item);
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
containers.AddRange(item.GetComponents<ItemContainer>());
}
return spawnedItems;
return newItems;
}
}
}
@@ -22,14 +22,14 @@ namespace Barotrauma
public int Quantity { get; set; }
public bool? IsStoreComponentEnabled { get; set; }
public readonly int BuyerCharacterInfoId;
public readonly int BuyerCharacterInfoIdentifier;
public PurchasedItem(ItemPrefab itemPrefab, int quantity, int buyerCharacterInfoId)
{
ItemPrefabIdentifier = itemPrefab.Identifier;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyerCharacterInfoId;
BuyerCharacterInfoIdentifier = buyerCharacterInfoId;
}
#if CLIENT
@@ -44,7 +44,7 @@ namespace Barotrauma
ItemPrefabIdentifier = itemPrefabId;
Quantity = quantity;
IsStoreComponentEnabled = null;
BuyerCharacterInfoId = buyer?.Character?.Info?.ID ?? Character.Controlled?.Info?.ID ?? 0;
BuyerCharacterInfoIdentifier = buyer?.Character?.Info?.GetIdentifier() ?? Character.Controlled?.Info?.GetIdentifier() ?? 0;
}
public override string ToString()
@@ -284,11 +284,10 @@ namespace Barotrauma
foreach (PurchasedItem item in newItems)
{
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
sb.Append($"\n - {item.ItemPrefab.Name} x{item.Quantity}");
price += itemValue;
}
GameServer.Log($"{NetworkMember.ClientLogName(client, client?.Name ?? "Unknown")} purchased {newItems.Count} item(s) for {TextManager.FormatCurrency(price)}{sb.ToString()}", ServerLog.MessageType.Money);
}
#endif
@@ -317,7 +316,10 @@ namespace Barotrauma
// Exchange money
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
campaign.TryPurchase(client, itemValue);
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
if (GameMain.IsSingleplayer)
{
GameAnalyticsManager.AddMoneySpentEvent(itemValue, GameAnalyticsManager.MoneySink.Store, item.ItemPrefab.Identifier.Value);
}
store.Balance += itemValue;
if (removeFromCrate)
{
@@ -368,12 +370,13 @@ namespace Barotrauma
public void CreatePurchasedItems()
{
purchasedIDCards.Clear();
var items = new List<PurchasedItem>();
foreach (var storeSpecificItems in PurchasedItems)
{
items.AddRange(storeSpecificItems.Value);
}
CreateItems(items, Submarine.MainSub);
CreateItems(items, Submarine.MainSub, this);
PurchasedItems.Clear();
OnPurchasedItemsChanged?.Invoke();
}
@@ -407,7 +410,7 @@ namespace Barotrauma
if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; }
if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; }
if (!ItemAndAllContainersInteractable(item)) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("donttakeitems")) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
return true;
}).Distinct();
@@ -428,7 +431,7 @@ namespace Barotrauma
if (!item.Prefab.CanBeSold) { return false; }
if (item.SpawnedInCurrentOutpost) { return false; }
if (!item.Prefab.AllowSellingWhenBroken && item.ConditionPercentage < 90.0f) { return false; }
if (confirmedItems.Any(ci => ci.Item == item)) { return false; }
if (confirmedItems != null && confirmedItems.Any(ci => ci.Item == item)) { return false; }
if (UndeterminedSoldEntities.TryGetValue(item.Prefab, out int count))
{
int newCount = count - 1;
@@ -448,13 +451,58 @@ namespace Barotrauma
if (containedItems.None()) { return true; }
// Allow selling the item if contained items are unsellable and set to be removed on deconstruct
if (itemContainer.RemoveContainedItemsOnDeconstruct && containedItems.All(it => !it.Prefab.CanBeSold)) { return true; }
// Otherwise there must be no contained items or the contained items must be confirmed as sold
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
if (confirmedItems != null)
{
// Otherwise there must be no contained items or the contained items must be confirmed as sold
if (!containedItems.All(it => confirmedItems.Any(ci => ci.Item == it))) { return false; }
}
}
return true;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub)
public static ItemContainer GetOrCreateCargoContainerFor(ItemPrefab item, ISpatialEntity cargoRoomOrSpawnPoint, ref List<ItemContainer> availableContainers)
{
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(item.CargoContainerIdentifier))
{
itemContainer = availableContainers.Find(ac =>
ac.Inventory.CanBePut(item) &&
(ac.Item.Prefab.Identifier == item.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(item.CargoContainerIdentifier)));
if (itemContainer == null)
{
ItemPrefab containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == item.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(item.CargoContainerIdentifier)));
if (containerPrefab == null)
{
DebugConsole.AddWarning($"CargoManager: could not find the item prefab for container {item.CargoContainerIdentifier}!");
return null;
}
Vector2 containerPosition = cargoRoomOrSpawnPoint is Hull cargoRoom ? GetCargoPos(cargoRoom, containerPrefab) : cargoRoomOrSpawnPoint.Position;
Item containerItem = new Item(containerPrefab, containerPosition, cargoRoomOrSpawnPoint.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.AddWarning($"CargoManager: No ItemContainer component found in {containerItem.Prefab.Identifier}!");
return null;
}
availableContainers.Add(itemContainer);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
}
return itemContainer;
}
public static void CreateItems(List<PurchasedItem> itemsToSpawn, Submarine sub, CargoManager cargoManager)
{
if (itemsToSpawn.Count == 0) { return; }
@@ -496,60 +544,26 @@ namespace Barotrauma
}
List<ItemContainer> availableContainers = new List<ItemContainer>();
ItemPrefab containerPrefab = null;
foreach (PurchasedItem pi in itemsToSpawn)
{
Vector2 position = GetCargoPos(cargoRoom, pi.ItemPrefab);
for (int i = 0; i < pi.Quantity; i++)
{
ItemContainer itemContainer = null;
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
{
itemContainer = availableContainers.Find(ac =>
ac.Inventory.CanBePut(pi.ItemPrefab) &&
(ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (itemContainer == null)
{
containerPrefab = ItemPrefab.Prefabs.Find(ep =>
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant())));
if (containerPrefab == null)
{
DebugConsole.ThrowError("Cargo spawning failed - could not find the item prefab for container \"" + pi.ItemPrefab.CargoContainerIdentifier + "\"!");
continue;
}
Vector2 containerPosition = GetCargoPos(cargoRoom, containerPrefab);
Item containerItem = new Item(containerPrefab, containerPosition, wp.Submarine);
itemContainer = containerItem.GetComponent<ItemContainer>();
if (itemContainer == null)
{
DebugConsole.ThrowError("Cargo spawning failed - container \"" + containerItem.Name + "\" does not have an ItemContainer component!");
continue;
}
availableContainers.Add(itemContainer);
#if SERVER
if (GameMain.Server != null)
{
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(itemContainer.Item));
}
#endif
}
}
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
itemContainer?.Inventory.TryPutItem(item, null);
itemSpawned(item);
var itemContainer = GetOrCreateCargoContainerFor(pi.ItemPrefab, cargoRoom, ref availableContainers);
itemContainer?.Inventory.TryPutItem(item, null);
var idCard = item.GetComponent<IdCard>();
if (cargoManager != null && idCard != null && pi.BuyerCharacterInfoIdentifier != 0)
{
cargoManager.purchasedIDCards.Add((pi, idCard));
}
itemSpawned(pi, item);
#if SERVER
Entity.Spawner?.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(Item item)
static void itemSpawned(PurchasedItem purchased, Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
if (sub != null)
@@ -565,6 +579,23 @@ namespace Barotrauma
itemsToSpawn.Clear();
}
private readonly List<(PurchasedItem purchaseInfo, IdCard idCard)> purchasedIDCards = new List<(PurchasedItem purchaseInfo, IdCard idCard)>();
public void InitPurchasedIDCards()
{
foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
}
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
@@ -603,7 +634,7 @@ namespace Barotrauma
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity),
new XAttribute("storeid", storeSpecificItems.Key),
new XAttribute("buyer", item.BuyerCharacterInfoId)));
new XAttribute("buyer", item.BuyerCharacterInfoIdentifier)));
}
}
parentElement.Add(itemsElement);
@@ -51,8 +51,6 @@ namespace Barotrauma
public ReadyCheck ActiveReadyCheck;
public XElement ActiveOrdersElement { get; set; }
public CrewManager(bool isSinglePlayer)
{
IsSinglePlayer = isSinglePlayer;
@@ -493,9 +491,8 @@ namespace Barotrauma
partial void UpdateProjectSpecific(float deltaTime);
private void SaveActiveOrders(XElement parentElement)
public void SaveActiveOrders(XElement element)
{
ActiveOrdersElement = new XElement("activeorders");
// Only save orders with no fade out time (e.g. ignore orders)
var ordersToSave = new List<Order>();
foreach (var activeOrder in ActiveOrders)
@@ -504,14 +501,13 @@ namespace Barotrauma
if (order == null || activeOrder.FadeOutTime.HasValue) { continue; }
ordersToSave.Add(order.WithManualPriority(CharacterInfo.HighestManualOrderPriority));
}
CharacterInfo.SaveOrders(ActiveOrdersElement, ordersToSave.ToArray());
parentElement?.Add(ActiveOrdersElement);
CharacterInfo.SaveOrders(element, ordersToSave.ToArray());
}
public void LoadActiveOrders()
public void LoadActiveOrders(XElement element)
{
if (ActiveOrdersElement == null) { return; }
foreach (var orderInfo in CharacterInfo.LoadOrders(ActiveOrdersElement))
if (element == null) { return; }
foreach (var orderInfo in CharacterInfo.LoadOrders(element))
{
IIgnorable ignoreTarget = null;
if (orderInfo.IsIgnoreOrder)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
@@ -107,6 +108,8 @@ namespace Barotrauma
protected XElement petsElement;
protected XElement ActiveOrdersElement { get; set; }
public CampaignSettings Settings;
private readonly List<Mission> extraMissions = new List<Mission>();
@@ -739,8 +742,10 @@ namespace Barotrauma
foreach (LocationConnection connection in Map.Connections)
{
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
connection.LevelData.Difficulty = connection.Difficulty;
connection.LevelData.IsBeaconActive = false;
connection.LevelData = new LevelData(connection)
{
IsBeaconActive = false
};
connection.LevelData.HasHuntingGrounds = connection.LevelData.OriginallyHadHuntingGrounds;
}
foreach (Location location in Map.Locations)
@@ -1032,5 +1037,184 @@ namespace Barotrauma
}
}
protected void LeaveUnconnectedSubs(Submarine leavingSub)
{
if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub))
{
Submarine.MainSub = leavingSub;
GameMain.GameSession.Submarine = leavingSub;
GameMain.GameSession.SubmarineInfo = leavingSub.Info;
leavingSub.Info.FilePath = System.IO.Path.Combine(SaveUtil.TempPath, leavingSub.Info.Name + ".sub");
var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub);
GameMain.GameSession.OwnedSubmarines.Add(leavingSub.Info);
foreach (Submarine sub in subsToLeaveBehind)
{
GameMain.GameSession.OwnedSubmarines.RemoveAll(s => s != leavingSub.Info && s.Name == sub.Info.Name);
MapEntity.mapEntityList.RemoveAll(e => e.Submarine == sub && e is LinkedSubmarine);
LinkedSubmarine.CreateDummy(leavingSub, sub);
}
}
}
public SubmarineInfo SwitchSubs()
{
TransferItemsBetweenSubs();
RefreshOwnedSubmarines();
PendingSubmarineSwitch = null;
return GameMain.GameSession.SubmarineInfo;
}
/// <summary>
/// Also serializes the current sub.
/// </summary>
protected void TransferItemsBetweenSubs()
{
Submarine currentSub = GameMain.GameSession.Submarine;
if (currentSub == null || currentSub.Removed)
{
DebugConsole.ThrowError("Cannot transfer items between subs, because the current sub is null or removed!");
return;
}
var itemsToTransfer = new List<(Item item, Item container)>();
if (PendingSubmarineSwitch != null)
{
// Remove items from the old sub
foreach (Item item in Item.ItemList)
{
if (item.Removed) { continue; }
if (item.NonInteractable) { continue; }
if (item.HiddenInGame) { continue; }
if (item.Submarine != currentSub) { continue; }
if (item.Prefab.DontTransferBetweenSubs) { continue; }
if (item.GetRootInventoryOwner() is Character) { continue; }
if (item.GetComponent<Holdable>() == null && item.GetComponent<Wearable>() == null && item.GetComponent<Projectile>() == null) { continue; }
if (item.Components.Any(c => c is Holdable h && h.Attached)) { continue; }
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
itemsToTransfer.Add((item, item.Container));
item.Submarine = null;
}
foreach (var (item, container) in itemsToTransfer)
{
if (container?.Submarine != null)
{
// Drop the item if it's not inside another item set to be transferred.
item.Drop(null, createNetworkEvent: false, setTransform: false);
}
}
}
// Serialize the current sub
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(currentSub);
if (PendingSubmarineSwitch != null && itemsToTransfer.Any())
{
// Load the new sub
var newSub = new Submarine(PendingSubmarineSwitch);
// Move the transferred items
List<ItemContainer> availableContainers = Item.ItemList
.Where(it => it.Submarine == newSub && it.HasTag("crate") && !it.NonInteractable && !it.HiddenInGame && !it.Removed)
.Select(it => it.GetComponent<ItemContainer>())
.Where(c => c != null)
.ToList();
foreach (var (item, oldContainer) in itemsToTransfer)
{
Item newContainer = null;
item.Submarine = newSub;
if (item.Container == null)
{
newContainer = newSub.FindContainerFor(item, onlyPrimary: true, checkTransferConditions: true);
}
if (item.Container == null && (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null, createNetworkEvent: false)))
{
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, newSub);
Hull spawnHull = wp?.CurrentHull ?? Hull.HullList.Where(h => h.Submarine == newSub && !h.IsWetRoom).GetRandomUnsynced();
if (spawnHull == null)
{
DebugConsole.AddWarning($"Failed to transfer items between subs. No cargo waypoint or dry hulls found in the new sub.");
return;
}
if (spawnHull != null)
{
var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, spawnHull, ref availableContainers);
if (cargoContainer == null || !cargoContainer.Inventory.TryPutItem(item, user: null, createNetworkEvent: false))
{
item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false);
}
}
else
{
DebugConsole.AddWarning($"Failed to transfer item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
}
}
string newContainerName = newContainer == null ? "(null)" : $"{newContainer.Prefab.Identifier} ({newContainer.Tags})";
string msg = "Item transfer log error.";
if (oldContainer != null)
{
if (newContainer == null && oldContainer == item.Container)
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) contained inside {oldContainer.Prefab.Identifier} ({oldContainer.ID})";
}
else
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) from {oldContainer.Prefab.Identifier} ({oldContainer.Tags}) to {newContainerName}";
}
}
else
{
msg = $"Transferred {item.Prefab.Identifier} ({item.ID}) to {newContainerName}";
}
#if DEBUG
DebugConsole.NewMessage(msg);
#else
DebugConsole.Log(msg);
#endif
}
// Serialize the new sub
PendingSubmarineSwitch = new SubmarineInfo(newSub);
}
}
protected void RefreshOwnedSubmarines()
{
if (PendingSubmarineSwitch != null)
{
SubmarineInfo previousSub = GameMain.GameSession.SubmarineInfo;
GameMain.GameSession.SubmarineInfo = PendingSubmarineSwitch;
for (int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
{
if (GameMain.GameSession.OwnedSubmarines[i].Name == previousSub.Name)
{
GameMain.GameSession.OwnedSubmarines[i] = previousSub;
break;
}
}
}
}
public void SavePets(XElement parentElement = null)
{
petsElement = new XElement("pets");
PetBehavior.SavePets(petsElement);
parentElement?.Add(petsElement);
}
public void LoadPets()
{
if (petsElement != null)
{
PetBehavior.LoadPets(petsElement);
}
}
public void SaveActiveOrders(XElement parentElement = null)
{
ActiveOrdersElement = new XElement("activeorders");
CrewManager?.SaveActiveOrders(ActiveOrdersElement);
parentElement?.Add(ActiveOrdersElement);
}
public void LoadActiveOrders()
{
CrewManager?.LoadActiveOrders(ActiveOrdersElement);
}
}
}
@@ -155,7 +155,7 @@ namespace Barotrauma
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
CrewManager.AddCharacterElements(subElement);
CrewManager.ActiveOrdersElement = subElement.GetChildElement("activeorders");
ActiveOrdersElement = subElement.GetChildElement("activeorders");
break;
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
@@ -275,7 +275,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, Client? client = null)
public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -293,15 +293,12 @@ namespace Barotrauma
}
}
}
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && cost > 0)
{
Campaign!.TryPurchase(client, cost);
}
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
Campaign!.PendingSubmarineSwitch = newSubmarine;
return newSubmarine;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
@@ -600,10 +597,13 @@ namespace Barotrauma
{
//only place items and corpses here in single player
//the server does this after loading the respawn shuttle
Level?.SpawnNPCs();
Level?.SpawnCorpses();
Level?.PrepareBeaconStation();
AutoItemPlacer.PlaceIfNeeded();
if (Level != null)
{
Level.SpawnNPCs();
Level.SpawnCorpses();
Level.PrepareBeaconStation();
}
AutoItemPlacer.SpawnItems();
}
if (GameMode is MultiPlayerCampaign mpCampaign)
{
@@ -836,6 +836,11 @@ namespace Barotrauma
{
GUI.TogglePauseMenu();
}
if (IsTabMenuOpen)
{
ToggleTabMenu();
}
GUI.PreventPauseMenuToggle = true;
if (!(GameMode is TestGameMode) && Screen.Selected == GameMain.GameScreen && RoundSummary != null)
@@ -1072,8 +1077,21 @@ namespace Barotrauma
rootElement.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal));
rootElement.Add(new XAttribute("version", GameMain.Version));
var submarineInfo = Campaign?.PendingSubmarineSwitch ?? SubmarineInfo;
rootElement.Add(new XAttribute("submarine", submarineInfo == null ? "" : submarineInfo.Name));
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
{
bool hasNewPendingSub = Campaign.PendingSubmarineSwitch != null &&
Campaign.PendingSubmarineSwitch.MD5Hash.StringRepresentation != Submarine.Info.MD5Hash.StringRepresentation;
if (hasNewPendingSub)
{
Campaign.SwitchSubs();
}
else
{
SubmarineInfo = new SubmarineInfo(Submarine);
}
}
rootElement.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name));
if (OwnedSubmarines != null)
{
List<string> ownedSubmarineNames = new List<string>();
@@ -17,7 +17,6 @@ namespace Barotrauma
Deselect,
Shoot,
Command,
ToggleInventory,
TakeOneFromInventorySlot,
TakeHalfFromInventorySlot,
NextFireMode,
@@ -99,8 +99,8 @@ namespace Barotrauma.Items.Components
{
if (!docked && value)
{
if (DockingTarget == null) AttemptDock();
if (DockingTarget == null) return;
if (DockingTarget == null) { AttemptDock(); }
if (DockingTarget == null) { return; }
docked = true;
}
@@ -126,6 +126,14 @@ namespace Barotrauma.Items.Components
/// </summary>
public event Action OnUnDocked;
private bool outpostAutoDockingPromptShown;
enum AllowOutpostAutoDocking
{
Ask, Yes, No
}
private AllowOutpostAutoDocking allowOutpostAutoDocking = AllowOutpostAutoDocking.Ask;
public DockingPort(Item item, ContentXElement element)
: base(item, element)
{
@@ -622,7 +630,8 @@ namespace Barotrauma.Items.Components
{
bodies[i + j * 2] = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X, hullRects[i].Y - hullRects[i].Height * j)),
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)));
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].Right, hullRects[i].Y - hullRects[i].Height * j)),
BodyType.Static);
}
}
@@ -632,7 +641,9 @@ namespace Barotrauma.Items.Components
ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width),
ConvertUnits.ToSimUnits(hullRects[0].Height),
density: 0.0f,
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition));
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition),
Physics.CollisionWall,
Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
outsideBlocker.UserData = this;
}
@@ -742,7 +753,8 @@ namespace Barotrauma.Items.Components
{
bodies[i + j * 2] = GameMain.World.CreateEdge(
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)),
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)));
ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height)),
BodyType.Static);
}
}
@@ -752,7 +764,9 @@ namespace Barotrauma.Items.Components
ConvertUnits.ToSimUnits(hullRects[0].Width),
ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height),
density: 0.0f,
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition));
offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition),
Physics.CollisionWall,
Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
outsideBlocker.UserData = this;
}
@@ -778,8 +792,6 @@ namespace Barotrauma.Items.Components
if (body == null) { continue; }
body.BodyType = BodyType.Static;
body.Friction = 0.5f;
body.CollisionCategories = Physics.CollisionWall;
}
}
@@ -947,7 +959,7 @@ namespace Barotrauma.Items.Components
{
foreach (Body body in bodies)
{
if (body == null) continue;
if (body == null) { continue; }
GameMain.World.Remove(body);
}
bodies = null;
@@ -961,6 +973,9 @@ namespace Barotrauma.Items.Components
{
item.CreateServerEvent(this);
}
#elif CLIENT
autodockingVerification?.Close();
autodockingVerification = null;
#endif
OnUnDocked?.Invoke();
OnUnDocked = null;
@@ -1140,27 +1155,86 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if CLIENT
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient &&
!(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(ClientPermissions.ManageMap) ?? false))
{
return;
}
#endif
if (dockingCooldown > 0.0f) { return; }
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
bool newDockedState = wasDocked;
switch (connection.Name)
{
case "toggle":
if (signal.value != "0")
{
Docked = !docked;
newDockedState = !docked;
}
break;
case "set_active":
case "set_state":
Docked = signal.value != "0";
newDockedState = signal.value != "0";
break;
}
if (newDockedState != wasDocked)
{
bool tryingToToggleOutpostDocking = docked ?
DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false :
FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false;
//trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character
// -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port
// in a way that makes always makes it dock/undock immediately at the start of the roun
if (tryingToToggleOutpostDocking && signal.sender == null)
{
if (allowOutpostAutoDocking == AllowOutpostAutoDocking.Ask)
{
#if CLIENT
if (!outpostAutoDockingPromptShown)
{
autodockingVerification = new GUIMessageBox(string.Empty,
TextManager.Get(newDockedState ? "autodockverification" : "autoundockverification"),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
autodockingVerification.Buttons[0].OnClicked += (btn, userdata) =>
{
autodockingVerification?.Close();
autodockingVerification = null;
if (item.Removed || GameMain.Client == null) { return false; }
allowOutpostAutoDocking = AllowOutpostAutoDocking.Yes;
item.CreateClientEvent(this);
return true;
};
autodockingVerification.Buttons[1].OnClicked += (btn, userdata) =>
{
autodockingVerification?.Close();
autodockingVerification = null;
if (item.Removed || GameMain.Client == null) { return false; }
allowOutpostAutoDocking = AllowOutpostAutoDocking.No;
item.CreateClientEvent(this);
return true;
};
}
#endif
outpostAutoDockingPromptShown = true;
return;
}
else if (allowOutpostAutoDocking == AllowOutpostAutoDocking.No)
{
return;
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
Docked = newDockedState;
}
#if SERVER
if (signal.sender != null && docked != wasDocked)
{
@@ -241,12 +241,14 @@ namespace Barotrauma.Items.Components
Body = new PhysicsBody(
ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
0.0f,
1.5f)
radius: 0.0f,
density: 1.5f,
BodyType.Static,
Physics.CollisionWall,
Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile,
findNewContacts: false)
{
UserData = item,
CollisionCategories = Physics.CollisionWall,
BodyType = BodyType.Static,
Friction = 0.5f
};
Body.SetTransformIgnoreContacts(
@@ -258,11 +260,16 @@ namespace Barotrauma.Items.Components
}
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
base.Move(amount);
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
if (ignoreContacts)
{
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
#if CLIENT
UpdateConvexHulls();
@@ -164,6 +164,8 @@ namespace Barotrauma.Items.Components
public bool SwingWhenAiming { get; set; }
[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; }
[Editable, Serialize(false, IsPropertySaveable.No)]
public bool DisableHeadRotation { get; set; }
[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
@@ -180,11 +182,12 @@ namespace Barotrauma.Items.Components
Pusher = null;
if (element.GetAttributeBool("blocksplayers", false))
{
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density)
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius,
item.body.Density,
BodyType.Dynamic,
Physics.CollisionItemBlocking,
Physics.CollisionCharacter | Physics.CollisionProjectile)
{
BodyType = BodyType.Dynamic,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile,
CollisionCategories = Physics.CollisionItemBlocking,
Enabled = false,
UserData = this
};
@@ -79,11 +79,18 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (trigger != null && amount.LengthSquared() > 0.00001f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
if (ignoreContacts)
{
trigger.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
else
{
trigger.SetTransform(item.SimPosition, 0.0f);
}
}
}
@@ -119,17 +126,19 @@ namespace Barotrauma.Items.Components
}
var body = item.body ?? holdable.Body;
if (body != null)
{
trigger = new PhysicsBody(body.width, body.height, body.radius, body.Density)
trigger = new PhysicsBody(body.width, body.height, body.radius,
body.Density,
BodyType.Static,
Physics.CollisionWall,
Physics.CollisionNone,
findNewContacts: false)
{
UserData = item
};
trigger.FarseerBody.SetIsSensor(true);
trigger.FarseerBody.BodyType = BodyType.Static;
trigger.FarseerBody.CollisionCategories = Physics.CollisionWall;
trigger.FarseerBody.CollidesWith = Physics.CollisionNone;
}
}
@@ -111,8 +111,9 @@ namespace Barotrauma.Items.Components
ActivateNearbySleepingCharacters();
reloadTimer = reload;
reloadTimer /= (1f + character.GetStatValue(StatTypes.MeleeAttackSpeed));
reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier));
reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer;
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
@@ -216,6 +217,10 @@ namespace Barotrauma.Items.Components
{
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
if (ac.InWater)
{
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
}
}
else
{
@@ -71,6 +71,7 @@ namespace Barotrauma.Items.Components
//return if someone is already trying to pick the item
if (pickTimer > 0.0f) { return false; }
if (picker == null || picker.Inventory == null) { return false; }
if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; }
if (PickingTime > 0.0f)
{
@@ -226,7 +227,7 @@ namespace Barotrauma.Items.Components
{
foreach (Connection c in connectionPanel.Connections)
{
foreach (Wire w in c.Wires)
foreach (Wire w in c.Wires.ToArray())
{
if (w == null) continue;
w.Item.Drop(character);
@@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || character.Removed) return false;
if (character == null || character.Removed) { return false; }
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; }
IsActive = true;
@@ -55,12 +55,11 @@ namespace Barotrauma.Items.Components
if (UsableIn == UseEnvironment.Water) { return true; }
}
Vector2 dir = Vector2.Normalize(character.CursorPosition - character.Position);
//move upwards if the cursor is at the position of the character
if (!MathUtils.IsValid(dir)) dir = Vector2.UnitY;
Vector2 dir = character.CursorPosition - character.Position;
if (!MathUtils.IsValid(dir)) { return true; }
float length = 200;
dir = dir.ClampLength(length) / length;
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier;
if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; }
foreach (Limb limb in character.AnimController.Limbs)
@@ -416,7 +416,7 @@ namespace Barotrauma.Items.Components
}
}
public virtual void Move(Vector2 amount) { }
public virtual void Move(Vector2 amount, bool ignoreContacts = false) { }
/// <summary>a Character has picked the item</summary>
public virtual bool Pick(Character picker)
@@ -315,7 +315,7 @@ namespace Barotrauma.Items.Components
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
SetContainedItemPositions();
}
@@ -751,7 +751,11 @@ namespace Barotrauma.Items.Components
return;
}
#endif
Inventory.AllItemsMod.ForEach(it => it.Drop(null));
//if we're unloading the whole sub, no need to drop anything (everything's going to be removed anyway)
if (!Submarine.Unloading)
{
Inventory.AllItemsMod.ForEach(it => it.Drop(null));
}
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
@@ -462,19 +462,8 @@ namespace Barotrauma.Items.Components
{
dir = dir == Direction.Left ? Direction.Right : Direction.Left;
}
userPos.X = -UserPos.X;
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X;
Vector2 flippedPos =
new Vector2(
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
limbPositions[i].Position.Y);
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
}
userPos.X = -UserPos.X;
FlipLimbPositions();
}
public override void FlipY(bool relativeToSub)
@@ -519,6 +508,11 @@ namespace Barotrauma.Items.Components
{
if (Screen.Selected == GameMain.SubEditorScreen)
{
if (item.FlippedX)
{
FlipLimbPositions();
}
// Don't save flipped positions.
foreach (var limbPos in limbPositions)
{
element.Add(new XElement("limbposition",
@@ -526,6 +520,10 @@ namespace Barotrauma.Items.Components
new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)),
new XAttribute("allowusinglimb", limbPos.AllowUsingLimb)));
}
if (item.FlippedX)
{
FlipLimbPositions();
}
}
return element;
}
@@ -558,5 +556,29 @@ namespace Barotrauma.Items.Components
}
}
}
private void FlipLimbPositions()
{
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X;
Vector2 flippedPos =
new Vector2(
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
limbPositions[i].Position.Y);
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
}
}
public override void Reset()
{
base.Reset();
LoadLimbPositions(originalElement);
if (item.FlippedX)
{
FlipLimbPositions();
}
}
}
}
@@ -150,7 +150,7 @@ namespace Barotrauma.Items.Components
{
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
}
return voltage;
return currPowerConsumption <= 0.0f ? 1.0f : voltage;
}
set
{
@@ -231,6 +231,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description:"Enable only if you want to make the projectile ignore collisions with other projectiles when it's shot. Doesn't have any effect, if the item is not set to be damaged by projectiles.")]
public bool IgnoreProjectilesWhileActive
{
get;
set;
}
public Body StickTarget
{
get;
@@ -405,6 +412,10 @@ namespace Barotrauma.Items.Components
item.body.CollisionCategories = Physics.CollisionProjectile;
item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive)
{
item.body.CollidesWith |= Physics.CollisionProjectile;
}
IsActive = true;
@@ -0,0 +1,10 @@
namespace Barotrauma.Items.Components
{
sealed class AndComponent : BooleanOperatorComponent
{
public AndComponent(Item item, ContentXElement element)
: base(item, element) { }
protected override bool GetOutput(int numTrueInputs) => numTrueInputs >= 2;
}
}
@@ -3,7 +3,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class AndComponent : ItemComponent
abstract class BooleanOperatorComponent : ItemComponent
{
protected string output, falseOutput;
@@ -70,22 +70,25 @@ namespace Barotrauma.Items.Components
}
}
public AndComponent(Item item, ContentXElement element)
public BooleanOperatorComponent(Item item, ContentXElement element)
: base(item, element)
{
timeSinceReceived = new float[] { Math.Max(timeFrame * 2.0f, 0.1f), Math.Max(timeFrame * 2.0f, 0.1f) };
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
protected abstract bool GetOutput(int numTrueInputs);
public sealed override void Update(float deltaTime, Camera cam)
{
bool state = true;
int receivedInputs = 0;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] > timeFrame) { state = false; }
if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; }
timeSinceReceived[i] += deltaTime;
}
bool state = GetOutput(receivedInputs);
string signalOut = state ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut))
{
@@ -0,0 +1,10 @@
namespace Barotrauma.Items.Components
{
sealed class OrComponent : BooleanOperatorComponent
{
public OrComponent(Item item, ContentXElement element)
: base(item, element) { }
protected override bool GetOutput(int numTrueInputs) => numTrueInputs > 0;
}
}
@@ -0,0 +1,10 @@
namespace Barotrauma.Items.Components
{
sealed class XorComponent : BooleanOperatorComponent
{
public XorComponent(Item item, ContentXElement element)
: base(item, element) { }
protected override bool GetOutput(int numTrueInputs) => numTrueInputs == 1;
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -19,11 +20,8 @@ namespace Barotrauma.Items.Components
public readonly string Name;
public readonly LocalizedString DisplayName;
private readonly Wire[] wires;
public IEnumerable<Wire> Wires
{
get { return wires; }
}
private readonly HashSet<Wire> wires;
public IReadOnlyCollection<Wire> Wires => wires;
private readonly Item item;
@@ -31,7 +29,7 @@ namespace Barotrauma.Items.Components
public readonly List<StatusEffect> Effects;
public readonly ushort[] wireId;
public readonly List<ushort> LoadedWireIds;
//The grid the connection is a part of
public GridInfo Grid;
@@ -92,7 +90,7 @@ namespace Barotrauma.Items.Components
MaxWires = Math.Max(element.Elements().Count(e => e.Name.ToString().Equals("link", StringComparison.OrdinalIgnoreCase)), MaxWires);
MaxPlayerConnectableWires = element.GetAttributeInt("maxplayerconnectablewires", MaxWires);
wires = new Wire[MaxWires];
wires = new HashSet<Wire>();
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
@@ -150,23 +148,15 @@ namespace Barotrauma.Items.Components
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
wireId = new ushort[MaxWires];
LoadedWireIds = new List<ushort>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "link":
int index = -1;
for (int i = 0; i < MaxWires; i++)
{
if (wireId[i] < 1) { index = i; }
}
if (index == -1) { break; }
int id = subElement.GetAttributeInt("w", 0);
if (id < 0) { id = 0; }
wireId[index] = idRemap.GetOffsetId(id);
if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); }
break;
case "statuseffect":
@@ -185,138 +175,111 @@ namespace Barotrauma.Items.Components
private void RefreshRecipients()
{
recipients.Clear();
for (int i = 0; i < MaxWires; i++)
foreach (var wire in wires)
{
if (wires[i] == null) continue;
Connection recipient = wires[i].OtherConnection(this);
if (recipient != null) recipients.Add(recipient);
Connection recipient = wire.OtherConnection(this);
if (recipient != null) { recipients.Add(recipient); }
}
recipientsDirty = false;
}
public int FindEmptyIndex()
{
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) return i;
}
return -1;
}
public int FindWireIndex(Wire wire)
{
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == wire) return i;
}
return -1;
}
public int FindWireIndex(Item wireItem)
{
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null && wireItem == null) return i;
if (wires[i] != null && wires[i].Item == wireItem) return i;
}
return -1;
}
public Wire FindWireByItem(Item it)
=> Wires.FirstOrDefault(w => w.Item == it);
public bool WireSlotsAvailable()
=> wires.Count < MaxWires;
public bool TryAddLink(Wire wire)
{
for (int i = 0; i < MaxWires; i++)
if (wire is null
|| wires.Contains(wire)
|| !WireSlotsAvailable())
{
if (wires[i] == null)
{
SetWire(i, wire);
return true;
}
return false;
}
return false;
wires.Add(wire);
return true;
}
public void SetWire(int index, Wire wire)
public void DisconnectWire(Wire wire)
{
Wire previousWire = wires[index];
if (wire != previousWire && previousWire != null)
{
var otherConnection = previousWire.OtherConnection(this);
if (otherConnection != null)
{
//Change the connection grids or flag them for updating
if (IsPower && otherConnection.IsPower && Grid != null)
{
//Check if both connections belong to a larger grid
if (otherConnection.recipients.Count > 1 && recipients.Count > 1)
{
Powered.ChangedConnections.Add(otherConnection);
Powered.ChangedConnections.Add(this);
}
else if (recipients.Count > 1)
{
//This wire was the only one at the other grid
otherConnection.Grid?.RemoveConnection(otherConnection);
otherConnection.Grid = null;
}
else if (otherConnection.recipients.Count > 1)
{
Grid?.RemoveConnection(this);
Grid = null;
}
else if (Grid.Connections.Count == 2)
{
//Delete the grid as these were the only 2 devices
Powered.Grids.Remove(Grid.ID);
Grid = null;
otherConnection.Grid = null;
}
}
otherConnection.recipientsDirty = true;
}
}
if (wire == null || !wires.Contains(wire)) { return; }
wires[index] = wire;
var prevOtherConnection = wire.OtherConnection(this);
if (prevOtherConnection != null)
{
//Change the connection grids or flag them for updating
if (IsPower && prevOtherConnection.IsPower && Grid != null)
{
//Check if both connections belong to a larger grid
if (prevOtherConnection.recipients.Count > 1 && recipients.Count > 1)
{
Powered.ChangedConnections.Add(prevOtherConnection);
Powered.ChangedConnections.Add(this);
}
else if (recipients.Count > 1)
{
//This wire was the only one at the other grid
prevOtherConnection.Grid?.RemoveConnection(prevOtherConnection);
prevOtherConnection.Grid = null;
}
else if (prevOtherConnection.recipients.Count > 1)
{
Grid?.RemoveConnection(this);
Grid = null;
}
else if (Grid.Connections.Count == 2)
{
//Delete the grid as these were the only 2 devices
Powered.Grids.Remove(Grid.ID);
Grid = null;
prevOtherConnection.Grid = null;
}
}
prevOtherConnection.recipientsDirty = true;
}
wires.Remove(wire);
recipientsDirty = true;
if (wire != null)
}
public void ConnectWire(Wire wire)
{
if (wire == null || !TryAddLink(wire)) { return; }
ConnectionPanel.DisconnectedWires.Remove(wire);
var otherConnection = wire.OtherConnection(this);
if (otherConnection != null)
{
ConnectionPanel.DisconnectedWires.Remove(wire);
var otherConnection = wire.OtherConnection(this);
if (otherConnection != null)
//Set the other connection grid if a grid exists already
if (Powered.ValidPowerConnection(this, otherConnection))
{
//Set the other connection grid if a grid exists already
if (Powered.ValidPowerConnection(this, otherConnection))
if (Grid == null && otherConnection.Grid != null)
{
if (Grid == null && otherConnection.Grid != null)
{
otherConnection.Grid.AddConnection(this);
Grid = otherConnection.Grid;
}
else if (Grid != null && otherConnection.Grid == null)
{
Grid.AddConnection(otherConnection);
otherConnection.Grid = Grid;
}
else
{
//Flag change so that proper grids can be formed
Powered.ChangedConnections.Add(this);
Powered.ChangedConnections.Add(otherConnection);
}
otherConnection.Grid.AddConnection(this);
Grid = otherConnection.Grid;
}
else if (Grid != null && otherConnection.Grid == null)
{
Grid.AddConnection(otherConnection);
otherConnection.Grid = Grid;
}
else
{
//Flag change so that proper grids can be formed
Powered.ChangedConnections.Add(this);
Powered.ChangedConnections.Add(otherConnection);
}
otherConnection.recipientsDirty = true;
}
otherConnection.recipientsDirty = true;
}
recipientsDirty = true;
}
public void SendSignal(Signal signal)
{
for (int i = 0; i < MaxWires; i++)
foreach (var wire in wires)
{
if (wires[i] == null) { continue; }
Connection recipient = wires[i].OtherConnection(this);
Connection recipient = wire.OtherConnection(this);
if (recipient == null) { continue; }
if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }
@@ -350,35 +313,32 @@ namespace Barotrauma.Items.Components
}
}
for (int i = 0; i < MaxWires; i++)
foreach (var wire in wires)
{
if (wires[i] == null) continue;
wires[i].RemoveConnection(this);
wires[i] = null;
wire.RemoveConnection(this);
recipientsDirty = true;
}
wires.Clear();
}
public void ConnectLinked()
public void InitializeFromLoaded()
{
if (wireId == null) return;
if (LoadedWireIds.Count == 0) { return; }
for (int i = 0; i < MaxWires; i++)
for (int i = 0; i < LoadedWireIds.Count; i++)
{
if (wireId[i] == 0) { continue; }
if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; }
if (!(Entity.FindEntityByID(wireId[i]) is Item wireItem)) { continue; }
wires[i] = wireItem.GetComponent<Wire>();
recipientsDirty = true;
if (wires[i] != null)
var wire = wireItem.GetComponent<Wire>();
if (wire != null && TryAddLink(wire))
{
if (wires[i].Item.body != null) wires[i].Item.body.Enabled = false;
wires[i].Connect(this, false, false);
wires[i].FixNodeEnds();
if (wire.Item.body != null) wire.Item.body.Enabled = false;
wire.Connect(this, false, false);
wire.FixNodeEnds();
recipientsDirty = true;
}
}
LoadedWireIds.Clear();
}
@@ -386,19 +346,10 @@ namespace Barotrauma.Items.Components
{
XElement newElement = new XElement(IsOutput ? "output" : "input", new XAttribute("name", Name));
Array.Sort(wires, delegate (Wire wire1, Wire wire2)
foreach (var wire in wires.OrderBy(w => w.Item.ID))
{
if (wire1 == null) return 1;
if (wire2 == null) return -1;
return wire1.Item.ID.CompareTo(wire2.Item.ID);
});
for (int i = 0; i < MaxWires; i++)
{
if (wires[i] == null) continue;
newElement.Add(new XElement("link",
new XAttribute("w", wires[i].Item.ID.ToString())));
new XAttribute("w", wire.Item.ID.ToString())));
}
parentElement.Add(newElement);
@@ -49,7 +49,7 @@ namespace Barotrauma.Items.Components
public bool TemporarilyLocked
{
get { return Level.IsLoadedOutpost && item.GetComponent<DockingPort>() != null; }
get { return Level.IsLoadedOutpost && (item.GetComponent<DockingPort>()?.Docked ?? false); }
}
//connection panels can't be deactivated externally (by signals or status effects)
@@ -99,7 +99,7 @@ namespace Barotrauma.Items.Components
{
foreach (Connection c in Connections)
{
c.ConnectLinked();
c.InitializeFromLoaded();
}
if (disconnectedWireIds != null)
@@ -286,25 +286,8 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
{
if (loadedConnections[i].wireId.Length == Connections[i].wireId.Length)
{
loadedConnections[i].wireId.CopyTo(Connections[i].wireId, 0);
}
else
{
//backwards compatibility when maximum number of wires has changed
foreach (ushort id in loadedConnections[i].wireId)
{
for (int j = 0; j < Connections[i].wireId.Length; j++)
{
if (Connections[i].wireId[j] == 0)
{
Connections[i].wireId[j] = id;
break;
}
}
}
}
Connections[i].LoadedWireIds.Clear();
Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds);
}
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty<ushort>()).ToList();
@@ -361,10 +344,8 @@ namespace Barotrauma.Items.Components
DisconnectedWires.Clear();
foreach (Connection c in Connections)
{
foreach (Wire wire in c.Wires)
foreach (Wire wire in c.Wires.ToArray())
{
if (wire == null) { continue; }
if (wire.OtherConnection(c) == null) //wire not connected to anything else
{
#if CLIENT
@@ -408,13 +389,14 @@ namespace Barotrauma.Items.Components
foreach (Connection connection in Connections)
{
msg.WriteVariableUInt32((uint)connection.Wires.Count);
foreach (Wire wire in connection.Wires)
{
msg.Write(wire?.Item == null ? (ushort)0 : wire.Item.ID);
}
}
msg.Write((ushort)DisconnectedWires.Count());
msg.Write((ushort)DisconnectedWires.Count);
foreach (Wire disconnectedWire in DisconnectedWires)
{
msg.Write(disconnectedWire.Item.ID);
@@ -187,7 +187,7 @@ namespace Barotrauma.Items.Components
set;
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
#if CLIENT
Light.Position += amount;
@@ -1,33 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class OrComponent : AndComponent
{
public OrComponent(Item item, ContentXElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
bool state = false;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) { state = true; }
timeSinceReceived[i] += deltaTime;
}
string signalOut = state ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut))
{
//deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal)
if (!state) { IsActive = false; }
return;
}
item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
}
}
@@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components
// = no point in receiving
if (!LinkToChat)
{
if (signalOutConnection == null || !signalOutConnection.Wires.Any(w => w != null))
if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0)
{
return false;
}
@@ -143,12 +143,11 @@ namespace Barotrauma.Items.Components
{
if (connections[i] == null || connections[i].Item != item) { continue; }
foreach (Wire wire in connections[i].Wires)
if (connections[i].Wires.Contains(this))
{
if (wire != this) continue;
SetConnectedDirty();
connections[i].SetWire(connections[i].FindWireIndex(wire), null);
connections[i].DisconnectWire(this);
}
connections[i] = null;
@@ -597,15 +596,16 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < 2; i++)
{
if (connections[i] == null) { continue; }
int wireIndex = connections[i].FindWireIndex(item);
if (wireIndex == -1) { continue; }
var wire = connections[i].FindWireByItem(item);
if (wire is null) { continue; }
#if SERVER
if (!connections[i].Item.Removed && (!connections[i].Item.Submarine?.Loading ?? true) && (!Level.Loaded?.Generating ?? true))
{
connections[i].Item.CreateServerEvent(connections[i].Item.GetComponent<ConnectionPanel>());
}
#endif
connections[i].SetWire(wireIndex, null);
connections[i].DisconnectWire(wire);
connections[i] = null;
}
@@ -1,34 +0,0 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
class XorComponent : AndComponent
{
public XorComponent(Item item, ContentXElement element)
: base(item, element)
{
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
int receivedInputs = 0;
for (int i = 0; i < timeSinceReceived.Length; i++)
{
if (timeSinceReceived[i] <= timeFrame) { receivedInputs += 1; }
timeSinceReceived[i] += deltaTime;
}
bool state = receivedInputs == 1;
string signalOut = state ? output : falseOutput;
if (string.IsNullOrEmpty(signalOut))
{
//deactivate the component if state is false and there's no false output (will be woken up by non-zero signals in ReceiveSignal)
if (!state) { IsActive = false; }
return;
}
item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out");
}
}
}
@@ -3,9 +3,8 @@ using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Items.Components
{
@@ -93,13 +92,11 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f)
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
BodyType = BodyType.Static,
CollidesWith = LevelTrigger.GetCollisionCategories(triggeredBy),
CollisionCategories = Physics.CollisionWall,
UserData = item
};
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody.FarseerBody.OnCollision += OnCollision;
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
@@ -215,12 +212,18 @@ namespace Barotrauma.Items.Components
body.ApplyForce(force);
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
base.Move(amount);
if (PhysicsBody != null)
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
if (ignoreContacts)
{
PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
PhysicsBody.Submarine = item.Submarine;
}
}
@@ -661,6 +661,7 @@ namespace Barotrauma.Items.Components
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
if (!batteries.Any()) { break; }
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
@@ -1151,8 +1152,12 @@ namespace Barotrauma.Items.Components
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
if (enemy.IsDead || !enemy.Enabled) { continue; }
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
@@ -115,7 +115,7 @@ namespace Barotrauma
private readonly Quality qualityComponent;
private readonly ConcurrentQueue<float> impactQueue = new ConcurrentQueue<float>();
private ConcurrentQueue<float> impactQueue;
//a dictionary containing lists of the status effects in all the components of the item
private readonly bool[] hasStatusEffectsOfType;
@@ -835,33 +835,35 @@ namespace Barotrauma
var rand = new Random(ID);
density = MathHelper.Lerp(minDensity, maxDensity, (float)rand.NextDouble());
}
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density);
string collisionCategory = subElement.GetAttributeString("collisioncategory", null);
string collisionCategoryStr = subElement.GetAttributeString("collisioncategory", null);
Category collisionCategory = Physics.CollisionItem;
Category collidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform;
if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0)
{
//force collision category to Character to allow projectiles and weapons to hit
//(we could also do this by making the projectiles and weapons hit CollisionItem
//and check if the collision should be ignored in the OnCollision callback, but
//that'd make the hit detection more expensive because every item would be included)
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
collisionCategory = Physics.CollisionCharacter;
}
if (collisionCategory != null)
if (collisionCategoryStr != null)
{
if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat))
if (!Physics.TryParseCollisionCategory(collisionCategoryStr, out Category cat))
{
DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")");
DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategoryStr + ")");
}
else
{
body.CollisionCategories = cat;
collisionCategory = cat;
if (cat.HasFlag(Physics.CollisionCharacter))
{
body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
collisionCategory |= Physics.CollisionProjectile;
}
}
}
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false);
body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f);
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
body.UserData = this;
@@ -1261,12 +1263,7 @@ namespace Barotrauma
partial void SetActiveSpriteProjSpecific();
public override void Move(Vector2 amount)
{
Move(amount, ignoreContacts: false);
}
public void Move(Vector2 amount, bool ignoreContacts)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -1289,7 +1286,7 @@ namespace Barotrauma
}
foreach (ItemComponent ic in components)
{
ic.Move(amount);
ic.Move(amount, ignoreContacts);
}
if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); }
@@ -1703,9 +1700,12 @@ namespace Barotrauma
public override void Update(float deltaTime, Camera cam)
{
while (impactQueue.TryDequeue(out float impact))
if (impactQueue != null)
{
HandleCollision(impact);
while (impactQueue.TryDequeue(out float impact))
{
HandleCollision(impact);
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true))
@@ -1959,6 +1959,7 @@ namespace Barotrauma
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
return true;
@@ -2680,21 +2681,21 @@ namespace Barotrauma
foreach (ItemComponent ic in components) { ic.Unequip(character); }
}
public List<Pair<object, SerializableProperty>> GetProperties<T>()
public List<(object obj, SerializableProperty property)> GetProperties<T>()
{
List<Pair<object, SerializableProperty>> allProperties = new List<Pair<object, SerializableProperty>>();
List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>();
List<SerializableProperty> itemProperties = SerializableProperty.GetProperties<T>(this);
foreach (var itemProperty in itemProperties)
{
allProperties.Add(new Pair<object, SerializableProperty>(this, itemProperty));
allProperties.Add((this, itemProperty));
}
foreach (ItemComponent ic in components)
{
List<SerializableProperty> componentProperties = SerializableProperty.GetProperties<T>(ic);
foreach (var componentProperty in componentProperties)
{
allProperties.Add(new Pair<object, SerializableProperty>(ic, componentProperty));
allProperties.Add((ic, componentProperty));
}
}
return allProperties;
@@ -2708,13 +2709,13 @@ namespace Barotrauma
SerializableProperty property = extraData.SerializableProperty;
if (property != null)
{
var propertyOwner = allProperties.Find(p => p.Second == property);
var propertyOwner = allProperties.Find(p => p.property == property);
if (allProperties.Count > 1)
{
msg.Write((byte)allProperties.FindIndex(p => p.Second == property));
msg.Write((byte)allProperties.FindIndex(p => p.property == property));
}
object value = property.GetValue(propertyOwner.First);
object value = property.GetValue(propertyOwner.obj);
if (value is string stringVal)
{
msg.Write(stringVal);
@@ -2795,7 +2796,7 @@ namespace Barotrauma
}
}
private List<Pair<object, SerializableProperty>> GetInGameEditableProperties(bool ignoreConditions = false)
private List<(object obj, SerializableProperty property)> GetInGameEditableProperties(bool ignoreConditions = false)
{
if (ignoreConditions)
{
@@ -2804,7 +2805,7 @@ namespace Barotrauma
else
{
return GetProperties<ConditionallyEditable>()
.Where(ce => ce.Second.GetAttribute<ConditionallyEditable>().IsEditable(this))
.Where(ce => ce.property.GetAttribute<ConditionallyEditable>().IsEditable(this))
.Union(GetProperties<InGameEditable>()).ToList();
}
}
@@ -2823,8 +2824,8 @@ namespace Barotrauma
}
bool allowEditing = true;
object parentObject = allProperties[propertyIndex].First;
SerializableProperty property = allProperties[propertyIndex].Second;
object parentObject = allProperties[propertyIndex].obj;
SerializableProperty property = allProperties[propertyIndex].property;
if (inGameEditableOnly && parentObject is ItemComponent ic)
{
if (!ic.AllowInGameEditing) { allowEditing = false; }
@@ -253,6 +253,12 @@ namespace Barotrauma
public readonly float MinCondition;
public readonly int MinAmount;
public readonly int MaxAmount;
// Overrides min and max, if defined.
public readonly int Amount;
public readonly bool CampaignOnly;
public readonly bool NotCampaign;
public readonly bool TransferOnlyOnePerContainer;
public readonly bool AllowTransfersHere = true;
public PreferredContainer(XElement element)
{
@@ -261,21 +267,26 @@ namespace Barotrauma
SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f);
MinAmount = element.GetAttributeInt("minamount", 0);
MaxAmount = Math.Max(MinAmount, element.GetAttributeInt("maxamount", 0));
Amount = element.GetAttributeInt("amount", 0);
MaxCondition = element.GetAttributeFloat("maxcondition", 100f);
MinCondition = element.GetAttributeFloat("mincondition", 0f);
CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly);
NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign);
TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer);
AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere);
if (element.Attribute("spawnprobability") == null)
if (element.GetAttribute("spawnprobability") == null)
{
//if spawn probability is not defined but amount is, assume the probability is 1
if (MaxAmount > 0)
if (MaxAmount > 0 || Amount > 0)
{
SpawnProbability = 1.0f;
}
}
else if (element.Attribute("minamount") == null && element.Attribute("maxamount") == null)
else if (element.GetAttribute("minamount") == null && element.GetAttribute("maxamount") == null && element.GetAttribute("amount") == null)
{
//spawn probability defined but amount isn't, assume amount is 1
MinAmount = MaxAmount = 1;
MinAmount = MaxAmount = Amount = 1;
SpawnProbability = element.GetAttributeFloat("spawnprobability", 0.0f);
}
}
@@ -600,6 +611,9 @@ namespace Barotrauma
public ImmutableHashSet<Identifier> AllowDroppingOnSwapWith { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool DontTransferBetweenSubs { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -1084,7 +1098,7 @@ namespace Barotrauma
//legacy support
identifier = GenerateLegacyIdentifier(name);
}
prefab = Find(p => p is ItemPrefab && p.Identifier == identifier) as ItemPrefab;
Prefabs.TryGet(identifier, out prefab);
//not found, see if we can find a prefab with a matching alias
if (prefab == null && !string.IsNullOrEmpty(name))
@@ -1104,12 +1118,13 @@ namespace Barotrauma
return prefab;
}
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false)
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary, bool requireConditionRequirement = false, bool checkTransferConditions = false)
{
isPreferencesDefined = PreferredContainers.Any();
isSecondary = false;
if (!isPreferencesDefined) { return true; }
if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Primary, targetContainer)))
if (PreferredContainers.Any(pc => (!requireConditionRequirement || HasConditionRequirement(pc)) && IsItemConditionAcceptable(item, pc) &&
IsContainerPreferred(pc.Primary, targetContainer) && (!checkTransferConditions || CanBeTransferred(item.Prefab.Identifier, pc, targetContainer))))
{
return true;
}
@@ -1132,6 +1147,8 @@ namespace Barotrauma
}
private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition;
private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) =>
pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item));
public static bool IsContainerPreferred(IEnumerable<Identifier> preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id));
public static bool IsContainerPreferred(IEnumerable<Identifier> preferences, IEnumerable<Identifier> ids) => ids.Any(id => preferences.Contains(id));
@@ -214,7 +214,7 @@ namespace Barotrauma.MapCreatures.Behavior
[Serialize(400, IsPropertySaveable.Yes, "How much health the root has.")]
public int RootHealth { get; set; }
[Serialize(0.0005f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
[Serialize(0.00025f, IsPropertySaveable.Yes, "How fast the root's health regenerates per each grown branch.")]
public float HealthRegenPerBranch { get; set; }
[Serialize(30, IsPropertySaveable.Yes, "How far away from the root branches can regenerate health (in number of branches). The amount of regen decreases lineary further from the root.")]
@@ -1148,7 +1148,7 @@ namespace Barotrauma.MapCreatures.Behavior
return;
}
#if SERVER
if (!wasRemoved)
if (!wasRemoved && Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new BranchRemoveEventData(branch));
}
@@ -1199,7 +1199,10 @@ namespace Barotrauma.MapCreatures.Behavior
StateMachine?.State?.Exit();
#if SERVER
CreateNetworkMessage(new KillEventData());
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new KillEventData());
}
#endif
}
@@ -1220,8 +1223,11 @@ namespace Barotrauma.MapCreatures.Behavior
}
_entityList.Remove(this);
#if SERVER
CreateNetworkMessage(new RemoveEventData());
#if SERVER
if (Parent != null && !Parent.Removed)
{
CreateNetworkMessage(new RemoveEventData());
}
#endif
}
@@ -148,11 +148,12 @@ namespace Barotrauma
InsertToList();
float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2;
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize);
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize,
BodyType.Static,
Physics.CollisionWall,
Physics.CollisionCharacter,
findNewContacts: false);
outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})";
outsideCollisionBlocker.BodyType = BodyType.Static;
outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall;
outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter;
outsideCollisionBlocker.Enabled = false;
#if CLIENT
Resized += newRect => IsHorizontal = newRect.Width < newRect.Height;
@@ -165,7 +166,7 @@ namespace Barotrauma
return new Gap(rect, IsHorizontal, Submarine);
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -326,14 +327,6 @@ namespace Barotrauma
{
lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f);
}
if (FlowTargetHull != null && IsRoomToRoom)
{
var otherRoom = linkedTo[1] == FlowTargetHull ? linkedTo[0] : linkedTo[1];
if ((otherRoom as Hull).Volume < FlowTargetHull.Volume)
{
lerpedFlowForce = Vector2.Zero;
}
}
openedTimer -= deltaTime;
@@ -590,7 +590,7 @@ namespace Barotrauma
return index;
}
public override void Move(Vector2 amount)
public override void Move(Vector2 amount, bool ignoreContacts = false)
{
if (!MathUtils.IsValid(amount))
{
@@ -157,9 +157,6 @@ namespace Barotrauma
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
if (targetCells.Count == 0) { return pathCells; }
@@ -213,10 +210,6 @@ namespace Barotrauma
} while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
Debug.WriteLine("gettooclose: " + sw2.ElapsedMilliseconds + " ms");
sw2.Restart();
return pathCells;
}
@@ -351,7 +344,7 @@ namespace Barotrauma
BodyType = BodyType.Static,
CollisionCategories = Physics.CollisionLevel
};
GameMain.World.Add(cellBody);
GameMain.World.Add(cellBody, findNewContacts: false);
for (int n = cells.Count - 1; n >= 0; n-- )
{
@@ -429,7 +422,9 @@ namespace Barotrauma
Vertices bodyVertices = new Vertices(triangles[i]);
PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f);
Fixture fixture = new Fixture(polygon)
Fixture fixture = new Fixture(polygon,
Physics.CollisionLevel,
Physics.CollisionAll)
{
UserData = cell
};
@@ -446,8 +441,6 @@ namespace Barotrauma
}
cell.Body = cellBody;
}
cellBody.CollisionCategories = Physics.CollisionLevel;
cellBody.ResetMassData();
return cellBody;
@@ -299,11 +299,41 @@ namespace Barotrauma
/// Random integers generated during the level generation. If these values differ between clients/server,
/// it means the levels aren't identical for some reason and there will most likely be major ID mismatches.
/// </summary>
public List<int> EqualityCheckValues
public enum LevelGenStage
{
get;
private set;
} = new List<int>();
GenStart,
TunnelGen,
VoronoiGen,
VoronoiGen2,
VoronoiGen3,
Ruins,
FloatingIce,
LevelBodies,
IceSpires,
TopAndBottom,
PlaceLevelObjects,
GenerateItems,
Finish
}
private readonly Dictionary<LevelGenStage, int> equalityCheckValues = Enum.GetValues(typeof(LevelGenStage))
.Cast<LevelGenStage>()
.Select(k => (k, 0))
.ToDictionary();
public IReadOnlyDictionary<LevelGenStage, int> EqualityCheckValues => equalityCheckValues;
private void GenerateEqualityCheckValue(LevelGenStage stage)
{
equalityCheckValues[stage] = Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient);
}
private void ClearEqualityCheckValues()
{
foreach (LevelGenStage stage in Enum.GetValues(typeof(LevelGenStage)))
{
equalityCheckValues[stage] = 0;
}
}
public List<Entity> EntitiesBeforeGenerate { get; private set; } = new List<Entity>();
public int EntityCountBeforeGenerate { get; private set; }
@@ -404,7 +434,7 @@ namespace Barotrauma
Loaded = this;
Generating = true;
EqualityCheckValues.Clear();
ClearEqualityCheckValues();
EntitiesBeforeGenerate = GetEntities().ToList();
EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count();
@@ -414,7 +444,7 @@ namespace Barotrauma
EndLocation = GameMain.GameSession?.EndLocation;
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.GenStart);
LevelObjectManager = new LevelObjectManager();
@@ -477,7 +507,7 @@ namespace Barotrauma
(int)MathHelper.Lerp(borders.Bottom - Math.Max(minMainPathWidth, ExitDistance * 1.5f), borders.Y + minMainPathWidth, GenerationParams.EndPosition.Y));
endExitPosition = new Point(endPosition.X, borders.Bottom);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.TunnelGen);
//----------------------------------------------------------------------------------
//generate the initial nodes for the main path and smaller tunnels
@@ -573,7 +603,7 @@ namespace Barotrauma
GenerateAbyssArea();
GenerateCaves(mainPath);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen);
//----------------------------------------------------------------------------------
//generate voronoi sites
@@ -678,7 +708,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen2);
//----------------------------------------------------------------------------------
// construct the voronoi graph and cells
@@ -796,7 +826,7 @@ namespace Barotrauma
startPosition.X = (int)pathCells[0].Site.Coord.X;
startExitPosition.X = startPosition.X;
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.VoronoiGen3);
//----------------------------------------------------------------------------------
// remove unnecessary cells and create some holes at the bottom of the level
@@ -1025,7 +1055,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.Ruins);
//----------------------------------------------------------------------------------
// create some ruins
@@ -1038,7 +1068,7 @@ namespace Barotrauma
GenerateRuin(ruinPositions[i], mirror);
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.FloatingIce);
//----------------------------------------------------------------------------------
// create floating ice chunks
@@ -1070,7 +1100,7 @@ namespace Barotrauma
}
}
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.LevelBodies);
//----------------------------------------------------------------------------------
// generate the bodies and rendered triangles of the cells
@@ -1175,7 +1205,7 @@ namespace Barotrauma
}
#endif
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.IceSpires);
//----------------------------------------------------------------------------------
// create ice spires
@@ -1210,7 +1240,7 @@ namespace Barotrauma
CreateOutposts();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.TopAndBottom);
//----------------------------------------------------------------------------------
// top barrier & sea floor
@@ -1252,15 +1282,15 @@ namespace Barotrauma
CreateWrecks();
CreateBeaconStation();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.PlaceLevelObjects);
LevelObjectManager.PlaceObjects(this, GenerationParams.LevelObjectAmount);
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.GenerateItems);
GenerateItems();
EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.ServerAndClient));
GenerateEqualityCheckValue(LevelGenStage.Finish);
#if CLIENT
backgroundCreatureManager.SpawnCreatures(this, GenerationParams.BackgroundCreatureAmount);
@@ -2606,7 +2636,7 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage("Level resources spawned: " + itemCount + "\n" +
" Spawn points containing resources: " + PathPoints.Where(p => p.ClusterLocations.Any()).Count() + "/" + PathPoints.Count + "\n" +
" Total value: "+ PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))+" mk");
" Total value: " + PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))) + " mk");
if (AbyssResources.Count > 0)
{
@@ -3688,6 +3718,7 @@ namespace Barotrauma
if (wreckFiles.None())
{
DebugConsole.ThrowError("No wreck files found in the selected content packages!");
Wrecks = new List<Submarine>();
return;
}
wreckFiles.Shuffle(Rand.RandSync.ServerAndClient);
@@ -4112,12 +4143,12 @@ namespace Barotrauma
int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
pathPoints.Shuffle(Rand.RandSync.Unsynced);
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
corpsePoints.Shuffle(Rand.RandSync.Unsynced);
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
pathPoints.Shuffle(Rand.RandSync.Unsynced);
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
var usedJobs = new HashSet<JobPrefab>();
int spawnCounter = 0;
for (int j = 0; j < corpseCount; j++)
{
@@ -4126,18 +4157,18 @@ namespace Barotrauma
CorpsePrefab selectedPrefab;
if (job == null)
{
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck);
selectedPrefab = GetCorpsePrefab(usedJobs);
}
else
{
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier));
selectedPrefab = GetCorpsePrefab(usedJobs, p => p.Job == "any" || p.Job == job.Identifier);
if (selectedPrefab == null)
{
corpsePoints.Remove(sp);
pathPoints.Remove(sp);
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
// Deduce the job from the selected prefab
selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck);
selectedPrefab = GetCorpsePrefab(usedJobs);
}
}
if (selectedPrefab == null) { continue; }
@@ -4156,28 +4187,65 @@ namespace Barotrauma
pathPoints.Remove(sp);
}
job ??= selectedPrefab.GetJobPrefab();
job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
if (job == null) { continue; }
if (job.Identifier == "captain" || job.Identifier == "engineer" || job.Identifier == "medicaldoctor" || job.Identifier == "securityofficer")
{
// Only spawn one of these jobs per wreck
usedJobs.Add(job);
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, randSync: Rand.RandSync.ServerAndClient);
var corpse = Character.Create(CharacterPrefab.HumanSpeciesName, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
corpse.AnimController.FindHull(worldPos, setSubmarine: true);
corpse.TeamID = CharacterTeamType.None;
corpse.EnableDespawn = false;
selectedPrefab.GiveItems(corpse, wreck);
corpse.CharacterHealth.ApplyAffliction(corpse.AnimController.MainLimb, AfflictionPrefab.OxygenLow.Instantiate(200));
bool applyBurns = Rand.Value() < 0.1f;
bool applyDamage = Rand.Value() < 0.3f;
foreach (var limb in corpse.AnimController.Limbs)
{
if (applyDamage && (limb.type == LimbType.Head || Rand.Value() < 0.5f))
{
var prefab = AfflictionPrefab.BiteWounds;
float max = prefab.MaxStrength / prefab.DamageOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
if (applyBurns)
{
var prefab = AfflictionPrefab.Burn;
float max = prefab.MaxStrength / prefab.BurnOverlayAlpha;
corpse.CharacterHealth.ApplyAffliction(limb, prefab.Instantiate(GetStrength(limb, max)));
}
static float GetStrength(Limb limb, float max)
{
float strength = Rand.Range(0, max);
if (limb.type != LimbType.Head)
{
strength = Math.Min(strength, Rand.Range(0, max));
}
return strength;
}
}
corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
corpse.GiveIdCardTags(sp);
#if SERVER
if (selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isServerOrSingleplayer && selectedPrefab.MinMoney >= 0 && selectedPrefab.MaxMoney > 0)
{
corpse.Wallet.Give(Rand.Range(selectedPrefab.MinMoney, selectedPrefab.MaxMoney, Rand.RandSync.Unsynced));
}
#endif
spawnCounter++;
static CorpsePrefab GetCorpsePrefab(Func<CorpsePrefab, bool> predicate)
static CorpsePrefab GetCorpsePrefab(HashSet<JobPrefab> usedJobs, Func<CorpsePrefab, bool> predicate = null)
{
IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate);
IEnumerable<CorpsePrefab> filteredPrefabs = CorpsePrefab.Prefabs.Where(p =>
usedJobs.None(j => j.Identifier == p.Job.ToIdentifier()) &&
p.SpawnPosition == PositionType.Wreck &&
(predicate == null || predicate(p)));
return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced);
}
}
@@ -4270,7 +4338,7 @@ namespace Barotrauma
blockedRects?.Clear();
EntitiesBeforeGenerate?.Clear();
EqualityCheckValues?.Clear();
ClearEqualityCheckValues();
if (Ruins != null)
{
@@ -20,7 +20,7 @@ namespace Barotrauma
public readonly string Seed;
public float Difficulty;
public readonly float Difficulty;
public readonly Biome Biome;
@@ -141,8 +141,8 @@ namespace Barotrauma
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
Biome = locationConnection.Biome;
Type = LevelType.LocationConnection;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Biome.Identifier);
Difficulty = locationConnection.Difficulty;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.LocationConnection, Difficulty, Biome.Identifier);
float sizeFactor = MathUtils.InverseLerp(
MapGenerationParams.Instance.SmallLevelConnectionLength,
@@ -171,13 +171,13 @@ namespace Barotrauma
/// <summary>
/// Instantiates level data using the properties of the location
/// </summary>
public LevelData(Location location)
public LevelData(Location location, float difficulty)
{
Seed = location.BaseName;
Biome = location.Biome;
Type = LevelType.Outpost;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Biome.Identifier);
Difficulty = 0.0f;
Difficulty = difficulty;
GenerationParams = LevelGenerationParams.GetRandom(Seed, LevelType.Outpost, Difficulty, Biome.Identifier);
var rand = new MTRandom(ToolBox.StringToInt(Seed));
int width = (int)MathHelper.Lerp(GenerationParams.MinWidth, GenerationParams.MaxWidth, (float)rand.NextDouble());
@@ -200,14 +200,16 @@ namespace Barotrauma
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
generationParams.Type;
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
float selectedDifficulty = difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient);
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type, selectedDifficulty); }
var biome =
Biome.Prefabs.FirstOrDefault(b => generationParams?.AllowedBiomeIdentifiers.Contains(b.Identifier) ?? false) ??
Biome.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
var levelData = new LevelData(
seed,
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.ServerAndClient),
selectedDifficulty,
Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient),
generationParams,
biome);

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