diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs index d74d45e..9920449 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/DataTypes/SfxEvent.cs @@ -2,13 +2,44 @@ using System.Collections.Generic; using System.Linq; using PG.Commons.Hashing; -using PG.StarWarsGame.Engine.Xml.Parsers.Data; +using PG.StarWarsGame.Engine.Xml.Tags; using PG.StarWarsGame.Files.XML; namespace PG.StarWarsGame.Engine.DataTypes; public sealed class SfxEvent : XmlObject { + public const byte MaxVolumeValue = 100; + public const byte MaxPitchValue = 200; + public const byte MinPitchValue = 50; + public const byte MaxPan2dValue = 100; + public const byte MinPriorityValue = 1; + public const byte MaxPriorityValue = 5; + public const byte MaxProbability = 100; + public const sbyte MinMaxInstances = 0; + public const sbyte InfinitivePlayCount = -1; + public const float MinLoopSeconds = 0.0f; + public const float MinVolumeSaturation = 0.0f; + + // Default values which are not the default value of the type + public const byte DefaultPriority = 3; + public const bool DefaultIs3d = true; + public const byte DefaultProbability = 100; + public const sbyte DefaultPlayCount = 1; + public const sbyte DefaultMaxInstances = 1; + public const byte DefaultMinVolume = 100; + public const byte DefaultMaxVolume = 100; + public const byte DefaultMinPitch = 100; + public const byte DefaultMaxPitch = 100; + public const byte DefaultMinPan2d = 50; + public const byte DefaultMaxPan2d = 50; + public const float DefaultVolumeSaturationDistance = 300.0f; + + private SfxEvent? _preset; + private string? _presetName; + private string? _overlapTestName; + private string? _chainedSfxEvent; + private IReadOnlyList? _textIds; private IReadOnlyList? _preSamples; private IReadOnlyList? _samples; private IReadOnlyList? _postSamples; @@ -20,10 +51,70 @@ public sealed class SfxEvent : XmlObject private bool? _isUnitResponseVo; private bool? _isAmbientVo; private bool? _isLocalized; + private bool? _playSequentially; + private bool? _killsPreviousObjectsSfx; + private byte? _priority; + private byte? _probability; + private sbyte? _playCount; + private sbyte? _maxInstances; + private uint? _minPredelay; + private uint? _maxPredelay; + private uint? _minPostdelay; + private uint? _maxPostdelay; + private float? _loopFadeInSeconds; + private float? _loopFadeOutSeconds; + private float? _volumeSaturationDistance; + + private static readonly Func PriorityCoercion = priority => + { + if (!priority.HasValue) + return DefaultPriority; + if (priority < MinPriorityValue) + return MinPriorityValue; + if (priority > MaxPriorityValue) + return MaxPriorityValue; + return priority; + }; + + private static readonly Func MaxInstancesCoercion = maxInstances => + { + if (!maxInstances.HasValue) + return DefaultMaxInstances; + if (maxInstances < MinMaxInstances) + return MinMaxInstances; + return maxInstances; + }; + + private static readonly Func ProbabilityCoercion = probability => + { + if (!probability.HasValue) + return DefaultProbability; + if (probability > MaxProbability) + return MaxProbability; + return probability; + }; + + private static readonly Func PlayCountCoercion = playCount => + { + if (!playCount.HasValue) + return DefaultPlayCount; + if (playCount < InfinitivePlayCount) + return InfinitivePlayCount; + return playCount; + }; + + private static readonly Func LoopAndSaturationCoercion = loopSeconds => + { + if (!loopSeconds.HasValue) + return DefaultVolumeSaturationDistance; + if (loopSeconds < MinLoopSeconds) + return MinLoopSeconds; + return loopSeconds; + }; - public bool IsPreset { get; } + public bool IsPreset => LazyInitValue(ref _isPreset, SfxEventXmlTags.IsPreset, false)!.Value; - public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; + public bool Is3D => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, DefaultIs3d)!.Value; public bool Is2D => LazyInitValue(ref _is2D, SfxEventXmlTags.Is2D, false)!.Value; @@ -37,9 +128,11 @@ public sealed class SfxEvent : XmlObject public bool IsLocalized => LazyInitValue(ref _isLocalized, SfxEventXmlTags.Localize, false)!.Value; - public string? UsePresetName { get; } + public SfxEvent? Preset => LazyInitValue(ref _preset, SfxEventXmlTags.PresetXRef, null); + + public string? UsePresetName => LazyInitValue(ref _presetName, SfxEventXmlTags.UsePreset, null); - public bool PlaySequentially => LazyInitValue(ref _is3D, SfxEventXmlTags.Is3D, false)!.Value; + public bool PlaySequentially => LazyInitValue(ref _playSequentially, SfxEventXmlTags.PlaySequentially, false)!.Value; public IEnumerable AllSamples => PreSamples.Concat(Samples).Concat(PostSamples); @@ -49,19 +142,36 @@ public sealed class SfxEvent : XmlObject public IReadOnlyList PostSamples => LazyInitValue(ref _postSamples, SfxEventXmlTags.PostSamples, Array.Empty()); - public IReadOnlyList LocalizedTextIDs { get; } + public IReadOnlyList LocalizedTextIDs => LazyInitValue(ref _textIds, SfxEventXmlTags.TextID, Array.Empty()); + + public byte Priority => LazyInitValue(ref _priority, SfxEventXmlTags.Priority, DefaultPriority, PriorityCoercion)!.Value; + + public byte Probability => LazyInitValue(ref _probability, SfxEventXmlTags.Probability, DefaultProbability, ProbabilityCoercion)!.Value; - public byte Priority { get; } + public sbyte PlayCount => LazyInitValue(ref _playCount, SfxEventXmlTags.PlayCount, DefaultPlayCount, PlayCountCoercion)!.Value; - public byte Probability { get; } + public float LoopFadeInSeconds => LazyInitValue(ref _loopFadeInSeconds, SfxEventXmlTags.LoopFadeInSeconds, 0f, LoopAndSaturationCoercion)!.Value; - public sbyte PlayCount { get; } + public float LoopFadeOutSeconds => LazyInitValue(ref _loopFadeOutSeconds, SfxEventXmlTags.LoopFadeOutSeconds, 0f, LoopAndSaturationCoercion)!.Value; - public float LoopFadeInSeconds { get; } + public sbyte MaxInstances => LazyInitValue(ref _maxInstances, SfxEventXmlTags.MaxInstances, DefaultMaxInstances, MaxInstancesCoercion)!.Value; - public float LoopFadeOutInSeconds { get; } + public uint MinPredelay => LazyInitValue(ref _minPredelay, SfxEventXmlTags.MinPredelay, 0u)!.Value; - public sbyte MaxInstances { get; } + public uint MaxPredelay => LazyInitValue(ref _maxPredelay, SfxEventXmlTags.MaxPredelay, 0u)!.Value; + + public uint MinPostdelay => LazyInitValue(ref _minPostdelay, SfxEventXmlTags.MinPostdelay, 0u)!.Value; + + public uint MaxPostdelay => LazyInitValue(ref _maxPostdelay, SfxEventXmlTags.MaxPostdelay, 0u)!.Value; + + public float VolumeSaturationDistance => LazyInitValue(ref _volumeSaturationDistance, + SfxEventXmlTags.VolumeSaturationDistance, DefaultVolumeSaturationDistance, LoopAndSaturationCoercion)!.Value; + + public bool KillsPreviousObjectsSfx => LazyInitValue(ref _killsPreviousObjectsSfx, SfxEventXmlTags.KillsPreviousObjectSFX, false)!.Value; + + public string? OverlapTestName => LazyInitValue(ref _overlapTestName, SfxEventXmlTags.OverlapTest, null); + + public string? ChainedSfxEventName => LazyInitValue(ref _chainedSfxEvent, SfxEventXmlTags.ChainedSfxEvent, null); public byte MinVolume { get; } @@ -75,26 +185,76 @@ public sealed class SfxEvent : XmlObject public byte MaxPan2D { get; } - public uint MinPredelay { get; } - - public uint MaxPredelay { get; } + internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, + XmlLocationInfo location) + : base(name, nameCrc, properties, location) + { + var minMaxVolume = GetMinMaxVolume(properties); + MinVolume = minMaxVolume.min; + MaxVolume = minMaxVolume.max; - public uint MinPostdelay { get; } + var minMaxPitch = GetMinMaxPitch(properties); + MinPitch = minMaxPitch.min; + MaxPitch = minMaxPitch.max; - public uint MaxPostdelay { get; } + var minMaxPan = GetMinMaxPan2d(properties); + MinPan2D = minMaxPan.min; + MaxPan2D = minMaxPan.max; + } - public float VolumeSaturationDistance { get; } + private static (byte min, byte max) GetMinMaxVolume(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinVolume, SfxEventXmlTags.MaxVolume, DefaultMinVolume, + DefaultMaxVolume, null, MaxVolumeValue); + } - public bool KillsPreviousObjectsSfx { get; } + private static (byte min, byte max) GetMinMaxPitch(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinPitch, SfxEventXmlTags.MaxPitch, DefaultMinPitch, + DefaultMaxPitch, MinPitchValue, MaxPitchValue); + } - public string? OverlapTestName { get; } + private static (byte min, byte max) GetMinMaxPan2d(IReadOnlyValueListDictionary properties) + { + return GetMinMaxValues(properties, SfxEventXmlTags.MinPan2D, SfxEventXmlTags.MaxPan2D, DefaultMinPan2d, + DefaultMaxPan2d, null, MaxPan2dValue); + } - public string? ChainedSfxEventName { get; } - internal SfxEvent(string name, Crc32 nameCrc, IReadOnlyValueListDictionary properties, - XmlLocationInfo location) - : base(name, nameCrc, properties, location) + private static (byte min, byte max) GetMinMaxValues( + IReadOnlyValueListDictionary properties, + string minTag, + string maxTag, + byte defaultMin, + byte defaultMax, + byte? totalMinValue, + byte? totalMaxValue) { - + var minValue = !properties.TryGetLastValue(minTag, out var minObj) ? defaultMin : Convert.ToByte(minObj); + var maxValue = !properties.TryGetLastValue(maxTag, out var maxObj) ? defaultMax : Convert.ToByte(maxObj); + + if (totalMaxValue.HasValue) + { + if (minValue > totalMaxValue) + minValue = totalMaxValue.Value; + if (maxValue > totalMaxValue) + maxValue = totalMaxValue.Value; + } + + if (totalMinValue.HasValue) + { + if (minValue < totalMinValue) + minValue = totalMinValue.Value; + if (maxValue < totalMinValue) + maxValue = totalMinValue.Value; + } + + if (minValue > maxValue) + minValue = maxValue; + + if (maxValue < minValue) + maxValue = minValue; + + return (minValue, maxValue); } } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj index 0c08df4..ddd8497 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/PG.StarWarsGame.Engine.csproj @@ -26,6 +26,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs index 991865b..b767564 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/Data/SfxEventParser.cs @@ -3,49 +3,12 @@ using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; +using PG.StarWarsGame.Engine.Xml.Tags; using PG.StarWarsGame.Files.XML; using PG.StarWarsGame.Files.XML.Parsers; namespace PG.StarWarsGame.Engine.Xml.Parsers.Data; -public static class SfxEventXmlTags -{ - public const string IsPreset = "Is_Preset"; - public const string UsePreset = "Use_Preset"; - public const string Samples = "Samples"; - public const string PreSamples = "Pre_Samples"; - public const string PostSamples = "Post_Samples"; - public const string TextID = "Text_ID"; - public const string PlaySequentially = "Play_Sequentially"; - public const string Priority = "Priority"; - public const string Probability = "Probability"; - public const string PlayCount = "Play_Count"; - public const string LoopFadeInSeconds = "Loop_Fade_In_Seconds"; - public const string LoopFadeOutSeconds = "Loop_Fade_Out_Seconds"; - public const string MaxInstances = "Max_Instances"; - public const string MinVolume = "Min_Volume"; - public const string MaxVolume = "Max_Volume"; - public const string MinPitch = "Min_Pitch"; - public const string MaxPitch = "Max_Pitch"; - public const string MinPan2D = "Min_Pan2D"; - public const string MaxPan2D = "Max_Pan2D"; - public const string MinPredelay = "Min_Predelay"; - public const string MaxPredelay = "Max_Predelay"; - public const string MinPostdelay = "Min_Postdelay"; - public const string MaxPostdelay = "Max_Postdelay"; - public const string VolumeSaturationDistance = "Volume_Saturation_Distance"; - public const string KillsPreviousObjectSFX = "Kills_Previous_Object_SFX"; - public const string OverlapTest = "Overlap_Test"; - public const string Localize = "Localize"; - public const string Is2D = "Is_2D"; - public const string Is3D = "Is_3D"; - public const string IsGui = "Is_GUI"; - public const string IsHudVo = "Is_HUD_VO"; - public const string IsUnitResponseVo = "Is_Unit_Response_VO"; - public const string IsAmbientVo = "Is_Ambient_VO"; - public const string ChainedSfxEvent = "Chained_SFXEvent"; -} - public sealed class SfxEventParser( IReadOnlyValueListDictionary parsedElements, IServiceProvider serviceProvider) @@ -86,7 +49,7 @@ public sealed class SfxEventParser( case SfxEventXmlTags.Probability: case SfxEventXmlTags.MinVolume: case SfxEventXmlTags.MaxVolume: - return PrimitiveParserProvider.ByteParser; + return PrimitiveParserProvider.Max100ByteParser; case SfxEventXmlTags.MinPredelay: case SfxEventXmlTags.MaxPredelay: case SfxEventXmlTags.MinPostdelay: @@ -101,9 +64,7 @@ public sealed class SfxEventParser( } } - public override SfxEvent Parse( - XElement element, - out Crc32 nameCrc) + public override SfxEvent Parse(XElement element, out Crc32 nameCrc) { var name = GetNameAttributeValue(element); nameCrc = HashingService.GetCrc32Upper(name.AsSpan(), PGConstants.PGCrc32Encoding); @@ -119,19 +80,15 @@ protected override bool OnParsed(string tag, object value, ValueListDictionary currentXmlProperties, SfxEvent preset) + private static void CopySfxPreset(ValueListDictionary currentXmlProperties, SfxEvent preset) { /* * The engine also copies the Use_Preset *of* the preset, (which almost most cases is null) @@ -153,8 +110,9 @@ private void CopySfxPreset(ValueListDictionary currentXmlProper continue; currentXmlProperties.Add(keyValuePair.Key, keyValuePair.Value); } - } + currentXmlProperties.Add(SfxEventXmlTags.PresetXRef, preset); + } public override SfxEvent Parse(XElement element) => throw new NotSupportedException(); } \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs index 35e8e02..559414e 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Engine/Xml/Parsers/File/SfxEventFileParser.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.Logging; using PG.Commons.Hashing; using PG.StarWarsGame.Engine.DataTypes; using PG.StarWarsGame.Engine.Xml.Parsers.Data; @@ -17,6 +18,8 @@ protected override void Parse(XElement element, IValueListDictionary Parse(XElement element) if (trimmedValued.Length == 0) return Array.Empty(); + if (trimmedValued.Length > 0x2000) + { + Logger?.LogWarning($"Input value is too long '{trimmedValued.Length}' at {XmlLocationInfo.FromElement(element)}"); + return Array.Empty(); + } + var entries = trimmedValued.Split(Separators, StringSplitOptions.RemoveEmptyEntries); return entries; diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs new file mode 100644 index 0000000..7c767b3 --- /dev/null +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PetroglyphXmlMax100ByteParser.cs @@ -0,0 +1,37 @@ +using System; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; + +namespace PG.StarWarsGame.Files.XML.Parsers.Primitives; + +public sealed class PetroglyphXmlMax100ByteParser : PetroglyphXmlPrimitiveElementParser +{ + internal PetroglyphXmlMax100ByteParser(IServiceProvider serviceProvider) : base(serviceProvider) + { + + } + + public override byte Parse(XElement element) + { + var intValue = PrimitiveParserProvider.IntParser.Parse(element); + + if (intValue > 100) + intValue = 100; + + var asByte = (byte)intValue; + if (intValue != asByte) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected a byte value (0 - 255) but got value '{intValue}' at {location}"); + } + + // Add additional check, cause the PG implementation is broken, but we need to stay "bug-compatible". + if (asByte > 100) + { + var location = XmlLocationInfo.FromElement(element); + Logger?.LogWarning($"Expected a byte value (0 - 100) but got value '{asByte}' at {location}"); + } + + return asByte; + } +} \ No newline at end of file diff --git a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs index d749197..57f5f15 100644 --- a/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs +++ b/src/PetroglyphTools/PG.StarWarsGame.Files.XML/Parsers/Primitives/PrimitiveParserProvider.cs @@ -10,6 +10,7 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim private readonly Lazy _lazyIntParser = new(() => new PetroglyphXmlIntegerParser(serviceProvider)); private readonly Lazy _lazyFloatParser = new(() => new PetroglyphXmlFloatParser(serviceProvider)); private readonly Lazy _lazyByteParser = new(() => new PetroglyphXmlByteParser(serviceProvider)); + private readonly Lazy _lazyMax100ByteParser = new(() => new PetroglyphXmlMax100ByteParser(serviceProvider)); private readonly Lazy _lazyBoolParser = new(() => new PetroglyphXmlBooleanParser(serviceProvider)); private readonly Lazy _lazyCommaStringKeyListParser = new(() => new CommaSeparatedStringKeyValueListParser(serviceProvider)); @@ -19,6 +20,7 @@ internal class PrimitiveParserProvider(IServiceProvider serviceProvider) : IPrim public PetroglyphXmlIntegerParser IntParser => _lazyIntParser.Value; public PetroglyphXmlFloatParser FloatParser => _lazyFloatParser.Value; public PetroglyphXmlByteParser ByteParser => _lazyByteParser.Value; + public PetroglyphXmlMax100ByteParser Max100ByteParser => _lazyMax100ByteParser.Value; public PetroglyphXmlBooleanParser BooleanParser => _lazyBoolParser.Value; public CommaSeparatedStringKeyValueListParser CommaSeparatedStringKeyValueListParser => _lazyCommaStringKeyListParser.Value; } \ No newline at end of file