Build 0.20.9.0

This commit is contained in:
Markus Isberg
2022-12-01 21:59:53 +02:00
parent df805574c4
commit 31d2dc658e
66 changed files with 953 additions and 505 deletions
@@ -1838,7 +1838,7 @@ namespace Barotrauma
}
if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f;
bool advance = !canAttack && Character.CurrentHull == null || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
@@ -1856,10 +1856,18 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (!Character.InWater)
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
if (Character.CurrentHull == null && !canAttack)
{
SteeringManager.SteeringWander();
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5);
}
else
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
@@ -1882,7 +1890,7 @@ namespace Barotrauma
}
Entity targetEntity = wallTarget?.Structure ?? SelectedAiTarget?.Entity;
IDamageable damageTarget = targetEntity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack && targetEntity != null)
if (AttackLimb?.attack is Attack { Ranged: true} attack)
{
AimRangedAttack(attack, targetEntity);
}
@@ -1901,7 +1909,7 @@ namespace Barotrauma
public void AimRangedAttack(Attack attack, Entity targetEntity)
{
if (attack == null || attack.Ranged == false || targetEntity == null) { return; }
if (attack is not { Ranged: true } || targetEntity is not { Removed: false }) { return; }
Character.SetInput(InputType.Aim, false, true);
Limb limb = GetLimbToRotate(attack);
if (limb != null)
@@ -1907,8 +1907,8 @@ namespace Barotrauma
visibleHulls = VisibleHulls;
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreWater = character.IsProtectedFromPressure();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
@@ -77,11 +77,11 @@ namespace Barotrauma
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (GameMain.GameSession.RoundDuration > 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
if (GameMain.GameSession.RoundDuration < 30.0f) { currentFlags.Add("Initial".ToIdentifier()); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
if (GameMain.GameSession.RoundDuration > 120.0f &&
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
@@ -876,7 +876,20 @@ namespace Barotrauma
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
},
onAbandon: () => Abandon = true);
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref followTargetObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
});
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
{
@@ -25,6 +25,8 @@ namespace Barotrauma
private readonly Item item;
public Item ItemToContain { get; private set; }
public int? TargetSlot;
private AIObjectiveGetItem getItemObjective;
private AIObjectiveGoTo goToObjective;
@@ -126,7 +128,19 @@ namespace Barotrauma
}
if (character.CanInteractWith(container.Item, checkLinked: false))
{
if (RemoveExisting || (RemoveExistingWhenNecessary && !container.Inventory.CanBePut(ItemToContain)))
static bool CanBePut(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.CanBePutInSlot(itemToContain, targetSlot.Value);
}
else
{
return inventory.CanBePut(itemToContain);
}
}
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
{
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
}
@@ -136,7 +150,20 @@ namespace Barotrauma
}
Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain);
if (container.Inventory.TryPutItem(ItemToContain, null))
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
{
if (targetSlot.HasValue)
{
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null);
}
else
{
return inventory.TryPutItem(itemToContain, user: null);
}
}
if (TryPutItem(container.Inventory, TargetSlot, ItemToContain))
{
if (MoveWholeStack && slots != null)
{
@@ -144,7 +171,7 @@ namespace Barotrauma
{
foreach (Item item in originalInventory.GetItemsAt(slot).ToList())
{
container.Inventory.TryPutItem(item, null);
TryPutItem(container.Inventory, TargetSlot, item);
}
}
}
@@ -23,9 +23,8 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (!character.IsOnPlayerTeam) { return Targets.None() ? 0 : 100; }
int totalEnemies = Targets.Count();
if (totalEnemies == 0) { return 0; }
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam) { return 100; }
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them.
@@ -103,13 +103,19 @@ namespace Barotrauma
character.Speak(TextManager.Get("DialogGetOxygenTank").Value, null, 0, "getoxygentank".ToIdentifier(), 30.0f);
}
}
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
var container = targetItem.GetComponent<ItemContainer>();
var objective = new AIObjectiveContainItem(character, OXYGEN_SOURCE, container, objectiveManager, spawnItemIfNotFound: character.TeamID == CharacterTeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
RemoveExistingWhenNecessary = true
};
if (container.HasSubContainers)
{
objective.TargetSlot = container.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
}
return objective;
},
onAbandon: () =>
{
@@ -262,7 +262,7 @@ namespace Barotrauma
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && character.NeedsAir && !character.HasAbilityFlag(AbilityFlags.ImmuneToPressure);
bool needsDivingGear = (needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit));
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (Mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
@@ -435,7 +435,7 @@ namespace Barotrauma
spawnPoint ??= WayPoint.WayPointList.Where(wp => wp.SpawnType == SpawnType.Human && wp.Submarine?.Info.Type == SubmarineType.Player).GetRandomUnsynced();
spawnPos = spawnPoint?.WorldPosition ?? Submarine.MainSub.WorldPosition;
}
var pet = Character.Create(speciesName, spawnPos, seed);
var pet = Character.Create(speciesName, spawnPos, seed, spawnInitialItems: false);
var petBehavior = (pet?.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior != null)
{
@@ -11,8 +11,8 @@ namespace Barotrauma
get { return aiController; }
}
public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll)
public AICharacter(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isNetworkPlayer = false, RagdollParams ragdoll = null, bool spawnInitialItems = true)
: base(prefab, position, seed, characterInfo, id: id, isRemotePlayer: isNetworkPlayer, ragdollParams: ragdoll, spawnInitialItems)
{
InitProjSpecific();
}
@@ -529,8 +529,17 @@ namespace Barotrauma
private Color speechBubbleColor;
private float speechBubbleTimer;
/// <summary>
/// Prevents the character from interacting with items or characters
/// </summary>
public bool DisableInteract { get; set; }
/// <summary>
/// Prevents the character from highlighting items or characters with the cursor,
/// meaning it can't interact with anything but the things it has currently selected/equipped
/// </summary>
public bool DisableFocusingOnEntities { get; set; }
//text displayed when the character is highlighted if custom interact is set
public LocalizedString CustomInteractHUDText { get; private set; }
private Action<Character, Character> onCustomInteract;
@@ -1088,9 +1097,9 @@ namespace Barotrauma
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null)
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
{
return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll);
return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent: true, ragdoll, spawnInitialItems);
}
/// <summary>
@@ -1105,16 +1114,16 @@ namespace Barotrauma
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="createNetworkEvent">Should clients receive a network event about the creation of this character?</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true)
public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
speciesName = Path.GetFileNameWithoutExtension(speciesName);
}
return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound);
return Create(speciesName.ToIdentifier(), position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, throwErrorIfNotFound, spawnInitialItems);
}
public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true)
public static Character Create(Identifier speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool throwErrorIfNotFound = true, bool spawnInitialItems = true)
{
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (prefab == null)
@@ -1131,29 +1140,29 @@ namespace Barotrauma
return null;
}
return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll);
return Create(prefab, position, seed, characterInfo, id, isRemotePlayer, hasAi, createNetworkEvent, ragdoll, spawnInitialItems);
}
public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null)
public static Character Create(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null, bool spawnInitialItems = true)
{
Character newCharacter = null;
if (prefab.Identifier != CharacterPrefab.HumanSpeciesName)
{
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
var ai = new EnemyAIController(aiCharacter, seed);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else if (hasAi)
{
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
var aiCharacter = new AICharacter(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
var ai = new HumanAIController(aiCharacter);
aiCharacter.SetAI(ai);
newCharacter = aiCharacter;
}
else
{
newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
newCharacter = new Character(prefab, position, seed, characterInfo, id, isRemotePlayer, ragdoll, spawnInitialItems);
}
#if SERVER
@@ -1170,7 +1179,7 @@ namespace Barotrauma
wallet = new Wallet(Option<Character>.Some(this));
}
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
protected Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null, bool spawnInitialItems = true)
: this(null, id)
{
this.Seed = seed;
@@ -1280,7 +1289,8 @@ namespace Barotrauma
{
Inventory = new CharacterInventory(
inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random),
this);
this,
spawnInitialItems);
}
if (healthElements.Count == 0)
{
@@ -2719,7 +2729,7 @@ namespace Barotrauma
#if CLIENT
if (isLocalPlayer)
{
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities)
{
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
{
@@ -2754,6 +2764,7 @@ namespace Barotrauma
focusedItem = null;
}
findFocusedTimer -= deltaTime;
DisableFocusingOnEntities = false;
}
#endif
var head = AnimController.GetLimb(LimbType.Head);
@@ -4249,11 +4260,14 @@ namespace Barotrauma
if (!isNetworkMessage)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (GameMain.NetworkMember is { IsClient: true }) { return; }
}
CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); }
if (GameMain.NetworkMember is not { IsClient: true } || isNetworkMessage)
{
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true);
}
if (IsDead)
{
BreakJoints();
@@ -4930,7 +4944,7 @@ namespace Barotrauma
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.TalentIdentifiers.None(HasTalent))
if (!talentOption.HasMaxTalents(info.UnlockedTalents))
{
return false;
}
@@ -0,0 +1,12 @@
namespace Barotrauma;
partial class CharacterHUD
{
static partial void RecreateHudTextsIfControllingProjSpecific(Character character);
static partial void RecreateHudTextsIfFocusedProjSpecific(params Item[] items);
public static void RecreateHudTextsIfControlling(Character character) => RecreateHudTextsIfControllingProjSpecific(character);
public static void RecreateHudTextsIfFocused(params Item[] items) => RecreateHudTextsIfFocusedProjSpecific(items);
}
@@ -132,7 +132,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return (Vitality <= 0.0f || Character.IsDead) && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious); }
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -72,7 +72,7 @@ namespace Barotrauma
bool allowCheats = false;
#if CLIENT
allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is EditorScreen);
allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true });
#endif
if (!allowCheats && !CheatsEnabled && IsCheat)
{
@@ -150,7 +150,7 @@ namespace Barotrauma
MaxAttachableCount,
ExplosionRadiusMultiplier,
ExplosionDamageMultiplier,
FabricateMedicineSpeedMultiplier,
FabricationSpeed,
BallastFloraDamageMultiplier,
HoldBreathMultiplier,
Apprenticeship,
@@ -1,3 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
@@ -53,6 +54,11 @@ namespace Barotrauma
break;
}
conditionals = conditionalList;
if (itemTags.None() && ItemIdentifiers.None())
{
DebugConsole.ThrowError($"Error in event \"{ParentEvent.Prefab.Identifier}\". {nameof(CheckItemAction)} does't define either tags or identifiers of the item to check.");
}
}
protected override bool? DetermineSuccess()
@@ -100,7 +106,22 @@ namespace Barotrauma
{
if (inventory == null) { return false; }
int count = 0;
foreach (Item item in inventory.FindAllItems(it => itemTags.Any(it.HasTag) || itemIdentifierSplit.Contains(it.Prefab.Identifier), recursive: Recursive))
HashSet<Item> eventTargets = new HashSet<Item>();
foreach (Identifier tag in itemTags)
{
foreach (var target in ParentEvent.GetTargets(tag))
{
if (target is Item item)
{
eventTargets.Add(item);
}
}
}
foreach (Item item in inventory.FindAllItems(it =>
itemTags.Any(it.HasTag) ||
itemIdentifierSplit.Contains(it.Prefab.Identifier) ||
eventTargets.Contains(it),
recursive: Recursive))
{
if (!ConditionalsMatch(item, character)) { continue; }
count++;
@@ -80,7 +80,7 @@ namespace Barotrauma
public bool DisableEvents
{
get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; }
get { return IsFirstRound && GameMain.GameSession.RoundDuration < FirstRoundEventDelay; }
}
public bool CheatsEnabled;
@@ -764,7 +764,7 @@ namespace Barotrauma
/// </remarks>
public static ImmutableHashSet<Character> GetSessionCrewCharacters(CharacterType type)
{
if (GameMain.GameSession.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
if (GameMain.GameSession?.CrewManager is not { } crewManager) { return ImmutableHashSet<Character>.Empty; }
IEnumerable<Character> players;
IEnumerable<Character> bots;
@@ -907,7 +907,7 @@ namespace Barotrauma
{
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name.Value ?? "none"), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0), RoundDuration);
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count ?? 0), RoundDuration);
foreach (Mission mission in missions)
{
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
@@ -56,6 +56,10 @@ namespace Barotrauma
}
EndReadyCheck();
#if CLIENT
msgBox?.Close();
#endif
}
}
}
@@ -51,7 +51,7 @@ namespace Barotrauma
return slotString == null ? Array.Empty<string>() : slotString.Split(',');
}
public CharacterInventory(XElement element, Character character)
public CharacterInventory(XElement element, Character character, bool spawnInitialItems)
: base(character, ParseSlotTypes(element).Length)
{
this.character = character;
@@ -84,6 +84,8 @@ namespace Barotrauma
InitProjSpecific(element);
if (!spawnInitialItems) { return; }
#if CLIENT
//clients don't create items until the server says so
if (GameMain.Client != null) { return; }
@@ -94,7 +96,7 @@ namespace Barotrauma
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
string itemIdentifier = subElement.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab))
{
DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.");
continue;
@@ -200,6 +202,7 @@ namespace Barotrauma
#if CLIENT
CreateSlots();
#endif
CharacterHUD.RecreateHudTextsIfControlling(character);
//if the item was equipped and there are more items in the same stack, equip one of those items
if (tryEquipFromSameStack && wasEquipped)
{
@@ -498,6 +501,7 @@ namespace Barotrauma
HintManager.OnObtainedItem(character, item);
}
#endif
CharacterHUD.RecreateHudTextsIfControlling(character);
if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo)
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
@@ -47,6 +47,13 @@ namespace Barotrauma.Items.Components
{
return ContainableItems == null || ContainableItems.Count == 0 || ContainableItems.Any(c => c.MatchesItem(itemPrefab));
}
public bool MatchesItem(Identifier identifierOrTag)
{
return
ContainableItems == null || ContainableItems.Count == 0 ||
ContainableItems.Any(c => c.Identifiers.Contains(identifierOrTag) && !c.ExcludedIdentifiers.Contains(identifierOrTag));
}
}
public readonly NamedEvent<ItemContainer> OnContainedItemsChanged = new NamedEvent<ItemContainer>();
@@ -374,7 +381,7 @@ namespace Barotrauma.Items.Components
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
SetContainedActive(true);
}
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
@@ -386,9 +393,9 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
activeContainedItems.RemoveAll(i => i.Item == containedItem);
//deactivate if the inventory is empty
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
@@ -664,6 +671,18 @@ namespace Barotrauma.Items.Components
return relatedItem;
}
/// <summary>
/// Returns the index of the first slot whose restrictions match the specified tag or identifier
/// </summary>
public int? FindSuitableSubContainerIndex(Identifier itemTagOrIdentifier)
{
for (int i = 0; i < slotRestrictions.Length; i++)
{
if (slotRestrictions[i].MatchesItem(itemTagOrIdentifier)) { return i; }
}
return null;
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
switch (connection.Name)
@@ -603,6 +603,13 @@ namespace Barotrauma.Items.Components
}
partial void UpdateRequiredTimeProjSpecific();
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
{
return
(user != null && user.HasRecipeForItem(item.Identifier)) ||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
@@ -610,8 +617,7 @@ namespace Barotrauma.Items.Components
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!character.HasRecipeForItem(fabricableItem.TargetItem.Identifier) &&
GameSession.GetSessionCrewCharacters(CharacterType.Bot).None(c => c.HasRecipeForItem(fabricableItem.TargetItem.Identifier)))
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
{
return false;
}
@@ -678,9 +684,10 @@ namespace Barotrauma.Items.Components
//fabricating takes 100 times longer if degree of success is close to 0
//characters with a higher skill than required can fabricate up to 100% faster
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
if (user is not null && fabricableItem.TargetItem is { } it && it.Tags.Contains("medical"))
if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
{
time *= 1f + user.GetStatValue(StatTypes.FabricateMedicineSpeedMultiplier);
time /= 1f + it.Tags.Sum(tag => info.GetSavedStatValue(StatTypes.FabricationSpeed, tag));
}
return time;
}
@@ -25,12 +25,8 @@ namespace Barotrauma.Items.Components
public List<Hull> LinkedHulls = new List<Hull>();
}
private DateTime resetDataTime;
private bool hasPower;
private readonly Dictionary<Hull, HullData> hullDatas;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Does the machine require inputs from water detectors in order to show the water levels inside rooms.")]
public bool RequireWaterDetectors
{
@@ -77,7 +73,6 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
IsActive = true;
hullDatas = new Dictionary<Hull, HullData>();
InitProjSpecific();
}
@@ -85,37 +80,6 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
//reset data if we haven't received anything in a while
//(so that outdated hull info won't be shown if detectors stop sending signals)
if (DateTime.Now > resetDataTime)
{
foreach (HullData hullData in hullDatas.Values)
{
if (!hullData.Distort)
{
if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; }
if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; }
}
}
resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
}
#if CLIENT
if (cardRefreshTimer > cardRefreshDelay)
{
if (item.Submarine is { } sub)
{
UpdateIDCards(sub);
}
cardRefreshTimer = 0;
}
else
{
cardRefreshTimer += deltaTime;
}
#endif
hasPower = Voltage > MinVoltage;
if (hasPower)
{
@@ -140,67 +104,5 @@ namespace Barotrauma.Items.Components
{
return picker != null;
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
Item source = signal.source;
if (source == null || source.CurrentHull == null) { return; }
Hull sourceHull = source.CurrentHull;
if (!hullDatas.TryGetValue(sourceHull, out HullData hullData))
{
hullData = new HullData();
hullDatas.Add(sourceHull, hullData);
}
if (hullData.Distort) { return; }
switch (connection.Name)
{
case "water_data_in":
//cheating a bit because water detectors don't actually send the water level
bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
hullData.ReceivedWaterAmount = null;
hullData.LastWaterDataTime = Timing.TotalTime;
if (fromWaterDetector)
{
hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull);
}
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedWaterAmount = null;
if (fromWaterDetector)
{
linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull);
}
}
break;
case "oxygen_data_in":
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
{
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.ReceivedOxygenAmount = oxy;
hullData.LastOxygenDataTime = Timing.TotalTime;
foreach (var linked in sourceHull.linkedTo)
{
if (linked is not Hull linkedHull) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedOxygenAmount = oxy;
}
break;
}
}
}
}
@@ -11,7 +11,7 @@ namespace Barotrauma.Items.Components
{
const float NetworkUpdateIntervalHigh = 0.5f;
const float TemperatureBoostAmount = 20;
const float TemperatureBoostAmount = 25;
//the rate at which the reactor is being run on (higher rate -> higher temperature)
private float fissionRate;
@@ -26,10 +26,6 @@ namespace Barotrauma.Items.Components
//amount of power generated balanced with the load)
private bool autoTemp;
//automatical adjustment to the power output when
//turbine output and temperature are in the optimal range
private float autoAdjustAmount;
private float fuelConsumptionRate;
private float meltDownTimer, meltDownDelay;
@@ -53,6 +49,8 @@ namespace Barotrauma.Items.Components
private float temperatureBoost;
public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
private bool _powerOn;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
@@ -314,7 +312,7 @@ namespace Barotrauma.Items.Components
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime);
#if CLIENT
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost;
#endif
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
@@ -248,6 +248,7 @@ namespace Barotrauma.Items.Components
if (container?.Inventory == null) { return; }
bool recreateHudTexts = false;
for (var i = 0; i < container.Inventory.Capacity; i++)
{
if (i < 0 || GrowableSeeds.Length <= i) { continue; }
@@ -257,6 +258,7 @@ namespace Barotrauma.Items.Components
if (growable != null)
{
recreateHudTexts |= GrowableSeeds[i] != growable;
GrowableSeeds[i] = growable;
growable.IsActive = true;
}
@@ -267,11 +269,14 @@ namespace Barotrauma.Items.Components
// Kill the plant if it's somehow removed
oldGrowable.Decayed = true;
oldGrowable.IsActive = false;
recreateHudTexts = true;
}
GrowableSeeds[i] = null;
}
}
#if CLIENT
CharacterHUD.RecreateHudTexts |= recreateHudTexts;
#endif
// server handles this
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -9,6 +9,7 @@ namespace Barotrauma.Items.Components
{
//[power/min]
private float capacity;
private float adjustedCapacity;
private float charge, prevCharge;
@@ -66,7 +67,11 @@ namespace Barotrauma.Items.Components
public float Capacity
{
get => capacity;
set { capacity = Math.Max(value, 1.0f); }
set
{
capacity = Math.Max(value, 1.0f);
adjustedCapacity = GetCapacity();
}
}
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The current charge of the device.")]
@@ -76,10 +81,10 @@ namespace Barotrauma.Items.Components
set
{
if (!MathUtils.IsValid(value)) return;
charge = MathHelper.Clamp(value, 0.0f, capacity);
charge = MathHelper.Clamp(value, 0.0f, adjustedCapacity);
//send a network event if the charge has changed by more than 5%
if (Math.Abs(charge - lastSentCharge) / capacity > 0.05f)
if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f)
{
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); }
@@ -89,7 +94,7 @@ namespace Barotrauma.Items.Components
}
}
public float ChargePercentage => MathUtils.Percentage(Charge, GetCapacity());
public float ChargePercentage => MathUtils.Percentage(Charge, adjustedCapacity);
[Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")]
public float MaxRechargeSpeed
@@ -157,6 +162,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
adjustedCapacity = GetCapacity();
if (item.Connections == null)
{
IsActive = false;
@@ -164,7 +170,7 @@ namespace Barotrauma.Items.Components
}
isRunning = true;
float chargeRatio = charge / capacity;
float chargeRatio = charge / adjustedCapacity;
if (chargeRatio > 0.0f)
{
@@ -180,7 +186,7 @@ namespace Barotrauma.Items.Components
item.SendSignal(((int)Math.Round(CurrPowerOutput)).ToString(), "power_value_out");
item.SendSignal(((int)Math.Round(loadReading)).ToString(), "load_value_out");
item.SendSignal(((int)Math.Round(Charge)).ToString(), "charge");
item.SendSignal(((int)Math.Round(Charge / capacity * 100)).ToString(), "charge_%");
item.SendSignal(((int)Math.Round(Charge / adjustedCapacity * 100)).ToString(), "charge_%");
item.SendSignal(((int)Math.Round(RechargeSpeed / maxRechargeSpeed * 100)).ToString(), "charge_rate");
}
@@ -193,16 +199,16 @@ namespace Barotrauma.Items.Components
if (connection == powerIn)
{
//Don't draw power if fully charged
if (charge >= capacity)
if (charge >= adjustedCapacity)
{
charge = capacity;
charge = adjustedCapacity;
return 0;
}
else
{
if (item.Condition <= 0.0f) { return 0.0f; }
float missingCharge = capacity - charge;
float missingCharge = adjustedCapacity - charge;
float targetRechargeSpeed = rechargeSpeed;
if (ExponentialRechargeSpeed)
@@ -239,7 +245,7 @@ namespace Barotrauma.Items.Components
if (connection == powerOut)
{
float maxOutput;
float chargeRatio = prevCharge / capacity;
float chargeRatio = prevCharge / adjustedCapacity;
if (chargeRatio < 0.1f)
{
maxOutput = Math.Max(chargeRatio * 10.0f, 0.0f) * MaxOutPut;
@@ -292,7 +298,7 @@ namespace Barotrauma.Items.Components
else
{
//Decrease charge based on how much power is leaving the device
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, GetCapacity());
Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, adjustedCapacity);
prevCharge = Charge;
}
}
@@ -639,13 +639,16 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer.Item.Use(deltaTime, null);
//Use root container (e.g. loader) too in case it needs to react to firing somehow
var rootContainer = projectileContainer.Item.GetRootContainer();
if (rootContainer != projectileContainer.Item)
if (rootContainer != null && rootContainer != projectileContainer.Item)
{
rootContainer.Use(deltaTime, null);
}
else
{
projectileContainer.Item.Use(deltaTime, null);
}
}
}
else
@@ -576,11 +576,13 @@ namespace Barotrauma
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
}
#endif
CharacterHUD.RecreateHudTextsIfControlling(user);
if (item.body != null)
{
item.body.Enabled = false;
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
}
#if SERVER
@@ -924,6 +926,7 @@ namespace Barotrauma
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
}
#endif
CharacterHUD.RecreateHudTextsIfFocused(item);
}
}
@@ -1092,6 +1092,12 @@ namespace Barotrauma
}
}
var holdables = components.Where(c => c is Holdable);
if (holdables.Count() > 1)
{
DebugConsole.AddWarning($"Item {Prefab.Identifier} has multiple {nameof(Holdable)} components ({string.Join(", ", holdables.Select(h => h.GetType().Name))}).");
}
InsertToList();
ItemList.Add(this);
if (Prefab.IsDangerous) { dangerousItems.Add(this); }
@@ -1104,7 +1110,6 @@ namespace Barotrauma
if (HasTag("logic")) { isLogic = true; }
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
Components.ForEach(c => c.ApplyStatusEffects(ActionType.OnSpawn, 1.0f));
RecalculateConditionValues();
#if CLIENT
Submarine.ForceVisibilityRecheck();
@@ -2881,11 +2886,14 @@ namespace Barotrauma
}
foreach (ItemComponent ic in components) { ic.Equip(character); }
CharacterHUD.RecreateHudTextsIfControlling(character);
}
public void Unequip(Character character)
{
foreach (ItemComponent ic in components) { ic.Unequip(character); }
CharacterHUD.RecreateHudTextsIfControlling(character);
}
public List<(object obj, SerializableProperty property)> GetProperties<T>()
@@ -3427,12 +3435,15 @@ namespace Barotrauma
//if the item was in full condition considering the unmodified health
//(not taking possible HealthMultipliers added by mods into account),
//make sure it stays in full condition
bool wasFullCondition = prevCondition >= item.Prefab.Health;
if (wasFullCondition)
if (item.condition > 0)
{
item.condition = item.MaxCondition;
bool wasFullCondition = prevCondition >= item.Prefab.Health;
if (wasFullCondition)
{
item.condition = item.MaxCondition;
}
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
}
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
}
item.lastSentCondition = item.condition;
item.RecalculateConditionValues();
@@ -27,7 +27,7 @@ namespace Barotrauma
public bool IgnoreInEditor { get; set; }
private ImmutableHashSet<Identifier> excludedIdentifiers;
public ImmutableHashSet<Identifier> ExcludedIdentifiers { get; private set; }
private RelationType type;
@@ -87,20 +87,20 @@ namespace Barotrauma
public string JoinedExcludedIdentifiers
{
get { return string.Join(",", excludedIdentifiers); }
get { return string.Join(",", ExcludedIdentifiers); }
set
{
if (value == null) return;
excludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet();
ExcludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet();
}
}
public bool MatchesItem(Item item)
{
if (item == null) { return false; }
if (excludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
foreach (var excludedIdentifier in excludedIdentifiers)
if (ExcludedIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
foreach (var excludedIdentifier in ExcludedIdentifiers)
{
if (item.HasTag(excludedIdentifier)) { return false; }
}
@@ -118,8 +118,8 @@ namespace Barotrauma
public bool MatchesItem(ItemPrefab itemPrefab)
{
if (itemPrefab == null) { return false; }
if (excludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; }
foreach (var excludedIdentifier in excludedIdentifiers)
if (ExcludedIdentifiers.Contains(itemPrefab.Identifier)) { return false; }
foreach (var excludedIdentifier in ExcludedIdentifiers)
{
if (itemPrefab.Tags.Contains(excludedIdentifier)) { return false; }
}
@@ -138,7 +138,7 @@ namespace Barotrauma
public RelatedItem(Identifier[] identifiers, Identifier[] excludedIdentifiers)
{
this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
this.excludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
statusEffects = new List<StatusEffect>();
}
@@ -230,7 +230,7 @@ namespace Barotrauma
element.Add(new XAttribute(nameof(ItemPos), ItemPos.Value));
}
if (excludedIdentifiers.Count > 0)
if (ExcludedIdentifiers.Count > 0)
{
element.Add(new XAttribute("excludedidentifiers", JoinedExcludedIdentifiers));
}
@@ -228,6 +228,9 @@ namespace Barotrauma
continue;
}
Vector2 edgeDiff = edge.Point2 - edge.Point1;
Vector2 edgeDir = Vector2.Normalize(edgeDiff);
//If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
//don't touch this edge. Otherwise we may end up closing off small passages between cells.
var adjacentEmptyCell = edge.AdjacentCell(cell);
@@ -238,8 +241,10 @@ namespace Barotrauma
//find the edge at the opposite side of the adjacent cell
foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges)
{
if (Vector2.Dot(adjacentEmptyCell.Center - edge.Center, adjacentEmptyCell.Center - otherEdge.Center) > 0 &&
otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid)
if (otherEdge == edge || otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { continue; }
Vector2 otherEdgeDir = Vector2.Normalize(otherEdge.Point2 - otherEdge.Point1);
//dot product is > 0.7 if the edges are roughly parallel
if (Math.Abs(Vector2.Dot(otherEdgeDir, edgeDir)) > 0.7f)
{
adjacentEdge = otherEdge;
break;
@@ -251,13 +256,11 @@ namespace Barotrauma
continue;
}
}
List<Vector2> edgePoints = new List<Vector2>();
Vector2 edgeNormal = edge.GetNormal(cell);
float edgeLength = Vector2.Distance(edge.Point1, edge.Point2);
int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1);
Vector2 edgeDir = edge.Point2 - edge.Point1;
for (int i = 0; i <= pointCount; i++)
{
if (i == 0)
@@ -275,7 +278,7 @@ namespace Barotrauma
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
Vector2 extrudedPoint =
edge.Point1 +
edgeDir * (i / (float)pointCount) +
edgeDiff * (i / (float)pointCount) +
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
@@ -709,7 +709,7 @@ namespace Barotrauma
if (Rand.Range(0, 10, Rand.RandSync.ServerAndClient) != 0) { continue; }
}
if (!TooClose(siteX, siteY))
if (!TooCloseToOtherSites(siteX, siteY))
{
siteCoordsX.Add(siteX);
siteCoordsY.Add(siteY);
@@ -717,14 +717,14 @@ namespace Barotrauma
if (closeToCave)
{
for (int x2 = x; x2 < x + siteInterval.X; x2 += caveSiteInterval)
for (int x2 = x - siteInterval.X; x2 < x + siteInterval.X; x2 += caveSiteInterval)
{
for (int y2 = y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
for (int y2 = y - siteInterval.Y; y2 < y + siteInterval.Y; y2 += caveSiteInterval)
{
int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.ServerAndClient);
if (!TooClose(caveSiteX, caveSiteY))
if (!TooCloseToOtherSites(caveSiteX, caveSiteY, caveSiteInterval))
{
siteCoordsX.Add(caveSiteX);
siteCoordsY.Add(caveSiteY);
@@ -735,11 +735,12 @@ namespace Barotrauma
}
}
bool TooClose(double siteX, double siteY)
bool TooCloseToOtherSites(double siteX, double siteY, float minDistance = 10.0f)
{
float minDistanceSqr = minDistance * minDistance;
for (int i = 0; i < siteCoordsX.Count; i++)
{
if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < 10.0f * 10.0f)
if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < minDistanceSqr)
{
return true;
}
@@ -720,7 +720,7 @@ namespace Barotrauma
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
{
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 10)
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10)
{
//ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall
//(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
@@ -23,13 +23,22 @@ namespace Barotrauma.Networking
{
Success = 0x00,
Heartbeat = 0x01,
RequestShutdown = 0xCC,
Crash = 0xFF
}
private static ManualResetEvent writeManualResetEvent;
private static volatile bool shutDown;
public static bool HasShutDown => shutDown;
private enum StatusEnum
{
NeverStarted,
Active,
RequestedShutDown,
ShutDown
}
private static volatile StatusEnum status = StatusEnum.NeverStarted;
public static bool HasShutDown => status is StatusEnum.ShutDown;
private const int ReadBufferSize = MsgConstants.MTU * 2;
private static byte[] readTempBytes;
@@ -38,7 +47,7 @@ namespace Barotrauma.Networking
private static ConcurrentQueue<byte[]> msgsToWrite;
private static ConcurrentQueue<string> errorsToWrite;
private static ConcurrentQueue<byte[]> msgsToRead;
private static Thread readThread;
@@ -48,6 +57,8 @@ namespace Barotrauma.Networking
private static void PrivateStart()
{
status = StatusEnum.Active;
readIncOffset = 0;
readIncTotal = 0;
@@ -58,8 +69,6 @@ namespace Barotrauma.Networking
msgsToRead = new ConcurrentQueue<byte[]>();
shutDown = false;
readCancellationToken = new CancellationTokenSource();
writeManualResetEvent = new ManualResetEvent(false);
@@ -80,7 +89,13 @@ namespace Barotrauma.Networking
private static void PrivateShutDown()
{
shutDown = true;
if (Thread.CurrentThread != GameMain.MainThread)
{
throw new InvalidOperationException(
$"Cannot call {nameof(ChildServerRelay)}.{nameof(PrivateShutDown)} from a thread other than the main one");
}
if (status is StatusEnum.NeverStarted) { return; }
status = StatusEnum.ShutDown;
writeManualResetEvent?.Set();
readCancellationToken?.Cancel();
readThread?.Join(); readThread = null;
@@ -93,18 +108,18 @@ namespace Barotrauma.Networking
}
private static int ReadIncomingMsgs()
private static Option<int> ReadIncomingMsgs()
{
Task<int> readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token);
if (readTask is null) { return -1; }
if (readTask is null) { return Option<int>.None(); }
int timeOutMilliseconds = 100;
for (int i = 0; i < 150; i++)
{
if (shutDown)
if (status is StatusEnum.ShutDown)
{
readCancellationToken?.Cancel();
return -1;
return Option<int>.None();
}
try
{
@@ -115,36 +130,36 @@ namespace Barotrauma.Networking
}
catch (AggregateException aggregateException)
{
if (aggregateException.InnerException is OperationCanceledException) { return -1; }
if (aggregateException.InnerException is OperationCanceledException) { return Option<int>.None(); }
throw;
}
catch (OperationCanceledException)
{
return -1;
return Option<int>.None();
}
}
if (readTask.Status != TaskStatus.RanToCompletion)
if (readTask.Status == TaskStatus.RanToCompletion)
{
bool swallowException = shutDown
&& ((readTask.Exception?.InnerException is ObjectDisposedException)
|| (readTask.Exception?.InnerException is System.IO.IOException));
if (swallowException)
{
readCancellationToken?.Cancel();
return -1;
}
throw new Exception(
$"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.",
readTask.Exception);
return Option<int>.Some(readTask.Result);
}
return readTask.Result;
bool swallowException =
status is not StatusEnum.Active
&& readTask.Exception?.InnerException is ObjectDisposedException or System.IO.IOException;
if (swallowException)
{
readCancellationToken?.Cancel();
return Option<int>.None();
}
throw new Exception(
$"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.",
readTask.Exception);
}
private static void CheckPipeConnected(string name, PipeType pipe)
{
if (!(pipe is { IsConnected: true }))
if (status is StatusEnum.Active && pipe is not { IsConnected: true })
{
throw new Exception($"{name} was disconnected unexpectedly");
}
@@ -155,7 +170,7 @@ namespace Barotrauma.Networking
private static void UpdateRead()
{
Span<byte> msgLengthSpan = stackalloc byte[4 + 1];
while (!shutDown)
while (!HasShutDown)
{
CheckPipeConnected(nameof(readStream), readStream);
@@ -165,10 +180,9 @@ namespace Barotrauma.Networking
{
if (readIncOffset >= readIncTotal)
{
readIncTotal = ReadIncomingMsgs();
if (!ReadIncomingMsgs().TryUnwrap(out readIncTotal)) { return false; }
readIncOffset = 0;
if (readIncTotal == 0) { Thread.Yield(); continue; }
if (readIncTotal < 0) { return false; }
}
readTo[i] = readTempBytes[readIncOffset];
readIncOffset++;
@@ -176,7 +190,7 @@ namespace Barotrauma.Networking
return true;
}
if (!readBytes(msgLengthSpan)) { shutDown = true; break; }
if (!readBytes(msgLengthSpan)) { status = StatusEnum.ShutDown; break; }
int msgLength = msgLengthSpan[0]
| (msgLengthSpan[1] << 8)
@@ -184,24 +198,24 @@ namespace Barotrauma.Networking
| (msgLengthSpan[3] << 24);
WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4];
if (msgLength > 0)
{
byte[] msg = new byte[msgLength];
if (!readBytes(msg.AsSpan())) { shutDown = true; break; }
byte[] msg = msgLength > 0 ? new byte[msgLength] : Array.Empty<byte>();
if (msg.Length > 0 && !readBytes(msg.AsSpan())) { status = StatusEnum.ShutDown; break; }
switch (writeStatus)
{
case WriteStatus.Success:
msgsToRead.Enqueue(msg);
break;
case WriteStatus.Heartbeat:
//do nothing
break;
case WriteStatus.Crash:
HandleCrashString(Encoding.UTF8.GetString(msg));
shutDown = true;
break;
}
switch (writeStatus)
{
case WriteStatus.Success:
msgsToRead.Enqueue(msg);
break;
case WriteStatus.Heartbeat:
//do nothing
break;
case WriteStatus.RequestShutdown:
status = StatusEnum.ShutDown;
break;
case WriteStatus.Crash:
HandleCrashString(Encoding.UTF8.GetString(msg));
status = StatusEnum.ShutDown;
break;
}
Thread.Yield();
@@ -210,13 +224,11 @@ namespace Barotrauma.Networking
private static void UpdateWrite()
{
while (!shutDown)
while (!HasShutDown)
{
CheckPipeConnected(nameof(writeStream), writeStream);
byte[] msg;
void writeMsg(WriteStatus writeStatus)
void writeMsg(WriteStatus writeStatus, byte[] msg)
{
// It's SUPER IMPORTANT that this stack allocation
// remains in this local function and is never inlined,
@@ -224,21 +236,19 @@ namespace Barotrauma.Networking
// when the function returns; placing it in the loop
// this method is based around would lead to a stack
// overflow real quick!
Span<byte> bytesToWrite = stackalloc byte[4 + 1 + msg.Length];
Span<byte> headerBytes = stackalloc byte[4 + 1];
bytesToWrite[0] = (byte)(msg.Length & 0xFF);
bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF);
bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF);
bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF);
headerBytes[0] = (byte)(msg.Length & 0xFF);
headerBytes[1] = (byte)((msg.Length >> 8) & 0xFF);
headerBytes[2] = (byte)((msg.Length >> 16) & 0xFF);
headerBytes[3] = (byte)((msg.Length >> 24) & 0xFF);
bytesToWrite[4] = (byte)writeStatus;
Span<byte> msgSlice = bytesToWrite.Slice(4 + 1, msg.Length);
msg.AsSpan().CopyTo(msgSlice);
headerBytes[4] = (byte)writeStatus;
try
{
writeStream?.Write(bytesToWrite);
writeStream?.Write(headerBytes);
writeStream?.Write(msg);
}
catch (Exception exception)
{
@@ -246,7 +256,7 @@ namespace Barotrauma.Networking
{
case ObjectDisposedException _:
case System.IO.IOException _:
if (!shutDown) { throw; }
if (!HasShutDown) { throw; }
break;
default:
throw;
@@ -254,29 +264,34 @@ namespace Barotrauma.Networking
}
}
if (status is StatusEnum.RequestedShutDown)
{
writeMsg(WriteStatus.RequestShutdown, Array.Empty<byte>());
status = StatusEnum.ShutDown;
}
while (errorsToWrite.TryDequeue(out var error))
{
msg = Encoding.UTF8.GetBytes(error);
writeMsg(WriteStatus.Crash);
shutDown = true;
writeMsg(WriteStatus.Crash, Encoding.UTF8.GetBytes(error));
status = StatusEnum.ShutDown;
}
while (msgsToWrite.TryDequeue(out msg))
while (msgsToWrite.TryDequeue(out var msg))
{
writeMsg(WriteStatus.Success);
writeMsg(WriteStatus.Success, msg);
if (shutDown) { break; }
if (HasShutDown) { break; }
}
if (!shutDown)
if (!HasShutDown)
{
writeManualResetEvent.Reset();
if (!writeManualResetEvent.WaitOne(1000))
{
if (shutDown) { return; }
if (HasShutDown) { return; }
//heartbeat to keep the other end alive
msg = Array.Empty<byte>(); writeMsg(WriteStatus.Heartbeat);
writeMsg(WriteStatus.Heartbeat, Array.Empty<byte>());
}
}
}
@@ -284,7 +299,7 @@ namespace Barotrauma.Networking
public static void Write(byte[] msg)
{
if (shutDown) { return; }
if (HasShutDown) { return; }
if (msg.Length > 0x1fff_ffff)
{
@@ -298,7 +313,7 @@ namespace Barotrauma.Networking
public static bool Read(out byte[] msg)
{
if (shutDown) { msg = null; return false; }
if (HasShutDown) { msg = null; return false; }
return msgsToRead.TryDequeue(out msg);
}