Build 0.20.8.0

This commit is contained in:
Markus Isberg
2022-11-25 19:56:30 +02:00
parent ecb6d40b4b
commit df805574c4
111 changed files with 2347 additions and 1283 deletions
@@ -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;
}
@@ -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; }
@@ -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);
@@ -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)
@@ -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();
}
}
}
@@ -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;
}*/
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
+74 -1
View File
@@ -1,3 +1,65 @@
---------------------------------------------------------------------------------------------------------
v0.20.8.0
---------------------------------------------------------------------------------------------------------
Changes and additions:
- Added various new loot items to different creatures.
- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death.
- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version.
- Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself).
- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions.
- Addressed "overactive hammerheads" (#10072) by increasing the perception ranges of the monster mission variants.
- Added a new honking scary random event to beacon stations.
- Added a reactor infographic designed to help new players better understand the reactor interface. It's accessible through a help button on the top right corner of the interface.
- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately.
Unstable only:
- Fixed status effects with OnUse not always working in the multiplayer mode. Fixes the sounds not playing when the medical items were used on the health interface.
- Fixed mutually exclusive talents being both selectable.
- Fixed "Medical Expertise" not boosting bandages, but applying heals to the entire body instead.
- Fixed "Graduation Ceremony" granting extra talent points.
- Fixed skillbooks triggering talent effects, allowing enormous XP gains when combined with certain captain talents.
- Don't allow laying in beds when wearing an exosuit.
- Added sounds for Petraptor and Defensebot.
- Added sounds for the new weapons.
- Adjusted Shotgun and Scrap Cannon spread.
- Fixed interacting with a wrecked periscope doing nothing.
- Fixed double-clicking on a stack of ammo not reloading the equipped weapon.
- Fixed fabricator pulling materials from the previous user's inventory even if the character is no longer at the fabricator.
- Adjusted item editing hud position to prevent it from overlapping with the affliction icons.
- Fixed active tutorial child objectives not being re-added to the list after resolution change.
- Fixed wiring interfaces becoming blank after changing resolution.
- Fixed changing the selected location on the campaign map changing what outpost you are currently at according to the tab menu.
- Fixed players who've been inside a clown crate becoming impossible to grab/heal for the rest of the round.
- Fixed medical syringes (or other melee weapons) hitting characters inside a clown crate.
- Fixed some inconsistencies with turret loader box positions.
- Fixed items that should only be available in stores past a certain difficulty not being available at all.
- Fixed dedicated server not using the "ispublic" setting configured in serversettings.xml.
Modding:
- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched.
- Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target.
- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value.
Bugfixes:
- Attempt to fix occasional "mission equality to check failed" errors + added some extra data to the error message to help diagnose it if it still occurs.
- Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range.
- Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled.
- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes.
- Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed.
- Fixed mods failing to show up in the mods list at all when they have certain kinds of errors.
- Mods with errors can no longer be enabled.
- Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open.
- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands.
- Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives).
- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret.
- The "respawnnow" console command forces a respawn even if there's less than the minimum amount of players waiting for a respawn.
- Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right.
- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor).
- Fixed items equipped in the health interface slot being sellable.
- Fixed inconsistent view ranges of large turrets.
- Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite.
---------------------------------------------------------------------------------------------------------
v0.20.7.0
---------------------------------------------------------------------------------------------------------
@@ -37,10 +99,12 @@ Bugfixes:
- Fixed certain genetic effects (such as regeneration from Hammerhead Matriarch genes) not working properly when multiple characters have the same effect.
- Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel.
- Fixed incorrect tags in the vending machine in EngineeringModule_01_Colony, causing engineering gear to spawn in the output slot.
- Fixed hair being visible under hats where it shouldn't be. Only happened after loading a saved game (#10396).
Modding:
- Made it possible to check if some value is null or not with PropertyConditionals (e.g. CurrentHull="eq null").
- Added UseEnvironment.None to Propulsion component.
- Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime.
---------------------------------------------------------------------------------------------------------
v0.20.6.0
@@ -157,6 +221,9 @@ Balance:
Bugfixes:
- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor".
- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders).
- Fix bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them.
- Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window.
Modding:
- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses).
@@ -185,10 +252,16 @@ Unstable only:
- Add missing recipe unlocks: Ceremonial Sword, Handcannon.
- Moved "Steady Tune" to tier 2, added a new "Trickle Down" talent to the 3rd tier.
Bugfixes:
- Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously.
---------------------------------------------------------------------------------------------------------
v0.20.1.0
---------------------------------------------------------------------------------------------------------
Changes:
- Flashlight can now be attached on all the ranged weapons held with two hands.
Tutorial improvements:
- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost. 1st version: feedback and issue reports are much appreciated!
- Various fixes and improvements to the Basic and Role tutorials.
@@ -406,7 +479,7 @@ Changes and additions:
- Added a warning if a new keybind overlaps with any of the player's existing binds.
- Overvoltage makes devices perform better, increasing the output of engines, making fabricators, deconstructors and pumps operate faster, electrical discharge coils do more damage, batteries recharge faster and oxygen generators generate more oxygen. Encourages operating the reactor manually and hopefully makes it a little more engaging.
- Added more randomness to junction box overvoltage damage, and made partially damaged boxes take more damage from overvoltage. Prevents all boxes from breaking at the same time, making overvoltage less of a pain to deal with and intentionally overvolting devices more worthwhile.
- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate a new benefit to operating reactors manually.
- Added manual temperature adjustment buttons which immediately increase/decrease the temperature of the reactor for a brief amount of time on manual control (bumps the gauge up/down by a fifth, and the boost fades out in 20 seconds). Allows reacting to load fluctuations very quickly, and conserving fuel by operating the reactor at a lower fission rate a new benefit to operating reactors manually.
- Signals no longer set the fission and turbine rates of the reactor instantaneously, making automated reactor circuits less overpowered. They are still viable, but especially now with the addition of the extra incentives for operating the reactor manually, they're no longer as clearly the best and most efficient way to operate the reactor, making manual operation more worthwhile.
- Made the "distort" camera effect a little less obtrusive and glitchy-looking (smoother texture + less heavy effect).
- Made water-sensitive materials (lithium, potassium, sodium) spawn in waterproof chemical crates.