Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -16,7 +16,7 @@ namespace Barotrauma
static class AchievementManager
{
private static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create(
public static readonly ImmutableHashSet<Identifier> SupportedAchievements = ImmutableHashSet.Create(
"killmoloch".ToIdentifier(),
"killhammerhead".ToIdentifier(),
"killendworm".ToIdentifier(),
@@ -146,6 +146,9 @@ namespace Barotrauma
public static void OnStartRound(Biome biome = null)
{
#if CLIENT
SteamTimelineManager.OnRoundStarted();
#endif
roundData = new RoundData();
foreach (Item item in Item.ItemList)
{
@@ -161,6 +164,7 @@ namespace Barotrauma
if (GameMain.Client != null) { return; }
#endif
if (biome != null && GameMain.GameSession?.GameMode is CampaignMode)
{
string shortBiomeIdentifier = biome.Identifier.Value.Replace(" ", "");
@@ -233,8 +237,8 @@ namespace Barotrauma
//convert submarine velocity to km/h
Vector2 submarineVel = Physics.DisplayToRealWorldRatio * ConvertUnits.ToDisplayUnits(sub.Velocity) * 3.6f;
//achievement for going > 100 km/h
if (Math.Abs(submarineVel.X) > 100.0f)
//achievement for going > 50 km/h
if (Math.Abs(submarineVel.X) > 50.0f)
{
//all conscious characters inside the sub get an achievement
UnlockAchievement("subhighvelocity".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
@@ -279,7 +283,7 @@ namespace Barotrauma
if (c == null || c.Removed) { return; }
if (c.HasEquippedItem("clownmask".ToIdentifier()) &&
c.HasEquippedItem("clowncostume".ToIdentifier()))
c.HasEquippedItem("clowngear".ToIdentifier()))
{
UnlockAchievement(c, "clowncostume".ToIdentifier());
}
@@ -407,9 +411,33 @@ namespace Barotrauma
UnlockAchievement(reviver, "healcrit".ToIdentifier());
}
#if CLIENT
private static void CheckSteamTimelineEvents(Character killedCharacter, CauseOfDeath causeOfDeath)
{
if (killedCharacter == Character.Controlled)
{
SteamTimelineManager.OnPlayerDied(killedCharacter, causeOfDeath);
return;
}
bool pvpkill = killedCharacter.IsHuman && GameMain.GameSession?.GameMode is PvPMode;
float combatStrength = killedCharacter.Params.AI?.CombatStrength ?? 0;
bool significantCombatStrength = combatStrength >= 300;
if (pvpkill || significantCombatStrength)
{
// note: sometimes the causeOfDeath.Killer is null in multiplayer
SteamTimelineManager.OnSignificantEnemyDied(killedCharacter, causeOfDeath);
}
}
#endif
public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath)
{
#if CLIENT
CheckSteamTimelineEvents(character, causeOfDeath);
// If this is a multiplayer game, the client should let the server handle achievements
if (GameMain.Client != null || GameMain.GameSession == null) { return; }
@@ -417,40 +445,41 @@ namespace Barotrauma
causeOfDeath.Killer != null &&
causeOfDeath.Killer == Character.Controlled)
{
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled, 1);
}
#elif SERVER
if (character != causeOfDeath.Killer && causeOfDeath.Killer != null)
{
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled , 1);
IncrementStat(causeOfDeath.Killer, character.IsHuman ? AchievementStat.HumansKilled : AchievementStat.MonstersKilled, 1);
}
#endif
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName}".ToIdentifier());
if (character.CurrentHull != null)
{
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName}indoors".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName}indoors".ToIdentifier());
}
if (character.SpeciesName.EndsWith("boss"))
{
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("boss", "")}".ToIdentifier());
if (character.CurrentHull != null)
{
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("boss", "")}indoors".ToIdentifier());
}
}
if (character.SpeciesName.EndsWith("_m"))
{
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("_m", "")}".ToIdentifier());
if (character.CurrentHull != null)
{
UnlockAchievement(causeOfDeath.Killer, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
UnlockKillAchievement(causeOfDeath.Killer, character, $"kill{character.SpeciesName.Replace("_m", "")}indoors".ToIdentifier());
}
}
#if SERVER
if (character.SpeciesName == "Jove" &&
GameMain.GameSession.Campaign is MultiPlayerCampaign &&
GameMain.Server?.ServerSettings is { IronmanModeActive: true })
(GameMain.Server?.ServerSettings is { IronmanModeActive: true } or { RespawnMode: RespawnMode.Permadeath }))
{
UnlockAchievement(
identifier: "europasfinest".ToIdentifier(),
@@ -464,8 +493,12 @@ namespace Barotrauma
causeOfDeath.Killer != character)
{
UnlockAchievement(causeOfDeath.Killer, "killclown".ToIdentifier());
if (character.CharacterHealth?.GetAffliction("psychosis") != null)
{
UnlockAchievement(causeOfDeath.Killer, "whatsmirksbelow".ToIdentifier());
}
}
if (character.CharacterHealth?.GetAffliction("psychoclown") != null &&
character.CurrentHull?.Submarine.Info is { Type: SubmarineType.BeaconStation })
{
@@ -516,6 +549,20 @@ namespace Barotrauma
#endif
}
private static void UnlockKillAchievement(Character killer, Character target, Identifier identifier)
{
if (killer != null &&
target.Params.UnlockKillAchievementForWholeCrew &&
GameSession.GetSessionCrewCharacters(CharacterType.Player).Contains(killer))
{
UnlockAchievement(identifier, unlockClients: true, characterConditions: c => c != null);
}
else
{
UnlockAchievement(killer, identifier);
}
}
public static void OnTraitorWin(Character character)
{
#if CLIENT
@@ -525,9 +572,15 @@ namespace Barotrauma
UnlockAchievement(character, "traitorwin".ToIdentifier());
}
public static void OnRoundEnded(GameSession gameSession)
public static void OnRoundEnded(GameSession gameSession, bool roundInterrupted = false)
{
#if CLIENT
SteamTimelineManager.OnRoundEnded();
#endif
if (CheatsEnabled) { return; }
// no processing for achievements if player quit to menu or such.
if (roundInterrupted) { return; }
//made it to the destination
if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit)
@@ -713,7 +766,9 @@ namespace Barotrauma
private static void UnlockAchievementsOnPlatforms(Identifier identifier)
{
if (unlockedAchievements.Contains(identifier)) { return; }
DebugConsole.NewMessage($"Attempting to unlock achievement {identifier}...");
if (SteamManager.IsInitialized)
{
if (SteamManager.UnlockAchievement(identifier))
@@ -4396,11 +4396,11 @@ namespace Barotrauma
else if (canAttackDoors && HasValidPath())
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door is { CanBeTraversed: false } && !door.HasAccess(Character))
if (door is { CanBeTraversed: false } && !door.HasAccess(Character) && door.Item.AiTarget is { } doorAiTarget)
{
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
if (SelectedAiTarget != doorAiTarget || State != AIState.Attack)
{
SelectTarget(door.Item.AiTarget, CurrentTargetMemory.Priority);
SelectTarget(doorAiTarget, CurrentTargetMemory.Priority);
State = AIState.Attack;
return false;
}
@@ -787,6 +787,13 @@ namespace Barotrauma
foreach (Item item in Character.HeldItems)
{
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.UnequipAutomatically) { continue; }
//NPC set to operate the item they're holding, don't put it away
if (ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateItem &&
(operateItem.OperateTarget == item || operateItem.Component?.Item == item))
{
continue;
}
if (Character.TryPutItemInAnySlot(item)) { continue; }
if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon))
@@ -69,6 +69,7 @@ namespace Barotrauma
}
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
IsFindDivingGearSubObjective = true,
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -49,6 +49,13 @@ namespace Barotrauma
public const float DefaultReach = 100;
public const float MaxReach = 150;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
@@ -378,6 +385,7 @@ namespace Barotrauma
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
IsFindDivingGearSubObjective = IsFindDivingGearSubObjective,
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character),
SpeakIfFails = false,
@@ -14,6 +14,13 @@ namespace Barotrauma
public override bool KeepDivingGearOn => GetTargetHull() == null;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
@@ -379,85 +386,89 @@ namespace Barotrauma
}
}
if (Abandon) { return; }
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
if (!IsFindDivingGearSubObjective)
{
if (HumanAIController.HasDivingSuit(followTarget))
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
if (HumanAIController.HasDivingSuit(followTarget))
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
tryToGetDivingGear = true;
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (!getDivingGearIfNeeded)
{
if (needsEquipment)
else if (tryToGetDivingGear)
{
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
}
else
{
if (character.LockHands)
if (!getDivingGearIfNeeded)
{
cantFindDivingGear = true;
if (needsEquipment)
{
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
}
}
if (cantFindDivingGear && needsDivingSuit)
else
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit (or mask)
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit (or mask)
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
}
}
if (IsDoneFollowing())
{
OnCompleted();
@@ -347,7 +347,10 @@ namespace Barotrauma
useItemTimer = 0.05f;
StartUsingItem();
if (!allowMovement)
//make the character move towards the item they're using...
if (!allowMovement &&
//...except if they've selected an item that controls the character's direction (e.g. a periscope)
character.SelectedSecondaryItem?.GetComponent<Controller>() is not { Direction: Direction.Left or Direction.Right })
{
TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -4,7 +4,11 @@ namespace Barotrauma
{
enum CauseOfDeathType
{
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected,
/// <summary>
/// Special cause of death type returned by <see cref="Character.CauseOfDeathType"/> when the character is not dead.
/// </summary>
None
}
class CauseOfDeath
@@ -589,6 +589,9 @@ namespace Barotrauma
public Identifier VariantOf => Prefab.VariantOf;
/// <summary>
/// Non-localized name of the character (for characters with info, their name, for monsters, their species). E.g. "Mudraptor_veteran", "John Smith".
/// </summary>
public string Name
{
get
@@ -597,6 +600,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Localized display name of the character (e.g. "Mudraptor Veteran", "John Smith") - this should generally be used in any the player sees.
/// </summary>
public string DisplayName
{
get
@@ -1207,6 +1213,11 @@ namespace Barotrauma
private set;
}
/// <summary>
/// Can be used by mods to check the cause of death of the character using conditionals (e.g. if some <see cref="OnDeath"/> effects should or should not be triggered by certain causes of death).
/// </summary>
public CauseOfDeathType CauseOfDeathType => CauseOfDeath?.Type ?? CauseOfDeathType.None;
//can other characters select (= grab) this character
public bool CanBeSelected
{
@@ -2406,7 +2417,7 @@ namespace Barotrauma
{
if (!CanInteractWith(item)) { continue; }
if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item))
if (SelectedItem?.OwnInventory != null && !SelectedItem.OwnInventory.Locked && SelectedItem.OwnInventory.CanBePut(item))
{
SelectedItem.OwnInventory.TryPutItem(item, this);
}
@@ -5703,6 +5714,7 @@ namespace Barotrauma
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
if (GameMain.GameSession != null && GameMain.GameSession.UnlockedRecipes.Contains(recipeIdentifier)) { return true; }
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
@@ -408,6 +408,11 @@ namespace Barotrauma
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
/// <summary>
/// Which of the character's extra talents (talents unlocked from outside their own talent tree) are reset when the talents are resetted using e.g. Mindwipe?
/// </summary>
public HashSet<Identifier> ResettableExtraTalents { get; private set; } = new HashSet<Identifier>();
private int talentResetCount;
/// <summary>
@@ -792,14 +797,6 @@ namespace Barotrauma
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0);
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
}
private void SetPersonalityTrait()
@@ -1016,10 +1013,22 @@ namespace Barotrauma
}
UnlockedTalents.Add(talentIdentifier);
if (talentElement.GetAttributeBool("resettable", defaultValue: false))
{
ResettableExtraTalents.Add(talentIdentifier);
}
}
}
}
TalentRefundPoints = infoElement.GetAttributeInt("refundpoints", 0);
int loadedLastRewardDistribution = infoElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
LoadHeadAttachments();
}
@@ -1535,7 +1544,13 @@ namespace Barotrauma
if (TalentRefundPoints <= 0) { return; }
//e.g. talents from endocrine booster or extra talents some special NPC has
//stored in a list so we can re-unlock them on the character
var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList();
//remove resettable talents, so they DON'T get re-unlocked
foreach (var resettableExtraTalent in ResettableExtraTalents)
{
talentsFromOutsideTree.Remove(resettableExtraTalent);
}
UnlockedTalents.Clear();
SavedStatValues.Clear();
@@ -1570,6 +1585,9 @@ namespace Barotrauma
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
newName = Networking.Client.SanitizeName(newName);
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
@@ -1673,7 +1691,10 @@ namespace Barotrauma
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
talentElement.Add(
new XElement("Talent",
new XAttribute("identifier", talentIdentifier),
new XAttribute("resettable", ResettableExtraTalents.Contains(talentIdentifier))));
}
charElement.Add(savedStatElement);
@@ -882,6 +882,16 @@ namespace Barotrauma
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
/// <summary>
/// The speed of the affliction overlay animation.
/// Only applicable with AfflictionOverlayAnimated, and the overlay has to be a spritesheet so there's something to animate.
/// </summary>
public float AfflictionOverlayAnimSpeed
{
get;
set;
}
public ImmutableDictionary<Identifier, float> TreatmentSuitabilities
{
@@ -1005,6 +1015,10 @@ namespace Barotrauma
case "afflictionoverlay":
AfflictionOverlay = new Sprite(subElement);
break;
case "afflictionoverlayanimated":
AfflictionOverlay = new SpriteSheet(subElement);
AfflictionOverlayAnimSpeed = subElement.GetAttributeFloat("animspeed", 1.0f);
break;
case "statvalue":
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.",
contentPackage: element.ContentPackage);
@@ -302,7 +302,7 @@ namespace Barotrauma
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
item.UnequipAutomatically = false;
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
@@ -191,6 +191,7 @@ namespace Barotrauma
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
item.UnequipAutomatically = false;
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
@@ -308,6 +308,9 @@ namespace Barotrauma
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
/// <summary>
/// Is the limb the waist, a part of a leg or a tail?
/// </summary>
public bool IsLowerBody
{
get
@@ -330,22 +333,33 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the limb a leg or a part of a leg (upper or lower leg or foot)
/// </summary>
public bool IsLeg
{
get
{
switch (type)
return type switch
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
return true;
default:
return false;
}
LimbType.LeftFoot or LimbType.LeftLeg or LimbType.LeftThigh or LimbType.RightFoot or LimbType.RightLeg or LimbType.RightThigh => true,
_ => false,
};
}
}
/// <summary>
/// Is the limb an arm or a part of an arm (upper or lower arm or hand)
/// </summary>
public bool IsArm
{
get
{
return type switch
{
LimbType.LeftArm or LimbType.LeftForearm or LimbType.LeftHand or LimbType.RightArm or LimbType.RightForearm or LimbType.RightHand => true,
_ => false,
};
}
}
@@ -156,6 +156,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The multiplier of the minimum distance required between this character and the player/submarine before the music starts playing. The default distance is twice the length of the submarine, or a minimum of 50 meters."), Editable]
public float MusicRangeMultiplier { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the entire crew get an achievement (assuming there is one) if someone from the crew kills the character?")]
public bool UnlockKillAchievementForWholeCrew { get; set; }
public readonly CharacterFile File;
public bool IsPet => AI?.IsPet ?? false;
@@ -69,9 +69,9 @@ namespace Barotrauma.Abilities
switch (targetType)
{
case TargetType.Enemy:
return !HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: false);
return !HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.Ally:
return HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: true);
return HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.NotSelf:
return targetCharacter != character;
case TargetType.Alive:
@@ -84,5 +84,6 @@ namespace Barotrauma.Abilities
return true;
}
}
}
}
@@ -65,7 +65,7 @@ namespace Barotrauma.Abilities
}
if (!nearbyCharactersAppliesToAllies)
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character, onlySameTeam: true));
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
@@ -3,7 +3,7 @@
class CharacterAbilityPsychoClown : CharacterAbility
{
private readonly StatTypes statType;
private readonly float maxValue;
private readonly float minValue, maxValue;
private readonly string afflictionIdentifier;
private float lastValue = 0f;
public override bool AllowClientSimulation => true;
@@ -11,8 +11,9 @@
public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
maxValue = abilityElement.GetAttributeFloat(nameof(maxValue), 0f);
minValue = abilityElement.GetAttributeFloat(nameof(minValue), 0f);
afflictionIdentifier = abilityElement.GetAttributeString(nameof(afflictionIdentifier), "");
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -31,7 +32,7 @@
afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength;
}
lastValue = afflictionStrength * maxValue;
lastValue = minValue + afflictionStrength * (maxValue - minValue);
Character.ChangeStat(statType, lastValue);
}
else
@@ -67,6 +67,7 @@ namespace Barotrauma.Abilities
{
if (Character.HasTalent(identifier)) { continue; }
Character.GiveTalent(identifier);
Character.Info.ResettableExtraTalents.Add(identifier);
}
}
@@ -73,10 +73,12 @@ namespace Barotrauma
: TextManager.Get(value).Fallback(value);
conn.SetLabel(newLabel, this);
conn.Connection.DisplayNameOverride = string.IsNullOrWhiteSpace(value) ? null : newLabel;
}
else
{
conn.SetLabel(conn.Connection.DisplayName, this);
conn.Connection.DisplayNameOverride = null;
}
}
#endif
@@ -21,7 +21,7 @@ namespace Barotrauma
{
TextManager.TextPacks.TryAdd(language, ImmutableList<TextPack>.Empty);
}
var newPack = new TextPack(this, mainElement, language);
var newPack = new TextPack(this, mainElement, language, load: language == GameSettings.CurrentConfig.Language);
var newList = TextManager.TextPacks[language].Add(newPack);
TextManager.TextPacks.TryRemove(language, out _);
TextManager.TextPacks.TryAdd(language, newList);
@@ -76,10 +76,12 @@ namespace Barotrauma
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
public ContentPath? GetAttributeContentPath(string key) => Element.GetAttributeContentPath(key, ContentPackage);
public int? GetAttributeNullableInt(string key) => Element.GetAttributeNullableInt(key);
public int GetAttributeInt(string key, int def) => Element.GetAttributeInt(key, def);
public ushort GetAttributeUInt16(string key, ushort def) => Element.GetAttributeUInt16(key, def);
public int[]? GetAttributeIntArray(string key, int[]? def) => Element.GetAttributeIntArray(key, def);
public ushort[]? GetAttributeUshortArray(string key, ushort[]? def) => Element.GetAttributeUshortArray(key, def);
public float? GetAttributeNullableFloat(string key) => Element.GetAttributeNullableFloat(key);
public float GetAttributeFloat(string key, float def) => Element.GetAttributeFloat(key, def);
public float[]? GetAttributeFloatArray(string key, float[]? def) => Element.GetAttributeFloatArray(key, def);
public float GetAttributeFloat(float def, params string[] keys) => Element.GetAttributeFloat(def, keys);
@@ -162,6 +162,8 @@ namespace Barotrauma
private static readonly int messagesPerFile = 800;
public const string SavePath = "ConsoleLogs";
private static WeakReference<Character> previousControlledCharacter; // For SP freecam
private static void AssignOnExecute(string names, Action<string[]> onExecute)
{
var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
@@ -935,8 +937,29 @@ namespace Barotrauma
if (GameMain.Client == null)
{
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
if (Character.Controlled == null)
{
// Exiting freecam - try to return to previous character
Character prevCharacter = null;
if (previousControlledCharacter != null && previousControlledCharacter.TryGetTarget(out prevCharacter) &&
prevCharacter != null && !prevCharacter.IsDead && !prevCharacter.Removed)
{
Character.Controlled = prevCharacter;
NewMessage("Exiting freecam mode", Color.Yellow);
}
else
{
NewMessage("Could not regain control of the previous character (dead or removed).", Color.Red);
}
}
else
{
// Entering freecam - store current character ID
previousControlledCharacter = new WeakReference<Character>(Character.Controlled);
Character.Controlled = null;
GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
NewMessage("Entering freecam mode", Color.Yellow);
}
}
else
{
@@ -1348,14 +1371,13 @@ namespace Barotrauma
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
if (Level.Loaded?.EndOutpost == null)
{
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return;
}
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null)
{
@@ -1616,7 +1638,18 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
}
if (Level.Loaded.StartOutpost != null &&
Level.Loaded.StartOutpost.Info.OutpostTags.Contains("PvPOutpost".ToIdentifier()))
{
ThrowError("Chose a PvP outpost for the start of the level. This is probably not intentional, unless there's a PvP outpost that's also intended to be used in normal levels?");
}
if (Level.Loaded.EndOutpost != null &&
Level.Loaded.EndOutpost.Info.OutpostTags.Contains("PvPOutpost".ToIdentifier()))
{
ThrowError("Chose a PvP outpost for the end of the level. This is probably not intentional, unless there's a PvP outpost that's also intended to be used in normal levels?");
}
var levelCells = Level.Loaded.GetCells(
Submarine.MainSub.WorldPosition,
Math.Max(Submarine.MainSub.Borders.Width / Level.GridCellSize, 2));
@@ -1796,7 +1829,7 @@ namespace Barotrauma
int targetLevel = prefab.GetMaxLevelForCurrentSub() - upgradeManager.GetRealUpgradeLevel(prefab, category);
for (int i = 0; i < targetLevel; i++)
{
upgradeManager.PurchaseUpgrade(prefab, category, force: true);
upgradeManager.TryPurchaseUpgrade(prefab, category, force: true);
}
NewMessage($"Upgraded {category.Identifier}.{prefab.Identifier} by {targetLevel} levels.", Color.DarkGreen);
}
@@ -2297,6 +2330,7 @@ namespace Barotrauma
commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true));
commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true));
commands.Add(new Command("loslightingfreecam", "Toggles line of sight effect, lighting, and enables freecam mode. (client-only)", null, isCheat: true));
InitProjectSpecific();
@@ -2911,7 +2945,7 @@ namespace Barotrauma
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
}
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType teamType, out bool addToCrew);
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew);
if (usePreConfiguredNPC)
{
@@ -2950,25 +2984,30 @@ namespace Barotrauma
}
else if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) is { } prefab)
{
Entity.Spawner.AddCharacterToSpawnQueue(args[0].ToIdentifier(), spawnPosition, prefab.HasCharacterInfo ? new CharacterInfo(prefab.Identifier) : null, onSpawn: newCharacter =>
{
SetTeamAndCrew(newCharacter);
});
Entity.Spawner.AddCharacterToSpawnQueue(args[0].ToIdentifier(), spawnPosition, prefab.HasCharacterInfo ? new CharacterInfo(prefab.Identifier) : null, onSpawn: SetTeamAndCrew);
}
void SetTeamAndCrew(Character newCharacter)
{
newCharacter.TeamID = teamType;
if (teamType.HasValue)
{
newCharacter.TeamID = teamType.Value;
}
else if (isHuman)
{
newCharacter.TeamID = Character.Controlled?.TeamID ?? CharacterTeamType.Team1;
}
if (addToCrew)
{
GameMain.GameSession?.CrewManager.AddCharacter(newCharacter);
}
}
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType teamType, out bool addToCrew)
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew)
{
spawnPosition = Vector2.Zero;
spawnPoint = null;
teamType = null;
int argIndex = characterArgumentCount;
if (args.Length > argIndex)
@@ -3024,15 +3063,14 @@ namespace Barotrauma
{
teamType = (CharacterTeamType)teamID;
}
else if (!Enum.TryParse(args[argIndex], ignoreCase: true, out teamType))
else if (Enum.TryParse(args[argIndex], ignoreCase: true, out CharacterTeamType parsedTeamType))
{
teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
ThrowError($"\"{args[argIndex]}\" is not a valid team id. Defaulting to {teamType}.");
teamType = parsedTeamType;
}
else
{
ThrowError($"\"{args[argIndex]}\" is not a valid team id.");
}
}
else
{
teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
}
argIndex++;
@@ -3393,6 +3431,9 @@ namespace Barotrauma
public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
{
error = AddContentPackageInfoToMessage(error, contentPackage);
#if CLIENT
SteamTimelineManager.OnError(error, e);
#endif
if (e != null)
{
error += " {" + e.Message + "}\n";
@@ -123,7 +123,7 @@ namespace Barotrauma
/// </summary>
OnAbility = 23,
/// <summary>
/// Executes once when a specific Containable is placed inside an ItemContainer. Only valid for Containables defined in an ItemContainer component.
/// Executes once when a specific Containable is placed inside an ItemContainer. Only valid for Containables defined in an ItemContainer component. Does not execute if the item is placed into the container when loading an existing submarine or a character (i.e. when the item was "already in" the container).
/// </summary>
OnInserted = 24,
/// <summary>
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
@@ -53,7 +53,7 @@ static class EosSessionManager
// If the session already exists then this failure is not a problem
return;
}
DebugConsole.ThrowError($"Failed to create session: {result}");
DebugConsole.ThrowError($"Failed to create Epic Online Services session: {result}");
return;
}
CurrentOwnedSession = Option.Some(newOwnedSession);
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -30,6 +30,9 @@ namespace Barotrauma
[Serialize(120.0f, IsPropertySaveable.Yes, description: "How long it takes for the NPC to \"cool down\" (stop attacking).")]
public float CoolDown { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC revert back to a normal state when the event resets?")]
public bool AbandonOnReset { get; set; }
private bool isFinished = false;
@@ -81,7 +84,7 @@ namespace Barotrauma
public override void Reset()
{
if (affectedNpcs != null)
if (affectedNpcs != null && AbandonOnReset)
{
foreach (var npc in affectedNpcs)
{
@@ -1,4 +1,4 @@
namespace Barotrauma
namespace Barotrauma
{
/// <summary>
/// Makes a specific character invulnerable to damage and unable to die.
@@ -36,13 +36,21 @@ namespace Barotrauma
{
if (target != null && target is Character character)
{
if (UpdateAfflictions)
if (Enabled)
{
character.CharacterHealth.Unkillable = Enabled;
if (UpdateAfflictions)
{
character.CharacterHealth.Unkillable = true;
}
else
{
character.GodMode = true;
}
}
else
{
character.GodMode = Enabled;
character.CharacterHealth.Unkillable = false;
character.GodMode = false;
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,7 +14,10 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")]
public bool Wait { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop waiting when the event resets?")]
public bool AbandonOnReset { get; set; }
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
@@ -76,7 +79,7 @@ namespace Barotrauma
public override void Reset()
{
if (affectedNpcs != null)
if (affectedNpcs != null && AbandonOnReset)
{
foreach (var npc in affectedNpcs)
{
@@ -367,7 +367,7 @@ namespace Barotrauma
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.IsEmpty ? null : SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
}
private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
@@ -422,12 +422,32 @@ namespace Barotrauma
}
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPoints.Any())
var spawnPointsWithTag = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPointsWithTag.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
potentialSpawnPoints = spawnPointsWithTag.ToList();
}
else
{
//no spawnpoints with the tag we want -> choose something with no tags
TryGetSpawnPointsWithNoTag();
}
}
else
{
//if no tags are specified, prefer a spawnpoint with no tags, i.e. prefer a "generic" spawnpoint instead of some special one like a jail spawnpoint
TryGetSpawnPointsWithNoTag();
}
void TryGetSpawnPointsWithNoTag()
{
var spawnPointsWithNoTag = potentialSpawnPoints.Where(wp => wp.Tags.None());
if (spawnPointsWithNoTag.Any())
{
potentialSpawnPoints = spawnPointsWithNoTag.ToList();
}
}
if (potentialSpawnPoints.None())
{
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
@@ -777,7 +777,15 @@ namespace Barotrauma
var locationType = location.GetLocationTypeToDisplay();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
if (eventSet.LocationTypeIdentifiers == null) { return false; }
// EventLocationType is used to have the event set consider the location id as something else, for example "city" to get events that go to city locations
bool hasMatchingEventLocationId = !locationType.EventLocationType.IsEmpty &&
eventSet.LocationTypeIdentifiers.Contains(locationType.EventLocationType);
bool hasMatchingLocationId = eventSet.LocationTypeIdentifiers.Contains(locationType.Identifier);
return hasMatchingEventLocationId || hasMatchingLocationId;
}
private Location GetEventLocation()
@@ -156,9 +156,34 @@ namespace Barotrauma
}
}
}
private int previousKillTargetsRemaining = -1;
private void TrackKillTargetCount()
{
if (requireKill.Count == 0) { return; }
if (previousKillTargetsRemaining == -1)
{
previousKillTargetsRemaining = requireKill.Count();
}
int killTargetsRemaining = requireKill.Count(c => !c.Removed && !c.IsDead && !(c.LockHands && c.Submarine == Submarine.MainSub));
// at least one of the targets have been eliminated
if (killTargetsRemaining < previousKillTargetsRemaining)
{
#if CLIENT
SteamTimelineManager.OnOutpostTargetEliminated(this);
#endif
}
previousKillTargetsRemaining = killTargetsRemaining;
}
protected override void UpdateMissionSpecific(float deltaTime)
{
TrackKillTargetCount();
if (State != HostagesKilledState)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
@@ -9,14 +9,30 @@ namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly ContentXElement itemConfig;
private readonly ContentXElement terroristItemConfig;
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
/// <summary>
/// Number of escorted characters by default.
/// </summary>
private readonly int baseEscortedCharacters;
/// <summary>
/// A scaling factor for the number of escorted characters, relative to the recommended crew size of the sub. The total amount of escorted characters is calculated as
/// baseEscortedCharacters + scalingEscortedCharacters * (RecommendedCrewSizeMin + RecommendedCrewSizeMax) / 2
/// </summary>
private readonly float scalingEscortedCharacters;
/// <summary>
/// The probability for the escorted characters to be "terrorists" (turning them hostile when the sub has progressed enough in the level).
/// A value of 0.5 would mean about half of the characters are terrorist, 1 would mean they all are. There's 20% of randomness applied to the value to make it less predictable.
/// </summary>
private readonly float terroristChance;
/// <summary>
/// Dialog tag the terrorists use in their dialog when they become hostile.
/// </summary>
private readonly string terroristAnnounceDialogTag;
private int calculatedReward;
private Submarine missionSub;
@@ -26,7 +42,6 @@ namespace Barotrauma
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
private readonly string terroristAnnounceDialogTag = string.Empty;
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
@@ -35,8 +50,10 @@ namespace Barotrauma
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty);
terroristItemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag =
prefab.ConfigElement.GetAttributeString("dialogterroristannounce",
prefab.ConfigElement.GetAttributeString("terroristAnnounceDialogTag", string.Empty));
CalculateReward();
}
@@ -94,35 +111,29 @@ namespace Barotrauma
randSync = Rand.RandSync.Unsynced;
}
List<HumanPrefab> humanPrefabsToSpawn = new List<HumanPrefab>();
List<(HumanPrefab humanPrefab, List<StatusEffect> statusEffects)> humanPrefabsToSpawn = new List<(HumanPrefab humanPrefab, List<StatusEffect> statusEffects)>();
foreach (ContentXElement characterElement in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
var humanPrefab = GetHumanPrefabFromElement(characterElement);
for (int i = 0; i < count; i++)
{
humanPrefabsToSpawn.Add(humanPrefab);
}
foreach (var element in characterElement.Elements())
{
if (element.NameAsIdentifier() == "statuseffect")
List<StatusEffect> characterStatusEffects = new List<StatusEffect>();
foreach (var element in characterElement.Elements())
{
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
if (!characterStatusEffects.ContainsKey(humanPrefab))
if (element.NameAsIdentifier() == "statuseffect")
{
characterStatusEffects[humanPrefab] = new List<StatusEffect> { newEffect };
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
characterStatusEffects.Add(newEffect);
}
else
{
characterStatusEffects[humanPrefab].Add(newEffect);
}
}
humanPrefabsToSpawn.Add((humanPrefab, characterStatusEffects));
}
}
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
foreach (var humanPrefab in humanPrefabsToSpawn)
foreach ((var humanPrefab, var statusEffectList) in humanPrefabsToSpawn)
{
if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
var jobPrefab = humanPrefab.GetJobPrefab(randSync);
@@ -136,23 +147,19 @@ namespace Barotrauma
}
}
}
foreach (var humanPrefab in humanPrefabsToSpawn)
foreach ((var humanPrefab, var statusEffectList) in humanPrefabsToSpawn)
{
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
if (spawnedCharacter.AIController is HumanAIController humanAI)
{
humanAI.InitMentalStateManager();
}
if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList))
foreach (var statusEffect in statusEffectList)
{
foreach (var statusEffect in statusEffectList)
{
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
if (terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
@@ -256,13 +263,23 @@ namespace Barotrauma
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
if (!string.IsNullOrEmpty(terroristAnnounceDialogTag))
{
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
character.Speak(TextManager.Get(terroristAnnounceDialogTag).Value, null, Rand.Range(0.5f, 3f));
}
ContentXElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
foreach (var itemElement in terroristItemConfig.Elements())
{
HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f;
var selectedItemElement = itemElement;
if (itemElement.NameAsIdentifier() == "chooserandom".ToIdentifier())
{
selectedItemElement = itemElement.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= levelDifficulty);
}
if (selectedItemElement != null)
{
if (levelDifficulty < selectedItemElement.GetAttributeFloat(0f, "mindifficulty")) { continue; }
HumanPrefab.InitializeItem(character, selectedItemElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
}
}
}
}
}
@@ -26,6 +26,7 @@ namespace Barotrauma
{
if (state != value)
{
int previousState = state;
state = value;
TryTriggerEvents(state);
#if SERVER
@@ -38,6 +39,7 @@ namespace Barotrauma
#endif
ShowMessage(State);
OnMissionStateChanged?.Invoke(this);
MissionStateChanged(previousState);
}
}
}
@@ -198,6 +200,11 @@ namespace Barotrauma
Messages = messages.ToImmutableArray();
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
if (prefab.ConfigElement.GetChildElements("Characters").Count() > 1)
{
DebugConsole.AddWarning($"Error in mission {Prefab.Identifier}: multiple <Characters> elements found. Only the first one will be used.",
contentPackage: prefab.ContentPackage);
}
}
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
@@ -214,6 +221,8 @@ namespace Barotrauma
}
return message;
}
protected virtual void MissionStateChanged(int previousState) {}
public virtual void SetLevel(LevelData level) { }
@@ -109,6 +109,8 @@ namespace Barotrauma
public readonly bool AllowRetry;
public readonly bool ShowSonarLabels;
public readonly bool ShowInMenus, ShowStartMessage;
public readonly bool IsSideObjective;
@@ -219,11 +221,12 @@ namespace Barotrauma
}
}
Reward = element.GetAttributeInt("reward", 1);
ExperienceMultiplier = element.GetAttributeFloat("experiencemultiplier", 1.0f);
AllowRetry = element.GetAttributeBool("allowretry", false);
ShowInMenus = element.GetAttributeBool("showinmenus", true);
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
Reward = element.GetAttributeInt(nameof(Reward), 1);
ExperienceMultiplier = element.GetAttributeFloat(nameof(ExperienceMultiplier), 1.0f);
AllowRetry = element.GetAttributeBool(nameof(AllowRetry), false);
ShowSonarLabels = element.GetAttributeBool(nameof(ShowSonarLabels), true);
ShowInMenus = element.GetAttributeBool(nameof(ShowInMenus), true);
ShowStartMessage = element.GetAttributeBool(nameof(ShowStartMessage), true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
@@ -237,11 +240,12 @@ namespace Barotrauma
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt("commonness", 1);
AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true);
Commonness = element.GetAttributeInt(nameof(Commonness), 1);
AllowOtherMissionsInLevel = element.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true);
if (element.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
int difficulty = element.GetAttributeInt(nameof(Difficulty), MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty);
@@ -173,6 +173,17 @@ namespace Barotrauma
}
}
protected override void MissionStateChanged(int previousState)
{
// state of 1+ here means the mission is completed
if (previousState == 0 && State >= 1)
{
#if CLIENT
SteamTimelineManager.OnMonsterMissionTargetsKilled(this);
#endif
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
switch (State)
@@ -92,13 +92,38 @@ namespace Barotrauma
set
{
if (value == state) { return; }
bool wasRetrieved = Retrieved;
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
if (!wasRetrieved && Retrieved)
{
OnTargetRetrieved();
}
else if (state == RetrievalState.PickedUp)
{
OnTargetPickedUp();
}
}
}
private void OnTargetRetrieved()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetRetrieved(Item, mission);
#endif
}
private void OnTargetPickedUp()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetPickedUp(Item, mission);
#endif
}
public bool Interacted;
private readonly SalvageMission mission;
@@ -469,13 +494,13 @@ namespace Barotrauma
target.Item.ExternalHighlight = true;
#endif
target.Item.UpdateTransform();
if (target.Item.CurrentHull == null)
if (target.Item.CurrentHull == null && target.Item.body != null)
{
//prevent the body from moving if it spawned outside the hulls (we don't want it e.g. falling to the bottom of a cave or into the abyss)
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
}
else if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
target.Item.OnInteract += () =>
{
@@ -168,6 +168,17 @@ namespace Barotrauma
}
}
protected override void MissionStateChanged(int previousState)
{
// detect successful scanned targets increasing after scan is completed
if (previousState < State)
{
#if CLIENT
SteamTimelineManager.OnScanSuccessful(this);
#endif
}
}
private void GetScanners()
{
foreach (var startingItem in startingItems)
@@ -206,7 +206,9 @@ namespace Barotrauma
LocalizedString reputationName = GetReputationName(normalizedValue);
LocalizedString formattedReputation = TextManager.GetWithVariables("reputationformat",
("[reputationname]", reputationName),
("[reputationvalue]", ((int)Math.Round(value)).ToString()));
//simply cast to float (dropping the decimals)
//we don't want any rounding here, otherwise it might look like you have enough rep to reach some threshold when you're actually some fraction off
("[reputationvalue]", ((int)value).ToString()));
if (addColorTags)
{
formattedReputation = $"‖color:{XMLExtensions.ToStringHex(GetReputationColor(normalizedValue))}‖{formattedReputation}‖end‖";
@@ -160,10 +160,34 @@ namespace Barotrauma
protected set;
}
public bool PurchasedLostShuttlesInLatestSave, PurchasedHullRepairsInLatestSave, PurchasedItemRepairsInLatestSave;
/// <summary>
/// Has recovery of lost shuttles been purchased in the latest save? Determines whether the shuttles should be recovered when loading into the round.
/// </summary>
public bool PurchasedLostShuttlesInLatestSave;
/// <summary>
/// Have hull repairs been purchased in the latest save? Determines whether the walls will be repaired when loading into the round.
/// </summary>
public bool PurchasedHullRepairsInLatestSave;
/// <summary>
/// Has repairing damaged items been purchased in the latest save? Determines whether the items will be repaired when loading into the round.
/// </summary>
public bool PurchasedItemRepairsInLatestSave;
/// <summary>
/// Have hull repairs been purchased on the current round?
/// </summary>
public virtual bool PurchasedHullRepairs { get; set; }
/// <summary>
/// Has recovery of lost shuttles been purchased on the current round?
/// </summary>
public virtual bool PurchasedLostShuttles { get; set; }
/// <summary>
/// Has repairing damaged items been purchased on the current round?
/// </summary>
public virtual bool PurchasedItemRepairs { get; set; }
public bool DivingSuitWarningShown;
@@ -441,6 +465,16 @@ namespace Barotrauma
currentLocation.DeselectMission(mission);
}
}
foreach (var mission in currentLocation.AvailableMissions)
{
//if the mission isn't shown in menus, it cannot be selected by the player -> must be something that is supposed to be automatically selected
if (!mission.Prefab.ShowInMenus)
{
currentLocation.SelectMission(mission);
}
}
if (levelData.HasBeaconStation && !levelData.IsBeaconActive && Missions.None(m => m.Prefab.Type == Tags.MissionTypeBeacon))
{
var beaconMissionPrefabs = MissionPrefab.Prefabs.Where(m => m.IsSideObjective && m.Type == Tags.MissionTypeBeacon);
@@ -1632,6 +1666,70 @@ namespace Barotrauma
parentElement?.Add(petsElement);
}
/// <summary>
/// Loads the parts of a campaign save that are the same between single player and multiplayer saves.
/// </summary>
public void LoadSaveSharedSingleAndMultiplayer(XElement element)
{
PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
DebugConsole.CheatsEnabled = true;
if (!AchievementManager.CheatsEnabled)
{
AchievementManager.CheatsEnabled = true;
#if CLIENT
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive achievements until you restart the game.");
#else
DebugConsole.NewMessage("Cheat commands have been enabled.", Color.Red);
#endif
}
}
//backwards compatibility for saves made prior to the addition of personal wallets
int oldMoney = element.GetAttributeInt("money", 0);
if (oldMoney > 0)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = oldMoney
};
}
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "cargo":
CargoManager.LoadPurchasedItems(subElement);
break;
case "pendingupgrades": //backwards compatibility
case "upgrademanager":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: IsSinglePlayer);
break;
case "pets":
petsElement = subElement;
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(Option<Character>.None(), subElement);
break;
case "stats":
LoadStats(subElement);
break;
case "eventmanager":
GameMain.GameSession.EventManager.Load(subElement);
break;
case "unlockedrecipe":
GameMain.GameSession.UnlockRecipe(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), showNotifications: false);
break;
}
}
}
public void LoadPets()
{
if (petsElement != null)
@@ -161,23 +161,7 @@ namespace Barotrauma
/// </summary>
private void Load(XElement element)
{
PurchasedLostShuttlesInLatestSave = element.GetAttributeBool("purchasedlostshuttles", false);
PurchasedHullRepairsInLatestSave = element.GetAttributeBool("purchasedhullrepairs", false);
PurchasedItemRepairsInLatestSave = element.GetAttributeBool("purchaseditemrepairs", false);
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
if (CheatsEnabled)
{
DebugConsole.CheatsEnabled = true;
if (!AchievementManager.CheatsEnabled)
{
AchievementManager.CheatsEnabled = true;
#if CLIENT
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive achievements until you restart the game.");
#else
DebugConsole.NewMessage("Cheat commands have been enabled.", Color.Red);
#endif
}
}
LoadSaveSharedSingleAndMultiplayer(element);
foreach (var subElement in element.Elements())
{
@@ -215,30 +199,11 @@ namespace Barotrauma
}
}
break;
case "upgrademanager":
case "pendingupgrades":
UpgradeManager = new UpgradeManager(this, subElement, isSingleplayer: false);
break;
case "bots" when GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer:
CrewManager.HasBots = subElement.GetAttributeBool("hasbots", false);
CrewManager.AddCharacterElements(subElement);
ActiveOrdersElement = subElement.GetChildElement("activeorders");
break;
case "cargo":
CargoManager?.LoadPurchasedItems(subElement);
break;
case "pets":
petsElement = subElement;
break;
case "stats":
LoadStats(subElement);
break;
case "eventmanager":
GameMain.GameSession.EventManager.Load(subElement);
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(Option<Character>.None(), subElement);
break;
#if SERVER
case "traitormanager":
GameMain.Server?.TraitorManager?.Load(subElement);
@@ -253,15 +218,6 @@ namespace Barotrauma
}
}
int oldMoney = element.GetAttributeInt("money", 0);
if (oldMoney > 0)
{
Bank = new Wallet(Option<Character>.None())
{
Balance = oldMoney
};
}
UpgradeManager ??= new UpgradeManager(this);
#if SERVER
@@ -170,6 +170,9 @@ namespace Barotrauma
public Submarine? Submarine { get; set; }
private readonly HashSet<Identifier> unlockedRecipes = new HashSet<Identifier>();
public IEnumerable<Identifier> UnlockedRecipes => unlockedRecipes;
public CampaignDataPath DataPath { get; set; }
public bool TraitorsEnabled =>
@@ -275,6 +278,10 @@ namespace Barotrauma
}
}
break;
//NOTE: if you're adding something that's supposed to load something that's persistent in a campaign,
//this is probably not the correct place! This is where the GameSession itself is initialized,
//and if you let's say quit to the server lobby and reload, this method won't be called again.
//You should probably add it to CampaignMode.LoadSaveSharedSingleAndMultiplayer
}
}
}
@@ -385,7 +392,7 @@ namespace Barotrauma
var dummyLocations = new Location[2];
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);
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), zone: null, biomeId: null, rand, requireOutpost: true, forceLocationType);
}
return dummyLocations;
}
@@ -401,6 +408,7 @@ namespace Barotrauma
public void LoadPreviousSave()
{
AchievementManager.OnRoundEnded(this, roundInterrupted: true);
Submarine.Unload();
SaveUtil.LoadGame(DataPath);
}
@@ -1491,6 +1499,25 @@ namespace Barotrauma
#endif
}
public void UnlockRecipe(Identifier identifier, bool showNotifications)
{
if (unlockedRecipes.Add(identifier))
{
#if CLIENT
if (showNotifications)
{
foreach (var character in GetSessionCrewCharacters(CharacterType.Both))
{
LocalizedString recipeName = TextManager.Get($"entityname.{identifier}").Fallback(identifier.Value);
character.AddMessage(TextManager.GetWithVariable("recipeunlockednotification", "[name]", recipeName).Value, GUIStyle.Yellow, playSound: true);
}
}
#else
GameMain.Server.UnlockRecipe(identifier);
#endif
}
}
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
{
errorMsg = "";
@@ -1602,9 +1629,11 @@ namespace Barotrauma
ownedSubsElement.Add(new XElement("sub", new XAttribute("name", ownedSub.Name)));
}
}
if (Map != null) { rootElement.Add(new XAttribute("mapseed", Map.Seed)); }
rootElement.Add(new XAttribute("selectedcontentpackagenames",
string.Join("|", ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent).Select(cp => cp.Name.Replace("|", @"\|")))));
XElement permadeathsElement = new XElement("permadeaths");
foreach (var kvp in permadeathsPerAccount)
@@ -169,12 +169,14 @@ namespace Barotrauma
/// Purchased upgrades are temporarily stored in <see cref="PendingUpgrades"/> and they are applied
/// after the next round starts similarly how items are spawned in the stowage room after the round starts.
/// </remarks>
public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null)
public bool TryPurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null)
{
if (!HasPermissionToManageUpgrades(client)) { return false; }
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot upgrade when switching to another submarine.");
return;
return false;
}
int price = prefab.Price.GetBuyPrice(prefab, GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
@@ -185,7 +187,7 @@ namespace Barotrauma
if (currentLevel + 1 > maxLevel)
{
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
return;
return false;
}
bool TryTakeResources(Character character)
@@ -203,17 +205,17 @@ namespace Barotrauma
switch (GameMain.NetworkMember)
{
case null when Character.Controlled is { } controlled: // singleplayer
if (!TryTakeResources(controlled)) { return; }
if (!TryTakeResources(controlled)) { return false; }
break;
case { IsClient: true }:
if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; }
if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return false; }
break;
case { IsServer: true } when client?.Character is { } character:
if (!TryTakeResources(character)) { return; }
if (!TryTakeResources(character)) { return false; }
break;
default:
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player.");
return;
return false;
}
}
@@ -270,11 +272,15 @@ namespace Barotrauma
PurchasedUpgrades.Add(new PurchasedUpgrade(prefab, category));
#endif
OnUpgradesChanged?.Invoke(this);
return true;
}
else
{
DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
$"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
return false;
}
}
@@ -297,6 +303,8 @@ namespace Barotrauma
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null)
{
if (!HasPermissionToManageUpgrades(client)) { return; }
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
@@ -398,8 +406,10 @@ namespace Barotrauma
/// <summary>
/// Cancels the currently pending item swap, or uninstalls the item if there's no swap pending
/// </summary>
public void CancelItemSwap(Item itemToRemove, bool force = false)
public void CancelItemSwap(Item itemToRemove, bool force = false, Client? client = null)
{
if (!HasPermissionToManageUpgrades(client)) { return; }
if (!CanUpgradeSub())
{
DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
@@ -757,6 +767,18 @@ namespace Barotrauma
Campaign.PendingSubmarineSwitch.Name == Submarine.MainSub.Info.Name;
}
public bool HasPermissionToManageUpgrades(Client? client = null)
{
if (!GameMain.IsMultiplayer) { return true; }
#if SERVER
if (client is null) { return false; }
return CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageCampaign);
#elif CLIENT
return CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageCampaign);
#endif
}
public void Save(XElement? parent)
{
if (parent == null) { return; }
@@ -220,6 +220,22 @@ namespace Barotrauma
return false;
}
/// <summary>
/// Can the item be put in the inventory in a slot of the specified type (i.e. is there a suitable free slot or a stack the item can be put in).
/// </summary>
public bool CanBePut(Item item, InvSlotType slotType)
{
for (int i = 0; i < capacity; i++)
{
if (slotType.HasFlag(SlotTypes[i]))
{
if (CanBePutInSlot(item, i)) { return true; }
}
}
return false;
}
public override bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
{
return
@@ -179,7 +179,8 @@ namespace Barotrauma.Items.Components
var prevDockingTarget = DockingTarget;
Undock(applyEffects: false);
Dock(prevDockingTarget);
Lock(isNetworkMessage: true, applyEffects: false);
//don't move subs at this point, it will mess up the placement logic when flipping multi-part subs
Lock(isNetworkMessage: true, applyEffects: false, moveSubs: false);
}
}
@@ -281,7 +282,7 @@ namespace Barotrauma.Items.Components
OnDocked = null;
}
public void Lock(bool isNetworkMessage, bool applyEffects = true)
public void Lock(bool isNetworkMessage, bool applyEffects = true, bool moveSubs = true)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) { return; }
@@ -312,16 +313,19 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnUse, 1.0f);
}
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.Info.IsOutpost)
if (moveSubs)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
DockingTarget.item.Submarine.Info.IsOutpost)
{
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
}
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
item.Submarine.Info.IsOutpost)
{
DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
}
}
ConnectWireBetweenPorts();
@@ -349,31 +349,31 @@ namespace Barotrauma.Items.Components
// used for debugging where a vine failed to grow
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
[Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows.")]
[Serialize(1f, IsPropertySaveable.Yes, "How fast the plant grows. Value of 1 means a vine attempts to grow every 10 seconds while 2 and 0.5 mean every 5 and 20 seconds respectively.")]
public float GrowthSpeed { get; set; }
[Serialize(100f, IsPropertySaveable.Yes, "How long the plant can go without watering.")]
public float MaxHealth { get; set; }
[Serialize(100f, IsPropertySaveable.Yes, "How much water the plant can hold. Affects how long the plant can survive without water.")]
public float MaxWater { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while in water.")]
public float FloodTolerance { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "How much extra water the plant uses per second while it is submerged in a flooded hull.")]
public float ExtraWaterUsedPerSecondWhileFlooded { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "How much damage the plant takes while growing.")]
public float Hardiness { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "How much water the plant consumes passively per second.")]
public float WaterUsedPerSecond { get; set; }
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a seed is produced.")]
public float SeedRate { get; set; }
[Serialize(0.01f, IsPropertySaveable.Yes, "Percentage chance of a seed item being produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance. Not used in vanilla plants.")]
public float SeedSpawnChance { get; set; }
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced.")]
public float ProductRate { get; set; }
[Serialize(0.01f, IsPropertySaveable.Yes, "How often a product item is produced on growth ticks (every 10 seconds without a multiplier). 0.01 means 1% chance.")]
public float ProductSpawnChance { get; set; }
[Serialize(0.5f, IsPropertySaveable.Yes, "Probability of an attribute being randomly modified in a newly produced seed.")]
[Serialize(0.5f, IsPropertySaveable.Yes, "Completely unused property that was added on the first design pass but due to the first pass being too complex was never used and now it is used by mods so it cannot be removed.")]
public float MutationProbability { get; set; }
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, "Color of the flowers.")]
public Color FlowerTint { get; set; }
[Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn when fully grown")]
[Serialize(3, IsPropertySaveable.Yes, "Number of flowers drawn.")]
public int FlowerQuantity { get; set; }
[Serialize(0.25f, IsPropertySaveable.Yes, "Size of the flower sprites.")]
@@ -403,9 +403,12 @@ namespace Barotrauma.Items.Components
[Serialize("1,1,1,1", IsPropertySaveable.Yes, "Probability for the plant to grow in a direction.")]
public Vector4 GrowthWeights { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, "How much damage is taken from fires.")]
[Serialize(0.0f, IsPropertySaveable.Yes, "How much water is lost due to fires every 10 seconds.")]
public float FireVulnerability { get; set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, "Modifier to the percentage of product and seed items produced before the plant is fully grown based on how many vines have been grown. 0 would mean no products or seeds are produced while 0.5 would mean half of the normal amount.")]
public Vector2 LinearProductAndSeedMultiplierBeforeFullyGrown { get; set; }
private const float increasedDeathSpeed = 10f;
private bool accelerateDeath;
private float health;
@@ -417,7 +420,7 @@ namespace Barotrauma.Items.Components
public float Health
{
get => health;
set => health = Math.Clamp(value, 0, MaxHealth);
set => health = Math.Clamp(value, 0, MaxWater);
}
public bool Decayed { get; set; }
@@ -441,7 +444,14 @@ namespace Barotrauma.Items.Components
{
SerializableProperty.DeserializeProperties(this, element);
Health = MaxHealth;
// backwards compatibility
MaxWater = element.GetAttributeFloat("maxhealth", MaxWater);
WaterUsedPerSecond = element.GetAttributeFloat("hardiness", WaterUsedPerSecond);
ExtraWaterUsedPerSecondWhileFlooded = element.GetAttributeFloat("floodtolerance", ExtraWaterUsedPerSecondWhileFlooded);
ProductSpawnChance = element.GetAttributeFloat("productrate", ProductSpawnChance);
SeedSpawnChance = element.GetAttributeFloat("seedrate", SeedSpawnChance);
Health = MaxWater;
if (element.HasElements)
{
@@ -492,10 +502,7 @@ namespace Barotrauma.Items.Components
{
if (Decayed) { return; }
if (FullyGrown)
{
TryGenerateProduct(planter, slot);
}
TryGenerateProduct(planter, slot);
if (Health > 0)
{
@@ -504,11 +511,11 @@ namespace Barotrauma.Items.Components
// fertilizer makes the plant tick faster, compensate by halving water requirement
float multipler = planter.Fertilizer > 0 ? 0.5f : 1f;
Health -= (accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness) * multipler;
Health -= (accelerateDeath ? WaterUsedPerSecond * increasedDeathSpeed : WaterUsedPerSecond) * multipler;
if (planter.Item.InWater)
{
Health -= FloodTolerance * multipler;
Health -= ExtraWaterUsedPerSecondWhileFlooded * multipler;
}
#if SERVER
if (FullyGrown)
@@ -535,7 +542,7 @@ namespace Barotrauma.Items.Components
private void UpdateBranchHealth()
{
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
Color healthColor = Color.White * (1.0f - Health / MaxWater);
foreach (VineTile vine in Vines)
{
vine.HealthColor = healthColor;
@@ -546,11 +553,24 @@ namespace Barotrauma.Items.Components
{
productDelay++;
if (productDelay <= maxProductDelay) { return; }
productDelay = 0;
bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
float spawnChanceMultiplier = 1f;
if (!FullyGrown)
{
if (LinearProductAndSeedMultiplierBeforeFullyGrown.NearlyEquals(Vector2.Zero)) { return; }
float growthProgress = Vines.Count / (float)MaximumVines;
spawnChanceMultiplier = MathHelper.Lerp(LinearProductAndSeedMultiplierBeforeFullyGrown.X, LinearProductAndSeedMultiplierBeforeFullyGrown.Y, growthProgress);
if (MathUtils.NearlyEqual(spawnChanceMultiplier, 0f)) { return; }
}
bool spawnProduct = Rand.Range(0f, 1f) < (ProductSpawnChance * spawnChanceMultiplier),
spawnSeed = Rand.Range(0f, 1f) < (SeedSpawnChance * spawnChanceMultiplier);
Vector2 spawnPos;
@@ -678,7 +698,7 @@ namespace Barotrauma.Items.Components
}
}
fireCheckCooldown = 5f;
fireCheckCooldown = 10f;
}
else
{
@@ -1,4 +1,5 @@
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
@@ -44,7 +45,12 @@ namespace Barotrauma.Items.Components
private bool attachable, attached, attachedByDefault;
private Voronoi2.VoronoiCell attachTargetCell;
private PhysicsBody body;
/// <summary>
/// The item's original physics body (if one exists). When the item is attached to a wall, it's <see cref="Item.body"/> gets set to null,
/// and we use this field to keep track of the original body.
/// </summary>
private PhysicsBody originalBody;
public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
@@ -62,7 +68,7 @@ namespace Barotrauma.Items.Components
public PhysicsBody Body
{
get { return item.body ?? body; }
get { return item.body ?? originalBody; }
}
[Serialize(false, IsPropertySaveable.Yes, description: "Is the item currently attached to a wall (only valid if Attachable is set to true).")]
@@ -84,6 +90,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, IsPropertySaveable.Yes, description: "Camera offset to apply when aiming this item. Only valid if Aimable is set to true.")]
public float CameraAimOffset { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the character adjust its pose when aiming with the item. Most noticeable underwater, where the character will rotate its entire body to face the direction the item is aimed at.")]
public bool ControlPose
{
@@ -119,6 +128,32 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "When enabled, the item can only be attached to a position where it touches the floor.")]
public bool AttachesToFloor
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can the item be attached inside doors?")]
public bool AllowAttachInsideDoors
{
get;
set;
}
private HashSet<Identifier> disallowAttachingOverTags = new HashSet<Identifier>();
[Editable, Serialize("", IsPropertySaveable.Yes)]
public string DisallowAttachingOverTags
{
get => disallowAttachingOverTags.ConvertToString();
set
{
disallowAttachingOverTags = value.ToIdentifiers().ToHashSet();
}
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -275,7 +310,7 @@ namespace Barotrauma.Items.Components
public Holdable(Item item, ContentXElement element)
: base(item, element)
{
body = item.body;
originalBody = item.body;
Pusher = null;
if (element.GetAttributeBool("blocksplayers", false))
@@ -411,9 +446,9 @@ namespace Barotrauma.Items.Components
if (attachable)
{
if (body != null)
if (originalBody != null)
{
item.body = body;
item.body = originalBody;
}
DeattachFromWall();
}
@@ -514,9 +549,9 @@ namespace Barotrauma.Items.Components
if (character != null) { item.Submarine = character.Submarine; }
if (item.body == null)
{
if (body != null)
if (originalBody != null)
{
item.body = body;
item.body = originalBody;
}
else
{
@@ -565,13 +600,45 @@ namespace Barotrauma.Items.Components
public bool CanBeAttached(Character user)
{
return CanBeAttached(user, out _);
}
private static List<Item> tempOverlappingItems = new List<Item>();
private bool CanBeAttached(Character user, out IEnumerable<Item> overlappingItems)
{
tempOverlappingItems.Clear();
overlappingItems = tempOverlappingItems;
if (!attachable || !Reattachable) { return false; }
//can be attached anywhere in sub editor
if (Screen.Selected == GameMain.SubEditorScreen) { return true; }
if (AttachesToFloor && item.CurrentHull == null) { return false; }
Vector2 attachPos = user == null ? item.WorldPosition : GetAttachPosition(user, useWorldCoordinates: true);
if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors)
{
var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true);
Vector2 size = item.Rect.Size.ToVector2() / 2;
foreach (Item otherItem in Item.ItemList)
{
if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; }
if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; }
if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) &&
(otherItem.GetComponent<Door>() == null || AllowAttachInsideDoors))
{
continue;
}
Rectangle worldRect = otherItem.WorldRect;
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
tempOverlappingItems.Add(otherItem);
}
if (tempOverlappingItems.Any()) { return false; }
}
//can be attached anywhere inside hulls
if (item.CurrentHull != null && Submarine.RectContains(item.CurrentHull.WorldRect, attachPos)) { return true; }
@@ -670,14 +737,19 @@ namespace Barotrauma.Items.Components
{
if (!attachable) { return; }
if (body == null)
if (originalBody == null)
{
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
}
body.Enabled = false;
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
item.body = null;
originalBody.Enabled = false;
originalBody.SetTransformIgnoreContacts(originalBody.SimPosition, rotation: 0.0f);
if (item.body != null)
{
item.body.Dir = 1;
item.body = null;
}
item.GetComponents<LightComponent>().ForEach(static light => light.SetLightSourceTransform());
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
@@ -689,7 +761,7 @@ namespace Barotrauma.Items.Components
{
//set to submarine-relative position
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
body.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
originalBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
item.Submarine = attachTarget.Submarine;
}
@@ -833,6 +905,9 @@ namespace Barotrauma.Items.Components
{
item.Drop(character);
item.SetTransform(ConvertUnits.ToSimUnits(GetAttachPosition(character)), 0.0f, findNewHull: false);
//don't find the new hull in SetTransform, because that'd also potentially change the submarine (teleport the item outside if it's attached outside)
//instead just find the hull, so the item is considered to be in the right hull
item.CurrentHull = Hull.FindHull(item.WorldPosition, item.CurrentHull);
//the light source won't get properly updated if lighting is disabled (even though the light sprite is still drawn when lighting is disabled)
//so let's ensure the light source is up-to-date
RefreshLightSources(item);
@@ -869,34 +944,60 @@ namespace Barotrauma.Items.Components
Vector2 mouseDiff = user.CursorWorldPosition - user.WorldPosition;
mouseDiff = mouseDiff.ClampLength(MaxAttachDistance);
Vector2 submarinePos = useWorldCoordinates && user.Submarine != null ? user.Submarine.Position : Vector2.Zero;
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
Vector2 attachPos = userPos + mouseDiff;
Vector2 halfSize = new Vector2(item.Rect.Width, item.Rect.Height) / 2;
//offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
//in the sub editor, we align the top-left corner of the item with the grid
//but here the origin of the item is placed at the attach position, so we need to offset it
Vector2 offset = new Vector2(
-(item.Rect.Width / 2) % Submarine.GridSize.X,
(item.Rect.Height / 2) % Submarine.GridSize.Y);
-halfSize.X % Submarine.GridSize.X,
halfSize.Y % Submarine.GridSize.Y);
if (user.Submarine != null)
{
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
Vector2 padding = halfSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(user.Position),
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall,
/*don't ignore sensors so the raycast can hit open doors or broken walls*/
ignoreSensors: AllowAttachInsideDoors, customPredicate: (Fixture fixture) =>
{
if (fixture.UserData is Door) { return false; }
return true;
}) != null)
{
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
Vector2 pickedPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset - submarinePos;
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
return
attachPos =
new Vector2(
(mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
(mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
- offset;
RoundToGrid(pickedPos.X, Submarine.GridSize.X, roundingDir: -Math.Sign(mouseDiff.X)),
RoundToGrid(pickedPos.Y, Submarine.GridSize.Y, roundingDir: -Math.Sign(mouseDiff.Y)))
- offset + submarinePos;
}
if (AttachesToFloor)
{
//if attaching to floor, do a raycast down and move the attach pos where it hits
float size = item.Rect.Height / 2.0f;
Vector2 rayStart = attachPos - submarinePos;
Vector2 rayEnd = rayStart - Vector2.UnitY * MaxAttachDistance * 2;
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(rayStart),
ConvertUnits.ToSimUnits(rayEnd), collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform) != null)
{
attachPos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + Vector2.UnitY * size + submarinePos;
}
else
{
return Vector2.Zero;
}
}
}
else if (Level.Loaded != null)
@@ -919,9 +1020,30 @@ namespace Barotrauma.Items.Components
}
}
return new Vector2(
MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
//subtract the submarine position so we're doing the rounding in the sub's
//internal/local coordinate space regardless if we're using world coordinates
//(otherwise the rounding would behave differently depending on the value of useWorldCoordinates)
Vector2 offsetAttachPos = attachPos + offset - submarinePos;
return
new Vector2(
RoundToGrid(offsetAttachPos.X, Submarine.GridSize.X),
//don't round the vertical position if we're attaching to floor - we want the item to align with the floor, not the grid
AttachesToFloor ? offsetAttachPos.Y : RoundToGrid(offsetAttachPos.Y, Submarine.GridSize.Y))
- offset + submarinePos;
///<param name="roundingDir">If < 0, the method rounds down. If > 0, rounds up. If 0, rounds to the closest integer.</param>
static float RoundToGrid(float position, float gridSize, int roundingDir = 0)
{
if (roundingDir < 0)
{
return MathF.Floor(position / gridSize) * gridSize;
}
else if (roundingDir > 0)
{
return MathF.Ceiling(position / gridSize) * gridSize;
}
return MathUtils.RoundTowardsClosest(position, gridSize);
}
}
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
@@ -977,7 +1099,7 @@ namespace Barotrauma.Items.Components
return;
}
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && CanBeAttached(picker))
if (picker == Character.Controlled && picker.IsKeyDown(InputType.Aim) && attachable && Reattachable)
{
Drawable = true;
}
@@ -1115,11 +1237,11 @@ namespace Barotrauma.Items.Components
}
else
{
if (body != null)
if (originalBody != null)
{
body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
item.body = body;
body.Enabled = item.ParentInventory == null;
originalBody.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
item.body = originalBody;
originalBody.Enabled = item.ParentInventory == null;
}
DeattachFromWall();
}
@@ -1134,7 +1256,7 @@ namespace Barotrauma.Items.Components
Pusher.Remove();
Pusher = null;
}
body = null;
originalBody = null;
}
public override XElement Save(XElement parentElement)
@@ -435,55 +435,65 @@ namespace Barotrauma.Items.Components
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
if (Attack != null)
{
Attack.SetUser(user);
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
bool applyAttack = true;
if (Attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(targetEntity as ISerializableEntity)) ||
Attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(user)))
{
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
applyAttack = false;
}
else if (targetCharacter != null)
if (applyAttack)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
{
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
if (targetLimb.character.Removed) { return; }
targetLimb.character.LastDamageSource = item;
Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
}
else if (targetCharacter != null)
{
if (targetCharacter.Removed) { return; }
targetCharacter.LastDamageSource = item;
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
#if CLIENT
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
{
Character.Controlled.UpdateHUDProgressBar(targetItem,
targetItem.WorldPosition,
targetItem.Condition / targetItem.MaxCondition,
emptyColor: GUIStyle.HealthBarColorLow,
fullColor: GUIStyle.HealthBarColorHigh,
textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
}
#endif
}
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
RestoreCollision();
hitting = false;
User = null;
}
else
{
return;
}
}
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
RestoreCollision();
hitting = false;
User = null;
}
else
{
return;
}
}
}
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
float barOffset = 10f * GUI.Scale;
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
user?.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxWater, GUIStyle.Blue, GUIStyle.Blue, "progressbar.watering");
#endif
}
}
@@ -47,7 +47,7 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
//actual throwing logic is handled in Update
return characterUsable || character == null;
return (characterUsable && !UsageDisabledByRangedWeapon(character)) || character == null;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
@@ -111,24 +111,28 @@ namespace Barotrauma.Items.Components
return;
}
if (throwState != ThrowState.Throwing)
bool aim = false;
if (!UsageDisabledByRangedWeapon(picker))
{
if (picker.IsKeyDown(InputType.Aim))
if (throwState != ThrowState.Throwing)
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
if (picker.IsKeyDown(InputType.Aim))
{
if (picker.IsKeyDown(InputType.Shoot)) { throwState = ThrowState.Initiated; }
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
else if (throwState != ThrowState.Initiated)
{
throwAngle = ThrowAngleStart;
}
}
bool aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
aim = picker.IsKeyDown(InputType.Aim) && picker.CanAim;
}
if (picker.IsDead || !picker.AllowInput)
{
throwState = ThrowState.None;
aim = false;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
//return if the status effect got rid of the picker somehow
@@ -998,6 +998,13 @@ namespace Barotrauma.Items.Components
/// </summary>
public virtual void OnItemLoaded() { }
/// <summary>
/// Implement in a base class if the instances of the component contain some sort of data that isn't serialized using the normal serializable properties
/// (i.e. some data that changes per-item and isn't loaded from the prefab, but that isn't a property marked with [Serialize] either),
/// but that must be copied when cloning the item.
/// </summary>
public virtual void Clone(ItemComponent original) { }
public virtual void OnScaleChanged() { }
/// <summary>
@@ -1093,7 +1100,6 @@ namespace Barotrauma.Items.Components
componentElement.Add(newElement);
}
SerializableProperty.SerializeProperties(this, componentElement);
parentElement.Add(componentElement);
@@ -438,9 +438,14 @@ namespace Barotrauma.Items.Components
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
activeContainedItems.Add(activeContainedItem);
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
containedItem.OnInsertedEffectsApplied)
{
continue;
}
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
}
containedItem.OnInsertedEffectsApplied = true;
}
}
}
@@ -511,6 +516,8 @@ namespace Barotrauma.Items.Components
activeContainedItem.StatusEffect.Apply(ActionType.OnRemoved, deltaTime: 1, item, targets);
}
containedItem.OnInsertedEffectsApplied = false;
activeContainedItems.RemoveAll(i => i.Item == containedItem);
containedItems.RemoveAll(i => i.Item == containedItem);
item.SetContainedItemPositions();
@@ -680,7 +687,7 @@ namespace Barotrauma.Items.Components
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
{
if (!ShouldApplyEffects(activeContainedItem)) continue;
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
@@ -993,6 +1000,8 @@ namespace Barotrauma.Items.Components
}
else
{
//flip if flipped on one axis but not both (flipping on both axes is basically "double negative" and makes the rotation normal again)
if (flippedX ^ flippedY) { rotation = -rotation; }
rotation += -item.RotationRad;
}
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
@@ -1050,8 +1059,17 @@ namespace Barotrauma.Items.Components
transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
flippedX = item.RootContainer?.FlippedX ?? (item.FlippedX && item.Prefab.CanSpriteFlipX);
flippedY = item.RootContainer?.FlippedY ?? (item.FlippedY && item.Prefab.CanSpriteFlipY);
if (item.RootContainer != null)
{
flippedX = item.RootContainer.FlippedX && item.RootContainer.Prefab.CanSpriteFlipX;
flippedY = item.RootContainer.FlippedY && item.RootContainer.Prefab.CanSpriteFlipY;
}
else
{
flippedX = item.FlippedX && item.Prefab.CanSpriteFlipX;
flippedY = item.FlippedY && item.Prefab.CanSpriteFlipY;
}
var rootBody = item.RootContainer?.body ?? item.body;
bool bodyFlipped = rootBody is { Dir: -1 };
@@ -1123,10 +1141,13 @@ namespace Barotrauma.Items.Components
}
}
private bool initializingLoadedItems;
public override void OnMapLoaded()
{
if (itemIds != null)
{
initializingLoadedItems = true;
for (ushort i = 0; i < itemIds.Length; i++)
{
if (i >= Inventory.Capacity)
@@ -1138,10 +1159,11 @@ namespace Barotrauma.Items.Components
}
foreach (ushort id in itemIds[i])
{
if (!(Entity.FindEntityByID(id) is Item item)) { continue; }
if (Entity.FindEntityByID(id) is not Item item) { continue; }
Inventory.TryPutItem(item, i, false, false, null, createNetworkEvent: false, ignoreCondition: true);
}
}
initializingLoadedItems = false;
itemIds = null;
}
@@ -349,7 +349,7 @@ namespace Barotrauma.Items.Components
// Don't move lower body limbs if there's another selected secondary item that should control them
if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; }
// Don't move hands if there's a selected primary item that should control them
if (!limb.IsLowerBody && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
if (lb.AllowUsingLimb)
{
switch (lb.LimbType)
@@ -564,7 +564,7 @@ namespace Barotrauma.Items.Components
{
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
#if SERVER
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedItem == item)
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character != null)
{
GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
}
@@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components
private double lastReceivedSteeringSignalTime;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, description: "Is autopilot currently on or not?", AlwaysUseInstanceValues = true)]
public bool AutoPilot
{
get { return autoPilot; }
@@ -110,17 +110,15 @@ namespace Barotrauma.Items.Components
{
base.OnItemLoaded();
IsActive = true;
#if CLIENT
var lights = item.GetComponents<LightComponent>();
if (lights.Any())
{
lightComponents = lights.ToList();
foreach (var light in lightComponents)
foreach (var light in item.GetComponents<LightComponent>())
{
light.Light.Enabled = false;
light.IsOn = false;
}
}
#endif
container = item.GetComponent<ItemContainer>();
GrowableSeeds = new Growable[container.Capacity];
}
@@ -233,7 +231,6 @@ namespace Barotrauma.Items.Components
{
base.Update(deltaTime, cam);
#if CLIENT
if (lightComponents != null && lightComponents.Count > 0)
{
bool hasSeed = false;
@@ -243,10 +240,9 @@ namespace Barotrauma.Items.Components
}
foreach (var light in lightComponents)
{
light.Light.Enabled = hasSeed;
light.IsOn = hasSeed;
}
}
#endif
if (container?.Inventory == null) { return; }
@@ -0,0 +1,230 @@
#nullable enable
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
internal partial class PowerDistributor : PowerTransfer
{
private const int MaxNameLength = 32;
private const int SupplyRatioSteps = 20;
private const float SupplyRatioStep = 1f / SupplyRatioSteps;
private partial class PowerGroup
{
private readonly PowerDistributor distributor;
public readonly Connection PowerOut;
public readonly Connection? RatioInput, RatioOutput;
private string name;
public string Name
{
get => name;
set
{
name = value;
DisplayName = TextManager.Get(name).Fallback(name);
#if CLIENT
UpdateNameBox();
#endif
}
}
public LocalizedString? DisplayName { get; private set; }
private float supplyRatio = 1f;
public float SupplyRatio
{
get => supplyRatio;
set
{
if (!MathUtils.IsValid(value)) { return; }
supplyRatio = MathUtils.RoundTowardsClosest(MathHelper.Clamp(value, 0f, 1f), SupplyRatioStep);
#if CLIENT
UpdateSlider();
#endif
}
}
public float DisplayRatio
{
get => MathUtils.RoundToInt(supplyRatio * 100);
set => SupplyRatio = value / 100f;
}
public float Load;
public float ModifiedLoad => Load * SupplyRatio;
public PowerGroup(PowerDistributor distributor, Connection power, XElement? element = null, Connection? ratioInput = null, Connection? ratioOutput = null)
{
this.distributor = distributor;
PowerOut = power;
RatioInput = ratioInput;
RatioOutput = ratioOutput;
distributor.powerGroups.Add(this);
name = TextManager.GetWithVariable("groupx", "[num]", distributor.powerGroups.Count.ToString()).Value;
SupplyRatio = 1f;
if (element != null)
{
name = element.GetAttributeString("name", name);
SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio);
}
#if CLIENT
CreateGUI();
#endif
}
#region Signals
public void ReceiveRatioSignal(Signal signal)
{
if (!float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float receivedSignal) || !MathUtils.IsValid(receivedSignal)) { return; }
DisplayRatio = receivedSignal;
}
public void SendRatioSignal() => distributor.item.SendSignal(new Signal(DisplayRatio.ToString()), RatioOutput);
#endregion
}
private readonly List<PowerGroup> powerGroups = new List<PowerGroup>();
protected override PowerPriority Priority => PowerPriority.Relay;
public PowerDistributor(Item item, ContentXElement element) : base(item, element) { }
public override void OnItemLoaded()
{
base.OnItemLoaded();
IEnumerable<Connection> ratioInputs = Item.Connections.Where(static conn => !conn.IsOutput && conn.Name.StartsWith("set_supply_ratio"));
IEnumerable<Connection> ratioOutputs = Item.Connections.Where(static conn => conn.IsOutput && conn.Name.StartsWith("supply_ratio_out"));
for (int i = 0; i < powerOuts.Count; i++)
{
new PowerGroup(this, powerOuts[i], cachedGroupData.ElementAtOrDefault(i), ratioInputs.ElementAtOrDefault(i), ratioOutputs.ElementAtOrDefault(i));
}
cachedGroupData.Clear();
}
public override void Clone(ItemComponent original)
{
if (original is not PowerDistributor originalPowerDistributor) { return; }
for (int i = 0; i < powerOuts.Count; i++)
{
powerGroups[i].SupplyRatio = originalPowerDistributor.powerGroups[i].SupplyRatio;
powerGroups[i].Name = originalPowerDistributor.powerGroups[i].Name;
}
}
#region Signals
protected override void SendSignals()
{
item.SendSignal(MathUtils.RoundToInt(powerIn.Grid?.Power ?? 0f).ToString(), "power_value_out");
item.SendSignal(MathUtils.RoundToInt(GetCurrentPowerConsumption(powerIn)).ToString(), "load_value_out");
powerGroups.ForEach(static group => group.SendRatioSignal());
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (item.Condition <= 0f || connection.IsPower) { return; }
if (connection.IsOutput) { return; }
powerGroups.FirstOrDefault(group => group.RatioInput == connection)?.ReceiveRatioSignal(signal);
}
#endregion
#region Power Calculation
private bool IsShortCircuited(Connection conn) => powerIn.Grid == conn.Grid;
public override float GetCurrentPowerConsumption(Connection? connection = null)
{
if (connection != powerIn) { return -1f; }
if (isBroken) { return 0f; }
return powerGroups.Sum(group => IsShortCircuited(group.PowerOut) ? 0f : group.ModifiedLoad) + ExtraLoad;
}
private float CalculatePowerOut(PowerGroup group)
{
if (isBroken || powerIn.Grid == null || IsShortCircuited(group.PowerOut)) { return 0f; }
return Math.Max(group.ModifiedLoad * Voltage, 0f);
}
public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load)
{
if (connection == powerIn) { return 0f; }
PowerGroup group = powerGroups.First(group => group.PowerOut == connection);
group.Load = load;
return CalculatePowerOut(group);
}
#endregion
#region Serialization
private readonly List<XElement> cachedGroupData = new List<XElement>();
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
foreach (PowerGroup powerGroup in powerGroups)
{
componentElement.Add(new XElement("PowerGroup",
new XAttribute("name", powerGroup.Name),
new XAttribute("ratio", powerGroup.SupplyRatio)));
}
return componentElement;
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
if (usePrefabValues) { return; }
foreach (XElement element in componentElement.Elements())
{
cachedGroupData.Add(element);
}
}
#endregion
#region Networking
private enum EventType { NameChange, RatioChange }
private void SharedEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null)
{
EventData data = ExtractEventData<EventData>(extraData);
msg.WriteRangedInteger((int)data.EventType, 0, 1);
msg.WriteRangedInteger(powerGroups.IndexOf(data.PowerGroup), 0, powerGroups.Count - 1);
switch (data.EventType)
{
case EventType.NameChange:
msg.WriteString(data.PowerGroup.Name);
break;
case EventType.RatioChange:
msg.WriteRangedInteger(MathUtils.RoundToInt(data.PowerGroup.SupplyRatio / SupplyRatioStep), 0, SupplyRatioSteps);
break;
}
}
private void SharedEventRead(IReadMessage msg, out EventType eventType, out PowerGroup powerGroup, out string newName, out float newRatio)
{
eventType = (EventType)msg.ReadRangedInteger(0, 1);
powerGroup = powerGroups[msg.ReadRangedInteger(0, powerGroups.Count - 1)];
newName = eventType == EventType.NameChange ? string.Concat(msg.ReadString().Take(MaxNameLength)) : powerGroup.Name;
newRatio = eventType == EventType.RatioChange ? msg.ReadRangedInteger(0, SupplyRatioSteps) * SupplyRatioStep : powerGroup.SupplyRatio;
}
private readonly record struct EventData(PowerGroup PowerGroup, EventType EventType) : IEventData;
#endregion
}
}
@@ -177,18 +177,7 @@ namespace Barotrauma.Items.Components
{
RefreshConnections();
if (Timing.TotalTime > extraLoadSetTime + 1.0)
{
//Decay the extra load to 0 from either positive or negative
if (extraLoad > 0)
{
extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0);
}
else
{
extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0);
}
}
UpdateExtraLoad(deltaTime);
if (!CanTransfer) { return; }
@@ -200,6 +189,28 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime);
SendSignals();
UpdateOvervoltage(deltaTime);
}
protected virtual void UpdateExtraLoad(float deltaTime)
{
if (Timing.TotalTime <= extraLoadSetTime + 1.0) { return; }
//Decay the extra load to 0 from either positive or negative
if (extraLoad > 0)
{
extraLoad = Math.Max(extraLoad - 1000.0f * deltaTime, 0);
}
else
{
extraLoad = Math.Min(extraLoad + 1000.0f * deltaTime, 0);
}
}
protected virtual void SendSignals()
{
float powerReadingOut = 0;
float loadReadingOut = ExtraLoad;
if (powerLoad < 0)
@@ -226,7 +237,10 @@ namespace Barotrauma.Items.Components
}
item.SendSignal(powerSignal, "power_value_out");
item.SendSignal(loadSignal, "load_value_out");
}
protected virtual void UpdateOvervoltage(float deltaTime)
{
//if the item can't be fixed, don't allow it to break
if (!item.Repairables.Any() || !CanBeOverloaded) { return; }
@@ -234,46 +248,45 @@ namespace Barotrauma.Items.Components
Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 };
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
if (!Overload || GameMain.NetworkMember is { IsClient: true }) { return; }
if (overloadCooldownTimer > 0.0f)
{
if (overloadCooldownTimer > 0.0f)
{
overloadCooldownTimer -= deltaTime;
return;
}
overloadCooldownTimer -= deltaTime;
return;
}
//damage the item if voltage is too high (except if running as a client)
float prevCondition = item.Condition;
//some randomness to prevent all junction boxes from breaking at the same time
if (Rand.Range(0.0f, 1.0f) < 0.01f)
{
//damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time)
float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition);
item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor;
}
if (item.Condition <= 0.0f && prevCondition > 0.0f)
{
overloadCooldownTimer = OverloadCooldown;
//damage the item if voltage is too high (except if running as a client)
float prevCondition = item.Condition;
//some randomness to prevent all junction boxes from breaking at the same time
if (Rand.Range(0.0f, 1.0f) < 0.01f)
{
//damaged boxes are more sensitive to overvoltage (also preventing all boxes from breaking at the same time)
float conditionFactor = MathHelper.Lerp(5.0f, 1.0f, item.Condition / item.MaxCondition);
item.Condition -= deltaTime * Rand.Range(10.0f, 500.0f) * conditionFactor;
}
if (item.Condition > 0.0f || prevCondition <= 0.0f) { return; }
overloadCooldownTimer = OverloadCooldown;
#if CLIENT
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
Vector2 baseVel = Rand.Vector(300.0f);
for (int i = 0; i < 10; i++)
{
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
}
SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull);
Vector2 baseVel = Rand.Vector(300.0f);
for (int i = 0; i < 10; i++)
{
var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition,
baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull);
if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f);
}
#endif
float currentIntensity = GameMain.GameSession?.EventManager != null ?
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
float currentIntensity = GameMain.GameSession?.EventManager != null ?
GameMain.GameSession.EventManager.CurrentIntensity : 0.5f;
//higher probability for fires if the current intensity is low
if (FireProbability > 0.0f &&
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
{
new FireSource(item.WorldPosition);
}
}
//higher probability for fires if the current intensity is low
if (FireProbability > 0.0f &&
Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity))
{
new FireSource(item.WorldPosition);
}
}
@@ -424,7 +437,7 @@ namespace Barotrauma.Items.Components
}
}
if (this is not RelayComponent)
if (this is not RelayComponent and not PowerDistributor)
{
if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput))
{
@@ -452,7 +465,7 @@ namespace Barotrauma.Items.Components
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && ic is not RelayComponent) { continue; }
if (ic is PowerTransfer and not RelayComponent and not PowerDistributor) { continue; }
ic.ReceiveSignal(signal, recipient);
}
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
#if CLIENT
using Barotrauma.Sounds;
#endif
@@ -93,7 +94,22 @@ namespace Barotrauma.Items.Components
/// </summary>
protected float powerConsumption;
protected Connection powerIn, powerOut;
protected Connection powerIn;
protected List<Connection> powerOuts = new List<Connection>();
/// <summary>
/// Throws an error if there is more than one power out connection.<br/>
/// Use <see cref="powerOuts"/> if a component should handle multiple outputs.
/// </summary>
protected Connection powerOut
{
get
{
if (powerOuts.Count > 1) { DebugConsole.ThrowErrorOnce($"{item.ID}.multiplePowerOut", $"Item {item.Name} ({item.Prefab.Identifier}) has multiple power outputs, but only supports one!"); }
return powerOuts.FirstOrDefault();
}
}
protected bool powerInIsPowerOut => powerOuts.Contains(powerIn);
/// <summary>
/// Maximum voltage factor when the device is being overvolted. I.e. how many times more effectively the device can function when it's being overvolted
@@ -153,9 +169,10 @@ namespace Barotrauma.Items.Components
{
if (powerIn?.Grid != null) { return powerIn.Grid.Voltage; }
}
else if (powerOut != null)
else if (powerOuts.Any())
{
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
IEnumerable<Connection> gridConnections = powerOuts.Where(static conn => conn.Grid != null);
if (gridConnections.Any()) { return gridConnections.Average(static conn => conn.Grid.Voltage); }
}
if (this is PowerTransfer && item.Condition <= 0.0f)
@@ -245,18 +262,19 @@ namespace Barotrauma.Items.Components
{
powerIn = c;
}
else if (c.Name == "power_out")
{
powerOut = c;
// Connection takes the lowest priority
if (Priority > powerOut.Priority)
{
powerOut.Priority = Priority;
}
}
else if (c.Name == "power")
{
powerIn = powerOut = c;
powerIn = c;
powerOuts.Add(c);
}
else if (c.IsOutput)
{
powerOuts.Add(c);
// Connection takes the lowest priority
if (Priority > c.Priority)
{
c.Priority = Priority;
}
}
}
else
@@ -271,11 +289,11 @@ namespace Barotrauma.Items.Components
DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange);
#endif
}
powerOut = c;
powerOuts.Add(c);
// Connection takes the lowest priority
if (Priority > powerOut.Priority)
if (Priority > c.Priority)
{
powerOut.Priority = Priority;
c.Priority = Priority;
}
}
else
@@ -335,9 +353,9 @@ namespace Barotrauma.Items.Components
{
powered.powerIn.Grid = null;
}
if (powered.powerOut != null)
foreach (Connection powerOut in powered.powerOuts)
{
powered.powerOut.Grid = null;
powerOut.Grid = null;
}
}
@@ -345,8 +363,10 @@ namespace Barotrauma.Items.Components
foreach (Powered powered in poweredList)
{
if (powered.Item.Condition <= 0f) { continue; }
//Probe through all connections that don't have a gridID
if (powered.powerIn != null && powered.powerIn.Grid == null && powered.powerIn != powered.powerOut && powered.Item.Condition > 0.0f)
if (powered.powerIn != null && powered.powerIn.Grid == null && !powered.powerInIsPowerOut)
{
// Only create grids for networks with more than 1 device
if (powered.powerIn.Recipients.Count > 0)
@@ -356,13 +376,16 @@ namespace Barotrauma.Items.Components
}
}
if (powered.powerOut != null && powered.powerOut.Grid == null && powered.Item.Condition > 0.0f)
foreach (Connection powerOut in powered.powerOuts)
{
//Only create grids for networks with more than 1 device
if (powered.powerOut.Recipients.Count > 0)
if (powerOut != null && powerOut.Grid == null)
{
GridInfo grid = PropagateGrid(powered.powerOut);
Grids[grid.ID] = grid;
//Only create grids for networks with more than 1 device
if (powerOut.Recipients.Count > 0)
{
GridInfo grid = PropagateGrid(powerOut);
Grids[grid.ID] = grid;
}
}
}
}
@@ -479,7 +502,7 @@ namespace Barotrauma.Items.Components
powered.Voltage -= deltaTime;
//Handle the device if it's got a power connection
if (powered.powerIn != null && powered.powerOut != powered.powerIn)
if (powered.powerIn != null && !powered.powerInIsPowerOut)
{
//Get the new load for the connection
float currLoad = powered.GetCurrentPowerConsumption(powered.powerIn);
@@ -507,10 +530,10 @@ namespace Barotrauma.Items.Components
}
//Handle the device power depending on if its powerout
if (powered.powerOut != null)
foreach (Connection powerOut in powered.powerOuts)
{
//Get the connection's load
float currLoad = powered.GetCurrentPowerConsumption(powered.powerOut);
float currLoad = powered.GetCurrentPowerConsumption(powerOut);
//Update the device's output load to the correct variable
if (powered is PowerTransfer pt)
@@ -529,20 +552,20 @@ namespace Barotrauma.Items.Components
if (currLoad >= 0)
{
//Add to the grid load if possible
if (powered.powerOut.Grid != null)
if (powerOut.Grid != null)
{
powered.powerOut.Grid.Load += currLoad;
powerOut.Grid.Load += currLoad;
}
}
else if (powered.powerOut.Grid != null)
else if (powerOut.Grid != null)
{
//Add connection as a source to be processed
powered.powerOut.Grid.AddSrc(powered.powerOut);
powerOut.Grid.AddSrc(powerOut);
}
else
{
//Perform power calculations for the singular connection
float loadOut = -powered.GetConnectionPowerOut(powered.powerOut, 0, powered.MinMaxPowerOut(powered.powerOut, 0), 0);
float loadOut = -powered.GetConnectionPowerOut(powerOut, 0, powered.MinMaxPowerOut(powerOut, 0), 0);
if (powered is PowerTransfer pt2)
{
pt2.PowerLoad = loadOut;
@@ -557,7 +580,7 @@ namespace Barotrauma.Items.Components
}
//Indicate grid is resolved as it was the only device
powered.GridResolved(powered.powerOut);
powered.GridResolved(powerOut);
}
}
}
@@ -625,7 +648,7 @@ namespace Barotrauma.Items.Components
public virtual float GetCurrentPowerConsumption(Connection connection = null)
{
// If a handheld device there is no consumption
if (powerIn == null && powerOut == null)
if (powerIn == null && powerOuts.None())
{
return 0;
}
@@ -664,7 +687,7 @@ namespace Barotrauma.Items.Components
/// <returns>Power pushed to the grid</returns>
public virtual float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
{
return conn == powerOut ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
return powerOuts.Contains(conn) ? MathHelper.Max(-CurrPowerConsumption, 0) : 0;
}
/// <summary>
@@ -233,6 +233,7 @@ namespace Barotrauma.Items.Components
var cloneNode = InputOutputNodes[ioIndex];
cloneNode.Position = origNode.Position;
cloneNode.ReplaceAllConnectionLabelOverrides(origNode.ConnectionLabelOverrides);
}
if (!clonedContainedItems.Any()) { return; }
@@ -18,7 +18,14 @@ namespace Barotrauma.Items.Components
public readonly int MaxWires = 5;
public readonly string Name;
public readonly LocalizedString DisplayName;
private readonly LocalizedString _displayName;
public LocalizedString DisplayName
{
get => DisplayNameOverride ?? _displayName;
private init => _displayName = value;
}
public LocalizedString DisplayNameOverride;
private readonly HashSet<Wire> wires;
public IReadOnlyCollection<Wire> Wires => wires;
@@ -160,8 +167,7 @@ namespace Barotrauma.Items.Components
DisplayName = Name;
}
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
IsPower = element.GetAttributeBool("ispower", Name is "power_in" or "power" or "power_out");
LoadedWires = new List<(ushort wireId, int? connectionIndex)>();
foreach (var subElement in element.Elements())
@@ -325,23 +325,18 @@ namespace Barotrauma.Items.Components
private bool TriggersOn(Character character, bool triggerFromHumans, bool triggerFromPets, bool triggerFromMonsters)
{
if (IgnoreDead && character.IsDead) { return false; }
if (character.IsHuman)
{
if (!triggerFromHumans) { return false; }
}
else if (character.IsPet)
if (character.IsPet)
{
if (!triggerFromPets) { return false; }
}
else if (character.IsHuman || CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
{
if (!triggerFromHumans) { return false; }
}
else
{
// Not a human or a pet -> monster?
if (!triggerFromMonsters) { return false; }
if (CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
{
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
return false;
}
}
// Check matching character, if defined.
if (targetCharacters.Any())
@@ -553,9 +553,9 @@ namespace Barotrauma.Items.Components
return new List<Vector2>(nodes);
}
public void SetNodes(List<Vector2> nodes)
public void SetNodes(IEnumerable<Vector2> nodes)
{
this.nodes = new List<Vector2>(nodes);
this.nodes = nodes.ToList();
UpdateSections();
}
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
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
public float ForceFluctuationStrength
{
get
{
@@ -33,7 +33,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How fast (cycles per second) the force fluctuates.", alwaysUseInstanceValues: true)]
private float ForceFluctuationFrequency
public float ForceFluctuationFrequency
{
get
{
@@ -45,7 +45,7 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(0.01f, IsPropertySaveable.Yes, description: "How often (in seconds) the force fluctuation is calculated.", alwaysUseInstanceValues: true)]
private float ForceFluctuationInterval
public float ForceFluctuationInterval
{
get
{
@@ -542,6 +542,10 @@ namespace Barotrauma.Items.Components
}
IsActive = false;
#if CLIENT
//stop any sounds that may have been looping while wearing the item
StopSounds(ActionType.OnWearing);
#endif
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -429,8 +429,6 @@ namespace Barotrauma
}
}
public float RotationRad { get; private set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
public float Rotation
{
@@ -705,6 +703,22 @@ namespace Barotrauma
get; set;
}
/// <summary>
/// Have the <see cref="ActionType.OnInserted"/> effects of the item already triggered when it was placed inside it's current container?
/// Used to prevent the effects from executing again when e.g. an existing character (who's inventory items' effects already triggered on some earlier round) spawns mid-round.
/// </summary>
[Serialize(false, IsPropertySaveable.Yes)]
public bool OnInsertedEffectsApplied
{
get; set;
}
/// <summary>
/// Were the <see cref="ActionType.OnInserted"/> effects already been applied when the item first spawned (loaded from a save)?
/// Needed for communicating to the clients whether they should trigger when the item spawns.
/// </summary>
public bool OnInsertedEffectsAppliedOnPreviousRound;
public Color Color
{
get { return spriteColor; }
@@ -765,7 +779,7 @@ namespace Barotrauma
}
[Serialize(false, IsPropertySaveable.Yes)]
private bool HasBeenInstantiatedOnce { get; set; }
public bool HasBeenInstantiatedOnce { get; set; }
//the default value should be Prefab.Health, but because we can't use it in the attribute,
//we'll just use NaN (which does nothing) and set the default value in the constructor/load
@@ -807,9 +821,37 @@ namespace Barotrauma
set;
}
private bool? isDangerous;
/// <summary>
/// Bots avoid rooms with dangerous items in them. Normally this value is <see cref="ItemPrefab.IsDangerous">defined in the prefab</see>,
/// but this property can be used to override the prefab value.
/// </summary>
public bool IsDangerous
{
get { return isDangerous ?? Prefab.IsDangerous; }
set
{
isDangerous = value;
if (!value)
{
_dangerousItems.Remove(this);
}
else
{
_dangerousItems.Add(this);
}
}
}
[Editable, Serialize(false, isSaveable: IsPropertySaveable.Yes, "When enabled will prevent the item from taking damage from all sources")]
public bool InvulnerableToDamage { get; set; }
/// <summary>
/// Should bots automatically unequip the item? Normally always true, but disabled on items that have been configured to be equipped by default in an item set or the character's job items.
/// Note that the value is not saved: if the NPC becomes persistent (e.g. Artie Dolittle hired to the crew) they will no longer keep holding the item.
/// </summary>
public bool UnequipAutomatically = true;
/// <summary>
/// Was the item stolen during the current round. Note that it's possible for the items to be found in the player's inventory even though they weren't actually stolen.
/// For example, a guard can place handcuffs there. So use <see cref="Illegitimate"/> for checking if the item is illegitimately held.
@@ -1084,6 +1126,9 @@ namespace Barotrauma
public bool IsLadder { get; }
/// <summary>
/// Secondary items can be selected at the same time with a primary item (e.g. a ladder or a chair can be selected at the same time with some device).
/// </summary>
public bool IsSecondaryItem { get; }
private ItemStatManager statManager;
@@ -1369,13 +1414,7 @@ namespace Barotrauma
conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier;
}
}
if (!HasBeenInstantiatedOnce)
{
// This only needs to be done on the very first instantiation.
// MaxCondition will be multiplied in RecalculateConditionValues(), ensuring
// that Condition will stay in line with the multiplier from then on.
condition *= conditionMultiplierCampaign;
}
condition *= conditionMultiplierCampaign;
RecalculateConditionValues();
@@ -1478,13 +1517,14 @@ namespace Barotrauma
{
ItemComponent component = components[i],
cloneComp = clone.components[i];
if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox)
if (component.GetType() == cloneComp.GetType())
{
continue;
cloneComp.Clone(component);
}
if (component is CircuitBox origBox && cloneComp is CircuitBox cloneBox)
{
cloneBox.CloneFrom(origBox, clonedContainedItems);
}
cloneBox.CloneFrom(origBox, clonedContainedItems);
}
clone.FullyInitialized = true;
@@ -2293,6 +2333,7 @@ namespace Barotrauma
private void SendPendingNetworkUpdatesInternal()
{
DebugConsole.NewMessage($"Sending status event for item {Name}", Color.Gray);
CreateStatusEvent(loadingRound: false);
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
@@ -2300,6 +2341,17 @@ namespace Barotrauma
public void CreateStatusEvent(bool loadingRound)
{
//A little hacky: clients aren't allowed to apply OnFire effects themselves, which means effects that rely on the "onfire" status tag
//won't work properly. But let's notify clients of the item being on fire when it breaks, so they can e.g. make tanks explode.
//An alternative could be to allow clients to run OnFire effects, but I suspect it could lead to desyncs if/when there's minor
//discrepancies in the progress of the fires (which is most likely why running them was disabled on clients).
if (GameMain.NetworkMember is { IsServer: true } &&
condition <= 0.0f &&
StatusEffect.DurationList.Any(d => d.Targets.Contains(this) && d.Parent.HasTag(Barotrauma.Tags.OnFireStatusEffectTag)))
{
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnFire));
}
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
}
@@ -2326,10 +2378,11 @@ namespace Barotrauma
}
private bool isActive = true;
public bool IsInRemoveQueue;
public override void Update(float deltaTime, Camera cam)
{
if (!isActive || IsLayerHidden) { return; }
if (!isActive || IsLayerHidden || IsInRemoveQueue) { return; }
if (impactQueue != null)
{
@@ -2685,14 +2738,14 @@ namespace Barotrauma
partial void OnCollisionProjSpecific(float impact);
public override void FlipX(bool relativeToSub)
public override void FlipX(bool relativeToSub, bool force = false)
{
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipX(relativeToSub);
if (!Prefab.CanFlipX)
if (!Prefab.CanFlipX && !force)
{
flippedX = false;
FlippedX = false;
return;
}
@@ -2714,14 +2767,14 @@ namespace Barotrauma
SetContainedItemPositions();
}
public override void FlipY(bool relativeToSub)
public override void FlipY(bool relativeToSub, bool force = false)
{
//call the base method even if the item can't flip, to handle repositioning when flipping the whole sub
base.FlipY(relativeToSub);
if (!Prefab.CanFlipY)
if (!Prefab.CanFlipY && !force)
{
flippedY = false;
FlippedY = false;
return;
}
@@ -4059,6 +4112,10 @@ namespace Barotrauma
}
}
//store this at this point so we can tell the clients whether the effects had already been applied when the item was first loaded,
//(in which case a client should not execute them when they spawn the item)
item.OnInsertedEffectsAppliedOnPreviousRound = item.OnInsertedEffectsApplied;
item.ParseLinks(element, idRemap);
bool thisIsOverride = element.GetAttributeBool("isoverride", false);
@@ -4124,8 +4181,8 @@ namespace Barotrauma
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); }
float prevRotation = item.Rotation;
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(relativeToSub: false, force: true); }
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(relativeToSub: false, force: true); }
item.Rotation = prevRotation;
if (appliedSwap != null)
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
@@ -132,10 +133,30 @@ namespace Barotrauma
{
public readonly Identifier ItemPrefabIdentifier;
public ItemPrefab ItemPrefab =>
ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab;
[MaybeNull, AllowNull]
public ItemPrefab cachedItemPrefab;
[MaybeNull, AllowNull]
private Md5Hash prevContentPackagesHash;
[MaybeNull]
public ItemPrefab ItemPrefab
{
get
{
if (prevContentPackagesHash == null ||
!prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash))
{
cachedItemPrefab = ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab)
? prefab
: MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab;
prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash;
}
return cachedItemPrefab;
}
}
public override UInt32 UintIdentifier { get; }
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab == null ? Enumerable.Empty<ItemPrefab>() : ItemPrefab.ToEnumerable();
@@ -145,7 +166,7 @@ namespace Barotrauma
public override bool MatchesItem(Item item)
{
return item?.Prefab.Identifier == ItemPrefabIdentifier;
return item?.Prefab.Identifier == (ItemPrefab?.Identifier ?? ItemPrefabIdentifier);
}
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) :
@@ -711,6 +732,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
public bool HideConditionInTooltip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier UnlockedRecipeInToolTip { get; set; }
//if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
//if false, trigger areas define areas that can be used to highlight the item
[Serialize(true, IsPropertySaveable.No)]
@@ -864,7 +888,7 @@ namespace Barotrauma
[Serialize(10.0f, IsPropertySaveable.No)]
public float MaxScale { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Bots avoid rooms with dangerous items in them.")]
public bool IsDangerous { get; private set; }
private int maxStackSize;
@@ -1285,6 +1309,11 @@ namespace Barotrauma
this.LevelCommonness = levelCommonness.ToImmutableDictionary();
this.LevelQuantity = levelQuantity.ToImmutableDictionary();
//flipping holdable items vertically is not properly supported (uses the orientation of the physics body, which depends on which direction the character holding the item is facing)
//so let's by default make the item non-flippable, but if there's some use case where the item needs to flip vertically, it can be enabled by explicitly defining it in the XML.
bool canFlipYByDefault = ConfigElement.GetChildElement(nameof(Holdable)) == null;
CanFlipY = ConfigElement.GetAttributeBool(nameof(CanFlipY), def: canFlipYByDefault);
// Backwards compatibility
if (storePrices.Any())
{
@@ -34,6 +34,7 @@ namespace Barotrauma
this.Linkable = linkable;
this.AllowedLinks = (allowedLinks ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
this.Aliases = (aliases ?? Enumerable.Empty<string>()).Concat(identifier.Value.ToEnumerable()).ToImmutableHashSet();
this.Scale = 1;
}
public static CoreEntityPrefab HullPrefab { get; private set; }
@@ -570,11 +570,11 @@ namespace Barotrauma.MapCreatures.Behavior
if (HasBrokenThrough)
{
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 5 seconds
// I wasn't 100% sure what the performance impact on this so I decide to limit it to only check every 10 seconds
if (fireCheckCooldown <= 0)
{
UpdateFireSources();
fireCheckCooldown = 5f;
fireCheckCooldown = 10f;
}
else
{
@@ -8,6 +8,8 @@ namespace Barotrauma
private Vector2 maxSize;
public bool CausedByPsychosis;
protected override float SpreadToOtherHullsProbability => 0.0f;
public DummyFireSource(Vector2 maxSize, Vector2 worldPosition, Hull spawningHull = null, bool isNetworkMessage = false) :
base(worldPosition, spawningHull, sourceCharacter: null, isNetworkMessage: isNetworkMessage)
@@ -22,10 +22,11 @@ namespace Barotrauma
/// How often the FireSource checks whether it can spread to nearby hulls.
/// </summary>
const float SpreadToOtherHullsInterval = 5.0f;
/// <summary>
/// The probability of the fire spreading to a nearby hull when the <see cref="TrySpreadToNearbyHulls"/> check is made.
/// </summary>
const float SpreadToOtherHullsProbability = 0.15f;
protected virtual float SpreadToOtherHullsProbability => 0.15f;
protected Hull hull;
@@ -252,11 +253,14 @@ namespace Barotrauma
LimitSize();
spreadToOtherHullsTimer -= deltaTime;
if (spreadToOtherHullsTimer <= 0.0f)
if (SpreadToOtherHullsProbability > 0.0f)
{
TrySpreadToNearbyHulls();
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
spreadToOtherHullsTimer -= deltaTime;
if (spreadToOtherHullsTimer <= 0.0f)
{
TrySpreadToNearbyHulls();
spreadToOtherHullsTimer = SpreadToOtherHullsInterval;
}
}
if (size.X > 256.0f && this is not DummyFireSource)
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
@@ -829,13 +830,14 @@ namespace Barotrauma
foreach (var connectedGap in hull.ConnectedGaps)
{
if (connectedGap == this) { continue; }
if (connectedGap.IsRoomToRoom != IsRoomToRoom) { continue; }
//let the "more open" gap reduce this gap's flow rate
//or if they're both equally open, let the one that was created first handle it
//(note that we can't use Entity.ID here because gaps on walls don't have IDs)
if (connectedGap.open > open ||
(connectedGap.open == open && connectedGap.CreationIndex < CreationIndex))
{
Rectangle intersection = Rectangle.Intersect(rect, connectedGap.rect);
Rectangle intersection = Rectangle.Intersect(rect.ToWorldRect(), connectedGap.rect.ToWorldRect());
if (intersection.Width > 0 && intersection.Height > 0)
{
//reduce flow rate based on how much of this gap is covered by the connected one, and how open the connected one is
@@ -843,6 +845,7 @@ namespace Barotrauma
intersection.Height / (float)rect.Height :
intersection.Width / (float)rect.Width;
overlappingGapFlowRateReduction += relativeOverlap * connectedGap.open;
overlappingGaps.Add(connectedGap);
}
}
if (overlappingGapFlowRateReduction >= 1.0f)
@@ -442,6 +442,8 @@ namespace Barotrauma
/// </summary>
public bool IsBlue => ColorExtensions.IsBlueDominant(AveragePaintedColor, minimumAlpha: 100);
public const int MaxFireSources = 16;
public List<FireSource> FireSources { get; private set; }
public List<DummyFireSource> FakeFireSources { get; private set; }
@@ -731,6 +733,10 @@ namespace Barotrauma
}
else
{
if (FireSources.Count >= MaxFireSources)
{
return;
}
FireSources.Add(fireSource);
}
}
@@ -781,8 +787,9 @@ namespace Barotrauma
{
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
msg.WriteRangedInteger(Math.Min(FireSources.Count, 16), 0, 16);
for (int i = 0; i < Math.Min(FireSources.Count, 16); i++)
System.Diagnostics.Debug.Assert(FireSources.Count <= MaxFireSources, $"Too many fire sources ({FireSources.Count}) in hull {ID} (max {MaxFireSources}).");
msg.WriteRangedInteger(Math.Min(FireSources.Count, MaxFireSources), 0, MaxFireSources);
for (int i = 0; i < Math.Min(FireSources.Count, MaxFireSources); i++)
{
var fireSource = FireSources[i];
Vector2 normalizedPos = new Vector2(
@@ -828,7 +835,7 @@ namespace Barotrauma
{
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
int fireSourceCount = msg.ReadRangedInteger(0, 16);
int fireSourceCount = msg.ReadRangedInteger(0, MaxFireSources);
newFireSources = new NetworkFireSource[fireSourceCount];
for (int i = 0; i < fireSourceCount; i++)
{
@@ -2,6 +2,7 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
{
@@ -4481,6 +4481,10 @@ namespace Barotrauma
}
}
}
bool onlyEntrance = LevelData.Type != LevelData.LevelType.Outpost;
LocationType locationType = location?.Type;
if (missionForcedOutpostParamsId != null &&
OutpostGenerationParams.OutpostParams.TryGet(missionForcedOutpostParamsId, out var missionForcedOutpostParams))
{
@@ -4490,6 +4494,13 @@ namespace Barotrauma
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
else if (locationType != null &&
locationType.GetForcedOutpostGenerationParams() is { } forcedOutpostGenerationParams &&
//do not use the forced parameters if we want to generate only the entrance, and the parameters define a full, pre-built outpost
(!onlyEntrance || forcedOutpostGenerationParams.OutpostFilePath.IsNullOrEmpty()))
{
outpostGenerationParams = forcedOutpostGenerationParams;
}
else
{
outpostGenerationParams =
@@ -4497,7 +4508,6 @@ namespace Barotrauma
LevelData.GetSuitableOutpostGenerationParams(location, LevelData).GetRandom(Rand.RandSync.ServerAndClient);
}
LocationType locationType = location?.Type;
if (locationType == null)
{
locationType = LocationType.Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
@@ -4512,7 +4522,7 @@ namespace Barotrauma
if (location != null)
{
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: onlyEntrance, LevelData.AllowInvalidOutpost);
}
else
{
@@ -370,19 +370,36 @@ namespace Barotrauma
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location, LevelData levelData)
{
var suitableParams = OutpostGenerationParams.OutpostParams
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
var paramsForGameMode = OutpostGenerationParams.OutpostParams.Where(p =>
p.AllowedGameModeIdentifiers.None() || GameMain.GameSession?.GameMode is not GameMode gameMode || p.AllowedGameModeIdentifiers.Contains(gameMode.Preset.Identifier));
var paramsWithMatchingLevelType = paramsForGameMode
.Where(p => p.LevelType == null || levelData.Type == p.LevelType);
//1. try finding params specifically for this location type
var suitableParams = paramsWithMatchingLevelType
.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams
.Where(p => p.LevelType == null || levelData.Type == p.LevelType)
.Where(p => location == null || !p.AllowedLocationTypes.Any());
//2. not found, if the location type is configured to use the modules of some other location type,
// see if we could use that location type's generation params
if (!location.Type.UseOutpostModulesOfLocationType.IsEmpty)
{
suitableParams = paramsWithMatchingLevelType
.Where(p => p.AllowedLocationTypes.Contains(location.Type.UseOutpostModulesOfLocationType));
}
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
//3. still not found, choose some parameters that are suitable for any location type
suitableParams = paramsWithMatchingLevelType
.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = paramsForGameMode;
}
}
}
return suitableParams;
}
@@ -37,7 +37,7 @@ namespace Barotrauma.RuinGeneration
public void Generate(Level level, LocationType locationType, Point position, bool mirror = false)
{
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false);
Submarine = OutpostGenerator.Generate(generationParams, locationType, onlyEntrance: false, allowInvalidOutpost: level.LevelData.AllowInvalidOutpost);
Submarine.Info.Name = $"Ruin ({level.Seed})";
Submarine.Info.Type = SubmarineType.Ruin;
Submarine.TeamID = CharacterTeamType.None;
@@ -478,6 +478,11 @@ namespace Barotrauma
}
}
/// <summary>
/// Missions that are available and visible in menus (<see cref="MissionPrefab.ShowInMenus"/>)
/// </summary>
public IEnumerable<Mission> AvailableAndVisibleMissions => AvailableMissions.Where(m => m.Prefab.ShowInMenus);
private readonly List<Mission> selectedMissions = new List<Mission>();
public IEnumerable<Mission> SelectedMissions
{
@@ -580,9 +585,9 @@ namespace Barotrauma
return $"Location ({DisplayName ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
public Location(Vector2 mapPosition, int? zone, Identifier? biomeId, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, biomeId, requireOutpost);
CreateRandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
@@ -782,12 +787,12 @@ namespace Barotrauma
}
}
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
public static Location CreateRandom(Vector2 position, int? zone, Identifier? biomeId, Random rand, bool requireOutpost, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
return new Location(position, zone, biomeId, rand, requireOutpost, forceLocationType, existingLocations);
}
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true)
public void ChangeType(CampaignMode campaign, LocationType newType, bool createStores = true, bool unlockInitialMissions = true)
{
if (newType == Type) { return; }
@@ -826,10 +831,10 @@ namespace Barotrauma
if (Type.Faction == Identifier.Empty) { Faction = null; }
if (Type.SecondaryFaction == Identifier.Empty) { SecondaryFaction = null; }
}
if (!IsCriticallyRadiated())
if (unlockInitialMissions && !IsCriticallyRadiated())
{
UnlockInitialMissions(Rand.RandSync.Unsynced);
UnlockInitialMissions(Rand.RandSync.Unsynced);
}
if (createStores)
@@ -1257,7 +1262,7 @@ namespace Barotrauma
{
if (type?.NameFormats == null || !type.NameFormats.Any() || nameFormatIndex < 0)
{
return TextManager.Get(nameId);
return TextManager.Get(nameId).Fallback(nameId.Value);
}
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
}
@@ -13,7 +13,7 @@ namespace Barotrauma
class LocationType : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
private readonly ImmutableArray<string> rawNames;
private readonly ImmutableArray<Sprite> portraits;
@@ -21,13 +21,14 @@ namespace Barotrauma
private readonly ImmutableArray<(Identifier Identifier, float Commonness, bool AlwaysAvailableIfMissingFromCrew)> hireableJobs;
private readonly float totalHireableWeight;
public readonly Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
public readonly Dictionary<int, int> MinCountPerZone = new Dictionary<int, int>();
public readonly LocalizedString Name;
public readonly LocalizedString Description;
public readonly Identifier ForceLocationName;
/// <summary>
/// Forces all locations of this LocationType to use the given name. Can either be a text tag or the actual name.
/// </summary>
public readonly Identifier ForceLocationName;
public readonly float BeaconStationChance;
@@ -43,11 +44,151 @@ namespace Barotrauma
public readonly ImmutableArray<Identifier> MissionIdentifiers;
public readonly ImmutableArray<Identifier> MissionTags;
public readonly List<string> HideEntitySubcategories = new List<string>();
public abstract class AreaSettingData
{
public int? MinCount { get; }
public int? MaxCount { get; }
public float Commonness { get; }
public bool IsEnterable { get; private set; }
/// <summary>
/// Desired position in the biome or difficulty zone. A value between 0 and 1, where 0 is the left side of the zone/biome, and 1 the right side.
/// </summary>
public float? DesiredPosition { get; }
public bool HasCounts => MinCount.HasValue && MaxCount.HasValue && (MaxCount > 0 || MinCount > 0);
public virtual bool HasValidData => true;
public bool AllowAsBiomeGate { get; private set; }
internal AreaSettingData(int? minCount, int? maxCount, float commonness, float? desiredPosition)
{
MinCount = minCount;
MaxCount = maxCount;
Commonness = commonness;
DesiredPosition = desiredPosition;
}
public virtual bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return false;
}
public virtual bool MatchesLocation(Map map, Location location)
{
return false;
}
public virtual bool MatchesZone(int zoneIndex)
{
return false;
}
public virtual bool MatchesBiome(Identifier biomeIdentifier)
{
return false;
}
public virtual bool Matches(int? zone = null, Identifier? biomeId = null)
{
return false;
}
}
public class BiomeSettingData : AreaSettingData
{
public Identifier BiomeIdentifier { get; }
public override bool HasValidData => Biome.Prefabs.ContainsKey(BiomeIdentifier);
public BiomeSettingData(Identifier biomeIdentifier, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
: base(minCount, maxCount, commonness, desiredPosition)
{
if (minCount > maxCount)
{
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in biome {biomeIdentifier}.",
contentPackage: locationType.ContentPackage);
}
BiomeIdentifier = biomeIdentifier;
}
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return locationTypeCount.BiomeId == BiomeIdentifier;
}
public override bool MatchesLocation(Map map, Location location)
{
return BiomeIdentifier == location.Biome?.Identifier;
}
public override bool MatchesBiome(Identifier biomeIdentifier)
{
return BiomeIdentifier == biomeIdentifier;
}
public override bool Matches(int? zone = null, Identifier? biomeId = null)
{
return biomeId.HasValue && biomeId == BiomeIdentifier;
}
}
public class DifficultyZoneSettingData : AreaSettingData
{
public int DifficultyZone { get; }
public DifficultyZoneSettingData(int difficultyZone, int? minCount, int? maxCount, float commonness, float? desiredPosition, LocationType locationType)
: base(minCount, maxCount, commonness, desiredPosition)
{
if (minCount > maxCount)
{
DebugConsole.AddWarning($"Error in location type {locationType.Identifier}: minimum count larger than maximum count in difficulty zone {difficultyZone}.",
contentPackage: locationType.ContentPackage);
}
DifficultyZone = difficultyZone;
}
public override bool MatchesRemainingCount(MapLocationTypeGenerator.LocationTypeCount locationTypeCount)
{
return locationTypeCount.DifficultyZone == DifficultyZone;
}
public override bool MatchesLocation(Map map, Location location)
{
return DifficultyZone == map.GetZoneIndex(location.MapPosition.X);
}
public override bool MatchesZone(int zoneIndex)
{
return DifficultyZone == zoneIndex;
}
public override bool Matches(int? zone = null, Identifier? biomeId = null)
{
return zone.HasValue && zone == DifficultyZone;
}
}
public readonly List<AreaSettingData> AreaSettings = new List<AreaSettingData>();
public readonly List<string> HideEntitySubcategories;
public enum BiomeGateSetting
{
/// <summary>
/// Can be used as a gate between biomes, but not required
/// </summary>
Allow,
/// <summary>
/// Cannot be used as a gate between biomes
/// </summary>
Deny,
/// <summary>
/// Must be used as a gate between biomes during map generation
/// </summary>
Force
}
public BiomeGateSetting BiomeGate { get; private set; }
public bool ForceAsStartOutpost { get; private set; }
/// <summary>
/// Can this location type be used in the random, non-campaign levels that don't take place in any specific zone
@@ -108,10 +249,26 @@ namespace Barotrauma
private readonly Identifier forceOutpostGenerationParamsIdentifier;
/// <summary>
/// Can be used to make the location type use the same background music tracks as another location type.
/// </summary>
public readonly Identifier BackgroundMusicLocationType;
/// <summary>
/// If set to true, only event sets that explicitly define this location type in <see cref="EventSet.LocationTypeIdentifiers"/> can be selected at this location. Defaults to false.
/// </summary>
public bool IgnoreGenericEvents { get; }
/// <summary>
/// Used as criteria for validating if a given event set is suitable for this locationType.
/// For example, if set to "city", events that appear in "city" type locations can also appear here.
/// </summary>
public Identifier EventLocationType { get; private set; }
/// <summary>
/// If set, outpost modules configured to be suitable for the specified location type can also be used in this type of location.
/// </summary>
public Identifier UseOutpostModulesOfLocationType { get; set; }
public Color SpriteColor
{
@@ -134,7 +291,7 @@ namespace Barotrauma
public int DailySpecialsCount { get; } = 1;
public int RequestedGoodsCount { get; } = 1;
public readonly bool ShowSonarMarker = true;
public readonly bool ShowSonarMarker;
public override string ToString()
{
@@ -145,13 +302,41 @@ namespace Barotrauma
{
Name = TextManager.Get("LocationName." + Identifier, "unknown");
Description = TextManager.Get("LocationDescription." + Identifier, "");
// for location types based on others, e.g., Named Unique outpost, we may want to override the name of the type to still say Outpost on the map:
var forceNameId = element.GetAttributeIdentifier("ForceLocationTypeName", string.Empty);
if (!forceNameId.IsEmpty)
{
var forcedName = TextManager.Get("LocationName." + forceNameId);
if (!forcedName.IsNullOrEmpty())
{
Name = forcedName;
}
}
var forceDescriptionId = element.GetAttributeIdentifier("ForceLocationTypeDescription", string.Empty);
if (!forceDescriptionId.IsEmpty)
{
var forcedDescription = TextManager.Get("LocationDescription." + forceDescriptionId);
if (!forcedDescription.IsNullOrEmpty())
{
Description = forcedDescription;
}
}
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
UsePortraitInRandomLoadingScreens = element.GetAttributeBool(nameof(UsePortraitInRandomLoadingScreens), true);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
AllowAsBiomeGate = element.GetAttributeBool(nameof(AllowAsBiomeGate), true);
bool allowAsBiomeGateLegacy = element.GetAttributeBool("allowasbiomegate", true);
BiomeGate = element.GetAttributeEnum("BiomeGate", def: allowAsBiomeGateLegacy ? BiomeGateSetting.Allow : BiomeGateSetting.Deny);
if (BiomeGate != BiomeGateSetting.Deny && !HasOutpost)
{
DebugConsole.AddWarning($"Potential error in location type {Identifier}: the location is set to be allowed as a biome gate, but will never be chosen as one because it has no outpost.",
contentPackage: ContentPackage);
}
ForceAsStartOutpost = element.GetAttributeBool(nameof(ForceAsStartOutpost), false);
AllowInRandomLevels = element.GetAttributeBool(nameof(AllowInRandomLevels), true);
Faction = element.GetAttributeIdentifier(nameof(Faction), Identifier.Empty);
@@ -168,17 +353,24 @@ namespace Barotrauma
DescriptionInRadiation = element.GetAttributeIdentifier(nameof(DescriptionInRadiation), "locationdescription.abandonedirradiated");
forceOutpostGenerationParamsIdentifier = element.GetAttributeIdentifier("forceoutpostgenerationparams", Identifier.Empty);
BackgroundMusicLocationType = element.GetAttributeIdentifier(nameof(BackgroundMusicLocationType), Identifier.Empty);
IgnoreGenericEvents = element.GetAttributeBool(nameof(IgnoreGenericEvents), false);
EventLocationType = element.GetAttributeIdentifier(nameof(EventLocationType), Identifier.Empty);
UseOutpostModulesOfLocationType = element.GetAttributeIdentifier(nameof(UseOutpostModulesOfLocationType), Identifier.Empty);
IsAnyOutpost = element.GetAttributeBool(nameof(IsAnyOutpost), def: HasOutpost);
string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC");
Enum.TryParse(teamStr, out OutpostTeam);
if (element.GetAttribute("name") != null)
if (element.GetAttribute(nameof(ForceLocationName)) != null ||
element.GetAttribute("name") != null)
{
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
ForceLocationName = element.GetAttributeIdentifier(nameof(ForceLocationName),
//backwards compatibility
def: element.GetAttributeIdentifier("name", string.Empty));
}
else
{
@@ -214,15 +406,18 @@ namespace Barotrauma
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
foreach (string commonnessPerZoneStr in commonnessPerZoneStrs)
{
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
string[] splitCommonnessPerZone = commonnessPerZoneStr.Split(':');
if (splitCommonnessPerZone.Length != 2 ||
!int.TryParse(splitCommonnessPerZone[0].Trim(), out int zoneIndex) ||
!float.TryParse(splitCommonnessPerZone[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float zoneCommonness))
{
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"");
DebugConsole.ThrowError("Failed to read commonness values for location type \"" + Identifier + "\" - commonness should be given in the format \"zone1index: zone1commonness, zone2index: zone2commonness\"", contentPackage: element.ContentPackage);
break;
}
CommonnessPerZone[zoneIndex] = zoneCommonness;
if (zoneCommonness <= 0.0f) { continue; }
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness, minCount: null);
}
string[] minCountPerZoneStrs = element.GetAttributeStringArray("mincountperzone", Array.Empty<string>());
@@ -233,13 +428,40 @@ namespace Barotrauma
!int.TryParse(splitMinCountPerZone[0].Trim(), out int zoneIndex) ||
!int.TryParse(splitMinCountPerZone[1].Trim(), out int minCount))
{
DebugConsole.ThrowError("Failed to read minimum count values for location type \"" + Identifier + "\" - minimum counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"");
DebugConsole.ThrowError("Failed to read minimum zone count values for location type \"" + Identifier +
"\" - minimum zone counts should be given in the format \"zone1index: zone1mincount, zone2index: zone2mincount\"", contentPackage: element.ContentPackage);
break;
}
MinCountPerZone[zoneIndex] = minCount;
if (minCount <= 0) { continue; }
AugmentDifficultyZoneSettings(zoneIndex, zoneCommonness: null, minCount);
}
var portraits = new List<Sprite>();
var hireableJobs = new List<(Identifier, float, bool)>();
void AugmentDifficultyZoneSettings(int zoneIndex, float? zoneCommonness, int? minCount)
{
var existingSettings = AreaSettings.Find(areaSettingData => areaSettingData is DifficultyZoneSettingData difficultyZoneSettingData &&
difficultyZoneSettingData.DifficultyZone == zoneIndex);
if (existingSettings != null)
{
int index = AreaSettings.IndexOf(existingSettings);
AreaSettings[index] = new DifficultyZoneSettingData(zoneIndex,
minCount ?? existingSettings.MinCount,
//note that assigning minCount to maxCount is intentional here:
//previously it was only possible to define minCount (essentially the same as just defining "count" now)
maxCount: minCount ?? existingSettings.MaxCount,
commonness: zoneCommonness ?? existingSettings.Commonness, desiredPosition: null, locationType: this);
}
else
{
AreaSettings.Add(new DifficultyZoneSettingData(zoneIndex, minCount ?? 0, maxCount: minCount ?? 0, commonness: zoneCommonness ?? 0, desiredPosition: null, locationType: this));
}
}
var portraitsList = new List<Sprite>();
var hireableJobsList = new List<(Identifier, float, bool)>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -249,7 +471,7 @@ namespace Barotrauma
float jobCommonness = subElement.GetAttributeFloat("commonness", 1.0f);
bool availableIfMissing = subElement.GetAttributeBool("AlwaysAvailableIfMissingFromCrew", false);
totalHireableWeight += jobCommonness;
hireableJobs.Add((jobIdentifier, jobCommonness, availableIfMissing));
hireableJobsList.Add((jobIdentifier, jobCommonness, availableIfMissing));
break;
case "symbol":
Sprite = new Sprite(subElement, lazyLoad: true);
@@ -265,7 +487,7 @@ namespace Barotrauma
var portrait = new Sprite(subElement, lazyLoad: true);
if (portrait != null)
{
portraits.Add(portrait);
portraitsList.Add(portrait);
}
break;
case "store":
@@ -281,10 +503,71 @@ namespace Barotrauma
DailySpecialsCount = subElement.GetAttributeInt("dailyspecialscount", DailySpecialsCount);
RequestedGoodsCount = subElement.GetAttributeInt("requestedgoodscount", RequestedGoodsCount);
break;
case "areasettings":
ParseAreaSettings(subElement);
break;
}
}
this.portraits = portraitsList.ToImmutableArray();
this.hireableJobs = hireableJobsList.ToImmutableArray();
void ParseAreaSettings(ContentXElement areaSettingsElement)
{
Identifier biomeIdentifier = areaSettingsElement.GetAttributeIdentifier("biome", Identifier.Empty);
int zone = areaSettingsElement.GetAttributeInt("zone", 0);
if (biomeIdentifier == Identifier.Empty && zone == 0)
{
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - biome identifier and zone are both missing.", contentPackage: element.ContentPackage);
return;
}
if (biomeIdentifier != Identifier.Empty && zone != 0)
{
DebugConsole.ThrowError("Failed to read area settings for locationType \"" + Identifier + "\" - both biome identifier and zone are defined. Must be one or the other.", contentPackage: element.ContentPackage);
return;
}
bool HasComma(string intAttributeName)
{
var attr = areaSettingsElement.GetAttribute(intAttributeName);
if (attr == null) { return false;}
return attr.Value.Contains(',');
}
if (HasComma("mincount") || HasComma("maxcount") || HasComma("count"))
{
DebugConsole.LogError($"AreaSettings for locationType {Identifier} has comma inside int count attribute. This causes the resulting parse to combine the numbers, resulting in incorrect amount of locations.",
contentPackage: ContentPackage);
}
int? minCount = areaSettingsElement.GetAttributeNullableInt("mincount");
int? maxCount = areaSettingsElement.GetAttributeNullableInt("maxcount");
int? count = areaSettingsElement.GetAttributeNullableInt("count");
float? desiredPosition = areaSettingsElement.GetAttributeNullableFloat("desiredposition");
float commonness = areaSettingsElement.GetAttributeFloat("commonness", 0);
// if set, count overrides min and max count to eliminate randomness
if (count.HasValue)
{
minCount = count;
maxCount = count;
}
else if (minCount.HasValue && maxCount.HasValue && minCount <= 0 && maxCount <= 0)
{
DebugConsole.AddWarning("Failed to read count value for location type \"" + Identifier + "\" - both min and max count are 0.", contentPackage: element.ContentPackage);
return;
}
if (biomeIdentifier != Identifier.Empty)
{
AreaSettings.Add(new BiomeSettingData(biomeIdentifier, minCount, maxCount, commonness, desiredPosition, locationType: this));
}
else
{
AreaSettings.Add(new DifficultyZoneSettingData(zone, minCount, maxCount, commonness, desiredPosition, locationType: this));
}
}
this.portraits = portraits.ToImmutableArray();
this.hireableJobs = hireableJobs.ToImmutableArray();
}
public IEnumerable<JobPrefab> GetHireablesMissingFromCrew()
@@ -383,7 +666,7 @@ namespace Barotrauma
return rawNames[rand.Next() % rawNames.Length];
}
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
public static LocationType Random(Random rand, int? zone = null, Identifier? biomeId = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
{
Debug.Assert(Prefabs.Any(), "LocationType.list.Count == 0, you probably need to initialize LocationTypes");
@@ -392,19 +675,22 @@ namespace Barotrauma
(predicate == null || predicate(lt)) && IsValid(lt))
.OrderBy(p => p.UintIdentifier).ToArray();
bool IsValid(LocationType lt)
bool IsValid(LocationType locationType)
{
if (requireOutpost && !lt.HasOutpost) { return false; }
if (zone.HasValue)
{
if (!lt.CommonnessPerZone.ContainsKey(zone.Value)) { return false; }
}
if (requireOutpost && !locationType.HasOutpost) { return false; }
bool validZone = !zone.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesZone(zone.Value));
bool validBiome = !biomeId.HasValue || locationType.AreaSettings.Any(areaSetting => areaSetting.MatchesBiome(biomeId.Value));
if (!validZone && !validBiome) { return false; }
//if zone is not defined, this is a "random" (non-campaign) level
//-> don't choose location types that aren't allowed in those
else if (!lt.AllowInRandomLevels)
if (!zone.HasValue && !biomeId.HasValue && !locationType.AllowInRandomLevels)
{
return false;
}
return true;
}
@@ -413,11 +699,12 @@ namespace Barotrauma
DebugConsole.ThrowError("Could not generate a random location type - no location types for the zone " + zone + " found!");
}
if (zone.HasValue)
if (zone.HasValue || biomeId.HasValue)
{
return ToolBox.SelectWeightedRandom(
allowedLocationTypes,
allowedLocationTypes.Select(a => a.CommonnessPerZone[zone.Value]).ToArray(),
allowedLocationTypes.Select(allowedType =>
allowedType.AreaSettings.Find(areaSetting => areaSetting.MatchesZone(zone.Value) || areaSetting.MatchesBiome(biomeId.Value))?.Commonness ?? 0).ToArray(),
rand);
}
else
@@ -425,6 +712,17 @@ namespace Barotrauma
return allowedLocationTypes[rand.Next() % allowedLocationTypes.Length];
}
}
public bool IsValidForZoneOrBiome(int? zone, Identifier? biomeIdentifier)
{
//if zone is not defined, this is a "random" (non-campaign) level
//-> don't choose location types that aren't allowed in those
if (!zone.HasValue && !AllowInRandomLevels) { return false; }
if (!zone.HasValue && !biomeIdentifier.HasValue) { return true; }
return AreaSettings.Any(setting => setting.Matches(zone, biomeIdentifier));
}
public OutpostGenerationParams GetForcedOutpostGenerationParams()
{
@@ -434,6 +732,11 @@ namespace Barotrauma
}
return null;
}
public bool HasCounts()
{
return AreaSettings.Any(setting => setting.HasCounts);
}
public override void Dispose() { }
}
@@ -77,6 +77,9 @@ namespace Barotrauma
public Radiation Radiation;
private bool trackedLocationDiscoveryAndVisitOrder = true;
private IOrderedEnumerable<Biome> _orderedBiomes;
public IOrderedEnumerable<Biome> OrderedBiomes => _orderedBiomes ??= Biome.Prefabs.GetOrdered();
public Map(CampaignSettings settings)
{
@@ -240,10 +243,7 @@ namespace Barotrauma
Vector2 mapPos = new Vector2(
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations)
{
Biome = endLocations.First().Biome
};
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, firstEndLocation.Biome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
Locations.Add(newEndLocation);
endLocations.Add(newEndLocation);
@@ -365,6 +365,14 @@ namespace Barotrauma
{
CurrentLocation.ChangeType(campaign, tutorialOutpost);
}
else
{
var forceStartOutpostType = LocationType.Prefabs.Where(lt => lt.ForceAsStartOutpost).GetRandom(Rand.RandSync.ServerAndClient);
if (forceStartOutpostType != null)
{
CurrentLocation.ChangeType(campaign, forceStartOutpostType);
}
}
Discover(CurrentLocation);
Visit(CurrentLocation);
CurrentLocation.CreateStores();
@@ -396,13 +404,16 @@ namespace Barotrauma
y + generationParams.VoronoiSiteVariance.Y * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient)));
}
}
// put some of this stuff in a helper class, this function is getting unwieldy
MapLocationTypeGenerator mapLocationTypeGenerator = new MapLocationTypeGenerator(campaign, this);
Voronoi voronoi = new Voronoi(0.5f);
List<GraphEdge> edges = voronoi.MakeVoronoiGraph(voronoiSites, Width, Height);
Vector2 margin = new Vector2(
Math.Min(10, Width * 0.1f),
Math.Min(10, Height * 0.2f));
Math.Min(10, Width * 0.1f),
Math.Min(10, Height * 0.2f));
float startX = margin.X, endX = Width - margin.X;
float startY = margin.Y, endY = Height - margin.Y;
@@ -413,8 +424,6 @@ 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; }
@@ -442,33 +451,18 @@ namespace Barotrauma
Vector2 position = points[positionIndex];
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
int zone = GetZoneIndex(position.X);
if (!locationsPerZone.ContainsKey(zone))
{
locationsPerZone[zone] = new List<Location>();
}
LocationType forceLocationType = null;
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;
}
}
}
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.ServerAndClient),
requireOutpost: false, forceLocationType: forceLocationType, existingLocations: Locations);
locationsPerZone[zone].Add(newLocations[i]);
newLocations[i] = Location.CreateRandom(position, zone, GetBiome(position.X)?.Identifier, Rand.GetRNG(Rand.RandSync.ServerAndClient),
requireOutpost: false, forceLocationType: null, existingLocations: Locations);
mapLocationTypeGenerator.AddToLocationsPerZone(zone, newLocations[i]);
Locations.Add(newLocations[i]);
}
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
Connections.Add(newConnection);
Connections.Add(newConnection);
}
//remove connections that are too short
@@ -481,7 +475,6 @@ namespace Barotrauma
{
continue;
}
//locations.Remove(connection.Locations[0]);
Connections.Remove(connection);
@@ -572,7 +565,6 @@ namespace Barotrauma
{
(zone1, zone2) = (zone2, zone1);
}
if (generationParams.GateCount[zone1] == 0) { continue; }
if (!connectionsBetweenZones[zone1].Any())
@@ -589,7 +581,7 @@ namespace Barotrauma
}
}
else if (connectionsBetweenZones[zone1].Count() < generationParams.GateCount[zone1] &&
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
connectionsBetweenZones[zone1].None(c => c.Locations.Contains(connection.Locations[0]) || c.Locations.Contains(connection.Locations[1])))
{
connectionsBetweenZones[zone1].Add(connection);
}
@@ -599,6 +591,8 @@ namespace Barotrauma
}
}
var orderedPrefabs = LocationType.Prefabs.GetOrdered();
List<Location> forciblyReassignedGateLocations = new List<Location>();
var gateFactions = campaign.Factions.Where(f => f.Prefab.ControlledOutpostPercentage > 0).OrderBy(f => f.Prefab.Identifier).ToList();
for (int i = Connections.Count - 1; i >= 0; i--)
{
@@ -617,20 +611,37 @@ namespace Barotrauma
{
var leftMostLocation =
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
Connections[i].Locations[0] :
Connections[i].Locations[1];
Connections[i].Locations[0] :
Connections[i].Locations[1];
if (!AllowAsBiomeGate(leftMostLocation.Type))
{
var potentialGateLocationTypes = orderedPrefabs.Where(AllowAsBiomeGate);
LocationType gateLocationType =
//choose some location type that's allowed in this zone/biome, and has a non zero commonness (instead of some fixed count)
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.Commonness > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
//if not found, use something with a fixed count
potentialGateLocationTypes.Where(lt => lt.AreaSettings.Any(areaSettings => areaSettings.MatchesLocation(this, leftMostLocation) && areaSettings.MinCount > 0)).GetRandom(Rand.RandSync.ServerAndClient) ??
//if that's not found either, try finding a type that doesn't spawn in any biome, but is allowed as a biome gate
//(a mod might have some special "biome gate" location types that are meant just for the gate locations)
potentialGateLocationTypes.Where(lt => lt.AreaSettings.None(areaSettings => areaSettings.Commonness > 0.0f || areaSettings.HasCounts)).GetRandom(Rand.RandSync.ServerAndClient);
if (gateLocationType == null)
{
DebugConsole.ThrowError($"Failed to find a suitable location type for a gate location between zones {zone1} and {zone2}.");
continue;
}
leftMostLocation.ChangeType(
campaign,
LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => AllowAsBiomeGate(lt)),
createStores: false);
gateLocationType,
createStores: false,
unlockInitialMissions: false);
forciblyReassignedGateLocations.Add(leftMostLocation);
}
static bool AllowAsBiomeGate(LocationType lt)
{
//checking for "abandoned" is not strictly necessary here because it's now configured to not be allowed as a biome gate
//but might be better to keep it for backwards compatibility (previously we relied only on that check)
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.AllowAsBiomeGate;
return lt.HasOutpost && lt.Identifier != "abandoned" && lt.BiomeGate != LocationType.BiomeGateSetting.Deny;
}
leftMostLocation.IsGateBetweenBiomes = true;
@@ -663,8 +674,8 @@ namespace Barotrauma
if (!connection.Locked) { continue; }
var rightMostLocation =
connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ?
connection.Locations[0] :
connection.Locations[1];
connection.Locations[0] :
connection.Locations[1];
//if all of the other connected locations are to the left (= if there's no path forwards from the outpost),
//create a new connection to the closest location to the right
@@ -693,8 +704,18 @@ namespace Barotrauma
//remove orphans
Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l)));
AssignBiomes(Rand.GetRNG(Rand.RandSync.ServerAndClient));
AssignBiomes(new MTRandom(ToolBox.StringToInt(Seed)));
var gateLocations = Locations.Where(l => l.IsGateBetweenBiomes);
foreach (var gateLocation in forciblyReassignedGateLocations)
{
//remove the gate locations who's types we've reassigned from the remaining types left to assign,
//(i.e. if we want just 1 of some location type, and we were forced to choose it as a gate, don't use that type again)
//must be done after assigning biomes
mapLocationTypeGenerator.RemoveOneFromTotals(gateLocation.Type, gateLocation);
}
mapLocationTypeGenerator.AssignForcedBiomeGateTypes(gateLocations);
foreach (LocationConnection connection in Connections)
{
@@ -713,8 +734,13 @@ namespace Barotrauma
if (LocationType.Prefabs.TryGet("outpost", out LocationType startLocationType))
{
startLocation.ChangeType(campaign, startLocationType, createStores: false);
mapLocationTypeGenerator.AddToFilled(startLocation);
}
mapLocationTypeGenerator.AssignLocationTypesBasedOnDesiredPosition(gateLocations);
//create proper level data and stores for all locations
//(needs to be done before AssignLocationCounts, since LevelData may be required for the location type changes)
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
@@ -733,6 +759,15 @@ namespace Barotrauma
}
}
}
//needs to be done after the LevelData has been assigned above
foreach (var gateLocation in forciblyReassignedGateLocations)
{
gateLocation.UnlockInitialMissions(Rand.RandSync.ServerAndClient);
}
List<Location> locationsToAssign = Locations.ToList();
locationsToAssign.Remove(GetPreviousToEndLocation());
mapLocationTypeGenerator.AssignLocationTypesBasedOnCount(gateLocations, locations: locationsToAssign);
foreach (LocationConnection connection in Connections)
{
@@ -740,7 +775,6 @@ namespace Barotrauma
}
CreateEndLocation(campaign);
float CalculateDifficulty(float mapPosition, Biome biome)
{
float settingsFactor = campaign.Settings.LevelDifficultyMultiplier;
@@ -779,23 +813,27 @@ namespace Barotrauma
float zoneWidth = Width / generationParams.DifficultyZones;
int zoneIndex = (int)Math.Floor(xPos / zoneWidth) + 1;
zoneIndex = Math.Clamp(zoneIndex, 1, generationParams.DifficultyZones - 1);
return Biome.Prefabs.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
return OrderedBiomes.FirstOrDefault(b => b.AllowedZones.Contains(zoneIndex));
}
/// <summary>
/// Assign biomes for the connections between locations, ad for the locations that don't yet have a biome assigned.
/// </summary>
private void AssignBiomes(Random rand)
{
var biomes = Biome.Prefabs;
float zoneWidth = Width / generationParams.DifficultyZones;
List<Biome> allowedBiomes = new List<Biome>(10);
for (int i = 0; i < generationParams.DifficultyZones; i++)
{
int zoneIndex = i + 1;
allowedBiomes.Clear();
allowedBiomes.AddRange(biomes.Where(b => b.AllowedZones.Contains(generationParams.DifficultyZones - i)));
float zoneX = zoneWidth * (generationParams.DifficultyZones - i);
allowedBiomes.AddRange(OrderedBiomes.Where(b => b.AllowedZones.Contains(zoneIndex)));
float zoneX = zoneWidth * (zoneIndex);
foreach (Location location in Locations)
{
if (location.Biome != null) { continue; }
if (location.MapPosition.X < zoneX)
{
location.Biome = allowedBiomes[rand.Next() % allowedBiomes.Count];
@@ -812,6 +850,9 @@ namespace Barotrauma
System.Diagnostics.Debug.Assert(Connections.All(c => c.Biome != null));
}
/// <summary>
/// Returns the location prior to the final location. The type of this location is hard-coded just as that of the final location.
/// </summary>
private Location GetPreviousToEndLocation()
{
Location previousToEndLocation = null;
@@ -970,9 +1011,8 @@ namespace Barotrauma
}
}
#endregion Generation
public void MoveToNextLocation()
{
if (SelectedLocation == null && Level.Loaded?.EndLocation != null)
@@ -1167,7 +1207,6 @@ namespace Barotrauma
{
List<Location> nextLocations = CurrentLocation.Connections.Where(c => !c.Locked).Select(c => c.OtherLocation(CurrentLocation)).ToList();
List<Location> undiscoveredLocations = nextLocations.FindAll(l => !l.Discovered);
if (undiscoveredLocations.Count > 0 && preferUndiscovered)
{
SelectLocation(undiscoveredLocations[Rand.Int(undiscoveredLocations.Count, Rand.RandSync.Unsynced)]);
@@ -1285,8 +1324,8 @@ namespace Barotrauma
{
location.PendingLocationTypeChange =
(location.PendingLocationTypeChange.Value.typeChange,
location.PendingLocationTypeChange.Value.delay - 1,
location.PendingLocationTypeChange.Value.parentMission);
location.PendingLocationTypeChange.Value.delay - 1,
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
return ChangeLocationType(campaign, location, location.PendingLocationTypeChange.Value.typeChange);
@@ -1317,8 +1356,8 @@ namespace Barotrauma
{
location.PendingLocationTypeChange =
(selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
null);
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
parentMission: null);
}
else
{
@@ -1554,7 +1593,7 @@ namespace Barotrauma
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
LocalizedString prevLocationName = location.DisplayName;
LocationType prevLocationType = location.Type;
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.GetOrdered().First();
location.ChangeType(campaign, newLocationType);
if (showNotifications && prevLocationType != location.Type)
@@ -1636,7 +1675,7 @@ namespace Barotrauma
if (index < 0) { return null; }
return Locations[index];
}
}
void Discover(Location location)
@@ -1774,4 +1813,4 @@ namespace Barotrauma
partial void RemoveProjSpecific();
}
}
}
@@ -0,0 +1,397 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal class MapLocationTypeGenerator
{
internal class LocationTypeCount
{
public int AmountToAssign;
public int? DifficultyZone;
public Identifier? BiomeId;
public LocationTypeCount(int amountToAssign, int difficultyZone)
{
AmountToAssign = amountToAssign;
DifficultyZone = difficultyZone;
}
public LocationTypeCount(int amountToAssign, Identifier biomeId)
{
AmountToAssign = amountToAssign;
BiomeId = biomeId;
}
public string ToDebugString()
{
if (DifficultyZone.HasValue)
{
return $"x{AmountToAssign} in (zone {DifficultyZone.Value})";
}
else if (BiomeId.HasValue)
{
return $"x{AmountToAssign} in (biome {BiomeId.Value})";
}
return $"x{AmountToAssign}";
}
}
/// <summary>
/// Actual amounts of location types that need to be assigned to locations, after resolving random variation from min/max counts.
/// </summary>
private Dictionary<LocationType, List<LocationTypeCount>> locationTypeAmountsToAssign;
private readonly Map map;
private readonly CampaignMode campaign;
/// <summary>
/// List of locations that have been filled from specific requirements and should not be overwritten by some subsequent generation pass (e.g. the start outpost).
/// </summary>
private readonly List<Location> filledLocations;
private readonly Dictionary<int, List<Location>> locationsPerZone = new Dictionary<int, List<Location>>();
private readonly IOrderedEnumerable<LocationType> orderedLocationTypes = LocationType.Prefabs.GetOrdered();
private bool IsEveryLocationTypeAssigned
{
get
{
return locationTypeAmountsToAssign.SelectMany(kvp => kvp.Value)
.None(locationTypeCount => locationTypeCount.AmountToAssign > 0);
}
}
public MapLocationTypeGenerator(CampaignMode campaign, Map map)
{
filledLocations = new List<Location>();
this.map = map;
this.campaign = campaign;
locationTypeAmountsToAssign = new Dictionary<LocationType, List<LocationTypeCount>>();
GenerateTotalAmountsToAssign();
}
private void GenerateTotalAmountsToAssign()
{
foreach (var locationTypePrefab in orderedLocationTypes)
{
foreach (var areaSetting in locationTypePrefab.AreaSettings)
{
if (!areaSetting.HasCounts) { continue; }
if (!areaSetting.HasValidData)
{
// the only case for invalid data right now is the biome id
DebugConsole.AddWarning($"Biome ID is invalid for AreaSetting in locationType '{locationTypePrefab.Identifier}'. Skipping invalid setting.", locationTypePrefab.ContentPackage);
continue;
}
int amountToAdd = Rand.GetRNG(Rand.RandSync.ServerAndClient).Next(areaSetting.MinCount ?? 0, areaSetting.MaxCount ?? 0 + 1);
if (amountToAdd <= 0) { continue; }
// data is either for a biome or a zone, but not both
var existingCount = GetExistingCount(locationTypePrefab, areaSetting);
if (existingCount != null)
{
existingCount.AmountToAssign += amountToAdd;
}
else
{
if (!locationTypeAmountsToAssign.ContainsKey(locationTypePrefab))
{
locationTypeAmountsToAssign[locationTypePrefab] = new List<LocationTypeCount>();
}
locationTypeAmountsToAssign[locationTypePrefab].Add(CreateNewCount(areaSetting, amountToAdd));
}
}
}
LocationTypeCount CreateNewCount(LocationType.AreaSettingData areaSettingData, int amountToAdd)
{
if (areaSettingData is LocationType.BiomeSettingData biomeSettingData)
{
return new LocationTypeCount(amountToAdd, biomeSettingData.BiomeIdentifier);
}
else if (areaSettingData is LocationType.DifficultyZoneSettingData difficultyZoneSettingData)
{
return new LocationTypeCount(amountToAdd, difficultyZoneSettingData.DifficultyZone);
}
else
{
throw new ArgumentException("Unrecognized areaSettingData");
}
}
LocationTypeCount? GetExistingCount(LocationType locationTypePrefab, LocationType.AreaSettingData areaSettingData)
{
if (!locationTypeAmountsToAssign.TryGetValue(locationTypePrefab, out List<LocationTypeCount>? value)) { return null; }
return value.FirstOrDefault(areaSettingData.MatchesRemainingCount);
}
}
public void AddToLocationsPerZone(int zone, Location location)
{
if (!locationsPerZone.ContainsKey(zone))
{
locationsPerZone[zone] = new List<Location>();
}
locationsPerZone[zone].Add(location);
}
public void AddToFilled(Location location)
{
if (filledLocations.Contains(location)) { return; }
filledLocations.Add(location);
}
public bool IsFilled(Location location)
{
return filledLocations.Contains(location);
}
public static void ChangeLocationTypeAndName(CampaignMode campaign, Location location, LocationType suitableLocationType)
{
location.ChangeType(campaign, suitableLocationType, createStores: false, unlockInitialMissions: false);
if (!suitableLocationType.ForceLocationName.IsEmpty)
{
location.ForceName(suitableLocationType.ForceLocationName);
}
}
public void AssignForcedBiomeGateTypes(IEnumerable<Location> gateLocations)
{
foreach (Location gateLocation in gateLocations)
{
foreach (LocationType locationType in orderedLocationTypes)
{
if (locationType.BiomeGate != LocationType.BiomeGateSetting.Force) { continue; }
int zone = map.GetZoneIndex(gateLocation.MapPosition.X);
// if there are no counts left for this location type, skip it
if (locationType.HasCounts() && !TypeHasRemainingCountForLocation(locationType, gateLocation)) { continue; }
// wrong faction, can't place here
if (!locationType.Faction.IsEmpty && locationType.Faction != gateLocation.Faction?.Prefab.Identifier)
{
continue;
}
// if the location already happens to be of the type we want to assign, skip and remove from totals
if (gateLocation.Type == locationType)
{
AddToFilled(gateLocation);
RemoveOneFromTotals(locationType, gateLocation);
break;
}
if (!IsFilled(gateLocation) &&
locationType.IsValidForZoneOrBiome(zone, gateLocation.Biome.Identifier))
{
AddToFilled(gateLocation);
ChangeLocationTypeAndName(campaign, gateLocation, locationType);
RemoveOneFromTotals(locationType, gateLocation);
break;
}
}
}
}
private bool TypeHasRemainingCountForLocation(LocationType countLocationType, Location location)
{
if (!locationTypeAmountsToAssign.TryGetValue(countLocationType, out List<LocationTypeCount>? locationTypeCounts))
{
return false;
}
bool hasZoneCount = locationTypeCounts.Any(ltc => ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X) && ltc.AmountToAssign > 0);
bool hasBiomeCount = locationTypeCounts.Any(ltc => ltc.BiomeId == location.Biome.Identifier && ltc.AmountToAssign > 0);
return hasZoneCount || hasBiomeCount;
}
private int GetRemainingCount(LocationType locationType, LocationType.AreaSettingData areaSetting)
{
locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts);
if (locationTypeCounts == null || locationTypeCounts.None()) { return 0; }
var match = locationTypeCounts.FirstOrDefault(ltc => areaSetting.MatchesRemainingCount(ltc));
return match?.AmountToAssign ?? 0;
}
public void RemoveOneFromTotals(LocationType locationType, Location location)
{
if (!locationTypeAmountsToAssign.TryGetValue(locationType, out List<LocationTypeCount>? locationTypeCounts))
{
return;
}
var zoneMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.DifficultyZone == map.GetZoneIndex(location.MapPosition.X));
if (zoneMatch != null)
{
zoneMatch.AmountToAssign--;
}
var biomeMatch = locationTypeCounts.FirstOrDefault(ltc => ltc.AmountToAssign > 0 && ltc.BiomeId == location.Biome.Identifier);
if (biomeMatch != null)
{
biomeMatch.AmountToAssign--;
}
}
/// <summary>
/// Assign the location types that should be placed in some specific part of a biome/zone <see cref="LocationType.AreaSettingData.DesiredPosition"/>.
/// </summary>
public void AssignLocationTypesBasedOnDesiredPosition(IEnumerable<Location> gateLocations)
{
foreach (LocationType locationType in LocationType.Prefabs)
{
foreach (var areaSetting in locationType.AreaSettings)
{
if (!areaSetting.DesiredPosition.HasValue) { continue; }
int remainingCount = GetRemainingCount(locationType, areaSetting);
if (remainingCount == 0) { continue; }
var locations = map.Locations.Where(location => areaSetting.MatchesLocation(map, location)).Where(location => !gateLocations.Contains(location));
if (locations.None()) { continue; }
FillLocations(locations, areaSetting.DesiredPosition.Value, remainingCount, locationType);
}
}
void FillLocations(IEnumerable<Location> locations, float desiredPosition, int locationCount, LocationType locationType)
{
float areaStart = locations.Min(l => l.MapPosition.X);
float areaEnd = locations.Max(l => l.MapPosition.X);
float desiredMapPosition = MathHelper.Lerp(areaStart, areaEnd, desiredPosition);
List<Location> sortedLocations = locations.Where(location => !IsFilled(location)).ToList();
sortedLocations.Sort((firstLocation, secondLocation) => Math.Abs(firstLocation.MapPosition.X - desiredMapPosition).CompareTo(secondLocation.MapPosition.X - desiredMapPosition));
for (int i = 0; i < locationCount; i++)
{
if (sortedLocations.None()) { break; }
var closestLocation = sortedLocations.First();
ChangeLocationTypeAndName(campaign, closestLocation, locationType);
AddToFilled(closestLocation);
RemoveOneFromTotals(closestLocation.Type, closestLocation);
sortedLocations.Remove(closestLocation);
}
}
}
public void AssignLocationTypesBasedOnCount(IEnumerable<Location> gateLocations, IEnumerable<Location> locations)
{
// generate lists of all the instances of location types that we are supposed to try and fit into the available locations
List<Location> shuffledLocations = locations.ToList();
shuffledLocations.Shuffle(Rand.GetRNG(Rand.RandSync.ServerAndClient));
foreach (Location location in shuffledLocations)
{
if (IsFilled(location)) { continue; }
bool isBiomeGate = gateLocations.Contains(location);
if (isBiomeGate && location.Type.BiomeGate == LocationType.BiomeGateSetting.Force)
{
//forced as a biome gate, let's not touch this location
continue;
}
if (IsEveryLocationTypeAssigned) { break; }
var suitableLocationType = TryPickSuitableLocationTypeFromTotals(location, isBiomeGate);
// if we found something suitable, change the location type and name, and add to filled locations
// (otherwise we will honor the initial random location type)
if (suitableLocationType != null)
{
ChangeLocationTypeAndName(campaign, location, suitableLocationType);
AddToFilled(location);
}
}
// warn if we couldn't fill in all the desired counts
if (!IsEveryLocationTypeAssigned)
{
ContentPackage? nonVanillaContentPackage = null;
StringBuilder sb = new StringBuilder("Following location types could not be assigned to locations:\n");
foreach ((LocationType locationType, List<LocationTypeCount> locationTypeCounts) in locationTypeAmountsToAssign)
{
foreach (var locationTypeCount in locationTypeCounts)
{
if (locationTypeCount.AmountToAssign > 0)
{
if (locationType.ContentPackage != ContentPackageManager.VanillaCorePackage)
{
nonVanillaContentPackage = locationType.ContentPackage;
}
sb.AppendLine($"- {locationType.Identifier} - {locationType.Name} ({locationTypeCount.ToDebugString()})");
}
}
}
DebugConsole.AddWarning(sb.ToString(),
//blame the mod where one of the problematic location types is defined in
contentPackage: nonVanillaContentPackage);
}
}
private LocationType? TryPickSuitableLocationTypeFromTotals(Location location, bool isBiomeGate)
{
int locationZone = map.GetZoneIndex(location.MapPosition.X);
Identifier locationBiomeId = location.Biome.Identifier;
LocationType? suitableLocationType = null;
//find location type counts that haven't been fully assigned yet
List<(LocationType LocationType, LocationTypeCount Count)> potentialLocationTypeCounts = [];
foreach ((LocationType locationType, List<LocationTypeCount> countList) in locationTypeAmountsToAssign)
{
//if we're picking a potential new type for a biome gate, it must be a location type that's allowed as a biome gate
if (isBiomeGate && locationType.BiomeGate == LocationType.BiomeGateSetting.Deny) { continue; }
foreach (var locationTypeCount in countList)
{
if (locationTypeCount.AmountToAssign > 0)
{
potentialLocationTypeCounts.Add((locationType, locationTypeCount));
}
}
}
var zoneMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.DifficultyZone == locationZone);
var biomeMatches = potentialLocationTypeCounts.Where(locationTypeCount => locationTypeCount.Count.BiomeId == locationBiomeId);
if (zoneMatches.None() && biomeMatches.None())
{
return null;
}
// if both lists have something, we will try to find a location type that is in both lists
if (zoneMatches.Any() && biomeMatches.Any())
{
var dualMatch = zoneMatches.FirstOrDefault(zoneCount => biomeMatches.Any(biomeCount => biomeCount.LocationType == zoneCount.LocationType));
if (dualMatch.LocationType != null)
{
suitableLocationType = dualMatch.LocationType;
}
}
// no dual match, find individual match
if (suitableLocationType == null)
{
suitableLocationType = zoneMatches.Any() ?
zoneMatches.First().LocationType : biomeMatches.First().LocationType;
}
if (suitableLocationType != null)
{
RemoveOneFromTotals(suitableLocationType, location);
}
return suitableLocationType;
}
}
}
@@ -47,9 +47,8 @@ namespace Barotrauma
public readonly List<MapEntity> linkedTo = new List<MapEntity>();
protected bool flippedX, flippedY;
public bool FlippedX { get { return flippedX; } }
public bool FlippedY { get { return flippedY; } }
public bool FlippedX { get; protected set; }
public bool FlippedY { get; protected set; }
public bool ShouldBeSaved = true;
@@ -91,6 +90,16 @@ namespace Barotrauma
}
}
public virtual float RotationRad { get; protected set; }
/// <summary>
/// Rotation taking into account flipping: if the entity is flipped on either axis, the rotation is negated
/// (but not if it's flipped on both axes, two flips is essentially double negation).
/// </summary>
public float RotationRadWithFlipping => FlippedX ^ FlippedY ? -RotationRad : RotationRad;
public float RotationWithFlipping => MathHelper.ToDegrees(RotationRadWithFlipping);
public virtual Rectangle Rect
{
get { return rect; }
@@ -697,9 +706,10 @@ namespace Barotrauma
/// Flip the entity horizontally
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the y-axis of the sub it's inside</param>
public virtual void FlipX(bool relativeToSub)
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
public virtual void FlipX(bool relativeToSub, bool force = false)
{
flippedX = !flippedX;
FlippedX = !FlippedX;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
@@ -711,9 +721,10 @@ namespace Barotrauma
/// Flip the entity vertically
/// </summary>
/// <param name="relativeToSub">Should the entity be flipped across the x-axis of the sub it's inside</param>
public virtual void FlipY(bool relativeToSub)
/// <param name="force">Forces the item to be flipped even if it's configured not to be flippable.</param>
public virtual void FlipY(bool relativeToSub, bool force = false)
{
flippedY = !flippedY;
FlippedY = !FlippedY;
if (!relativeToSub || Submarine == null) { return; }
Vector2 relative = WorldPosition - Submarine.WorldPosition;
@@ -23,6 +23,12 @@ namespace Barotrauma
get { return allowedLocationTypes; }
}
private readonly HashSet<Identifier> allowedGameModeIdentifiers = new HashSet<Identifier>();
public IEnumerable<Identifier> AllowedGameModeIdentifiers
{
get { return allowedGameModeIdentifiers; }
}
[Serialize(-1, IsPropertySaveable.Yes, description: "Should this type of outpost be forced to the locations at the end of the campaign map? 0 = first end level, 1 = second end level, and so on."), Editable(MinValueInt = -1, MaxValueInt = 10)]
public int ForceToEndLocationIndex
@@ -279,6 +285,7 @@ namespace Barotrauma
{
Name = element.GetAttributeString("name", Identifier.Value);
allowedLocationTypes = element.GetAttributeIdentifierArray("allowedlocationtypes", Array.Empty<Identifier>()).ToHashSet();
allowedGameModeIdentifiers = element.GetAttributeIdentifierArray("allowedgamemodes", Array.Empty<Identifier>()).ToHashSet();
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (element.GetAttribute("leveltype") != null)
@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -110,6 +111,17 @@ namespace Barotrauma
}
}
var prebuiltOutpostInfo = ChooseOutpost(generationParams);
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
}
private static SubmarineInfo ChooseOutpost(OutpostGenerationParams generationParams)
{
var outpostFiles = ContentPackageManager.EnabledPackages.All
.SelectMany(p => p.GetFiles<OutpostFile>())
.Where(f => !TutorialPrefab.Prefabs.Any(tp => tp.OutpostPath == f.Path))
@@ -120,6 +132,50 @@ namespace Barotrauma
{
outpostInfos.Add(new SubmarineInfo(outpostFile.Path.Value));
}
//if there's missions selected that allow outpost selection from some specific set of outposts,
//choose one of those outposts
List<SubmarineInfo> outpostInfosSuitableForMission = new List<SubmarineInfo>();
if (GameMain.GameSession?.GameMode is { } gameMode)
{
foreach (var mission in gameMode.Missions)
{
if (!mission.Prefab.AllowOutpostSelectionFromTag.IsEmpty)
{
foreach (var outpostInfo in outpostInfos)
{
if (outpostInfo.OutpostTags.Contains(mission.Prefab.AllowOutpostSelectionFromTag) &&
!outpostInfosSuitableForMission.Contains(outpostInfo))
{
outpostInfosSuitableForMission.Add(outpostInfo);
}
}
}
}
}
//if an outpost has been select in the server settings, choose that...
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
//...but only if the outpost is suitable for the mission (or if the mission has no specific requirements for the outpost)
if (outpostInfosSuitableForMission.None() ||
outpostInfosSuitableForMission.Any(outpostInfo => outpostInfo.OutpostTags.Contains(serverSettings.SelectedOutpostName)))
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
return matchingOutpost;
}
}
}
if (outpostInfosSuitableForMission.Any())
{
return outpostInfosSuitableForMission.GetRandom(Rand.RandSync.ServerAndClient);
}
if (generationParams.OutpostTag.IsEmpty)
{
outpostInfos = outpostInfos.FindAll(o => o.OutpostTags.None());
@@ -139,24 +195,7 @@ namespace Barotrauma
{
throw new Exception("Failed to generate an outpost. Could not generate an outpost from the available outpost modules and there are no pre-built outposts available.");
}
var prebuiltOutpostInfo = outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
serverSettings.SelectedOutpostName != "Random")
{
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
if (matchingOutpost != null)
{
prebuiltOutpostInfo = matchingOutpost;
}
}
prebuiltOutpostInfo.Type = SubmarineType.Outpost;
sub = new Submarine(prebuiltOutpostInfo);
sub.Info.OutpostGenerationParams = generationParams;
location?.RemoveTakenItems();
EnableFactionSpecificEntities(sub, location);
return sub;
return outpostInfos.GetRandom(Rand.RandSync.ServerAndClient);
}
private static Submarine GenerateFromModules(OutpostGenerationParams generationParams, OutpostModuleFile[] outpostModuleFiles, Submarine sub, LocationType locationType, Location location, bool onlyEntrance = false, bool allowInvalidOutpost = false)
@@ -260,13 +299,15 @@ namespace Barotrauma
if (hasForceOutpostWithInitialFlag)
{
DebugConsole.NewMessage($"Using Force outpost module as initial in Outpost generation: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}", Color.Yellow);
DebugConsole.NewMessage($"Forcing module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" as the initial module...", Color.Yellow);
usedForceOutpostModule = GameMain.GameSession.ForceOutpostModule;
GameMain.GameSession.ForceOutpostModule = null;
}
if (initialModule == null)
{
//reset the forced outpost module so that it won't be used
//if we attempt to generate a new outpost later after this failed attempt
GameMain.GameSession.ForceOutpostModule = null;
throw new Exception("Failed to generate an outpost (no airlock modules found).");
}
foreach (Identifier initialFlag in initialModule.OutpostModuleInfo.ModuleFlags)
@@ -297,25 +338,34 @@ namespace Barotrauma
remainingOutpostGenerationTries--;
continue;
}
DebugConsole.ThrowError($"Could not place force outpost module: {GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}");
GameMain.GameSession.ForceOutpostModule = null;
DebugConsole.ThrowError($"Could not force the outpost module \"{GameMain.GameSession.ForceOutpostModule.OutpostModuleInfo.Name}\" to the outpost. Loading the module as-is...");
return null;
}
if (GameMain.GameSession != null)
{
GameMain.GameSession.ForceOutpostModule = null;
}
if (pendingModuleFlags.Any(flag => flag != "none"))
{
if (!allowInvalidOutpost)
{
remainingOutpostGenerationTries--;
if (remainingOutpostGenerationTries <= 0)
if (remainingOutpostGenerationTries > 0)
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
//tries left -> don't finish generating the outpost, try generating another layout
continue;
}
else
{
//out of tries, log an error, but let the method continue into loading the outpost (even if it doesn't have all the required modules)
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags));
}
continue;
}
else
{
DebugConsole.ThrowError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
DebugConsole.AddSafeError("Could not generate an outpost with all of the required modules. Some modules may not have enough connections at the edges to generate a valid layout. Pending modules: " + string.Join(", ", pendingModuleFlags) + ". Won't retry because invalid outposts are allowed.");
}
}
@@ -354,7 +404,7 @@ namespace Barotrauma
remainingOutpostGenerationTries--;
}
DebugConsole.AddSafeError("Failed to generate an outpost without overlapping modules. Trying to use a pre-built outpost instead...");
DebugConsole.AddSafeError("Failed to generate an outpost with a valid layout and all the required modules. Trying to use a pre-built outpost instead...");
return null;
List<MapEntity> loadEntities(Submarine sub)
@@ -463,10 +513,13 @@ namespace Barotrauma
int maxMoveAmount = Math.Max(2000, selectedModules.Max(m => Math.Max(m.Bounds.Width, m.Bounds.Height)));
bool overlapsFound = true;
PlacedModule overlappingModule1, overlappingModule2, moduleBelowAirlock;
int iteration = 0;
const int MaxIterations = 20;
while (overlapsFound)
{
overlapsFound = false;
overlappingModule1 = overlappingModule2 = moduleBelowAirlock = null;
foreach (PlacedModule placedModule in selectedModules)
{
if (placedModule.PreviousModule == null) { continue; }
@@ -478,6 +531,8 @@ namespace Barotrauma
int remainingOverlapPreventionTries = 10;
while (FindOverlap(subsequentModules, otherModules, out var module1, out var module2) && remainingOverlapPreventionTries > 0)
{
overlappingModule1 = module1;
overlappingModule2 = module2;
overlapsFound = true;
if (FindOverlapSolution(subsequentModules, module1, module2, selectedModules, generationParams.MinHallwayLength, maxMoveAmount, out Dictionary<PlacedModule, Vector2> solution))
{
@@ -492,16 +547,40 @@ namespace Barotrauma
}
remainingOverlapPreventionTries--;
}
if (remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
//check that the module doesn't extend below the airlock and potentially overlap with the sub
if (generationParams is not RuinGenerationParams &&
//if we've already exhausted half of the retries, accept potential overlaps
remainingOutpostGenerationTries > MaxOutpostGenerationRetries / 2 &&
//if the module is horizontally very far, it's ok to expand below the airlock
(placedModule.Bounds.X + placedModule.Offset.X < 5000 && placedModule.Bounds.Right + placedModule.Offset.X > -5000) &&
ModuleBelowInitialModule(placedModule, selectedModules.First()))
{
moduleBelowAirlock = placedModule;
overlapsFound = true;
}
}
iteration++;
if (iteration > 10)
if (iteration > MaxIterations)
{
#if DEBUG
string warningMsg = "Failed to create an outpost layout with no overlaps.";
if (overlappingModule1 != null && overlappingModule2 != null)
{
warningMsg += $" Overlapping modules: {overlappingModule1.Info.Name}, {overlappingModule2.Info.Name}.";
}
if (moduleBelowAirlock != null)
{
warningMsg += $" Module below airlock: {moduleBelowAirlock.Info.Name}.";
}
if (remainingOutpostGenerationTries > 0)
{
warningMsg += " Retrying...";
}
DebugConsole.AddWarning(warningMsg);
#endif
generationFailed = true;
break;
}
@@ -931,7 +1010,7 @@ namespace Barotrauma
Rectangle initialModuleBounds = initialModule.Bounds;
initialModuleBounds.Location += (initialModule.Offset + initialModule.MoveOffset).ToPoint();
return bounds.Bottom < initialModuleBounds.Bottom;
return bounds.Y < initialModuleBounds.Y;
}
/// <summary>
@@ -1053,8 +1132,8 @@ namespace Barotrauma
}
modulesWithCorrectFlags = modulesWithCorrectFlags.Where(m => m.OutpostModuleInfo.GapPositions.HasFlag(gapPosition) && m.OutpostModuleInfo.CanAttachToPrevious.HasFlag(gapPosition));
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
var suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: true);
var suitableModulesForAnyOutpost = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: true, requireCorrectLocationType: true, requireLocationTypeSpecific: false);
if (!suitableModules.Any())
{
//no suitable module found, see if we can find a "generic" module that's not meant for any specific type of outpost
@@ -1062,12 +1141,12 @@ namespace Barotrauma
//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 = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: true);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: 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 = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, disallowNonLocationTypeSpecific: false);
suitableModules = GetSuitableModules(modulesWithCorrectFlags, requireAllowAttachToPrevious: false, requireCorrectLocationType: true, requireLocationTypeSpecific: false);
}
}
@@ -1109,20 +1188,12 @@ namespace Barotrauma
return suitableModule;
}
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool disallowNonLocationTypeSpecific)
IEnumerable<SubmarineInfo> GetSuitableModules(IEnumerable<SubmarineInfo> modules, bool requireAllowAttachToPrevious, bool requireCorrectLocationType, bool requireLocationTypeSpecific)
{
IEnumerable<SubmarineInfo> suitable = modules;
if (requireCorrectLocationType)
{
if (disallowNonLocationTypeSpecific)
{
//don't use OutpostModuleInfo.IsLocationTypeAllowed here - we're trying to choose a module specifically for this location type, not modules suitable for any location type
suitable = modules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier));
}
else
{
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType));
}
suitable = modules.Where(m => m.OutpostModuleInfo.IsAllowedInLocationType(locationType, requireLocationTypeSpecific: requireLocationTypeSpecific));
}
if (requireAllowAttachToPrevious && prevModule != null)
{
@@ -138,10 +138,15 @@ namespace Barotrauma
return allowedLocationTypes.None() || allowedLocationTypes.Contains("Any".ToIdentifier());
}
public bool IsAllowedInLocationType(LocationType locationType)
/// <param name="requireLocationTypeSpecific">Does the module need to be explicitly configured as suitable for the location type, or is it ok if it's allowed for any location type?</param>
public bool IsAllowedInLocationType(LocationType locationType, bool requireLocationTypeSpecific = false)
{
if (locationType == null || IsAllowedInAnyLocationType()) { return true; }
return allowedLocationTypes.Contains(locationType.Identifier);
if (locationType == null) { return true; }
if (!requireLocationTypeSpecific && IsAllowedInAnyLocationType()) { return true; }
return
allowedLocationTypes.Contains(locationType.Identifier) ||
(!locationType.UseOutpostModulesOfLocationType.IsEmpty && allowedLocationTypes.Contains(locationType.UseOutpostModulesOfLocationType));
}
public void DetermineGapPositions(Submarine sub)
@@ -251,14 +251,13 @@ namespace Barotrauma
}
}
protected float rotationRad = 0f;
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
public float Rotation
{
get => MathHelper.ToDegrees(rotationRad);
get => MathHelper.ToDegrees(RotationRad);
set
{
rotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
RotationRad = MathHelper.WrapAngle(MathHelper.ToRadians(value));
if (StairDirection != Direction.None)
{
CreateStairBodies();
@@ -373,7 +372,7 @@ namespace Barotrauma
{
get
{
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.rotationRad;
float rotation = MathHelper.ToRadians(Prefab.BodyRotation) + this.RotationRad;
if (IsHorizontal)
{
if (FlippedX) { rotation = -MathHelper.Pi - rotation; }
@@ -396,9 +395,9 @@ namespace Barotrauma
get
{
Vector2 bodyOffset = Prefab.BodyOffset;
if (rotationRad != 0f)
if (RotationRad != 0f)
{
bodyOffset = MathUtils.RotatePoint(bodyOffset, -rotationRad);
bodyOffset = MathUtils.RotatePoint(bodyOffset, -RotationRad);
}
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
@@ -627,7 +626,7 @@ namespace Barotrauma
Body newBody = GameMain.World.CreateRectangle(bodyWidth, bodyHeight, 1.5f);
var rotationWithFlip = FlippedX ^ FlippedY ? -rotationRad : rotationRad;
float rotationWithFlip = RotationRadWithFlipping;
newBody.BodyType = BodyType.Static;
Vector2 stairRectHeightDiff = new Vector2(0f, stairHeight / 2.0f - rect.Height / 2.0f);
@@ -764,8 +763,8 @@ namespace Barotrauma
public override Quad2D GetTransformedQuad()
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
FlippedX != FlippedY
? rotationRad
: -rotationRad);
? RotationRad
: -RotationRad);
/// <summary>
/// Checks if there's a structure items can be attached to at the given position and returns it.
@@ -1280,7 +1279,7 @@ namespace Barotrauma
gapRect.Width += 20;
gapRect.Height += 20;
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(rotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
bool rotatedEnoughToChangeOrientation = (MathUtils.WrapAngleTwoPi(RotationRad - MathHelper.PiOver4) % MathHelper.Pi < MathHelper.PiOver2);
if (rotatedEnoughToChangeOrientation)
{
var center = gapRect.Location + gapRect.Size.FlipY() / new Point(2);
@@ -1345,6 +1344,9 @@ namespace Barotrauma
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
{
CreateWallDamageExplosion(gap, attacker, createWallDamageProjectiles);
#if CLIENT
SteamTimelineManager.OnHullBreached(this);
#endif
}
}
@@ -1378,7 +1380,7 @@ namespace Barotrauma
{
const float explosionRange = 500.0f;
float explosionStrength = gap.Open;
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
if (linkedHull != null)
{
@@ -1595,7 +1597,7 @@ namespace Barotrauma
partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
public override void FlipX(bool relativeToSub)
public override void FlipX(bool relativeToSub, bool force = false)
{
base.FlipX(relativeToSub);
@@ -1623,7 +1625,7 @@ namespace Barotrauma
}
}
public override void FlipY(bool relativeToSub)
public override void FlipY(bool relativeToSub, bool force = false)
{
base.FlipY(relativeToSub);
@@ -1141,6 +1141,17 @@ namespace Barotrauma
}
}
foreach (var dockingPort in DockingPort.List)
{
//a little hacky: undock and redock to ensure the hulls and gaps between docking ports are correct
//after all the parts of the submarine have been flipped and moved to correct places.
if (dockingPort.DockingTarget is { } dockingTarget)
{
dockingPort.Undock();
dockingPort.Dock(dockingTarget);
}
}
Item.UpdateHulls();
Gap.UpdateHulls();
#if CLIENT
@@ -2090,6 +2101,10 @@ namespace Barotrauma
foreach (Submarine sub in _loaded)
{
sub.Remove();
if (sub.Info.LazyLoad)
{
sub.Info.UnloadSubmarineElement();
}
}
loaded.Clear();
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -194,10 +194,27 @@ namespace Barotrauma
set;
}
/// <summary>
/// When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.
/// </summary>
public readonly bool LazyLoad;
private XElement submarineElement;
public XElement SubmarineElement
{
get;
private set;
get
{
if (LazyLoad && submarineElement == null)
{
Reload();
}
return submarineElement;
}
private set
{
submarineElement = value;
}
}
public override string ToString()
@@ -263,7 +280,11 @@ namespace Barotrauma
RequiredContentPackages = new HashSet<string>();
}
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true)
/// <summary>
/// Creates a new SubmarineInfo from a file.
/// </summary>
/// <param name="lazyLoad">When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.</param>
public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true, bool lazyLoad = false)
{
FilePath = filePath;
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
@@ -296,11 +317,17 @@ namespace Barotrauma
else
{
SubmarineElement = element;
}
}
Name = SubmarineElement.GetAttributeString("name", null) ?? Name;
Init();
if (lazyLoad)
{
LazyLoad = true;
SubmarineElement = null;
}
}
public SubmarineInfo(Submarine sub) : this(sub.Info)
@@ -512,6 +539,11 @@ namespace Barotrauma
if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
}
public void UnloadSubmarineElement()
{
SubmarineElement = null;
}
public bool IsVanillaSubmarine()
{
if (FilePath == null) { return false; }
@@ -689,7 +721,7 @@ namespace Barotrauma
RemoveSavedSub(filePath);
if (File.Exists(filePath))
{
var subInfo = new SubmarineInfo(filePath);
var subInfo = new SubmarineInfo(filePath, lazyLoad: true);
if (!subInfo.IsFileCorrupted)
{
savedSubmarines.Add(subInfo);
@@ -21,6 +21,7 @@ namespace Barotrauma.Networking
ServerMessageBox = 10,
ServerMessageBoxInGame = 11,
Team = 12,
BlockedBySpamFilter = 13,
}
public enum PlayerConnectionChangeType { None = 0, Joined = 1, Kicked = 2, Disconnected = 3, Banned = 4 }
@@ -39,24 +40,26 @@ namespace Barotrauma.Networking
/// </summary>
public const float SpeakRangeVOIP = 1000.0f;
public const float BlockedBySpamFilterTime = 10.0f;
private static readonly string dateTimeFormatLongTimePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
public static Color[] MessageColor =
{
new Color(190, 198, 205), //default
new Color(204, 74, 78), //error
new Color(136, 177, 255), //dead
new Color(136, 177, 255), //dead
new Color(157, 225, 160), //server
new Color(238, 208, 0), //radio
new Color(64, 240, 89), //private
new Color(255, 255, 255), //console
new Color(255, 255, 255), //messagebox
new Color(255, 128, 0), //order
new Color(), // ServerLog
new Color(), // ServerMessageBox
new Color(), // ServerMessageBoxInGame
//new Color(128, 0, 255), // team
new Color(86, 91, 205), // team
new Color(255, 128, 0), //order
new Color(), // ServerLog
new Color(), // ServerMessageBox
new Color(), // ServerMessageBoxInGame
new Color(86, 91, 205), // team
new Color(255, 0, 0), // blocked by spam filter
};
public readonly string Text;
@@ -92,13 +92,6 @@ namespace Barotrauma.Networking
CharacterID = value.ID;
}
}
else
{
if (value != null)
{
DebugConsole.NewMessage(value.Name, Color.Yellow);
}
}
character = value;
if (character != null)
{
@@ -272,12 +265,15 @@ namespace Barotrauma.Networking
SetPermissions(permissions, permittedCommands);
}
public static string SanitizeName(string name)
/// <summary>
/// Strips out newlines and some common non-renderable symbols (ASCII codes below 32) out of the name, and optionally limits the maximum size.
/// </summary>
public static string SanitizeName(string name, int maxLength = MaxNameLength)
{
name = name.Trim();
if (name.Length > MaxNameLength)
name = name.Trim().Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');
if (name.Length > maxLength)
{
name = name.Substring(0, MaxNameLength);
name = name.Substring(0, maxLength);
}
string rName = "";
for (int i = 0; i < name.Length; i++)
@@ -377,6 +377,7 @@ namespace Barotrauma
if (IsInRemoveQueue(item) || item.Removed) { return; }
spawnOrRemoveQueue.Enqueue(item);
item.IsInRemoveQueue = true;
foreach (var containedItem in item.ContainedItems)
{
@@ -1,8 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Networking
{
@@ -103,6 +101,7 @@ namespace Barotrauma.Networking
CIRCUITBOX,
MONEY,
READY_CHECK, //start, end and update a ready check
UNLOCKRECIPE, //unlocking a fabrication recipe
SEND_BACKUP_INDICES // the server sends a list of available backups for a save file
}
@@ -1,25 +1,23 @@
using System;
using System.Collections.Generic;
using RestSharp;
using System;
using System.Text.Json;
using System.Threading.Tasks;
using RestSharp;
namespace Barotrauma.Networking;
sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator
{
#warning FIXME change URL back to the non-experimental one once this passes QA
private const string consentServerUrl = "https://barotraumagame.com/baromaster/experimental/";
private const string consentServerFile = "getOwnerSteamId.php";
private const string ServerUrl = "https://barotraumagame.com/baromaster/";
private const string ServerFile = "getOwnerSteamId.php";
private const int RemoteRequestVersion = 1;
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data);
var client = new RestClient(consentServerUrl);
var client = new RestClient(ServerUrl);
var request = new RestRequest(consentServerFile, Method.GET);
var request = new RestRequest(ServerFile, Method.GET);
request.AddParameter("authticket", ticketData);
request.AddParameter("request_version", RemoteRequestVersion);
@@ -57,6 +57,18 @@ namespace Barotrauma
{
ReplaceAttribute(element, attribute);
}
//collect the names of the elements that we want to remove all instances of
//(e.g. if a variant wants to remove all fabrication recipes)
List<Identifier> elementNamesToRemove = new List<Identifier>();
foreach (var subElement in replacement.Elements())
{
if (subElement.Name.ToString().Equals("clearall", StringComparison.OrdinalIgnoreCase))
{
elementNamesToRemove.AddRange(subElement.Elements().Select(e => e.Name.ToIdentifier()));
}
}
foreach (XElement replacementSubElement in replacement.Elements())
{
int index = replacement.Elements().ToList().FindAll(e => e.Name.ToString().Equals(replacementSubElement.Name.ToString(), StringComparison.OrdinalIgnoreCase)).IndexOf(replacementSubElement);
@@ -67,7 +79,17 @@ namespace Barotrauma
bool cleared = false;
foreach (var subElement in element.Elements())
{
if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase))
if (elementNamesToRemove.Contains(subElement.NameAsIdentifier()))
{
if (!elementsToRemove.Contains(subElement)) { elementsToRemove.Add(subElement); }
matchingElementFound = true;
continue;
}
if (replacementSubElement.Name.ToString().Equals("clearall", StringComparison.OrdinalIgnoreCase))
{
continue;
}
else if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase))
{
matchingElementFound = true;
newElementsFromBase.Clear();
@@ -45,6 +45,20 @@ namespace Barotrauma
{
OnSort = onSort;
}
/// <summary>
/// For iterating through the Prefabs in a deterministic order (e.g. for map generation). Sorting is not cached, so use sparingly.
/// </summary>
public IOrderedEnumerable<T> GetOrdered()
{
// UintIdentifier comparison is preferred to Identifier comparison that uses strings
if ((typeof(T).IsAssignableFrom(typeof(PrefabWithUintIdentifier))))
{
return this.OrderBy(p => (p as PrefabWithUintIdentifier)!.UintIdentifier);
}
return this.OrderBy(p => p.Identifier);
}
/// <summary>
/// Method to be called when calling Add(T prefab, bool override).
@@ -141,7 +141,10 @@ namespace Barotrauma
foreach (PhysicsBody body in PhysicsBody.List)
{
if (body.Enabled && body.BodyType != FarseerPhysics.BodyType.Static)
//update character (colliders) regardless if they're enabled or not, so that the draw position is updated
//necessary to sync the character's position even if the character is ragdolled and the collider is disabled
if ((body.Enabled || body.UserData is Character) &&
body.BodyType != BodyType.Static)
{
body.Update();
}
@@ -1,6 +1,7 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using RestSharp.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -12,6 +13,10 @@ using System.Xml.Linq;
namespace Barotrauma
{
/// <summary>
/// Is the value of the property saved when saving (serializing) the entity?
/// Can be set to false if e.g. the value doesn't ever change from the prefab value, or if changes to it shouldn't persist between rounds.
/// </summary>
public enum IsPropertySaveable
{
Yes,
@@ -883,7 +888,16 @@ namespace Barotrauma
public static Dictionary<Identifier, SerializableProperty> DeserializeProperties(object obj, XElement element = null)
{
Dictionary<Identifier, SerializableProperty> dictionary = GetProperties(obj);
#if DEBUG
var nonPublicProperties = obj.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var property in nonPublicProperties)
{
if (property.GetAttribute<Serialize>() != null)
{
DebugConsole.ThrowError($"The property {property.Name} in class {obj.GetType()} is set as serializable, but isn't public. Serializable properties must have at least a public getter.");
}
}
#endif
foreach (var property in dictionary.Values)
{
//set the value of the property to the default value if there is one
@@ -239,6 +239,16 @@ namespace Barotrauma
return element.GetAttributeIdentifierArray(key, null, trim)?.ToImmutableHashSet()
?? defaultValue;
}
public static float? GetAttributeNullableFloat(this XElement element, string attributeName)
{
if (element.GetAttribute(attributeName) is XAttribute attribute)
{
// if there is an error in parsing, we will return the default value, which may cause edge case errors down the line
return GetAttributeFloat(attribute, 0.0f);
}
return null;
}
public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName)
{
@@ -253,7 +263,7 @@ namespace Barotrauma
return defaultValue;
}
public static float GetAttributeFloat(this XElement element, string name, float defaultValue) => GetAttributeFloat(element?.GetAttribute(name), defaultValue);
public static float GetAttributeFloat(this XAttribute attribute, float defaultValue)
@@ -352,6 +362,16 @@ namespace Barotrauma
}
return false;
}
public static int? GetAttributeNullableInt(this XElement element, string attributeName)
{
if (element.GetAttribute(attributeName) is XAttribute attribute)
{
// if there is an error in parsing, we will return the default value, which may cause edge case errors down the line
return GetAttributeInt(attribute, 0);
}
return null;
}
public static int GetAttributeInt(this XElement element, string name, int defaultValue) => GetAttributeInt(element?.GetAttribute(name), defaultValue);
@@ -632,7 +632,7 @@ namespace Barotrauma
GameMain.SoundManager?.ApplySettings();
#endif
if (languageChanged) { TextManager.ClearCache(); }
if (languageChanged) { TextManager.LanguageChanged(); }
}
public static void SaveCurrentConfig()
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -1,10 +1,8 @@
using System;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -12,7 +10,11 @@ namespace Barotrauma
{
public readonly DelayedEffect Parent;
public readonly Entity Entity;
public readonly Vector2? WorldPosition;
public Vector2? WorldPosition;
/// <summary>
/// Should the delayed effect attempt to determine the position of the effect based on the targets, or just use the position that was passed to the constructor.
/// </summary>
public bool GetPositionBasedOnTargets;
public readonly Vector2? StartPosition;
public readonly List<ISerializableEntity> Targets;
public float Delay;
@@ -62,7 +64,10 @@ namespace Barotrauma
{
foreach (var existingEffect in DelayList)
{
if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target) { return; }
if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target)
{
return;
}
}
}
if (!IsValidTarget(target)) { return; }
@@ -74,7 +79,11 @@ namespace Barotrauma
switch (delayType)
{
case DelayTypes.Timer:
DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition ?? GetPosition(entity, currentTargets, worldPosition), startPosition: null));
var newDelayListElement = new DelayedListElement(this, entity, currentTargets, delay, worldPosition ?? GetPosition(entity, currentTargets, worldPosition), startPosition: null)
{
GetPositionBasedOnTargets = worldPosition == null
};
DelayList.Add(newDelayListElement);
break;
case DelayTypes.ReachCursor:
Projectile projectile = (entity as Item)?.GetComponent<Projectile>();
@@ -171,7 +180,16 @@ namespace Barotrauma
{
case DelayTypes.Timer:
element.Delay -= deltaTime;
if (element.Delay > 0.0f) { continue; }
if (element.Delay > 0.0f)
{
//if the delayed effect is supposed to get the position from the targets,
//keep refreshing the position until the effect runs (so e.g. a delayed effect runs at the last known position of a monster before it despawned)
if (element.GetPositionBasedOnTargets && element.Entity is { Removed: false })
{
element.WorldPosition = element.Parent.GetPosition(element.Entity, element.Parent.currentTargets);
}
continue;
}
break;
case DelayTypes.ReachCursor:
if (Vector2.Distance(element.Entity.WorldPosition, element.StartPosition.Value) < element.Delay) { continue; }

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