Build 0.18.4.0

This commit is contained in:
Markus Isberg
2022-05-31 23:13:05 +09:00
parent 077917fa5d
commit 64db1a6a44
175 changed files with 4916 additions and 2393 deletions
@@ -34,14 +34,18 @@ namespace Barotrauma
public float SoundRange
{
get { return soundRange; }
set
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
if (soundRange > 0.0f && !Static && FadeOutTime > 0.0f)
{
NeedsUpdate = true;
}
}
}
@@ -55,7 +59,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
if (sightRange > 0 && !Static && FadeOutTime > 0.0f)
{
NeedsUpdate = true;
}
}
}
@@ -99,13 +107,33 @@ namespace Barotrauma
/// </summary>
public bool InDetectable
{
get => inDetectable || (SoundRange <= 0 && SightRange <= 0);
set => inDetectable = value;
get
{
return inDetectable || (SoundRange <= 0 && SightRange <= 0);
}
set
{
inDetectable = value;
if (inDetectable)
{
NeedsUpdate = true;
}
}
}
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
/// <summary>
/// Does the AI target do something that requires Update() to be called (e.g. static targets don't need to be updated)
/// </summary>
public bool NeedsUpdate
{
get;
private set;
} = true;
public TargetType Type { get; private set; }
public enum TargetType
@@ -190,14 +218,22 @@ namespace Barotrauma
if (!Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight && SightRange > 0)
if (!StaticSight && sightRange > 0)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound && SoundRange > 0)
if (!StaticSound && soundRange > 0)
{
DecreaseSoundRange(deltaTime);
}
if (sightRange <= 0 && soundRange <= 0)
{
NeedsUpdate = false;
}
}
else
{
NeedsUpdate = false;
}
}
@@ -1440,14 +1440,33 @@ namespace Barotrauma
}
else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5)
{
reachTimer += deltaTime;
if (reachTimer > reachTimeOut)
Vector2 targetVelocity = Vector2.Zero;
Submarine targetSub = SelectedAiTarget.Entity.Submarine;
if (targetSub != null)
{
reachTimer = 0;
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
return;
targetVelocity = targetSub.Velocity;
}
else if (targetCharacter != null)
{
targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
}
else if (SelectedAiTarget.Entity is Item i && i.body != null)
{
targetVelocity = i.body.LinearVelocity;
}
float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared();
float targetSpeed = targetVelocity.LengthSquared();
if (mySpeed < 0.1f || mySpeed > targetSpeed)
{
reachTimer += deltaTime;
if (reachTimer > reachTimeOut)
{
reachTimer = 0;
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
return;
}
}
}
@@ -866,8 +866,8 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Deconstructor>() != null) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -882,7 +882,12 @@ namespace Barotrauma
}
else
{
return isPreferencesDefined ? 0 : 1;
if (isPreferencesDefined)
{
// Use any valid locker as a fall back container.
return container.Item.HasTag("locker") ? 0.5f : 0;
}
return 1;
}
}
}
@@ -1950,11 +1955,10 @@ namespace Barotrauma
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
}
float dangerousItemsFactor = 1f;
foreach (Item item in Item.ItemList)
foreach (Item item in Item.DangerousItems)
{
if (item.CurrentHull != hull) { continue; }
if (item.Prefab != null && item.Prefab.IsDangerous)
{
if (item.CurrentHull == hull)
{
dangerousItemsFactor = 0;
break;
}
@@ -245,7 +245,7 @@ namespace Barotrauma
{
get
{
if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
@@ -48,16 +48,29 @@ namespace Barotrauma
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
}
return Priority;
}
@@ -202,7 +215,7 @@ namespace Barotrauma
// This is an approximation, because we don't know the exact reach until the pose is taken.
// And even then the actual range depends on the direction we are aiming to.
// Found out that without any multiplier the value (209) is often too short.
return repairTool.Range + armLength * 1.3f;
return repairTool.Range + armLength * 2;
}
}
}
@@ -185,6 +185,11 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
}
}
else
{
@@ -290,12 +295,25 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
}
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
if (character.IsClimbing)
{
if (character.AnimController.GetHeightFromFloor() < 0.1f)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
return;
}
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
{
@@ -142,7 +142,7 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
@@ -238,7 +238,7 @@ namespace Barotrauma
};
if (repairTool != null)
{
objective.CloseEnough = repairTool.Range * 0.75f;
objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character);
}
return objective;
},
@@ -1,8 +1,6 @@
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using FarseerPhysics;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -90,6 +88,10 @@ namespace Barotrauma
{
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null)
{
steering = -steering;
}
host.Steering = steering;
}
@@ -22,8 +22,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
#warning TODO: this is kinda janky, this should probably be done better
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName));
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
if (!character.VariantOf.IsEmpty)
{
_ragdollParams.ApplyVariantScale(character.Params.VariantFile);
@@ -74,7 +74,7 @@ namespace Barotrauma
}
}
public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count;
public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count;
private bool frozen;
public bool Frozen
@@ -1850,36 +1850,30 @@ namespace Barotrauma
}
/// <summary>
/// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary.
/// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
/// </summary>
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
{
Limb limb = null;
if (HasMultipleLimbsOfSameType)
if (limbDictionary.TryGetValue(limbType, out Limb limb))
{
for (int i = 0; i < 10; i++)
if (excludeSevered && limb.IsSevered)
{
limbDictionary.TryGetValue(limbType, out limb);
if (limb == null)
limb = null;
}
}
if (limb == null && HasMultipleLimbsOfSameType)
{
// Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
foreach (var l in limbs)
{
if (l.type != limbType) { continue; }
if (!excludeSevered || !l.IsSevered)
{
// No limbs found
break;
}
if (!excludeSevered || !limb.IsSevered)
{
// Found a valid limb
limb = l;
break;
}
}
}
else
{
limbDictionary.TryGetValue(limbType, out limb);
}
if (excludeSevered && limb != null && limb.IsSevered)
{
limb = null;
}
return limb;
}
@@ -117,7 +117,29 @@ namespace Barotrauma
protected Key[] keys;
public HumanPrefab HumanPrefab;
private HumanPrefab humanPrefab;
public HumanPrefab HumanPrefab
{
get { return humanPrefab; }
set
{
if (humanPrefab == value) { return; }
humanPrefab = value;
if (humanPrefab != null)
{
HumanPrefabHealthMultiplier = humanPrefab.HealthMultiplier;
if (GameMain.NetworkMember != null)
{
HumanPrefabHealthMultiplier *= humanPrefab.HealthMultiplierInMultiplayer;
}
}
else
{
HumanPrefabHealthMultiplier = 1.0f;
}
}
}
private CharacterTeamType teamID;
public CharacterTeamType TeamID
@@ -1192,7 +1214,7 @@ namespace Barotrauma
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
}
if (Params.Husk && speciesName != "husk")
if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
{
// Get the non husked name and find the ragdoll with it
var matchingAffliction = AfflictionPrefab.List
@@ -1392,7 +1414,7 @@ namespace Barotrauma
if (inputType == InputType.Up || inputType == InputType.Down ||
inputType == InputType.Left || inputType == InputType.Right)
{
var invertControls = CharacterHealth.GetAffliction("invertcontrols");
var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier());
if (invertControls != null)
{
switch (inputType)
@@ -1652,14 +1674,9 @@ namespace Barotrauma
}
/// <summary>
/// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier
/// Health multiplier of the human prefab this character is an instance of (if any)
/// </summary>
public float StaticHealthMultiplier { get; private set; } = 1;
public void AddStaticHealthMultiplier(float newMultiplier)
{
StaticHealthMultiplier *= newMultiplier;
}
public float HumanPrefabHealthMultiplier { get; private set; } = 1;
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
@@ -4824,21 +4841,21 @@ namespace Barotrauma
}
}
private readonly List<AbilityFlags> abilityFlags = new List<AbilityFlags>();
private AbilityFlags abilityFlags;
public void AddAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Add(abilityFlag);
abilityFlags |= abilityFlag;
}
public void RemoveAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Remove(abilityFlag);
abilityFlags &= ~abilityFlag;
}
public bool HasAbilityFlag(AbilityFlags abilityFlag)
{
return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<Identifier, float> abilityResistances = new Dictionary<Identifier, float>();
@@ -35,6 +35,7 @@ namespace Barotrauma
if (newValue > _strength)
{
PendingAdditionStrength = Prefab.GrainBurst;
Duration = Prefab.Duration;
}
_strength = newValue;
}
@@ -60,6 +61,8 @@ namespace Barotrauma
public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime;
public float Duration;
/// <summary>
/// Which character gave this affliction
/// </summary>
@@ -75,6 +78,8 @@ namespace Barotrauma
_strength = strength;
Identifier = prefab.Identifier;
Duration = prefab.Duration;
foreach (var periodicEffect in prefab.PeriodicEffects)
{
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
@@ -315,8 +320,7 @@ namespace Barotrauma
public bool HasFlag(AbilityFlags flagType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; }
return currentEffect.AfflictionAbilityFlags.Contains(flagType);
return currentEffect.AfflictionAbilityFlags.HasFlag(flagType);
}
private AfflictionPrefab.Effect GetViableEffect()
@@ -235,7 +235,7 @@ namespace Barotrauma
public Identifier[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
public AbilityFlags AfflictionAbilityFlags;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -265,7 +265,7 @@ namespace Barotrauma
break;
case "abilityflag":
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AfflictionAbilityFlags.Add(flagType);
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.");
@@ -354,6 +354,11 @@ namespace Barotrauma
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// </summary>
public readonly float Duration;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
@@ -407,8 +412,10 @@ namespace Barotrauma
!IsBuff &&
AfflictionType != "geneticmaterialbuff" &&
AfflictionType != "geneticmaterialdebuff");
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0);
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
{
@@ -1,12 +1,11 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
using System.Globalization;
using Barotrauma.Abilities;
namespace Barotrauma
{
@@ -148,7 +147,7 @@ namespace Barotrauma
{
max += Character.Info.Job.Prefab.VitalityModifier;
}
max *= Character.StaticHealthMultiplier;
max *= Character.HumanPrefabHealthMultiplier;
max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
return max * Character.HealthMultiplier;
}
@@ -700,6 +699,7 @@ namespace Barotrauma
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
existingAffliction.Strength = newStrength;
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
@@ -759,6 +759,15 @@ namespace Barotrauma
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
if (affliction.Prefab.Duration > 0.0f)
{
affliction.Duration -= deltaTime;
if (affliction.Duration <= 0.0f)
{
afflictionsToRemove.Add(affliction);
continue;
}
}
afflictionsToUpdate.Add(kvp);
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
@@ -112,12 +112,6 @@ namespace Barotrauma
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
{
npc.AddStaticHealthMultiplier(HealthMultiplier);
if (GameMain.NetworkMember != null)
{
npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer);
}
var humanAI = npc.AIController as HumanAIController;
if (humanAI != null)
{
@@ -138,61 +138,53 @@ namespace Barotrauma
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
{
return (T)ragdoll;
}
string selectedFile = null;
void tryFolderForSpecies(Identifier species, out string err)
Identifier ragdollSpecies = speciesName;
if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab))
{
err = null;
string folder = GetFolder(species);
if (!prefab.VariantOf.IsEmpty)
{
ragdollSpecies = prefab.VariantOf;
}
string error = null;
string folder = GetFolder(ragdollSpecies);
if (!Directory.Exists(folder))
{
err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
return;
}
string[] files = Directory.GetFiles(folder);
if (files.None())
{
err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(species);
error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
string[] files = Directory.GetFiles(folder);
if (files.None())
{
err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
}
}
if (error != null)
{
DebugConsole.ThrowError(error);
}
}
tryFolderForSpecies(speciesName, out var error);
Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)
? prefab.VariantOf
: Identifier.Empty;
if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty)
{
tryFolderForSpecies(parentSpeciesName, out error);
}
if (!error.IsNullOrEmpty())
{
DebugConsole.ThrowError(error);
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
@@ -200,7 +192,7 @@ namespace Barotrauma
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T r = new T();
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies))
{
if (!ragdolls.ContainsKey(r.Name))
{
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
abstract class AbilityConditionDataless : AbilityCondition
{
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -19,10 +16,9 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect()
{
IEnumerable<Character> chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character);
foreach (Character character in chosenCharacters)
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (!allowSelf && character == Character) { continue; }
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects
{
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
@@ -30,7 +24,14 @@ namespace Barotrauma.Abilities
private bool IsApplicable(AbilityObject abilityObject)
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition(abilityObject));
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition(abilityObject))
{
return false;
}
}
return true;
}
}
}
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupInterval : CharacterAbilityGroup
{
@@ -49,7 +43,14 @@ namespace Barotrauma.Abilities
private bool IsApplicable()
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition());
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition())
{
return false;
}
}
return true;
}
}
}
@@ -1,8 +1,6 @@
using System;
using Barotrauma.Abilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma
{
@@ -2,12 +2,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -69,10 +67,10 @@ namespace Barotrauma
.ToImmutableHashSet();
}
public static Result<ContentFile, string> CreateFromXElement(ContentPackage contentPackage, XElement element)
public static Result<ContentFile, LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
static Result<ContentFile, string> fail(string error, string? stackTrace = null)
=> Result<ContentFile, string>.Failure(error, stackTrace);
static Result<ContentFile, LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception));
Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
@@ -95,11 +93,11 @@ namespace Barotrauma
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, string>.Success(file);
: Result<ContentFile, LoadError>.Success(file);
}
catch (Exception e)
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace());
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e);
}
}
@@ -125,5 +123,23 @@ namespace Barotrauma
}
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
public readonly struct LoadError
{
public readonly string Message;
public readonly Exception? Exception;
public LoadError(string message, Exception? exception)
{
Message = message;
Exception = exception;
}
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
}
}
@@ -14,7 +14,7 @@ namespace Barotrauma
{
public abstract class ContentPackage
{
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 17, 16, 0);
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 3, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -33,11 +33,11 @@ namespace Barotrauma
public readonly Version GameVersion;
public readonly string ModVersion;
public readonly Md5Hash Hash;
public Md5Hash Hash { get; private set; }
public readonly DateTime? InstallTime;
public readonly ImmutableArray<ContentFile> Files;
public readonly ImmutableArray<(string error, string? stackTrace)> Errors;
public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
public async Task<bool> IsUpToDate()
{
@@ -55,7 +55,7 @@ namespace Barotrauma
/// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary>
public readonly bool HasMultiplayerSyncedContent;
public bool HasMultiplayerSyncedContent { get; private set; }
protected ContentPackage(XDocument doc, string path)
{
@@ -84,13 +84,13 @@ namespace Barotrauma
.ToArray();
Files = fileResults
.OfType<Success<ContentFile, string>>()
.OfType<Success<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Value)
.ToImmutableArray();
Errors = fileResults
.OfType<Failure<ContentFile, string>>()
.Select(f => (f.Error, f.StackTrace))
.OfType<Failure<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Error)
.ToImmutableArray();
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
@@ -127,18 +127,13 @@ namespace Barotrauma
try
{
if (doc.Root.GetAttributeBool("corepackage", false))
{
return new CorePackage(doc, path);
}
else
{
return new RegularPackage(doc, path);
}
return doc.Root.GetAttributeBool("corepackage", false)
? (ContentPackage)new CorePackage(doc, path)
: new RegularPackage(doc, path);
}
catch (Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
e = e.GetInnermost();
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
}
@@ -278,12 +273,42 @@ namespace Barotrauma
Files.ForEach(f => f.UnloadFile());
}
public override int GetHashCode()
public void ReloadSubsAndItemAssemblies()
{
byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4));
return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3];
XDocument doc = XMLExtensions.TryLoadXml(Path);
List<ContentFile> newFileList = new List<ContentFile>();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
foreach (var result in fileResults)
{
switch (result)
{
case Success<ContentFile, ContentFile.LoadError> { Value: var file }:
if (file is BaseSubFile || file is ItemAssemblyFile)
{
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
break;
}
}
UnloadFilesOfType<BaseSubFile>();
UnloadFilesOfType<ItemAssemblyFile>();
Files = newFileList.ToImmutableArray();
Hash = CalculateHash();
LoadFilesOfType<BaseSubFile>();
LoadFilesOfType<ItemAssemblyFile>();
}
public static bool PathAllowedAsLocalModFile(string path)
{
#if DEBUG
@@ -305,21 +330,17 @@ namespace Barotrauma
public void LogErrors()
{
if (Errors.Any())
if (!Errors.Any())
{
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package\"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(e => errorToStr(e.error, e.stackTrace))));
static string errorToStr(string error, string? stackTrace)
{
string str = error;
if (stackTrace != null)
{
str += '\n' + stackTrace;
}
return str;
}
return;
}
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(errorToStr)));
static string errorToStr(ContentFile.LoadError error)
=> error.ToString();
}
}
}
@@ -430,9 +430,9 @@ namespace Barotrauma
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors)
foreach (ContentFile.LoadError error in VanillaCorePackage.Errors)
{
DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace));
DebugConsole.ThrowError(error.ToString());
}
}
@@ -27,7 +27,7 @@ namespace Barotrauma
public string BaseUri => Element.BaseUri;
public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null.");
public XDocument? Document => Element.Document;
public ContentXElement? FirstElement() => Elements().FirstOrDefault();
@@ -1836,8 +1836,8 @@ namespace Barotrauma
ThrowError($"No start item set identifier defined!");
return;
}
AutoItemPlacer.StartItemSet = args[0].ToIdentifier();
NewMessage($"Start item set changed to \"{AutoItemPlacer.StartItemSet}\"");
AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier();
NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\"");
}, isCheat: false));
//"dummy commands" that only exist so that the server can give clients permissions to use them
@@ -131,21 +131,22 @@ namespace Barotrauma
MaxAttachableCount,
}
[Flags]
public enum AbilityFlags
{
None,
MustWalk,
ImmuneToPressure,
IgnoredByEnemyAI,
MoveNormallyWhileDragging,
CanTinker,
CanTinkerFabricatorsAndDeconstructors,
TinkeringPowersDevices,
GainSkillPastMaximum,
RetainExperienceForNewCharacter,
AllowSecondOrderedTarget,
PowerfulCPR,
AlwaysStayConscious,
None = 0,
MustWalk = 0x1,
ImmuneToPressure = 0x2,
IgnoredByEnemyAI = 0x4,
MoveNormallyWhileDragging = 0x8,
CanTinker = 0x10,
CanTinkerFabricatorsAndDeconstructors = 0x20,
TinkeringPowersDevices = 0x40,
GainSkillPastMaximum = 0x80,
RetainExperienceForNewCharacter = 0x100,
AllowSecondOrderedTarget = 0x200,
PowerfulCPR = 0x400,
AlwaysStayConscious = 0x800,
}
[Flags]
@@ -156,9 +157,24 @@ namespace Barotrauma
Both = Bot | Player
}
public enum StartingBalanceAmount
{
Low,
Medium,
High,
}
public enum GameDifficulty
{
Easy,
Medium,
Hard,
Hellish
}
public enum NumberType
{
Int,
Float
}
}
}
@@ -80,7 +80,7 @@ namespace Barotrauma
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastUpdateID++;
mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
if (prefab != null)
{
@@ -64,8 +64,6 @@ namespace Barotrauma
campaign.GetWallet(client).Give(Amount);
}
}
((MultiPlayerCampaign)campaign).LastUpdateID++;
#else
campaign.Wallet.Give(Amount);
#endif
@@ -49,6 +49,9 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SpawnPointTag { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")]
public bool RequireSpawnPointTag { get; set; }
private readonly HashSet<Identifier> targetModuleTags = new HashSet<Identifier>();
[Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")]
@@ -79,7 +82,7 @@ namespace Barotrauma
public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase));
ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
}
public override bool IsFinished(ref string goTo)
@@ -110,22 +113,40 @@ namespace Barotrauma
if (humanPrefab != null)
{
ISpatialEntity spawnPos = GetSpawnPos();
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
if (spawnPos != null)
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
{
foreach (Item item in newCharacter.Inventory.AllItems)
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
foreach (Item item in newCharacter.Inventory.AllItems)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
}
}
}
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
}
}
else if (!SpeciesName.IsEmpty)
{
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
@@ -134,20 +155,9 @@ namespace Barotrauma
});
}
}
else if (!SpeciesName.IsEmpty)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
else if (!ItemIdentifier.IsEmpty)
{
if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab))
if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)");
}
@@ -178,7 +188,11 @@ namespace Barotrauma
if (spawnInventory == null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned);
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned);
}
}
else
{
@@ -244,10 +258,10 @@ namespace Barotrauma
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable());
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag);
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false)
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
@@ -274,18 +288,24 @@ namespace Barotrauma
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed));
if (spawnPoints.Any())
if (requireTaggedSpawnPoint || spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (potentialSpawnPoints.Count == 0)
if (potentialSpawnPoints.None())
{
DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
{
DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White);
}
else
{
DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
}
return null;
}
@@ -307,7 +327,7 @@ namespace Barotrauma
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
}
if (!validSpawnPoints.Any())
if (validSpawnPoints.None())
{
DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})");
return potentialSpawnPoints.GetRandomUnsynced();
@@ -320,7 +340,7 @@ namespace Barotrauma
}
//if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags
if (spawnpointTags == null || !spawnpointTags.Any())
if (spawnpointTags == null || spawnpointTags.None())
{
var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any());
if (spawnPoints.Any())
@@ -260,9 +260,15 @@ namespace Barotrauma
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
float extraDifficulty = 0;
if (GameMain.GameSession.Campaign?.Settings != null)
{
extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty;
}
float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100);
var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s =>
level.Difficulty >= s.MinLevelDifficulty &&
level.Difficulty <= s.MaxLevelDifficulty).ToArray();
modifiedDifficulty >= s.MinLevelDifficulty &&
modifiedDifficulty <= s.MaxLevelDifficulty).ToArray();
if (suitableSettings.Length == 0)
{
@@ -437,7 +437,7 @@ namespace Barotrauma
{
minDistance = 5000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation))
{
minDistance = 3000;
}
@@ -10,7 +10,7 @@ namespace Barotrauma
{
public static bool OutputDebugInfo = false;
public static void SpawnItems()
public static void SpawnItems(Identifier? startItemSet = null)
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
@@ -23,7 +23,7 @@ namespace Barotrauma
var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; }
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
SpawnStartItems(sub);
SpawnStartItems(sub, startItemSet);
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID);
CreateAndPlace(subs);
@@ -62,17 +62,23 @@ namespace Barotrauma
CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
}
public static Identifier StartItemSet = new Identifier("normal");
public static Identifier DefaultStartItemSet = new Identifier("normal");
/// <summary>
/// Spawns the items defined in the start item set in the specified sub.
/// </summary>
private static void SpawnStartItems(Submarine sub)
private static void SpawnStartItems(Submarine sub, Identifier? startItemSet)
{
if (!Barotrauma.StartItemSet.Sets.TryGet(StartItemSet, out StartItemSet itemSet))
Identifier setIdentifier = startItemSet ?? DefaultStartItemSet;
if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet))
{
DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{StartItemSet}\"!");
return;
DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!");
if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet))
{
DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!");
return;
}
itemSet = defaultSet;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
ISpatialEntity initialSpawnPos;
@@ -164,7 +170,7 @@ namespace Barotrauma
var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier);
foreach (ItemPrefab ip in itemPrefabs)
{
if (!ip.PreferredContainers.Any()) { continue; }
if (ip.PreferredContainers.None()) { continue; }
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);
@@ -10,63 +10,6 @@ using System.Xml.Linq;
namespace Barotrauma
{
internal struct CampaignSettings
{
public static CampaignSettings Empty => new CampaignSettings();
// Anything that uses this field I wasn't sure if actually needed the proper campaign settings to be passed down
public static CampaignSettings Unsure => Empty;
public bool RadiationEnabled { get; set; }
public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount();
private int maxMissionCount;
public int MaxMissionCount
{
get { return maxMissionCount; }
set { maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit); }
}
public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10;
public const int MinMissionCountLimit = 1;
public CampaignSettings(IReadMessage inc)
{
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = inc.ReadBoolean();
MaxMissionCount = inc.ReadRangedInteger(MinMissionCountLimit, MaxMissionCountLimit);
}
public CampaignSettings(XElement element)
{
maxMissionCount = DefaultMaxMissionCount;
RadiationEnabled = element.GetAttributeBool(nameof(RadiationEnabled).ToLowerInvariant(), true);
MaxMissionCount = element.GetAttributeInt(nameof(MaxMissionCount).ToLowerInvariant(), DefaultMaxMissionCount);
}
public void Serialize(IWriteMessage msg)
{
msg.Write(RadiationEnabled);
msg.WriteRangedInteger(MaxMissionCount, MinMissionCountLimit, MaxMissionCountLimit);
}
public int GetAddedMissionCount()
{
int count = 0;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
}
return count;
}
public XElement Save()
{
return new XElement(nameof(CampaignSettings), new XAttribute(nameof(RadiationEnabled).ToLowerInvariant(), RadiationEnabled), new XAttribute(nameof(MaxMissionCount).ToLowerInvariant(), MaxMissionCount));
}
}
abstract partial class CampaignMode : GameMode
{
[NetworkSerialize]
@@ -149,9 +92,8 @@ namespace Barotrauma
//key = dialog flag, double = Timing.TotalTime when the line was last said
private readonly Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
public bool PurchasedHullRepairs, PurchasedLostShuttles, PurchasedItemRepairs;
public SubmarineInfo PendingSubmarineSwitch;
public bool TransferItemsOnSubSwitch { get; set; }
protected Map map;
public Map Map
@@ -189,12 +131,16 @@ namespace Barotrauma
protected set;
}
protected CampaignMode(GameModePreset preset)
public virtual bool PurchasedHullRepairs { get; set; }
public virtual bool PurchasedLostShuttles { get; set; }
public virtual bool PurchasedItemRepairs { get; set; }
protected CampaignMode(GameModePreset preset, CampaignSettings settings)
: base(preset)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = InitialMoney
Balance = settings.InitialMoney
};
CargoManager = new CargoManager(this);
@@ -596,6 +542,7 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
else
@@ -729,7 +676,6 @@ namespace Barotrauma
}
}
public void EndCampaign()
{
foreach (Character c in Character.CharacterList)
@@ -741,7 +687,7 @@ namespace Barotrauma
}
foreach (LocationConnection connection in Map.Connections)
{
connection.Difficulty = MathHelper.Lerp(connection.Difficulty, 100.0f, 0.25f);
connection.Difficulty = connection.Biome.MaxDifficulty;
connection.LevelData = new LevelData(connection)
{
IsBeaconActive = false
@@ -750,6 +696,7 @@ namespace Barotrauma
}
foreach (Location location in Map.Locations)
{
location.LevelData = new LevelData(location, location.Biome.MaxDifficulty);
location.Reset();
}
Map.SetLocation(Map.Locations.IndexOf(Map.StartLocation));
@@ -873,7 +820,7 @@ namespace Barotrauma
const float MaxDist = 3000.0f;
const float MinDist = 2500.0f;
if (!Level.IsLoadedOutpost) { return; }
if (!Level.IsLoadedFriendlyOutpost) { return; }
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
@@ -1058,7 +1005,10 @@ namespace Barotrauma
public SubmarineInfo SwitchSubs()
{
TransferItemsBetweenSubs();
if (TransferItemsOnSubSwitch)
{
TransferItemsBetweenSubs();
}
RefreshOwnedSubmarines();
PendingSubmarineSwitch = null;
return GameMain.GameSession.SubmarineInfo;
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Xml.Linq;
namespace Barotrauma
{
internal static class CampaignModePresets
{
public static readonly ImmutableArray<CampaignSettings> List;
public static readonly ImmutableDictionary<Identifier, CampaignSettingDefinitions> Definitions;
private static readonly string fileListPath = Path.Combine("Data", "campaignsettings.xml");
static CampaignModePresets()
{
if (!File.Exists(fileListPath) || !(XMLExtensions.TryLoadXml(fileListPath)?.Root is { } docRoot))
{
List = ImmutableArray<CampaignSettings>.Empty;
return;
}
List<CampaignSettings> list = new List<CampaignSettings>();
Dictionary<Identifier, CampaignSettingDefinitions> definitions = new Dictionary<Identifier, CampaignSettingDefinitions>();
foreach (XElement element in docRoot.Elements())
{
Identifier name = element.NameAsIdentifier();
if (name == CampaignSettings.LowerCaseSaveElementName)
{
list.Add(new CampaignSettings(element));
}
else if (name == nameof(CampaignSettingDefinitions))
{
foreach (XElement subElement in element.Elements())
{
definitions.Add(subElement.NameAsIdentifier(), new CampaignSettingDefinitions(subElement));
}
}
}
List = list.ToImmutableArray();
Definitions = definitions.ToImmutableDictionary();
}
}
internal readonly struct CampaignSettingDefinitions
{
// Definitely not the best way to do this
private readonly ImmutableDictionary<Identifier, Either<int, float>> values;
public CampaignSettingDefinitions(XElement element)
{
var definitions = new Dictionary<Identifier, Either<int, float>>();
foreach (XAttribute attribute in element.Attributes())
{
Identifier name = attribute.NameAsIdentifier();
if (attribute.Value.Contains('.'))
{
definitions.Add(name, element.GetAttributeFloat(name.Value, 0));
}
else
{
definitions.Add(name, element.GetAttributeInt(name.Value, 0));
}
}
values = definitions.ToImmutableDictionary();
}
public float GetFloat(Identifier identifier)
{
return values.TryGetValue(identifier, out Either<int, float> value) && value.TryGet(out float range) ? range : 0.0f;
}
public int GetInt(Identifier identifier)
{
return values.TryGetValue(identifier, out Either<int, float> value) && value.TryGet(out int integer) ? integer : 0;
}
}
}
@@ -0,0 +1,114 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
internal class CampaignSettings : INetSerializableStruct, ISerializableEntity
{
public static CampaignSettings Empty => new CampaignSettings(element: null);
public string Name => "CampaignSettings";
public const string LowerCaseSaveElementName = "campaignsettings";
[Serialize("", IsPropertySaveable.Yes)]
public string PresetName { get; set; } = string.Empty;
[Serialize(false, IsPropertySaveable.Yes), NetworkSerialize]
public bool RadiationEnabled { get; set; }
private int maxMissionCount;
[Serialize(DefaultMaxMissionCount, IsPropertySaveable.Yes), NetworkSerialize(MinValueInt = MinMissionCountLimit, MaxValueInt = MaxMissionCountLimit)]
public int MaxMissionCount
{
get => maxMissionCount;
set => maxMissionCount = MathHelper.Clamp(value, MinMissionCountLimit, MaxMissionCountLimit);
}
public int TotalMaxMissionCount => MaxMissionCount + GetAddedMissionCount();
[Serialize(StartingBalanceAmount.Medium, IsPropertySaveable.Yes), NetworkSerialize]
public StartingBalanceAmount StartingBalanceAmount { get; set; }
[Serialize(GameDifficulty.Medium, IsPropertySaveable.Yes), NetworkSerialize]
public GameDifficulty Difficulty { get; set; }
[Serialize("normal", IsPropertySaveable.Yes), NetworkSerialize]
public Identifier StartItemSet { get; set; }
public int InitialMoney
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(StartingBalanceAmount).ToIdentifier(), out var definition))
{
return definition.GetInt(StartingBalanceAmount.ToIdentifier());
}
return 8000;
}
}
public float ExtraEventManagerDifficulty
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(ExtraEventManagerDifficulty).ToIdentifier(), out var definition))
{
return definition.GetFloat(Difficulty.ToIdentifier());
}
return 0;
}
}
public float LevelDifficultyMultiplier
{
get
{
if (CampaignModePresets.Definitions.TryGetValue(nameof(LevelDifficultyMultiplier).ToIdentifier(), out var definition))
{
return definition.GetFloat(Difficulty.ToIdentifier());
}
return 1.0f;
}
}
public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10;
public const int MinMissionCountLimit = 1;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
// required for INetSerializableStruct
public CampaignSettings()
{
SerializableProperties = SerializableProperty.GetProperties(this);
}
public CampaignSettings(XElement? element = null)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public XElement Save()
{
XElement saveElement = new XElement(LowerCaseSaveElementName);
SerializableProperty.SerializeProperties(this, saveElement, saveIfDefault: true);
return saveElement;
}
private static int GetAddedMissionCount()
{
int count = 0;
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
count += (int)character.GetStatValue(StatTypes.ExtraMissionCount);
}
return count;
}
}
}
@@ -29,7 +29,11 @@ namespace Barotrauma
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
missions.Add(Mission.LoadRandom(locations, seed, false, missionType));
var mission = Mission.LoadRandom(locations, seed, false, missionType);
if (mission != null)
{
missions.Add(mission);
}
}
protected static IEnumerable<MissionPrefab> ValidateMissionPrefabs(IEnumerable<MissionPrefab> missionPrefabs, Dictionary<MissionType, Type> missionClasses)
@@ -12,19 +12,60 @@ namespace Barotrauma
{
public const int MinimumInitialMoney = 500;
private UInt16 lastUpdateID;
public UInt16 LastUpdateID
[Flags]
public enum NetFlags : UInt16
{
get
{
#if SERVER
if (GameMain.Server != null && lastUpdateID < 1) { lastUpdateID++; }
#endif
return lastUpdateID;
}
set { lastUpdateID = value; }
Misc = 0x1,
MapAndMissions = 0x2,
UpgradeManager = 0x4,
SubList = 0x8,
ItemsInBuyCrate = 0x10,
ItemsInSellFromSubCrate = 0x20,
PurchasedItems = 0x80,
SoldItems = 0x100,
Reputation = 0x200,
CharacterInfo = 0x800
}
private readonly Dictionary<NetFlags, UInt16> lastUpdateID;
public UInt16 GetLastUpdateIdForFlag(NetFlags flag)
{
if (!ValidateFlag(flag)) { return 0; }
return lastUpdateID[flag];
}
public void SetLastUpdateIdForFlag(NetFlags flag, UInt16 id)
{
if (!ValidateFlag(flag)) { return; }
lastUpdateID[flag] = id;
}
public void IncrementLastUpdateIdForFlag(NetFlags flag)
{
if (!ValidateFlag(flag)) { return; }
if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
lastUpdateID[flag]++;
}
public void IncrementAllLastUpdateIds()
{
foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
{
if (!lastUpdateID.ContainsKey(flag)) { lastUpdateID[flag] = 0; }
lastUpdateID[flag]++;
}
}
private bool ValidateFlag(NetFlags flag)
{
if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
#if DEBUG
throw new InvalidOperationException($"\"{flag}\" is not a valid campaign update flag.");
#else
return false;
#endif
}
private UInt16 lastSaveID;
public UInt16 LastSaveID
{
@@ -35,11 +76,11 @@ namespace Barotrauma
#endif
return lastSaveID;
}
set
set
{
#if SERVER
//trigger a campaign update to notify the clients of the changed save ID
lastUpdateID++;
IncrementLastUpdateIdForFlag(NetFlags.Misc);
#endif
lastSaveID = value;
}
@@ -52,23 +93,33 @@ namespace Barotrauma
get; set;
}
private MultiPlayerCampaign() : base(GameModePreset.MultiPlayerCampaign)
private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings)
{
currentCampaignID++;
lastUpdateID = new Dictionary<NetFlags, ushort>();
foreach (NetFlags flag in Enum.GetValues(typeof(NetFlags)))
{
#if SERVER
//server starts from a higher ID to ensure we send the initial state
lastUpdateID[flag] = 1;
#else
lastUpdateID[flag] = 0;
#endif
}
CampaignID = currentCampaignID;
CampaignMetadata = new CampaignMetadata(this);
UpgradeManager = new UpgradeManager(this);
InitCampaignData();
}
public static MultiPlayerCampaign StartNew(string mapSeed, SubmarineInfo selectedSub, CampaignSettings settings)
public static MultiPlayerCampaign StartNew(string mapSeed, CampaignSettings settings)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
MultiPlayerCampaign campaign = new MultiPlayerCampaign(settings);
//only the server generates the map, the clients load it from a save file
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
campaign.map = new Map(campaign, mapSeed, settings);
campaign.Settings = settings;
campaign.map = new Map(campaign, mapSeed);
}
campaign.InitProjSpecific();
return campaign;
@@ -76,7 +127,7 @@ namespace Barotrauma
public static MultiPlayerCampaign LoadNew(XElement element)
{
MultiPlayerCampaign campaign = new MultiPlayerCampaign();
MultiPlayerCampaign campaign = new MultiPlayerCampaign(CampaignSettings.Empty);
campaign.Load(element);
campaign.InitProjSpecific();
campaign.IsFirstRound = false;
@@ -124,18 +175,17 @@ namespace Barotrauma
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "campaignsettings":
case CampaignSettings.LowerCaseSaveElementName:
Settings = new CampaignSettings(subElement);
#if CLIENT
GameMain.NetworkMember.ServerSettings.MaxMissionCount = Settings.MaxMissionCount;
GameMain.NetworkMember.ServerSettings.RadiationEnabled = Settings.RadiationEnabled;
GameMain.NetworkMember.ServerSettings.CampaignSettings = Settings;
#endif
break;
case "map":
if (map == null)
{
//map not created yet, loading this campaign for the first time
map = Map.Load(this, subElement, Settings);
map = Map.Load(this, subElement);
}
else
{
@@ -72,7 +72,7 @@ namespace Barotrauma
get
{
if (Map != null) { return Map.CurrentLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[0];
}
@@ -83,7 +83,7 @@ namespace Barotrauma
get
{
if (Map != null) { return Map.SelectedLocation; }
if (dummyLocations == null) { CreateDummyLocations(); }
if (dummyLocations == null) { dummyLocations = CreateDummyLocations(LevelData?.Seed ?? string.Empty); }
if (dummyLocations == null) { throw new NullReferenceException("dummyLocations is null somehow!"); }
return dummyLocations[1];
}
@@ -207,7 +207,7 @@ namespace Barotrauma
}
else if (gameModePreset.GameModeType == typeof(MultiPlayerCampaign))
{
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
var campaign = MultiPlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
if (selectedSub != null)
{
campaign.Bank.Deduct(selectedSub.Price);
@@ -218,7 +218,7 @@ namespace Barotrauma
#if CLIENT
else if (gameModePreset.GameModeType == typeof(SinglePlayerCampaign))
{
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), selectedSub, settings);
var campaign = SinglePlayerCampaign.StartNew(seed ?? ToolBox.RandomSeed(8), settings);
if (selectedSub != null)
{
campaign.Bank.TryDeduct(selectedSub.Price);
@@ -245,25 +245,15 @@ namespace Barotrauma
}
}
private void CreateDummyLocations(LocationType? forceLocationType = null)
public static Location[] CreateDummyLocations(string seed, LocationType? forceLocationType = null)
{
dummyLocations = new Location[2];
string seed = "";
if (GameMain.GameSession != null && GameMain.GameSession.Level != null)
{
seed = GameMain.GameSession.Level.Seed;
}
else if (GameMain.NetLobbyScreen != null)
{
seed = GameMain.NetLobbyScreen.LevelSeed;
}
var dummyLocations = new Location[2];
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
for (int i = 0; i < 2; i++)
{
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null, rand, requireOutpost: true, forceLocationType: forceLocationType);
}
return dummyLocations;
}
public void LoadPreviousSave()
@@ -275,7 +265,7 @@ namespace Barotrauma
/// <summary>
/// Switch to another submarine. The sub is loaded when the next round starts.
/// </summary>
public void SwitchSubmarine(SubmarineInfo newSubmarine, int cost, Client? client = null)
public void SwitchSubmarine(SubmarineInfo newSubmarine, bool transferItems, int cost, Client? client = null)
{
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
@@ -299,6 +289,7 @@ namespace Barotrauma
}
GameAnalyticsManager.AddMoneySpentEvent(cost, GameAnalyticsManager.MoneySink.SubmarineSwitch, newSubmarine.Name);
Campaign!.PendingSubmarineSwitch = newSubmarine;
Campaign!.TransferItemsOnSubSwitch = transferItems;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
@@ -309,6 +300,9 @@ namespace Barotrauma
{
GameAnalyticsManager.AddMoneySpentEvent(newSubmarine.Price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
OwnedSubmarines.Add(newSubmarine);
#if SERVER
(Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
#endif
}
}
@@ -345,7 +339,7 @@ namespace Barotrauma
!missionPrefab.AllowedConnectionTypes.Any())
{
LocationType? locationType = LocationType.Prefabs.FirstOrDefault(lt => missionPrefab.AllowedLocationTypes.Any(m => m == lt.Identifier));
CreateDummyLocations(locationType);
dummyLocations = CreateDummyLocations(levelSeed, locationType);
randomLevel = LevelData.CreateRandom(levelSeed, difficulty, levelGenerationParams, requireOutpost: true);
break;
}
@@ -430,7 +424,7 @@ namespace Barotrauma
Level? level = null;
if (levelData != null)
{
level = Level.Generate(levelData, mirrorLevel, startOutpost, endOutpost);
level = Level.Generate(levelData, mirrorLevel, StartLocation, EndLocation, startOutpost, endOutpost);
}
InitializeLevel(level);
@@ -603,7 +597,7 @@ namespace Barotrauma
Level.SpawnCorpses();
Level.PrepareBeaconStation();
}
AutoItemPlacer.SpawnItems();
AutoItemPlacer.SpawnItems(Campaign?.Settings.StartItemSet);
}
if (GameMode is MultiPlayerCampaign mpCampaign)
{
@@ -360,9 +360,6 @@ namespace Barotrauma
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Items.Any(it => it != item))
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
#endif
if (!slots[i].First().AllowedSlots.Contains(InvSlotType.Any) || !TryPutItem(slots[i].FirstOrDefault(), character, new List<InvSlotType> { InvSlotType.Any }, true, ignoreCondition))
{
free = false;
@@ -382,9 +379,6 @@ namespace Barotrauma
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents<Pickable>().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty())
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
#endif
bool removeFromOtherSlots = item.ParentInventory != this;
if (placedInSlot == -1 && inWrongSlot)
{
@@ -454,9 +448,6 @@ namespace Barotrauma
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
return false;
}
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[index])) { hidePersonalSlots = false; }
#endif
//there's already an item in the slot
if (slots[index].Any())
{
@@ -480,9 +471,6 @@ namespace Barotrauma
foreach (InvSlotType allowedSlot in pickable.AllowedSlots)
{
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
@@ -144,9 +144,16 @@ namespace Barotrauma.Items.Components
}
}
public bool Combine(GeneticMaterial otherGeneticMaterial, Character user)
public enum CombineResult
{
if (!CanBeCombinedWith(otherGeneticMaterial)) { return false; }
None,
Refined,
Combined
}
public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user)
{
if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; }
float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
@@ -158,7 +165,7 @@ namespace Barotrauma.Items.Components
{
MakeTainted();
}
return true;
return CombineResult.Refined;
}
else
{
@@ -171,7 +178,7 @@ namespace Barotrauma.Items.Components
{
MakeTainted();
}
return false;
return CombineResult.Combined;
}
}
@@ -697,14 +697,17 @@ namespace Barotrauma.Items.Components
Vector2 fromCharacterToLeak = leak.WorldPosition - character.AnimController.AimSourceWorldPos;
float dist = fromCharacterToLeak.Length();
float reach = AIObjectiveFixLeak.CalculateReach(this, character);
if (dist > reach * 3)
if (dist > reach * 2)
{
// Too far away -> consider this done and hope the AI is smart enough to move closer
Reset();
return true;
}
character.AIController.SteeringManager.Reset();
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
if (!character.AnimController.InWater)
{
// TODO: use the collider size?
@@ -714,34 +717,25 @@ namespace Barotrauma.Items.Components
humanAnim.Crouching = true;
}
}
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater))
if (!character.IsClimbing)
{
// Steer closer
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater))
{
// Swimming inside the sub
if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && (indoorSteering.CurrentPath.Unreachable || indoorSteering.CurrentPath.Finished))
// Steer closer
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
if (!character.InWater)
{
Vector2 dir = Vector2.Normalize(fromCharacterToLeak);
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
}
else
{
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
dir.Y = 0;
}
character.AIController.SteeringManager.SteeringManual(deltaTime, dir);
}
else
else if (dist < reach * 0.25f && !character.IsClimbing)
{
// Swimming outside the sub
character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak));
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
}
else if (dist < reach * 0.25f)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
if (dist <= reach)
if (dist <= reach || character.IsClimbing)
{
// In range
character.CursorPosition = leak.WorldPosition;
@@ -815,7 +809,7 @@ namespace Barotrauma.Items.Components
}
bool leakFixed = (leak.Open <= 0.0f || leak.Removed) &&
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Average(s => s.damage) < 1);
(leak.ConnectedWall == null || leak.ConnectedWall.Sections.Max(s => s.damage) < 0.1f);
if (leakFixed && leak.FlowTargetHull?.DisplayName != null && character.IsOnPlayerTeam)
{
@@ -187,7 +187,12 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No)]
public bool RemoveContainedItemsOnDeconstruct { get; set; }
private SlotRestrictions[] slotRestrictions;
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private Vector2 prevContainedItemPositions;
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
@@ -237,10 +242,11 @@ namespace Barotrauma.Items.Components
}
}
Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
slotRestrictions = new SlotRestrictions[totalCapacity];
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems);
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems));
}
int subContainerIndex = capacity;
@@ -268,11 +274,13 @@ namespace Barotrauma.Items.Components
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
{
slotRestrictions[i] = new SlotRestrictions(subMaxStackSize, subContainableItems);
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems));
}
subContainerIndex += subCapacity;
}
capacity = totalCapacity;
slotRestrictions = newSlotRestrictions.ToImmutableArray();
System.Diagnostics.Debug.Assert(totalCapacity == slotRestrictions.Length);
InitProjSpecific(element);
}
@@ -365,18 +373,21 @@ namespace Barotrauma.Items.Components
return false;
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public override void Update(float deltaTime, Camera cam)
{
if (!string.IsNullOrEmpty(SpawnWithId) && !alwaysContainedItemsSpawned)
{
SpawnAlwaysContainedItems();
alwaysContainedItemsSpawned = true;
}
if (item.ParentInventory is CharacterInventory ownerInventory)
{
item.SetContainedItemPositions();
if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f)
{
SetContainedItemPositions();
prevContainedItemPositions = item.Position;
}
if (AutoInject)
{
@@ -397,7 +408,7 @@ namespace Barotrauma.Items.Components
item.body.Enabled &&
item.body.FarseerBody.Awake)
{
item.SetContainedItemPositions();
SetContainedItemPositions();
}
else if (activeContainedItems.Count == 0)
{
@@ -147,7 +147,7 @@ namespace Barotrauma.Items.Components
CancelUsing(user);
user = null;
}
if (!IsToggle) { IsActive = false; }
if (!IsToggle || item.Connections == null) { IsActive = false; }
return;
}
@@ -231,28 +231,40 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier))
{
user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
}
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
{
if (geneticMaterial1.Combine(geneticMaterial2, user))
var result = geneticMaterial1.Combine(geneticMaterial2, user);
if (result == GeneticMaterial.CombineResult.Refined)
{
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
}
if (result != GeneticMaterial.CombineResult.None)
{
OnCombinedOrRefined();
}
allowRemove = false;
return;
}
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
else
{
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
OnCombinedOrRefined();
}
}
}
void OnCombinedOrRefined()
{
user?.CheckTalents(AbilityEffectType.OnGeneticMaterialCombinedOrRefined);
foreach (Character character in Character.GetFriendlyCrew(user))
{
character.CheckTalents(AbilityEffectType.OnCrewGeneticMaterialCombinedOrRefined);
}
}
}
@@ -400,8 +400,8 @@ namespace Barotrauma.Items.Components
private void IncreaseSkillLevel(Character user, float deltaTime)
{
if (user?.Info == null) { return; }
// Do not increase the helm skill when "steering" the sub in an outpost level
if (GameMain.GameSession?.Campaign != null && Level.IsLoadedOutpost) { return; }
// Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck)
if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
user.Info.IncreaseSkillLevel(
@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
@@ -81,6 +80,8 @@ namespace Barotrauma.Items.Components
private ItemContainer? container;
private float growthTickTimer;
private List<LightComponent>? lightComponents;
public Planter(Item item, ContentXElement element) : base(item, element)
{
canBePicked = true;
@@ -107,10 +108,14 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
IsActive = true;
#if CLIENT
lightComponent = item.GetComponent<LightComponent>();
if (lightComponent != null)
var lights = item.GetComponents<LightComponent>();
if (lights.Any())
{
lightComponent.Light.Enabled = false;
lightComponents = lights.ToList();
foreach (var light in lightComponents)
{
light.Light.Enabled = false;
}
}
#endif
container = item.GetComponent<ItemContainer>();
@@ -227,12 +232,17 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
#if CLIENT
if (lightComponent != null)
if (lightComponents != null && lightComponents.Count > 0)
{
bool hasSeed = false;
foreach (Growable? seed in GrowableSeeds) { hasSeed |= seed != null; }
lightComponent.Light.Enabled = hasSeed;
foreach (Growable? seed in GrowableSeeds)
{
hasSeed |= seed != null;
}
foreach (var light in lightComponents)
{
light.Light.Enabled = hasSeed;
}
}
#endif
@@ -602,7 +602,7 @@ namespace Barotrauma.Items.Components
private bool ShouldDeteriorate()
{
if (Level.IsLoadedOutpost) { return false; }
if (Level.IsLoadedFriendlyOutpost) { return false; }
if (LastActiveTime > Timing.TotalTime) { return true; }
foreach (ItemComponent ic in item.Components)
@@ -13,12 +13,13 @@ namespace Barotrauma.Items.Components
private float updateTimer;
[Flags]
public enum TargetType
{
Any,
Human,
Monster,
Wall
Human = 1,
Monster = 2,
Wall = 4,
Any = Human | Monster | Wall,
}
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
@@ -179,6 +180,11 @@ namespace Barotrauma.Items.Components
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); }
if (MotionDetected)
{
ApplyStatusEffects(ActionType.OnUse, deltaTime);
}
updateTimer -= deltaTime;
if (updateTimer > 0.0f) { return; }
@@ -199,8 +205,7 @@ namespace Barotrauma.Items.Components
float broadRangeX = Math.Max(rangeX * 2, 500);
float broadRangeY = Math.Max(rangeY * 2, 500);
if (item.CurrentHull == null && item.Submarine != null &&
(Target == TargetType.Wall || Target == TargetType.Any))
if (item.CurrentHull == null && item.Submarine != null && Target.HasFlag(TargetType.Wall))
{
if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity))
{
@@ -248,7 +253,7 @@ namespace Barotrauma.Items.Components
}
}
if (Target != TargetType.Wall)
if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Monster))
{
foreach (Character c in Character.CharacterList)
{
@@ -258,14 +263,13 @@ namespace Barotrauma.Items.Components
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
switch (Target)
if (c.IsHuman)
{
case TargetType.Human:
if (!c.IsHuman) { continue; }
break;
case TargetType.Monster:
if (c.IsHuman || c.IsPet) { continue; }
break;
if (!Target.HasFlag(TargetType.Human)) { continue; }
}
else if (!c.IsPet)
{
if (!Target.HasFlag(TargetType.Monster)) { continue; }
}
//do a rough check based on the position of the character's collider first
@@ -4,6 +4,7 @@ using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Barotrauma.Items.Components
@@ -12,6 +13,46 @@ namespace Barotrauma.Items.Components
{
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
public float Force { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)]
public bool DistanceBasedForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
public bool ForceFluctuation { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
private float ForceFluctuationStrength
{
get
{
return forceFluctuationStrength;
}
set
{
forceFluctuationStrength = Math.Clamp(value, 0.0f, 1.0f);
}
}
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)]
private float ForceFluctuationFrequency
{
get
{
return forceFluctuationFrequency;
}
set
{
forceFluctuationFrequency = Math.Max(value, 0.01f);
}
}
[Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)]
private float ForceFluctuationInterval
{
get
{
return forceFluctuationInterval;
}
set
{
forceFluctuationInterval = Math.Max(value, 0.01f);
}
}
public PhysicsBody PhysicsBody { get; private set; }
private float Radius { get; set; }
@@ -38,11 +79,6 @@ namespace Barotrauma.Items.Components
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly bool distanceBasedForce;
private readonly bool forceFluctuation;
private readonly float forceFluctuationStrength;
private readonly float forceFluctuationFrequency;
private readonly float forceFluctuationInterval;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
/// <summary>
/// Effects applied to entities inside the trigger
@@ -53,6 +89,10 @@ namespace Barotrauma.Items.Components
/// </summary>
private readonly List<Attack> attacks = new List<Attack>();
private float forceFluctuationStrength;
private float forceFluctuationFrequency;
private float forceFluctuationInterval;
public TriggerComponent(Item item, ContentXElement element) : base(item, element)
{
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
@@ -61,15 +101,6 @@ namespace Barotrauma.Items.Components
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.");
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
distanceBasedForce = element.GetAttributeBool("distancebasedforce", false);
forceFluctuation = element.GetAttributeBool("forcefluctuation", false);
forceFluctuationStrength = element.GetAttributeFloat("forcefluctuationstrength", 1.0f);
forceFluctuationStrength = Math.Clamp(forceFluctuationStrength, 0.0f, 1.0f);
forceFluctuationFrequency = element.GetAttributeFloat("fluctuationfrequency", 1.0f);
forceFluctuationFrequency = Math.Max(forceFluctuationFrequency, 0.01f);
forceFluctuationInterval = element.GetAttributeFloat("fluctuationinterval", 0.01f);
forceFluctuationInterval = Math.Max(forceFluctuationInterval, 0.01f);
string parentDebugName = $"TriggerComponent in {item.Name}";
foreach (var subElement in element.Elements())
{
@@ -153,14 +184,14 @@ namespace Barotrauma.Items.Components
TriggerActive = triggerers.Any();
if (forceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
ForceFluctuationTimer += deltaTime;
if (ForceFluctuationTimer >= forceFluctuationInterval)
if (ForceFluctuationTimer >= ForceFluctuationInterval)
{
float v = MathF.Sin(2 * MathF.PI * forceFluctuationFrequency * TimeInLevel);
float v = MathF.Sin(2 * MathF.PI * ForceFluctuationFrequency * TimeInLevel);
float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
CurrentForceFluctuation = MathHelper.Lerp(1.0f - forceFluctuationStrength, 1.0f, amount);
CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount);
ForceFluctuationTimer = 0.0f;
GameMain.NetworkMember?.CreateEntityEvent(this);
}
@@ -179,7 +210,7 @@ namespace Barotrauma.Items.Components
LevelTrigger.ApplyAttacks(attacks, item.WorldPosition, deltaTime);
}
if (Force < 0.01f)
if (Math.Abs(Force) < 0.01f)
{
// Just ignore very minimal forces
continue;
@@ -205,7 +236,7 @@ namespace Barotrauma.Items.Components
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = distanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff);
if (force.LengthSquared() < 0.01f) { return; }
@@ -227,5 +258,43 @@ namespace Barotrauma.Items.Components
PhysicsBody.Submarine = item.Submarine;
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
base.ReceiveSignal(signal, connection);
switch (connection.Name)
{
case "set_force":
if (!FloatTryParse(signal, out float force)) { break; }
Force = force;
break;
case "set_distancebasedforce":
if (!bool.TryParse(signal.value, out bool distanceBasedForce)) { break; }
DistanceBasedForce = distanceBasedForce;
break;
case "set_forcefluctuation":
if (!bool.TryParse(signal.value, out bool forceFluctuation)) { break; }
ForceFluctuation = forceFluctuation;
break;
case "set_forcefluctuationstrength":
if (!FloatTryParse(signal, out float forceFluctuationStrength)) { break; }
ForceFluctuationStrength = forceFluctuationStrength;
break;
case "set_forcefluctuationfrequency":
if (!FloatTryParse(signal, out float forceFluctuationFrequency)) { break; }
ForceFluctuationFrequency = forceFluctuationFrequency;
break;
case "set_forcefluctuationinterval":
if (!FloatTryParse(signal, out float forceFluctuationInterval)) { break; }
ForceFluctuationInterval = forceFluctuationInterval;
break;
}
static bool FloatTryParse(Signal signal, out float value)
{
return float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
}
}
}
}
}
@@ -4,9 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using FarseerPhysics.Dynamics;
@@ -20,8 +18,6 @@ namespace Barotrauma.Items.Components
private Vector2 barrelPos;
private Vector2 transformedBarrelPos;
private LightComponent lightComponent;
private float rotation, targetRotation;
@@ -71,6 +67,8 @@ namespace Barotrauma.Items.Components
public Character ActiveUser;
private float resetActiveUserTimer;
private List<LightComponent> lightComponents;
public float Rotation
{
get { return rotation; }
@@ -168,10 +166,13 @@ namespace Barotrauma.Items.Components
rotation = (minRotation + maxRotation) / 2;
#if CLIENT
if (lightComponent != null)
if (lightComponents != null)
{
lightComponent.Rotation = rotation;
lightComponent.Light.Rotation = -rotation;
foreach (var light in lightComponents)
{
light.Rotation = rotation;
light.Light.Rotation = -rotation;
}
}
#endif
}
@@ -331,27 +332,39 @@ namespace Barotrauma.Items.Components
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
targetRotation = rotation;
FindLightComponent();
UpdateTransformedBarrelPos();
}
private void FindLightComponent()
private void FindLightComponents()
{
if (lightComponents != null)
{
// Can't run again, because of reparenting.
return;
}
foreach (LightComponent lc in item.GetComponents<LightComponent>())
{
// Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it.
if (lc?.Parent == this)
{
lightComponent = lc;
break;
if (lightComponents == null)
{
lightComponents = new List<LightComponent>();
}
lightComponents.Add(lc);
}
}
#if CLIENT
if (lightComponent != null)
if (lightComponents != null)
{
lightComponent.Parent = null;
lightComponent.Rotation = Rotation - item.RotationRad;
lightComponent.Light.Rotation = -rotation;
foreach (var light in lightComponents)
{
// We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active)
light.Parent = null;
light.Rotation = Rotation - item.RotationRad;
light.Light.Rotation = -rotation;
}
}
#endif
}
@@ -428,7 +441,7 @@ namespace Barotrauma.Items.Components
if (MathUtils.NearlyEqual(minRotation, maxRotation))
{
UpdateLightComponent();
UpdateLightComponents();
return;
}
@@ -452,7 +465,7 @@ namespace Barotrauma.Items.Components
}
// Do not increase the weapons skill when operating a turret in an outpost level
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost))
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
{
user.Info.IncreaseSkillLevel("weapons".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
@@ -509,14 +522,17 @@ namespace Barotrauma.Items.Components
aiFindTargetTimer -= deltaTime;
}
UpdateLightComponent();
UpdateLightComponents();
}
private void UpdateLightComponent()
private void UpdateLightComponents()
{
if (lightComponent != null)
if (lightComponents != null)
{
lightComponent.Rotation = Rotation - item.RotationRad;
foreach (var light in lightComponents)
{
light.Rotation = Rotation - item.RotationRad;
}
}
}
@@ -1601,21 +1617,24 @@ namespace Barotrauma.Items.Components
}
break;
case "toggle_light":
if (lightComponent != null && signal.value != "0")
if (lightComponents != null && signal.value != "0")
{
lightComponent.IsOn = !lightComponent.IsOn;
UpdateLightComponent();
foreach (var light in lightComponents)
{
light.IsOn = !light.IsOn;
}
UpdateLightComponents();
}
break;
case "set_light":
if (lightComponent != null)
if (lightComponents != null)
{
bool shouldBeOn = signal.value != "0";
if (shouldBeOn != lightComponent.IsOn)
foreach (var light in lightComponents)
{
lightComponent.IsOn = shouldBeOn;
UpdateLightComponent();
light.IsOn = shouldBeOn;
}
UpdateLightComponents();
}
break;
}
@@ -1633,7 +1652,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
FindLightComponent();
FindLightComponents();
targetRotation = rotation;
if (!loadedBaseRotation.HasValue)
{
@@ -6,7 +6,6 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using System.Collections.Immutable;
using Barotrauma.Abilities;
namespace Barotrauma
@@ -82,7 +81,20 @@ namespace Barotrauma
public string Sound { get; private set; }
public Point? SheetIndex { get; private set; }
public LightComponent LightComponent { get; set; }
public LightComponent LightComponent => LightComponents?.FirstOrDefault();
public List<LightComponent> LightComponents
{
get
{
if (_lightComponents == null)
{
_lightComponents = new List<LightComponent>();
}
return _lightComponents;
}
}
private List<LightComponent> _lightComponents;
public int Variant { get; set; }
@@ -338,11 +350,14 @@ namespace Barotrauma.Items.Components
foreach (var lightElement in subElement.Elements())
{
if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; }
wearableSprites[i].LightComponent = new LightComponent(item, lightElement)
wearableSprites[i].LightComponents.Add(new LightComponent(item, lightElement)
{
Parent = this
};
item.AddComponent(wearableSprites[i].LightComponent);
});
foreach (var light in wearableSprites[i].LightComponents)
{
item.AddComponent(light);
}
}
i++;
@@ -413,7 +428,10 @@ namespace Barotrauma.Items.Components
IsActive = true;
if (wearableSprite.LightComponent != null)
{
wearableSprite.LightComponent.ParentBody = equipLimb.body;
foreach (var light in wearableSprite.LightComponents)
{
light.ParentBody = equipLimb.body;
}
}
limb[i] = equipLimb;
@@ -467,7 +485,10 @@ namespace Barotrauma.Items.Components
if (wearableSprites[i].LightComponent != null)
{
wearableSprites[i].LightComponent.ParentBody = null;
foreach (var light in wearableSprites[i].LightComponents)
{
light.ParentBody = null;
}
}
equipLimb.WearingItems.RemoveAll(w => w != null && w == wearableSprites[i]);
@@ -494,7 +515,6 @@ namespace Barotrauma.Items.Components
}
item.SetTransform(picker.SimPosition, 0.0f);
item.SetContainedItemPositions();
item.ApplyStatusEffects(ActionType.OnWearing, deltaTime, picker);
@@ -24,13 +24,18 @@ namespace Barotrauma
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
{
public static List<Item> ItemList = new List<Item>();
private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
public static IReadOnlyCollection<Item> DangerousItems { get { return dangerousItems; } }
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
public static bool ShowLinks = true;
private readonly HashSet<Identifier> tags;
private bool isWire, isLogic;
private readonly bool isWire, isLogic;
private Hull currentHull;
public Hull CurrentHull
@@ -279,7 +284,10 @@ namespace Barotrauma
if (Screen.Selected == GameMain.SubEditorScreen)
{
SetContainedItemPositions();
GetComponent<LightComponent>()?.SetLightSourceTransform();
foreach (var light in GetComponents<LightComponent>())
{
light.SetLightSourceTransform();
}
}
#endif
}
@@ -1001,6 +1009,10 @@ namespace Barotrauma
InsertToList();
ItemList.Add(this);
if (Prefab.IsDangerous)
{
dangerousItems.Add(this);
}
DebugConsole.Log("Created " + Name + " (" + ID + ")");
@@ -1092,17 +1104,16 @@ namespace Barotrauma
component.OnActiveStateChanged += (bool isActive) =>
{
bool hasSounds = false;
bool needsSoundUpdate = false;
#if CLIENT
hasSounds = component.HasSounds;
needsSoundUpdate = component.NeedsSoundUpdate();
#endif
//component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it,
//nor status effects, sounds or conditionals that would need to run
//nor sounds or conditionals that would need to run
if (!isActive && !component.UpdateWhenInactive &&
!hasSounds &&
!needsSoundUpdate &&
component.Parent == null &&
(component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) &&
(component.statusEffectLists == null || !component.statusEffectLists.Any()))
(component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()))
{
if (updateableComponents.Contains(component)) { updateableComponents.Remove(component); }
}
@@ -1499,6 +1510,11 @@ namespace Barotrauma
public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null)
{
if (effect.intervalTimer > 0.0f)
{
effect.intervalTimer -= deltaTime;
return;
}
if (!isNetworkEvent && checkCondition)
{
if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; }
@@ -1630,6 +1646,7 @@ namespace Barotrauma
foreach (ItemComponent ic in components)
{
ic.PlaySound(ActionType.OnBroken);
ic.StopSounds(ActionType.OnActive);
}
if (Screen.Selected == GameMain.SubEditorScreen) { return; }
#endif
@@ -1722,6 +1739,19 @@ namespace Barotrauma
public override void Update(float deltaTime, Camera cam)
{
#if SERVER
if (!(Submarine is { Loading: true }))
{
sendConditionUpdateTimer -= deltaTime;
if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f)
{
SendPendingNetworkUpdates();
}
}
#endif
if (!isActive) { return; }
if (impactQueue != null)
{
while (impactQueue.TryDequeue(out float impact))
@@ -1730,22 +1760,11 @@ namespace Barotrauma
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (!Submarine?.Loading ?? true))
{
sendConditionUpdateTimer -= deltaTime;
if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f)
{
SendPendingNetworkUpdates();
}
}
if (aiTarget != null)
if (aiTarget != null && aiTarget.NeedsUpdate)
{
aiTarget.Update(deltaTime);
}
if (!isActive) { return; }
ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
@@ -1846,7 +1865,10 @@ namespace Barotrauma
}
else
{
if (updateableComponents.Count == 0 && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled))
if (updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
(body == null || !body.Enabled))
{
#if CLIENT
positionBuffer.Clear();
@@ -1983,6 +2005,7 @@ namespace Barotrauma
impactQueue ??= new ConcurrentQueue<float>();
impactQueue.Enqueue(impact);
isActive = true;
return true;
}
@@ -2501,11 +2524,9 @@ namespace Barotrauma
if (ic.Use(deltaTime, character))
{
ic.WasUsed = true;
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);
if (ic.DeleteOnUse) { remove = true; }
@@ -2534,11 +2555,9 @@ namespace Barotrauma
if (ic.SecondaryUse(deltaTime, character))
{
ic.WasSecondaryUsed = true;
#if CLIENT
ic.PlaySound(ActionType.OnSecondaryUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);
if (ic.DeleteOnUse) { remove = true; }
@@ -3219,9 +3238,17 @@ namespace Barotrauma
item.RecalculateConditionValues();
item.SetActiveSprite();
if (submarine?.Info.GameVersion != null)
Version savedVersion = submarine?.Info.GameVersion;
if (element.Document?.Root != null && element.Document.Root.Name.ToString().Equals("gamesession", StringComparison.OrdinalIgnoreCase))
{
SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion);
//character inventories are loaded from the game session file - use the version number of the saved game session instead of the sub
//(the sub may have already been saved and up-to-date, even though the character inventories aren't)
savedVersion = new Version(element.Document.Root.GetAttributeString("version", "0.0.0.0"));
}
if (savedVersion != null)
{
SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, savedVersion);
}
foreach (ItemComponent component in item.components)
@@ -3342,6 +3369,7 @@ namespace Barotrauma
ic.ShallowRemove();
}
ItemList.Remove(this);
dangerousItems.Remove(this);
if (body != null)
{
@@ -3400,6 +3428,7 @@ namespace Barotrauma
#endif
}
ItemList.Remove(this);
dangerousItems.Remove(this);
if (body != null)
{
@@ -26,9 +26,15 @@ namespace Barotrauma
public readonly ImmutableArray<StartItem> Items;
/// <summary>
/// The order in which the sets are displayed in menus
/// </summary>
public readonly int Order;
public StartItemSet(ContentXElement element, StartItemsFile file) : base(file, element.GetAttributeIdentifier("identifier", Identifier.Empty))
{
Items = element.Elements().Select(e => new StartItem(e!)).ToImmutableArray();
Order = element.GetAttributeInt("order", 0);
}
public override void Dispose() { }
@@ -10,12 +10,11 @@ using System.Net;
namespace Barotrauma
{
#warning TODO: MapEntityPrefab should be constrained further to not include item assemblies, as assemblies are effectively not entities at all
partial class ItemAssemblyPrefab : MapEntityPrefab
{
public static readonly PrefabCollection<ItemAssemblyPrefab> Prefabs = new PrefabCollection<ItemAssemblyPrefab>();
public static readonly string VanillaSaveFolder = Path.Combine("Content", "Items", "Assemblies");
private readonly XElement configElement;
public readonly ImmutableArray<(Identifier Identifier, Rectangle Rect)> DisplayEntities;
@@ -49,7 +49,7 @@ namespace Barotrauma
Cave = 0x4,
Ruin = 0x8,
Wreck = 0x10,
BeaconStation = 0x20, // Not used anywhere
BeaconStation = 0x20,
Abyss = 0x40,
AbyssCave = 0x80
}
@@ -395,6 +395,13 @@ namespace Barotrauma
/// </summary>
public static bool IsLoadedOutpost => Loaded?.Type == LevelData.LevelType.Outpost;
/// <summary>
/// Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1)
/// </summary>
public static bool IsLoadedFriendlyOutpost =>
loaded?.Type == LevelData.LevelType.Outpost &&
(loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC || loaded?.StartLocation?.Type?.OutpostTeam == CharacterTeamType.Team1);
public LevelGenerationParams GenerationParams
{
get { return LevelData.GenerationParams; }
@@ -421,7 +428,7 @@ namespace Barotrauma
borders = new Rectangle(Point.Zero, levelData.Size);
}
public static Level Generate(LevelData levelData, bool mirror, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
public static Level Generate(LevelData levelData, bool mirror, Location startLocation, Location endLocation, SubmarineInfo startOutpost = null, SubmarineInfo endOutpost = null)
{
Debug.Assert(levelData.Biome != null);
if (levelData.Biome == null) { throw new ArgumentException("Biome was null"); }
@@ -433,11 +440,11 @@ namespace Barotrauma
preSelectedStartOutpost = startOutpost,
preSelectedEndOutpost = endOutpost
};
level.Generate(mirror);
level.Generate(mirror, startLocation, endLocation);
return level;
}
private void Generate(bool mirror)
private void Generate(bool mirror, Location startLocation, Location endLocation)
{
Loaded?.Remove();
Loaded = this;
@@ -454,8 +461,8 @@ namespace Barotrauma
if (LevelData.ForceOutpostGenerationParams == null)
{
StartLocation = GameMain.GameSession?.StartLocation;
EndLocation = GameMain.GameSession?.EndLocation;
StartLocation = startLocation;
EndLocation = endLocation;
}
GenerateEqualityCheckValue(LevelGenStage.GenStart);
@@ -509,7 +516,7 @@ namespace Barotrauma
Rectangle pathBorders = borders;
pathBorders.Inflate(
-Math.Min(Math.Min(minMainPathWidth * 2, MaxSubmarineWidth), borders.Width / 5),
-Math.Min(minMainPathWidth, borders.Height / 5));
-Math.Min(minMainPathWidth * 2, borders.Height / 5));
if (pathBorders.Width <= 0) { throw new InvalidOperationException($"The width of the level's path area is invalid ({pathBorders.Width})"); }
if (pathBorders.Height <= 0) { throw new InvalidOperationException($"The height of the level's path area is invalid ({pathBorders.Height})"); }
@@ -1713,7 +1720,7 @@ namespace Barotrauma
#endif
}
}
else
else if (abyssHeight > 30000)
{
//if the bottom of the abyss area is below crush depth, try to move it up to keep (most) of the abyss content above crush depth
//but only if start of the abyss is above crush depth (no point in doing this if all of it is below crush depth)
@@ -3527,6 +3534,8 @@ namespace Barotrauma
}
else if (type == SubmarineType.BeaconStation)
{
PositionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.BeaconStation, submarine: sub));
sub.ShowSonarMarker = false;
sub.DockedTo.ForEach(s => s.ShowSonarMarker = false);
sub.PhysicsBody.FarseerBody.BodyType = BodyType.Static;
@@ -3940,7 +3949,7 @@ namespace Barotrauma
//the submarine port has to be at the top of the sub
if (port.Item.WorldPosition.Y < Submarine.MainSub.WorldPosition.Y) { continue; }
float dist = Math.Abs(port.Item.WorldPosition.X - Submarine.MainSub.WorldPosition.X);
if (dist < closestDistance)
if (dist < closestDistance || subPort.MainDockingPort)
{
subPort = port;
closestDistance = dist;
@@ -4023,6 +4032,26 @@ namespace Barotrauma
DebugConsole.ThrowError("No BeaconStation files found in the selected content packages!");
return;
}
var beaconInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsBeacon);
for (int i = beaconStationFiles.Count - 1; i >= 0; i--)
{
var beaconStationFile = beaconStationFiles[i];
var matchingInfo = beaconInfos.SingleOrDefault(info => info.FilePath == beaconStationFile.Path.Value);
Debug.Assert(matchingInfo != null);
if (matchingInfo?.BeaconStationInfo is BeaconStationInfo beaconInfo)
{
if (LevelData.Difficulty < beaconInfo.MinLevelDifficulty || LevelData.Difficulty > beaconInfo.MaxLevelDifficulty)
{
beaconStationFiles.RemoveAt(i);
}
}
}
if (beaconStationFiles.None())
{
DebugConsole.ThrowError($"No BeaconStation files found for the level difficulty {LevelData.Difficulty}!");
return;
}
var contentFile = beaconStationFiles.GetRandom(Rand.RandSync.ServerAndClient);
string beaconStationName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path.Value);
@@ -4078,24 +4107,22 @@ namespace Barotrauma
{
if (!(GameMain.NetworkMember?.IsClient ?? false))
{
//empty the reactor
if (reactorContainer != null)
bool allowDisconnectedWires = true;
bool allowDamagedWalls = true;
if (BeaconStation.Info?.BeaconStationInfo is BeaconStationInfo info)
{
foreach (Item item in reactorContainer.Inventory.AllItems)
{
if (item.NonInteractable) { continue; }
Spawner.AddItemToRemoveQueue(item);
}
allowDisconnectedWires = info.AllowDisconnectedWires;
allowDamagedWalls = info.AllowDamagedWalls;
}
//remove wires
float removeWireMinDifficulty = 20.0f;
float removeWireProbability = MathUtils.InverseLerp(removeWireMinDifficulty, 100.0f, LevelData.Difficulty) * 0.5f;
if (removeWireProbability > 0.0f)
if (removeWireProbability > 0.0f && allowDisconnectedWires)
{
foreach (Item item in beaconItems.Where(it => it.GetComponent<Wire>() != null).ToList())
{
if (item.NonInteractable) { continue; }
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
Wire wire = item.GetComponent<Wire>();
if (wire.Locked) { continue; }
if (wire.Connections[0] != null && (wire.Connections[0].Item.NonInteractable || wire.Connections[0].Item.GetComponent<ConnectionPanel>().Locked))
@@ -4115,8 +4142,8 @@ namespace Barotrauma
connection.ConnectionPanel.DisconnectedWires.Add(wire);
wire.RemoveConnection(connection.Item);
#if SERVER
connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
wire.CreateNetworkEvent();
connection.ConnectionPanel.Item.CreateServerEvent(connection.ConnectionPanel);
wire.CreateNetworkEvent();
#endif
}
}
@@ -4124,23 +4151,25 @@ namespace Barotrauma
}
}
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
if (allowDamagedWalls)
{
if (item.NonInteractable) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
//break powered items
foreach (Item item in beaconItems.Where(it => it.Components.Any(c => c is Powered) && it.Components.Any(c => c is Repairable)))
{
item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
if (item.NonInteractable || item.InvulnerableToDamage) { continue; }
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.5f)
{
item.Condition *= Rand.Range(0.6f, 0.8f, Rand.RandSync.Unsynced);
}
}
}
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
{
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
//poke holes in the walls
foreach (Structure structure in Structure.WallList.Where(s => s.Submarine == BeaconStation))
{
int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
if (Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < 0.25f)
{
int sectionIndex = Rand.Range(0, structure.SectionCount - 1, Rand.RandSync.Unsynced);
structure.AddDamage(sectionIndex, Rand.Range(structure.MaxHealth * 0.2f, structure.MaxHealth, Rand.RandSync.Unsynced));
}
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
public readonly string Seed;
public float Difficulty;
public readonly float Difficulty;
public readonly Biome Biome;
@@ -432,7 +432,7 @@ namespace Barotrauma
if (sub != null)
{
bool leaveBehind = false;
if (!sub.DockedTo.Contains(Submarine.MainSub))
if (sub.Submarine != null && !sub.DockedTo.Contains(sub.Submarine))
{
System.Diagnostics.Debug.Assert(Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit);
if (Submarine.MainSub.AtEndExit)
@@ -78,7 +78,7 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved campaign map from XML
/// </summary>
private Map(CampaignMode campaign, XElement element, CampaignSettings settings) : this(settings)
private Map(CampaignMode campaign, XElement element) : this(campaign.Settings)
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
@@ -104,7 +104,7 @@ namespace Barotrauma
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement)
{
Enabled = settings.RadiationEnabled
Enabled = campaign.Settings.RadiationEnabled
};
break;
}
@@ -208,12 +208,12 @@ namespace Barotrauma
/// <summary>
/// Generate a new campaign map from the seed
/// </summary>
public Map(CampaignMode campaign, string seed, CampaignSettings settings) : this(settings)
public Map(CampaignMode campaign, string seed) : this(campaign.Settings)
{
Seed = seed;
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
Generate();
Generate(campaign.Settings);
if (Locations.Count == 0)
{
@@ -228,10 +228,7 @@ namespace Barotrauma
foreach (Location location in Locations)
{
if (location.Type.Identifier != "outpost") { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
SetStartLocation(location);
}
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
if (CurrentLocation == null)
@@ -239,25 +236,36 @@ namespace Barotrauma
foreach (Location location in Locations)
{
if (!location.Type.HasOutpost) { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
SetStartLocation(location);
}
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
if (StartLocation?.LevelData != null)
void SetStartLocation(Location location)
{
StartLocation.LevelData.Difficulty = 0;
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
}
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
foreach (var locationConnection in StartLocation.Connections)
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
int loops = campaign.CampaignMetadata.GetInt("campaign.endings".ToIdentifier(), 0);
if (loops == 0 && (campaign.Settings.Difficulty == GameDifficulty.Easy || campaign.Settings.Difficulty == GameDifficulty.Medium))
{
if (locationConnection.Difficulty > 0.0f)
if (StartLocation != null)
{
locationConnection.Difficulty = 0.0f;
locationConnection.LevelData = new LevelData(locationConnection);
StartLocation.LevelData = new LevelData(StartLocation, 0);
}
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
foreach (var locationConnection in StartLocation.Connections)
{
if (locationConnection.Difficulty > 0.0f)
{
locationConnection.Difficulty = 0.0f;
locationConnection.LevelData = new LevelData(locationConnection);
}
}
}
@@ -276,7 +284,7 @@ namespace Barotrauma
#region Generation
private void Generate()
private void Generate(CampaignSettings settings)
{
Connections.Clear();
Locations.Clear();
@@ -294,7 +302,6 @@ namespace Barotrauma
Voronoi voronoi = new Voronoi(0.5f);
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
float zoneWidth = Width / generationParams.DifficultyZones;
Vector2 margin = new Vector2(
Math.Min(10, Width * 0.1f),
@@ -310,6 +317,7 @@ namespace Barotrauma
voronoiSites.Clear();
Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
bool possibleStartOutpostCreated = false;
foreach (GraphEdge edge in edges)
{
if (edge.Point1 == edge.Point2) { continue; }
@@ -344,12 +352,26 @@ namespace Barotrauma
}
LocationType forceLocationType = null;
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
if (!possibleStartOutpostCreated)
{
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
float zoneWidth = Width / generationParams.DifficultyZones;
float threshold = zoneWidth * 0.1f;
if (position.X < threshold)
{
forceLocationType = locationType;
break;
LocationType.Prefabs.TryGet("outpost", out forceLocationType);
possibleStartOutpostCreated = true;
}
}
if (forceLocationType == null)
{
foreach (LocationType locationType in LocationType.Prefabs.OrderBy(lt => lt.Identifier))
{
if (locationType.MinCountPerZone.TryGetValue(zone, out int minCount) && locationsPerZone[zone].Count(l => l.Type == locationType) < minCount)
{
forceLocationType = locationType;
break;
}
}
}
@@ -455,9 +477,7 @@ namespace Barotrauma
if (zone1 == zone2) { continue; }
if (zone1 > zone2)
{
int temp = zone2;
zone2 = zone1;
zone1 = temp;
(zone1, zone2) = (zone2, zone1);
}
if (generationParams.GateCount[zone1] == 0) { continue; }
@@ -527,32 +547,43 @@ namespace Barotrauma
foreach (LocationConnection connection in Connections)
{
float difficulty = connection.CenterPos.X / Width * 100;
float minDifficulty = 0;
float maxDifficulty = 100;
var biome = connection.Biome;
if (biome != null)
if (connection.Locations.Any(l => l.IsGateBetweenBiomes))
{
minDifficulty = connection.Biome.MinDifficulty;
maxDifficulty = connection.Biome.MaxDifficulty;
if (connection.Locked)
{
connection.Difficulty = maxDifficulty;
}
connection.Difficulty = connection.Locations.Min(l => l.Biome.MaxDifficulty);
}
else
{
connection.Difficulty = CalculateDifficulty(connection.CenterPos.X, connection.Biome);
}
connection.Difficulty = MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
}
CreateEndLocation();
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f));
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
}
foreach (LocationConnection connection in Connections)
{
connection.LevelData = new LevelData(connection);
}
float CalculateDifficulty(float mapPosition, Biome biome)
{
float settingsFactor = settings.LevelDifficultyMultiplier;
float minDifficulty = 0;
float maxDifficulty = 100;
float difficulty = mapPosition / Width * 100;
System.Diagnostics.Debug.Assert(biome != null);
if (biome != null)
{
minDifficulty = biome.MinDifficulty;
maxDifficulty = biome.MaxDifficulty;
float diff = 1 - settingsFactor;
difficulty *= 1 - (1f / biome.AllowedZones.Max() * diff);
}
return MathHelper.Clamp(difficulty, minDifficulty, maxDifficulty);
}
}
partial void GenerateLocationConnectionVisuals();
@@ -633,6 +664,11 @@ namespace Barotrauma
if (EndLocation == null || previousToEndLocation == null) { return; }
if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
{
previousToEndLocation.ChangeType(locationType);
}
//remove all locations from the end biome except the end location
for (int i = Locations.Count - 1; i >= 0; i--)
{
@@ -652,7 +688,7 @@ namespace Barotrauma
}
//removed all connections from the second-to-last location, need to reconnect it
if (!previousToEndLocation.Connections.Any())
if (previousToEndLocation.Connections.None())
{
Location connectTo = Locations.First();
foreach (Location location in Locations)
@@ -759,6 +795,7 @@ namespace Barotrauma
CurrentLocation = Locations[index];
CurrentLocation.Discover();
CurrentLocation.CreateStores();
if (prevLocation != CurrentLocation)
{
var connection = CurrentLocation.Connections.Find(c => c.Locations.Contains(prevLocation));
@@ -766,10 +803,8 @@ namespace Barotrauma
{
connection.Passed = true;
}
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
CurrentLocation.CreateStores();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
}
public void SelectLocation(int index)
@@ -789,6 +824,7 @@ namespace Barotrauma
return;
}
Location prevSelected = SelectedLocation;
SelectedLocation = Locations[index];
var currentDisplayLocation = GameMain.GameSession?.Campaign?.GetCurrentDisplayLocation();
SelectedConnection =
@@ -798,7 +834,10 @@ namespace Barotrauma
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
if (prevSelected != SelectedLocation)
{
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
}
public void SelectLocation(Location location)
@@ -811,13 +850,17 @@ namespace Barotrauma
return;
}
Location prevSelected = SelectedLocation;
SelectedLocation = location;
SelectedConnection = Connections.Find(c => c.Locations.Contains(CurrentLocation) && c.Locations.Contains(SelectedLocation));
if (SelectedConnection?.Locked ?? false)
{
DebugConsole.ThrowError("A locked connection was selected - this should not be possible.\n" + Environment.StackTrace.CleanupStackTrace());
}
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
if (prevSelected != SelectedLocation)
{
OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection);
}
}
public void SelectMission(IEnumerable<int> missionIndices)
@@ -830,23 +873,24 @@ namespace Barotrauma
return;
}
CurrentLocation.SetSelectedMissionIndices(missionIndices);
foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
if (!missionIndices.SequenceEqual(GetSelectedMissionIndices()))
{
if (selectedMission.Locations[0] != CurrentLocation ||
selectedMission.Locations[1] != CurrentLocation)
CurrentLocation.SetSelectedMissionIndices(missionIndices);
foreach (Mission selectedMission in CurrentLocation.SelectedMissions.ToList())
{
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (selectedMission.Locations[1] != SelectedLocation)
if (selectedMission.Locations[0] != CurrentLocation ||
selectedMission.Locations[1] != CurrentLocation)
{
CurrentLocation.DeselectMission(selectedMission);
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (selectedMission.Locations[1] != SelectedLocation)
{
CurrentLocation.DeselectMission(selectedMission);
}
}
}
OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
}
OnMissionsSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMissions);
}
public void SelectRandomLocation(bool preferUndiscovered)
@@ -1070,9 +1114,9 @@ namespace Barotrauma
/// <summary>
/// Load a previously saved map from an xml element
/// </summary>
public static Map Load(CampaignMode campaign, XElement element, CampaignSettings settings)
public static Map Load(CampaignMode campaign, XElement element)
{
Map map = new Map(campaign, element, settings);
Map map = new Map(campaign, element);
map.LoadState(element, false);
#if CLIENT
map.DrawOffset = -map.CurrentLocation.MapPosition;
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class BeaconStationInfo : ISerializableEntity
{
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool AllowDamagedWalls { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool AllowDisconnectedWires { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes), Editable]
public float MinLevelDifficulty { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes), Editable]
public float MaxLevelDifficulty { get; set; }
public string Name { get; private set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public BeaconStationInfo(SubmarineInfo submarineInfo, XElement element)
{
Name = $"BeaconStationInfo ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public BeaconStationInfo(SubmarineInfo submarineInfo)
{
Name = $"BeaconStationInfo ({submarineInfo.Name})";
SerializableProperties = SerializableProperty.DeserializeProperties(this);
}
public BeaconStationInfo(BeaconStationInfo original)
{
Name = original.Name;
SerializableProperties = new Dictionary<Identifier, SerializableProperty>();
foreach (KeyValuePair<Identifier, SerializableProperty> kvp in original.SerializableProperties)
{
SerializableProperties.Add(kvp.Key, kvp.Value);
if (SerializableProperty.GetSupportedTypeName(kvp.Value.PropertyType) != null)
{
kvp.Value.TrySetValue(this, kvp.Value.GetValue(original));
}
}
}
public void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element);
}
}
}
@@ -830,43 +830,44 @@ namespace Barotrauma
private static SubmarineInfo GetRandomModule(OutpostModuleInfo prevModule, IEnumerable<SubmarineInfo> modules, Identifier moduleFlag, OutpostModuleInfo.GapPosition gapPosition, LocationType locationType, bool allowDifferentLocationType)
{
IEnumerable<SubmarineInfo> availableModules = null;
IEnumerable<SubmarineInfo> modulesWithCorrectFlags = null;
if (moduleFlag.IsEmpty || moduleFlag.Equals("none"))
{
availableModules = modules
modulesWithCorrectFlags = modules
.Where(m => !m.OutpostModuleInfo.ModuleFlags.Any() || (m.OutpostModuleInfo.ModuleFlags.Count() == 1 && m.OutpostModuleInfo.ModuleFlags.Contains("none".ToIdentifier())));
}
else
{
availableModules = modules
modulesWithCorrectFlags = modules
.Where(m => m.OutpostModuleInfo.ModuleFlags.Contains(moduleFlag));
}
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
availableModules = availableModules.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
if (prevModule != null)
var suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
if (!suitableModules.Any())
{
availableModules = availableModules.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));// && CanAttachTo(prevModule, m.OutpostModuleInfo));
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
//still not found, see if we can find something that's otherwise suitable but not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
}
//still not found! Try if we can find a generic module that's not meant to attach to the previous module
if (!suitableModules.Any())
{
suitableModules = GetSuitable(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
}
}
if (availableModules.Count() == 0) { return null; }
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
//if not found, search for modules suitable for any location type
if (allowDifferentLocationType && !modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
if (!suitableModules.Any())
{
if (allowDifferentLocationType)
{
if (modulesWithCorrectFlags.Any())
DebugConsole.NewMessage($"Could not find a suitable module for the location type {locationType}. Module flag: {moduleFlag}.", Color.Orange);
return ToolBox.SelectWeightedRandom(availableModules.ToList(), availableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
return ToolBox.SelectWeightedRandom(modulesWithCorrectFlags.ToList(), modulesWithCorrectFlags.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
else
{
@@ -875,7 +876,28 @@ namespace Barotrauma
}
else
{
return ToolBox.SelectWeightedRandom(modulesSuitableForLocationType.ToList(), modulesSuitableForLocationType.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
return ToolBox.SelectWeightedRandom(suitableModules.ToList(), suitableModules.Select(m => m.OutpostModuleInfo.Commonness).ToList(), Rand.RandSync.ServerAndClient);
}
IEnumerable<SubmarineInfo> GetSuitable(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
{
IEnumerable<SubmarineInfo> suitable = modules;
if (requireCorrectLocationType)
{
if (disallowNonLocationTypeSpecific)
{
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
}
else
{
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier) || !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
}
if (requireAllowAttachToPrevious && prevModule != null)
{
suitable = suitable.Where(m => CanAttachTo(m.OutpostModuleInfo, prevModule));
}
return suitable;
}
}
@@ -1590,10 +1612,6 @@ namespace Barotrauma
{
npc.CharacterHealth.Unkillable = true;
}
else
{
npc.AddStaticHealthMultiplier(humanPrefab.HealthMultiplier);
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.ServerAndClient);
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
@@ -1483,8 +1483,10 @@ namespace Barotrauma
{
if (item.Submarine != this) continue;
if (item.ParentInventory != null || item.body != null) continue;
var lightComponent = item.GetComponent<Items.Components.LightComponent>();
if (lightComponent != null) lightComponent.LightColor = new Color(lightComponent.LightColor, lightComponent.LightColor.A / 255.0f * 0.5f);
foreach (var light in item.GetComponents<LightComponent>())
{
light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f);
}
}
}
GenerateOutdoorNodes();
@@ -1555,7 +1557,7 @@ namespace Barotrauma
element.Add(new XAttribute("cargocapacity", cargoCapacity));
element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin));
element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? ""));
element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience.ToString()));
element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages)));
if (Info.Type == SubmarineType.OutpostModule)
@@ -1632,6 +1634,7 @@ namespace Barotrauma
Type = Info.Type,
FilePath = filePath,
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
BeaconStationInfo = Info.BeaconStationInfo != null ? new BeaconStationInfo(Info.BeaconStationInfo) : null,
Name = Path.GetFileNameWithoutExtension(filePath)
};
#if CLIENT
@@ -39,7 +39,15 @@ namespace Barotrauma
public SubmarineTag Tags { get; private set; }
public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2;
public string RecommendedCrewExperience;
public enum CrewExperienceLevel
{
Unknown,
CrewExperienceLow,
CrewExperienceMid,
CrewExperienceHigh
}
public CrewExperienceLevel RecommendedCrewExperience;
/// <summary>
/// A random int that gets assigned when saving the sub. Used in mp campaign to verify that sub files match
@@ -89,6 +97,7 @@ namespace Barotrauma
public SubmarineClass SubmarineClass;
public OutpostModuleInfo OutpostModuleInfo { get; set; }
public BeaconStationInfo BeaconStationInfo { get; set; }
public bool IsOutpost => Type == SubmarineType.Outpost || Type == SubmarineType.OutpostModule;
@@ -280,6 +289,10 @@ namespace Barotrauma
{
OutpostModuleInfo = new OutpostModuleInfo(original.OutpostModuleInfo);
}
if (original.BeaconStationInfo != null)
{
BeaconStationInfo = new BeaconStationInfo(original.BeaconStationInfo);
}
#if CLIENT
PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage) : null;
#endif
@@ -330,7 +343,24 @@ namespace Barotrauma
CargoCapacity = SubmarineElement.GetAttributeInt("cargocapacity", -1);
RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0);
RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0);
RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown");
var recommendedCrewExperience = SubmarineElement.GetAttributeIdentifier("recommendedcrewexperience", CrewExperienceLevel.Unknown.ToIdentifier());
// Backwards compatibility
if (recommendedCrewExperience == "Beginner")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceLow;
}
else if (recommendedCrewExperience == "Intermediate")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceMid;
}
else if (recommendedCrewExperience == "Experienced")
{
RecommendedCrewExperience = CrewExperienceLevel.CrewExperienceHigh;
}
else
{
Enum.TryParse(recommendedCrewExperience.Value, ignoreCase: true, out RecommendedCrewExperience);
}
if (SubmarineElement?.Attribute("type") != null)
{
@@ -341,6 +371,10 @@ namespace Barotrauma
{
OutpostModuleInfo = new OutpostModuleInfo(this, SubmarineElement);
}
else if (Type == SubmarineType.BeaconStation)
{
BeaconStationInfo = new BeaconStationInfo(this, SubmarineElement);
}
}
}
@@ -359,20 +393,6 @@ namespace Barotrauma
SubmarineClass = SubmarineClass.Undefined;
}
//backwards compatibility (use text tags instead of the actual text)
if (RecommendedCrewExperience == "Beginner")
{
RecommendedCrewExperience = "CrewExperienceLow";
}
else if (RecommendedCrewExperience == "Intermediate")
{
RecommendedCrewExperience = "CrewExperienceMid";
}
else if (RecommendedCrewExperience == "Experienced")
{
RecommendedCrewExperience = "CrewExperienceHigh";
}
RequiredContentPackages.Clear();
string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", Array.Empty<string>());
foreach (string contentPackageName in contentPackageNames)
@@ -528,6 +548,11 @@ namespace Barotrauma
OutpostModuleInfo.Save(newElement);
OutpostModuleInfo = new OutpostModuleInfo(this, newElement);
}
else if (Type == SubmarineType.BeaconStation)
{
BeaconStationInfo.Save(newElement);
BeaconStationInfo = new BeaconStationInfo(this, newElement);
}
XDocument doc = new XDocument(newElement);
doc.Root.Add(new XAttribute("name", Name));
@@ -590,6 +615,7 @@ namespace Barotrauma
List<string> filePaths = new List<string>();
foreach (BaseSubFile subFile in contentPackageSubs)
{
if (!File.Exists(subFile.Path.Value)) { continue; }
if (!filePaths.Any(fp => fp == subFile.Path))
{
filePaths.Add(subFile.Path.Value);
@@ -158,7 +158,7 @@ namespace Barotrauma.Networking
private static void UpdateRead()
{
Span<byte> msgLengthSpan = stackalloc byte[3];
Span<byte> msgLengthSpan = stackalloc byte[4 + 1];
while (!shutDown)
{
CheckPipeConnected(nameof(readStream), readStream);
@@ -182,8 +182,11 @@ namespace Barotrauma.Networking
if (!readBytes(msgLengthSpan)) { shutDown = true; break; }
int msgLength = msgLengthSpan[0] | (msgLengthSpan[1] << 8);
WriteStatus writeStatus = (WriteStatus)msgLengthSpan[2];
int msgLength = msgLengthSpan[0]
| (msgLengthSpan[1] << 8)
| (msgLengthSpan[2] << 16)
| (msgLengthSpan[3] << 24);
WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4];
if (msgLength > 0)
{
@@ -225,12 +228,15 @@ namespace Barotrauma.Networking
// when the function returns; placing it in the loop
// this method is based around would lead to a stack
// overflow real quick!
Span<byte> bytesToWrite = stackalloc byte[3 + msg.Length];
Span<byte> bytesToWrite = stackalloc byte[4 + 1 + msg.Length];
bytesToWrite[0] = (byte)(msg.Length & 0xFF);
bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF);
bytesToWrite[2] = (byte)writeStatus;
Span<byte> msgSlice = bytesToWrite.Slice(3, msg.Length);
bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF);
bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF);
bytesToWrite[4] = (byte)writeStatus;
Span<byte> msgSlice = bytesToWrite.Slice(4 + 1, msg.Length);
msg.AsSpan().CopyTo(msgSlice);
@@ -284,6 +290,12 @@ namespace Barotrauma.Networking
{
if (shutDown) { return; }
if (msg.Length > 0x1fff_ffff)
{
//This message is extremely long and is close to breaking
//ChildServerRelay, so let's not allow this to go through!
return;
}
msgsToWrite.Enqueue(msg);
writeManualResetEvent.Set();
}
@@ -28,6 +28,8 @@ namespace Barotrauma.Networking
REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization
UPDATE_CHARACTERINFO,
ERROR, //tell the server that an error occurred
CREW, //hiring UI
MEDICAL, //medical clinic
@@ -900,27 +900,13 @@ namespace Barotrauma.Networking
private set;
}
[Serialize(true, IsPropertySaveable.Yes)]
public bool RadiationEnabled
{
get;
set;
}
[Serialize(LootedMoneyDestination.Bank, IsPropertySaveable.Yes)]
public LootedMoneyDestination LootedMoneyDestination { get; set; }
[Serialize(999999, IsPropertySaveable.Yes)]
public int MaximumMoneyTransferRequest { get; set; }
private int maxMissionCount = CampaignSettings.DefaultMaxMissionCount;
[Serialize(CampaignSettings.DefaultMaxMissionCount, IsPropertySaveable.Yes)]
public int MaxMissionCount
{
get { return maxMissionCount; }
set { maxMissionCount = MathHelper.Clamp(value, CampaignSettings.MinMissionCountLimit, CampaignSettings.MaxMissionCountLimit); }
}
public CampaignSettings CampaignSettings { get; set; } = CampaignSettings.Empty;
private bool allowSubVoting;
//Don't serialize: the value is set based on SubSelectionMode
@@ -786,6 +786,9 @@ namespace Barotrauma
case nameof(Character.HealthMultiplier):
{ if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } }
break;
case nameof(Character.PropulsionSpeedMultiplier):
{ if (parentObject is Character character) { character.PropulsionSpeedMultiplier = value; return true; } }
break;
}
return false;
}
@@ -799,6 +802,12 @@ namespace Barotrauma
case nameof(Character.ObstructVision):
{ if (parentObject is Character character) { character.ObstructVision = value; return true; } }
break;
case nameof(Character.HideFace):
{ if (parentObject is Character character) { character.HideFace = value; return true; } }
break;
case nameof(Character.UseHullOxygen):
{ if (parentObject is Character character) { character.UseHullOxygen = value; return true; } }
break;
case nameof(LightComponent.IsOn):
{ if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } }
break;
@@ -490,6 +490,10 @@ namespace Barotrauma
{
font.Prefabs.ForEach(p => p.LoadFont());
}
foreach (var componentStyle in GUIStyle.ComponentStyles)
{
componentStyle.RefreshSize();
}
}
GameMain.SoundManager?.ApplySettings();
@@ -94,7 +94,13 @@ namespace Barotrauma
public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{
if (this.type != type || !HasRequiredItems(entity)) { return; }
if (this.type != type) { return; }
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
if (!HasRequiredItems(entity)) { return; }
if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; }
if (!Stackable)
{
@@ -132,7 +132,11 @@ namespace Barotrauma
public enum SpawnPositionType
{
This,
//the inventory of the StatusEffect's target entity
ThisInventory,
//the same inventory the StatusEffect's target entity is in (only valid if the target is an Item)
SameInventory,
//the inventory of an item in the inventory of the StatusEffect's target entity (e.g. a container in the character's inventory)
ContainedInventory
}
@@ -308,11 +312,26 @@ namespace Barotrauma
private readonly float lifeTime;
private float lifeTimer;
public float intervalTimer;
public static readonly List<DurationListElement> DurationList = new List<DurationListElement>();
public readonly bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply.
/// <summary>
/// Always do the conditional checks for the duration/delay. If false, only check conditional on apply.
/// </summary>
public readonly bool CheckConditionalAlways;
public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets?
/// <summary>
/// Only valid if the effect has a duration or delay. Can the effect be applied on the same target(s)s if the effect is already being applied?
/// </summary>
public readonly bool Stackable = true;
/// <summary>
/// The interval at which the effect is executed. The difference between delay and interval is that effects with a delay find the targets, check the conditions, etc
/// immediately when Apply is called, but don't apply the effects until the delay has passed. Effects with an interval check if the interval has passed when Apply is
/// called and apply the effects if it has, otherwise they do nothing.
/// </summary>
public readonly float Interval;
#if CLIENT
private readonly bool playSoundOnRequiredItemFailure = false;
@@ -450,6 +469,8 @@ namespace Barotrauma
TargetSlot = element.GetAttributeInt("targetslot", -1);
Interval = element.GetAttributeFloat("interval", 0.0f);
Range = element.GetAttributeFloat("range", 0.0f);
Offset = element.GetAttributeVector2("offset", Vector2.Zero);
string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null);
@@ -556,6 +577,7 @@ namespace Barotrauma
" - sounds should be defined as child elements of the StatusEffect, not as attributes.");
break;
case "delay":
case "interval":
break;
case "range":
if (!HasTargetType(TargetType.NearbyCharacters) && !HasTargetType(TargetType.NearbyItems))
@@ -1094,6 +1116,12 @@ namespace Barotrauma
{
if (this.type != type) { return; }
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
currentTargets.Clear();
foreach (ISerializableEntity target in targets)
{
@@ -1195,7 +1223,11 @@ namespace Barotrauma
lifeTimer -= deltaTime;
if (lifeTimer <= 0) { return; }
}
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
Hull hull = GetHull(entity);
Vector2 position = GetPosition(entity, targets, worldPosition);
if (useItemCount > 0)
@@ -1717,6 +1749,26 @@ namespace Barotrauma
}
}
break;
case ItemSpawnInfo.SpawnPositionType.SameInventory:
{
Inventory inventory = null;
if (entity is Character character)
{
inventory = character.Inventory;
}
else if (entity is Item item)
{
inventory = item.ParentInventory;
}
if (inventory != null)
{
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, inventory, spawnIfInventoryFull: chosenItemSpawnInfo.SpawnIfInventoryFull, onSpawned: (Item newItem) =>
{
newItem.Condition = newItem.MaxCondition * chosenItemSpawnInfo.Condition;
});
}
}
break;
case ItemSpawnInfo.SpawnPositionType.ContainedInventory:
{
Inventory thisInventory = null;
@@ -1756,6 +1808,8 @@ namespace Barotrauma
ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true);
intervalTimer = Interval;
static Character CharacterFromTarget(ISerializableEntity target)
{
Character targetCharacter = target as Character;
@@ -414,7 +414,7 @@ namespace Barotrauma.Steam
{
await Task.Yield();
Identifier extension = Path.GetExtension(from).ToIdentifier();
if (extension == ".xml" && shouldCorrectPaths == ShouldCorrectPaths.Yes)
if (extension == ".xml")
{
try
{
@@ -427,10 +427,14 @@ namespace Barotrauma.Steam
{
throw new Exception($"Could not load \"{from}\": doc is null");
}
await CorrectPaths(
fileListDir: fileListDir,
modName: modName,
element: doc.Root ?? throw new NullReferenceException());
if (shouldCorrectPaths == ShouldCorrectPaths.Yes)
{
await CorrectPaths(
fileListDir: fileListDir,
modName: modName,
element: doc.Root ?? throw new NullReferenceException());
}
doc.SaveSafe(to);
return;
}
@@ -15,6 +15,21 @@ namespace Barotrauma
public bool IsNone() => this is None<T>;
public bool IsSome() => this is Some<T>;
public bool TryUnwrap(out T outValue)
{
switch (this)
{
case Some<T> { Value: var value }:
outValue = value;
return true;
case None<T> _:
outValue = default;
return false;
default:
throw new ArgumentOutOfRangeException();
}
}
public Option<TType> Select<TType>(Func<T, TType> selector) =>
this switch
{
@@ -11,8 +11,8 @@ namespace Barotrauma
public static Success<T, TError> Success(T value)
=> new Success<T, TError>(value);
public static Failure<T, TError> Failure(TError error, string? stackTrace)
=> new Failure<T, TError>(error, stackTrace);
public static Failure<T, TError> Failure(TError error)
=> new Failure<T, TError>(error);
}
public sealed class Success<T, TError> : Result<T, TError>
@@ -34,14 +34,11 @@ namespace Barotrauma
{
public readonly TError Error;
public readonly string? StackTrace;
public override bool IsSuccess => false;
public Failure(TError error, string? stackTrace)
public Failure(TError error)
{
Error = error;
StackTrace = stackTrace;
}
}
}
@@ -18,6 +18,23 @@ namespace Barotrauma.IO
".bat", ".sh", //shell scripts
}.ToIdentifiers().ToImmutableArray();
public ref struct Skipper
{
public void Dispose()
{
SkipValidationInDebugBuilds = false;
}
}
/// <summary>
/// Skips validation for as long as the returned object remains in scope (remember to use using)
/// </summary>
public static Skipper SkipInDebugBuilds()
{
SkipValidationInDebugBuilds = true;
return new Skipper();
}
/// <summary>
/// When set to true, the game is allowed to modify the vanilla content in debug builds. Has no effect in non-debug builds.
/// </summary>
@@ -126,6 +126,10 @@ namespace Barotrauma
public static void LoadGame(string filePath)
{
//ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub)
//can happen if a gamesession is interrupted ungracefully (exception during loading)
Submarine.Unload();
GameMain.GameSession = null;
DebugConsole.Log("Loading save file: " + filePath);
DecompressToDirectory(filePath, TempPath, null);