Build 0.20.8.0
This commit is contained in:
@@ -77,11 +77,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
|
||||
{
|
||||
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 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 (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 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))
|
||||
|
||||
@@ -206,7 +206,7 @@ namespace Barotrauma
|
||||
public float Stun { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
|
||||
public bool OnlyHumans { get; private set; }
|
||||
public bool OnlyHumans { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
public string ApplyForceOnLimbs
|
||||
@@ -322,7 +322,7 @@ namespace Barotrauma
|
||||
List<Affliction> multipliedAfflictions = new List<Affliction>();
|
||||
foreach (Affliction affliction in Afflictions.Keys)
|
||||
{
|
||||
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction.Probability));
|
||||
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier, affliction));
|
||||
}
|
||||
return multipliedAfflictions;
|
||||
}
|
||||
|
||||
@@ -529,7 +529,7 @@ namespace Barotrauma
|
||||
private Color speechBubbleColor;
|
||||
private float speechBubbleTimer;
|
||||
|
||||
public bool ResetInteract;
|
||||
public bool DisableInteract { get; set; }
|
||||
|
||||
//text displayed when the character is highlighted if custom interact is set
|
||||
public LocalizedString CustomInteractHUDText { get; private set; }
|
||||
@@ -798,6 +798,7 @@ namespace Barotrauma
|
||||
public float MaxHealth => MaxVitality;
|
||||
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
|
||||
public bool IsLatched => AIController is EnemyAIController enemyAI && enemyAI.LatchOntoAI != null && enemyAI.LatchOntoAI.IsAttached;
|
||||
public float EmpVulnerability => Params.Health.EmpVulnerability;
|
||||
|
||||
public float Bloodloss
|
||||
{
|
||||
@@ -845,6 +846,12 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
public bool IgnoreMeleeWeapons
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
|
||||
/// </summary>
|
||||
@@ -895,6 +902,10 @@ namespace Barotrauma
|
||||
{
|
||||
itemSelectedTime = Timing.TotalTime;
|
||||
}
|
||||
if (prevSelectedItem != _selectedItem && prevSelectedItem?.OnDeselect != null)
|
||||
{
|
||||
prevSelectedItem.OnDeselect(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
@@ -2687,9 +2698,9 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (ResetInteract)
|
||||
if (DisableInteract)
|
||||
{
|
||||
ResetInteract = false;
|
||||
DisableInteract = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2710,25 +2721,31 @@ namespace Barotrauma
|
||||
{
|
||||
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this))
|
||||
{
|
||||
if ((findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) && (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld))
|
||||
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
|
||||
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
|
||||
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
||||
if (!PlayerInput.PrimaryMouseButtonHeld() || Barotrauma.Inventory.DraggingItemToWorld)
|
||||
{
|
||||
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
|
||||
aimAssist = 0.0f;
|
||||
}
|
||||
FocusedCharacter = CanInteract || CanEat ? FindCharacterAtPosition(mouseSimPos) : null;
|
||||
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
float aimAssist = GameSettings.CurrentConfig.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
|
||||
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
||||
{
|
||||
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
|
||||
aimAssist = 0.0f;
|
||||
}
|
||||
|
||||
var item = FindItemAtPosition(mouseSimPos, aimAssist);
|
||||
|
||||
focusedItem = CanInteract ? item : null;
|
||||
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
|
||||
{
|
||||
FocusedCharacter = null;
|
||||
focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null;
|
||||
if (focusedItem != null && focusedItem.CampaignInteractionType != CampaignMode.InteractionType.None)
|
||||
{
|
||||
FocusedCharacter = null;
|
||||
}
|
||||
findFocusedTimer = 0.05f;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (focusedItem != null && !CanInteractWith(focusedItem)) { focusedItem = null; }
|
||||
if (FocusedCharacter != null && !CanInteractWith(FocusedCharacter)) { FocusedCharacter = null; }
|
||||
}
|
||||
findFocusedTimer = 0.05f;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2957,6 +2974,15 @@ namespace Barotrauma
|
||||
{
|
||||
UpdateProjSpecific(deltaTime, cam);
|
||||
|
||||
if (InvisibleTimer > 0.0f)
|
||||
{
|
||||
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
|
||||
{
|
||||
InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
|
||||
}
|
||||
InvisibleTimer -= deltaTime;
|
||||
}
|
||||
|
||||
KnockbackCooldownTimer -= deltaTime;
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
|
||||
@@ -2996,6 +3022,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
HideFace = false;
|
||||
IgnoreMeleeWeapons = false;
|
||||
|
||||
UpdateSightRange(deltaTime);
|
||||
UpdateSoundRange(deltaTime);
|
||||
@@ -4102,7 +4129,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
|
||||
if (Screen.Selected != GameMain.GameScreen) { return; }
|
||||
if (newStun > 0 && Params.Health.StunImmunity) { return; }
|
||||
if (newStun > 0 && Params.Health.StunImmunity)
|
||||
{
|
||||
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
|
||||
if (Math.Sign(newStun) != Math.Sign(Stun))
|
||||
{
|
||||
|
||||
@@ -1238,7 +1238,7 @@ namespace Barotrauma
|
||||
|
||||
partial void OnSkillChanged(Identifier skillIdentifier, float prevLevel, float newLevel);
|
||||
|
||||
public void GiveExperience(int amount, bool isMissionExperience = false)
|
||||
public void GiveExperience(int amount)
|
||||
{
|
||||
int prevAmount = ExperiencePoints;
|
||||
|
||||
@@ -1292,6 +1292,18 @@ namespace Barotrauma
|
||||
return experienceRequired + ExperienceRequiredPerLevel(level);
|
||||
}
|
||||
|
||||
public int GetExperienceRequiredForLevel(int level)
|
||||
{
|
||||
int currentLevel = GetCurrentLevel(out int experienceRequired);
|
||||
if (currentLevel >= level) { return 0; }
|
||||
int required = experienceRequired;
|
||||
for (int i = currentLevel + 1; i <= level; i++)
|
||||
{
|
||||
required += ExperienceRequiredPerLevel(i);
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
public int GetCurrentLevel()
|
||||
{
|
||||
return GetCurrentLevel(out _);
|
||||
@@ -1309,7 +1321,7 @@ namespace Barotrauma
|
||||
return level;
|
||||
}
|
||||
|
||||
private int ExperienceRequiredPerLevel(int level)
|
||||
private static int ExperienceRequiredPerLevel(int level)
|
||||
{
|
||||
return BaseExperienceRequired + AddedExperienceRequiredPerLevel * level;
|
||||
}
|
||||
|
||||
+14
-2
@@ -61,6 +61,9 @@ namespace Barotrauma
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
|
||||
public bool DivideByLimbCount { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
|
||||
public bool MultiplyByMaxVitality { get; private set; }
|
||||
|
||||
public float DamagePerSecond;
|
||||
public float DamagePerSecondTimer;
|
||||
public float PreviousVitalityDecrease;
|
||||
@@ -104,6 +107,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy properties here instead of using SerializableProperties (with reflection).
|
||||
/// </summary>
|
||||
public void CopyProperties(Affliction source)
|
||||
{
|
||||
Probability = source.Probability;
|
||||
DivideByLimbCount = source.DivideByLimbCount;
|
||||
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
|
||||
}
|
||||
|
||||
public void Serialize(XElement element)
|
||||
{
|
||||
@@ -115,10 +127,10 @@ namespace Barotrauma
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
public Affliction CreateMultiplied(float multiplier, float probability)
|
||||
public Affliction CreateMultiplied(float multiplier, Affliction affliction)
|
||||
{
|
||||
var instance = Prefab.Instantiate(NonClampedStrength * multiplier, Source);
|
||||
instance.Probability = probability;
|
||||
instance.CopyProperties(affliction);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -693,8 +693,15 @@ namespace Barotrauma
|
||||
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
|
||||
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
|
||||
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
|
||||
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun") { return; }
|
||||
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == "stun")
|
||||
{
|
||||
if (Character.EmpVulnerability <= 0 || GetAfflictionStrength("emp", allowLimbAfflictions: false) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (Character.Params.Health.PoisonImmunity && newAffliction.Prefab.AfflictionType == "poison") { return; }
|
||||
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == "emp") { return; }
|
||||
if (newAffliction.Prefab is AfflictionPrefabHusk huskPrefab)
|
||||
{
|
||||
if (huskPrefab.TargetSpecies.None(s => s == Character.SpeciesName))
|
||||
|
||||
@@ -757,6 +757,10 @@ namespace Barotrauma
|
||||
}
|
||||
if (!foundMatchingModifier && random > affliction.Probability) { continue; }
|
||||
float finalDamageModifier = damageMultiplier;
|
||||
if (affliction.Prefab.AfflictionType == "emp" && character.EmpVulnerability > 0)
|
||||
{
|
||||
finalDamageModifier *= character.EmpVulnerability;
|
||||
}
|
||||
foreach (DamageModifier damageModifier in tempModifiers)
|
||||
{
|
||||
float damageModifierValue = damageModifier.DamageMultiplier;
|
||||
@@ -766,9 +770,13 @@ namespace Barotrauma
|
||||
}
|
||||
finalDamageModifier *= damageModifierValue;
|
||||
}
|
||||
if (affliction.MultiplyByMaxVitality)
|
||||
{
|
||||
finalDamageModifier *= character.MaxVitality / 100f;
|
||||
}
|
||||
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
|
||||
{
|
||||
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction.Probability);
|
||||
newAffliction = affliction.CreateMultiplied(finalDamageModifier, affliction);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -501,6 +501,9 @@ namespace Barotrauma
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
public bool PoisonImmunity { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes), Editable]
|
||||
public float EmpVulnerability { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
|
||||
public bool ApplyAfflictionColors { get; private set; }
|
||||
|
||||
|
||||
+9
-4
@@ -37,14 +37,19 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return false; }
|
||||
|
||||
// FIXME there's probably a better way to check the faction affiliated with the mission later
|
||||
foreach (Identifier factionIdentifier in mission.ReputationRewards.Keys)
|
||||
foreach (var (factionIdentifier, amount) in mission.ReputationRewards)
|
||||
{
|
||||
if (factions.Where(faction => factionIdentifier == faction.Prefab.Identifier).Any(static faction => faction.GetPlayerAffiliationStatus() != FactionAffiliation.Affiliated))
|
||||
if (amount <= 0) { continue; }
|
||||
|
||||
Faction faction = factions.FirstOrDefault(faction => factionIdentifier == faction.Prefab.Identifier);
|
||||
|
||||
if (faction?.GetPlayerAffiliationStatus() is FactionAffiliation.Affiliated)
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return missionType.Contains(mission.Prefab.Type);
|
||||
|
||||
+19
-1
@@ -5,15 +5,33 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
|
||||
public override bool AppliesEffectOnIntervalUpdate => true;
|
||||
|
||||
private readonly int amount;
|
||||
private readonly int level;
|
||||
|
||||
public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
amount = abilityElement.GetAttributeInt("amount", 0);
|
||||
level = abilityElement.GetAttributeInt("level", 0);
|
||||
|
||||
if (amount == 0 && level == 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.");
|
||||
}
|
||||
if (amount > 0 && level > 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyEffectSpecific(Character targetCharacter)
|
||||
{
|
||||
targetCharacter.Info?.GiveExperience(amount);
|
||||
if (amount != 0)
|
||||
{
|
||||
targetCharacter.Info?.GiveExperience(amount);
|
||||
}
|
||||
if (level > 0)
|
||||
{
|
||||
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
class CharacterAbilityRevive : CharacterAbility
|
||||
{
|
||||
public override bool AppliesEffectOnIntervalUpdate => true;
|
||||
|
||||
public CharacterAbilityRevive(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
}
|
||||
|
||||
private void ApplyEffectSpecific()
|
||||
{
|
||||
Character.Revive(removeAllAfflictions: false);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-4
@@ -35,10 +35,8 @@ namespace Barotrauma.Abilities
|
||||
foreach (Identifier identifier in selectedTalentTree)
|
||||
{
|
||||
if (Character.HasTalent(identifier)) { continue; }
|
||||
if (Character.GiveTalent(identifier))
|
||||
{
|
||||
Character.Info.AdditionalTalentPoints++;
|
||||
}
|
||||
|
||||
Character.GiveTalent(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,23 +139,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
|
||||
{
|
||||
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
|
||||
return !talentOptionStage.HasMaxTalents(selectedTalents) && TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
|
||||
}
|
||||
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
|
||||
if (!optionStageCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
/*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents);
|
||||
if (!hasTalentInThisTier)
|
||||
{
|
||||
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
|
||||
{
|
||||
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
|
||||
}
|
||||
break;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-24
@@ -67,10 +67,10 @@ namespace Barotrauma
|
||||
.ToImmutableHashSet();
|
||||
}
|
||||
|
||||
public static Result<ContentFile, LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
|
||||
public static Result<ContentFile, ContentPackage.LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
|
||||
{
|
||||
static Result<ContentFile, LoadError> fail(string error, Exception? exception = null)
|
||||
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception));
|
||||
static Result<ContentFile, ContentPackage.LoadError> fail(string error, Exception? exception = null)
|
||||
=> Result<ContentFile, ContentPackage.LoadError>.Failure(new ContentPackage.LoadError(error, exception));
|
||||
|
||||
Identifier elemName = element.NameAsIdentifier();
|
||||
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
|
||||
@@ -83,6 +83,8 @@ namespace Barotrauma
|
||||
{
|
||||
return fail($"No content path defined for file of type \"{elemName}\"");
|
||||
}
|
||||
|
||||
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
|
||||
try
|
||||
{
|
||||
filePath = type.MutateContentPath(filePath);
|
||||
@@ -90,10 +92,16 @@ namespace Barotrauma
|
||||
{
|
||||
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
|
||||
}
|
||||
|
||||
var file = type.CreateInstance(contentPackage, filePath);
|
||||
return file is null
|
||||
? throw new Exception($"Content type is not implemented correctly")
|
||||
: Result<ContentFile, LoadError>.Success(file);
|
||||
if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); }
|
||||
|
||||
if (errorCatcher.Errors.Any())
|
||||
{
|
||||
return fail(
|
||||
$"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\"");
|
||||
}
|
||||
return Result<ContentFile, ContentPackage.LoadError>.Success(file);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -123,23 +131,5 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
|
||||
|
||||
public readonly struct LoadError
|
||||
{
|
||||
public readonly string Message;
|
||||
public readonly Exception? Exception;
|
||||
|
||||
public LoadError(string message, Exception? exception)
|
||||
{
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Message
|
||||
+ (Exception is { StackTrace: var stackTrace }
|
||||
? '\n' + stackTrace.CleanupStackTrace()
|
||||
: string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
-51
@@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -14,6 +13,15 @@ namespace Barotrauma
|
||||
{
|
||||
public abstract class ContentPackage
|
||||
{
|
||||
public readonly record struct LoadError(string Message, Exception? Exception)
|
||||
{
|
||||
public override string ToString()
|
||||
=> Message
|
||||
+ (Exception is { StackTrace: var stackTrace }
|
||||
? '\n' + stackTrace.CleanupStackTrace()
|
||||
: string.Empty);
|
||||
}
|
||||
|
||||
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
|
||||
|
||||
public const string LocalModsDir = "LocalMods";
|
||||
@@ -37,12 +45,30 @@ namespace Barotrauma
|
||||
public readonly Option<DateTime> InstallTime;
|
||||
|
||||
public ImmutableArray<ContentFile> Files { get; private set; }
|
||||
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Errors that occurred when loading this content package.
|
||||
/// Currently, all errors are considered fatal and the game
|
||||
/// will refuse to load a content package that has any errors.
|
||||
/// </summary>
|
||||
public ImmutableArray<LoadError> FatalLoadErrors { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// An error that occurred when trying to enable this mod.
|
||||
/// This field doesn't directly affect whether or not this mod
|
||||
/// can be enabled, but if it's been set to anything other than
|
||||
/// Option.None then the game has already refused to enable it
|
||||
/// at least once.
|
||||
/// </summary>
|
||||
public Option<ContentPackageManager.LoadProgress.Error> EnableError { get; private set; }
|
||||
= Option.None;
|
||||
|
||||
public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome();
|
||||
|
||||
public async Task<bool> IsUpToDate()
|
||||
{
|
||||
if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
|
||||
if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; }
|
||||
if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; }
|
||||
if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
|
||||
|
||||
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
|
||||
@@ -55,20 +81,25 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Does the content package include some content that needs to match between all players in multiplayer.
|
||||
/// </summary>
|
||||
public bool HasMultiplayerSyncedContent { get; private set; }
|
||||
public bool HasMultiplayerSyncedContent { get; }
|
||||
|
||||
protected ContentPackage(XDocument doc, string path)
|
||||
{
|
||||
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
|
||||
|
||||
Path = path.CleanUpPathCrossPlatform();
|
||||
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
|
||||
|
||||
Name = rootElement.GetAttributeString("name", "").Trim();
|
||||
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
|
||||
.Select(n => n.Trim()).ToImmutableArray();
|
||||
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
|
||||
|
||||
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
|
||||
|
||||
|
||||
if (Name.IsNullOrWhiteSpace() && AltNames.Any())
|
||||
{
|
||||
Name = AltNames.First();
|
||||
}
|
||||
|
||||
UgcId = steamWorkshopId != 0
|
||||
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
|
||||
: Option<ContentPackageId>.None();
|
||||
@@ -85,23 +116,31 @@ namespace Barotrauma
|
||||
.ToArray();
|
||||
|
||||
Files = fileResults
|
||||
.OfType<Success<ContentFile, ContentFile.LoadError>>()
|
||||
.Select(f => f.Value)
|
||||
.Successes()
|
||||
.ToImmutableArray();
|
||||
|
||||
Errors = fileResults
|
||||
.OfType<Failure<ContentFile, ContentFile.LoadError>>()
|
||||
.Select(f => f.Error)
|
||||
FatalLoadErrors = fileResults
|
||||
.Failures()
|
||||
.ToImmutableArray();
|
||||
|
||||
AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty");
|
||||
|
||||
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
|
||||
|
||||
Hash = CalculateHash();
|
||||
var expectedHash = rootElement.GetAttributeString("expectedhash", "");
|
||||
if (HashMismatches(expectedHash))
|
||||
{
|
||||
DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})");
|
||||
FatalLoadErrors = FatalLoadErrors.Add(
|
||||
new LoadError(
|
||||
Message: $"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}",
|
||||
Exception: null
|
||||
));
|
||||
}
|
||||
|
||||
FatalLoadErrors = FatalLoadErrors
|
||||
.Concat(errorCatcher.Errors.Select(err => new LoadError(err.Text, null)))
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
public bool HashMismatches(string expectedHash)
|
||||
@@ -122,21 +161,21 @@ namespace Barotrauma
|
||||
public bool NameMatches(string name)
|
||||
=> NameMatches(name.ToIdentifier());
|
||||
|
||||
public static ContentPackage? TryLoad(string path)
|
||||
public static Result<ContentPackage, Exception> TryLoad(string path)
|
||||
{
|
||||
var (success, failure) = Result<ContentPackage, Exception>.GetFactoryMethods();
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(path);
|
||||
|
||||
try
|
||||
{
|
||||
return doc.Root.GetAttributeBool("corepackage", false)
|
||||
? (ContentPackage)new CorePackage(doc, path)
|
||||
: new RegularPackage(doc, path);
|
||||
return success(doc.Root.GetAttributeBool("corepackage", false)
|
||||
? new CorePackage(doc, path)
|
||||
: new RegularPackage(doc, path));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e = e.GetInnermost();
|
||||
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
|
||||
return null;
|
||||
return failure(e.GetInnermost());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +220,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load \"{Name}\" at {Path}: {errorMsg}");
|
||||
FatalLoadErrors = FatalLoadErrors.Add(new LoadError(errorMsg, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,17 +240,19 @@ namespace Barotrauma
|
||||
Failure
|
||||
}
|
||||
|
||||
public LoadResult LoadPackage()
|
||||
public LoadResult LoadContent()
|
||||
{
|
||||
foreach (var p in LoadPackageEnumerable())
|
||||
foreach (var p in LoadContentEnumerable())
|
||||
{
|
||||
if (p.Exception != null) { return LoadResult.Failure; }
|
||||
if (p.Result.IsFailure) { return LoadResult.Failure; }
|
||||
}
|
||||
return LoadResult.Success;
|
||||
}
|
||||
|
||||
public IEnumerable<ContentPackageManager.LoadProgress> LoadPackageEnumerable()
|
||||
public IEnumerable<ContentPackageManager.LoadProgress> LoadContentEnumerable()
|
||||
{
|
||||
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
|
||||
|
||||
ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
|
||||
=> Files.Where(predicate.Invoke).ToArray()
|
||||
#if DEBUG
|
||||
@@ -227,6 +268,7 @@ namespace Barotrauma
|
||||
for (int i = 0; i < filesToLoad.Length; i++)
|
||||
{
|
||||
Exception? exception = null;
|
||||
|
||||
try
|
||||
{
|
||||
//do not allow exceptions thrown here to crash the game
|
||||
@@ -234,42 +276,53 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var innermost = e.GetInnermost();
|
||||
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}");
|
||||
exception = e;
|
||||
}
|
||||
if (exception != null)
|
||||
{
|
||||
yield return ContentPackageManager.LoadProgress.Failure(exception);
|
||||
break;
|
||||
yield break;
|
||||
}
|
||||
yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length);
|
||||
|
||||
if (errorCatcher.Errors.Any())
|
||||
{
|
||||
yield return ContentPackageManager.LoadProgress.Failure(
|
||||
ContentPackageManager.LoadProgress.Error
|
||||
.Reason.ConsoleErrorsThrown);
|
||||
yield break;
|
||||
}
|
||||
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
|
||||
}
|
||||
}
|
||||
|
||||
//Load the UI and text files first. This is to allow the game
|
||||
//to render the text in the loading screen as soon as possible.
|
||||
var priorityFiles = getFilesToLoad(f => f is UIStyleFile || f is TextFile);
|
||||
var priorityFiles = getFilesToLoad(f => f is UIStyleFile or TextFile);
|
||||
|
||||
var remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
|
||||
|
||||
var loadEnumerable =
|
||||
loadFiles(priorityFiles, 0)
|
||||
.Concat(loadFiles(remainder, priorityFiles.Length));
|
||||
|
||||
|
||||
foreach (var p in loadEnumerable)
|
||||
{
|
||||
if (p.Exception != null)
|
||||
if (p.Result.TryUnwrapFailure(out var failure))
|
||||
{
|
||||
HandleLoadException(p.Exception);
|
||||
errorCatcher.Dispose();
|
||||
UnloadContent();
|
||||
EnableError = Option.Some(failure);
|
||||
yield return p;
|
||||
break;
|
||||
yield break;
|
||||
}
|
||||
yield return p;
|
||||
}
|
||||
errorCatcher.Dispose();
|
||||
}
|
||||
|
||||
protected abstract void HandleLoadException(Exception e);
|
||||
|
||||
public void UnloadPackage()
|
||||
public void UnloadContent()
|
||||
{
|
||||
Files.ForEach(f => f.UnloadFile());
|
||||
}
|
||||
@@ -284,21 +337,16 @@ namespace Barotrauma
|
||||
.Select(e => ContentFile.CreateFromXElement(this, e))
|
||||
.ToArray();
|
||||
|
||||
foreach (var result in fileResults)
|
||||
foreach (var file in fileResults.Successes())
|
||||
{
|
||||
switch (result)
|
||||
if (file is BaseSubFile or ItemAssemblyFile)
|
||||
{
|
||||
case Success<ContentFile, ContentFile.LoadError> { Value: var file }:
|
||||
if (file is BaseSubFile || file is ItemAssemblyFile)
|
||||
{
|
||||
newFileList.Add(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
|
||||
newFileList.Add(existingFile ?? file);
|
||||
}
|
||||
break;
|
||||
newFileList.Add(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
|
||||
newFileList.Add(existingFile ?? file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,16 +379,16 @@ namespace Barotrauma
|
||||
|
||||
public void LogErrors()
|
||||
{
|
||||
if (!Errors.Any())
|
||||
if (!FatalLoadErrors.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DebugConsole.AddWarning(
|
||||
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
|
||||
string.Join('\n', Errors.Select(errorToStr)));
|
||||
string.Join('\n', FatalLoadErrors.Select(errorToStr)));
|
||||
|
||||
static string errorToStr(ContentFile.LoadError error)
|
||||
static string errorToStr(LoadError error)
|
||||
=> error.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
-5
@@ -43,10 +43,5 @@ namespace Barotrauma
|
||||
"Core package requires at least one of the following content types: " +
|
||||
string.Join(", ", missingFileTypes.Select(t => t.Type.Name)));
|
||||
}
|
||||
|
||||
protected override void HandleLoadException(Exception e)
|
||||
{
|
||||
throw new Exception($"An exception was thrown while loading \"{Name}\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
-7
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -9,11 +8,5 @@ namespace Barotrauma
|
||||
{
|
||||
AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package");
|
||||
}
|
||||
|
||||
protected override void HandleLoadException(Exception e)
|
||||
{
|
||||
UnloadPackage();
|
||||
DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
-66
@@ -47,18 +47,19 @@ namespace Barotrauma
|
||||
{
|
||||
var oldCore = Core;
|
||||
if (newCore == oldCore) { yield break; }
|
||||
Core?.UnloadPackage();
|
||||
if (newCore.FatalLoadErrors.Any()) { yield break; }
|
||||
Core?.UnloadContent();
|
||||
Core = newCore;
|
||||
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; }
|
||||
foreach (var p in newCore.LoadContentEnumerable()) { yield return p; }
|
||||
SortContent();
|
||||
yield return new LoadProgress(1.0f);
|
||||
yield return LoadProgress.Progress(1.0f);
|
||||
}
|
||||
|
||||
public static void ReloadCore()
|
||||
{
|
||||
if (Core == null) { return; }
|
||||
Core.UnloadPackage();
|
||||
Core.LoadPackage();
|
||||
Core.UnloadContent();
|
||||
Core.LoadContent();
|
||||
SortContent();
|
||||
}
|
||||
|
||||
@@ -79,10 +80,14 @@ namespace Barotrauma
|
||||
if (ReferenceEquals(inNewRegular, regular)) { yield break; }
|
||||
if (inNewRegular.SequenceEqual(regular)) { yield break; }
|
||||
ThrowIfDuplicates(inNewRegular);
|
||||
var newRegular = inNewRegular.ToList();
|
||||
var newRegular = inNewRegular
|
||||
// Refuse to enable packages with load errors
|
||||
// so people are forced away from broken mods
|
||||
.Where(r => !r.FatalLoadErrors.Any())
|
||||
.ToList();
|
||||
IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
|
||||
RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
|
||||
toUnload.ForEach(r => r.UnloadPackage());
|
||||
toUnload.ForEach(r => r.UnloadContent());
|
||||
|
||||
Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
|
||||
|
||||
@@ -90,9 +95,9 @@ namespace Barotrauma
|
||||
{
|
||||
var package = toLoad[i];
|
||||
loadingRange = new Range<float>(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length);
|
||||
foreach (var progress in package.LoadPackageEnumerable())
|
||||
foreach (var progress in package.LoadContentEnumerable())
|
||||
{
|
||||
if (progress.Exception != null)
|
||||
if (progress.Result.IsFailure)
|
||||
{
|
||||
//If an exception was thrown while loading this package, refuse to add it to the list of enabled packages
|
||||
newRegular.Remove(package);
|
||||
@@ -103,7 +108,7 @@ namespace Barotrauma
|
||||
}
|
||||
regular.Clear(); regular.AddRange(newRegular);
|
||||
SortContent();
|
||||
yield return new LoadProgress(1.0f);
|
||||
yield return LoadProgress.Progress(1.0f);
|
||||
}
|
||||
|
||||
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
|
||||
@@ -231,10 +236,12 @@ namespace Barotrauma
|
||||
public sealed partial class PackageSource : ICollection<ContentPackage>
|
||||
{
|
||||
private readonly Predicate<string>? skipPredicate;
|
||||
private readonly Action<string, Exception>? onLoadFail;
|
||||
|
||||
public PackageSource(string dir, Predicate<string>? skipPredicate)
|
||||
public PackageSource(string dir, Predicate<string>? skipPredicate, Action<string, Exception>? onLoadFail)
|
||||
{
|
||||
this.skipPredicate = skipPredicate;
|
||||
this.onLoadFail = onLoadFail;
|
||||
directory = dir;
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
@@ -278,25 +285,30 @@ namespace Barotrauma
|
||||
{
|
||||
var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
|
||||
if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; }
|
||||
if (File.Exists(fileListPath))
|
||||
{
|
||||
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
|
||||
|
||||
ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath);
|
||||
if (newPackage is CorePackage corePackage)
|
||||
{
|
||||
corePackages.Add(corePackage);
|
||||
}
|
||||
else if (newPackage is RegularPackage regularPackage)
|
||||
{
|
||||
regularPackages.Add(regularPackage);
|
||||
}
|
||||
|
||||
if (!(newPackage is null))
|
||||
{
|
||||
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
|
||||
}
|
||||
if (!File.Exists(fileListPath)) { continue; }
|
||||
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
|
||||
|
||||
var result = ContentPackage.TryLoad(fileListPath);
|
||||
if (!result.TryUnwrapSuccess(out var newPackage))
|
||||
{
|
||||
onLoadFail?.Invoke(
|
||||
fileListPath,
|
||||
result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable"));
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (newPackage)
|
||||
{
|
||||
case CorePackage corePackage:
|
||||
corePackages.Add(corePackage);
|
||||
break;
|
||||
case RegularPackage regularPackage:
|
||||
regularPackages.Add(regularPackage);
|
||||
break;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,8 +360,20 @@ namespace Barotrauma
|
||||
public bool IsReadOnly => true;
|
||||
}
|
||||
|
||||
public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null);
|
||||
public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath);
|
||||
public static readonly PackageSource LocalPackages
|
||||
= new PackageSource(
|
||||
ContentPackage.LocalModsDir,
|
||||
skipPredicate: null,
|
||||
onLoadFail: null);
|
||||
public static readonly PackageSource WorkshopPackages = new PackageSource(
|
||||
ContentPackage.WorkshopModsDir,
|
||||
skipPredicate: SteamManager.Workshop.IsInstallingToPath,
|
||||
onLoadFail: (fileListPath, exception) =>
|
||||
{
|
||||
// Delete Workshop mods that fail to load to
|
||||
// force a reinstall on next launch if necessary
|
||||
Directory.TryDelete(Path.GetDirectoryName(fileListPath)!);
|
||||
});
|
||||
|
||||
public static CorePackage? VanillaCorePackage { get; private set; } = null;
|
||||
|
||||
@@ -373,63 +397,77 @@ namespace Barotrauma
|
||||
EnabledPackages.DisableRemovedMods();
|
||||
}
|
||||
|
||||
public static ContentPackage? ReloadContentPackage(ContentPackage p)
|
||||
public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
|
||||
{
|
||||
ContentPackage? newPackage = ContentPackage.TryLoad(p.Path);
|
||||
if (newPackage is CorePackage core)
|
||||
{
|
||||
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
|
||||
}
|
||||
else if (newPackage is RegularPackage regular)
|
||||
{
|
||||
int index = EnabledPackages.Regular.IndexOf(p);
|
||||
if (index >= 0)
|
||||
{
|
||||
var newRegular = EnabledPackages.Regular.ToArray();
|
||||
newRegular[index] = regular;
|
||||
EnabledPackages.SetRegular(newRegular);
|
||||
}
|
||||
}
|
||||
var result = ContentPackage.TryLoad(p.Path);
|
||||
|
||||
if (newPackage != null)
|
||||
if (result.TryUnwrapSuccess(out var newPackage))
|
||||
{
|
||||
switch (newPackage)
|
||||
{
|
||||
case CorePackage core:
|
||||
{
|
||||
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
|
||||
|
||||
break;
|
||||
}
|
||||
case RegularPackage regular:
|
||||
{
|
||||
int index = EnabledPackages.Regular.IndexOf(p);
|
||||
if (index >= 0)
|
||||
{
|
||||
var newRegular = EnabledPackages.Regular.ToArray();
|
||||
newRegular[index] = regular;
|
||||
EnabledPackages.SetRegular(newRegular);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LocalPackages.SwapPackage(p, newPackage);
|
||||
WorkshopPackages.SwapPackage(p, newPackage);
|
||||
}
|
||||
EnabledPackages.DisableRemovedMods();
|
||||
return newPackage;
|
||||
return result;
|
||||
}
|
||||
|
||||
public readonly struct LoadProgress
|
||||
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
|
||||
{
|
||||
public readonly float Value;
|
||||
public readonly Exception? Exception;
|
||||
|
||||
public LoadProgress(float value)
|
||||
public readonly record struct Error(
|
||||
Error.Reason ErrorReason,
|
||||
Option<Exception> Exception)
|
||||
{
|
||||
Value = value;
|
||||
Exception = null;
|
||||
}
|
||||
public enum Reason { Exception, ConsoleErrorsThrown }
|
||||
|
||||
private LoadProgress(Exception exception)
|
||||
{
|
||||
Value = -1f;
|
||||
Exception = exception;
|
||||
public Error(Reason reason) : this(reason, Option.None) { }
|
||||
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
|
||||
}
|
||||
|
||||
public static LoadProgress Failure(Exception exception)
|
||||
=> new LoadProgress(exception);
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(exception)));
|
||||
|
||||
public static LoadProgress Failure(Error.Reason reason)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Failure(new Error(reason)));
|
||||
|
||||
public static LoadProgress Progress(float value)
|
||||
=> new LoadProgress(
|
||||
Result<float, Error>.Success(value));
|
||||
|
||||
public LoadProgress Transform(Range<float> range)
|
||||
=> Exception != null
|
||||
? this
|
||||
: new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value));
|
||||
=> Result.TryUnwrapSuccess(out var value)
|
||||
? new LoadProgress(
|
||||
Result<float, Error>.Success(
|
||||
MathHelper.Lerp(range.Start, range.End, value)))
|
||||
: this;
|
||||
}
|
||||
|
||||
public static void LoadVanillaFileList()
|
||||
{
|
||||
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
|
||||
foreach (ContentFile.LoadError error in VanillaCorePackage.Errors)
|
||||
foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors)
|
||||
{
|
||||
DebugConsole.ThrowError(error.ToString());
|
||||
}
|
||||
@@ -444,6 +482,8 @@ namespace Barotrauma
|
||||
|
||||
if (VanillaCorePackage is null) { LoadVanillaFileList(); }
|
||||
|
||||
SteamManager.Workshop.DeleteUnsubscribedMods();
|
||||
|
||||
CorePackage enabledCorePackage = VanillaCorePackage!;
|
||||
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
|
||||
|
||||
@@ -512,7 +552,7 @@ namespace Barotrauma
|
||||
yield return p.Transform(loadingRange);
|
||||
}
|
||||
|
||||
yield return new LoadProgress(1.0f);
|
||||
yield return LoadProgress.Progress(1.0f);
|
||||
}
|
||||
|
||||
public static void LogEnabledRegularPackageErrors()
|
||||
|
||||
@@ -5,6 +5,7 @@ using Barotrauma.Steam;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
@@ -15,12 +16,12 @@ using Barotrauma.MapCreatures.Behavior;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
struct ColoredText
|
||||
readonly struct ColoredText
|
||||
{
|
||||
public string Text;
|
||||
public Color Color;
|
||||
public bool IsCommand;
|
||||
public bool IsError;
|
||||
public readonly string Text;
|
||||
public readonly Color Color;
|
||||
public readonly bool IsCommand;
|
||||
public readonly bool IsError;
|
||||
|
||||
public readonly string Time;
|
||||
|
||||
@@ -31,7 +32,7 @@ namespace Barotrauma
|
||||
this.IsCommand = isCommand;
|
||||
this.IsError = isError;
|
||||
|
||||
Time = DateTime.Now.ToString();
|
||||
Time = DateTime.Now.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,13 +92,57 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Queue<ColoredText> queuedMessages = new Queue<ColoredText>();
|
||||
private static readonly ConcurrentQueue<ColoredText> queuedMessages
|
||||
= new ConcurrentQueue<ColoredText>();
|
||||
|
||||
public static readonly NamedEvent<ColoredText> MessageHandler = new NamedEvent<ColoredText>();
|
||||
|
||||
public struct ErrorCatcher : IDisposable
|
||||
{
|
||||
private readonly List<ColoredText> errors;
|
||||
private readonly bool wasConsoleOpen;
|
||||
private Identifier handlerId;
|
||||
public IReadOnlyList<ColoredText> Errors => errors;
|
||||
|
||||
private ErrorCatcher(Identifier handlerId)
|
||||
{
|
||||
this.handlerId = handlerId;
|
||||
#if CLIENT
|
||||
this.wasConsoleOpen = IsOpen;
|
||||
#else
|
||||
this.wasConsoleOpen = false;
|
||||
#endif
|
||||
this.errors = new List<ColoredText>();
|
||||
|
||||
//create a local variable that can be captured by lambdas
|
||||
var errs = this.errors;
|
||||
|
||||
MessageHandler.Register(handlerId, msg =>
|
||||
{
|
||||
if (!msg.IsError) { return; }
|
||||
errs.Add(msg);
|
||||
});
|
||||
}
|
||||
|
||||
public static ErrorCatcher Create()
|
||||
=> new ErrorCatcher(ToolBox.RandomSeed(25).ToIdentifier());
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (handlerId.IsEmpty) { return; }
|
||||
MessageHandler.Deregister(handlerId);
|
||||
handlerId = Identifier.Empty;
|
||||
#if CLIENT
|
||||
DebugConsole.IsOpen = wasConsoleOpen;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static partial void ShowHelpMessage(Command command);
|
||||
|
||||
const int MaxMessages = 300;
|
||||
|
||||
public static List<ColoredText> Messages = new List<ColoredText>();
|
||||
public static readonly List<ColoredText> Messages = new List<ColoredText>();
|
||||
|
||||
public delegate void QuestionCallback(string answer);
|
||||
private static QuestionCallback activeQuestionCallback;
|
||||
@@ -2287,11 +2332,10 @@ namespace Barotrauma
|
||||
private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
|
||||
{
|
||||
if (string.IsNullOrEmpty(msg)) { return; }
|
||||
|
||||
lock (queuedMessages)
|
||||
{
|
||||
queuedMessages.Enqueue(new ColoredText(msg, color, isCommand, isError));
|
||||
}
|
||||
|
||||
var newMsg = new ColoredText(msg, color, isCommand, isError);
|
||||
queuedMessages.Enqueue(newMsg);
|
||||
MessageHandler.Invoke(newMsg);
|
||||
}
|
||||
|
||||
public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1)
|
||||
|
||||
@@ -404,7 +404,7 @@ namespace Barotrauma
|
||||
{
|
||||
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
|
||||
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
|
||||
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
|
||||
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value));
|
||||
}
|
||||
|
||||
// apply money gains afterwards to prevent them from affecting XP gains
|
||||
|
||||
@@ -594,7 +594,7 @@ namespace Barotrauma
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(
|
||||
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}",
|
||||
value: Timing.TotalTime - GameMain.GameSession.RoundStartTime);
|
||||
value: GameMain.GameSession.RoundDuration);
|
||||
}
|
||||
}, delayBetweenSpawns * i);
|
||||
i++;
|
||||
|
||||
@@ -310,5 +310,17 @@ namespace Barotrauma.Extensions
|
||||
=> source
|
||||
.OfType<Some<T>>()
|
||||
.Select(some => some.Value);
|
||||
|
||||
public static IEnumerable<TSuccess> Successes<TSuccess, TFailure>(
|
||||
this IEnumerable<Result<TSuccess, TFailure>> source)
|
||||
=> source
|
||||
.OfType<Success<TSuccess, TFailure>>()
|
||||
.Select(s => s.Value);
|
||||
|
||||
public static IEnumerable<TFailure> Failures<TSuccess, TFailure>(
|
||||
this IEnumerable<Result<TSuccess, TFailure>> source)
|
||||
=> source
|
||||
.OfType<Failure<TSuccess, TFailure>>()
|
||||
.Select(f => f.Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace Barotrauma
|
||||
if (!data.ContainsKey(identifier))
|
||||
{
|
||||
data.Add(identifier, value);
|
||||
SteamAchievementManager.OnCampaignMetadataSet(identifier, value);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Barotrauma
|
||||
|
||||
public bool DisableEvents
|
||||
{
|
||||
get { return IsFirstRound && Timing.TotalTime < GameMain.GameSession.RoundStartTime + FirstRoundEventDelay; }
|
||||
get { return IsFirstRound && GameMain.GameSession.RoundDuration > FirstRoundEventDelay; }
|
||||
}
|
||||
|
||||
public bool CheatsEnabled;
|
||||
@@ -249,7 +249,7 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < wall.SectionCount; i++)
|
||||
{
|
||||
wall.SetDamage(i, 0, createNetworkEvent: false);
|
||||
wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1094,6 +1094,7 @@ namespace Barotrauma
|
||||
if (item.Components.None(c => c is Pickable)) { continue; }
|
||||
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
|
||||
if (item.Components.Any(c => c is Wire w && w.Connections.Any(c => c != null))) { continue; }
|
||||
if (item.Container?.GetComponent<ItemContainer>() is { DrawInventory: false }) { continue; }
|
||||
itemsToTransfer.Add((item, item.Container));
|
||||
item.Submarine = null;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ namespace Barotrauma
|
||||
private Location[]? dummyLocations;
|
||||
public CrewManager? CrewManager;
|
||||
|
||||
public double RoundStartTime;
|
||||
public float RoundDuration
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public double TimeSpentCleaning, TimeSpentPainting;
|
||||
|
||||
@@ -353,6 +356,7 @@ namespace Barotrauma
|
||||
#if DEBUG
|
||||
DateTime startTime = DateTime.Now;
|
||||
#endif
|
||||
RoundDuration = 0.0f;
|
||||
AfflictionPrefab.LoadAllEffects();
|
||||
|
||||
MirrorLevel = mirrorLevel;
|
||||
@@ -503,7 +507,7 @@ namespace Barotrauma
|
||||
|
||||
RoundSummary = new RoundSummary(GameMode, Missions, StartLocation, EndLocation);
|
||||
|
||||
if (!(GameMode is TutorialMode) && !(GameMode is TestGameMode))
|
||||
if (GameMode is not TutorialMode && GameMode is not TestGameMode)
|
||||
{
|
||||
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
|
||||
if (EndLocation != null && levelData != null)
|
||||
@@ -571,9 +575,9 @@ namespace Barotrauma
|
||||
GameMode.Start();
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
int prevEntityCount = Entity.GetEntities().Count();
|
||||
int prevEntityCount = Entity.GetEntities().Count;
|
||||
mission.Start(Level.Loaded);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count() != prevEntityCount)
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Entity.GetEntities().Count != prevEntityCount)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
$"Entity count has changed after starting a mission ({mission.Prefab.Identifier}) as a client. " +
|
||||
@@ -612,7 +616,7 @@ namespace Barotrauma
|
||||
CreatureMetrics.Instance.RecentlyEncountered.Clear();
|
||||
|
||||
GameMain.GameScreen.Cam.Position = Character.Controlled?.WorldPosition ?? Submarine.MainSub.WorldPosition;
|
||||
RoundStartTime = Timing.TotalTime;
|
||||
RoundDuration = 0.0f;
|
||||
GameMain.ResetFrameTime();
|
||||
IsRunning = true;
|
||||
}
|
||||
@@ -713,6 +717,7 @@ namespace Barotrauma
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
RoundDuration += deltaTime;
|
||||
EventManager?.Update(deltaTime);
|
||||
GameMode?.Update(deltaTime);
|
||||
//backwards for loop because the missions may get completed and removed from the list in Update()
|
||||
@@ -865,17 +870,16 @@ namespace Barotrauma
|
||||
#else
|
||||
bool success = GameMain.Server.ConnectedClients.Any(c => c.InGame && c.Character != null && !c.Character.IsDead);
|
||||
#endif
|
||||
double roundDuration = Timing.TotalTime - RoundStartTime;
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
success ? GameAnalyticsManager.ProgressionStatus.Complete : GameAnalyticsManager.ProgressionStatus.Fail,
|
||||
GameMode?.Preset.Identifier.Value ?? "none",
|
||||
roundDuration);
|
||||
RoundDuration);
|
||||
string eventId = "EndRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
|
||||
LogEndRoundStats(eventId);
|
||||
if (GameMode is CampaignMode campaignMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MoneyEarned", GetAmountOfMoney(crewCharacters) - prevMoney);
|
||||
campaignMode.TotalPlayTime += roundDuration;
|
||||
campaignMode.TotalPlayTime += RoundDuration;
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnRoundEnded();
|
||||
@@ -901,21 +905,20 @@ namespace Barotrauma
|
||||
|
||||
public void LogEndRoundStats(string eventId)
|
||||
{
|
||||
double roundDuration = Timing.TotalTime - RoundStartTime;
|
||||
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 + "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);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier + ":" + (mission.Completed ? "Completed" : "Failed"), RoundDuration);
|
||||
}
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
||||
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
||||
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), roundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none" + ":" + levelId), RoundDuration);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"), RoundDuration);
|
||||
}
|
||||
|
||||
if (Submarine.MainSub != null)
|
||||
|
||||
@@ -315,6 +315,7 @@ namespace Barotrauma.Items.Components
|
||||
if (f2.Body.UserData is Limb targetLimb)
|
||||
{
|
||||
if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
|
||||
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
|
||||
var targetCharacter = targetLimb.character;
|
||||
if (targetCharacter == picker) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
@@ -330,6 +331,7 @@ namespace Barotrauma.Items.Components
|
||||
else if (f2.Body.UserData is Character targetCharacter)
|
||||
{
|
||||
if (targetCharacter == picker || targetCharacter == User) { return false; }
|
||||
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
|
||||
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
|
||||
@@ -816,7 +816,14 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
hasRequiredItems = itemList.Any(Predicate);
|
||||
if (itemList.Any(Predicate))
|
||||
{
|
||||
hasRequiredItems = !relatedItem.RequireEmpty;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasRequiredItems = relatedItem.MatchOnEmpty || relatedItem.RequireEmpty;
|
||||
}
|
||||
if (!hasRequiredItems)
|
||||
{
|
||||
shouldBreak = true;
|
||||
|
||||
@@ -107,7 +107,7 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
|
||||
public int ItemsPerRow { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the contents in the item's inventory be visible? Disabled on items like magazines that spawn the contents as needed.")]
|
||||
public bool DrawInventory
|
||||
{
|
||||
get;
|
||||
@@ -135,9 +135,6 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No)]
|
||||
public bool AllowAccess { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool AccessOnlyWhenBroken { get; set; }
|
||||
|
||||
@@ -530,12 +527,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
||||
{
|
||||
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
return DrawInventory && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
|
||||
}
|
||||
|
||||
public override bool Select(Character character)
|
||||
{
|
||||
if (!AllowAccess) { return false; }
|
||||
if (!DrawInventory) { return false; }
|
||||
if (item.Container != null) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
@@ -571,7 +568,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
{
|
||||
if (!AllowAccess) { return false; }
|
||||
if (!DrawInventory) { return false; }
|
||||
if (AccessOnlyWhenBroken)
|
||||
{
|
||||
if (item.Condition > 0)
|
||||
|
||||
@@ -539,7 +539,7 @@ namespace Barotrauma.Items.Components
|
||||
var prevUser = user;
|
||||
CancelFabricating();
|
||||
|
||||
amountRemaining--;
|
||||
amountRemaining--;
|
||||
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
|
||||
{
|
||||
//keep fabricating if we can fabricate more
|
||||
@@ -745,7 +745,7 @@ namespace Barotrauma.Items.Components
|
||||
itemList.AddRange(container.Inventory.AllItems);
|
||||
}
|
||||
}
|
||||
if (user?.Inventory != null)
|
||||
if (user?.Inventory != null && user.SelectedItem == item)
|
||||
{
|
||||
itemList.AddRange(user.Inventory.AllItems);
|
||||
linkedInventories.Add(user.Inventory);
|
||||
|
||||
@@ -65,14 +65,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
get
|
||||
{
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
return (float)(Timing.TotalTime - GameMain.GameSession.RoundStartTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return GameMain.GameSession?.RoundDuration ?? 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -639,7 +639,13 @@ namespace Barotrauma.Items.Components
|
||||
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null && projectileContainer.Item != item)
|
||||
{
|
||||
projectileContainer?.Item.Use(deltaTime, null);
|
||||
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)
|
||||
{
|
||||
rootContainer.Use(deltaTime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -649,7 +655,7 @@ namespace Barotrauma.Items.Components
|
||||
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
|
||||
//use linked projectile containers in case they have to react to the turret being launched somehow
|
||||
//(play a sound, spawn more projectiles)
|
||||
if (!(e is Item linkedItem)) { continue; }
|
||||
if (e is not Item linkedItem) { continue; }
|
||||
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
|
||||
if (linkedItem.Condition <= 0.0f)
|
||||
{
|
||||
@@ -692,7 +698,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
foreach (MapEntity e in item.linkedTo)
|
||||
{
|
||||
if (!(e is Item linkedItem)) { continue; }
|
||||
if (e is not Item linkedItem) { continue; }
|
||||
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
|
||||
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
|
||||
{
|
||||
@@ -872,7 +878,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void LaunchProjSpecific();
|
||||
|
||||
private void ShiftItemsInProjectileContainer(ItemContainer container)
|
||||
private static void ShiftItemsInProjectileContainer(ItemContainer container)
|
||||
{
|
||||
if (container == null) { return; }
|
||||
bool moved;
|
||||
@@ -1063,8 +1069,8 @@ namespace Barotrauma.Items.Components
|
||||
character.AIController.SelectTarget(null);
|
||||
}
|
||||
|
||||
bool canShoot = true;
|
||||
if (!HasPowerToShoot())
|
||||
bool canShoot = HasPowerToShoot();
|
||||
if (!canShoot)
|
||||
{
|
||||
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
|
||||
float lowestCharge = 0.0f;
|
||||
@@ -1089,7 +1095,6 @@ namespace Barotrauma.Items.Components
|
||||
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
|
||||
identifier: "supercapacitorisbroken".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 30.0f);
|
||||
canShoot = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1104,7 +1109,6 @@ namespace Barotrauma.Items.Components
|
||||
character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
|
||||
identifier: "turrethasnopower".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 30.0f);
|
||||
canShoot = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,7 +1287,7 @@ namespace Barotrauma.Items.Components
|
||||
closestDistance = shootDistance;
|
||||
foreach (var wall in Level.Loaded.ExtraWalls)
|
||||
{
|
||||
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; }
|
||||
if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
|
||||
foreach (var cell in wall.Cells)
|
||||
{
|
||||
if (cell.DoesDamage)
|
||||
@@ -1405,19 +1409,18 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
|
||||
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
|
||||
Body worldTarget = CheckLineOfSight(start, end);
|
||||
bool shoot;
|
||||
if (closestEnemy != null && closestEnemy.Submarine != null)
|
||||
{
|
||||
start -= closestEnemy.Submarine.SimPosition;
|
||||
end -= closestEnemy.Submarine.SimPosition;
|
||||
Body transformedTarget = CheckLineOfSight(start, end);
|
||||
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
|
||||
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
|
||||
}
|
||||
else
|
||||
{
|
||||
shoot = CanShoot(worldTarget, character);
|
||||
canShoot = CanShoot(worldTarget, character);
|
||||
}
|
||||
if (!shoot) { return false; }
|
||||
if (!canShoot) { return false; }
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFireTurret").Value,
|
||||
@@ -1471,6 +1474,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (targetBody.UserData is ISpatialEntity e)
|
||||
{
|
||||
if (e is Structure s && s.Indestructible) { return false; }
|
||||
Submarine sub = e.Submarine ?? e as Submarine;
|
||||
if (!targetSubmarines && e is Submarine) { return false; }
|
||||
if (sub == null) { return false; }
|
||||
@@ -1559,7 +1563,7 @@ namespace Barotrauma.Items.Components
|
||||
return projectiles;
|
||||
}
|
||||
|
||||
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
|
||||
private static void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
|
||||
{
|
||||
stopSearching = false;
|
||||
if (projectileContainer.Condition <= 0.0f) { return; }
|
||||
|
||||
@@ -866,6 +866,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Action<Character> OnDeselect;
|
||||
|
||||
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
|
||||
: this(new Rectangle(
|
||||
(int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale),
|
||||
@@ -2155,12 +2157,15 @@ namespace Barotrauma
|
||||
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
|
||||
}
|
||||
|
||||
contact.GetWorldManifold(out Vector2 normal, out _);
|
||||
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
|
||||
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
||||
if (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > 1.0f)
|
||||
{
|
||||
contact.GetWorldManifold(out Vector2 normal, out _);
|
||||
if (contact.FixtureA.Body == f1.Body) { normal = -normal; }
|
||||
float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal);
|
||||
impactQueue ??= new ConcurrentQueue<float>();
|
||||
impactQueue.Enqueue(impact);
|
||||
}
|
||||
|
||||
impactQueue ??= new ConcurrentQueue<float>();
|
||||
impactQueue.Enqueue(impact);
|
||||
isActive = true;
|
||||
|
||||
return true;
|
||||
@@ -2774,6 +2779,7 @@ namespace Barotrauma
|
||||
if (GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(conditionalActionType, ic, character, targetLimb));
|
||||
GameMain.NetworkMember.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnUse, ic, character, targetLimb));
|
||||
}
|
||||
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
|
||||
@@ -1156,7 +1156,7 @@ namespace Barotrauma
|
||||
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
|
||||
{
|
||||
priceInfo = GetPriceInfo(store);
|
||||
return priceInfo is { CanBeBought: true } && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
|
||||
return priceInfo is { CanBeBought: true } && (store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty;
|
||||
}
|
||||
|
||||
public bool CanBeBoughtFrom(Location location)
|
||||
@@ -1167,7 +1167,7 @@ namespace Barotrauma
|
||||
var priceInfo = GetPriceInfo(store.Value);
|
||||
if (priceInfo == null) { continue; }
|
||||
if (!priceInfo.CanBeBought) { continue; }
|
||||
if ((location.LevelData?.Difficulty ?? 0) < priceInfo.MinLevelDifficulty) { continue; }
|
||||
if (location.LevelData.Difficulty < priceInfo.MinLevelDifficulty) { continue; }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -27,13 +27,14 @@ namespace Barotrauma
|
||||
private readonly bool applyFireEffects;
|
||||
private readonly string[] ignoreFireEffectsForTags;
|
||||
private readonly bool ignoreCover;
|
||||
private readonly bool onlyInside,onlyOutside;
|
||||
private readonly float flashDuration;
|
||||
private readonly float? flashRange;
|
||||
private readonly string decal;
|
||||
private readonly float decalSize;
|
||||
private readonly bool applyToSelf;
|
||||
|
||||
public bool OnlyInside, OnlyOutside;
|
||||
|
||||
private readonly float itemRepairStrength;
|
||||
|
||||
public readonly HashSet<Submarine> IgnoredSubmarines = new HashSet<Submarine>();
|
||||
@@ -81,8 +82,8 @@ namespace Barotrauma
|
||||
ignoreFireEffectsForTags = element.GetAttributeStringArray("ignorefireeffectsfortags", Array.Empty<string>(), convertToLowerInvariant: true);
|
||||
|
||||
ignoreCover = element.GetAttributeBool("ignorecover", false);
|
||||
onlyInside = element.GetAttributeBool("onlyinside", false);
|
||||
onlyOutside = element.GetAttributeBool("onlyoutside", false);
|
||||
OnlyInside = element.GetAttributeBool("onlyinside", false);
|
||||
OnlyOutside = element.GetAttributeBool("onlyoutside", false);
|
||||
|
||||
flash = element.GetAttributeBool("flash", showEffects);
|
||||
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
|
||||
@@ -176,13 +177,12 @@ namespace Barotrauma
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
float distSqr = Vector2.DistanceSquared(item.WorldPosition, worldPosition);
|
||||
if (distSqr > displayRangeSqr) continue;
|
||||
|
||||
float distFactor = 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
|
||||
if (distSqr > displayRangeSqr) { continue; }
|
||||
float distFactor = CalculateDistanceFactor(distSqr, displayRange);
|
||||
|
||||
//damage repairable power-consuming items
|
||||
var powered = item.GetComponent<Powered>();
|
||||
if (powered == null || !powered.VulnerableToEMP) continue;
|
||||
if (powered == null || !powered.VulnerableToEMP) { continue; }
|
||||
if (item.Repairables.Any())
|
||||
{
|
||||
item.Condition -= item.MaxCondition * EmpStrength * distFactor;
|
||||
@@ -195,6 +195,7 @@ namespace Barotrauma
|
||||
powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor;
|
||||
}
|
||||
}
|
||||
static float CalculateDistanceFactor(float distSqr, float displayRange) => 1.0f - (float)Math.Sqrt(distSqr) / displayRange;
|
||||
}
|
||||
|
||||
if (itemRepairStrength > 0.0f)
|
||||
@@ -288,10 +289,16 @@ namespace Barotrauma
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (c == attacker && !applyToSelf) { continue; }
|
||||
//if (c == attacker && !applyToSelf) { continue; }
|
||||
|
||||
if (onlyInside && c.Submarine == null) { continue; }
|
||||
else if (onlyOutside && c.Submarine != null) { continue; }
|
||||
if (OnlyInside && c.Submarine == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (OnlyOutside && c.Submarine != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 explosionPos = worldPosition;
|
||||
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
|
||||
@@ -337,15 +344,19 @@ namespace Barotrauma
|
||||
modifiedAfflictions.Clear();
|
||||
foreach (Affliction affliction in attack.Afflictions.Keys)
|
||||
{
|
||||
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
|
||||
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
|
||||
float limbCountFactor = Math.Min(distFactors.Count, 15);
|
||||
float dmgMultiplier = distFactor;
|
||||
if (affliction.DivideByLimbCount)
|
||||
{
|
||||
float limbCountFactor = distFactors.Count;
|
||||
if (affliction.Prefab.LimbSpecific && affliction.Prefab.AfflictionType == "damage")
|
||||
{
|
||||
// Shouldn't go above 15, or the damage can be unexpectedly low -> doesn't break armor
|
||||
// Effectively this makes large explosions more effective against large creatures (because more limbs are affected), but I don't think that's necessarily a bad thing.
|
||||
limbCountFactor = Math.Min(distFactors.Count, 15);
|
||||
}
|
||||
dmgMultiplier /= limbCountFactor;
|
||||
}
|
||||
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction.Probability));
|
||||
modifiedAfflictions.Add(affliction.CreateMultiplied(dmgMultiplier, affliction));
|
||||
}
|
||||
c.LastDamageSource = damageSource;
|
||||
if (attacker == null)
|
||||
@@ -353,29 +364,29 @@ namespace Barotrauma
|
||||
if (damageSource is Item item)
|
||||
{
|
||||
attacker = item.GetComponent<Projectile>()?.User;
|
||||
if (attacker == null)
|
||||
{
|
||||
attacker = item.GetComponent<MeleeWeapon>()?.User;
|
||||
}
|
||||
attacker ??= item.GetComponent<MeleeWeapon>()?.User;
|
||||
}
|
||||
}
|
||||
|
||||
if (attack.Afflictions.Any() || attack.Stun > 0.0f)
|
||||
{
|
||||
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
|
||||
if (attackData.Afflictions != null)
|
||||
if (!attack.OnlyHumans || c.IsHuman)
|
||||
{
|
||||
modifiedAfflictions.AddRange(attackData.Afflictions);
|
||||
}
|
||||
AbilityAttackData attackData = new AbilityAttackData(Attack, c, attacker);
|
||||
if (attackData.Afflictions != null)
|
||||
{
|
||||
modifiedAfflictions.AddRange(attackData.Afflictions);
|
||||
}
|
||||
|
||||
//use a position slightly from the limb's position towards the explosion
|
||||
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
|
||||
Vector2 dir = worldPosition - limb.WorldPosition;
|
||||
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
|
||||
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
|
||||
damages.Add(limb, attackResult.Damage);
|
||||
//use a position slightly from the limb's position towards the explosion
|
||||
//ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods
|
||||
Vector2 dir = worldPosition - limb.WorldPosition;
|
||||
Vector2 hitPos = limb.WorldPosition + (dir.LengthSquared() <= 0.001f ? Rand.Vector(1.0f) : Vector2.Normalize(dir)) * 0.01f;
|
||||
AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker, damageMultiplier: attack.DamageMultiplier * attackData.DamageMultiplier);
|
||||
damages.Add(limb, attackResult.Damage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (attack.StatusEffects != null && attack.StatusEffects.Any())
|
||||
{
|
||||
attack.SetUser(attacker);
|
||||
@@ -438,7 +449,7 @@ namespace Barotrauma
|
||||
damagedStructureList.Clear();
|
||||
foreach (MapEntity entity in MapEntity.mapEntityList)
|
||||
{
|
||||
if (!(entity is Structure structure)) { continue; }
|
||||
if (entity is not Structure structure) { continue; }
|
||||
if (ignoredSubmarines != null && entity.Submarine != null && ignoredSubmarines.Contains(entity.Submarine)) { continue; }
|
||||
|
||||
if (structure.HasBody &&
|
||||
@@ -487,7 +498,7 @@ namespace Barotrauma
|
||||
|
||||
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!(Level.Loaded.ExtraWalls[i] is DestructibleLevelWall destructibleWall)) { continue; }
|
||||
if (Level.Loaded.ExtraWalls[i] is not DestructibleLevelWall destructibleWall) { continue; }
|
||||
foreach (var cell in destructibleWall.Cells)
|
||||
{
|
||||
if (cell.IsPointInside(worldPosition))
|
||||
@@ -510,7 +521,7 @@ namespace Barotrauma
|
||||
return damagedStructures;
|
||||
}
|
||||
|
||||
public void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
|
||||
public static void RangedBallastFloraDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null)
|
||||
{
|
||||
List<BallastFloraBehavior> ballastFlorae = new List<BallastFloraBehavior>();
|
||||
|
||||
|
||||
@@ -549,21 +549,24 @@ namespace Barotrauma
|
||||
if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress &&
|
||||
hull1.Surface < rect.Y)
|
||||
{
|
||||
//create a wave from the side of the hull the water is leaking from
|
||||
if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f)
|
||||
{
|
||||
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 6.0f;
|
||||
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
|
||||
|
||||
hull1.WaveVel[hull1.WaveY.Length - 1] += vel * deltaTime;
|
||||
hull1.WaveVel[hull1.WaveY.Length - 2] += vel * deltaTime;
|
||||
CreateWave(rect, hull1, hull1.WaveY.Length - 1, hull1.WaveY.Length - 2, flowForce, deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
float vel = ((rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[0])) * 6.0f;
|
||||
CreateWave(rect, hull1, 0, 1, flowForce, deltaTime);
|
||||
}
|
||||
static void CreateWave(Rectangle rect, Hull hull1, int index1, int index2, Vector2 flowForce, float deltaTime)
|
||||
{
|
||||
float vel = (rect.Y - rect.Height / 2) - (hull1.Surface + hull1.WaveY[index1]);
|
||||
vel *= Math.Min(Math.Abs(flowForce.X) / 200.0f, 1.0f);
|
||||
|
||||
hull1.WaveVel[0] += vel * deltaTime;
|
||||
hull1.WaveVel[1] += vel * deltaTime;
|
||||
if (vel > 0.0f)
|
||||
{
|
||||
hull1.WaveVel[index1] += vel * deltaTime;
|
||||
hull1.WaveVel[index2] += vel * deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -405,7 +405,7 @@ namespace Barotrauma
|
||||
if (wall.Submarine != sub) { continue; }
|
||||
for (int i = 0; i < wall.SectionCount; i++)
|
||||
{
|
||||
wall.SetDamage(i, 0, createNetworkEvent: false);
|
||||
wall.SetDamage(i, 0, createNetworkEvent: false, createExplosionEffect: false);
|
||||
}
|
||||
}
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
|
||||
@@ -318,7 +318,7 @@ namespace Barotrauma
|
||||
var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
if (characters.Any())
|
||||
{
|
||||
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier));
|
||||
price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier, includeSaved: false));
|
||||
price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag)));
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ namespace Barotrauma
|
||||
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
|
||||
}
|
||||
|
||||
public void ChangeType(LocationType newType)
|
||||
public void ChangeType(LocationType newType, bool createStores = true)
|
||||
{
|
||||
if (newType == Type) { return; }
|
||||
|
||||
@@ -683,7 +683,10 @@ namespace Barotrauma
|
||||
UnlockMissionByTag(Type.MissionTags.GetRandomUnsynced());
|
||||
}
|
||||
|
||||
CreateStores(force: true);
|
||||
if (createStores)
|
||||
{
|
||||
CreateStores(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void UnlockInitialMissions()
|
||||
|
||||
@@ -552,7 +552,8 @@ namespace Barotrauma
|
||||
Connections[i].Locations[1];
|
||||
if (!leftMostLocation.Type.HasOutpost || leftMostLocation.Type.Identifier == "abandoned")
|
||||
{
|
||||
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"));
|
||||
leftMostLocation.ChangeType(LocationType.Prefabs.OrderBy(lt => lt.Identifier).First(lt => lt.HasOutpost && lt.Identifier != "abandoned"),
|
||||
createStores: false);
|
||||
}
|
||||
leftMostLocation.IsGateBetweenBiomes = true;
|
||||
Connections[i].Locked = true;
|
||||
@@ -628,6 +629,7 @@ namespace Barotrauma
|
||||
foreach (Location location in Locations)
|
||||
{
|
||||
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
|
||||
location.CreateStores(force: true);
|
||||
}
|
||||
foreach (LocationConnection connection in Connections)
|
||||
{
|
||||
@@ -734,7 +736,7 @@ namespace Barotrauma
|
||||
|
||||
if (LocationType.Prefabs.TryGet("none", out LocationType locationType))
|
||||
{
|
||||
previousToEndLocation.ChangeType(locationType);
|
||||
previousToEndLocation.ChangeType(locationType, createStores: false);
|
||||
}
|
||||
|
||||
//remove all locations from the end biome except the end location
|
||||
|
||||
@@ -56,6 +56,8 @@ namespace Barotrauma
|
||||
//dimensions of the wall sections' physics bodies (only used for debug rendering)
|
||||
private readonly List<Vector2> bodyDebugDimensions = new List<Vector2>();
|
||||
|
||||
private static Explosion explosionOnBroken;
|
||||
|
||||
#if DEBUG
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
#else
|
||||
@@ -1083,7 +1085,7 @@ namespace Barotrauma
|
||||
return new AttackResult(damageAmount, null);
|
||||
}
|
||||
|
||||
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
|
||||
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true, bool createExplosionEffect = true)
|
||||
{
|
||||
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
|
||||
if (!Prefab.Body) { return; }
|
||||
@@ -1128,6 +1130,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
float prevGapOpenState = Sections[sectionIndex].gap?.Open ?? 0.0f;
|
||||
if (Sections[sectionIndex].gap == null)
|
||||
{
|
||||
Rectangle gapRect = Sections[sectionIndex].rect;
|
||||
@@ -1204,8 +1207,15 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
var gap = Sections[sectionIndex].gap;
|
||||
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
|
||||
Sections[sectionIndex].gap.Open = gapOpen;
|
||||
gap.Open = gapOpen;
|
||||
|
||||
//gap appeared or became much larger -> explosion effect
|
||||
if (gapOpen - prevGapOpenState > 0.25f && createExplosionEffect && !gap.IsRoomToRoom)
|
||||
{
|
||||
CreateWallDamageExplosion(gap, attacker);
|
||||
}
|
||||
}
|
||||
|
||||
float damageDiff = damage - Sections[sectionIndex].damage;
|
||||
@@ -1234,6 +1244,59 @@ namespace Barotrauma
|
||||
UpdateSections();
|
||||
}
|
||||
|
||||
private void CreateWallDamageExplosion(Gap gap, Character attacker)
|
||||
{
|
||||
const float explosionRange = 750.0f;
|
||||
float explosionStrength = gap.Open;
|
||||
|
||||
var linkedHull = gap.linkedTo.FirstOrDefault() as Hull;
|
||||
if (linkedHull != null)
|
||||
{
|
||||
//existing, nearby gaps leading to the same hull reduce the strength of the explosion
|
||||
// -> the first breached section does most (or all) of the damage, making it more consistent
|
||||
// (otherwise the damage would depend on how many structures and sections happen to be breached)
|
||||
foreach (var otherGap in linkedHull.ConnectedGaps)
|
||||
{
|
||||
if (otherGap == gap || otherGap.IsRoomToRoom || otherGap.Open < 0.25f) { continue; }
|
||||
explosionStrength -= Math.Max(0, explosionRange - Vector2.Distance(otherGap.WorldPosition, gap.WorldPosition)) / explosionRange;
|
||||
if (explosionStrength <= 0.0f) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
if (explosionOnBroken == null)
|
||||
{
|
||||
explosionOnBroken = new Explosion(explosionRange * gap.Open, force: 10.0f, damage: 0.0f, structureDamage: 0.0f, itemDamage: 0.0f);
|
||||
if (AfflictionPrefab.Prefabs.TryGet("lacerations".ToIdentifier(), out AfflictionPrefab lacerations))
|
||||
{
|
||||
explosionOnBroken.Attack.Afflictions.Add(lacerations.Instantiate(50.0f), null);
|
||||
}
|
||||
else
|
||||
{
|
||||
explosionOnBroken.Attack.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(5.0f), null);
|
||||
}
|
||||
explosionOnBroken.OnlyInside = true;
|
||||
explosionOnBroken.DisableParticles();
|
||||
}
|
||||
|
||||
explosionOnBroken.Attack.DamageMultiplier = explosionStrength;
|
||||
explosionOnBroken?.Explode(gap.WorldPosition, damageSource: null, attacker: attacker);
|
||||
#if CLIENT
|
||||
if (linkedHull != null)
|
||||
{
|
||||
for (int i = 0; i <= 50; i++)
|
||||
{
|
||||
Vector2 particlePos = new Vector2(Rand.Range(gap.WorldRect.X, gap.WorldRect.Right), Rand.Range(gap.WorldRect.Y - gap.WorldRect.Height, gap.WorldRect.Y));
|
||||
var velocity = gap.IsHorizontal ?
|
||||
gap.linkedTo[0].WorldPosition.X < gap.WorldPosition.X ? -Vector2.UnitX : Vector2.UnitX :
|
||||
gap.linkedTo[0].WorldPosition.Y < gap.WorldPosition.Y ? -Vector2.UnitY : Vector2.UnitY;
|
||||
velocity = new Vector2(velocity.X + Rand.Range(-0.2f, 0.2f), velocity.Y + Rand.Range(-0.2f, 0.2f));
|
||||
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, velocity * Rand.Range(100.0f, 3000.0f), collisionIgnoreTimer: 0.1f);
|
||||
if (particle == null) { break; }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
partial void OnHealthChangedProjSpecific(Character attacker, float damageAmount);
|
||||
|
||||
public void SetCollisionCategory(Category collisionCategory)
|
||||
@@ -1570,7 +1633,7 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < Sections.Length; i++)
|
||||
{
|
||||
SetDamage(i, Sections[i].damage, createNetworkEvent: false);
|
||||
SetDamage(i, Sections[i].damage, createNetworkEvent: false, createExplosionEffect: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -720,7 +720,7 @@ namespace Barotrauma
|
||||
|
||||
private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
|
||||
{
|
||||
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 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)
|
||||
|
||||
@@ -38,15 +38,15 @@ namespace Barotrauma.Networking
|
||||
READY_CHECK,
|
||||
READY_TO_SPAWN
|
||||
}
|
||||
enum ClientNetObject
|
||||
|
||||
enum ClientNetSegment
|
||||
{
|
||||
END_OF_MESSAGE, //self-explanatory
|
||||
SYNC_IDS, //ids of the last changes the client knows about
|
||||
CHAT_MESSAGE, //also self-explanatory
|
||||
VOTE, //you get the idea
|
||||
CHARACTER_INPUT,
|
||||
ENTITY_STATE,
|
||||
SPECTATING_POS
|
||||
SyncIds, //ids of the last changes the client knows about
|
||||
ChatMessage, //also self-explanatory
|
||||
Vote, //you get the idea
|
||||
CharacterInput,
|
||||
EntityState,
|
||||
SpectatingPos
|
||||
}
|
||||
|
||||
enum ClientNetError
|
||||
@@ -88,16 +88,15 @@ namespace Barotrauma.Networking
|
||||
MONEY,
|
||||
READY_CHECK //start, end and update a ready check
|
||||
}
|
||||
enum ServerNetObject
|
||||
enum ServerNetSegment
|
||||
{
|
||||
END_OF_MESSAGE,
|
||||
SYNC_IDS,
|
||||
CHAT_MESSAGE,
|
||||
VOTE,
|
||||
CLIENT_LIST,
|
||||
ENTITY_POSITION,
|
||||
ENTITY_EVENT,
|
||||
ENTITY_EVENT_INITIAL
|
||||
SyncIds,
|
||||
ChatMessage,
|
||||
Vote,
|
||||
ClientList,
|
||||
EntityPosition,
|
||||
EntityEvent,
|
||||
EntityEventInitial
|
||||
}
|
||||
|
||||
enum TraitorMessageType
|
||||
|
||||
@@ -272,6 +272,9 @@ namespace Barotrauma.Networking
|
||||
[NetworkSerialize]
|
||||
public bool IsMandatory;
|
||||
|
||||
[NetworkSerialize]
|
||||
public bool IsVanilla;
|
||||
|
||||
private Md5Hash? cachedHash;
|
||||
private DateTime? cachedDateTime;
|
||||
|
||||
@@ -305,6 +308,7 @@ namespace Barotrauma.Networking
|
||||
? ugcId.StringRepresentation
|
||||
: "";
|
||||
IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
|
||||
IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage;
|
||||
InstallTimeDiffInSeconds =
|
||||
contentPackage.InstallTime.TryUnwrap(out var installTime)
|
||||
? (uint)(installTime - referenceTime).TotalSeconds
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public State CurrentState { get; private set; }
|
||||
|
||||
public bool UseRespawnPrompt
|
||||
public static bool UseRespawnPrompt
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -191,6 +191,7 @@ namespace Barotrauma.Networking
|
||||
public void ForceRespawn()
|
||||
{
|
||||
ResetShuttle();
|
||||
RespawnCountdownStarted = true;
|
||||
RespawnTime = DateTime.Now;
|
||||
CurrentState = State.Waiting;
|
||||
}
|
||||
|
||||
@@ -246,11 +246,13 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier SkillIdentifier;
|
||||
public readonly float Amount;
|
||||
public readonly bool TriggerTalents;
|
||||
|
||||
public GiveSkill(XElement element, string parentDebugName)
|
||||
{
|
||||
SkillIdentifier = element.GetAttributeIdentifier("skillidentifier", Identifier.Empty);
|
||||
Amount = element.GetAttributeFloat("amount", 0);
|
||||
SkillIdentifier = element.GetAttributeIdentifier(nameof(SkillIdentifier), Identifier.Empty);
|
||||
Amount = element.GetAttributeFloat(nameof(Amount), 0);
|
||||
TriggerTalents = element.GetAttributeBool(nameof(TriggerTalents), true);
|
||||
|
||||
if (SkillIdentifier == Identifier.Empty)
|
||||
{
|
||||
@@ -424,7 +426,7 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private readonly bool multiplyAfflictionsByMaxVitality;
|
||||
private readonly bool? multiplyAfflictionsByMaxVitality;
|
||||
|
||||
public IEnumerable<CharacterSpawnInfo> SpawnCharacters
|
||||
{
|
||||
@@ -490,7 +492,11 @@ namespace Barotrauma
|
||||
talentTriggers = new List<Identifier>();
|
||||
giveExperiences = new List<int>();
|
||||
giveSkills = new List<GiveSkill>();
|
||||
multiplyAfflictionsByMaxVitality = element.GetAttributeBool("multiplyafflictionsbymaxvitality", false);
|
||||
var multiplyAfflictionsElement = element.GetAttribute(nameof(multiplyAfflictionsByMaxVitality));
|
||||
if (multiplyAfflictionsElement != null)
|
||||
{
|
||||
multiplyAfflictionsByMaxVitality = multiplyAfflictionsElement.GetAttributeBool(false);
|
||||
}
|
||||
|
||||
tags = new HashSet<string>(element.GetAttributeString("tags", "").Split(','));
|
||||
OnlyInside = element.GetAttributeBool("onlyinside", false);
|
||||
@@ -1294,7 +1300,7 @@ namespace Barotrauma
|
||||
}
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
if (!(targets[i] is Item item)) { continue; }
|
||||
if (targets[i] is not Item item) { continue; }
|
||||
for (int j = 0; j < useItemCount; j++)
|
||||
{
|
||||
if (item.Removed) { continue; }
|
||||
@@ -1327,6 +1333,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targets[i] is Character character && character.Inventory != null)
|
||||
{
|
||||
foreach (var containedItem in character.Inventory.AllItemsMod)
|
||||
{
|
||||
containedItem.Drop(dropper: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removeItem)
|
||||
@@ -1566,9 +1579,7 @@ namespace Barotrauma
|
||||
if (targetCharacter != null && !targetCharacter.Removed)
|
||||
{
|
||||
Identifier skillIdentifier = giveSkill.SkillIdentifier == "randomskill" ? GetRandomSkill() : giveSkill.SkillIdentifier;
|
||||
|
||||
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount);
|
||||
|
||||
targetCharacter.Info?.IncreaseSkillLevel(skillIdentifier, giveSkill.Amount, !giveSkill.TriggerTalents);
|
||||
Identifier GetRandomSkill()
|
||||
{
|
||||
return targetCharacter.Info?.Job?.GetSkills().GetRandomUnsynced()?.Identifier ?? Identifier.Empty;
|
||||
@@ -2140,10 +2151,10 @@ namespace Barotrauma
|
||||
return multiplier * AfflictionMultiplier;
|
||||
}
|
||||
|
||||
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool modifyByMaxVitality)
|
||||
private Affliction GetMultipliedAffliction(Affliction affliction, Entity entity, Character targetCharacter, float deltaTime, bool? multiplyByMaxVitality)
|
||||
{
|
||||
float afflictionMultiplier = GetAfflictionMultiplier(entity, targetCharacter, deltaTime);
|
||||
if (modifyByMaxVitality)
|
||||
if (multiplyByMaxVitality ?? affliction.MultiplyByMaxVitality)
|
||||
{
|
||||
afflictionMultiplier *= targetCharacter.MaxVitality / 100f;
|
||||
}
|
||||
@@ -2162,7 +2173,7 @@ namespace Barotrauma
|
||||
|
||||
if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f))
|
||||
{
|
||||
return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability);
|
||||
return affliction.CreateMultiplied(afflictionMultiplier, affliction);
|
||||
}
|
||||
return affliction;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Barotrauma.Steam
|
||||
{
|
||||
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
|
||||
{
|
||||
return new PublishedFileId[0];
|
||||
return Array.Empty<PublishedFileId>();
|
||||
}
|
||||
return Steamworks.SteamUGC.GetSubscribedItems();
|
||||
}
|
||||
|
||||
@@ -61,6 +61,12 @@ namespace Barotrauma.Steam
|
||||
if (set.Count == prevSize) { break; }
|
||||
prevSize = set.Count;
|
||||
}
|
||||
|
||||
// Remove items that do not have the correct consumer app ID,
|
||||
// which can happen on items that are not visible to the currently
|
||||
// logged in player (i.e. private & friends-only items)
|
||||
set.RemoveWhere(it => it.ConsumerApp != AppID);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
@@ -276,6 +282,62 @@ namespace Barotrauma.Steam
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ISet<ulong> GetInstalledItems()
|
||||
=> ContentPackageManager.WorkshopPackages
|
||||
.Select(p => p.UgcId)
|
||||
.NotNone()
|
||||
.OfType<SteamWorkshopId>()
|
||||
.Select(id => id.Value)
|
||||
.ToHashSet();
|
||||
|
||||
public static async Task<ISet<Steamworks.Ugc.Item>> GetPublishedAndSubscribedItems()
|
||||
{
|
||||
var allItems = (await GetAllSubscribedItems()).ToHashSet();
|
||||
allItems.UnionWith(await GetPublishedItems());
|
||||
|
||||
// This is a hack that eliminates subscribed mods that have been
|
||||
// made private. Players cannot download updates for these, so
|
||||
// we treat them as if they were deleted.
|
||||
allItems = (await Task.WhenAll(allItems.Select(it => GetItem(it.Id.Value))))
|
||||
.NotNull()
|
||||
.Where(it => it.ConsumerApp == AppID)
|
||||
.ToHashSet();
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
public static void DeleteUnsubscribedMods(Action<ContentPackage[]>? callback = null)
|
||||
{
|
||||
#if SERVER
|
||||
// Servers do not run this because they can't subscribe to anything
|
||||
return;
|
||||
#endif
|
||||
//If Steamworks isn't initialized then we can't know what the user has unsubscribed from
|
||||
if (!IsInitialized) { return; }
|
||||
if (!Steamworks.SteamClient.IsValid) { return; }
|
||||
if (!Steamworks.SteamClient.IsLoggedOn) { return; }
|
||||
|
||||
TaskPool.Add("DeleteUnsubscribedMods", GetPublishedAndSubscribedItems().WaitForLoadingScreen(), t =>
|
||||
{
|
||||
if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> items)) { return; }
|
||||
var ids = items.Select(it => it.Id.Value).ToHashSet();
|
||||
var toUninstall = ContentPackageManager.WorkshopPackages
|
||||
.Where(pkg
|
||||
=> !pkg.UgcId.TryUnwrap(out SteamWorkshopId workshopId)
|
||||
|| !ids.Contains(workshopId.Value))
|
||||
.ToArray();
|
||||
if (toUninstall.Any())
|
||||
{
|
||||
foreach (var pkg in toUninstall)
|
||||
{
|
||||
Directory.TryDelete(pkg.Dir, recursive: true);
|
||||
}
|
||||
ContentPackageManager.UpdateContentPackageList();
|
||||
}
|
||||
callback?.Invoke(toUninstall);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsInstallingToPath(string path)
|
||||
=> File.Exists(Path.Combine(Path.GetDirectoryName(path)!, ContentPackageManager.CopyIndicatorFileName));
|
||||
@@ -457,7 +519,7 @@ namespace Barotrauma.Steam
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError(
|
||||
DebugConsole.AddWarning(
|
||||
$"An exception was thrown when attempting to copy \"{from}\" to \"{to}\": {e.Message}\n{e.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -44,8 +43,9 @@ namespace Barotrauma
|
||||
roundData = new RoundData();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine == null || item.Submarine.Info.Type != SubmarineType.Player) { continue; }
|
||||
Reactor reactor = item.GetComponent<Reactor>();
|
||||
if (reactor != null) { roundData.Reactors.Add(reactor); }
|
||||
if (reactor != null && reactor.Item.Condition > 0.0f) { roundData.Reactors.Add(reactor); }
|
||||
}
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
cachedDistances.Clear();
|
||||
@@ -73,7 +73,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (c.IsDead) { continue; }
|
||||
//achievement for descending below crush depth and coming back
|
||||
if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f)
|
||||
if (GameMain.GameSession.RoundDuration > 30.0f)
|
||||
{
|
||||
if (c.Submarine != null && c.Submarine.AtDamageDepth || Level.Loaded.GetRealWorldDepth(c.WorldPosition.Y) > Level.Loaded.RealWorldCrushDepth)
|
||||
{
|
||||
@@ -97,7 +97,7 @@ namespace Barotrauma
|
||||
//get an achievement if they're still alive at the end of the round
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!c.IsDead && c.Submarine == sub) roundData.ReactorMeltdown.Add(c);
|
||||
if (!c.IsDead && c.Submarine == sub) { roundData.ReactorMeltdown.Add(c); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ namespace Barotrauma
|
||||
|
||||
//achievement for descending ridiculously deep
|
||||
float realWorldDepth = sub.RealWorldDepth;
|
||||
if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f)
|
||||
if (realWorldDepth > 5000.0f && GameMain.GameSession.RoundDuration > 30.0f)
|
||||
{
|
||||
//all conscious characters inside the sub get an achievement
|
||||
UnlockAchievement("subdeep".ToIdentifier(), true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious);
|
||||
@@ -219,6 +219,12 @@ namespace Barotrauma
|
||||
UnlockAchievement($"discover{biome.Identifier.Value.Replace(" ", "")}".ToIdentifier());
|
||||
}
|
||||
|
||||
public static void OnCampaignMetadataSet(Identifier identifier, object value)
|
||||
{
|
||||
if (identifier.IsEmpty || value is null) { return; }
|
||||
UnlockAchievement($"campaignmetadata_{identifier}_{value}".ToIdentifier());
|
||||
}
|
||||
|
||||
public static void OnItemRepaired(Item item, Character fixer)
|
||||
{
|
||||
#if CLIENT
|
||||
|
||||
@@ -36,9 +36,7 @@ namespace Barotrauma
|
||||
public EitherT(T value) { Value = value; }
|
||||
|
||||
public override string? ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})";
|
||||
|
||||
public override bool TryGet(out T t) { t = Value; return true; }
|
||||
public override bool TryGet(out U u) { u = default!; return false; }
|
||||
@@ -75,9 +73,7 @@ namespace Barotrauma
|
||||
public EitherU(U value) { Value = value; }
|
||||
|
||||
public override string? ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})";
|
||||
|
||||
public override bool TryGet(out T t) { t = default!; return false; }
|
||||
public override bool TryGet(out U u) { u = Value; return true; }
|
||||
|
||||
@@ -63,5 +63,21 @@ namespace Barotrauma
|
||||
=> !(a == b);
|
||||
|
||||
public abstract override string ToString();
|
||||
|
||||
public static implicit operator Option<T>(Option.UnspecifiedNone _)
|
||||
=> None();
|
||||
}
|
||||
|
||||
public static class Option
|
||||
{
|
||||
public sealed class UnspecifiedNone
|
||||
{
|
||||
private UnspecifiedNone() { }
|
||||
internal static readonly UnspecifiedNone Instance = new();
|
||||
}
|
||||
|
||||
public static UnspecifiedNone None => UnspecifiedNone.Instance;
|
||||
|
||||
public static Option<T> Some<T>(T value) => Option<T>.Some(value);
|
||||
}
|
||||
}
|
||||
@@ -59,5 +59,14 @@ namespace Barotrauma
|
||||
|
||||
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none();
|
||||
}
|
||||
|
||||
public static string NameWithGenerics(this Type t)
|
||||
{
|
||||
if (!t.IsGenericType) { return t.Name; }
|
||||
|
||||
string result = t.Name[..t.Name.IndexOf('`')];
|
||||
result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public abstract class Result<T, TError>
|
||||
@@ -13,6 +16,14 @@ namespace Barotrauma
|
||||
|
||||
public static Failure<T, TError> Failure(TError error)
|
||||
=> new Failure<T, TError>(error);
|
||||
|
||||
public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value);
|
||||
public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value);
|
||||
|
||||
public abstract override string? ToString();
|
||||
|
||||
public static (Func<T, Result<T, TError>> Success, Func<TError, Result<T, TError>> Failure) GetFactoryMethods()
|
||||
=> (Success, Failure);
|
||||
}
|
||||
|
||||
public sealed class Success<T, TError> : Result<T, TError>
|
||||
@@ -22,6 +33,21 @@ namespace Barotrauma
|
||||
public readonly T Value;
|
||||
public override bool IsSuccess => true;
|
||||
|
||||
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
|
||||
{
|
||||
value = Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})";
|
||||
|
||||
public Success(T value)
|
||||
{
|
||||
Value = value;
|
||||
@@ -35,6 +61,21 @@ namespace Barotrauma
|
||||
public readonly TError Error;
|
||||
|
||||
public override bool IsSuccess => false;
|
||||
|
||||
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
|
||||
{
|
||||
value = Error;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})";
|
||||
|
||||
public Failure(TError error)
|
||||
{
|
||||
|
||||
@@ -314,6 +314,19 @@ namespace Barotrauma.IO
|
||||
//TODO: validate recursion?
|
||||
System.IO.Directory.Delete(path, recursive);
|
||||
}
|
||||
|
||||
public static bool TryDelete(string path, bool recursive = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static DateTime GetLastWriteTime(string path)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Networking;
|
||||
|
||||
/*
|
||||
* What are segment tables for?
|
||||
*
|
||||
* Segment tables help make our networking packet reading code more robust by
|
||||
* clearly stating where part of a message begins. Previously we would've done
|
||||
* something like:
|
||||
*
|
||||
* msg.WriteByte(SegmentType.A);
|
||||
* ...
|
||||
* msg.WriteByte(SegmentType.B);
|
||||
* ...
|
||||
* msg.WriteByte(SegmentType.EndOfMessage);
|
||||
*
|
||||
* The problem with this design is that it's hard to debug when the writing and reading
|
||||
* code do not align for whatever reason. INetSerializableStruct is an awesome way
|
||||
* of avoiding that problem, but deploying it on a broad scale means rewriting most
|
||||
* of the netcode. That isn't going to happen any time soon, so this exists as an easier
|
||||
* way of increasing robustness.
|
||||
*
|
||||
* A segment table is laid out as follows:
|
||||
*
|
||||
* [TablePointer: UInt16]
|
||||
* [Segment: arbitrary]
|
||||
* ...
|
||||
* [Segment: arbitrary]
|
||||
* [NumberOfSegments: UInt16]
|
||||
* [(Identifier, SegmentPointer): (T, UInt16)]
|
||||
* ...
|
||||
* [(Identifier, SegmentPointer): (T, UInt16)]
|
||||
*
|
||||
* A pointer in this context is an offset relative to the BitPosition where the TablePointer is written.
|
||||
*
|
||||
* It is used as follows:
|
||||
*
|
||||
* using (var segmentTable = SegmentTableWriter<T>.StartWriting(outMsg))
|
||||
* {
|
||||
* segmentTable.StartNewSegment(T.A);
|
||||
* ... write segment to outMsg ...
|
||||
* segmentTable.StartNewSegment(T.B);
|
||||
* ... write segment to outMsg ...
|
||||
* }
|
||||
* peer.SendMessage(outMsg);
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* SegmentTableReader<T>.Read(inc,
|
||||
* segmentDataReader: (segment, inc) =>
|
||||
* {
|
||||
* switch (segment)
|
||||
* {
|
||||
* ... read segments ...
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The advantages of this approach are:
|
||||
* - If a message is truncated or corrupted near the end, it becomes far more obvious because the table
|
||||
* would not be read properly and look like garbage when printed to the console.
|
||||
* - If the reading and writing code for a segment disagree on something, issues will be isolated to that
|
||||
* one segment.
|
||||
* - The code no longer has to fiddle with padding and temporary buffers because the segment table is able
|
||||
* to handle content that is not byte-aligned just fine.
|
||||
* - Exception handling is far easier when using a segment table, when combined with a using statement
|
||||
* any uncaught exception will result in the entire table being skipped, allowing the remainder of the
|
||||
* message to still be read.
|
||||
* - It's harder to make mistakes in the implementation of segments themselves with this approach. By using
|
||||
* the SegmentTableWriter and SegmentTableReader types, you get a type-safe way of delimiting segments
|
||||
* and it's harder to forget to finalize a packet.
|
||||
*/
|
||||
|
||||
[NetworkSerialize]
|
||||
public readonly record struct Segment<T>(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct;
|
||||
|
||||
readonly ref struct SegmentTableWriter<T> where T : struct
|
||||
{
|
||||
private readonly IWriteMessage message;
|
||||
private readonly List<Segment<T>> segments;
|
||||
public readonly int PointerLocation;
|
||||
private SegmentTableWriter(IWriteMessage message, int pointerLocation)
|
||||
{
|
||||
this.message = message;
|
||||
this.PointerLocation = pointerLocation;
|
||||
this.segments = new List<Segment<T>>();
|
||||
}
|
||||
|
||||
public static SegmentTableWriter<T> StartWriting(IWriteMessage msg)
|
||||
{
|
||||
var retVal = new SegmentTableWriter<T>(msg, msg.BitPosition);
|
||||
msg.WriteUInt16(0); //reserve space for the table pointer
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private void ThrowOnInvalidState()
|
||||
{
|
||||
if (segments.Count >= UInt16.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>");
|
||||
}
|
||||
|
||||
if (message.BitPosition - PointerLocation > UInt16.MaxValue)
|
||||
{
|
||||
throw new OverflowException(
|
||||
$"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)");
|
||||
}
|
||||
}
|
||||
|
||||
public void StartNewSegment(T value)
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
segments.Add(new Segment<T>(value, (UInt16)(message.BitPosition-PointerLocation)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ThrowOnInvalidState();
|
||||
int tablePosition = message.BitPosition;
|
||||
|
||||
//rewrite the table pointer now that we know where the table ends
|
||||
message.BitPosition = PointerLocation;
|
||||
message.WriteUInt16((UInt16)(tablePosition-PointerLocation));
|
||||
|
||||
//write the table
|
||||
message.BitPosition = tablePosition;
|
||||
message.WriteUInt16((UInt16)segments.Count);
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
message.WriteNetSerializableStruct(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly ref struct SegmentTableReader<T> where T : struct
|
||||
{
|
||||
private class SegmentReadMsg : IReadMessage
|
||||
{
|
||||
private readonly IReadMessage underlyingMsg;
|
||||
private readonly IReadOnlyList<Segment<T>> segments;
|
||||
private readonly int segmentIndex;
|
||||
private readonly int offset;
|
||||
private readonly int lengthBits;
|
||||
public SegmentReadMsg(IReadMessage underlyingMsg, IReadOnlyList<Segment<T>> segments, int segmentIndex, int offset, int lengthBits)
|
||||
{
|
||||
this.underlyingMsg = underlyingMsg;
|
||||
this.segments = segments;
|
||||
this.segmentIndex = segmentIndex;
|
||||
this.offset = offset;
|
||||
this.lengthBits = lengthBits;
|
||||
|
||||
if (offset + lengthBits >= underlyingMsg.LengthBits)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Segment table is corrupt, segment length is invalid: {offset} + {lengthBits} >= {underlyingMsg.LengthBits}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Check()
|
||||
{
|
||||
if (BitPosition > lengthBits)
|
||||
{
|
||||
throw new Exception($"Tried to read too much data from segment.");
|
||||
}
|
||||
}
|
||||
|
||||
private TRead Check<TRead>(TRead v)
|
||||
{
|
||||
Check();
|
||||
return v;
|
||||
}
|
||||
|
||||
public bool ReadBoolean() => Check(underlyingMsg.ReadBoolean());
|
||||
|
||||
public void ReadPadBits()
|
||||
{
|
||||
Check(); underlyingMsg.ReadPadBits();
|
||||
}
|
||||
|
||||
public byte ReadByte() => Check(underlyingMsg.ReadByte());
|
||||
|
||||
public byte PeekByte() => Check(underlyingMsg.PeekByte());
|
||||
|
||||
public ushort ReadUInt16() => Check(underlyingMsg.ReadUInt16());
|
||||
|
||||
public short ReadInt16() => Check(underlyingMsg.ReadInt16());
|
||||
|
||||
public uint ReadUInt32() => Check(underlyingMsg.ReadUInt32());
|
||||
|
||||
public int ReadInt32() => Check(underlyingMsg.ReadInt32());
|
||||
|
||||
public ulong ReadUInt64() => Check(underlyingMsg.ReadUInt64());
|
||||
|
||||
public long ReadInt64() => Check(underlyingMsg.ReadInt64());
|
||||
|
||||
public float ReadSingle() => Check(underlyingMsg.ReadSingle());
|
||||
|
||||
public double ReadDouble() => Check(underlyingMsg.ReadDouble());
|
||||
|
||||
public uint ReadVariableUInt32() => Check(underlyingMsg.ReadVariableUInt32());
|
||||
|
||||
public string ReadString() => Check(underlyingMsg.ReadString());
|
||||
|
||||
public Identifier ReadIdentifier() => Check(underlyingMsg.ReadIdentifier());
|
||||
|
||||
public Color ReadColorR8G8B8() => Check(underlyingMsg.ReadColorR8G8B8());
|
||||
|
||||
public Color ReadColorR8G8B8A8() => Check(underlyingMsg.ReadColorR8G8B8A8());
|
||||
|
||||
public int ReadRangedInteger(int min, int max) => Check(underlyingMsg.ReadRangedInteger(min, max));
|
||||
|
||||
public float ReadRangedSingle(float min, float max, int bitCount) => Check(underlyingMsg.ReadRangedSingle(min, max, bitCount));
|
||||
|
||||
public byte[] ReadBytes(int numberOfBytes) => Check(underlyingMsg.ReadBytes(numberOfBytes));
|
||||
|
||||
public int BitPosition
|
||||
{
|
||||
get => underlyingMsg.BitPosition - offset;
|
||||
set => Check(underlyingMsg.BitPosition = value + offset);
|
||||
}
|
||||
|
||||
public int BytePosition => BitPosition / 8;
|
||||
|
||||
public byte[] Buffer => underlyingMsg.Buffer;
|
||||
|
||||
public int LengthBits
|
||||
{
|
||||
get => lengthBits;
|
||||
set => throw new InvalidOperationException($"Cannot resize {nameof(SegmentReadMsg)}");
|
||||
}
|
||||
|
||||
public int LengthBytes => lengthBits / 8;
|
||||
|
||||
public NetworkConnection Sender => underlyingMsg.Sender;
|
||||
}
|
||||
|
||||
private readonly IReadMessage message;
|
||||
private readonly List<Segment<T>> segments;
|
||||
private readonly int exitLocation;
|
||||
public readonly int PointerLocation;
|
||||
private SegmentTableReader(IReadMessage message, List<Segment<T>> segments, int pointerLocation, int exitLocation)
|
||||
{
|
||||
this.message = message;
|
||||
this.segments = segments;
|
||||
this.PointerLocation = pointerLocation;
|
||||
this.exitLocation = exitLocation;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Segment<T>> Segments => segments;
|
||||
|
||||
public enum BreakSegmentReading
|
||||
{
|
||||
No,
|
||||
Yes
|
||||
}
|
||||
|
||||
public delegate BreakSegmentReading SegmentDataReader(
|
||||
T segmentHeader,
|
||||
IReadMessage incMsg);
|
||||
|
||||
public delegate void ExceptionHandler(
|
||||
Segment<T> segmentWithError,
|
||||
Segment<T>[] previousSegments,
|
||||
Exception exceptionThrown);
|
||||
|
||||
public static void Read(
|
||||
IReadMessage msg,
|
||||
SegmentDataReader segmentDataReader,
|
||||
ExceptionHandler? exceptionHandler = null)
|
||||
{
|
||||
int pointerLocation = msg.BitPosition;
|
||||
int tablePointer = msg.ReadUInt16();
|
||||
int tableLocation = pointerLocation + tablePointer;
|
||||
|
||||
int returnPosition = msg.BitPosition;
|
||||
|
||||
//read the table
|
||||
var segments = new List<Segment<T>>();
|
||||
msg.BitPosition = tableLocation;
|
||||
int numSegments = msg.ReadUInt16();
|
||||
for (int i = 0; i < numSegments; i++)
|
||||
{
|
||||
segments.Add(INetSerializableStruct.Read<Segment<T>>(msg));
|
||||
}
|
||||
|
||||
//store the exit location and go back to the top
|
||||
int exitLocation = msg.BitPosition;
|
||||
msg.BitPosition = returnPosition;
|
||||
using var segmentTable = new SegmentTableReader<T>(msg, segments, pointerLocation, exitLocation);
|
||||
|
||||
for (int i = 0; i < segmentTable.Segments.Count; i++)
|
||||
{
|
||||
var segment = segmentTable.Segments[i];
|
||||
msg.BitPosition = segmentTable.PointerLocation + segment.Pointer;
|
||||
try
|
||||
{
|
||||
if (segmentDataReader(segment.Identifier, new SegmentReadMsg(
|
||||
msg,
|
||||
segments,
|
||||
i,
|
||||
offset: segmentTable.PointerLocation + segment.Pointer,
|
||||
lengthBits: (i < segmentTable.Segments.Count - 1 ? segments[i + 1].Pointer : tablePointer) -
|
||||
segment.Pointer))
|
||||
is BreakSegmentReading.Yes)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var prevSegments = segments.Take(i).ToArray();
|
||||
if (exceptionHandler is not null)
|
||||
{
|
||||
exceptionHandler(segment, prevSegments, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(
|
||||
$"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
|
||||
(prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}." : ""),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
message.BitPosition = exitLocation;
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,17 @@ namespace Barotrauma
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<T> WaitForLoadingScreen<T>(this Task<T> task)
|
||||
{
|
||||
var result = await task;
|
||||
#if CLIENT
|
||||
while (GameMain.Instance.LoadingScreenOpen)
|
||||
{
|
||||
await Task.Delay((int)(1000 * Timing.Step));
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user