From b92c587335a07c6475995ba008403b332006122b Mon Sep 17 00:00:00 2001 From: penev92 Date: Thu, 13 Oct 2022 22:19:36 +0300 Subject: [PATCH 1/6] Added SpriteSequenceFormat to ModManifest --- server/Oraide.Core/Entities/MiniYaml/ModManifest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs b/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs index b634295..e617140 100644 --- a/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs +++ b/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs @@ -14,12 +14,17 @@ public readonly struct ModManifest public readonly IReadOnlyList WeaponsFiles; + public readonly (string Type, IReadOnlyDictionary Metadata) SpriteSequenceFormat; + public ModManifest(IEnumerable yamlNodes) { MapsFolder = yamlNodes.FirstOrDefault(x => x.Key == "MapFolders")?.ChildNodes?.FirstOrDefault(x => x.Value == "System")?.Key; RulesFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Rules").ToList()); CursorsFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Cursors").ToList()); WeaponsFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Weapons").ToList()); + + var spriteSequenceFormatNode = yamlNodes.FirstOrDefault(x => x.Key == "SpriteSequenceFormat"); + SpriteSequenceFormat = (spriteSequenceFormatNode?.Value, spriteSequenceFormatNode?.ChildNodes?.ToDictionary(x => x.Key, y => y)); } static IEnumerable GetValues(IEnumerable yamlNodes, string key) From e96e8ccde7b253d48aa830a1478bb92b6b56d00a Mon Sep 17 00:00:00 2001 From: penev92 Date: Sun, 25 Sep 2022 19:07:17 +0300 Subject: [PATCH 2/6] Added loading of SpriteSequences as mod symbols --- .../Entities/MiniYaml/ModManifest.cs | 3 ++ .../MiniYaml/SpriteSequenceDefinition.cs | 29 +++++++++++++++++++ .../MiniYaml/SpriteSequenceImageDefinition.cs | 24 +++++++++++++++ .../Caching/Entities/ModSymbols.cs | 8 ++++- .../Caching/SymbolCache.cs | 3 +- .../YamlInformationProvider.cs | 5 ++++ .../YamlParsers/OpenRAMiniYamlParser.cs | 20 +++++++++++++ 7 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 server/Oraide.Core/Entities/MiniYaml/SpriteSequenceDefinition.cs create mode 100644 server/Oraide.Core/Entities/MiniYaml/SpriteSequenceImageDefinition.cs diff --git a/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs b/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs index e617140..e33802d 100644 --- a/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs +++ b/server/Oraide.Core/Entities/MiniYaml/ModManifest.cs @@ -14,6 +14,8 @@ public readonly struct ModManifest public readonly IReadOnlyList WeaponsFiles; + public readonly IReadOnlyList SpriteSequences; + public readonly (string Type, IReadOnlyDictionary Metadata) SpriteSequenceFormat; public ModManifest(IEnumerable yamlNodes) @@ -22,6 +24,7 @@ public ModManifest(IEnumerable yamlNodes) RulesFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Rules").ToList()); CursorsFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Cursors").ToList()); WeaponsFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Weapons").ToList()); + SpriteSequences = new ReadOnlyCollection(GetValues(yamlNodes, "Sequences").ToList()); var spriteSequenceFormatNode = yamlNodes.FirstOrDefault(x => x.Key == "SpriteSequenceFormat"); SpriteSequenceFormat = (spriteSequenceFormatNode?.Value, spriteSequenceFormatNode?.ChildNodes?.ToDictionary(x => x.Key, y => y)); diff --git a/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceDefinition.cs b/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceDefinition.cs new file mode 100644 index 0000000..1cca4d2 --- /dev/null +++ b/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceDefinition.cs @@ -0,0 +1,29 @@ +namespace Oraide.Core.Entities.MiniYaml +{ + public readonly struct SpriteSequenceDefinition + { + public readonly string Name; + + public readonly string ImageName; + + public readonly string FileName; + + public readonly MemberLocation Location; + + public SpriteSequenceDefinition(string name, string imageName, string fileName, MemberLocation location) + { + Name = name; + ImageName = imageName; + FileName = fileName; + Location = location; + } + + public override string ToString() => $"Sequence {Name}"; + + public string ToMarkdownInfoString() => "```csharp\n" + + $"Image \"{ImageName}\"\n" + + $"Sequence \"{Name}\"\n" + + $"File name \"{FileName}\"\n" + + "```"; + } +} diff --git a/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceImageDefinition.cs b/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceImageDefinition.cs new file mode 100644 index 0000000..487174f --- /dev/null +++ b/server/Oraide.Core/Entities/MiniYaml/SpriteSequenceImageDefinition.cs @@ -0,0 +1,24 @@ +namespace Oraide.Core.Entities.MiniYaml +{ + public readonly struct SpriteSequenceImageDefinition + { + public readonly string Name; + + public readonly MemberLocation Location; + + public readonly SpriteSequenceDefinition[] Sequences; + + public SpriteSequenceImageDefinition(string name, MemberLocation location, SpriteSequenceDefinition[] sequences) + { + Name = name; + Location = location; + Sequences = sequences; + } + + public override string ToString() => $"Image {Name}"; + + public string ToMarkdownInfoString() => "```csharp\n" + + $"Image \"{Name}\"" + + "\n```"; + } +} diff --git a/server/Oraide.LanguageServer/Caching/Entities/ModSymbols.cs b/server/Oraide.LanguageServer/Caching/Entities/ModSymbols.cs index 32ef933..9502692 100644 --- a/server/Oraide.LanguageServer/Caching/Entities/ModSymbols.cs +++ b/server/Oraide.LanguageServer/Caching/Entities/ModSymbols.cs @@ -30,15 +30,21 @@ public class ModSymbols /// public ILookup PaletteDefinitions { get; } + /// + /// A collection of all sprite sequence definitions. + /// + public ILookup SpriteSequenceImageDefinitions { get; } + public ModSymbols(ILookup actorDefinitions, ILookup weaponDefinitions, ILookup conditionDefinitions, ILookup cursorDefinitions, - ILookup paletteDefinitions) + ILookup paletteDefinitions, ILookup spriteSequenceImageDefinitions) { ActorDefinitions = actorDefinitions; WeaponDefinitions = weaponDefinitions; ConditionDefinitions = conditionDefinitions; CursorDefinitions = cursorDefinitions; PaletteDefinitions = paletteDefinitions; + SpriteSequenceImageDefinitions = spriteSequenceImageDefinitions; } } } diff --git a/server/Oraide.LanguageServer/Caching/SymbolCache.cs b/server/Oraide.LanguageServer/Caching/SymbolCache.cs index 0466fda..4cb4787 100644 --- a/server/Oraide.LanguageServer/Caching/SymbolCache.cs +++ b/server/Oraide.LanguageServer/Caching/SymbolCache.cs @@ -81,8 +81,9 @@ IReadOnlyDictionary CreateSymbolCachesPerMod() var conditionDefinitions = yamlInformationProvider.GetConditionDefinitions(modManifest.RulesFiles, mods); var cursorDefinitions = yamlInformationProvider.GetCursorDefinitions(modManifest.CursorsFiles, mods); var paletteDefinitions = yamlInformationProvider.GetPaletteDefinitions(modManifest.RulesFiles, mods, knownPaletteTypes); + var spriteSequenceDefinitions = yamlInformationProvider.GetSpriteSequenceDefinitions(modManifest.SpriteSequences, mods); - var modSymbols = new ModSymbols(actorDefinitions, weaponDefinitions, conditionDefinitions, cursorDefinitions, paletteDefinitions); + var modSymbols = new ModSymbols(actorDefinitions, weaponDefinitions, conditionDefinitions, cursorDefinitions, paletteDefinitions, spriteSequenceDefinitions); var mapsDir = OpenRaFolderUtils.ResolveFilePath(modManifest.MapsFolder, mods); var allMaps = mapsDir == null diff --git a/server/Oraide.MiniYaml/YamlInformationProvider.cs b/server/Oraide.MiniYaml/YamlInformationProvider.cs index e18f4e4..276a673 100644 --- a/server/Oraide.MiniYaml/YamlInformationProvider.cs +++ b/server/Oraide.MiniYaml/YamlInformationProvider.cs @@ -57,6 +57,11 @@ public ILookup GetPaletteDefinitions(IEnumerable GetSpriteSequenceDefinitions(IEnumerable referencedFiles, IReadOnlyDictionary mods) + { + return OpenRAMiniYamlParser.GetSpriteSequenceDefinitions(referencedFiles, mods); + } + public (IEnumerable Original, IEnumerable Flattened) ParseText(string text, string fileUriString = null) { return OpenRAMiniYamlParser.ParseText(text, fileUriString); diff --git a/server/Oraide.MiniYaml/YamlParsers/OpenRAMiniYamlParser.cs b/server/Oraide.MiniYaml/YamlParsers/OpenRAMiniYamlParser.cs index 49af5f2..2992152 100644 --- a/server/Oraide.MiniYaml/YamlParsers/OpenRAMiniYamlParser.cs +++ b/server/Oraide.MiniYaml/YamlParsers/OpenRAMiniYamlParser.cs @@ -134,6 +134,26 @@ public static ILookup GetPaletteDefinitions(IEnumerab return paletteDefinitions.ToLookup(n => n.Name, m => m); } + public static ILookup GetSpriteSequenceDefinitions(IEnumerable referencedFiles, IReadOnlyDictionary mods) + { + var result = new List(); + + var yamlNodes = YamlNodesFromReferencedFiles(referencedFiles, mods); + foreach (var node in yamlNodes) + { + var location = new MemberLocation(node.Location.FileUri, node.Location.LineNumber, node.Location.CharacterPosition); + + var sequences = node.ChildNodes == null + ? Enumerable.Empty() + : node.ChildNodes.Select(x => + new SpriteSequenceDefinition(x.Key, x.ParentNode.Key, x.Value, new MemberLocation(x.Location.FileUri, x.Location.LineNumber, 1))); // HACK HACK HACK: Until the YAML Loader learns about character positions, we hardcode 1 here (since this is all for traits on actor definitions). + + result.Add(new SpriteSequenceImageDefinition(node.Key, location, sequences.ToArray())); + } + + return result.ToLookup(n => n.Name, m => m); + } + public static (IEnumerable Original, IEnumerable Flattened) ParseText(string text, string fileUriString = null) { var nodes = OpenRA.MiniYamlParser.MiniYamlLoader.FromString(text, discardCommentsAndWhitespace: false) From c610acc7bd73fca547e5b9d4199b52cfb294a331 Mon Sep 17 00:00:00 2001 From: penev92 Date: Sun, 25 Sep 2022 20:32:13 +0300 Subject: [PATCH 3/6] Added loading of SpriteSequences as map symbols --- server/Oraide.Core/Entities/MiniYaml/MapManifest.cs | 3 +++ .../Oraide.LanguageServer/Caching/Entities/MapSymbols.cs | 9 ++++++++- server/Oraide.LanguageServer/Caching/SymbolCache.cs | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/server/Oraide.Core/Entities/MiniYaml/MapManifest.cs b/server/Oraide.Core/Entities/MiniYaml/MapManifest.cs index e17fdba..6012983 100644 --- a/server/Oraide.Core/Entities/MiniYaml/MapManifest.cs +++ b/server/Oraide.Core/Entities/MiniYaml/MapManifest.cs @@ -13,6 +13,8 @@ public readonly struct MapManifest public readonly IReadOnlyList WeaponsFiles; + public readonly IReadOnlyList SpriteSequenceFiles; + public string MapFile => MapFolder == null ? null : $"{MapFolder}/map.yaml"; public string MapReference => MapFolder == null ? null : $"{mapsFolder}/{Path.GetFileName(MapFolder)}"; @@ -26,6 +28,7 @@ public MapManifest(string mapFolder, IEnumerable yamlNodes, string map MapFolder = mapFolder; RulesFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Rules", Path.GetFileName(mapFolder), mapsFolder).ToList()); WeaponsFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Weapons", Path.GetFileName(mapFolder), mapsFolder).ToList()); + SpriteSequenceFiles = new ReadOnlyCollection(GetValues(yamlNodes, "Sequences", Path.GetFileName(mapFolder), mapsFolder).ToList()); this.mapsFolder = mapsFolder; } diff --git a/server/Oraide.LanguageServer/Caching/Entities/MapSymbols.cs b/server/Oraide.LanguageServer/Caching/Entities/MapSymbols.cs index ec9f245..e74a5dc 100644 --- a/server/Oraide.LanguageServer/Caching/Entities/MapSymbols.cs +++ b/server/Oraide.LanguageServer/Caching/Entities/MapSymbols.cs @@ -25,13 +25,20 @@ public class MapSymbols /// public ILookup PaletteDefinitions { get; } + /// + /// A collection of all sprite sequence definitions in the current map's YAML grouped by their name. + /// + public ILookup SpriteSequenceImageDefinitions { get; } + public MapSymbols(ILookup actorDefinitions, ILookup weaponDefinitions, - ILookup conditionDefinitions, ILookup paletteDefinitions) + ILookup conditionDefinitions, ILookup paletteDefinitions, + ILookup spriteSequenceImageDefinitions) { ActorDefinitions = actorDefinitions; WeaponDefinitions = weaponDefinitions; ConditionDefinitions = conditionDefinitions; PaletteDefinitions = paletteDefinitions; + SpriteSequenceImageDefinitions = spriteSequenceImageDefinitions; } } } diff --git a/server/Oraide.LanguageServer/Caching/SymbolCache.cs b/server/Oraide.LanguageServer/Caching/SymbolCache.cs index 4cb4787..915d20b 100644 --- a/server/Oraide.LanguageServer/Caching/SymbolCache.cs +++ b/server/Oraide.LanguageServer/Caching/SymbolCache.cs @@ -115,8 +115,9 @@ public void AddMap(string modId, MapManifest mapManifest) var weaponDefinitions = yamlInformationProvider.GetWeaponDefinitions(mapManifest.WeaponsFiles, mods); var conditionDefinitions = yamlInformationProvider.GetConditionDefinitions(mapManifest.RulesFiles, mods); var paletteDefinitions = yamlInformationProvider.GetPaletteDefinitions(mapManifest.RulesFiles, mods, knownPaletteTypes); + var spriteSequenceImageDefinitions = yamlInformationProvider.GetSpriteSequenceDefinitions(mapManifest.SpriteSequenceFiles, mods); - var mapSymbols = new MapSymbols(actorDefinitions, weaponDefinitions, conditionDefinitions, paletteDefinitions); + var mapSymbols = new MapSymbols(actorDefinitions, weaponDefinitions, conditionDefinitions, paletteDefinitions, spriteSequenceImageDefinitions); Maps.Add(mapManifest.MapReference, mapSymbols); } From 9f357bd6049a887e1c17b773d3d0344eb04fdc3c Mon Sep 17 00:00:00 2001 From: penev92 Date: Sun, 25 Sep 2022 14:28:39 +0300 Subject: [PATCH 4/6] Added loading of SpriteSequence code symbols --- .../Oraide.Csharp/CodeInformationProvider.cs | 5 ++ .../CodeParsers/RoslynCodeParser.cs | 67 +++++++++++++++++-- .../CodeParsingSymbolGenerationStrategy.cs | 12 +++- .../FromStaticFileSymbolGenerationStrategy.cs | 33 ++++++++- .../ICodeSymbolGenerationStrategy.cs | 2 + .../Caching/Entities/CodeSymbols.cs | 9 ++- .../Caching/SymbolCache.cs | 4 +- 7 files changed, 123 insertions(+), 9 deletions(-) diff --git a/server/Oraide.Csharp/CodeInformationProvider.cs b/server/Oraide.Csharp/CodeInformationProvider.cs index 359c7e5..ece377a 100644 --- a/server/Oraide.Csharp/CodeInformationProvider.cs +++ b/server/Oraide.Csharp/CodeInformationProvider.cs @@ -62,6 +62,11 @@ public ILookup GetPaletteTraitInfos() return symbolGenerator.GetPaletteTraitInfos(); } + public ILookup GetSpriteSequenceInfos() + { + return symbolGenerator.GetSpriteSequenceInfos(); + } + string GetOpenRaFolder(string workspaceFolderPath, string defaultOpenRaFolderPath) { var oraFolderPath = ""; diff --git a/server/Oraide.Csharp/CodeParsers/RoslynCodeParser.cs b/server/Oraide.Csharp/CodeParsers/RoslynCodeParser.cs index ac462f7..0c01b6b 100644 --- a/server/Oraide.Csharp/CodeParsers/RoslynCodeParser.cs +++ b/server/Oraide.Csharp/CodeParsers/RoslynCodeParser.cs @@ -12,12 +12,13 @@ namespace Oraide.Csharp.CodeParsers { public static class RoslynCodeParser { - public static (ILookup, WeaponInfo, ILookup) Parse(in string oraFolderPath) + public static (ILookup, WeaponInfo, ILookup, ILookup) Parse(in string oraFolderPath) { var traitInfos = new List(); var weaponInfoFields = Array.Empty(); var warheadInfos = new List(); var projectileInfos = new List(); + var spriteSequences = new List(); var filePaths = Directory.EnumerateFiles(oraFolderPath, "*.cs", SearchOption.AllDirectories) .Where(x => !x.Contains("OpenRA.Test")); @@ -64,6 +65,36 @@ public static (ILookup, WeaponInfo, ILookup(); + foreach (var member in classDeclaration.Members) + { + if (member is FieldDeclarationSyntax fieldMember + && fieldMember.Modifiers.Any(x => x.ValueText == "static") + && fieldMember.Modifiers.Any(x => x.ValueText == "readonly") + && (fieldMember.Declaration.Type as GenericNameSyntax).Identifier.ValueText == "SpriteSequenceField") + { + foreach (var fieldInfo in ParseClassField(filePath, fileText, fieldMember)) + { + var type = string.Join(", ", (fieldMember.Declaration.Type as GenericNameSyntax).TypeArgumentList.Arguments); + var defaultValue = fieldInfo.DefaultValue.Split(',').Last().Trim(); + var newFieldInfo = new ClassFieldInfo(fieldInfo.Name, type, UserFriendlyTypeName(type), + defaultValue, fieldInfo.ClassName, fieldInfo.Location, fieldInfo.Description, fieldInfo.OtherAttributes); + + fields.Add(newFieldInfo); + } + } + } + + var newClassInfo = new SimpleClassInfo(classInfo.Name, classInfo.InfoName, classInfo.Description, classInfo.Location, + classInfo.InheritedTypes, fields.ToArray(), classInfo.IsAbstract); + + spriteSequences.Add(newClassInfo); + } } } } @@ -124,6 +155,7 @@ public static (ILookup, WeaponInfo, ILookup(); foreach (var (className, classFieldNames) in baseTypes) { + // TODO: This seems like it could be simplified a lot like SpriteSequences below. foreach (var typeFieldName in classFieldNames) { var fi = wi.PropertyInfos.FirstOrDefault(z => z.Name == typeFieldName); @@ -160,7 +192,34 @@ public static (ILookup, WeaponInfo, ILookup z.Name == "PaletteDefinition"))) .ToLookup(x => x.TraitInfoName, y => y); - return (finalTraitInfos.ToLookup(x => x.TraitInfoName, y => y), weaponInfo, paletteTraitInfos); + // Resolve warhead inheritance - load base types and a full list in fields - inherited or not. + var finalSpriteSequenceInfos = new List(spriteSequences.Count); + foreach (var ssi in spriteSequences) + { + var baseTypes = GetWarheadBaseTypes(spriteSequences, ssi.Name); + var fieldInfos = new List(); + foreach (var (baseClassName, _) in baseTypes) + { + var baseClassInfo = spriteSequences.FirstOrDefault(x => x.Name == baseClassName); + fieldInfos.AddRange(baseClassInfo.PropertyInfos); + } + + var spriteSequenceInfo = new SimpleClassInfo( + ssi.Name, + ssi.InfoName, + ssi.Description, + ssi.Location, + baseTypes.Where(x => x.TypeName != ssi.InfoName).Select(x => x.TypeName).ToArray(), + fieldInfos.ToArray(), + ssi.IsAbstract); + + finalSpriteSequenceInfos.Add(spriteSequenceInfo); + } + + return (finalTraitInfos.ToLookup(x => x.TraitInfoName, y => y), + weaponInfo, + paletteTraitInfos, + finalSpriteSequenceInfos.ToLookup(x => x.Name, y => y)); } // Files can potentially contain multiple TraitInfos. @@ -231,8 +290,8 @@ static IEnumerable ParseClassFields(string filePath, string file foreach (var member in classDeclaration.Members) if (member is FieldDeclarationSyntax fieldMember && fieldMember.Modifiers.Any(x => x.ValueText == "public") - && fieldMember.Modifiers.Any(x => x.ValueText == "readonly") - && fieldMember.Modifiers.All(x => x.ValueText != "static")) + && fieldMember.Modifiers.All(x => x.ValueText != "static") + && fieldMember.Modifiers.Any(x => x.ValueText == "readonly")) foreach (var fieldInfo in ParseClassField(filePath, fileText, fieldMember)) yield return fieldInfo; } diff --git a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/CodeParsingSymbolGenerationStrategy.cs b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/CodeParsingSymbolGenerationStrategy.cs index 6dfcafd..28da27f 100644 --- a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/CodeParsingSymbolGenerationStrategy.cs +++ b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/CodeParsingSymbolGenerationStrategy.cs @@ -11,6 +11,7 @@ class CodeParsingSymbolGenerationStrategy : ICodeSymbolGenerationStrategy ILookup traitInfos; WeaponInfo weaponInfo; ILookup paletteTraitInfos; + ILookup spriteSequenceInfos; public CodeParsingSymbolGenerationStrategy(string openRaFolder) { @@ -41,12 +42,21 @@ public ILookup GetPaletteTraitInfos() return paletteTraitInfos; } + public ILookup GetSpriteSequenceInfos() + { + if (spriteSequenceInfos == null) + Parse(); + + return spriteSequenceInfos; + } + void Parse() { - var (traits, weapons, palettes) = RoslynCodeParser.Parse(openRaFolder); + var (traits, weapons, palettes, spriteSequences) = RoslynCodeParser.Parse(openRaFolder); traitInfos = traits; weaponInfo = weapons; paletteTraitInfos = palettes; + spriteSequenceInfos = spriteSequences; } } } diff --git a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/FromStaticFileSymbolGenerationStrategy.cs b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/FromStaticFileSymbolGenerationStrategy.cs index 72fae78..2d42774 100644 --- a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/FromStaticFileSymbolGenerationStrategy.cs +++ b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/FromStaticFileSymbolGenerationStrategy.cs @@ -14,12 +14,14 @@ class FromStaticFileSymbolGenerationStrategy : ICodeSymbolGenerationStrategy readonly JObject traitsData; readonly JObject weaponsData; + readonly JObject spriteSequencesData; static readonly MemberLocation NoLocation = new MemberLocation(string.Empty, 0, 0); ILookup traitInfos; WeaponInfo weaponInfo; ILookup paletteTraitInfos; + ILookup spriteSequenceInfos; public FromStaticFileSymbolGenerationStrategy(string openRaFolder) { @@ -34,6 +36,10 @@ public FromStaticFileSymbolGenerationStrategy(string openRaFolder) var weaponsFile = Path.Combine(assemblyFolder, "docs", $"{LoadedVersion}-weapons.json"); var weaponsText = File.ReadAllText(weaponsFile); weaponsData = JsonConvert.DeserializeObject(weaponsText); + + var spriteSequencesFile = Path.Combine(assemblyFolder, "docs", $"{LoadedVersion}-sprite-sequences.json"); + var spriteSequencesText = File.ReadAllText(spriteSequencesFile); + spriteSequencesData = JsonConvert.DeserializeObject(spriteSequencesText); } public ILookup GetTraitInfos() @@ -97,6 +103,28 @@ public ILookup GetPaletteTraitInfos() return paletteTraitInfos; } + public ILookup GetSpriteSequenceInfos() + { + if (spriteSequenceInfos != null) + return spriteSequenceInfos; + + + var typeInfos = spriteSequencesData["SpriteSequenceTypes"]!.Select(x => + { + var baseTypes = GetBaseTypes(x); + var properties = ReadProperties(x); + + var name = x["Name"].ToString(); + return new SimpleClassInfo(name, name, x["Description"].ToString(), + NoLocation, baseTypes, properties, false); + }); + + return typeInfos.ToLookup(x => x.Name, y => y); + } + + + #region Private methods + string GetVersion(string openRaFolder) { var versionFile = Path.Combine(openRaFolder, "VERSION"); @@ -109,8 +137,7 @@ static ClassFieldInfo[] ReadProperties(JToken jToken) { var attributes = prop["OtherAttributes"] .Select(attribute => - (attribute["Name"].ToString(), - attribute["Value"].ToString().Replace("\r\n", "").Replace(" ", "").Replace(",", ", "))) + (attribute["Name"].ToString(), "")) .ToArray(); var p = new ClassFieldInfo(prop["PropertyName"].ToString(), prop["InternalType"].ToString(), prop["UserFriendlyType"].ToString(), @@ -126,5 +153,7 @@ static string[] GetBaseTypes(JToken jToken) .Select(y => y.ToString()) .ToArray(); } + + #endregion } } diff --git a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/ICodeSymbolGenerationStrategy.cs b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/ICodeSymbolGenerationStrategy.cs index 89a6334..bb19189 100644 --- a/server/Oraide.Csharp/CodeSymbolGenerationStrategies/ICodeSymbolGenerationStrategy.cs +++ b/server/Oraide.Csharp/CodeSymbolGenerationStrategies/ICodeSymbolGenerationStrategy.cs @@ -10,5 +10,7 @@ public interface ICodeSymbolGenerationStrategy WeaponInfo GetWeaponInfo(); ILookup GetPaletteTraitInfos(); + + ILookup GetSpriteSequenceInfos(); } } diff --git a/server/Oraide.LanguageServer/Caching/Entities/CodeSymbols.cs b/server/Oraide.LanguageServer/Caching/Entities/CodeSymbols.cs index 1a3cd27..0e55b4d 100644 --- a/server/Oraide.LanguageServer/Caching/Entities/CodeSymbols.cs +++ b/server/Oraide.LanguageServer/Caching/Entities/CodeSymbols.cs @@ -22,11 +22,18 @@ public class CodeSymbols /// public ILookup PaletteTraitInfos { get; } - public CodeSymbols(ILookup traitInfos, WeaponInfo weaponInfo, ILookup paletteTraitInfos) + /// + /// SpriteSequence information grouped by name. + /// + public ILookup SpriteSequenceInfos { get; } + + public CodeSymbols(ILookup traitInfos, WeaponInfo weaponInfo, ILookup paletteTraitInfos, + ILookup spriteSequenceInfos) { TraitInfos = traitInfos; WeaponInfo = weaponInfo; PaletteTraitInfos = paletteTraitInfos; + SpriteSequenceInfos = spriteSequenceInfos; } } } diff --git a/server/Oraide.LanguageServer/Caching/SymbolCache.cs b/server/Oraide.LanguageServer/Caching/SymbolCache.cs index 915d20b..904fd5a 100644 --- a/server/Oraide.LanguageServer/Caching/SymbolCache.cs +++ b/server/Oraide.LanguageServer/Caching/SymbolCache.cs @@ -53,8 +53,9 @@ IReadOnlyDictionary CreateSymbolCachesPerMod() var traitInfos = codeInformationProvider.GetTraitInfos(); var weaponInfo = codeInformationProvider.GetWeaponInfo(); var paletteTraitInfos = codeInformationProvider.GetPaletteTraitInfos(); + var spriteSequenceInfos = codeInformationProvider.GetSpriteSequenceInfos(); - var codeSymbols = new CodeSymbols(traitInfos, weaponInfo, paletteTraitInfos); + var codeSymbols = new CodeSymbols(traitInfos, weaponInfo, paletteTraitInfos, spriteSequenceInfos); var elapsedTotal = stopwatchTotal.Elapsed; Console.Error.WriteLine($"Took {elapsedTotal} to load code symbols:"); @@ -62,6 +63,7 @@ IReadOnlyDictionary CreateSymbolCachesPerMod() Console.Error.WriteLine($" {weaponInfo.ProjectileInfos.Length} projectileInfos"); Console.Error.WriteLine($" {weaponInfo.WarheadInfos.Length} warheadInfos"); Console.Error.WriteLine($" {paletteTraitInfos.Count} paletteTraitInfos"); + Console.Error.WriteLine($" {spriteSequenceInfos.Count} spriteSequenceInfos"); var stopwatchYaml = new Stopwatch(); stopwatchYaml.Start(); From b6ade2be274b62a0413c0dbff8e36507826c7110 Mon Sep 17 00:00:00 2001 From: penev92 Date: Sun, 25 Sep 2022 21:36:06 +0300 Subject: [PATCH 5/6] Added LSP features support for SpriteSequence files - Added hover info support for SpriteSequence files - Added completion support for SpriteSequence files - Added definition support for SpriteSequence files (This also means we handle GoTo/Peek/Find for Definition/Declaration/Implementations.) - Added SpriteSequenceImages as workspace symbols --- .../Entities/MiniYaml/CursorTarget.cs | 4 +- .../BaseRpcMessageHandler.cs | 20 ++++ .../Extensions/LspTypesExtensions.cs | 33 ++++++ .../TextDocumentCompletionHandler.cs | 83 ++++++++++++++ .../TextDocumentDefinitionHandler.cs | 74 ++++++++++++ .../TextDocument/TextDocumentHoverHandler.cs | 108 ++++++++++++++++++ .../WorkspaceSymbolRequestHandler.cs | 4 +- 7 files changed, 324 insertions(+), 2 deletions(-) diff --git a/server/Oraide.Core/Entities/MiniYaml/CursorTarget.cs b/server/Oraide.Core/Entities/MiniYaml/CursorTarget.cs index 6e0209a..d791d4e 100644 --- a/server/Oraide.Core/Entities/MiniYaml/CursorTarget.cs +++ b/server/Oraide.Core/Entities/MiniYaml/CursorTarget.cs @@ -19,9 +19,11 @@ public enum FileType Rules = 1, Weapons = 2, Cursors = 3, + SpriteSequences = 4, MapFile = 10, MapRules = 11, - MapWeapons = 12 + MapWeapons = 12, + MapSpriteSequences = 14, } public readonly struct CursorTarget diff --git a/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs b/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs index ef5d586..6f93c14 100644 --- a/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs +++ b/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs @@ -51,6 +51,8 @@ protected virtual bool TryGetCursorTarget(TextDocumentPositionParams positionPar fileType = FileType.Weapons; else if (modManifest.CursorsFiles.Contains(fileReference)) fileType = FileType.Cursors; + else if (modManifest.SpriteSequences.Contains(fileReference)) + fileType = FileType.SpriteSequences; else if (Path.GetFileName(filePath) == "map.yaml" && symbolCache[modId].Maps.Any(x => x.MapFolder == OpenRaFolderUtils.NormalizeFilePathString(Path.GetDirectoryName(filePath)))) fileType = FileType.MapFile; @@ -58,6 +60,8 @@ protected virtual bool TryGetCursorTarget(TextDocumentPositionParams positionPar fileType = FileType.MapRules; else if (symbolCache[modId].Maps.Any(x => x.WeaponsFiles.Contains(fileReference))) fileType = FileType.MapWeapons; + else if (symbolCache[modId].Maps.Any(x => x.SpriteSequenceFiles.Contains(fileReference))) + fileType = FileType.MapSpriteSequences; if (!openFileCache.ContainsFile(fileUri.AbsoluteUri)) { @@ -144,9 +148,11 @@ protected virtual object HandlePositionalRequest(TextDocumentPositionParams posi FileType.Rules => HandleRulesFile(cursorTarget), FileType.Weapons => HandleWeaponFile(cursorTarget), FileType.Cursors => HandleCursorsFile(cursorTarget), + FileType.SpriteSequences => HandleSpriteSequenceFile(cursorTarget), FileType.MapFile => HandleMapFile(cursorTarget), FileType.MapRules => HandleRulesFile(cursorTarget), FileType.MapWeapons => HandleWeaponFile(cursorTarget), + FileType.MapSpriteSequences => HandleSpriteSequenceFile(cursorTarget), _ => null }; } @@ -191,6 +197,16 @@ protected virtual object HandleMapFile(CursorTarget cursorTarget) }; } + protected virtual object HandleSpriteSequenceFile(CursorTarget cursorTarget) + { + return cursorTarget.TargetType switch + { + "key" => HandleSpriteSequenceFileKey(cursorTarget), + "value" => HandleSpriteSequenceFileValue(cursorTarget), + _ => null + }; + } + protected virtual object HandleRulesKey(CursorTarget cursorTarget) { return null; } protected virtual object HandleRulesValue(CursorTarget cursorTarget) { return null; } @@ -207,6 +223,10 @@ protected virtual object HandleMapFile(CursorTarget cursorTarget) protected virtual object HandleMapFileValue(CursorTarget cursorTarget) { return null; } + protected virtual object HandleSpriteSequenceFileKey(CursorTarget cursorTarget) { return null; } + + protected virtual object HandleSpriteSequenceFileValue(CursorTarget cursorTarget) { return null; } + #endregion protected bool TryGetModId(string fileUri, out string modId) diff --git a/server/Oraide.LanguageServer/Extensions/LspTypesExtensions.cs b/server/Oraide.LanguageServer/Extensions/LspTypesExtensions.cs index f9db448..d64ea0c 100644 --- a/server/Oraide.LanguageServer/Extensions/LspTypesExtensions.cs +++ b/server/Oraide.LanguageServer/Extensions/LspTypesExtensions.cs @@ -128,6 +128,28 @@ public static CompletionItem ToCompletionItem(this PaletteDefinition paletteDefi }; } + public static CompletionItem ToCompletionItem(this SpriteSequenceImageDefinition spriteSequenceImageDefinition) + { + return new CompletionItem + { + Label = spriteSequenceImageDefinition.Name, + Kind = CompletionItemKind.Unit, + Detail = "Sprite sequence image definition", + CommitCharacters = new[] { ":" } + }; + } + + public static CompletionItem ToCompletionItem(this SpriteSequenceDefinition spriteSequenceDefinition) + { + return new CompletionItem + { + Label = spriteSequenceDefinition.Name, + Kind = CompletionItemKind.Value, + Detail = "Sprite sequence definition", + CommitCharacters = new[] { ":" } + }; + } + public static IEnumerable ToSymbolInformation(this ActorDefinition actorDefinition) { yield return new SymbolInformation @@ -195,5 +217,16 @@ public static SymbolInformation ToSymbolInformation(this PaletteDefinition palet Location = paletteDefinition.Location.ToLspLocation(paletteDefinition.Type.Length) }; } + + public static SymbolInformation ToSymbolInformation(this SpriteSequenceImageDefinition spriteSequenceImageDefinition) + { + return new SymbolInformation + { + Name = spriteSequenceImageDefinition.Name, + Kind = SymbolKind.Struct, + Tags = Array.Empty(), + Location = spriteSequenceImageDefinition.Location.ToLspLocation(spriteSequenceImageDefinition.Name.Length) + }; + } } } diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs index 07e5541..7a279bb 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs @@ -23,6 +23,7 @@ public class TextDocumentCompletionHandler : BaseRpcMessageHandler IEnumerable conditionNames; IEnumerable cursorNames; IEnumerable paletteNames; + IEnumerable spriteSequenceImageNames; WeaponInfo weaponInfo; readonly CompletionItem inheritsCompletionItem = new () @@ -33,6 +34,14 @@ public class TextDocumentCompletionHandler : BaseRpcMessageHandler CommitCharacters = new[] { ":" } }; + readonly CompletionItem defaultsCompletionItem = new() + { + Label = "Defaults", + Kind = CompletionItemKind.Constructor, + Detail = "Sets default values for all sequences of this image.", + CommitCharacters = new[] { ":" } + }; + readonly CompletionItem warheadCompletionItem = new () { Label = "Warhead", @@ -97,12 +106,16 @@ protected override bool TryGetCursorTarget(TextDocumentPositionParams positionPa fileType = FileType.Weapons; else if (modManifest.CursorsFiles.Contains(fileReference)) fileType = FileType.Cursors; + else if (modManifest.SpriteSequences.Contains(fileReference)) + fileType = FileType.SpriteSequences; else if (Path.GetFileName(filePath) == "map.yaml" && symbolCache[modId].Maps.Any(x => x.MapFolder == Path.GetDirectoryName(filePath))) fileType = FileType.MapFile; else if (symbolCache[modId].Maps.Any(x => x.RulesFiles.Contains(fileReference))) fileType = FileType.MapRules; else if (symbolCache[modId].Maps.Any(x => x.WeaponsFiles.Contains(fileReference))) fileType = FileType.MapWeapons; + else if (symbolCache[modId].Maps.Any(x => x.SpriteSequenceFiles.Contains(fileReference))) + fileType = FileType.MapSpriteSequences; if (!openFileCache.ContainsFile(fileUri.AbsoluteUri)) { @@ -167,6 +180,7 @@ protected override void Initialize(CursorTarget cursorTarget) conditionNames = symbolCache[modId].ModSymbols.ConditionDefinitions.Select(x => x.First().ToCompletionItem()); cursorNames = symbolCache[modId].ModSymbols.CursorDefinitions.Select(x => x.First().ToCompletionItem()); paletteNames = symbolCache[modId].ModSymbols.PaletteDefinitions.Select(x => x.First().ToCompletionItem()); + spriteSequenceImageNames = symbolCache[modId].ModSymbols.SpriteSequenceImageDefinitions.Select(x => x.First().ToCompletionItem()); weaponInfo = symbolCache[modId].CodeSymbols.WeaponInfo; } @@ -409,6 +423,75 @@ protected override IEnumerable HandleMapFileValue(CursorTarget c } } + protected override IEnumerable HandleSpriteSequenceFileKey(CursorTarget cursorTarget) + { + IEnumerable HandleSpriteSequenceProperty() + { + var presentProperties = cursorTarget.TargetNode.ParentNode.ChildNodes.Select(x => x.Key).ToHashSet(); + var spriteSequenceFormat = symbolCache[modId].ModManifest.SpriteSequenceFormat.Type; + + // NOTE: This is copied from HandleRulesKey()! + return symbolCache[modId].CodeSymbols.SpriteSequenceInfos[spriteSequenceFormat] + .SelectMany(x => x.PropertyInfos) + .DistinctBy(y => y.Name) + .Where(x => !presentProperties.Contains(x.Name)) + .Select(z => z.ToCompletionItem()); + } + + switch (cursorTarget.TargetNodeIndentation) + { + // Get only sprite sequence image definitions. Presumably for reference and for overriding purposes. + case 0: + return spriteSequenceImageNames; + + // Get only "Inherits" and "Defaults". + case 1: + return new[] { inheritsCompletionItem, defaultsCompletionItem }; + + case 2: + return HandleSpriteSequenceProperty(); + + case 4: + { + if (cursorTarget.TargetNode.ParentNode.ParentNode.Key == "Combine") + return HandleSpriteSequenceProperty(); + + return Enumerable.Empty(); + } + + default: + return Enumerable.Empty(); + } + } + + protected override IEnumerable HandleSpriteSequenceFileValue(CursorTarget cursorTarget) + { + switch (cursorTarget.TargetNodeIndentation) + { + case 1: + { + if (cursorTarget.TargetNode.Key == "Inherits") + { + if (cursorTarget.FileType == FileType.MapSpriteSequences) + { + var mapReference = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.SpriteSequenceFiles.Contains(cursorTarget.FileReference)); + + if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + return spriteSequenceImageNames.Union(mapSymbols.SpriteSequenceImageDefinitions.Select(x => x.First().ToCompletionItem())); + } + + return spriteSequenceImageNames; + } + + return Enumerable.Empty(); + } + + default: + return Enumerable.Empty(); + } + } + #endregion } } diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs index 2fcec27..3d1a705 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs @@ -368,6 +368,80 @@ protected override IEnumerable HandleMapFileValue(CursorTarget cursorT } } + // NOTE: Rules and Weapons don't handle these the same ;( + protected override IEnumerable HandleSpriteSequenceFileKey(CursorTarget cursorTarget) + { + IEnumerable HandleSpriteSequenceProperty() + { + var spriteSequenceFormat = symbolCache[cursorTarget.ModId].ModManifest.SpriteSequenceFormat.Type; + var spriteSequenceType = symbolCache[cursorTarget.ModId].CodeSymbols.SpriteSequenceInfos[spriteSequenceFormat].First(); + + var fieldInfo = spriteSequenceType.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + if (fieldInfo.Name != null) + return new[] { fieldInfo.Location.ToLspLocation(cursorTarget.TargetString.Length) }; + + return Enumerable.Empty(); + } + + switch (cursorTarget.TargetNodeIndentation) + { + // NOTE: Copied from HandleWeaponsFileKey. + case 0: + { + var imageDefinitions = modSymbols.SpriteSequenceImageDefinitions.FirstOrDefault(x => x.Key == cursorTarget.TargetString); + if (imageDefinitions != null) + return imageDefinitions.Select(x => x.Location.ToLspLocation(imageDefinitions.Key.Length)); + + return Enumerable.Empty(); + } + + case 2: + return HandleSpriteSequenceProperty(); + + case 4: + { + if (cursorTarget.TargetNode.ParentNode.ParentNode.Key == "Combine") + return HandleSpriteSequenceProperty(); + + return Enumerable.Empty(); + } + + default: + return Enumerable.Empty(); + } + } + + protected override IEnumerable HandleSpriteSequenceFileValue(CursorTarget cursorTarget) + { + switch (cursorTarget.TargetNodeIndentation) + { + // NOTE: Copied from HandleWeaponsFileKey. + case 1: + { + if (cursorTarget.TargetNode.Key == "Inherits") + { + if (modSymbols.SpriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + return modSymbols.SpriteSequenceImageDefinitions[cursorTarget.TargetString].Select(x => x.Location.ToLspLocation(x.Name.Length)); + + if (cursorTarget.FileType == FileType.MapRules) + { + var mapReference = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.SpriteSequenceFiles.Contains(cursorTarget.FileReference)); + + if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + if (mapSymbols.SpriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + return mapSymbols.SpriteSequenceImageDefinitions[cursorTarget.TargetString].Select(x => x.Location.ToLspLocation(x.Name.Length)); + } + } + + return Enumerable.Empty(); + } + + default: + return Enumerable.Empty(); + } + } + #endregion } } diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs index 1cb716c..f79a785 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs @@ -515,6 +515,114 @@ protected override Hover HandleMapFileValue(CursorTarget cursorTarget) } } + protected override Hover HandleSpriteSequenceFileKey(CursorTarget cursorTarget) + { + Hover HandleSpriteSequenceName() + { + var content = $"```csharp\nSequence \"{cursorTarget.TargetString}\"\n```"; + return HoverFromHoverInfo(content, range); + } + + Hover HandleSpriteSequenceProperty() + { + var spriteSequenceFormat = symbolCache[cursorTarget.ModId].ModManifest.SpriteSequenceFormat.Type; + var fieldInfo = codeSymbols.SpriteSequenceInfos[spriteSequenceFormat].FirstOrDefault().PropertyInfos + .FirstOrDefault(x => x.Name == cursorTarget.TargetString); + + var content = fieldInfo.ToMarkdownInfoString(); + return HoverFromHoverInfo(content, range); + } + + switch (cursorTarget.TargetNodeIndentation) + { + case 0: + { + var content = $"```csharp\nImage \"{cursorTarget.TargetString}\"\n```"; + return HoverFromHoverInfo(content, range); + } + + case 1: + { + if (cursorTarget.TargetString == "Inherits") + return HoverFromHoverInfo($"Inherits (and possibly overwrites) sequences from image {cursorTarget.TargetNode.Value}", range); + + if (cursorTarget.TargetString == "Defaults") + return HoverFromHoverInfo("Sets default values for all sequences of this image.", range); + + return HandleSpriteSequenceName(); + } + + case 2: + return HandleSpriteSequenceProperty(); + + case 3: + { + if (cursorTarget.TargetNode.ParentNode.Key == "Combine") + return HandleSpriteSequenceName(); + + return null; + } + + case 4: + { + if (cursorTarget.TargetNode.ParentNode.ParentNode.Key == "Combine") + return HandleSpriteSequenceProperty(); + + return null; + } + } + + return null; + } + + protected override Hover HandleSpriteSequenceFileValue(CursorTarget cursorTarget) + { + Hover HandleSpriteSequenceFileName() + { + var content = $"```csharp\nFile \"{cursorTarget.TargetString}\"\n```"; + return HoverFromHoverInfo(content, range); + } + + switch (cursorTarget.TargetNodeIndentation) + { + case 1: + { + if (cursorTarget.TargetNode.Key == "Inherits") + { + if (modSymbols.SpriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + return HoverFromHoverInfo($"```csharp\nImage \"{cursorTarget.TargetString}\"\n```", range); + + if (cursorTarget.FileType == FileType.MapSpriteSequences) + { + var mapReference = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.SpriteSequenceFiles.Contains(cursorTarget.FileReference)); + + if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + if (mapSymbols.SpriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + return HoverFromHoverInfo($"```csharp\nImage \"{cursorTarget.TargetString}\"\n```", range); + } + } + else + { + return HandleSpriteSequenceFileName(); + } + + return null; + } + + case 3: + { + if (cursorTarget.TargetNode.ParentNode.Key == "Combine") + return HandleSpriteSequenceFileName(); + + return null; + } + + default: + return null; + } + } + #endregion private static Hover HoverFromHoverInfo(string content, Range range) diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/Workspace/WorkspaceSymbolRequestHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/Workspace/WorkspaceSymbolRequestHandler.cs index 835ff26..5055774 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/Workspace/WorkspaceSymbolRequestHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/Workspace/WorkspaceSymbolRequestHandler.cs @@ -33,8 +33,10 @@ public IEnumerable Symbols(WorkspaceSymbolParams request) .Select(cursor => cursor.First().ToSymbolInformation())); var palettes = symbolCache.ModSymbols.SelectMany(x => x.Value.ModSymbols.PaletteDefinitions .Select(palette => palette.First().ToSymbolInformation())); + var spriteSequenceImages = symbolCache.ModSymbols.SelectMany(x => x.Value.ModSymbols.SpriteSequenceImageDefinitions + .Select(spriteSequenceImage => spriteSequenceImage.First().ToSymbolInformation())); - return actors.Union(weapons).Union(conditions).Union(cursors).Union(palettes); + return actors.Union(weapons).Union(conditions).Union(cursors).Union(palettes).Union(spriteSequenceImages); } catch (Exception e) { From 8055c2d53a409d6ac10e5c0d77ec71bc83cbb8ac Mon Sep 17 00:00:00 2001 From: penev92 Date: Sun, 25 Sep 2022 21:28:04 +0300 Subject: [PATCH 6/6] Added SpriteSequence support for other file types This adds Hover/GoTo/Completion support for SpriteSequenceImage and SpriteSequence references (using `SequenceReferenceAttribute`) in Rules and Weapons files. --- .../BaseRpcMessageHandler.cs | 67 ++++++++++++++ .../TextDocumentCompletionHandler.cs | 79 ++++++++++++++++- .../TextDocumentDefinitionHandler.cs | 85 +++++++++++++++++- .../TextDocument/TextDocumentHoverHandler.cs | 88 ++++++++++++++++++- 4 files changed, 313 insertions(+), 6 deletions(-) diff --git a/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs b/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs index 6f93c14..0db0741 100644 --- a/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs +++ b/server/Oraide.LanguageServer/Abstractions/LanguageServerProtocolHandlers/BaseRpcMessageHandler.cs @@ -7,6 +7,7 @@ using OpenRA.MiniYamlParser; using Oraide.Core; using Oraide.Core.Entities; +using Oraide.Core.Entities.Csharp; using Oraide.Core.Entities.MiniYaml; using Oraide.LanguageServer.Caching; @@ -270,6 +271,72 @@ protected bool TryMergeYamlFiles(IEnumerable filePaths, out List(symbolCache[cursorTarget.ModId].ModManifest.RulesFiles); + if (mapManifest?.RulesFiles != null) + files.AddRange(mapManifest?.RulesFiles); + + return ResolveSpriteSequenceImageName(cursorTarget, fieldInfo, files); + } + + protected string ResolveSpriteSequenceImageNameForWeapons(CursorTarget cursorTarget, ClassFieldInfo fieldInfo, MapManifest? mapManifest) + { + var files = new List(symbolCache[cursorTarget.ModId].ModManifest.WeaponsFiles); + if (mapManifest?.WeaponsFiles != null) + files.AddRange(mapManifest?.WeaponsFiles); + + return ResolveSpriteSequenceImageName(cursorTarget, fieldInfo, files); + } + + private string ResolveSpriteSequenceImageName(CursorTarget cursorTarget, ClassFieldInfo fieldInfo, IEnumerable files) + { + // Initializing the target image name as the obscure default OpenRA uses - the actor name. + var imageName = cursorTarget.TargetNode.ParentNode.ParentNode.Key; + + // Resolve the actual name that we need to use. + var sequenceAttribute = fieldInfo.OtherAttributes.FirstOrDefault(x => x.Name == "SequenceReference"); + var imageFieldName = sequenceAttribute.Value != null && sequenceAttribute.Value.Contains(',') + ? sequenceAttribute.Value.Substring(0, sequenceAttribute.Value.IndexOf(',')) + : sequenceAttribute.Value; + + var modData = symbolCache[cursorTarget.ModId]; + var resolvedFileList = files.Select(x => OpenRaFolderUtils.ResolveFilePath(x, (modData.ModId, modData.ModFolder))); + if (TryMergeYamlFiles(resolvedFileList, out var nodes)) + { + // Check for overriding image names on the trait in question. + var actorNode = nodes.First(x => x.Key == cursorTarget.TargetNode.ParentNode.ParentNode.Key); + var traitNode = actorNode.Value.Nodes.FirstOrDefault(x => x.Key == cursorTarget.TargetNode.ParentNode.Key); + var imageNode = traitNode?.Value.Nodes.FirstOrDefault(x => x.Key == imageFieldName); + if (imageNode?.Value.Value != null) + imageName = imageNode.Value.Value; + } + + return imageName; + } + + protected IEnumerable GetSpriteSequencesForImage(string modId, string imageName, MapManifest? mapManifest) + { + var files = new List(symbolCache[modId].ModManifest.SpriteSequences); + if (mapManifest?.WeaponsFiles != null) + files.AddRange(mapManifest?.SpriteSequenceFiles); + + var resolvedFileList = files.Select(x => OpenRaFolderUtils.ResolveFilePath(x, (modId, symbolCache[modId].ModFolder))); + + if (TryMergeYamlFiles(resolvedFileList, out var imageNodes)) + { + var sequenceNodes = imageNodes.FirstOrDefault(x => x.Key == imageName)?.Value.Nodes; + var sequences = sequenceNodes? + .Where(x => x.Key != "Defaults") + .Select(x => new SpriteSequenceDefinition(x.Key, x.ParentNode?.Key, x.Value.Value, + new MemberLocation("", 0, 0))); + + return sequences ?? Enumerable.Empty(); + } + + return Enumerable.Empty(); + } + protected string NormalizeFilePath(string filePath) { // Because VSCode sends us weird partially-url-encoded file paths. diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs index 7a279bb..2d2f3fc 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentCompletionHandler.cs @@ -172,6 +172,7 @@ protected override void Initialize(CursorTarget cursorTarget) { modId = cursorTarget.ModId; + // TODO: Don't map everything to CompletionItems here! Defer that until we know what we need, then only map that (like in DefinitionHandler). // Using .First() is not great but we have no way to differentiate between traits of the same name // until the server learns the concept of a mod and loaded assemblies. traitNames = symbolCache[modId].CodeSymbols.TraitInfos.Where(x => !x.First().IsAbstract).Select(x => x.First().ToCompletionItem()); @@ -263,17 +264,21 @@ protected override IEnumerable HandleRulesValue(CursorTarget cur var tempCursorNames = cursorNames; var tempPaletteNames = paletteNames; + MapManifest mapManifest = default; if (cursorTarget.FileType == FileType.MapRules) { - var mapReference = symbolCache[cursorTarget.ModId].Maps + mapManifest = symbolCache[cursorTarget.ModId].Maps .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); - if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) { + // TODO: Don't map to everything CompletionItems here! Defer that until we know what we need, then only map that (like in DefinitionHandler). tempActorNames = tempActorNames.Union(mapSymbols.ActorDefinitions.Select(x => x.First().ToCompletionItem())); tempWeaponNames = tempWeaponNames.Union(mapSymbols.WeaponDefinitions.Select(x => x.First().ToCompletionItem())); tempConditionNames = tempConditionNames.Union(mapSymbols.ConditionDefinitions.Select(x => x.First().ToCompletionItem())); tempPaletteNames = tempPaletteNames.Union(mapSymbols.PaletteDefinitions.Select(x => x.First().ToCompletionItem())); + spriteSequenceImageNames = spriteSequenceImageNames.Union( + mapSymbols.SpriteSequenceImageDefinitions.Select(x => x.First().ToCompletionItem())); } } @@ -295,6 +300,23 @@ protected override IEnumerable HandleRulesValue(CursorTarget cur if (fieldInfo.OtherAttributes.Any(x => x.Name == "PaletteReference")) return tempPaletteNames.Where(x => !string.IsNullOrEmpty(x.Label)); + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (traitInfo.TraitPropertyInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value != null + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name))) + { + return spriteSequenceImageNames; + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + // Resolve sequence image inheritance so we can show all inherited sequences. + var imageName = ResolveSpriteSequenceImageNameForRules(cursorTarget, fieldInfo, mapManifest); + var sequences = GetSpriteSequencesForImage(cursorTarget.ModId, imageName, mapManifest); + return sequences.Select(x => x.ToCompletionItem()); + } + return Enumerable.Empty(); } @@ -378,7 +400,60 @@ protected override IEnumerable HandleWeaponValue(CursorTarget cu } case 2: + { + ClassFieldInfo fieldInfo = default; + var fieldInfos = Array.Empty(); + var parentNode = cursorTarget.TargetNode.ParentNode; + if (parentNode.Key == "Projectile") + { + var projectileInfo = symbolCache[modId].CodeSymbols.WeaponInfo.ProjectileInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (projectileInfo.Name != null) + { + fieldInfo = projectileInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = projectileInfo.PropertyInfos; + } + } + else if (parentNode.Key == "Warhead" || parentNode.Key.StartsWith("Warhead@")) + { + var warheadInfo = symbolCache[modId].CodeSymbols.WeaponInfo.WarheadInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (warheadInfo.Name != null) + { + fieldInfo = warheadInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = warheadInfo.PropertyInfos; + } + } + + MapManifest mapManifest = default; + if (cursorTarget.FileType == FileType.MapRules) + { + mapManifest = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); + + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) + { + spriteSequenceImageNames = spriteSequenceImageNames.Union( + mapSymbols.SpriteSequenceImageDefinitions.Select(x => x.First().ToCompletionItem())); + } + } + + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (fieldInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name))) + { + return spriteSequenceImageNames; + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + // Resolve sequence image inheritance so we can show all inherited sequences. + var imageName = ResolveSpriteSequenceImageNameForWeapons(cursorTarget, fieldInfo, mapManifest); + var sequences = GetSpriteSequencesForImage(cursorTarget.ModId, imageName, mapManifest); + return sequences.Select(x => x.ToCompletionItem()); + } + return Enumerable.Empty(); + } default: return Enumerable.Empty(); diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs index 3d1a705..959de93 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentDefinitionHandler.cs @@ -144,17 +144,24 @@ protected override IEnumerable HandleRulesValue(CursorTarget cursorTar var conditionDefinitions = modSymbols.ConditionDefinitions[cursorTarget.TargetString]; var cursorDefinitions = modSymbols.CursorDefinitions[cursorTarget.TargetString]; var paletteDefinitions = modSymbols.PaletteDefinitions[cursorTarget.TargetString]; + var spriteSequenceImageDefinitions = modSymbols.SpriteSequenceImageDefinitions; + MapManifest mapManifest = default; if (cursorTarget.FileType == FileType.MapRules) { - var mapReference = symbolCache[cursorTarget.ModId].Maps + mapManifest = symbolCache[cursorTarget.ModId].Maps .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); - if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) { actorDefinitions = actorDefinitions.Union(mapSymbols.ActorDefinitions[cursorTarget.TargetString]); weaponDefinitions = weaponDefinitions.Union(mapSymbols.WeaponDefinitions[cursorTarget.TargetString]); conditionDefinitions = conditionDefinitions.Union(mapSymbols.ConditionDefinitions[cursorTarget.TargetString]); + + spriteSequenceImageDefinitions = spriteSequenceImageDefinitions + .SelectMany(x => x) + .Union(mapSymbols.SpriteSequenceImageDefinitions.SelectMany(x => x)) + .ToLookup(x => x.Name, y => y); } } @@ -175,6 +182,22 @@ protected override IEnumerable HandleRulesValue(CursorTarget cursorTar if (fieldInfo.OtherAttributes.Any(x => x.Name == "PaletteReference")) return paletteDefinitions.Select(x => x.Location.ToLspLocation(x.Type.Length)); + + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (traitInfo.TraitPropertyInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name))) + { + return spriteSequenceImageDefinitions[cursorTarget.TargetString].Select(x => x.Location.ToLspLocation(x.Name.Length)); + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + var imageName = ResolveSpriteSequenceImageNameForRules(cursorTarget, fieldInfo, mapManifest); + return spriteSequenceImageDefinitions[imageName].SelectMany(x => x.Sequences) + .Where(x => x.Name == cursorTarget.TargetString) + .Select(x => x.Location.ToLspLocation(x.Name.Length)); + } } } @@ -287,7 +310,65 @@ protected override IEnumerable HandleWeaponValue(CursorTarget cursorTa } case 2: + { + ClassFieldInfo fieldInfo = default; + var fieldInfos = Array.Empty(); + var parentNode = cursorTarget.TargetNode.ParentNode; + if (parentNode.Key == "Projectile") + { + var projectileInfo = codeSymbols.WeaponInfo.ProjectileInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (projectileInfo.Name != null) + { + fieldInfo = projectileInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = projectileInfo.PropertyInfos; + } + } + else if (parentNode.Key == "Warhead" || parentNode.Key.StartsWith("Warhead@")) + { + var warheadInfo = codeSymbols.WeaponInfo.WarheadInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (warheadInfo.Name != null) + { + fieldInfo = warheadInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = warheadInfo.PropertyInfos; + } + } + + var spriteSequenceImageDefinitions = symbolCache[cursorTarget.ModId].ModSymbols.SpriteSequenceImageDefinitions; + + MapManifest mapManifest = default; + if (cursorTarget.FileType == FileType.MapRules) + { + mapManifest = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); + + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) + { + // Merge mod symbols with map symbols. + spriteSequenceImageDefinitions = spriteSequenceImageDefinitions + .SelectMany(x => x) + .Union(mapSymbols.SpriteSequenceImageDefinitions.SelectMany(x => x)) + .ToLookup(x => x.Name, y => y); + } + } + + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (fieldInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name))) + { + return spriteSequenceImageDefinitions[cursorTarget.TargetString].Select(x => x.Location.ToLspLocation(x.Name.Length)); + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + var imageName = ResolveSpriteSequenceImageNameForWeapons(cursorTarget, fieldInfo, mapManifest); + return spriteSequenceImageDefinitions[imageName].SelectMany(x => x.Sequences) + .Where(x => x.Name == cursorTarget.TargetString) + .Select(x => x.Location.ToLspLocation(x.Name.Length)); + } + return Enumerable.Empty(); + } default: return Enumerable.Empty(); diff --git a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs index f79a785..62ce130 100644 --- a/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs +++ b/server/Oraide.LanguageServer/LanguageServerProtocolHandlers/TextDocument/TextDocumentHoverHandler.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using LspTypes; using Oraide.Core; +using Oraide.Core.Entities.Csharp; using Oraide.Core.Entities.MiniYaml; using Oraide.LanguageServer.Abstractions.LanguageServerProtocolHandlers; using Oraide.LanguageServer.Caching; @@ -181,17 +182,23 @@ protected override Hover HandleRulesValue(CursorTarget cursorTarget) var conditionDefinitions = modSymbols.ConditionDefinitions.Select(x => x.Key); var cursorDefinitions = modSymbols.CursorDefinitions.Select(x => x.Key); var paletteDefinitions = modSymbols.PaletteDefinitions.Select(x => x.Key); + var spriteSequenceImageDefinitions = modSymbols.SpriteSequenceImageDefinitions; + MapManifest mapManifest = default; if (cursorTarget.FileType == FileType.MapRules) { - var mapReference = symbolCache[cursorTarget.ModId].Maps + mapManifest = symbolCache[cursorTarget.ModId].Maps .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); - if (mapReference.MapReference != null && symbolCache.Maps.TryGetValue(mapReference.MapReference, out var mapSymbols)) + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) { actorDefinitions = actorDefinitions.Union(mapSymbols.ActorDefinitions.Select(x => x.Key)); weaponDefinitions = weaponDefinitions.Union(mapSymbols.WeaponDefinitions.Select(x => x.Key)); conditionDefinitions = conditionDefinitions.Union(mapSymbols.ConditionDefinitions.Select(x => x.Key)); + spriteSequenceImageDefinitions = spriteSequenceImageDefinitions + .SelectMany(x => x) + .Union(mapSymbols.SpriteSequenceImageDefinitions.SelectMany(x => x)) + .ToLookup(x => x.Name, y => y); } } @@ -209,6 +216,7 @@ protected override Hover HandleRulesValue(CursorTarget cursorTarget) if (fieldInfo.OtherAttributes.Any(x => x.Name == "CursorReference") && cursorDefinitions.Contains(cursorTarget.TargetString)) { + // Maps can't define cursors, so this is fine using mod symbols only. var cursor = modSymbols.CursorDefinitions[cursorTarget.TargetString].First(); return HoverFromHoverInfo(cursor.ToMarkdownInfoString(), range); } @@ -218,6 +226,24 @@ protected override Hover HandleRulesValue(CursorTarget cursorTarget) var palette = modSymbols.PaletteDefinitions[cursorTarget.TargetString].First(); return HoverFromHoverInfo(palette.ToMarkdownInfoString(), range); } + + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (traitInfo.TraitPropertyInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name)) + && spriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + { + var image = spriteSequenceImageDefinitions[cursorTarget.TargetString].First(); + return HoverFromHoverInfo(image.ToMarkdownInfoString(), range); + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + var imageName = ResolveSpriteSequenceImageNameForRules(cursorTarget, fieldInfo, mapManifest); + var image = spriteSequenceImageDefinitions[imageName].First(); + var spriteSequence = image.Sequences.First(x => x.Name == cursorTarget.TargetString); + return HoverFromHoverInfo(spriteSequence.ToMarkdownInfoString(), range); + } } // Show explanation for world range value. @@ -396,6 +422,64 @@ protected override Hover HandleWeaponValue(CursorTarget cursorTarget) return HoverFromHoverInfo(content, range); } + ClassFieldInfo fieldInfo = default; + var fieldInfos = Array.Empty(); + var parentNode = cursorTarget.TargetNode.ParentNode; + if (parentNode.Key == "Projectile") + { + var projectileInfo = codeSymbols.WeaponInfo.ProjectileInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (projectileInfo.Name != null) + { + fieldInfo = projectileInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = projectileInfo.PropertyInfos; + } + } + else if (parentNode.Key == "Warhead" || parentNode.Key.StartsWith("Warhead@")) + { + var warheadInfo = codeSymbols.WeaponInfo.WarheadInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.ParentNode.Value); + if (warheadInfo.Name != null) + { + fieldInfo = warheadInfo.PropertyInfos.FirstOrDefault(x => x.Name == cursorTarget.TargetNode.Key); + fieldInfos = warheadInfo.PropertyInfos; + } + } + + var spriteSequenceImageDefinitions = modSymbols.SpriteSequenceImageDefinitions; + + MapManifest mapManifest = default; + if (cursorTarget.FileType == FileType.MapRules) + { + mapManifest = symbolCache[cursorTarget.ModId].Maps + .FirstOrDefault(x => x.RulesFiles.Contains(cursorTarget.FileReference)); + + if (mapManifest.MapReference != null && symbolCache.Maps.TryGetValue(mapManifest.MapReference, out var mapSymbols)) + { + // Merge mod symbols with map symbols. + spriteSequenceImageDefinitions = spriteSequenceImageDefinitions + .SelectMany(x => x) + .Union(mapSymbols.SpriteSequenceImageDefinitions.SelectMany(x => x)) + .ToLookup(x => x.Name, y => y); + } + } + + // Pretend there is such a thing as a "SequenceImageReferenceAttribute" until we add it in OpenRA one day. + // NOTE: This will improve if/when we add the attribute. + if (fieldInfos.Any(x => x.OtherAttributes.Any(y => y.Name == "SequenceReference" + && y.Value.Contains(',') ? y.Value.Substring(0, y.Value.IndexOf(',')) == fieldInfo.Name : y.Value == fieldInfo.Name)) + && spriteSequenceImageDefinitions.Contains(cursorTarget.TargetString)) + { + var image = spriteSequenceImageDefinitions[cursorTarget.TargetString].First(); + return HoverFromHoverInfo(image.ToMarkdownInfoString(), range); + } + + if (fieldInfo.OtherAttributes.Any(x => x.Name == "SequenceReference")) + { + var imageName = ResolveSpriteSequenceImageNameForWeapons(cursorTarget, fieldInfo, mapManifest); + var image = spriteSequenceImageDefinitions[imageName].First(); + var spriteSequence = image.Sequences.First(x => x.Name == cursorTarget.TargetString); + return HoverFromHoverInfo(spriteSequence.ToMarkdownInfoString(), range); + } + return null; }