diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs index b640f43d2b..94f2ce4d49 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractSpriteSequenceDocsCommand.cs @@ -50,6 +50,7 @@ namespace OpenRA.Mods.Common.UtilityCommands static string GenerateJson(string version, IEnumerable sequenceTypes) { var relatedEnumTypes = new HashSet(); + var pdbReaderCache = Utilities.CreatePdbReaderCache(); var sequenceTypesInfo = sequenceTypes .Where(x => !x.ContainsGenericParameters && !x.IsAbstract) @@ -58,6 +59,7 @@ namespace OpenRA.Mods.Common.UtilityCommands type.Namespace, type.Name, Description = string.Join(" ", Utility.GetCustomAttributes(type, false).SelectMany(d => d.Lines)), + Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache), InheritedTypes = type.BaseTypes() .Select(y => y.Name) .Where(y => y != type.Name && y != "Object"), diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs index 95ee951600..eac96b8330 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractTraitDocsCommand.cs @@ -48,6 +48,7 @@ namespace OpenRA.Mods.Common.UtilityCommands static string GenerateJson(string version, IEnumerable traitTypes, ObjectCreator objectCreator) { var relatedEnumTypes = new HashSet(); + var pdbReaderCache = Utilities.CreatePdbReaderCache(); var traitTypesInfo = traitTypes .Where(x => !x.ContainsGenericParameters && !x.IsAbstract) @@ -56,6 +57,7 @@ namespace OpenRA.Mods.Common.UtilityCommands type.Namespace, Name = type.Name.EndsWith("Info", StringComparison.Ordinal) ? type.Name[..^4] : type.Name, Description = string.Join(" ", Utility.GetCustomAttributes(type, false).SelectMany(d => d.Lines)), + Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache), RequiresTraits = RequiredTraitTypes(type) .Select(y => y.Name), InheritedTypes = type.BaseTypes() diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs index b9e8f285a8..4d3cf3e7f9 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractWeaponDocsCommand.cs @@ -53,6 +53,7 @@ namespace OpenRA.Mods.Common.UtilityCommands static string GenerateJson(string version, IEnumerable weaponTypes, ObjectCreator objectCreator) { var relatedEnumTypes = new HashSet(); + var pdbReaderCache = Utilities.CreatePdbReaderCache(); var weaponTypesInfo = weaponTypes .Where(x => !x.ContainsGenericParameters && !x.IsAbstract) @@ -61,6 +62,7 @@ namespace OpenRA.Mods.Common.UtilityCommands type.Namespace, Name = type.Name.EndsWith("Info", StringComparison.Ordinal) ? type.Name[..^4] : type.Name, Description = string.Join(" ", Utility.GetCustomAttributes(type, false).SelectMany(d => d.Lines)), + Filename = Utilities.GetSourceFilenameFromPdb(type, pdbReaderCache), InheritedTypes = type.BaseTypes() .Select(y => y.Name) .Where(y => y != type.Name && y != $"{type.Name}Info" && y != "Object"), diff --git a/OpenRA.Mods.Common/UtilityCommands/Utilities.cs b/OpenRA.Mods.Common/UtilityCommands/Utilities.cs index 48e61433c3..786767c79a 100644 --- a/OpenRA.Mods.Common/UtilityCommands/Utilities.cs +++ b/OpenRA.Mods.Common/UtilityCommands/Utilities.cs @@ -11,7 +11,12 @@ using System; using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using OpenRA.FileSystem; +using OpenRA.Primitives; namespace OpenRA.Mods.Common.UtilityCommands { @@ -48,5 +53,51 @@ namespace OpenRA.Mods.Common.UtilityCommands var topLevelNodes = MiniYaml.Load(fs, manifestNodes, mapProperty); return topLevelNodes.FirstOrDefault(n => n.Key == key); } + + public static Cache CreatePdbReaderCache() + { + return new Cache(assemblyPath => + { + var pdbPath = Path.ChangeExtension(assemblyPath, "pdb"); + using var fs = new FileStream(pdbPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + var provider = MetadataReaderProvider.FromPortablePdbStream(fs); + return provider.GetMetadataReader(); + }); + } + + public static string GetSourceFilenameFromPdb(Type type, Cache pdbReaderCache) + { + var filename = "(unknown)"; + try + { + var pdb = pdbReaderCache[type.Assembly.Location]; + + // Enumerate over ctors before other methods (in case this type is defined across multiple files) + var methodInfos = type.GetConstructors().Cast().Concat(type.GetMethods()); + foreach (var mi in methodInfos) + { + var definitionHandle = (MethodDefinitionHandle)MetadataTokens.Handle(mi.MetadataToken); + var sequencePoints = pdb.GetMethodDebugInformation(definitionHandle.ToDebugInformationHandle()) + .GetSequencePoints() + .ToList(); + + if (sequencePoints.Count == 0) + continue; + + filename = pdb.GetString(pdb.GetDocument(sequencePoints[0].Document).Name); + + // Remove the common path prefix to give a path relative to the repository root + for (var i = 0; i < filename.Length; i++) + if (filename[i] != type.Assembly.Location[i]) + return filename[i..]; + } + } + catch + { + // Ignored + } + + return filename; + } } }