Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using OpenRA.Mods.Common.UtilityCommands.Documentation.Objects;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Mods.Common.UtilityCommands.Documentation
|
||||
{
|
||||
public static class DocumentationHelpers
|
||||
{
|
||||
// CustomDebugInformation specification.
|
||||
// https://github.com/dotnet/roslyn/blob/main/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs
|
||||
static readonly Guid TypeDefinitionDocumentGuid = new("932E74BC-DBA9-4478-8D46-0F32A7BAB3D3");
|
||||
|
||||
public static IEnumerable<ExtractedClassFieldInfo> GetClassFieldInfos(Type type, IEnumerable<FieldLoader.FieldLoadInfo> fields,
|
||||
HashSet<Type> relatedEnumTypes, ObjectCreator objectCreator)
|
||||
{
|
||||
return fields
|
||||
.Select(fi =>
|
||||
{
|
||||
if (fi.Field.FieldType.IsEnum)
|
||||
relatedEnumTypes.Add(fi.Field.FieldType);
|
||||
|
||||
return new ExtractedClassFieldInfo
|
||||
{
|
||||
PropertyName = fi.YamlName,
|
||||
DefaultValue = FieldSaver.SaveField(objectCreator.CreateBasic(type), fi.Field.Name).Value.Value,
|
||||
InternalType = Util.InternalTypeName(fi.Field.FieldType),
|
||||
UserFriendlyType = Util.FriendlyTypeName(fi.Field.FieldType),
|
||||
Description = string.Join(" ", Utility.GetCustomAttributes<DescAttribute>(fi.Field, true).SelectMany(d => d.Lines)),
|
||||
OtherAttributes = fi.Field.CustomAttributes
|
||||
.Where(a => a.AttributeType.Name != nameof(DescAttribute) && a.AttributeType.Name != nameof(FieldLoader.LoadUsingAttribute))
|
||||
.Select(a =>
|
||||
{
|
||||
var name = a.AttributeType.Name;
|
||||
name = name.EndsWith("Attribute", StringComparison.Ordinal) ? name[..^9] : name;
|
||||
|
||||
return new ExtractedClassFieldAttributeInfo
|
||||
{
|
||||
Name = name,
|
||||
Parameters = a.Constructor.GetParameters()
|
||||
.Select(pi => new ExtractedClassFieldAttributeInfo.Parameter
|
||||
{
|
||||
Name = pi.Name,
|
||||
Value = Util.GetAttributeParameterValue(a.ConstructorArguments[pi.Position])
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<ExtractedEnumInfo> GetRelatedEnumInfos(
|
||||
HashSet<Type> relatedEnumTypes, Cache<string, IReadOnlyDictionary<string, ImmutableArray<string>>> pdbTypesCache)
|
||||
{
|
||||
return relatedEnumTypes.OrderBy(t => t.Name).Select(type => new ExtractedEnumInfo
|
||||
{
|
||||
Namespace = type.Namespace,
|
||||
Name = type.Name,
|
||||
Filename = GetSourceFilenameForType(type, pdbTypesCache),
|
||||
Values = Enum.GetNames(type).ToDictionary(x => Convert.ToInt32(Enum.Parse(type, x), NumberFormatInfo.InvariantInfo), y => y)
|
||||
});
|
||||
}
|
||||
|
||||
public static Cache<string, IReadOnlyDictionary<string, ImmutableArray<string>>> CreatePdbTypesCache()
|
||||
{
|
||||
return new Cache<string, IReadOnlyDictionary<string, ImmutableArray<string>>>(BuildTypeMap);
|
||||
}
|
||||
|
||||
public static string GetSourceFilenameForType(Type type,
|
||||
Cache<string, IReadOnlyDictionary<string, ImmutableArray<string>>> pdbTypesCache)
|
||||
{
|
||||
foreach (var file in pdbTypesCache[type.Assembly.Location])
|
||||
foreach (var t in file.Value)
|
||||
if (t.EndsWith($".{type.Name}", StringComparison.InvariantCultureIgnoreCase))
|
||||
return file.Key;
|
||||
|
||||
return "(unknown)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a map of document → contained types for the given assembly.
|
||||
/// </summary>
|
||||
static ReadOnlyDictionary<string, ImmutableArray<string>> BuildTypeMap(string assemblyPath)
|
||||
{
|
||||
// Open the PE (DLL/EXE) and get a MetadataReader for the TypeDefinitions.
|
||||
var dllBytes = File.ReadAllBytes(assemblyPath).ToImmutableArray();
|
||||
var pe = new PEReader(dllBytes);
|
||||
var peReader = pe.GetMetadataReader();
|
||||
|
||||
// Open the PDB and get a MetadataReader for the Documents.
|
||||
var pdbPath = Path.ChangeExtension(assemblyPath, "pdb");
|
||||
if (!Path.Exists(pdbPath))
|
||||
return ReadOnlyDictionary<string, ImmutableArray<string>>.Empty;
|
||||
|
||||
var pdbBytes = File.ReadAllBytes(pdbPath).ToImmutableArray();
|
||||
var pdbProvider = MetadataReaderProvider.FromPortablePdbImage(pdbBytes);
|
||||
var pdbReader = pdbProvider.GetMetadataReader();
|
||||
|
||||
var typesPerFile = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var typeDefinitionHandle in peReader.TypeDefinitions)
|
||||
{
|
||||
var typeDefinition = peReader.GetTypeDefinition(typeDefinitionHandle);
|
||||
var typeName = $"{peReader.GetString(typeDefinition.Namespace)}.{peReader.GetString(typeDefinition.Name)}";
|
||||
var documents = GetDocumentsForType(typeDefinition, typeDefinitionHandle, pdbReader);
|
||||
|
||||
foreach (var documentHandle in documents)
|
||||
{
|
||||
var filePath = pdbReader.GetString(pdbReader.GetDocument(documentHandle).Name);
|
||||
|
||||
// Remove the common path prefix to give a path relative to the repository root.
|
||||
for (var i = 0; i < filePath.Length; i++)
|
||||
{
|
||||
if (filePath[i] != assemblyPath[i])
|
||||
{
|
||||
filePath = filePath[i..];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!typesPerFile.TryGetValue(filePath, out var list))
|
||||
typesPerFile[filePath] = list = [];
|
||||
|
||||
list.Add(typeName);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReadOnlyDictionary<string, ImmutableArray<string>>(typesPerFile.ToDictionary(x => x.Key, y => y.Value.ToImmutableArray()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>Collects all source documents that can be associated with a given type.</para>
|
||||
/// <para>
|
||||
/// A document may be associated with a type in multiple ways:
|
||||
/// 1. Via methods declared on the type (method-level debug info).
|
||||
/// 2. Via sequence points inside those methods (IL-to-source mappings).
|
||||
/// 3. Via a type-level fallback document stored in custom debug information
|
||||
/// when the type has no debuggable methods.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A set of unique document handles.</returns>
|
||||
static HashSet<DocumentHandle> GetDocumentsForType(TypeDefinition typeDefinition, TypeDefinitionHandle typeDefinitionHandle, MetadataReader pdbReader)
|
||||
{
|
||||
var documents = new HashSet<DocumentHandle>();
|
||||
|
||||
// Collect documents referenced by methods declared on the type.
|
||||
// This includes:
|
||||
// - The primary document associated with the method itself
|
||||
// - Any additional documents referenced by sequence points within the method body
|
||||
//
|
||||
// Sequence points are required because a single method can map to multiple
|
||||
// source files (e.g. partial methods, generated code, or inlined logic).
|
||||
foreach (var methodDefinitionHandle in typeDefinition.GetMethods())
|
||||
{
|
||||
var methodDebugInformation = pdbReader.GetMethodDebugInformation(methodDefinitionHandle);
|
||||
if (!methodDebugInformation.Document.IsNil)
|
||||
documents.Add(methodDebugInformation.Document);
|
||||
|
||||
foreach (var sequencePoint in methodDebugInformation.GetSequencePoints())
|
||||
if (!sequencePoint.Document.IsNil)
|
||||
documents.Add(sequencePoint.Document);
|
||||
}
|
||||
|
||||
// Fallback for types with no method-level debug information.
|
||||
//
|
||||
// Some types (e.g. empty types, marker interfaces, or types stripped of methods)
|
||||
// still have an associated source document recorded at the type level.
|
||||
// This information is stored as custom debug information (CDI) on the type.
|
||||
//
|
||||
// We scan the type's custom debug records and extract the document only if the
|
||||
// CDI kind matches the well-known TypeDefinitionDocument GUID.
|
||||
foreach (var customDebugInformationHandle in pdbReader.GetCustomDebugInformation(typeDefinitionHandle))
|
||||
{
|
||||
var customDebugInformation = pdbReader.GetCustomDebugInformation(customDebugInformationHandle);
|
||||
if (pdbReader.GetGuid(customDebugInformation.Kind) != TypeDefinitionDocumentGuid)
|
||||
continue;
|
||||
|
||||
var blobReader = pdbReader.GetBlobReader(customDebugInformation.Value);
|
||||
while (blobReader.Offset < blobReader.Length)
|
||||
documents.Add(MetadataTokens.DocumentHandle(blobReader.ReadCompressedInteger()));
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user