From 2cfb681bd499c01f21e6e741ddc91343ecd4e838 Mon Sep 17 00:00:00 2001 From: Devin Garner Date: Mon, 16 Dec 2024 16:26:29 -0700 Subject: [PATCH] Major refactor of Jass Decompiler for ObjectManager data (units, items, regions, cameras, sounds) --- .../Extensions/SoundExtensions.cs | 21 + .../Extensions/UnitDataExtensions.cs | 2 +- src/War3Net.Build/War3Net.Build.csproj | 4 + .../Audio/MapSoundsDecompiler.cs | 661 +++++----- .../DecompilationContext.cs | 121 +- .../Environment/DecompileOptions.cs | 25 + .../Environment/MapCamerasDecompiler.cs | 240 ++-- .../Environment/MapRegionsDecompiler.cs | 240 ++-- .../FunctionDeclarationContext.cs | 2 +- .../JassDecompilerExtensions.cs | 63 + .../JassScriptDecompiler.cs | 136 +- .../JassScriptDecompilerHelpers.cs | 104 ++ .../Nullable_Class.cs | 87 ++ .../ObjectDataContext.cs | 14 +- .../VariableDeclarationContext.cs | 2 +- .../Widget/MapUnitsDecompiler.cs | 1175 ++++++++--------- .../Extensions/ExpressionSyntaxExtensions.cs | 190 ++- .../Audio/MapSoundsDecompilerTests.cs | 6 +- .../Environment/MapCamerasDecompilerTests.cs | 6 +- .../Environment/MapRegionsDecompilerTests.cs | 6 +- .../Script/MapTriggersDecompilerTests.cs | 4 +- .../Widget/MapUnitsDecompilerTests.cs | 6 +- 22 files changed, 1701 insertions(+), 1414 deletions(-) create mode 100644 src/War3Net.Build/Extensions/SoundExtensions.cs create mode 100644 src/War3Net.CodeAnalysis.Decompilers/Environment/DecompileOptions.cs create mode 100644 src/War3Net.CodeAnalysis.Decompilers/JassDecompilerExtensions.cs create mode 100644 src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompilerHelpers.cs create mode 100644 src/War3Net.CodeAnalysis.Decompilers/Nullable_Class.cs diff --git a/src/War3Net.Build/Extensions/SoundExtensions.cs b/src/War3Net.Build/Extensions/SoundExtensions.cs new file mode 100644 index 00000000..b70f8aa3 --- /dev/null +++ b/src/War3Net.Build/Extensions/SoundExtensions.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------ +// +// Licensed under the MIT license. +// See the LICENSE file in the project root for more information. +// +// ------------------------------------------------------------------------------ + +using War3Net.Build.Audio; +using War3Net.Build.Environment; + +namespace War3Net.Build.Extensions +{ + + public static class SoundExtensions + { + public static string GetVariableName(this Sound sound) + { + return $"gg_snd_{sound.Name.Replace(' ', '_')}"; + } + } +} \ No newline at end of file diff --git a/src/War3Net.Build/Extensions/UnitDataExtensions.cs b/src/War3Net.Build/Extensions/UnitDataExtensions.cs index e71b771c..adf3db9b 100644 --- a/src/War3Net.Build/Extensions/UnitDataExtensions.cs +++ b/src/War3Net.Build/Extensions/UnitDataExtensions.cs @@ -25,7 +25,7 @@ public static class UnitDataExtensions public static string GetVariableName(this UnitData unitData) { - return $"gg_unit_{unitData.TypeId.ToRawcode()}_{unitData.CreationNumber:D4}"; + return $"{(unitData.IsItem() ? "gg_item_" : "gg_unit_")}{unitData.TypeId.ToRawcode()}_{unitData.CreationNumber:D4}"; } public static string GetDropItemsFunctionName(this UnitData unitData, int id) diff --git a/src/War3Net.Build/War3Net.Build.csproj b/src/War3Net.Build/War3Net.Build.csproj index 126a36f1..0fafc698 100644 --- a/src/War3Net.Build/War3Net.Build.csproj +++ b/src/War3Net.Build/War3Net.Build.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/src/War3Net.CodeAnalysis.Decompilers/Audio/MapSoundsDecompiler.cs b/src/War3Net.CodeAnalysis.Decompilers/Audio/MapSoundsDecompiler.cs index d8297c7a..bd37dfde 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/Audio/MapSoundsDecompiler.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/Audio/MapSoundsDecompiler.cs @@ -1,390 +1,381 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// +// // ------------------------------------------------------------------------------ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using War3Net.Build.Audio; -using War3Net.CodeAnalysis.Jass.Extensions; using War3Net.CodeAnalysis.Jass.Syntax; namespace War3Net.CodeAnalysis.Decompilers { public partial class JassScriptDecompiler { - public bool TryDecompileMapSounds(MapSoundsFormatVersion formatVersion, [NotNullWhen(true)] out MapSounds? mapSounds) + [RegisterStatementParser] + internal void ParseSoundCreation(StatementParserInput input) { - foreach (var candidateFunction in GetCandidateFunctions("InitSounds")) + var variableAssignment = GetVariableAssignment(input.StatementChildren); + if (variableAssignment == null || !variableAssignment.StartsWith("gg_snd_")) { - if (TryDecompileMapSounds(candidateFunction.FunctionDeclaration, formatVersion, out mapSounds)) + return; + } + + var sound = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CreateSound") + .SafeMapFirst(x => + { + var filePath = Regex.Unescape(((JassStringLiteralExpressionSyntax)x.Arguments.Arguments[0]).Value); + Context.ImportedFileNames.Add(filePath); + + return new Sound + { + FilePath = filePath, + Flags = ParseSoundFlags(x.Arguments.Arguments, filePath), + FadeInRate = x.Arguments.Arguments[4].GetValueOrDefault(), + FadeOutRate = x.Arguments.Arguments[5].GetValueOrDefault(), + EaxSetting = ((JassStringLiteralExpressionSyntax)x.Arguments.Arguments[6]).Value, + DialogueTextKey = -1, + DialogueSpeakerNameKey = -1 + }; + }); + + if (sound != null) + { + sound.Name = variableAssignment; + Context.Add(sound, variableAssignment); + Context.HandledStatements.Add(input.Statement); + } + } + + [RegisterStatementParser] + internal void ParseSetSoundParamsFromLabel(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundParamsFromLabel") + .SafeMapFirst(x => + { + return new { - candidateFunction.Handled = true; + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + SoundName = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax)?.Value, + }; + }); - return true; + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.SoundName = match.SoundName; + Context.HandledStatements.Add(input.Statement); } } + } - mapSounds = null; - return false; + [RegisterStatementParser] + internal void ParseSetSoundFacialAnimationLabel(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundFacialAnimationLabel") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + FacialAnimationLabel = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax)?.Value, + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.FacialAnimationLabel = match.FacialAnimationLabel; + Context.HandledStatements.Add(input.Statement); + } + } } - public bool TryDecompileMapSounds(JassFunctionDeclarationSyntax functionDeclaration, MapSoundsFormatVersion formatVersion, [NotNullWhen(true)] out MapSounds? mapSounds) + [RegisterStatementParser] + internal void ParseSetSoundFacialAnimationGroupLabel(StatementParserInput input) { - if (functionDeclaration is null) + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundFacialAnimationGroupLabel") + .SafeMapFirst(x => { - throw new ArgumentNullException(nameof(functionDeclaration)); + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + FacialAnimationGroupLabel = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax)?.Value, + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.FacialAnimationGroupLabel = match.FacialAnimationGroupLabel; + Context.HandledStatements.Add(input.Statement); + } } + } - var sounds = new Dictionary(StringComparer.Ordinal); + [RegisterStatementParser] + internal void ParseSetSoundFacialAnimationSetFilepath(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundFacialAnimationSetFilepath") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + FacialAnimationSetFilepath = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax)?.Value, + }; + }); - foreach (var statement in functionDeclaration.Body.Statements) + if (match != null) { - if (statement is JassCommentSyntax || - statement is JassEmptySyntax) + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) { - continue; + sound.FacialAnimationSetFilepath = match.FacialAnimationSetFilepath; + Context.HandledStatements.Add(input.Statement); } - else if (statement is JassSetStatementSyntax setStatement) + } + } + + [RegisterStatementParser] + internal void ParseSetDialogueSpeakerNameKey(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetDialogueSpeakerNameKey") + .SafeMapFirst(x => + { + return new { - if (setStatement.Indexer is null && - setStatement.IdentifierName.Name.StartsWith("gg_snd_", StringComparison.Ordinal)) - { - if (setStatement.Value.Expression is JassInvocationExpressionSyntax invocationExpression && - string.Equals(invocationExpression.IdentifierName.Name, "CreateSound", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 7 && - invocationExpression.Arguments.Arguments[0] is JassStringLiteralExpressionSyntax fileNameLiteralExpression && - invocationExpression.Arguments.Arguments[1] is JassBooleanLiteralExpressionSyntax loopingLiteralExpression && - invocationExpression.Arguments.Arguments[2] is JassBooleanLiteralExpressionSyntax is3DLiteralExpression && - invocationExpression.Arguments.Arguments[3] is JassBooleanLiteralExpressionSyntax stopWhenOutOfRangeLiteralExpression && - invocationExpression.Arguments.Arguments[4].TryGetIntegerExpressionValue(out var fadeInRate) && - invocationExpression.Arguments.Arguments[5].TryGetIntegerExpressionValue(out var fadeOutRate) && - invocationExpression.Arguments.Arguments[6] is JassStringLiteralExpressionSyntax eaxSettingLiteralExpression) - { - var flags = (SoundFlags)0; - if (loopingLiteralExpression.Value) - { - flags |= SoundFlags.Looping; - } - - if (is3DLiteralExpression.Value) - { - flags |= SoundFlags.Is3DSound; - } - - if (stopWhenOutOfRangeLiteralExpression.Value) - { - flags |= SoundFlags.StopWhenOutOfRange; - } - - var filePath = Regex.Unescape(fileNameLiteralExpression.Value); - Context.ImportedFileNames.Add(filePath); - - if (!is3DLiteralExpression.Value && !IsInternalSound(filePath)) - { - flags |= SoundFlags.UNK16; - } - - sounds.Add(setStatement.IdentifierName.Name, new Sound - { - Name = setStatement.IdentifierName.Name, - FilePath = filePath, - EaxSetting = eaxSettingLiteralExpression.Value, - Flags = flags, - FadeInRate = fadeInRate, - FadeOutRate = fadeOutRate, - - DialogueTextKey = -1, - DialogueSpeakerNameKey = -1, - FacialAnimationLabel = string.Empty, - FacialAnimationGroupLabel = string.Empty, - FacialAnimationSetFilepath = string.Empty, - }); - } - else - { - mapSounds = null; - return false; - } - } - else if (setStatement.Value.Expression is JassStringLiteralExpressionSyntax stringLiteralExpression) - { - var flags = SoundFlags.Music; - - var filePath = Regex.Unescape(stringLiteralExpression.Value); - Context.ImportedFileNames.Add(filePath); - - if (!IsInternalSound(filePath)) - { - flags |= SoundFlags.UNK16; - } - - sounds.Add(setStatement.IdentifierName.Name, new Sound - { - Name = setStatement.IdentifierName.Name, - FilePath = filePath, - EaxSetting = string.Empty, - Flags = flags, - FadeInRate = 10, - FadeOutRate = 10, - }); - } - else - { - continue; - } - } - else + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + DialogueSpeakerNameKey_TriggerString = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax).Value, + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + if (match.DialogueSpeakerNameKey_TriggerString.StartsWith("TRIGSTR_", StringComparison.Ordinal) && int.TryParse(match.DialogueSpeakerNameKey_TriggerString["TRIGSTR_".Length..], NumberStyles.None, CultureInfo.InvariantCulture, out var dialogueSpeakerNameKey)) { - continue; + sound.DialogueSpeakerNameKey = dialogueSpeakerNameKey; + Context.HandledStatements.Add(input.Statement); } } - else if (statement is JassCallStatementSyntax callStatement) + } + } + + [RegisterStatementParser] + internal void ParseSetDialogueTextKey(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetDialogueTextKey") + .SafeMapFirst(x => + { + return new { - if (string.Equals(callStatement.IdentifierName.Name, "SetSoundParamsFromLabel", StringComparison.Ordinal)) - { - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundFacialAnimationLabel", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassStringLiteralExpressionSyntax stringLiteralExpression && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.FacialAnimationLabel = stringLiteralExpression.Value; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundFacialAnimationGroupLabel", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassStringLiteralExpressionSyntax stringLiteralExpression && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.FacialAnimationGroupLabel = stringLiteralExpression.Value; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundFacialAnimationSetFilepath", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassStringLiteralExpressionSyntax stringLiteralExpression && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.FacialAnimationSetFilepath = stringLiteralExpression.Value; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetDialogueSpeakerNameKey", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassStringLiteralExpressionSyntax stringLiteralExpression && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound) && - stringLiteralExpression.Value.StartsWith("TRIGSTR_", StringComparison.Ordinal) && - int.TryParse(stringLiteralExpression.Value["TRIGSTR_".Length..], NumberStyles.None, CultureInfo.InvariantCulture, out var dialogueSpeakerNameKey)) - { - sound.DialogueSpeakerNameKey = dialogueSpeakerNameKey; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetDialogueTextKey", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassStringLiteralExpressionSyntax stringLiteralExpression && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound) && - stringLiteralExpression.Value.StartsWith("TRIGSTR_", StringComparison.Ordinal) && - int.TryParse(stringLiteralExpression.Value["TRIGSTR_".Length..], NumberStyles.None, CultureInfo.InvariantCulture, out var dialogueTextKey)) - { - sound.DialogueTextKey = dialogueTextKey; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundDuration", StringComparison.Ordinal)) - { - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundDistanceCutoff", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var cutoff) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.DistanceCutoff = cutoff; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundChannel", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var channel) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.Channel = (SoundChannel)channel; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundVolume", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var volume) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.Volume = volume; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundPitch", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var pitch) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound)) - { - sound.Pitch = pitch; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundDistances", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var minDist) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var maxDist) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound) && - sound.Flags.HasFlag(SoundFlags.Is3DSound)) - { - sound.MinDistance = minDist; - sound.MaxDistance = maxDist; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundConeAngles", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var inside) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var outside) && - callStatement.Arguments.Arguments[3].TryGetIntegerExpressionValue(out var outsideVolume) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound) && - sound.Flags.HasFlag(SoundFlags.Is3DSound)) - { - sound.ConeAngleInside = inside; - sound.ConeAngleOutside = outside; - sound.ConeOutsideVolume = outsideVolume; - } - else - { - mapSounds = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetSoundConeOrientation", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y) && - callStatement.Arguments.Arguments[3].TryGetRealExpressionValue(out var z) && - sounds.TryGetValue(variableReferenceExpression.IdentifierName.Name, out var sound) && - sound.Flags.HasFlag(SoundFlags.Is3DSound)) - { - sound.ConeOrientation = new(x, y, z); - } - else - { - mapSounds = null; - return false; - } - } - else + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + DialogueTextKey_TriggerString = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax).Value, + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + if (match.DialogueTextKey_TriggerString.StartsWith("TRIGSTR_", StringComparison.Ordinal) && int.TryParse(match.DialogueTextKey_TriggerString["TRIGSTR_".Length..], NumberStyles.None, CultureInfo.InvariantCulture, out var dialogueTextKey)) { - mapSounds = null; - return false; + sound.DialogueTextKey = dialogueTextKey; + Context.HandledStatements.Add(input.Statement); } } - else + } + } + + [RegisterStatementParser] + internal void ParseSetSoundDistanceCutoff(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundDistanceCutoff") + .SafeMapFirst(x => + { + return new { - mapSounds = null; - return false; + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + DistanceCutoff = x.Arguments.Arguments[1].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.DistanceCutoff = match.DistanceCutoff; + Context.HandledStatements.Add(input.Statement); } } + } - if (sounds.Any()) + [RegisterStatementParser] + internal void ParseSetSoundChannel(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundChannel") + .SafeMapFirst(x => { - mapSounds = new MapSounds(formatVersion); - mapSounds.Sounds.AddRange(sounds.Values); - return true; + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Channel = (SoundChannel)x.Arguments.Arguments[1].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.Channel = match.Channel; + Context.HandledStatements.Add(input.Statement); + } } + } - mapSounds = null; - return false; + [RegisterStatementParser] + internal void ParseSetSoundVolume(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundVolume") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Volume = x.Arguments.Arguments[1].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.Volume = match.Volume; + Context.HandledStatements.Add(input.Statement); + } + } } - [Obsolete] - private static bool IsInternalSound(string filePath) + [RegisterStatementParser] + internal void ParseSetSoundPitch(StatementParserInput input) { - return filePath.StartsWith(@"Sound\", StringComparison.OrdinalIgnoreCase) - || filePath.StartsWith(@"Sound/", StringComparison.OrdinalIgnoreCase) - || filePath.StartsWith(@"UI\", StringComparison.OrdinalIgnoreCase) - || filePath.StartsWith(@"UI/", StringComparison.OrdinalIgnoreCase) - || filePath.StartsWith(@"Units\", StringComparison.OrdinalIgnoreCase) - || filePath.StartsWith(@"Units/", StringComparison.OrdinalIgnoreCase); + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundPitch") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Pitch = x.Arguments.Arguments[1].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.Pitch = match.Pitch; + Context.HandledStatements.Add(input.Statement); + } + } + } + + [RegisterStatementParser] + internal void ParseSetSoundDistances(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundDistances") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + MinDistance = x.Arguments.Arguments[1].GetValueOrDefault(), + MaxDistance = x.Arguments.Arguments[2].GetValueOrDefault() + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null) + { + sound.MinDistance = match.MinDistance; + sound.MaxDistance = match.MaxDistance; + Context.HandledStatements.Add(input.Statement); + } + } + } + + [RegisterStatementParser] + internal void ParseSetSoundConeAngles(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundConeAngles") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + ConeAngleInside = x.Arguments.Arguments[1].GetValueOrDefault(), + ConeAngleOutside = x.Arguments.Arguments[2].GetValueOrDefault(), + ConeOutsideVolume = x.Arguments.Arguments[3].GetValueOrDefault() + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null && sound.Flags.HasFlag(SoundFlags.Is3DSound)) + { + sound.ConeAngleInside = match.ConeAngleInside; + sound.ConeAngleOutside = match.ConeAngleOutside; + sound.ConeOutsideVolume = match.ConeOutsideVolume; + Context.HandledStatements.Add(input.Statement); + } + } + } + + [RegisterStatementParser] + internal void ParseSetSoundConeOrientation(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundConeOrientation") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + x = x.Arguments.Arguments[1].GetValueOrDefault(), + y = x.Arguments.Arguments[2].GetValueOrDefault(), + z = x.Arguments.Arguments[3].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var sound = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (sound != null && sound.Flags.HasFlag(SoundFlags.Is3DSound)) + { + sound.ConeOrientation = new(match.x, match.y, match.z); + Context.HandledStatements.Add(input.Statement); + } + } } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/DecompilationContext.cs b/src/War3Net.CodeAnalysis.Decompilers/DecompilationContext.cs index e26efe73..41e2da34 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/DecompilationContext.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/DecompilationContext.cs @@ -1,56 +1,35 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// // ------------------------------------------------------------------------------ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; -using War3Net.Build; using War3Net.Build.Info; using War3Net.Build.Script; -using War3Net.CodeAnalysis.Jass; using War3Net.CodeAnalysis.Jass.Syntax; namespace War3Net.CodeAnalysis.Decompilers { - internal sealed class DecompilationContext + public class DecompilationContext { - public DecompilationContext(Map map, Campaign? campaign, TriggerData? triggerData) + public DecompilationContext(JassCompilationUnitSyntax compilationUnit, DecompileOptions options = null, MapInfo mapInfo = null, TriggerData triggerData = null) { - if (map is null) - { - throw new ArgumentNullException(nameof(map)); - } - - if (map.Info is null) - { - throw new Exception(); - } - - if (map.Info.ScriptLanguage != ScriptLanguage.Jass) - { - throw new Exception(); - } + CompilationUnit = compilationUnit; + Options = options ?? new DecompileOptions(); + MapInfo = mapInfo; - if (string.IsNullOrEmpty(map.Script)) - { - throw new Exception(); - } - - ObjectData = new ObjectDataContext(map, campaign); TriggerData = new TriggerDataContext(triggerData); - var compilationUnit = JassSyntaxFactory.ParseCompilationUnit(map.Script); - var comments = new List(); var functionDeclarationsBuilder = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); var variableDeclarationsBuilder = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); - foreach (var declaration in compilationUnit.Declarations) + foreach (var declaration in CompilationUnit.Declarations) { if (declaration is JassCommentSyntax comment) { @@ -80,21 +59,91 @@ public DecompilationContext(Map map, Campaign? campaign, TriggerData? triggerDat FunctionDeclarations = functionDeclarationsBuilder.ToImmutable(); VariableDeclarations = variableDeclarationsBuilder.ToImmutable(); + ImportedFileNames = new(StringComparer.OrdinalIgnoreCase); + MaxPlayerSlots = mapInfo != null && mapInfo.EditorVersion >= EditorVersion.v6060 ? 24 : 12; + } + + public TriggerDataContext TriggerData { get; } + public ImmutableDictionary FunctionDeclarations { get; } + public ImmutableDictionary VariableDeclarations { get; } + public HashSet ImportedFileNames { get; } + public int MaxPlayerSlots { get; } + + public HashSet HandledStatements = new HashSet(); + public JassCompilationUnitSyntax CompilationUnit { get; } + public MapInfo MapInfo { get; } + public DecompileOptions Options { get; } - MaxPlayerSlots = map.Info.EditorVersion >= EditorVersion.v6060 ? 24 : 12; + private readonly Dictionary _variableNameToValueMapping = new(); + private readonly List _values = new(); + private int _lastCreationNumber; + + public int GetNextCreationNumber() + { + return _lastCreationNumber++; } - public ObjectDataContext ObjectData { get; } + public string GetVariableName(object value) + { + return _variableNameToValueMapping.FirstOrDefault(x => x.Value == value).Key; + } - public TriggerDataContext TriggerData { get; } + public void Add(T value, string variableName = null) where T : class + { + if (variableName != null) + { + _variableNameToValueMapping[variableName] = value; + } - public ImmutableDictionary FunctionDeclarations { get; } + _values.Add(value); + } - public ImmutableDictionary VariableDeclarations { get; } + public void Add_Struct(T value, string variableName = null) where T : struct + { + Add(new Nullable_Class(value), variableName); + } - public HashSet ImportedFileNames { get; } + public T Get(string variableName) where T : class + { + if (variableName == null) + { + return default; + } + + return _variableNameToValueMapping.GetValueOrDefault(variableName) as T; + } + + public Nullable_Class Get_Struct(string variableName = null) where T : struct + { + return Get>(variableName); + } + + public T GetLastCreated() where T : class + { + return _values.OfType().LastOrDefault(); + } + + public Nullable_Class GetLastCreated_Struct() where T : struct + { + return GetLastCreated>(); + } + + public IEnumerable GetAll() where T : class + { + return _values.OfType(); + } + + public IEnumerable> GetAll_Struct() where T : struct + { + return GetAll>(); + } + + private const string PSEUDO_VARIABLE_PREFIX = "##PSEUDO_VARIABLE_PREFIX##"; + internal string CreatePseudoVariableName(string type, string name = "") + { + return PSEUDO_VARIABLE_PREFIX + "_" + type.ToString() + "_" + name; + } - public int MaxPlayerSlots { get; } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/Environment/DecompileOptions.cs b/src/War3Net.CodeAnalysis.Decompilers/Environment/DecompileOptions.cs new file mode 100644 index 00000000..063578f8 --- /dev/null +++ b/src/War3Net.CodeAnalysis.Decompilers/Environment/DecompileOptions.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------ +// +// Licensed under the MIT license. +// See the LICENSE file in the project root for more information. +// +// ------------------------------------------------------------------------------ + +using War3Net.Build.Audio; +using War3Net.Build.Environment; +using War3Net.Build.Widget; + +namespace War3Net.CodeAnalysis.Decompilers +{ + public class DecompileOptions + { + public MapCamerasFormatVersion mapCamerasFormatVersion; + public bool mapCamerasUseNewFormat; + public MapRegionsFormatVersion mapRegionsFormatVersion; + public MapSoundsFormatVersion mapSoundsFormatVersion; + public MapWidgetsFormatVersion mapWidgetsFormatVersion; + public MapWidgetsSubVersion mapWidgetsSubVersion; + public bool mapWidgetsUseNewFormat = default; + public SpecialDoodadVersion specialDoodadVersion; + } +} \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/Environment/MapCamerasDecompiler.cs b/src/War3Net.CodeAnalysis.Decompilers/Environment/MapCamerasDecompiler.cs index b8e4dc26..65ddcd4a 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/Environment/MapCamerasDecompiler.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/Environment/MapCamerasDecompiler.cs @@ -1,176 +1,140 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// +// // ------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using War3Net.Build.Environment; -using War3Net.CodeAnalysis.Jass.Extensions; using War3Net.CodeAnalysis.Jass.Syntax; namespace War3Net.CodeAnalysis.Decompilers { + public partial class JassScriptDecompiler { - public bool TryDecompileMapCameras(MapCamerasFormatVersion formatVersion, bool useNewFormat, [NotNullWhen(true)] out MapCameras? mapCameras) + [RegisterStatementParser] + internal void ParseCameraCreation(StatementParserInput input) { - foreach (var candidateFunction in GetCandidateFunctions("CreateCameras")) + var variableAssignment = GetVariableAssignment(input.StatementChildren); + if (variableAssignment == null || !variableAssignment.StartsWith("gg_cam_")) + { + return; + } + + var camera = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CreateCameraSetup") + .SafeMapFirst(x => { - if (TryDecompileMapCameras(candidateFunction.FunctionDeclaration, formatVersion, useNewFormat, out mapCameras)) + return new Camera { - candidateFunction.Handled = true; + NearClippingPlane = Context.Options.mapCamerasUseNewFormat ? default : 100f, + }; + }); - return true; - } + if (camera != null) + { + camera.Name = variableAssignment["gg_cam_".Length..].Replace('_', ' '); + Context.Add(camera, variableAssignment); + Context.HandledStatements.Add(input.Statement); } - - mapCameras = null; - return false; } - public bool TryDecompileMapCameras(JassFunctionDeclarationSyntax functionDeclaration, MapCamerasFormatVersion formatVersion, bool useNewFormat, [NotNullWhen(true)] out MapCameras? mapCameras) + [RegisterStatementParser] + internal void ParseCameraSetupSetField(StatementParserInput input) { - if (functionDeclaration is null) + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CameraSetupSetField") + .SafeMapFirst(x => { - throw new ArgumentNullException(nameof(functionDeclaration)); - } - - var cameras = new Dictionary(StringComparer.Ordinal); + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + FieldName = ((JassVariableReferenceExpressionSyntax)x.Arguments.Arguments[1]).IdentifierName.Name, + Value = x.Arguments.Arguments[2].GetValueOrDefault(), + Duration = x.Arguments.Arguments[3].GetValueOrDefault(), + }; + }); - foreach (var statement in functionDeclaration.Body.Statements) + if (match != null) { - if (statement is JassCommentSyntax || - statement is JassEmptySyntax) - { - continue; - } - else if (statement is JassSetStatementSyntax setStatement) + var camera = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (camera != null) { - if (setStatement.Indexer is null && - setStatement.IdentifierName.Name.StartsWith("gg_cam_", StringComparison.Ordinal) && - setStatement.Value.Expression is JassInvocationExpressionSyntax invocationExpression && - string.Equals(invocationExpression.IdentifierName.Name, "CreateCameraSetup", StringComparison.Ordinal)) + var handled = true; + var value = match.Value; + switch (match.FieldName) { - if (invocationExpression.Arguments.Arguments.IsEmpty) - { - cameras.Add(setStatement.IdentifierName.Name, new Camera - { - Name = setStatement.IdentifierName.Name["gg_cam_".Length..].Replace('_', ' '), - NearClippingPlane = useNewFormat ? default : 100f, - }); - } - else - { - mapCameras = null; - return false; - } + case "CAMERA_FIELD_ZOFFSET": + camera.ZOffset = value; + break; + case "CAMERA_FIELD_ROTATION": + camera.Rotation = value; + break; + case "CAMERA_FIELD_ANGLE_OF_ATTACK": + camera.AngleOfAttack = value; + break; + case "CAMERA_FIELD_TARGET_DISTANCE": + camera.TargetDistance = value; + break; + case "CAMERA_FIELD_ROLL": + camera.Roll = value; + break; + case "CAMERA_FIELD_FIELD_OF_VIEW": + camera.FieldOfView = value; + break; + case "CAMERA_FIELD_FARZ": + camera.FarClippingPlane = value; + break; + case "CAMERA_FIELD_NEARZ": + camera.NearClippingPlane = value; + break; + case "CAMERA_FIELD_LOCAL_PITCH": + camera.LocalPitch = value; + break; + case "CAMERA_FIELD_LOCAL_YAW": + camera.LocalYaw = value; + break; + case "CAMERA_FIELD_LOCAL_ROLL": + camera.LocalRoll = value; + break; + default: + handled = false; + break; } - else - { - continue; - } - } - else if (statement is JassCallStatementSyntax callStatement) - { - if (string.Equals(callStatement.IdentifierName.Name, "CameraSetupSetField", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax cameraVariableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassVariableReferenceExpressionSyntax cameraFieldVariableReferenceExpression && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var value) && - callStatement.Arguments.Arguments[3].TryGetRealExpressionValue(out var duration) && - duration == 0f && - cameras.TryGetValue(cameraVariableReferenceExpression.IdentifierName.Name, out var camera)) - { - switch (cameraFieldVariableReferenceExpression.IdentifierName.Name) - { - case "CAMERA_FIELD_ZOFFSET": - camera.ZOffset = value; - break; - case "CAMERA_FIELD_ROTATION": - camera.Rotation = value; - break; - case "CAMERA_FIELD_ANGLE_OF_ATTACK": - camera.AngleOfAttack = value; - break; - case "CAMERA_FIELD_TARGET_DISTANCE": - camera.TargetDistance = value; - break; - case "CAMERA_FIELD_ROLL": - camera.Roll = value; - break; - case "CAMERA_FIELD_FIELD_OF_VIEW": - camera.FieldOfView = value; - break; - case "CAMERA_FIELD_FARZ": - camera.FarClippingPlane = value; - break; - case "CAMERA_FIELD_NEARZ": - camera.NearClippingPlane = value; - break; - case "CAMERA_FIELD_LOCAL_PITCH": - camera.LocalPitch = value; - break; - case "CAMERA_FIELD_LOCAL_YAW": - camera.LocalYaw = value; - break; - case "CAMERA_FIELD_LOCAL_ROLL": - camera.LocalRoll = value; - break; - } - } - else - { - mapCameras = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "CameraSetupSetDestPosition", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax cameraVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y) && - callStatement.Arguments.Arguments[3].TryGetRealExpressionValue(out var duration) && - duration == 0f && - cameras.TryGetValue(cameraVariableReferenceExpression.IdentifierName.Name, out var camera)) - { - camera.TargetPosition = new(x, y); - } - else - { - mapCameras = null; - return false; - } - } - else + + if (handled) { - mapCameras = null; - return false; + Context.HandledStatements.Add(input.Statement); } } - else - { - mapCameras = null; - return false; - } } + } - if (cameras.Any()) + [RegisterStatementParser] + internal void ParseCameraSetupSetDestPosition(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CameraSetupSetDestPosition") + .SafeMapFirst(x => { - mapCameras = new MapCameras(formatVersion, useNewFormat); - mapCameras.Cameras.AddRange(cameras.Values); - return true; - } + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + X = x.Arguments.Arguments[1].GetValueOrDefault(), + Y = x.Arguments.Arguments[2].GetValueOrDefault(), + Duration = x.Arguments.Arguments[3].GetValueOrDefault() + }; + }); - mapCameras = null; - return false; + if (match != null) + { + var camera = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (camera != null) + { + camera.TargetPosition = new(match.X, match.Y); + Context.HandledStatements.Add(input.Statement); + } + } } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/Environment/MapRegionsDecompiler.cs b/src/War3Net.CodeAnalysis.Decompilers/Environment/MapRegionsDecompiler.cs index 0e1149ec..273e80d1 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/Environment/MapRegionsDecompiler.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/Environment/MapRegionsDecompiler.cs @@ -1,192 +1,130 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// +// // ------------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using War3Net.Build; -using War3Net.Build.Environment; using War3Net.CodeAnalysis.Jass.Extensions; using War3Net.CodeAnalysis.Jass.Syntax; +using Region = War3Net.Build.Environment.Region; namespace War3Net.CodeAnalysis.Decompilers { public partial class JassScriptDecompiler { - public bool TryDecompileMapRegions(MapRegionsFormatVersion formatVersion, [NotNullWhen(true)] out MapRegions? mapRegions) + [RegisterStatementParser] + internal void ParseRegionCreation(StatementParserInput input) { - foreach (var candidateFunction in GetCandidateFunctions("CreateRegions")) + var variableAssignment = GetVariableAssignment(input.StatementChildren); + if (variableAssignment == null || !variableAssignment.StartsWith("gg_rct_")) { - if (TryDecompileMapRegions(candidateFunction.FunctionDeclaration, formatVersion, out mapRegions)) + return; + } + + var region = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "Rect") + .SafeMapFirst(x => + { + return new Region { - candidateFunction.Handled = true; + Left = x.Arguments.Arguments[0].GetValueOrDefault(), + Bottom = x.Arguments.Arguments[1].GetValueOrDefault(), + Right = x.Arguments.Arguments[2].GetValueOrDefault(), + Top = x.Arguments.Arguments[3].GetValueOrDefault(), + Color = System.Drawing.Color.FromArgb(unchecked((int)0xFF8080FF)), + CreationNumber = Context.GetNextCreationNumber() + }; + }); - return true; - } + if (region != null) + { + region.Name = variableAssignment["gg_rct_".Length..].Replace('_', ' '); + Context.Add(region, variableAssignment); + Context.HandledStatements.Add(input.Statement); } - - mapRegions = null; - return false; } - public bool TryDecompileMapRegions(JassFunctionDeclarationSyntax functionDeclaration, MapRegionsFormatVersion formatVersion, [NotNullWhen(true)] out MapRegions? mapRegions) + [RegisterStatementParser] + internal void ParseAddWeatherEffect(StatementParserInput input) { - if (functionDeclaration is null) + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "AddWeatherEffect") + .SafeMapFirst(x => { - throw new ArgumentNullException(nameof(functionDeclaration)); - } - - Region? currentRegion = null; - var createdRegions = new List(); - var regions = new Dictionary(StringComparer.Ordinal); + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + WeatherType = (WeatherType)((JassFourCCLiteralExpressionSyntax)x.Arguments.Arguments[1]).Value.InvertEndianness(), + }; + }); - foreach (var statement in functionDeclaration.Body.Statements) + if (match != null) { - if (statement is JassLocalVariableDeclarationStatementSyntax || - statement is JassCommentSyntax || - statement is JassEmptySyntax) + var region = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (region != null) { - continue; + region.WeatherType = match.WeatherType; + Context.HandledStatements.Add(input.Statement); } - else if (statement is JassSetStatementSyntax setStatement) - { - if (setStatement.Indexer is null && - setStatement.Value.Expression is JassInvocationExpressionSyntax invocationExpression) - { - if (setStatement.IdentifierName.Name.StartsWith("gg_rct_", StringComparison.Ordinal) && - string.Equals(invocationExpression.IdentifierName.Name, "Rect", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 4 && - invocationExpression.Arguments.Arguments[0].TryGetRealExpressionValue(out var minx) && - invocationExpression.Arguments.Arguments[1].TryGetRealExpressionValue(out var miny) && - invocationExpression.Arguments.Arguments[2].TryGetRealExpressionValue(out var maxx) && - invocationExpression.Arguments.Arguments[3].TryGetRealExpressionValue(out var maxy)) - { - currentRegion = new Region - { - Name = setStatement.IdentifierName.Name["gg_rct_".Length..].Replace('_', ' '), - Left = minx, - Bottom = miny, - Right = maxx, - Top = maxy, - Color = System.Drawing.Color.FromArgb(unchecked((int)0xFF8080FF)), - CreationNumber = regions.Count, - AmbientSound = string.Empty, - }; + } + } - createdRegions.Add(currentRegion); + [RegisterStatementParser] + internal void ParseSetSoundPosition(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetSoundPosition") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + x = x.Arguments.Arguments[1].GetValueOrDefault(), + y = x.Arguments.Arguments[2].GetValueOrDefault() + }; + }); - if (!regions.TryAdd(setStatement.IdentifierName.Name, currentRegion)) - { - regions[setStatement.IdentifierName.Name] = currentRegion; - } - } - else - { - mapRegions = null; - return false; - } - } - else if (string.Equals(setStatement.IdentifierName.Name, "we", StringComparison.Ordinal) && - string.Equals(invocationExpression.IdentifierName.Name, "AddWeatherEffect", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 2 && - invocationExpression.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax regionVariableReferenceExpression && - invocationExpression.Arguments.Arguments[1] is JassFourCCLiteralExpressionSyntax fourCCLiteralExpression && - regions.TryGetValue(regionVariableReferenceExpression.IdentifierName.Name, out var region)) - { - region.WeatherType = (WeatherType)fourCCLiteralExpression.Value.InvertEndianness(); - } - else - { - mapRegions = null; - return false; - } - } - else - { - continue; - } - } - else - { - continue; - } - } - else if (statement is JassCallStatementSyntax callStatement) + if (match != null) + { + var region = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (region != null) { - if (string.Equals(callStatement.IdentifierName.Name, "SetSoundPosition", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax soundVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y) && - currentRegion is not null && - currentRegion.CenterX == x && - currentRegion.CenterY == y && - (string.IsNullOrEmpty(currentRegion.AmbientSound) || - string.Equals(currentRegion.AmbientSound, soundVariableReferenceExpression.IdentifierName.Name, StringComparison.Ordinal))) - { - currentRegion.AmbientSound = soundVariableReferenceExpression.IdentifierName.Name; - } - else - { - mapRegions = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "RegisterStackedSound", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax soundVariableReferenceExpression && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var rectWidth) && - callStatement.Arguments.Arguments[3].TryGetRealExpressionValue(out var rectHeight) && - currentRegion is not null && - currentRegion.Width == rectWidth && - currentRegion.Height == rectHeight && - (string.IsNullOrEmpty(currentRegion.AmbientSound) || - string.Equals(currentRegion.AmbientSound, soundVariableReferenceExpression.IdentifierName.Name, StringComparison.Ordinal))) - { - currentRegion.AmbientSound = soundVariableReferenceExpression.IdentifierName.Name; - } - else - { - mapRegions = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "EnableWeatherEffect", StringComparison.Ordinal)) + if (region.CenterX == match.x && region.CenterY == match.y) { - continue; - } - else - { - mapRegions = null; - return false; + region.AmbientSound = match.VariableName; + Context.HandledStatements.Add(input.Statement); } } - else - { - mapRegions = null; - return false; - } } + } - if (regions.Any()) + [RegisterStatementParser] + internal void ParseRegisterStackedSound(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "RegisterStackedSound") + .SafeMapFirst(x => { - mapRegions = new MapRegions(formatVersion); - mapRegions.Regions.AddRange(createdRegions); - return true; - } + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Width = x.Arguments.Arguments[2].GetValueOrDefault(), + Height = x.Arguments.Arguments[3].GetValueOrDefault() + }; + }); - mapRegions = null; - return false; + if (match != null) + { + var region = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (region != null) + { + if (region.Width == match.Width && region.Height == match.Height) + { + region.AmbientSound = match.VariableName; + Context.HandledStatements.Add(input.Statement); + } + } + } } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/FunctionDeclarationContext.cs b/src/War3Net.CodeAnalysis.Decompilers/FunctionDeclarationContext.cs index 7a3b155e..54d760f0 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/FunctionDeclarationContext.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/FunctionDeclarationContext.cs @@ -12,7 +12,7 @@ namespace War3Net.CodeAnalysis.Decompilers { - internal sealed class FunctionDeclarationContext + public sealed class FunctionDeclarationContext { public FunctionDeclarationContext(JassFunctionDeclarationSyntax functionDeclaration, IEnumerable comments) { diff --git a/src/War3Net.CodeAnalysis.Decompilers/JassDecompilerExtensions.cs b/src/War3Net.CodeAnalysis.Decompilers/JassDecompilerExtensions.cs new file mode 100644 index 00000000..0bc6eb80 --- /dev/null +++ b/src/War3Net.CodeAnalysis.Decompilers/JassDecompilerExtensions.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------------ +// +// Licensed under the MIT license. +// See the LICENSE file in the project root for more information. +// +// ------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace War3Net.CodeAnalysis.Decompilers +{ + public static class JassDecompilerExtensions + { + public static T2 OneOf(this IEnumerable tokens, params Func, T2>[] mapping) where T2 : class + { + foreach (var map in mapping) + { + try + { + var result = map(tokens); + if (result != null) + { + return result; + } + } + catch + { + //swallow exceptions + } + } + + return default; + } + + public static Nullable_Class OneOf_Struct(this IEnumerable tokens, params Func, Nullable_Class>[] mapping) where T2 : struct + { + return OneOf(tokens, mapping); + } + + public static T2 SafeMapFirst(this IEnumerable tokens, Func mapping) where T2 : class + { + return tokens.Select(x => { + try + { + return mapping(x); + } + catch + { + //swallow exceptions + } + + return default; + }).Where(x => x != null).FirstOrDefault(); + } + + public static Nullable_Class SafeMapFirst_Struct(this IEnumerable tokens, Func> mapping) where T2 : struct + { + return SafeMapFirst(tokens, mapping); + } + } +} \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompiler.cs b/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompiler.cs index 1ccf7fe4..c4e5a843 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompiler.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompiler.cs @@ -1,90 +1,146 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// // ------------------------------------------------------------------------------ -using System; using System.Collections.Generic; +using System.Linq; +using System; +using System.Reflection; +using War3Net.Build.Environment; using War3Net.Build; -using War3Net.Build.Script; using War3Net.CodeAnalysis.Jass.Syntax; +using War3Net.Build.Audio; +using War3Net.Build.Widget; +using War3Net.Build.Info; namespace War3Net.CodeAnalysis.Decompilers { public partial class JassScriptDecompiler { - public JassScriptDecompiler(Map map) - : this(map, null, null) + [AttributeUsage(AttributeTargets.Method)] + private class RegisterStatementParserAttribute : Attribute { } - public JassScriptDecompiler(Map map, Campaign? campaign) - : this(map, campaign, null) + internal class StatementParserInput { + public IStatementLineSyntax Statement; + public List StatementChildren; } - public JassScriptDecompiler(Map map, TriggerData? triggerData) - : this(map, null, triggerData) + private static List _statementParsers; + static JassScriptDecompiler() { + _statementParsers = typeof(JassScriptDecompiler) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(m => m.GetCustomAttributes(typeof(RegisterStatementParserAttribute), false).Any()).ToList(); } - public JassScriptDecompiler(Map map, Campaign? campaign, TriggerData? triggerData) + public DecompilationContext Context { get; } + + public JassScriptDecompiler(JassCompilationUnitSyntax compilationUnit, DecompileOptions options = null, MapInfo mapInfo = null) { - Context = new DecompilationContext(map, campaign, triggerData); + Context = new DecompilationContext(compilationUnit, options, mapInfo); } - internal DecompilationContext Context { get; } - - private FunctionDeclarationContext? GetFunction(string functionName) + private void ProcessStatementParsers(IStatementLineSyntax statement, IEnumerable> statementParsers) { - if (Context.FunctionDeclarations.TryGetValue(functionName, out var functionDeclaration)) + var input = new StatementParserInput() { Statement = statement, StatementChildren = statement.GetChildren_RecursiveDepthFirst().ToList() }; + foreach (var parser in statementParsers) { - if (functionDeclaration.Handled) + try + { + parser(input); + if (Context.HandledStatements.Contains(statement)) + { + break; + } + } + catch { - throw new ArgumentException("Function has already been handled.", nameof(functionName)); + //swallow exceptions } + } + } + private FunctionDeclarationContext? GetFunction(string functionName) + { + if (Context.FunctionDeclarations.TryGetValue(functionName, out var functionDeclaration)) + { return functionDeclaration; } return null; } - private IEnumerable GetCandidateFunctions(string? expectedFunctionName = null) + public List GetFunctionStatements_EnteringCalls(string startingFunctionName) { - if (Context.FunctionDeclarations.TryGetValue("main", out var mainFunction)) + var functionDeclaration = Context.FunctionDeclarations.GetValueOrDefault(startingFunctionName); + if (functionDeclaration == null || functionDeclaration.Handled) { - if (!string.IsNullOrEmpty(expectedFunctionName) && Context.FunctionDeclarations.TryGetValue(expectedFunctionName, out var expectedFunction)) - { - if (expectedFunction.Handled) - { - throw new ArgumentException("Expected function has already been handled.", nameof(expectedFunctionName)); - } + return new List(); + } - yield return expectedFunction; + functionDeclaration.Handled = true; + var result = new List(); + foreach (var child in functionDeclaration.FunctionDeclaration.GetChildren_RecursiveDepthFirst()) + { + if (child is IStatementLineSyntax statement) + { + result.Add(statement); } - foreach (var statement in mainFunction.FunctionDeclaration.Body.Statements) + if (child is JassFunctionReferenceExpressionSyntax functionReference) { - if (statement is JassCallStatementSyntax callStatement && callStatement.Arguments.Arguments.IsEmpty) + result.AddRange(GetFunctionStatements_EnteringCalls(functionReference.IdentifierName.Name)); + } + else if (child is JassCallStatementSyntax callStatement) + { + if (string.Equals(callStatement.IdentifierName.Name, "ExecuteFunc", StringComparison.InvariantCultureIgnoreCase) && callStatement.Arguments.Arguments.FirstOrDefault() is JassStringLiteralExpressionSyntax execFunctionName) + { + result.AddRange(GetFunctionStatements_EnteringCalls(execFunctionName.Value)); + } + else { - if (string.Equals(callStatement.IdentifierName.Name, expectedFunctionName, StringComparison.Ordinal)) - { - continue; - } - - if (Context.FunctionDeclarations.TryGetValue(callStatement.IdentifierName.Name, out var candidateFunction) && - candidateFunction.IsActionsFunction && - !candidateFunction.Handled) - { - yield return candidateFunction; - } + result.AddRange(GetFunctionStatements_EnteringCalls(callStatement.IdentifierName.Name)); } } } + + return result.ToList(); + } + + public Map DecompileObjectManagerData() + { + if (Context.MapInfo?.ScriptLanguage == ScriptLanguage.Lua) + { + throw new Exception("Lua decompilation is not supported yet"); + } + + //todo: Run through Jass=>Lua transpiler, then run with NLua with native polyfills & method execution logging to better handle obfuscated code & to re-use the same logic for both Jass & Lua Decompilation (still need parser to direct interpreter what functions to execute for non-iterative patterns like TriggerRegisterUnitEvent for unit death item drop tables). + + var actions = _statementParsers.Select(parser => (Action)((StatementParserInput input) => parser.Invoke(this, new[] { input }))).ToList(); + + foreach (var function in Context.FunctionDeclarations) + { + function.Value.Handled = false; + } + var statements = GetFunctionStatements_EnteringCalls("config").Concat(GetFunctionStatements_EnteringCalls("main")).ToList(); + foreach (var statement in statements) + { + ProcessStatementParsers(statement, actions); + } + + var map = new Map() { Info = Context.MapInfo }; + map.Cameras = new MapCameras(Context.Options.mapCamerasFormatVersion, Context.Options.mapCamerasUseNewFormat) { Cameras = Context.GetAll().ToList() }; + map.Regions = new MapRegions(Context.Options.mapRegionsFormatVersion) { Regions = Context.GetAll().ToList() }; + map.Sounds = new MapSounds(Context.Options.mapSoundsFormatVersion) { Sounds = Context.GetAll().ToList() }; + map.Units = new MapUnits(Context.Options.mapWidgetsFormatVersion, Context.Options.mapWidgetsSubVersion, Context.Options.mapWidgetsUseNewFormat) { Units = Context.GetAll().ToList() }; + map.Doodads = new MapDoodads(Context.Options.mapWidgetsFormatVersion, Context.Options.mapWidgetsSubVersion, Context.Options.mapWidgetsUseNewFormat) { Doodads = Context.GetAll().ToList(), SpecialDoodads = Context.GetAll().ToList(), SpecialDoodadVersion = Context.Options.specialDoodadVersion }; + return map; } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompilerHelpers.cs b/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompilerHelpers.cs new file mode 100644 index 00000000..4f59fc44 --- /dev/null +++ b/src/War3Net.CodeAnalysis.Decompilers/JassScriptDecompilerHelpers.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------------ +// +// Licensed under the MIT license. +// See the LICENSE file in the project root for more information. +// +// ------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +using War3Net.Build.Audio; +using War3Net.Build.Info; +using War3Net.CodeAnalysis.Jass.Syntax; + +namespace War3Net.CodeAnalysis.Decompilers +{ + public partial class JassScriptDecompiler + { + private int? GetPlayerIndex(IJassSyntaxToken parameter) + { + var lastCreatedPlayerId = Context.GetLastCreated()?.Id; + + if (parameter is JassVariableReferenceExpressionSyntax variableReference) + { + if (string.Equals(variableReference.IdentifierName.Name, "PLAYER_NEUTRAL_AGGRESSIVE", StringComparison.Ordinal)) + { + return Context.MaxPlayerSlots; + } + else if (string.Equals(variableReference.IdentifierName.Name, "PLAYER_NEUTRAL_PASSIVE", StringComparison.Ordinal)) + { + return Context.MaxPlayerSlots + 3; + } + else + { + return Context.Get_Struct(Context.CreatePseudoVariableName(nameof(ParsePlayerIndex), variableReference.IdentifierName.Name)) ?? + Context.Get_Struct(Context.CreatePseudoVariableName(nameof(ParsePlayerIndex))) ?? lastCreatedPlayerId; + } + } + else if (parameter is IInvocationSyntax playerInvocationSyntax && playerInvocationSyntax.IdentifierName.Name == "Player") + { + if (playerInvocationSyntax.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax variableReferenceExpression) + { + if (string.Equals(variableReferenceExpression.IdentifierName.Name, "PLAYER_NEUTRAL_AGGRESSIVE", StringComparison.Ordinal)) + { + return Context.MaxPlayerSlots; + } + else if (string.Equals(variableReferenceExpression.IdentifierName.Name, "PLAYER_NEUTRAL_PASSIVE", StringComparison.Ordinal)) + { + return Context.MaxPlayerSlots + 3; + } + } + else if (playerInvocationSyntax.Arguments.Arguments[0].TryGetValue(out var playerId)) + { + return playerId; + } + } + + return lastCreatedPlayerId; + } + + private string GetVariableAssignment(IEnumerable statementChildren) + { + return statementChildren.OneOf(x => x.OfType().SafeMapFirst(x => x.IdentifierName.Name), + x => x.OfType().SafeMapFirst(x => x.IdentifierName.Name)); + } + + private SoundFlags ParseSoundFlags(ImmutableArray arguments, string filePath = default) + { + var flags = (SoundFlags)0; + if (((JassBooleanLiteralExpressionSyntax)arguments[1]).Value) + { + flags |= SoundFlags.Looping; + } + if (((JassBooleanLiteralExpressionSyntax)arguments[2]).Value) + { + flags |= SoundFlags.Is3DSound; + } + if (((JassBooleanLiteralExpressionSyntax)arguments[3]).Value) + { + flags |= SoundFlags.StopWhenOutOfRange; + } + + if ((flags & SoundFlags.Is3DSound) == SoundFlags.Is3DSound && !IsInternalSound(filePath ?? "")) + { + flags |= SoundFlags.UNK16; + } + + return flags; + } + + [Obsolete] + private static bool IsInternalSound(string filePath) + { + return filePath.StartsWith(@"Sound\", StringComparison.OrdinalIgnoreCase) + || filePath.StartsWith(@"Sound/", StringComparison.OrdinalIgnoreCase) + || filePath.StartsWith(@"UI\", StringComparison.OrdinalIgnoreCase) + || filePath.StartsWith(@"UI/", StringComparison.OrdinalIgnoreCase) + || filePath.StartsWith(@"Units\", StringComparison.OrdinalIgnoreCase) + || filePath.StartsWith(@"Units/", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/Nullable_Class.cs b/src/War3Net.CodeAnalysis.Decompilers/Nullable_Class.cs new file mode 100644 index 00000000..bacf6e32 --- /dev/null +++ b/src/War3Net.CodeAnalysis.Decompilers/Nullable_Class.cs @@ -0,0 +1,87 @@ +// ------------------------------------------------------------------------------ +// +// Licensed under the MIT license. +// See the LICENSE file in the project root for more information. +// +// ------------------------------------------------------------------------------ + +using System; + +namespace War3Net.CodeAnalysis.Decompilers +{ + public class Nullable_Class where T : struct + { + private Nullable _value { get; } + public T Value + { + get + { + return _value.Value; + } + } + + public Nullable_Class(T value) + : base() + { + _value = new Nullable(value); + } + + public bool HasValue + { + get + { + return _value.HasValue; + } + } + + public T GetValueOrDefault(T defaultValue = default) + { + return _value ?? defaultValue; + } + + public override string ToString() + { + return _value?.ToString(); + } + + public override bool Equals(object? other) + { + return _value.Equals(other); + } + + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + public static implicit operator Nullable_Class(T value) + { + return new Nullable_Class(value); + } + + public static explicit operator T(Nullable_Class value) + { + return value.Value; + } + + public static implicit operator Nullable_Class(Nullable value) + { + if (!value.HasValue) + { + return null; + } + + return new Nullable_Class(value.Value); + } + + public static implicit operator Nullable(Nullable_Class value) + { + if (value == null || !value.HasValue) + { + return null; + } + + return new Nullable(value.Value); + } + } +} \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/ObjectDataContext.cs b/src/War3Net.CodeAnalysis.Decompilers/ObjectDataContext.cs index 8a63c6fc..b86f9be1 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/ObjectDataContext.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/ObjectDataContext.cs @@ -11,13 +11,22 @@ namespace War3Net.CodeAnalysis.Decompilers { - internal sealed class ObjectDataContext + public sealed class ObjectDataContext { + /* + public readonly Map map; + public readonly Campaign campaign; + */ + public ObjectDataContext(Map map, Campaign? campaign) { - // TODO + /* + this.map = map; + this.campaign = campaign; + */ } + /* public ImmutableHashSet KnownUnitIds { get; set; } public ImmutableHashSet KnownItemIds { get; set; } @@ -35,5 +44,6 @@ public ObjectDataContext(Map map, Campaign? campaign) public ImmutableHashSet KnownTechIds { get; set; } public ImmutableHashSet KnownObjectIds { get; set; } + */ } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Decompilers/VariableDeclarationContext.cs b/src/War3Net.CodeAnalysis.Decompilers/VariableDeclarationContext.cs index d37bb87e..95bc1ef5 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/VariableDeclarationContext.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/VariableDeclarationContext.cs @@ -10,7 +10,7 @@ namespace War3Net.CodeAnalysis.Decompilers { - internal sealed class VariableDeclarationContext + public sealed class VariableDeclarationContext { public VariableDeclarationContext(JassGlobalDeclarationSyntax globalDeclaration) { diff --git a/src/War3Net.CodeAnalysis.Decompilers/Widget/MapUnitsDecompiler.cs b/src/War3Net.CodeAnalysis.Decompilers/Widget/MapUnitsDecompiler.cs index aa480b99..25b0129d 100644 --- a/src/War3Net.CodeAnalysis.Decompilers/Widget/MapUnitsDecompiler.cs +++ b/src/War3Net.CodeAnalysis.Decompilers/Widget/MapUnitsDecompiler.cs @@ -1,15 +1,15 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. -// // ------------------------------------------------------------------------------ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using War3Net.Build.Common; +using War3Net.Build.Environment; using War3Net.Build.Widget; using War3Net.CodeAnalysis.Jass.Extensions; using War3Net.CodeAnalysis.Jass.Syntax; @@ -19,734 +19,619 @@ namespace War3Net.CodeAnalysis.Decompilers { public partial class JassScriptDecompiler { - private int CreationNumber = 0; - - public bool TryDecompileMapUnits( - MapWidgetsFormatVersion formatVersion, - MapWidgetsSubVersion subVersion, - bool useNewFormat, - [NotNullWhen(true)] out MapUnits? mapUnits) + [RegisterStatementParser] + internal void ParseUnitCreation(StatementParserInput input) { - var createAllUnits = GetFunction("CreateAllUnits"); - var createAllItems = GetFunction("CreateAllItems"); - var config = GetFunction("config"); - var initCustomPlayerSlots = GetFunction("InitCustomPlayerSlots"); - - if (createAllUnits is null || - createAllItems is null || - config is null || - initCustomPlayerSlots is null) - { - mapUnits = null; - return false; - } + var variableAssignment = GetVariableAssignment(input.StatementChildren); + var unitData = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CreateUnit" || x.IdentifierName.Name == "BlzCreateUnitWithSkin").SafeMapFirst(x => + { + var args = x.Arguments.Arguments; - if (TryDecompileMapUnits( - createAllUnits.FunctionDeclaration, - createAllItems.FunctionDeclaration, - config.FunctionDeclaration, - initCustomPlayerSlots.FunctionDeclaration, - formatVersion, - subVersion, - useNewFormat, - out mapUnits)) - { - createAllUnits.Handled = true; - createAllItems.Handled = true; - initCustomPlayerSlots.Handled = true; - - return true; - } + int? ownerId = GetPlayerIndex(args[0]); + + if (ownerId == null) + { + return null; + } - mapUnits = null; - return false; + if (!args[1].TryGetValue(out var typeId)) + { + return null; + } + + var result = new UnitData + { + OwnerId = ownerId.Value, + TypeId = typeId.InvertEndianness(), + Position = new Vector3( + args[2].GetValueOrDefault(), + args[3].GetValueOrDefault(), + 0f + ), + Rotation = args[4].GetValueOrDefault() * (MathF.PI / 180f), + Scale = Vector3.One, + Flags = 2, + GoldAmount = 12500, + HeroLevel = 1, + CreationNumber = Context.GetNextCreationNumber() + }; + + result.SkinId = args.Length > 5 ? args[5].GetValueOrDefault() : result.TypeId; + return result; + }); + + if (unitData != null) + { + Context.HandledStatements.Add(input.Statement); + Context.Add(unitData, variableAssignment); + } } - public bool TryDecompileMapUnits( - JassFunctionDeclarationSyntax createAllUnitsFunction, - JassFunctionDeclarationSyntax createAllItemsFunction, - JassFunctionDeclarationSyntax configFunction, - JassFunctionDeclarationSyntax initCustomPlayerSlotsFunction, - MapWidgetsFormatVersion formatVersion, - MapWidgetsSubVersion subVersion, - bool useNewFormat, - [NotNullWhen(true)] out MapUnits? mapUnits) + [RegisterStatementParser] + internal void ParsePlayerIndex(StatementParserInput input) { - if (createAllUnitsFunction is null) + var variableAssignment = GetVariableAssignment(input.StatementChildren); + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "Player").SafeMapFirst(x => { - throw new ArgumentNullException(nameof(createAllUnitsFunction)); - } + if (!x.Arguments.Arguments[0].TryGetValue(out var playerId)) + { + return null; + } - if (createAllItemsFunction is null) + return new + { + PlayerIndex = playerId + }; + }); + + if (match != null && variableAssignment != null) { - throw new ArgumentNullException(nameof(createAllItemsFunction)); + Context.Add_Struct(match.PlayerIndex, Context.CreatePseudoVariableName(nameof(ParsePlayerIndex), variableAssignment)); + Context.Add_Struct(match.PlayerIndex, Context.CreatePseudoVariableName(nameof(ParsePlayerIndex))); + Context.HandledStatements.Add(input.Statement); } + } - if (configFunction is null) + [RegisterStatementParser] + internal void ParseResourceAmount(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetResourceAmount").SafeMapFirst(x => { - throw new ArgumentNullException(nameof(configFunction)); - } + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Amount = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); - if (initCustomPlayerSlotsFunction is null) + if (match != null) { - throw new ArgumentNullException(nameof(initCustomPlayerSlotsFunction)); + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + unit.GoldAmount = match.Amount; + Context.HandledStatements.Add(input.Statement); + } } + } - if (TryDecompileCreateUnitsFunction(createAllUnitsFunction, out var units) && - TryDecompileCreateItemsFunction(createAllItemsFunction, out var items) && - TryDecompileStartLocationPositionsConfigFunction(configFunction, out var startLocationPositions) && - TryDecompileInitCustomPlayerSlotsFunction(initCustomPlayerSlotsFunction, startLocationPositions, out var startLocations)) + [RegisterStatementParser] + internal void ParseSetUnitColor(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetUnitColor").SafeMapFirst(x => { - mapUnits = new MapUnits(formatVersion, subVersion, useNewFormat); - - mapUnits.Units.AddRange(units); - mapUnits.Units.AddRange(items); - mapUnits.Units.AddRange(startLocations); + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + CustomPlayerColorId = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); - return true; + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + unit.CustomPlayerColorId = match.CustomPlayerColorId; + Context.HandledStatements.Add(input.Statement); + } } - - mapUnits = null; - return false; } - private bool TryDecompileCreateUnitsFunction(JassFunctionDeclarationSyntax createUnitsFunction, [NotNullWhen(true)] out List? units) + [RegisterStatementParser] + internal void ParseAcquireRange(StatementParserInput input) { - var localPlayerVariableName = (string?)null; - var localPlayerVariableValue = (int?)null; - - var result = new List(); + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "SetUnitAcquireRange") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Range = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); - foreach (var statement in createUnitsFunction.Body.Statements) + if (match != null) { - if (statement is JassCommentSyntax || - statement is JassEmptySyntax) + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) { - continue; + unit.TargetAcquisition = match.Range; + Context.HandledStatements.Add(input.Statement); } - else if (statement is JassLocalVariableDeclarationStatementSyntax localVariableDeclarationStatement) + } + } + + [RegisterStatementParser] + internal void ParseUnitState(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "SetUnitState") + .SafeMapFirst(x => + { + return new { - var typeName = localVariableDeclarationStatement.Declarator.Type.TypeName.Name; + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + StateType = ((JassVariableReferenceExpressionSyntax)x.Arguments.Arguments[1]).IdentifierName.Name, + Value = x.Arguments.Arguments[2].GetValueOrDefault() + }; + }); - if (string.Equals(typeName, "player", StringComparison.Ordinal)) - { - if (localVariableDeclarationStatement.Declarator is JassVariableDeclaratorSyntax variableDeclarator && - variableDeclarator.Value is not null && - variableDeclarator.Value.Expression is JassInvocationExpressionSyntax playerInvocationExpression && - string.Equals(playerInvocationExpression.IdentifierName.Name, "Player", StringComparison.Ordinal) && - playerInvocationExpression.Arguments.Arguments.Length == 1 && - playerInvocationExpression.Arguments.Arguments[0].TryGetPlayerIdExpressionValue(Context.MaxPlayerSlots, out var playerId)) - { - localPlayerVariableName = variableDeclarator.IdentifierName.Name; - localPlayerVariableValue = playerId; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(typeName, "unit", StringComparison.Ordinal) || - string.Equals(typeName, "integer", StringComparison.Ordinal) || - string.Equals(typeName, "trigger", StringComparison.Ordinal) || - string.Equals(typeName, "real", StringComparison.Ordinal)) + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + if (match.StateType == "UNIT_STATE_LIFE") { - // TODO - - if (localVariableDeclarationStatement.Declarator is not JassVariableDeclaratorSyntax variableDeclarator || - variableDeclarator.Value is not null) - { - units = null; - return false; - } + unit.HP = (int)match.Value; + Context.HandledStatements.Add(input.Statement); } - else + else if (match.StateType == "UNIT_STATE_MANA") { - units = null; - return false; + unit.MP = (int)match.Value; + Context.HandledStatements.Add(input.Statement); } } - else if (statement is JassSetStatementSyntax setStatement) + } + } + + [RegisterStatementParser] + internal void ParseUnitInventory(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "UnitAddItemToSlotById") + .SafeMapFirst(x => + { + return new { - if (setStatement.Indexer is null) - { - if (setStatement.Value.Expression is JassInvocationExpressionSyntax invocationExpression) - { - if (string.Equals(invocationExpression.IdentifierName.Name, "CreateUnit", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 5 && - invocationExpression.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax playerVariableReferenceExpression && - invocationExpression.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var unitId) && - invocationExpression.Arguments.Arguments[2].TryGetRealExpressionValue(out var x) && - invocationExpression.Arguments.Arguments[3].TryGetRealExpressionValue(out var y) && - invocationExpression.Arguments.Arguments[4].TryGetRealExpressionValue(out var face) && - string.Equals(playerVariableReferenceExpression.IdentifierName.Name, localPlayerVariableName, StringComparison.Ordinal)) - { - var unit = new UnitData - { - OwnerId = localPlayerVariableValue.Value, - TypeId = unitId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = face * (MathF.PI / 180f), - Scale = Vector3.One, - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - unit.SkinId = unit.TypeId; - - result.Add(unit); - } - else - { - units = null; - return false; - } - } - else if (string.Equals(invocationExpression.IdentifierName.Name, "BlzCreateUnitWithSkin", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 6 && - invocationExpression.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax playerVariableReferenceExpression && - invocationExpression.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var unitId) && - invocationExpression.Arguments.Arguments[2].TryGetRealExpressionValue(out var x) && - invocationExpression.Arguments.Arguments[3].TryGetRealExpressionValue(out var y) && - invocationExpression.Arguments.Arguments[4].TryGetRealExpressionValue(out var face) && - invocationExpression.Arguments.Arguments[5].TryGetIntegerExpressionValue(out var skinId) && - string.Equals(playerVariableReferenceExpression.IdentifierName.Name, localPlayerVariableName, StringComparison.Ordinal)) - { - var unit = new UnitData - { - OwnerId = localPlayerVariableValue.Value, - TypeId = unitId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = face * (MathF.PI / 180f), - Scale = Vector3.One, - SkinId = skinId.InvertEndianness(), - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - result.Add(unit); - } - else - { - units = null; - return false; - } - } - else if (string.Equals(invocationExpression.IdentifierName.Name, "CreateTrigger", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(invocationExpression.IdentifierName.Name, "GetUnitState", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(invocationExpression.IdentifierName.Name, "RandomDistChoose", StringComparison.Ordinal)) - { - // TODO - continue; - } - else - { - units = null; - return false; - } - } - else if (setStatement.Value.Expression is JassArrayReferenceExpressionSyntax) - { - // TODO - continue; - } - else - { - units = null; - return false; - } - } - else + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + ItemId = x.Arguments.Arguments[1].GetValueOrDefault(), + Slot = x.Arguments.Arguments[2].GetValueOrDefault() + }; + }); + + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + unit.InventoryData.Add(new InventoryItemData { - units = null; - return false; - } + ItemId = match.ItemId, + Slot = match.Slot + }); + Context.HandledStatements.Add(input.Statement); } - else if (statement is JassCallStatementSyntax callStatement) + } + } + + [RegisterStatementParser] + internal void ParseHeroLevel(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "SetHeroLevel").SafeMapFirst(x => + { + return new { - if (string.Equals(callStatement.IdentifierName.Name, "SetResourceAmount", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var amount)) - { - result[^1].GoldAmount = amount; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetUnitColor", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassInvocationExpressionSyntax convertPlayerColorInvocationExpression && - string.Equals(convertPlayerColorInvocationExpression.IdentifierName.Name, "ConvertPlayerColor", StringComparison.Ordinal) && - convertPlayerColorInvocationExpression.Arguments.Arguments.Length == 1 && - convertPlayerColorInvocationExpression.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var playerColorId)) - { - result[^1].CustomPlayerColorId = playerColorId; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetUnitAcquireRange", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var acquireRange)) - { - const float CampAcquireRange = 200f; - result[^1].TargetAcquisition = acquireRange == CampAcquireRange ? -2f : acquireRange; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetUnitState", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1] is JassVariableReferenceExpressionSyntax unitStateVariableReferenceExpression) - { - if (string.Equals(unitStateVariableReferenceExpression.IdentifierName.Name, "UNIT_STATE_LIFE", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments[2] is JassBinaryExpressionSyntax binaryExpression && - binaryExpression.Left.TryGetRealExpressionValue(out var hp) && - binaryExpression.Operator == BinaryOperatorType.Multiplication && - binaryExpression.Right is JassVariableReferenceExpressionSyntax) - { - result[^1].HP = (int)(100 * hp); - } - else - { - units = null; - return false; - } - } - else if (string.Equals(unitStateVariableReferenceExpression.IdentifierName.Name, "UNIT_STATE_MANA", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments[2].TryGetIntegerExpressionValue(out var mp)) - { - result[^1].MP = mp; - } - else - { - units = null; - return false; - } - } - else - { - units = null; - return false; - } - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "UnitAddItemToSlotById", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var itemId) && - callStatement.Arguments.Arguments[2].TryGetIntegerExpressionValue(out var slot)) - { - result[^1].InventoryData.Add(new InventoryItemData - { - ItemId = itemId, - Slot = slot, - }); - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetHeroLevel", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var level) && - callStatement.Arguments.Arguments[2] is JassBooleanLiteralExpressionSyntax) - { - result[^1].HeroLevel = level; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetHeroStr", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var value) && - callStatement.Arguments.Arguments[2] is JassBooleanLiteralExpressionSyntax) - { - result[^1].HeroStrength = value; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetHeroAgi", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var value) && - callStatement.Arguments.Arguments[2] is JassBooleanLiteralExpressionSyntax) - { - result[^1].HeroAgility = value; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SetHeroInt", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0] is JassVariableReferenceExpressionSyntax unitVariableReferenceExpression && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var value) && - callStatement.Arguments.Arguments[2] is JassBooleanLiteralExpressionSyntax) - { - result[^1].HeroIntelligence = value; - } - else - { - units = null; - return false; - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "SelectHeroSkill", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "IssueImmediateOrder", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "RandomDistReset", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "RandomDistAddItem", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "TriggerRegisterUnitEvent", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (string.Equals(callStatement.IdentifierName.Name, "TriggerAddAction", StringComparison.Ordinal)) - { - // TODO - continue; - } - else if (callStatement.Arguments.Arguments.IsEmpty) - { - if (Context.FunctionDeclarations.TryGetValue(callStatement.IdentifierName.Name, out var subFunction) && - TryDecompileCreateUnitsFunction(subFunction.FunctionDeclaration, out var subFunctionResult)) - { - result.AddRange(subFunctionResult); - } - else - { - units = null; - return false; - } - } - else - { - units = null; - return false; - } + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Level = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + unit.HeroLevel = match.Level; + Context.HandledStatements.Add(input.Statement); } - else if (statement is JassIfStatementSyntax ifStatement) + } + } + + [RegisterStatementParser] + internal void ParseHeroAttributes(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "SetHeroStr" || + x.IdentifierName.Name == "SetHeroAgi" || + x.IdentifierName.Name == "SetHeroInt") + .SafeMapFirst(x => + { + return new { - if (ifStatement.Condition.Deparenthesize() is JassBinaryExpressionSyntax binaryExpression && - binaryExpression.Left is JassVariableReferenceExpressionSyntax && - binaryExpression.Operator == BinaryOperatorType.NotEquals && - binaryExpression.Right.TryGetIntegerExpressionValue(out var value) && - value == -1) + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + Attribute = x.IdentifierName.Name, + Value = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); + + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + var handled = true; + switch (match.Attribute) { - // TODO - continue; + case "SetHeroStr": + unit.HeroStrength = match.Value; + break; + case "SetHeroAgi": + unit.HeroAgility = match.Value; + break; + case "SetHeroInt": + unit.HeroIntelligence = match.Value; + break; + default: + handled = false; + break; } - else + + if (handled) { - units = null; - return false; + Context.HandledStatements.Add(input.Statement); } } - else + } + } + + [RegisterStatementParser] + internal void ParseHeroSkills(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "SelectHeroSkill") + .SafeMapFirst(x => + { + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + SkillId = x.Arguments.Arguments[1].GetValueOrDefault() + }; + }); + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) { - units = null; - return false; + unit.AbilityData.Add(new ModifiedAbilityData + { + AbilityId = match.SkillId, + HeroAbilityLevel = 1, + IsAutocastActive = false + }); + Context.HandledStatements.Add(input.Statement); } } - - units = result; - return true; } - private bool TryDecompileCreateItemsFunction(JassFunctionDeclarationSyntax createItemsFunction, [NotNullWhen(true)] out List? items) + [RegisterStatementParser] + internal void ParseIssueImmediateOrder(StatementParserInput input) { - var result = new List(); - - foreach (var statement in createItemsFunction.Body.Statements) + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "IssueImmediateOrder") + .SafeMapFirst(x => { - if (statement is JassCommentSyntax || - statement is JassEmptySyntax) + var abilityName = (x.Arguments.Arguments[1] as JassStringLiteralExpressionSyntax)?.Value; + bool? isAutoCastActive = null; + if (abilityName?.EndsWith("on", StringComparison.OrdinalIgnoreCase) == true) { - continue; + isAutoCastActive = true; } - else if (statement is JassSetStatementSyntax setStatement) + else if (abilityName?.EndsWith("off", StringComparison.OrdinalIgnoreCase) == true) { - if (setStatement.Value.Expression is JassInvocationExpressionSyntax invocationExpression) - { - if (string.Equals(invocationExpression.IdentifierName.Name, "CreateItem", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 3 && - invocationExpression.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var unitId) && - invocationExpression.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - invocationExpression.Arguments.Arguments[2].TryGetRealExpressionValue(out var y)) - { - var unit = new UnitData - { - OwnerId = Context.MaxPlayerSlots + 3, // NEUTRAL_PASSIVE - TypeId = unitId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = 0, - Scale = Vector3.One, - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - unit.SkinId = unit.TypeId; - - result.Add(unit); - } - } - else if (string.Equals(invocationExpression.IdentifierName.Name, "BlzCreateItemWithSkin", StringComparison.Ordinal)) - { - if (invocationExpression.Arguments.Arguments.Length == 4 && - invocationExpression.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var unitId) && - invocationExpression.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - invocationExpression.Arguments.Arguments[2].TryGetRealExpressionValue(out var y) && - invocationExpression.Arguments.Arguments[3].TryGetIntegerExpressionValue(out var skinId)) - { - var unit = new UnitData - { - OwnerId = Context.MaxPlayerSlots + 3, // NEUTRAL_PASSIVE - TypeId = unitId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = 0, - Scale = Vector3.One, - SkinId = skinId.InvertEndianness(), - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - result.Add(unit); - } - } - } + isAutoCastActive = false; } - else if (statement is JassCallStatementSyntax callStatement) + else { - if (string.Equals(callStatement.IdentifierName.Name, "CreateItem", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var itemId) && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y)) - { - var item = new UnitData - { - OwnerId = Context.MaxPlayerSlots + 3, // NEUTRAL_PASSIVE - TypeId = itemId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = 0, - Scale = Vector3.One, - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - item.SkinId = item.TypeId; - - result.Add(item); - } - } - else if (string.Equals(callStatement.IdentifierName.Name, "BlzCreateItemWithSkin", StringComparison.Ordinal)) + return null; + } + + return new + { + VariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + //AbilityName = abilityName, + IsAutocastActive = isAutoCastActive.Value + }; + }); + + if (match != null) + { + var unit = Context.Get(match.VariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + //todo: lookup abilityId by searching for name in _context.ObjectData.map.AbilityObjectData + //todo: lookup on/off via SLK Metadata instead of abilityName suffix [example: Modifications with FourCC aoro/aord are names for "on", aorf/aoru are names for "off"] + var ability = unit.AbilityData.LastOrDefault(); + if (ability != null) { - if (callStatement.Arguments.Arguments.Length == 4 && - callStatement.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var itemId) && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y) && - callStatement.Arguments.Arguments[3].TryGetIntegerExpressionValue(out var skinId)) - { - var item = new UnitData - { - OwnerId = Context.MaxPlayerSlots + 3, // NEUTRAL_PASSIVE - TypeId = itemId.InvertEndianness(), - Position = new Vector3(x, y, 0f), - Rotation = 0, - Scale = Vector3.One, - SkinId = skinId.InvertEndianness(), - Flags = 2, - GoldAmount = 12500, - HeroLevel = 1, - CreationNumber = CreationNumber++ - }; - - result.Add(item); - } + ability.IsAutocastActive = match.IsAutocastActive; + Context.HandledStatements.Add(input.Statement); } } } + } + + [RegisterStatementParser] + internal void ParseDropItemsOnDeath_TriggerRegister(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "TriggerRegisterUnitEvent") + .SafeMapFirst(x => + { + var eventName = (x.Arguments.Arguments[2] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name; + if (eventName != "EVENT_UNIT_DEATH" && eventName != "EVENT_UNIT_CHANGE_OWNER") + { + return null; + } - items = result; - return true; + return new + { + TriggerVariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + UnitVariableName = (x.Arguments.Arguments[1] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name + }; + }); + + if (match != null) + { + Context.Add(match.UnitVariableName, Context.CreatePseudoVariableName(nameof(ParseDropItemsOnDeath_TriggerRegister), match.TriggerVariableName)); + Context.HandledStatements.Add(input.Statement); + } } - private bool TryDecompileStartLocationPositionsConfigFunction(JassFunctionDeclarationSyntax configFunction, [NotNullWhen(true)] out Dictionary? startLocationPositions) + [RegisterStatementParser] + internal void ParseDropItemsOnDeath_TriggerAction(StatementParserInput input) { - var result = new Dictionary(); + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "TriggerAddAction") + .SafeMapFirst(x => + { + return new + { + TriggerVariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + FunctionName = (x.Arguments.Arguments[1] as JassFunctionReferenceExpressionSyntax)?.IdentifierName?.Name + }; + }); - foreach (var statement in configFunction.Body.Statements) + if (match != null) { - if (statement is JassCallStatementSyntax callStatement && - string.Equals(callStatement.IdentifierName.Name, "DefineStartLocation", StringComparison.Ordinal)) + var unitVariableName = Context.Get(Context.CreatePseudoVariableName(nameof(ParseDropItemsOnDeath_TriggerRegister), match.TriggerVariableName)); + + if (match.FunctionName != null && Context.FunctionDeclarations.TryGetValue(match.FunctionName, out var function)) { - if (callStatement.Arguments.Arguments.Length == 3 && - callStatement.Arguments.Arguments[0].TryGetIntegerExpressionValue(out var index) && - callStatement.Arguments.Arguments[1].TryGetRealExpressionValue(out var x) && - callStatement.Arguments.Arguments[2].TryGetRealExpressionValue(out var y)) + var statements = function.FunctionDeclaration.GetChildren_RecursiveDepthFirst().OfType().ToList(); + foreach (var statement in statements) { - result.Add(index, new Vector2(x, y)); + ProcessStatementParsers(statement, new Action[] { + input => ParseRandomDistReset(input, unitVariableName), + input => ParseRandomDistAddItem(input, unitVariableName), + }); } - else + + Context.HandledStatements.Add(input.Statement); + } + } + } + + private void ParseRandomDistReset(StatementParserInput input, string unitVariableName) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "RandomDistReset") + .SafeMapFirst(x => + { + return new RandomItemSet(); + }); + + if (match != null) + { + var unit = Context.Get(unitVariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + unit.ItemTableSets.Add(match); + } + Context.Add(match); + Context.HandledStatements.Add(input.Statement); + } + } + + private void ParseRandomDistAddItem(StatementParserInput input, string unitVariableName) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "RandomDistAddItem") + .SafeMapFirst(x => + { + return new RandomItemSetItem() + { + ItemId = x.Arguments.Arguments[0].GetValueOrDefault().InvertEndianness(), + Chance = x.Arguments.Arguments[1].GetValueOrDefault(), + }; + }); + + if (match != null) + { + var unit = Context.Get(unitVariableName) ?? Context.GetLastCreated(); + if (unit != null) + { + var itemSet = unit.ItemTableSets.LastOrDefault(); + if (itemSet != null) { - startLocationPositions = null; - return false; + itemSet.Items.Add(match); + Context.HandledStatements.Add(input.Statement); } } - else + } + } + + [RegisterStatementParser] + internal void ParseItemCreation(StatementParserInput input) + { + var variableAssignment = GetVariableAssignment(input.StatementChildren); + var unitData = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "CreateItem" || x.IdentifierName.Name == "BlzCreateItemWithSkin").SafeMapFirst(x => + { + var args = x.Arguments.Arguments; + + if (!args[0].TryGetValue(out var typeId)) { - continue; + return null; } + + var result = new UnitData + { + OwnerId = Context.MaxPlayerSlots + 3, // NEUTRAL_PASSIVE + TypeId = typeId.InvertEndianness(), + Position = new Vector3( + args[1].GetValueOrDefault(), + args[2].GetValueOrDefault(), + 0f + ), + Rotation = 0, + Scale = Vector3.One, + Flags = 2, + GoldAmount = 12500, + HeroLevel = 1, + CreationNumber = Context.GetNextCreationNumber() + }; + + result.SkinId = args.Length > 3 ? args[3].GetValueOrDefault() : result.TypeId; + + return result; + }); + + if (unitData != null) + { + Context.Add(unitData, variableAssignment); + Context.HandledStatements.Add(input.Statement); } + } + + [RegisterStatementParser] + internal void ParseDefineStartLocation(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "DefineStartLocation") + .SafeMapFirst(x => + { + return new + { + Index = x.Arguments.Arguments[0].GetValueOrDefault(), + Location = new Vector2(x.Arguments.Arguments[1].GetValueOrDefault(), x.Arguments.Arguments[2].GetValueOrDefault()) + }; + }); - startLocationPositions = result; - return true; + if (match != null) + { + Context.Add_Struct(match.Location, Context.CreatePseudoVariableName(nameof(ParseDefineStartLocation), match.Index.ToString())); + Context.HandledStatements.Add(input.Statement); + } } - private bool TryDecompileInitCustomPlayerSlotsFunction( - JassFunctionDeclarationSyntax initCustomPlayerSlotsFunction, - Dictionary startLocationPositions, - [NotNullWhen(true)] out List? startLocations) + [RegisterStatementParser] + internal void ParseSetPlayerStartLocation(StatementParserInput input) { - var result = new List(); + var variableAssignment = GetVariableAssignment(input.StatementChildren); + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "SetPlayerStartLocation") + .SafeMapFirst(x => + { + int? playerId = GetPlayerIndex(x.Arguments.Arguments[0]); + + if (playerId == null) + { + return null; + } + + var startLocationIndex = x.Arguments.Arguments[1].GetValueOrDefault(); + var startLocationPosition = Context.Get_Struct(Context.CreatePseudoVariableName(nameof(ParseDefineStartLocation), startLocationIndex.ToString())); + + if (startLocationPosition == null) + { + return null; + } + + var args = x.Arguments.Arguments; + var result = new UnitData + { + OwnerId = playerId.Value, + TypeId = "sloc".FromRawcode(), + Position = new Vector3(startLocationPosition.Value, 0f), + Rotation = MathF.PI * 1.5f, + Scale = Vector3.One, + Flags = 2, + GoldAmount = 12500, + HeroLevel = 0, + TargetAcquisition = 0, + CreationNumber = Context.GetNextCreationNumber() + }; + + result.SkinId = result.TypeId; + + return result; + }); + + if (match != null) + { + Context.Add(match, variableAssignment); + Context.HandledStatements.Add(input.Statement); + } + } - foreach (var statement in initCustomPlayerSlotsFunction.Body.Statements) + [RegisterStatementParser] + internal void ParseWaygateDestination(StatementParserInput input) + { + var match = input.StatementChildren.OfType().Where(x => + x.IdentifierName.Name == "WaygateSetDestination") + .SafeMapFirst(x => { - if (statement is JassCommentSyntax || - statement is JassEmptySyntax) + var regionVariableName = input.StatementChildren.OfType().Where(x => x.IdentifierName.Name == "GetRectCenterX" || x.IdentifierName.Name == "GetRectCenterY").SafeMapFirst(x => ((JassVariableReferenceExpressionSyntax)x.Arguments.Arguments[0]).IdentifierName.Name); + if (regionVariableName == null) { - continue; + var destination = new Vector2(x.Arguments.Arguments[1].GetValueOrDefault(), x.Arguments.Arguments[2].GetValueOrDefault()); + var region = Context.GetAll().LastOrDefault(x => x.CenterX == destination.X && x.CenterY == destination.Y); + regionVariableName = Context.GetVariableName(region); } - else if (statement is JassCallStatementSyntax callStatement) + + return new { - if (string.Equals(callStatement.IdentifierName.Name, "SetPlayerStartLocation", StringComparison.Ordinal)) - { - if (callStatement.Arguments.Arguments.Length == 2 && - callStatement.Arguments.Arguments[0] is JassInvocationExpressionSyntax playerInvocationExpression && - string.Equals(playerInvocationExpression.IdentifierName.Name, "Player", StringComparison.Ordinal) && - playerInvocationExpression.Arguments.Arguments.Length == 1 && - playerInvocationExpression.Arguments.Arguments[0].TryGetPlayerIdExpressionValue(Context.MaxPlayerSlots, out var playerId) && - callStatement.Arguments.Arguments[1].TryGetIntegerExpressionValue(out var startLocationNumber) && - startLocationPositions.TryGetValue(startLocationNumber, out var startLocationPosition)) - { - var unit = new UnitData - { - OwnerId = playerId, - TypeId = "sloc".FromRawcode(), - Position = new Vector3(startLocationPosition, 0f), - Rotation = MathF.PI * 1.5f, - Scale = Vector3.One, - Flags = 2, - GoldAmount = 12500, - HeroLevel = 0, - TargetAcquisition = 0, - CreationNumber = CreationNumber++ - }; - - unit.SkinId = unit.TypeId; - - result.Add(unit); - } - else - { - startLocations = null; - return false; - } - } - else - { - continue; - } - } - else + UnitVariableName = (x.Arguments.Arguments[0] as JassVariableReferenceExpressionSyntax)?.IdentifierName?.Name, + RegionVariableName = regionVariableName + }; + }); + + if (match != null) + { + var unit = Context.Get(match.UnitVariableName) ?? Context.GetLastCreated(); + var region = Context.Get(match.RegionVariableName) ?? Context.GetLastCreated(); + if (unit != null && region != null) { - startLocations = null; - return false; + unit.WaygateDestinationRegionId = region.CreationNumber; + Context.HandledStatements.Add(input.Statement); } } - - startLocations = result; - return true; } } } \ No newline at end of file diff --git a/src/War3Net.CodeAnalysis.Jass/Extensions/ExpressionSyntaxExtensions.cs b/src/War3Net.CodeAnalysis.Jass/Extensions/ExpressionSyntaxExtensions.cs index 620a3cc1..40f91a9e 100644 --- a/src/War3Net.CodeAnalysis.Jass/Extensions/ExpressionSyntaxExtensions.cs +++ b/src/War3Net.CodeAnalysis.Jass/Extensions/ExpressionSyntaxExtensions.cs @@ -1,16 +1,16 @@ // ------------------------------------------------------------------------------ -// +// // Licensed under the MIT license. // See the LICENSE file in the project root for more information. // // ------------------------------------------------------------------------------ using System; -using System.Globalization; +using War3Net.CodeAnalysis.Jass; using War3Net.CodeAnalysis.Jass.Syntax; -namespace War3Net.CodeAnalysis.Jass.Extensions +namespace War3Net.CodeAnalysis.Decompilers { public static class ExpressionSyntaxExtensions { @@ -24,79 +24,159 @@ public static IExpressionSyntax Deparenthesize(this IExpressionSyntax expression return expression; } - public static bool TryGetIntegerExpressionValue(this IExpressionSyntax expression, out int value) + public static T GetValueOrDefault(this IExpressionSyntax expression, T defaultValue = default) { - switch (expression) + if (expression.TryGetValue(out var value)) { - case JassDecimalLiteralExpressionSyntax decimalLiteralExpression: - value = decimalLiteralExpression.Value; - return true; - - case JassOctalLiteralExpressionSyntax octalLiteralExpression: - value = octalLiteralExpression.Value; - return true; - - case JassFourCCLiteralExpressionSyntax fourCCLiteralExpression: - value = fourCCLiteralExpression.Value; - return true; + return value; + } - case JassUnaryExpressionSyntax unaryExpression: - return int.TryParse(unaryExpression.ToString(), out value); + return defaultValue; + } - case JassHexadecimalLiteralExpressionSyntax hexLiteralExpression: - value = hexLiteralExpression.Value; - return true; + public static bool TryGetValue(this IExpressionSyntax expression, out T value) + { + value = default; - default: - value = default; - return false; + if (!expression.TryGetStringExpressionValue(out var stringValue)) + { + return false; } - } - public static bool TryGetPlayerIdExpressionValue(this IExpressionSyntax expression, int maxPlayerSlots, out int value) - { - if (expression is JassVariableReferenceExpressionSyntax variableReferenceExpression) + if (typeof(T) == typeof(string)) { - if (string.Equals(variableReferenceExpression.IdentifierName.Name, "PLAYER_NEUTRAL_AGGRESSIVE", StringComparison.Ordinal)) - { - value = maxPlayerSlots; - return true; - } - else if (string.Equals(variableReferenceExpression.IdentifierName.Name, "PLAYER_NEUTRAL_PASSIVE", StringComparison.Ordinal)) - { - value = maxPlayerSlots + 3; - return true; - } - else - { - value = default; - return false; - } + value = (T)(object)stringValue; + return true; } - else + + if (decimal.TryParse(stringValue, out var decimalValue)) { - return expression.TryGetIntegerExpressionValue(out value); + value = SafeConvertDecimalTo(decimalValue); + return true; } + + return false; } - public static bool TryGetRealExpressionValue(this IExpressionSyntax expression, out float value) + private static T SafeConvertDecimalTo(decimal value) { - if (expression.TryGetIntegerExpressionValue(out var intValue)) + if (Nullable.GetUnderlyingType(typeof(T)) != null) { - value = intValue; - return true; + var nonNullableType = Nullable.GetUnderlyingType(typeof(T)); + var method = typeof(ExpressionSyntaxExtensions).GetMethod(nameof(SafeConvertDecimalTo), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var genericMethod = method.MakeGenericMethod(nonNullableType); + var result = genericMethod.Invoke(null, new object[] { value }); + return (T)result; } - if (expression is JassRealLiteralExpressionSyntax realLiteralExpression && - float.TryParse(realLiteralExpression.ToString(), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value)) + if (typeof(T) == typeof(int)) { - return true; + return (T)(object)(int)Math.Clamp(value, int.MinValue, int.MaxValue); + } + else if (typeof(T) == typeof(uint)) + { + return (T)(object)(uint)Math.Clamp(value, uint.MinValue, uint.MaxValue); + } + else if (typeof(T) == typeof(byte)) + { + return (T)(object)(byte)Math.Clamp(value, byte.MinValue, byte.MaxValue); + } + else if (typeof(T) == typeof(sbyte)) + { + return (T)(object)(sbyte)Math.Clamp(value, sbyte.MinValue, sbyte.MaxValue); + } + else if (typeof(T) == typeof(short)) + { + return (T)(object)(short)Math.Clamp(value, short.MinValue, short.MaxValue); + } + else if (typeof(T) == typeof(ushort)) + { + return (T)(object)(ushort)Math.Clamp(value, ushort.MinValue, ushort.MaxValue); + } + else if (typeof(T) == typeof(long)) + { + return (T)(object)(long)Math.Clamp(value, long.MinValue, long.MaxValue); + } + else if (typeof(T) == typeof(ulong)) + { + return (T)(object)(ulong)Math.Clamp(value, ulong.MinValue, ulong.MaxValue); + } + else if (typeof(T) == typeof(bool)) + { + return (T)(object)(value != 0); + } + else if (typeof(T) == typeof(decimal)) + { + return (T)(object)value; + } + else if (typeof(T) == typeof(float)) + { + return (T)(object)(float)value; + } + else if (typeof(T) == typeof(double)) + { + return (T)(object)(double)value; } - if (expression is JassUnaryExpressionSyntax unaryExpression && - float.TryParse(unaryExpression.ToString(), NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value)) + return default; + } + + private static bool TryGetStringExpressionValue(this IExpressionSyntax expression, out string value) + { + switch (expression) { - return true; + case JassBooleanLiteralExpressionSyntax booleanLiteralExpression: + value = booleanLiteralExpression.Value ? "1" : "0"; + return true; + + case JassDecimalLiteralExpressionSyntax decimalLiteralExpression: + value = decimalLiteralExpression.Value.ToString(); + return true; + + case JassRealLiteralExpressionSyntax realLiteralExpression: + value = realLiteralExpression.IntPart + "." + realLiteralExpression.FracPart; + return true; + + case JassOctalLiteralExpressionSyntax octalLiteralExpression: + value = octalLiteralExpression.Value.ToString(); + return true; + + case JassFourCCLiteralExpressionSyntax fourCCLiteralExpression: + value = fourCCLiteralExpression.Value.ToString(); + return true; + + case JassUnaryExpressionSyntax unaryExpression: + if (unaryExpression.Operator != UnaryOperatorType.Not && TryGetStringExpressionValue(unaryExpression.Expression, out var unaryExpressionValue)) + { + value = (unaryExpression.Operator == UnaryOperatorType.Minus ? "-" : "") + unaryExpressionValue; + return true; + } + break; + + case JassHexadecimalLiteralExpressionSyntax hexLiteralExpression: + value = hexLiteralExpression.Value.ToString(); + return true; + + case JassStringLiteralExpressionSyntax stringLiteralExpression: + value = stringLiteralExpression.Value; + return true; + + case JassParenthesizedExpressionSyntax parenthesizedExpression: + if (TryGetStringExpressionValue(parenthesizedExpression.Expression, out var parenthesizedExpressionValue)) + { + value = parenthesizedExpressionValue; + return true; + } + break; + + case JassCharacterLiteralExpressionSyntax charLiteralExpression: + value = charLiteralExpression.Value.ToString(); + return true; + + case JassNullLiteralExpressionSyntax: + value = JassKeyword.Null; + return true; } value = default; diff --git a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Audio/MapSoundsDecompilerTests.cs b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Audio/MapSoundsDecompilerTests.cs index 13c1d06e..45c195cd 100644 --- a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Audio/MapSoundsDecompilerTests.cs +++ b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Audio/MapSoundsDecompilerTests.cs @@ -14,6 +14,7 @@ using War3Net.Build; using War3Net.Build.Audio; using War3Net.Build.Info; +using War3Net.CodeAnalysis.Jass; using War3Net.TestTools.UnitTesting; namespace War3Net.CodeAnalysis.Decompilers.Tests.Audio @@ -23,9 +24,10 @@ public class MapSoundsDecompilerTests { [DataTestMethod] [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] - public void TestDecompileMapSounds(Map map) + public void TestDecompileMapSounds(Map testMap) { - Assert.IsTrue(new JassScriptDecompiler(map).TryDecompileMapSounds(map.Sounds.FormatVersion, out var decompiledMapSounds), "Failed to decompile map sounds."); + var map = new JassScriptDecompiler(JassSyntaxFactory.ParseCompilationUnit(testMap.Script), new DecompileOptions() { mapSoundsFormatVersion = testMap.Sounds.FormatVersion }, testMap.Info).DecompileObjectManagerData(); + var decompiledMapSounds = map.Sounds; Assert.AreEqual(map.Sounds.Sounds.Count, decompiledMapSounds.Sounds.Count); for (var i = 0; i < decompiledMapSounds.Sounds.Count; i++) diff --git a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapCamerasDecompilerTests.cs b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapCamerasDecompilerTests.cs index df16dd50..12b0f28d 100644 --- a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapCamerasDecompilerTests.cs +++ b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapCamerasDecompilerTests.cs @@ -13,6 +13,7 @@ using War3Net.Build; using War3Net.Build.Info; +using War3Net.CodeAnalysis.Jass; using War3Net.TestTools.UnitTesting; namespace War3Net.CodeAnalysis.Decompilers.Tests.Environment @@ -22,9 +23,10 @@ public class MapCamerasDecompilerTests { [DataTestMethod] [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] - public void TestDecompileMapCameras(Map map) + public void TestDecompileMapCameras(Map testMap) { - Assert.IsTrue(new JassScriptDecompiler(map).TryDecompileMapCameras(map.Cameras.FormatVersion, map.Cameras.UseNewFormat, out var decompiledMapCameras), "Failed to decompile map cameras."); + var map = new JassScriptDecompiler(JassSyntaxFactory.ParseCompilationUnit(testMap.Script), new DecompileOptions() { mapCamerasFormatVersion = testMap.Cameras.FormatVersion, mapCamerasUseNewFormat = testMap.Cameras.UseNewFormat }, testMap.Info).DecompileObjectManagerData(); + var decompiledMapCameras = map.Cameras; Assert.AreEqual(map.Cameras.Cameras.Count, decompiledMapCameras.Cameras.Count); for (var i = 0; i < decompiledMapCameras.Cameras.Count; i++) diff --git a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapRegionsDecompilerTests.cs b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapRegionsDecompilerTests.cs index c0015fc0..79ff6027 100644 --- a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapRegionsDecompilerTests.cs +++ b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Environment/MapRegionsDecompilerTests.cs @@ -13,6 +13,7 @@ using War3Net.Build; using War3Net.Build.Info; +using War3Net.CodeAnalysis.Jass; using War3Net.TestTools.UnitTesting; namespace War3Net.CodeAnalysis.Decompilers.Tests.Environment @@ -22,9 +23,10 @@ public class MapRegionsDecompilerTests { [DataTestMethod] [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] - public void TestDecompileMapRegions(Map map) + public void TestDecompileMapRegions(Map testMap) { - Assert.IsTrue(new JassScriptDecompiler(map).TryDecompileMapRegions(map.Regions.FormatVersion, out var decompiledMapRegions), "Failed to decompile map regions."); + var map = new JassScriptDecompiler(JassSyntaxFactory.ParseCompilationUnit(testMap.Script), new DecompileOptions() { mapRegionsFormatVersion = testMap.Regions.FormatVersion }, testMap.Info).DecompileObjectManagerData(); + var decompiledMapRegions = map.Regions; Assert.AreEqual(map.Regions.Regions.Count, decompiledMapRegions.Regions.Count); for (var i = 0; i < decompiledMapRegions.Regions.Count; i++) diff --git a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Script/MapTriggersDecompilerTests.cs b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Script/MapTriggersDecompilerTests.cs index bc725845..7beeac78 100644 --- a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Script/MapTriggersDecompilerTests.cs +++ b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Script/MapTriggersDecompilerTests.cs @@ -14,6 +14,7 @@ using War3Net.Build; using War3Net.Build.Info; using War3Net.Build.Script; +using War3Net.CodeAnalysis.Jass; using War3Net.TestTools.UnitTesting; namespace War3Net.CodeAnalysis.Decompilers.Tests.Script @@ -25,7 +26,8 @@ public class MapTriggersDecompilerTests [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] public void TestDecompileMapTriggers(Map map) { - Assert.IsTrue(new JassScriptDecompiler(map).TryDecompileMapTriggers(map.Triggers.FormatVersion, map.Triggers.SubVersion, out var decompiledMapTriggers), "Failed to decompile map triggers."); + var decompiler = new JassScriptDecompiler(JassSyntaxFactory.ParseCompilationUnit(map.Script), new DecompileOptions(), map.Info); + Assert.IsTrue(decompiler.TryDecompileMapTriggers(map.Triggers.FormatVersion, map.Triggers.SubVersion, out var decompiledMapTriggers), "Failed to decompile map triggers."); Assert.AreEqual(map.Triggers.Variables.Count, decompiledMapTriggers.Variables.Count); for (var i = 0; i < decompiledMapTriggers.Variables.Count; i++) diff --git a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Widget/MapUnitsDecompilerTests.cs b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Widget/MapUnitsDecompilerTests.cs index 82c4359e..4b805ace 100644 --- a/tests/War3Net.CodeAnalysis.Decompilers.Tests/Widget/MapUnitsDecompilerTests.cs +++ b/tests/War3Net.CodeAnalysis.Decompilers.Tests/Widget/MapUnitsDecompilerTests.cs @@ -14,6 +14,7 @@ using War3Net.Build; using War3Net.Build.Extensions; using War3Net.Build.Info; +using War3Net.CodeAnalysis.Jass; using War3Net.Common.Extensions; using War3Net.TestTools.UnitTesting; @@ -24,9 +25,10 @@ public class MapUnitsDecompilerTests { [DataTestMethod] [DynamicData(nameof(GetTestData), DynamicDataSourceType.Method)] - public void TestDecompileMapUnits(Map map) + public void TestDecompileMapUnits(Map testMap) { - Assert.IsTrue(new JassScriptDecompiler(map).TryDecompileMapUnits(map.Units.FormatVersion, map.Units.SubVersion, map.Units.UseNewFormat, out var decompiledMapUnits), "Failed to decompile map units."); + var map = new JassScriptDecompiler(JassSyntaxFactory.ParseCompilationUnit(testMap.Script), new DecompileOptions() { mapWidgetsFormatVersion = testMap.Units.FormatVersion, mapWidgetsSubVersion = testMap.Units.SubVersion, mapWidgetsUseNewFormat = testMap.Units.UseNewFormat }, testMap.Info).DecompileObjectManagerData(); + var decompiledMapUnits = map.Units; var expectedMapUnits = map.Units.Units #if true