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; }