mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
0d31587855
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
1946 lines
84 KiB
C#
1946 lines
84 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.CodeAnalysis;
|
|
using Sybil;
|
|
using static Daybreak.Generators.MsvcDemangler;
|
|
using TypeKind = Daybreak.Generators.MsvcDemangler.TypeKind;
|
|
|
|
namespace Daybreak.Generators;
|
|
|
|
/// <summary>
|
|
/// Incremental source generator that reads PE exports from gwca.dll,
|
|
/// demangles MSVC-decorated C++ names to recover type and namespace
|
|
/// information, and emits a <c>GWCA</c> class with nested static classes
|
|
/// mirroring the C++ namespace hierarchy (e.g. GW::Agents → GWCA.GW.Agents).
|
|
///
|
|
/// Additionally scans C++ header files under Constants/ for enum and constexpr
|
|
/// declarations, emitting matching C# enums and const fields.
|
|
///
|
|
/// Types annotated with <c>[GWCAEquivalent("CppName")]</c> are resolved
|
|
/// automatically so the generated signatures use the correct C# types.
|
|
/// </summary>
|
|
[Generator(LanguageNames.CSharp)]
|
|
public sealed class GenerateGWCABindings : IIncrementalGenerator
|
|
{
|
|
private const string AttributeName = "GWCAEquivalentAttribute";
|
|
private const string AttributeShortName = "GWCAEquivalent";
|
|
private const string AttributeNamespace = "Daybreak.Generators";
|
|
private const string PropertyName = "GWCAName";
|
|
private const string Public = "public";
|
|
private const string StringType = "string";
|
|
private const string ParameterName = "gwcaName";
|
|
|
|
// ── Built-in type mappings (always available) ──────────────────
|
|
|
|
private static readonly Dictionary<string, string> BuiltInTypeMappings =
|
|
new()
|
|
{
|
|
["Vec2f"] = "global::System.Numerics.Vector2",
|
|
["Vec3f"] = "global::System.Numerics.Vector3",
|
|
// UIInteractionCallback is a typedef for a function pointer - map to nint
|
|
["UIInteractionCallback"] = "nint",
|
|
["UI::UIInteractionCallback"] = "nint",
|
|
["GW::UI::UIInteractionCallback"] = "nint",
|
|
};
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// IIncrementalGenerator
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
context.RegisterPostInitializationOutput(context =>
|
|
{
|
|
var compilationUnitBuilder = SyntaxBuilder.CreateCompilationUnit()
|
|
.WithNamespace(
|
|
SyntaxBuilder.CreateFileScopedNamespace(AttributeNamespace)
|
|
.WithClass(SyntaxBuilder.CreateClass(AttributeName)
|
|
.WithModifier(Public)
|
|
.WithConstructor(SyntaxBuilder.CreateConstructor(AttributeName)
|
|
.WithModifier(Public)
|
|
.WithParameter(StringType, ParameterName)
|
|
.WithBody($"this.{PropertyName} = {ParameterName};"))
|
|
.WithProperty(SyntaxBuilder.CreateProperty(StringType, PropertyName)
|
|
.WithModifier(Public)
|
|
.WithAccessor(SyntaxBuilder.CreateGetter()))
|
|
.WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage")
|
|
.WithRawArgument("AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = false, Inherited = false"))
|
|
.WithBaseClass(nameof(Attribute))));
|
|
var compilationUnitSyntax = compilationUnitBuilder.Build();
|
|
var source = compilationUnitSyntax.ToFullString();
|
|
context.AddSource($"{AttributeName}.g", source);
|
|
});
|
|
|
|
// Provider 1: DLL export names from AdditionalFiles
|
|
var exportsProvider = context.AdditionalTextsProvider
|
|
.Where(static f =>
|
|
Path.GetFileName(f.Path)
|
|
.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase))
|
|
.Select(static (f, _) =>
|
|
{
|
|
#pragma warning disable RS1035 // AdditionalText has no binary read API; file IO is required for PE parsing
|
|
try { return (f.Path, Exports: PeExportReader.ReadExportNames(File.ReadAllBytes(f.Path))); }
|
|
catch { return (f.Path, Exports: ImmutableArray<string>.Empty); }
|
|
#pragma warning restore RS1035
|
|
});
|
|
|
|
// Provider 2: [GWCAEquivalent] type mappings
|
|
var typeMappingsProvider = context.SyntaxProvider
|
|
.ForAttributeWithMetadataName(
|
|
"Daybreak.Generators.GWCAEquivalentAttribute",
|
|
predicate: static (_, _) => true,
|
|
transform: static (ctx, _) =>
|
|
{
|
|
if (ctx.Attributes.Length == 0)
|
|
return new TypeMapping(null!, null!, null!);
|
|
var attr = ctx.Attributes[0];
|
|
if (attr.ConstructorArguments.Length == 0)
|
|
return new TypeMapping(null!, null!, null!);
|
|
if (attr.ConstructorArguments[0].Value is not string gwcaName)
|
|
return new TypeMapping(null!, null!, null!);
|
|
var ns = ctx.TargetSymbol.ContainingNamespace?.ToDisplayString() ?? "";
|
|
return new TypeMapping(gwcaName, ctx.TargetSymbol.Name, ns);
|
|
})
|
|
.Where(static m => m.GwcaName is not null)
|
|
.Collect();
|
|
|
|
// Combine each matching DLL with all type mappings and generate
|
|
var combined = exportsProvider.Combine(typeMappingsProvider);
|
|
context.RegisterSourceOutput(combined, static (ctx, source) =>
|
|
EmitSource(source.Left.Path, source.Left.Exports, source.Right));
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Code generation
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static void EmitSource(
|
|
string dllPath,
|
|
ImmutableArray<string> exports,
|
|
ImmutableArray<TypeMapping> mappings)
|
|
{
|
|
if (exports.IsDefaultOrEmpty)
|
|
return;
|
|
|
|
// Build type map (gwcaName → fully qualified C# name)
|
|
var typeMap = new Dictionary<string, string>(BuiltInTypeMappings);
|
|
foreach (var m in mappings)
|
|
{
|
|
var fqn = string.IsNullOrEmpty(m.CsNamespace)
|
|
? m.CsName
|
|
: "global::" + m.CsNamespace + "." + m.CsName;
|
|
typeMap[m.GwcaName] = fqn;
|
|
}
|
|
|
|
// Build set of C export names (non-mangled exports)
|
|
var cExports = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var name in exports)
|
|
{
|
|
if (name.Length > 0 && name[0] != '?')
|
|
cExports.Add(name);
|
|
}
|
|
|
|
// Build namespace tree from mangled exports
|
|
var root = new NamespaceNode("GWCA");
|
|
int totalExports = 0;
|
|
int skippedExports = 0;
|
|
|
|
foreach (var mangledName in exports)
|
|
{
|
|
if (mangledName.Length == 0 || mangledName[0] != '?')
|
|
continue; // skip C exports
|
|
|
|
var info = Demangle(mangledName);
|
|
if (info is null)
|
|
{
|
|
skippedExports++;
|
|
continue;
|
|
}
|
|
|
|
totalExports++;
|
|
|
|
// Walk the namespace parts to find/create the correct node
|
|
var node = root;
|
|
foreach (var ns in info.NamespaceParts)
|
|
{
|
|
if (!node.Children.TryGetValue(ns, out var child))
|
|
{
|
|
child = new NamespaceNode(ns);
|
|
node.Children[ns] = child;
|
|
}
|
|
|
|
node = child;
|
|
}
|
|
|
|
node.Functions.Add(new ExportEntry(mangledName, info));
|
|
}
|
|
|
|
// ── Parse C++ headers for enums, constexpr, and structs ───
|
|
var headerRoot = new ConstantsNode("GWCA");
|
|
var dllDir = Path.GetDirectoryName(dllPath)!;
|
|
var gwcaIncludeDir = Path.Combine(dllDir, "Include", "GWCA");
|
|
#pragma warning disable RS1035
|
|
if (Directory.Exists(gwcaIncludeDir))
|
|
{
|
|
// Scan all subdirectories: Constants, GameEntities, GameContainers, Context, Managers, Packets
|
|
foreach (var subDir in Directory.GetDirectories(gwcaIncludeDir))
|
|
{
|
|
foreach (var headerFile in Directory.GetFiles(subDir, "*.h"))
|
|
{
|
|
try
|
|
{
|
|
var headerText = File.ReadAllText(headerFile);
|
|
CppHeaderParser.Parse(headerText, headerRoot);
|
|
}
|
|
catch
|
|
{
|
|
// Skip headers that fail to parse
|
|
}
|
|
}
|
|
}
|
|
// Also scan root GWCA headers
|
|
foreach (var headerFile in Directory.GetFiles(gwcaIncludeDir, "*.h"))
|
|
{
|
|
try
|
|
{
|
|
var headerText = File.ReadAllText(headerFile);
|
|
CppHeaderParser.Parse(headerText, headerRoot);
|
|
}
|
|
catch
|
|
{
|
|
// Skip headers that fail to parse
|
|
}
|
|
}
|
|
}
|
|
#pragma warning restore RS1035
|
|
|
|
// Collect names of all namespace classes from the export tree
|
|
// These will be skipped when emitting structs to avoid name collisions
|
|
var namespaceClassNames = new HashSet<string>(StringComparer.Ordinal);
|
|
CollectNamespaceClassNames(root, namespaceClassNames);
|
|
|
|
// Register generated enums in the typeMap so demangled signatures
|
|
// resolve to the generated C# enum types instead of falling back
|
|
// to int/uint. GWCAEquivalent entries take priority (already in map).
|
|
// IMPORTANT: Collect enums FIRST, then structs, so struct collision detection works.
|
|
CollectEnumMappings(headerRoot, "GWCA", typeMap);
|
|
CollectStructMappings(headerRoot, "GWCA", typeMap, namespaceClassNames, new HashSet<string>(StringComparer.Ordinal));
|
|
|
|
// Collect diagnostic info about parsed structs
|
|
var structDiagnostics = new List<string>();
|
|
CollectStructDiagnostics(headerRoot, "", structDiagnostics);
|
|
|
|
// Emit
|
|
var sb = new StringBuilder(65536);
|
|
EmitHeader(sb, totalExports, skippedExports);
|
|
|
|
// Add diagnostic comment about parsed structs
|
|
sb.AppendLine(" // ═══════════════════════════════════════════════════");
|
|
sb.AppendLine(" // Parsed structs diagnostic:");
|
|
foreach (var diag in structDiagnostics.OrderBy(d => d))
|
|
{
|
|
sb.AppendLine($" // {diag}");
|
|
}
|
|
sb.AppendLine(" // ═══════════════════════════════════════════════════");
|
|
sb.AppendLine();
|
|
|
|
// Emit the tree recursively (starting from GWCA's children)
|
|
foreach (var child in root.Children.Values.OrderBy(c => c.Name, StringComparer.Ordinal))
|
|
{
|
|
EmitNamespaceNode(sb, child, typeMap, cExports, 4);
|
|
}
|
|
|
|
// Emit constants from headers (merge into the same GWCA class)
|
|
// The headerRoot is "GWCA" → children are "GW", etc.
|
|
// Each node emits as a partial class, which C# merges with the
|
|
// identically-named class already emitted by the export tree.
|
|
// Skip any child named "GWCA" - its content should go at root level, not nested.
|
|
foreach (var child in headerRoot.Children.Values.OrderBy(c => c.Name, StringComparer.Ordinal))
|
|
{
|
|
if (child.Name == "GWCA")
|
|
{
|
|
// Emit GWCA namespace content directly at root level (not as nested class)
|
|
// But first emit its children normally
|
|
foreach (var grandchild in child.Children.Values.OrderBy(c => c.Name, StringComparer.Ordinal))
|
|
{
|
|
EmitConstantsNode(sb, grandchild, typeMap, namespaceClassNames, 4);
|
|
}
|
|
continue;
|
|
}
|
|
EmitConstantsNode(sb, child, typeMap, namespaceClassNames, 4);
|
|
}
|
|
|
|
sb.AppendLine("}"); // Close GWCA class
|
|
sb.AppendLine("}"); // Close Daybreak.API.Interop namespace
|
|
|
|
// Emit structs and enums into GuildWars namespace for consumer code compatibility
|
|
// Consumer code uses `using Daybreak.API.Interop.GuildWars;` to access types
|
|
sb.AppendLine();
|
|
sb.AppendLine("namespace Daybreak.API.Interop.GuildWars");
|
|
sb.AppendLine("{");
|
|
var emittedNames = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
// Collect inline array types needed by struct fields (for non-blittable array fields)
|
|
var inlineArrayTypes = new HashSet<(string csType, int size)>();
|
|
CollectInlineArrayTypes(headerRoot, typeMap, inlineArrayTypes);
|
|
|
|
// Emit manually-defined helper structs first (TLink, etc.)
|
|
EmitManualHelperStructs(sb, 4, emittedNames);
|
|
|
|
// Emit inline array types for non-blittable arrays (e.g., enum arrays)
|
|
EmitInlineArrayTypes(sb, 4, inlineArrayTypes, emittedNames);
|
|
|
|
// Emit enums first - they're often referenced by structs (e.g. Attribute enum vs Attribute struct)
|
|
EmitEnumsToGuildWarsNamespace(sb, headerRoot, 4, emittedNames);
|
|
EmitStructsToGuildWarsNamespace(sb, headerRoot, typeMap, 4, emittedNames, inlineArrayTypes);
|
|
sb.AppendLine();
|
|
EmitTypeAliasesToGuildWarsNamespace(sb, headerRoot, typeMap, 4, emittedNames);
|
|
sb.AppendLine("}");
|
|
|
|
// Write to Daybreak.API/Interop/GWCA.cs so LibraryImport generator can process it
|
|
// DLL is at Dependencies/GWCA/gwca.dll, so go up 2 levels to repo root
|
|
var repoRoot = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(dllPath)));
|
|
var outputPath = Path.Combine(repoRoot!, "Daybreak.API", "Interop", "GWCA.cs");
|
|
#pragma warning disable RS1035 // File IO required to emit source that LibraryImport generator can process
|
|
File.WriteAllText(outputPath, sb.ToString());
|
|
#pragma warning restore RS1035
|
|
}
|
|
|
|
// ── File header ────────────────────────────────────────────────
|
|
|
|
private static void EmitHeader(StringBuilder sb, int exportCount, int skippedCount)
|
|
{
|
|
sb.AppendLine("// <auto-generated>");
|
|
sb.AppendLine("// This file was generated by the GenerateGWCABindings source generator.");
|
|
sb.AppendLine("// Do not edit manually — changes will be overwritten on rebuild.");
|
|
sb.AppendLine("// </auto-generated>");
|
|
sb.AppendLine();
|
|
sb.AppendLine("#nullable enable");
|
|
sb.AppendLine();
|
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
sb.AppendLine("using System.Runtime.InteropServices;");
|
|
sb.AppendLine("using System.Runtime.InteropServices.Marshalling;");
|
|
sb.AppendLine();
|
|
sb.AppendLine("namespace Daybreak.API.Interop");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine();
|
|
sb.AppendLine("/// <summary>");
|
|
sb.AppendLine($"/// P/Invoke bindings for {exportCount} C++ exports from gwca.dll ({skippedCount} skipped).");
|
|
sb.AppendLine("/// Nested classes mirror the C++ namespace hierarchy (e.g. GW::Agents → GWCA.GW.Agents).");
|
|
sb.AppendLine("/// Types annotated with [GWCAEquivalent] are used in signatures where available.");
|
|
sb.AppendLine("/// </summary>");
|
|
sb.AppendLine("public static unsafe partial class GWCA");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine(" private const string DllName = \"gwca.dll\";");
|
|
}
|
|
|
|
// ── Recursive namespace/class emission ─────────────────────────
|
|
|
|
private static void EmitNamespaceNode(
|
|
StringBuilder sb,
|
|
NamespaceNode node,
|
|
Dictionary<string, string> typeMap,
|
|
HashSet<string> cExports,
|
|
int indent)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine($"{pad}public static partial class {SanitizeIdentifier(node.Name)}");
|
|
sb.AppendLine($"{pad}{{");
|
|
|
|
// Emit functions in this namespace, disambiguating overloads that
|
|
// would produce identical C# signatures
|
|
var signatureCounts = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
foreach (var entry in node.Functions.OrderBy(e => e.Info.FunctionName, StringComparer.Ordinal))
|
|
{
|
|
EmitFunction(sb, entry, typeMap, cExports, indent + 4, signatureCounts);
|
|
}
|
|
|
|
// Emit child namespaces
|
|
foreach (var child in node.Children.Values.OrderBy(c => c.Name, StringComparer.Ordinal))
|
|
{
|
|
EmitNamespaceNode(sb, child, typeMap, cExports, indent + 4);
|
|
}
|
|
|
|
sb.AppendLine($"{pad}}}");
|
|
}
|
|
|
|
// ── Single function emission ───────────────────────────────────
|
|
|
|
private static void EmitFunction(
|
|
StringBuilder sb,
|
|
ExportEntry entry,
|
|
Dictionary<string, string> typeMap,
|
|
HashSet<string> cExports,
|
|
int indent,
|
|
Dictionary<string, int> signatureCounts)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
var info = entry.Info;
|
|
|
|
// VarArgs — cannot be expressed with DllImport
|
|
if (info.IsVarArgs)
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine($"{pad}// {info.QualifiedCppName} — varargs, requires manual interop");
|
|
return;
|
|
}
|
|
|
|
var csRet = ResolveCsType(info.ReturnType, typeMap);
|
|
var retComment = TypeComment(info.ReturnType, typeMap);
|
|
|
|
// Check for unmapped struct types
|
|
bool hasUnmapped = HasUnmappedStruct(info.ReturnType, typeMap)
|
|
|| info.ParameterTypes.Any(pt => HasUnmappedStruct(pt, typeMap));
|
|
|
|
// Check if this function has std::function params and a matching C export exists
|
|
bool hasStdFunction = HasStdFunction(info.ReturnType) || info.ParameterTypes.Any(HasStdFunction);
|
|
bool useCExport = hasStdFunction && cExports.Contains(info.FunctionName);
|
|
if (useCExport)
|
|
{
|
|
// Re-evaluate hasUnmapped ignoring std::function (which becomes nint in C export)
|
|
hasUnmapped = HasUnmappedStructIgnoringStdFunction(info.ReturnType, typeMap)
|
|
|| info.ParameterTypes.Any(pt => HasUnmappedStructIgnoringStdFunction(pt, typeMap));
|
|
}
|
|
|
|
// Build parameter list
|
|
var parms = new List<string>();
|
|
var paramComments = new List<string>();
|
|
int pIdx = 0;
|
|
|
|
if (info.IsMember)
|
|
{
|
|
parms.Add("nint self");
|
|
pIdx++;
|
|
}
|
|
|
|
var usedNames = new HashSet<string>();
|
|
foreach (var pt in info.ParameterTypes)
|
|
{
|
|
// When using C export, std::function becomes a raw function pointer (nint)
|
|
var csType = (useCExport && IsStdFunction(pt))
|
|
? "nint"
|
|
: ResolveCsType(pt, typeMap);
|
|
var name = MakeParamName(pt, typeMap, pIdx,
|
|
info.ParameterTypes.Length + (info.IsMember ? 1 : 0));
|
|
while (!usedNames.Add(name))
|
|
name += "_" + pIdx;
|
|
|
|
var comment = (useCExport && IsStdFunction(pt))
|
|
? null // Don't add "unmapped" comment for std::function when using C export
|
|
: TypeComment(pt, typeMap);
|
|
if (comment is not null)
|
|
paramComments.Add(name + ": " + comment);
|
|
|
|
if (csType == "bool")
|
|
parms.Add("[MarshalAs(UnmanagedType.U1)] bool " + name);
|
|
else
|
|
parms.Add(csType + " " + name);
|
|
pIdx++;
|
|
}
|
|
|
|
var paramStr = string.Join(", ", parms);
|
|
|
|
// Comment lines
|
|
sb.AppendLine();
|
|
var commentParts = new List<string> { info.QualifiedCppName };
|
|
if (useCExport)
|
|
commentParts.Add("via C export");
|
|
if (retComment is not null)
|
|
commentParts.Add("returns " + retComment);
|
|
commentParts.AddRange(paramComments);
|
|
|
|
// Disambiguate overloads that collapse to the same C# signature
|
|
var sigKey = info.FunctionName + "(" + string.Join(",", parms.Select(p => p.Split(' ').First())) + ")";
|
|
if (!signatureCounts.TryGetValue(sigKey, out var count))
|
|
count = 0;
|
|
signatureCounts[sigKey] = count + 1;
|
|
var methodName = count == 0
|
|
? SanitizeIdentifier(info.FunctionName)
|
|
: SanitizeIdentifier(info.FunctionName) + "_" + count;
|
|
|
|
// Use C export name when available for std::function params
|
|
var entryPoint = useCExport ? info.FunctionName : entry.MangledName;
|
|
|
|
// Build LibraryImport attribute and calling convention. GWCA's free
|
|
// functions are __cdecl (the 'YA' in the mangled name) and members are
|
|
// __thiscall ('QAE'/'AAE'). The convention must be explicit on x86
|
|
// because stack cleanup differs from the Winapi/__stdcall default.
|
|
var conventionAttr = info.IsMember
|
|
? $"[UnmanagedCallConv(CallConvs = [typeof(CallConvThiscall)])]"
|
|
: $"[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]";
|
|
|
|
// Bool return type needs explicit marshalling
|
|
var retAttr = csRet == "bool" ? "[return: MarshalAs(UnmanagedType.U1)]" : "";
|
|
|
|
var prefix = hasUnmapped ? "// " : "";
|
|
sb.AppendLine($"{pad}// {string.Join(" | ", commentParts)}");
|
|
sb.AppendLine($"{pad}{prefix}[LibraryImport(DllName, EntryPoint = \"{entryPoint}\")]");
|
|
if (conventionAttr.Length > 0)
|
|
sb.AppendLine($"{pad}{prefix}{conventionAttr}");
|
|
if (retAttr.Length > 0)
|
|
sb.AppendLine($"{pad}{prefix}{retAttr}");
|
|
sb.AppendLine($"{pad}{prefix}public static partial {csRet} {methodName}({paramStr});");
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Constants / Enum emission from parsed headers
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Emits a <see cref="ConstantsNode"/> tree as nested static classes
|
|
/// containing enums and const fields. If a namespace node already
|
|
/// exists in the export tree, the constants are merged into it.
|
|
/// </summary>
|
|
private static void EmitConstantsNode(
|
|
StringBuilder sb,
|
|
ConstantsNode node,
|
|
Dictionary<string, string> typeMap,
|
|
HashSet<string> namespaceClassNames,
|
|
int indent,
|
|
string? emitNameOverride = null,
|
|
Stack<string>? ancestors = null)
|
|
{
|
|
ancestors ??= new Stack<string>();
|
|
var pad = new string(' ', indent);
|
|
|
|
// If the node has nothing to emit (no enums, no consts, no children with content), skip
|
|
if (!HasAnyContent(node))
|
|
return;
|
|
|
|
// CS0542: a nested type cannot share its name with the immediately enclosing
|
|
// type. Append underscores until unique against the immediate enclosing class.
|
|
var rawName = emitNameOverride ?? SanitizeIdentifier(node.Name);
|
|
var emitName = MakeUniqueAgainstEnclosing(rawName, ancestors);
|
|
|
|
// Always emit a partial class wrapper — C# allows multiple partial
|
|
// declarations for the same nested class, so this merges cleanly
|
|
// with any class already emitted by the export tree.
|
|
sb.AppendLine();
|
|
sb.AppendLine($"{pad}public static partial class {emitName}");
|
|
sb.AppendLine($"{pad}{{");
|
|
|
|
var innerIndent = indent + 4;
|
|
var innerPad = new string(' ', innerIndent);
|
|
|
|
// Push ourselves so descendants see us as their immediate enclosing type.
|
|
ancestors.Push(emitName);
|
|
try
|
|
{
|
|
// Emit enums
|
|
foreach (var enumDef in node.Enums.OrderBy(e => e.Name, StringComparer.Ordinal))
|
|
{
|
|
sb.AppendLine();
|
|
if (enumDef.Name is not null)
|
|
{
|
|
var baseType = MapCppTypeToCs(enumDef.UnderlyingType);
|
|
var enumName = MakeUniqueAgainstEnclosing(SanitizeIdentifier(enumDef.Name), ancestors);
|
|
sb.AppendLine($"{innerPad}public enum {enumName} : {baseType}");
|
|
sb.AppendLine($"{innerPad}{{");
|
|
foreach (var member in enumDef.Members)
|
|
{
|
|
var comment = member.Comment is not null ? $" // {member.Comment}" : "";
|
|
if (member.Value is not null)
|
|
sb.AppendLine($"{innerPad} {SanitizeIdentifier(member.Name)} = {member.Value},{comment}");
|
|
else
|
|
sb.AppendLine($"{innerPad} {SanitizeIdentifier(member.Name)},{comment}");
|
|
}
|
|
sb.AppendLine($"{innerPad}}}");
|
|
}
|
|
else
|
|
{
|
|
// Anonymous enum → emit as const fields
|
|
foreach (var member in enumDef.Members)
|
|
{
|
|
var csType = MapCppTypeToCs(enumDef.UnderlyingType);
|
|
var memberName = MakeUniqueAgainstEnclosing(SanitizeIdentifier(member.Name), ancestors);
|
|
var comment = member.Comment is not null ? $" // {member.Comment}" : "";
|
|
sb.AppendLine($"{innerPad}internal const {csType} {memberName} = {member.Value};{comment}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit constexpr fields
|
|
foreach (var field in node.Constants.OrderBy(f => f.Name, StringComparer.Ordinal))
|
|
{
|
|
var csType = MapCppTypeToCs(field.CppType);
|
|
var value = field.Value;
|
|
var fieldName = MakeUniqueAgainstEnclosing(SanitizeIdentifier(field.Name), ancestors);
|
|
var comment = field.Comment is not null ? $" // {field.Comment}" : "";
|
|
sb.AppendLine($"{innerPad}internal const {csType} {fieldName} = {value};{comment}");
|
|
}
|
|
|
|
// Type aliases are now emitted to GuildWars namespace, not nested GWCA classes
|
|
// Skip type alias emission here - see EmitTypeAliasesToGuildWarsNamespace
|
|
|
|
// Structs are now emitted to GuildWars namespace, not nested classes
|
|
// Skip struct emission here - see EmitStructsToGuildWarsNamespace
|
|
|
|
// Emit children. Each child computes its own unique name against the
|
|
// ancestor stack on entry, so no override is needed here.
|
|
foreach (var child in node.Children.Values.OrderBy(c => c.Name, StringComparer.Ordinal))
|
|
{
|
|
EmitConstantsNode(sb, child, typeMap, namespaceClassNames, innerIndent, emitNameOverride: null, ancestors);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ancestors.Pop();
|
|
}
|
|
|
|
sb.AppendLine($"{pad}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// CS0542 guard: returns <paramref name="name"/> with one or more trailing
|
|
/// underscores until it no longer collides with the immediately enclosing
|
|
/// type name (the top of the <paramref name="ancestors"/> stack).
|
|
/// </summary>
|
|
private static string MakeUniqueAgainstEnclosing(string name, Stack<string> ancestors)
|
|
{
|
|
if (ancestors.Count == 0)
|
|
return name;
|
|
|
|
var enclosing = ancestors.Peek();
|
|
var unique = name;
|
|
while (string.Equals(unique, enclosing, StringComparison.Ordinal))
|
|
unique += "_";
|
|
return unique;
|
|
}
|
|
|
|
private static bool HasAnyContent(ConstantsNode node)
|
|
{
|
|
// Note: TypeAliases and Structs are emitted to GuildWars namespace, not counted here
|
|
if (node.Enums.Count > 0 || node.Constants.Count > 0)
|
|
return true;
|
|
return node.Children.Values.Any(HasAnyContent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks the constants tree and adds each named enum to the type map.
|
|
/// Enums take priority over structs - this must be called first.
|
|
/// Existing entries (from <c>[GWCAEquivalent]</c>) are never overwritten.
|
|
/// </summary>
|
|
private static void CollectEnumMappings(
|
|
ConstantsNode node,
|
|
string csPath,
|
|
Dictionary<string, string> typeMap)
|
|
{
|
|
foreach (var enumDef in node.Enums)
|
|
{
|
|
if (enumDef.Name is null)
|
|
continue; // skip anonymous enums
|
|
|
|
if (!typeMap.ContainsKey(enumDef.Name))
|
|
typeMap[enumDef.Name] = "global::Daybreak.API.Interop." + csPath + "." + SanitizeIdentifier(enumDef.Name);
|
|
}
|
|
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
CollectEnumMappings(child, csPath + "." + SanitizeIdentifier(child.Name), typeMap);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks the constants tree and adds each struct to the type map.
|
|
/// Must be called AFTER CollectEnumMappings so struct collision detection works.
|
|
/// </summary>
|
|
private static void CollectStructMappings(
|
|
ConstantsNode node,
|
|
string csPath,
|
|
Dictionary<string, string> typeMap,
|
|
HashSet<string> namespaceClassNames,
|
|
HashSet<string> usedGuildWarsNames)
|
|
{
|
|
// First, collect all struct names in this node to avoid collisions
|
|
var structNamesInNode = new HashSet<string>(node.Structs.Select(s => s.Name), StringComparer.Ordinal);
|
|
|
|
foreach (var structDef in node.Structs)
|
|
{
|
|
// Only add structs that can actually be emitted
|
|
if (!CanEmitStruct(structDef))
|
|
continue;
|
|
|
|
// Determine the C# name, handling collisions with namespace classes or enums
|
|
var csName = SanitizeIdentifier(structDef.Name);
|
|
|
|
// Check if name collides with a namespace class, enum, or ANOTHER struct already using this name
|
|
if (namespaceClassNames.Contains(structDef.Name))
|
|
{
|
|
// Try "Data" suffix first, then "Struct" if that also collides
|
|
if (!structNamesInNode.Contains(structDef.Name + "Data") && !usedGuildWarsNames.Contains(csName + "Data"))
|
|
csName += "Data";
|
|
else if (!structNamesInNode.Contains(structDef.Name + "Struct") && !usedGuildWarsNames.Contains(csName + "Struct"))
|
|
csName += "Struct";
|
|
else
|
|
csName += "_"; // Last resort
|
|
}
|
|
// Check if name collides with an enum already in typeMap (different namespace, e.g. Constants::Attribute vs GW::Attribute)
|
|
// OR another struct already using this name in GuildWars namespace
|
|
else if (typeMap.ContainsKey(structDef.Name) || usedGuildWarsNames.Contains(csName))
|
|
{
|
|
// An enum or other type with this name exists - suffix the struct
|
|
if (!structNamesInNode.Contains(structDef.Name + "Struct") && !usedGuildWarsNames.Contains(csName + "Struct"))
|
|
csName += "Struct";
|
|
else if (!structNamesInNode.Contains(structDef.Name + "Data") && !usedGuildWarsNames.Contains(csName + "Data"))
|
|
csName += "Data";
|
|
else
|
|
csName += "_";
|
|
}
|
|
|
|
usedGuildWarsNames.Add(csName);
|
|
|
|
// Use a qualified key that includes the C++ path to distinguish structs with the same name
|
|
// from different namespaces (e.g., GW::Attribute vs GW::SkillbarMgr::Attribute)
|
|
var mapKey = structDef.Name;
|
|
var fqCsType = "global::Daybreak.API.Interop.GuildWars." + csName;
|
|
var simpleStructKey = "struct " + structDef.Name;
|
|
|
|
if (typeMap.ContainsKey(mapKey))
|
|
{
|
|
// Use qualified key to distinguish this struct from enum or another struct
|
|
// Include the csPath to make it unique (e.g., "struct GW.Attribute" vs "struct GW.SkillbarMgr.Attribute")
|
|
mapKey = "struct " + csPath.Replace("GWCA.", "") + "." + structDef.Name;
|
|
|
|
// Also add simple "struct X" key if not already taken (for field type resolution)
|
|
// This allows MapCppFieldTypeToCs to find the struct without knowing the full path
|
|
if (!typeMap.ContainsKey(simpleStructKey))
|
|
{
|
|
typeMap[simpleStructKey] = fqCsType;
|
|
}
|
|
}
|
|
|
|
// Structs go into GuildWars namespace (flat), not nested GWCA classes
|
|
typeMap[mapKey] = fqCsType;
|
|
}
|
|
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
CollectStructMappings(child, csPath + "." + SanitizeIdentifier(child.Name), typeMap, namespaceClassNames, usedGuildWarsNames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all namespace class names from the export tree recursively.
|
|
/// Used to avoid name collisions when emitting structs.
|
|
/// </summary>
|
|
private static void CollectNamespaceClassNames(NamespaceNode node, HashSet<string> names)
|
|
{
|
|
names.Add(node.Name);
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
CollectNamespaceClassNames(child, names);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects diagnostic information about parsed structs.
|
|
/// </summary>
|
|
private static void CollectStructDiagnostics(ConstantsNode node, string path, List<string> diagnostics)
|
|
{
|
|
var currentPath = string.IsNullOrEmpty(path) ? node.Name : path + "." + node.Name;
|
|
|
|
// Show namespace pop line info
|
|
if (node.DebugPopLine > 0)
|
|
{
|
|
diagnostics.Add($"[NAMESPACE] {currentPath} popped at line {node.DebugPopLine}");
|
|
}
|
|
|
|
foreach (var structDef in node.Structs)
|
|
{
|
|
var (canEmit, reason) = CanEmitStructWithReason(structDef);
|
|
var status = canEmit ? "OK" : $"SKIP: {reason}";
|
|
diagnostics.Add($"{currentPath}.{structDef.Name}: {structDef.Fields.Count} fields [{status}]");
|
|
}
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
CollectStructDiagnostics(child, currentPath, diagnostics);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a struct can be emitted (has no complex template fields or unresolved types).
|
|
/// </summary>
|
|
private static (bool canEmit, string? reason) CanEmitStructWithReason(CppStructDef structDef)
|
|
{
|
|
// Skip structs with no fields (usually forward declarations that got parsed)
|
|
if (structDef.Fields.Count == 0)
|
|
return (false, "no fields");
|
|
|
|
// Check for mixed offset/no-offset fields (can't use Explicit layout if not all have offsets)
|
|
bool hasAnyOffset = structDef.Fields.Any(f => f.Offset.HasValue);
|
|
bool allHaveOffsets = structDef.Fields.All(f => f.Offset.HasValue);
|
|
if (hasAnyOffset && !allHaveOffsets)
|
|
return (false, "mixed offset fields");
|
|
|
|
// Check for unresolvable fields (template types, missing types)
|
|
foreach (var field in structDef.Fields)
|
|
{
|
|
var cppType = field.CppType;
|
|
// Skip structs with template type parameters (T, etc.)
|
|
if (Regex.IsMatch(cppType, @"\bT\b") && !cppType.Contains("Array"))
|
|
return (false, $"template param T in field {field.Name}");
|
|
// Skip structs with complex template containers we can't represent
|
|
// Note: TLink<T> is handled separately in MapCppFieldTypeToCs (8-byte linked list node)
|
|
if (cppType.Contains("TList<") ||
|
|
cppType.Contains("PrioQ<") || cppType.Contains("PrioQLink<") ||
|
|
cppType.Contains("BaseArray<"))
|
|
return (false, $"complex template in field {field.Name}: {cppType}");
|
|
// Skip if field type contains unresolved typedef names
|
|
// Note: UIInteractionCallback is now mapped to nint in BuiltInTypeMappings
|
|
// Note: FrameRelation* (pointer) maps to nint automatically, but embedded FrameRelation
|
|
// is a problem because the struct itself is skipped (contains TList<FrameRelation>)
|
|
if (cppType.Contains("FriendsListArray") ||
|
|
cppType.Contains("PathNodeArray") || cppType.Contains("PathingMapArray") ||
|
|
cppType.Contains("BlockedPlaneArray") || cppType.Contains("AgentSummaryInfoSub") ||
|
|
cppType.Contains("EffectData") ||
|
|
cppType.Contains("SkillbarSkillData") || cppType.Contains("SkillbarData") ||
|
|
cppType == "FrameRelation") // Embedded FrameRelation - struct is skipped due to TList<>
|
|
return (false, $"unresolved typedef in field {field.Name}: {cppType}");
|
|
// Skip structs with function pointer fields (complex vtable types)
|
|
if (cppType.Contains("__fastcall") || cppType.Contains("__stdcall") ||
|
|
cppType.Contains("(__cdecl") || Regex.IsMatch(cppType, @"\(\s*\*\s*\w+\s*\)"))
|
|
return (false, $"function pointer in field {field.Name}: {cppType}");
|
|
// Skip structs with inline initializers in field declarations (e.g., "uint32_t k[4]{}")
|
|
if (cppType.Contains("{}"))
|
|
return (false, $"inline initializer in field {field.Name}: {cppType}");
|
|
// Skip fields referencing complex struct types we can't emit
|
|
// These are structs that have vtables, function pointers, or complex layouts
|
|
if (cppType.Contains("EquipmentVTable") || cppType.Contains("VTable"))
|
|
return (false, $"vtable in field {field.Name}: {cppType}");
|
|
}
|
|
return (true, null);
|
|
}
|
|
|
|
private static bool CanEmitStruct(CppStructDef structDef) => CanEmitStructWithReason(structDef).canEmit;
|
|
|
|
/// <summary>
|
|
/// Emits a C# struct from a parsed C++ struct definition.
|
|
/// </summary>
|
|
private static void EmitStruct(StringBuilder sb, CppStructDef structDef, Dictionary<string, string> typeMap, int indent, HashSet<(string csType, int size)>? inlineArrayTypes = null, string? csStructName = null)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
// Skip structs that can't be emitted
|
|
if (!CanEmitStruct(structDef))
|
|
return;
|
|
|
|
// Use provided name or default to sanitized original name
|
|
var structName = csStructName ?? SanitizeIdentifier(structDef.Name);
|
|
|
|
sb.AppendLine();
|
|
|
|
// Determine layout strategy based on whether we have offsets
|
|
bool hasOffsets = structDef.Fields.Any(f => f.Offset.HasValue);
|
|
var layoutKind = hasOffsets ? "Explicit" : "Sequential";
|
|
|
|
// Build the struct attributes
|
|
if (structDef.Size.HasValue)
|
|
sb.AppendLine($"{pad}[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.{layoutKind}, Pack = 1, Size = 0x{structDef.Size.Value:X})]");
|
|
else
|
|
sb.AppendLine($"{pad}[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.{layoutKind}, Pack = 1)]");
|
|
|
|
sb.AppendLine($"{pad}public unsafe struct {structName}");
|
|
sb.AppendLine($"{pad}{{");
|
|
|
|
var innerPad = new string(' ', indent + 4);
|
|
|
|
foreach (var field in structDef.Fields)
|
|
{
|
|
var csType = MapCppFieldTypeToCs(field.CppType, typeMap);
|
|
var fieldName = SanitizeIdentifier(ToPascalCase(field.Name));
|
|
var comment = field.Comment is not null ? $" // {field.Comment}" : "";
|
|
|
|
// Emit FieldOffset if we have explicit layout
|
|
if (hasOffsets && field.Offset.HasValue)
|
|
sb.AppendLine($"{innerPad}[global::System.Runtime.InteropServices.FieldOffset(0x{field.Offset.Value:X4})]");
|
|
|
|
// Handle fixed-size arrays
|
|
if (field.ArraySize is not null)
|
|
{
|
|
var size = ParseArraySize(field.ArraySize);
|
|
// Skip zero-length arrays (flexible array members in C++) - just add a comment
|
|
if (size <= 0)
|
|
{
|
|
sb.AppendLine($"{innerPad}// Flexible array member: {csType} {fieldName}[0]");
|
|
continue;
|
|
}
|
|
// For fixed arrays, we need a fixed buffer or explicit FieldOffset per element
|
|
// Use fixed buffer for blittable types
|
|
if (IsBlittableForFixed(csType))
|
|
{
|
|
sb.AppendLine($"{innerPad}public fixed {csType} {fieldName}[{size}];{comment}");
|
|
}
|
|
else if (inlineArrayTypes is not null && inlineArrayTypes.Contains((csType, size)))
|
|
{
|
|
// Use the inline array type we generated
|
|
var inlineArrayTypeName = GetInlineArrayTypeName(csType, size);
|
|
sb.AppendLine($"{innerPad}public {inlineArrayTypeName} {fieldName};{comment}");
|
|
}
|
|
else
|
|
{
|
|
// Fallback: use single element with comment (shouldn't happen if inlineArrayTypes is properly populated)
|
|
sb.AppendLine($"{innerPad}public {csType} {fieldName}; // [{field.ArraySize}]{comment}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine($"{innerPad}public {csType} {fieldName};{comment}");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine($"{pad}}}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits all emittable structs from the constants tree into the GuildWars namespace.
|
|
/// This provides compatibility for consumer code that uses `using Daybreak.API.Interop.GuildWars;`
|
|
/// </summary>
|
|
private static void EmitStructsToGuildWarsNamespace(StringBuilder sb, ConstantsNode node, Dictionary<string, string> typeMap, int indent, HashSet<string> emittedNames, HashSet<(string csType, int size)> inlineArrayTypes)
|
|
{
|
|
// Start with the node's own name as the path base (GWCA for headerRoot)
|
|
EmitStructsRecursive(sb, node, node.Name, typeMap, indent, emittedNames, inlineArrayTypes);
|
|
}
|
|
|
|
private static void EmitStructsRecursive(StringBuilder sb, ConstantsNode node, string currentPath, Dictionary<string, string> typeMap, int indent, HashSet<string> emittedNames, HashSet<(string csType, int size)> inlineArrayTypes)
|
|
{
|
|
foreach (var structDef in node.Structs.OrderBy(s => s.Name, StringComparer.Ordinal))
|
|
{
|
|
// Skip structs that can't be emitted
|
|
if (!CanEmitStruct(structDef))
|
|
continue;
|
|
|
|
// Get the C# name from typeMap (which includes collision-resolution suffixes like "Struct")
|
|
// Try direct name first, then qualified struct key (for collision cases)
|
|
string? fqCsName;
|
|
var directLookup = typeMap.TryGetValue(structDef.Name, out fqCsName);
|
|
var containsGuildWars = fqCsName?.Contains("GuildWars.") ?? false;
|
|
|
|
if (!directLookup || !containsGuildWars)
|
|
{
|
|
// If direct lookup failed or returned an enum (not in GuildWars namespace),
|
|
// try qualified struct key: "struct GW.StructName"
|
|
var qualifiedKey = "struct " + currentPath.Replace("GWCA.", "") + "." + structDef.Name;
|
|
typeMap.TryGetValue(qualifiedKey, out fqCsName);
|
|
}
|
|
|
|
if (fqCsName is null)
|
|
continue;
|
|
|
|
// Extract just the struct name from the fully-qualified name
|
|
// e.g. "global::Daybreak.API.Interop.GuildWars.ItemStruct" -> "ItemStruct"
|
|
var csName = fqCsName.Substring(fqCsName.LastIndexOf('.') + 1);
|
|
|
|
// Skip duplicates (including if an enum with the same name was emitted)
|
|
if (emittedNames.Contains(csName))
|
|
continue;
|
|
|
|
emittedNames.Add(csName);
|
|
EmitStruct(sb, structDef, typeMap, indent, inlineArrayTypes, csName);
|
|
}
|
|
|
|
// Recurse into children, appending the child's name to the current path
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
EmitStructsRecursive(sb, child, currentPath + "." + child.Name, typeMap, indent, emittedNames, inlineArrayTypes);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits manually-defined helper structs that can't be parsed from headers.
|
|
/// These are typically C++ template containers with known fixed layouts.
|
|
/// </summary>
|
|
private static void EmitManualHelperStructs(StringBuilder sb, int indent, HashSet<string> emittedNames)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
// TLink<T> - doubly-linked list node used in Agent and other structs
|
|
// C++ definition: struct TLink { TLink* prev_link; T* next_node; }
|
|
// Size: 8 bytes (2 pointers on x86)
|
|
if (emittedNames.Add("TLink"))
|
|
{
|
|
sb.AppendLine($"{pad}/// <summary>");
|
|
sb.AppendLine($"{pad}/// TLink<T> - doubly-linked list node (8 bytes: 2 pointers).");
|
|
sb.AppendLine($"{pad}/// Used in Agent structs for linked list chaining.");
|
|
sb.AppendLine($"{pad}/// </summary>");
|
|
sb.AppendLine($"{pad}[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)]");
|
|
sb.AppendLine($"{pad}public struct TLink");
|
|
sb.AppendLine($"{pad}{{");
|
|
sb.AppendLine($"{pad} public nint PrevLink;");
|
|
sb.AppendLine($"{pad} public nint NextNode;");
|
|
sb.AppendLine($"{pad}}}");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
// THash<T> - GWCA hash table, fixed 0x24 (36) bytes regardless of T.
|
|
// Treated as an opaque blob; consumers use GWCA helpers for traversal.
|
|
// See Dependencies/GWCA/Include/GWCA/GameContainers/Hash.h.
|
|
if (emittedNames.Add("THash"))
|
|
{
|
|
sb.AppendLine($"{pad}/// <summary>");
|
|
sb.AppendLine($"{pad}/// THash<T> - hash table (36 bytes, opaque to managed code).");
|
|
sb.AppendLine($"{pad}/// </summary>");
|
|
sb.AppendLine($"{pad}[global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1, Size = 0x24)]");
|
|
sb.AppendLine($"{pad}public struct THash");
|
|
sb.AppendLine($"{pad}{{");
|
|
sb.AppendLine($"{pad}}}");
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collects all inline array types needed for non-blittable array fields in emittable structs.
|
|
/// </summary>
|
|
private static void CollectInlineArrayTypes(ConstantsNode node, Dictionary<string, string> typeMap, HashSet<(string csType, int size)> inlineArrayTypes)
|
|
{
|
|
foreach (var structDef in node.Structs)
|
|
{
|
|
if (!CanEmitStruct(structDef))
|
|
continue;
|
|
|
|
foreach (var field in structDef.Fields)
|
|
{
|
|
if (field.ArraySize is null)
|
|
continue;
|
|
|
|
var size = ParseArraySize(field.ArraySize);
|
|
if (size <= 0)
|
|
continue;
|
|
|
|
var csType = MapCppFieldTypeToCs(field.CppType, typeMap);
|
|
if (!IsBlittableForFixed(csType))
|
|
{
|
|
inlineArrayTypes.Add((csType, size));
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
CollectInlineArrayTypes(child, typeMap, inlineArrayTypes);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits InlineArray types for non-blittable array fields.
|
|
/// Uses [InlineArray(N)] attribute introduced in .NET 8.
|
|
/// </summary>
|
|
private static void EmitInlineArrayTypes(StringBuilder sb, int indent, HashSet<(string csType, int size)> inlineArrayTypes, HashSet<string> emittedNames)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
foreach (var (csType, size) in inlineArrayTypes.OrderBy(x => x.csType).ThenBy(x => x.size))
|
|
{
|
|
// Generate a name like "AttributeArray12" or "SkillIDArray8"
|
|
var simpleName = GetSimpleTypeName(csType);
|
|
var arrayTypeName = $"{simpleName}Array{size}";
|
|
|
|
if (!emittedNames.Add(arrayTypeName))
|
|
continue;
|
|
|
|
sb.AppendLine($"{pad}/// <summary>");
|
|
sb.AppendLine($"{pad}/// Inline array of {size} {simpleName} elements.");
|
|
sb.AppendLine($"{pad}/// </summary>");
|
|
sb.AppendLine($"{pad}[global::System.Runtime.CompilerServices.InlineArray({size})]");
|
|
sb.AppendLine($"{pad}public unsafe struct {arrayTypeName}");
|
|
sb.AppendLine($"{pad}{{");
|
|
sb.AppendLine($"{pad} private {csType} _element0;");
|
|
sb.AppendLine($"{pad}}}");
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the simple type name from a fully-qualified type name.
|
|
/// e.g., "global::Daybreak.API.Interop.GuildWars.Attribute" -> "Attribute"
|
|
/// Handles pointer types: "ChatMessage*" -> "ChatMessagePtr"
|
|
/// </summary>
|
|
private static string GetSimpleTypeName(string csType)
|
|
{
|
|
// Handle pointer types
|
|
var isPointer = csType.EndsWith("*");
|
|
if (isPointer)
|
|
csType = csType.TrimEnd('*');
|
|
|
|
var lastDot = csType.LastIndexOf('.');
|
|
var name = lastDot >= 0 ? csType.Substring(lastDot + 1) : csType;
|
|
|
|
// Append Ptr for pointer types to make valid identifier
|
|
return isPointer ? name + "Ptr" : name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the inline array type name for a given element type and size.
|
|
/// </summary>
|
|
private static string GetInlineArrayTypeName(string csType, int size)
|
|
{
|
|
var simpleName = GetSimpleTypeName(csType);
|
|
return $"{simpleName}Array{size}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits all enums from the constants tree into the GuildWars namespace.
|
|
/// This provides compatibility for consumer code that uses `using Daybreak.API.Interop.GuildWars;`
|
|
/// </summary>
|
|
private static void EmitEnumsToGuildWarsNamespace(StringBuilder sb, ConstantsNode node, int indent, HashSet<string> emittedNames)
|
|
{
|
|
EmitEnumsRecursive(sb, node, indent, emittedNames);
|
|
}
|
|
|
|
private static void EmitEnumsRecursive(StringBuilder sb, ConstantsNode node, int indent, HashSet<string> emittedNames)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
foreach (var enumDef in node.Enums.OrderBy(e => e.Name, StringComparer.Ordinal))
|
|
{
|
|
// Skip anonymous enums
|
|
if (enumDef.Name is null)
|
|
continue;
|
|
|
|
// Skip duplicates (including if a struct with the same name was emitted)
|
|
if (emittedNames.Contains(enumDef.Name))
|
|
continue;
|
|
|
|
emittedNames.Add(enumDef.Name);
|
|
|
|
// Determine base type and map C++ types to C#
|
|
var baseType = MapCppEnumBaseType(enumDef.UnderlyingType ?? "int");
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine($"{pad}public enum {SanitizeIdentifier(enumDef.Name)} : {baseType}");
|
|
sb.AppendLine($"{pad}{{");
|
|
|
|
var innerPad = new string(' ', indent + 4);
|
|
foreach (var member in enumDef.Members)
|
|
{
|
|
if (member.Value is not null)
|
|
sb.AppendLine($"{innerPad}{SanitizeIdentifier(member.Name)} = {member.Value},");
|
|
else
|
|
sb.AppendLine($"{innerPad}{SanitizeIdentifier(member.Name)},");
|
|
}
|
|
sb.AppendLine($"{pad}}}");
|
|
}
|
|
|
|
// Recurse into children
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
EmitEnumsRecursive(sb, child, indent, emittedNames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits all type aliases (typedef Array<T>) from the constants tree into the GuildWars namespace.
|
|
/// </summary>
|
|
private static void EmitTypeAliasesToGuildWarsNamespace(StringBuilder sb, ConstantsNode node, Dictionary<string, string> typeMap, int indent, HashSet<string> emittedNames)
|
|
{
|
|
EmitTypeAliasesRecursive(sb, node, typeMap, indent, emittedNames);
|
|
}
|
|
|
|
private static void EmitTypeAliasesRecursive(StringBuilder sb, ConstantsNode node, Dictionary<string, string> typeMap, int indent, HashSet<string> emittedNames)
|
|
{
|
|
var pad = new string(' ', indent);
|
|
|
|
foreach (var alias in node.TypeAliases.OrderBy(a => a.AliasName, StringComparer.Ordinal))
|
|
{
|
|
// Skip duplicates
|
|
if (emittedNames.Contains(alias.AliasName))
|
|
continue;
|
|
|
|
emittedNames.Add(alias.AliasName);
|
|
|
|
var innerType = alias.TemplateArg.Replace("::", ".").Trim();
|
|
// Handle pointer types (e.g., "Item *" -> "nint")
|
|
if (innerType.EndsWith("*"))
|
|
innerType = "nint";
|
|
else if (typeMap.TryGetValue(innerType, out var mapped))
|
|
innerType = mapped;
|
|
else
|
|
{
|
|
// Try primitive type mapping
|
|
var primitiveMapping = MapPrimitiveType(innerType);
|
|
if (primitiveMapping != innerType)
|
|
innerType = primitiveMapping;
|
|
else
|
|
// Unmapped struct type - use nint as fallback
|
|
innerType = "nint";
|
|
}
|
|
|
|
sb.AppendLine($"{pad}public unsafe struct {alias.AliasName} {{ public global::Daybreak.API.Interop.GuildWars.GuildWarsArray<{innerType}> Value; }}");
|
|
}
|
|
|
|
// Recurse into children
|
|
foreach (var child in node.Children.Values)
|
|
{
|
|
EmitTypeAliasesRecursive(sb, child, typeMap, indent, emittedNames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a C++ enum underlying type to C# type.
|
|
/// </summary>
|
|
private static string MapCppEnumBaseType(string cppType)
|
|
{
|
|
return cppType switch
|
|
{
|
|
"uint8_t" or "unsigned char" => "byte",
|
|
"int8_t" or "char" or "signed char" => "sbyte",
|
|
"uint16_t" or "unsigned short" => "ushort",
|
|
"int16_t" or "short" => "short",
|
|
"uint32_t" or "unsigned int" or "unsigned long" => "uint",
|
|
"int32_t" or "int" or "long" => "int",
|
|
"uint64_t" or "unsigned long long" => "ulong",
|
|
"int64_t" or "long long" => "long",
|
|
_ => "int"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a C++ field type to a C# type for struct fields.
|
|
/// Handles pointers, qualified types, etc.
|
|
/// </summary>
|
|
private static string MapCppFieldTypeToCs(string cppType, Dictionary<string, string> typeMap)
|
|
{
|
|
cppType = cppType.Trim();
|
|
|
|
// Strip leading 'const' keyword
|
|
if (cppType.StartsWith("const "))
|
|
cppType = cppType.Substring(6).Trim();
|
|
|
|
// Strip leading 'struct' keyword (C++ forward decl style)
|
|
if (cppType.StartsWith("struct "))
|
|
cppType = cppType.Substring(7).Trim();
|
|
|
|
// Handle 'struct' inside template arguments (e.g., Array<struct Node*>)
|
|
cppType = Regex.Replace(cppType, @"<\s*struct\s+", "<");
|
|
|
|
// Strip namespace prefix before template detection (GW::Array<> -> Array<>)
|
|
// But preserve the inner type for recursive processing
|
|
if (cppType.StartsWith("GW::"))
|
|
cppType = cppType.Substring(4);
|
|
|
|
// Handle Array<T> - GWCA's Array template which is a {T* data, uint size, uint capacity} (12 bytes)
|
|
if (cppType.StartsWith("Array<"))
|
|
{
|
|
// Extract the inner type from Array<T>
|
|
var innerStart = cppType.IndexOf('<') + 1;
|
|
var innerEnd = cppType.LastIndexOf('>');
|
|
if (innerEnd > innerStart)
|
|
{
|
|
var innerCpp = cppType.Substring(innerStart, innerEnd - innerStart).Trim();
|
|
// Handle pointer inner types (e.g., Array<Agent*>)
|
|
// C# does NOT allow pointer types as generic type arguments (CS0306), so use nint
|
|
if (innerCpp.EndsWith("*"))
|
|
{
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<nint>";
|
|
}
|
|
// Handle non-pointer inner types
|
|
// Strip "struct " prefix
|
|
if (innerCpp.StartsWith("struct "))
|
|
innerCpp = innerCpp.Substring(7).Trim();
|
|
// Strip namespace
|
|
if (innerCpp.Contains("::"))
|
|
{
|
|
var parts = innerCpp.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries);
|
|
innerCpp = parts[parts.Length - 1];
|
|
}
|
|
// Map primitives
|
|
var csInner = MapPrimitiveType(innerCpp);
|
|
if (csInner != innerCpp)
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<" + csInner + ">";
|
|
// Check typeMap for structs/enums
|
|
if (typeMap.TryGetValue(innerCpp, out var mappedInner))
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<" + mappedInner + ">";
|
|
// Fallback for unmapped types
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<nint>";
|
|
}
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<nint>";
|
|
}
|
|
|
|
// Handle TLink<T> - linked list node, always 8 bytes (2 pointers)
|
|
if (cppType.StartsWith("TLink<"))
|
|
{
|
|
return "global::Daybreak.API.Interop.GuildWars.TLink";
|
|
}
|
|
|
|
// Handle THash<T> - hash table, always 0x24 bytes; opaque to managed code.
|
|
if (cppType.StartsWith("THash<"))
|
|
{
|
|
return "global::Daybreak.API.Interop.GuildWars.THash";
|
|
}
|
|
|
|
// Handle pointers
|
|
bool isPointer = cppType.EndsWith("*");
|
|
if (isPointer)
|
|
{
|
|
var inner = cppType.TrimEnd('*').Trim();
|
|
// Strip namespace qualifiers before lookup
|
|
var innerStripped = inner;
|
|
if (inner.Contains("::"))
|
|
{
|
|
var parts = inner.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries);
|
|
innerStripped = parts[parts.Length - 1];
|
|
}
|
|
|
|
var csInner = MapCppFieldTypeToCs(inner, typeMap);
|
|
// Special case: void* and char* and wchar_t* map to nint
|
|
if (csInner is "void" or "char" or "byte")
|
|
return "nint";
|
|
// If the inner type is unmapped (csInner equals the stripped name), use nint for the pointer
|
|
// A mapped type will have a fully-qualified name or be a known primitive
|
|
if (!IsKnownCsType(csInner) && csInner == innerStripped)
|
|
return "nint";
|
|
return csInner + "*";
|
|
}
|
|
|
|
// Check if original type is from Constants namespace (typically enums)
|
|
// In this case, prefer enum mapping over struct mapping
|
|
var isFromConstantsNamespace = cppType.Contains("Constants::");
|
|
|
|
// Strip namespace qualifiers (GW::, GW::Constants::, etc.)
|
|
if (cppType.Contains("::"))
|
|
{
|
|
var parts = cppType.Split(new[] { "::" }, StringSplitOptions.RemoveEmptyEntries);
|
|
cppType = parts[parts.Length - 1];
|
|
}
|
|
|
|
// Check if this type is in our generated enums/structs map
|
|
if (typeMap.TryGetValue(cppType, out var mapped))
|
|
{
|
|
// If original was from Constants:: namespace, it's an enum - use enum mapping
|
|
if (isFromConstantsNamespace)
|
|
return mapped;
|
|
// Otherwise, if it's a GuildWars type (struct), use it
|
|
if (mapped.Contains("GuildWars."))
|
|
return mapped;
|
|
}
|
|
// Try struct-prefixed key only if NOT from Constants namespace
|
|
// (for collision cases like Attribute struct vs enum)
|
|
if (!isFromConstantsNamespace && typeMap.TryGetValue("struct " + cppType, out var structAlt))
|
|
return structAlt;
|
|
// Fall back to original mapping (could be an enum)
|
|
if (mapped is not null)
|
|
return mapped;
|
|
|
|
// Handle common C++ types
|
|
return cppType switch
|
|
{
|
|
"uint32_t" => "uint",
|
|
"int32_t" => "int",
|
|
"uint16_t" => "ushort",
|
|
"int16_t" => "short",
|
|
"uint8_t" => "byte",
|
|
"int8_t" => "sbyte",
|
|
"uint64_t" => "ulong",
|
|
"int64_t" => "long",
|
|
"size_t" => "nuint",
|
|
"int" => "int",
|
|
"unsigned" => "uint",
|
|
"unsigned int" => "uint",
|
|
"short" => "short",
|
|
"unsigned short" => "ushort",
|
|
"long" => "int",
|
|
"unsigned long" => "uint",
|
|
"float" => "float",
|
|
"double" => "double",
|
|
"bool" => "byte", // C++ bool is 1 byte
|
|
"wchar_t" => "char",
|
|
"char" => "byte",
|
|
"void" => "void",
|
|
"DWORD" => "uint",
|
|
"FILETIME" => "ulong", // FILETIME is two DWORDs
|
|
"HMODULE" => "nint",
|
|
"HANDLE" => "nint",
|
|
"HWND" => "nint",
|
|
"uintptr_t" => "nuint",
|
|
"intptr_t" => "nint",
|
|
"Vec2f" => "global::System.Numerics.Vector2",
|
|
"Vec3f" => "global::System.Numerics.Vector3",
|
|
"Color" => "uint", // GWCA Color is usually uint32
|
|
// Known GWCA types that are typedefs to uint32_t (not enums)
|
|
"AgentID" => "uint",
|
|
"PlayerNumber" => "uint",
|
|
"ItemID" => "uint",
|
|
"PlayerID" => "uint",
|
|
// Everything else: assume it's a struct/enum name we'll reference directly
|
|
_ => cppType,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a type name is a known C# primitive or built-in type.
|
|
/// </summary>
|
|
private static bool IsKnownCsType(string csType)
|
|
{
|
|
return csType is "byte" or "sbyte" or "short" or "ushort" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "char" or "nint" or "nuint"
|
|
or "void" or "bool" or "string" or "object"
|
|
or "global::System.Numerics.Vector2" or "global::System.Numerics.Vector3";
|
|
}
|
|
|
|
private static bool IsBlittableForFixed(string csType)
|
|
{
|
|
// Note: nint and nuint are NOT valid for fixed buffers in C#
|
|
return csType is "byte" or "sbyte" or "short" or "ushort" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "char";
|
|
}
|
|
|
|
private static int ParseArraySize(string sizeExpr)
|
|
{
|
|
sizeExpr = sizeExpr.Trim();
|
|
if (sizeExpr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
return Convert.ToInt32(sizeExpr, 16);
|
|
if (int.TryParse(sizeExpr, out var size))
|
|
return size;
|
|
// If it's a named constant or expression, default to 1
|
|
return 1;
|
|
}
|
|
|
|
private static string ToPascalCase(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
return name;
|
|
|
|
// Handle snake_case: split by underscores and capitalize each part
|
|
if (name.Contains("_"))
|
|
{
|
|
var parts = name.Split('_');
|
|
var result = new StringBuilder();
|
|
foreach (var part in parts)
|
|
{
|
|
if (part.Length > 0)
|
|
{
|
|
result.Append(char.ToUpperInvariant(part[0]));
|
|
if (part.Length > 1)
|
|
result.Append(part.Substring(1));
|
|
}
|
|
}
|
|
return result.ToString();
|
|
}
|
|
|
|
// Just capitalize first letter
|
|
return char.ToUpperInvariant(name[0]) + name.Substring(1);
|
|
}
|
|
|
|
private static string MapCppTypeToCs(string? cppType)
|
|
{
|
|
if (cppType is null)
|
|
return "int";
|
|
return cppType switch
|
|
{
|
|
"uint32_t" => "uint",
|
|
"int32_t" => "int",
|
|
"uint16_t" => "ushort",
|
|
"int16_t" => "short",
|
|
"uint8_t" => "byte",
|
|
"int8_t" => "sbyte",
|
|
"int" => "int",
|
|
"unsigned int" => "uint",
|
|
"size_t" => "uint",
|
|
"float" => "float",
|
|
"double" => "double",
|
|
_ => "int",
|
|
};
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Identifier helpers
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static readonly HashSet<string> CsKeywords =
|
|
[
|
|
"abstract", "as", "base", "bool", "break", "byte", "case", "catch",
|
|
"char", "checked", "class", "const", "continue", "decimal", "default",
|
|
"delegate", "do", "double", "else", "enum", "event", "explicit",
|
|
"extern", "false", "finally", "fixed", "float", "for", "foreach",
|
|
"goto", "if", "implicit", "in", "int", "interface", "internal", "is",
|
|
"lock", "long", "namespace", "new", "null", "object", "operator",
|
|
"out", "override", "params", "private", "protected", "public",
|
|
"readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof",
|
|
"stackalloc", "static", "string", "struct", "switch", "this", "throw",
|
|
"true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe",
|
|
"ushort", "using", "virtual", "void", "volatile", "while",
|
|
];
|
|
|
|
private static string SanitizeIdentifier(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name))
|
|
return "_";
|
|
if (CsKeywords.Contains(name))
|
|
return "@" + name;
|
|
// If identifier starts with a digit, prefix with underscore
|
|
if (char.IsDigit(name[0]))
|
|
return "_" + name;
|
|
return name;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Type helpers
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Returns true if the type references a struct that is not in the typeMap.
|
|
/// Enums are excluded — they fall back to their underlying integer type.
|
|
/// </summary>
|
|
private static bool HasUnmappedStruct(DemangledType dt, Dictionary<string, string> typeMap)
|
|
{
|
|
if (dt.TemplateArg is not null && HasUnmappedStruct(dt.TemplateArg, typeMap))
|
|
return true;
|
|
|
|
if (dt.Kind is TypeKind.Struct)
|
|
{
|
|
if (dt.Name is "Array") return false;
|
|
return !typeMap.ContainsKey(dt.Name);
|
|
}
|
|
|
|
if (dt.Kind is TypeKind.Pointer)
|
|
{
|
|
if (dt.Name is "void" or "byte" or "ushort" or "short" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "bool" or "funcptr")
|
|
return false;
|
|
if (dt.Name is "Array") return false;
|
|
return !typeMap.ContainsKey(dt.Name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the type is a std::function (struct named "function").
|
|
/// </summary>
|
|
private static bool IsStdFunction(DemangledType dt)
|
|
{
|
|
// std::function is demangled as a Struct with name "function"
|
|
if (dt.Kind is TypeKind.Struct && dt.Name == "function")
|
|
return true;
|
|
// Also check pointer to std::function (const ref)
|
|
if (dt.Kind is TypeKind.Pointer && dt.Name == "function")
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if any parameter or return type involves std::function.
|
|
/// </summary>
|
|
private static bool HasStdFunction(DemangledType dt)
|
|
{
|
|
if (IsStdFunction(dt))
|
|
return true;
|
|
if (dt.TemplateArg is not null && HasStdFunction(dt.TemplateArg))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Like HasUnmappedStruct but ignores std::function types (which will be converted to nint via C export).
|
|
/// </summary>
|
|
private static bool HasUnmappedStructIgnoringStdFunction(DemangledType dt, Dictionary<string, string> typeMap)
|
|
{
|
|
// Skip std::function types entirely
|
|
if (IsStdFunction(dt))
|
|
return false;
|
|
|
|
if (dt.TemplateArg is not null && HasUnmappedStructIgnoringStdFunction(dt.TemplateArg, typeMap))
|
|
return true;
|
|
|
|
if (dt.Kind is TypeKind.Struct)
|
|
{
|
|
if (dt.Name is "Array") return false;
|
|
return !typeMap.ContainsKey(dt.Name);
|
|
}
|
|
|
|
if (dt.Kind is TypeKind.Pointer)
|
|
{
|
|
if (dt.Name is "void" or "byte" or "ushort" or "short" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "bool" or "funcptr")
|
|
return false;
|
|
if (dt.Name is "Array") return false;
|
|
return !typeMap.ContainsKey(dt.Name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a <see cref="DemangledType"/> to a C# type string, using the
|
|
/// GWCAEquivalent type map for struct/enum types.
|
|
/// </summary>
|
|
private static string ResolveCsType(DemangledType dt, Dictionary<string, string> typeMap)
|
|
{
|
|
switch (dt.Kind)
|
|
{
|
|
case TypeKind.Primitive:
|
|
return dt.Name;
|
|
|
|
case TypeKind.FuncPtr:
|
|
return "nint";
|
|
|
|
case TypeKind.Pointer:
|
|
{
|
|
if (dt.Name == "Array" && dt.TemplateArg is not null)
|
|
{
|
|
var innerType = ResolveTemplateArgType(dt.TemplateArg, typeMap);
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<" + innerType + ">*";
|
|
}
|
|
|
|
var inner = dt.Name switch
|
|
{
|
|
"void" => "void",
|
|
"byte" => "byte",
|
|
"ushort" => "ushort",
|
|
"short" => "short",
|
|
"int" => "int",
|
|
"uint" => "uint",
|
|
"long" => "long",
|
|
"ulong" => "ulong",
|
|
"float" => "float",
|
|
"double" => "double",
|
|
"bool" => "byte", // bool* not blittable
|
|
_ => ResolveStructOrEnumPointerInner(dt.Name, typeMap),
|
|
};
|
|
return inner + "*";
|
|
}
|
|
|
|
case TypeKind.Enum:
|
|
if (typeMap.TryGetValue(dt.Name, out var enumCs))
|
|
return enumCs;
|
|
return dt.UnderlyingType ?? "int";
|
|
|
|
case TypeKind.Struct:
|
|
if (dt.Name == "Array" && dt.TemplateArg is not null)
|
|
{
|
|
var innerType = ResolveTemplateArgType(dt.TemplateArg, typeMap);
|
|
return "global::Daybreak.API.Interop.GuildWars.GuildWarsArray<" + innerType + ">";
|
|
}
|
|
|
|
// First try direct lookup, but ensure we get a struct (GuildWars namespace), not an enum
|
|
if (typeMap.TryGetValue(dt.Name, out var structCs))
|
|
{
|
|
// If it's a GuildWars type, use it (it's the struct)
|
|
if (structCs.Contains("GuildWars."))
|
|
return structCs;
|
|
}
|
|
// If direct lookup returned an enum, try struct-prefixed key
|
|
if (typeMap.TryGetValue("struct " + dt.Name, out var structCsAlt))
|
|
return structCsAlt;
|
|
// Fall back to direct lookup result or name
|
|
if (structCs is not null)
|
|
return structCs;
|
|
return dt.Name;
|
|
|
|
default:
|
|
return "nint";
|
|
}
|
|
}
|
|
|
|
private static string ResolveTemplateArgType(DemangledType arg, Dictionary<string, string> typeMap)
|
|
{
|
|
return arg.Kind switch
|
|
{
|
|
TypeKind.Primitive => MapPrimitiveType(arg.Name),
|
|
TypeKind.Enum => typeMap.TryGetValue(arg.Name, out var e) ? e : arg.Name,
|
|
TypeKind.Struct => ResolveStructType(arg.Name, typeMap),
|
|
TypeKind.Pointer => "nint", // C# does NOT allow pointer types as generic type arguments (CS0306)
|
|
_ => "nint",
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a struct name to its C# type, handling name collisions with enums.
|
|
/// </summary>
|
|
private static string ResolveStructType(string name, Dictionary<string, string> typeMap)
|
|
{
|
|
// Try direct lookup - if it's in GuildWars namespace, it's the struct
|
|
if (typeMap.TryGetValue(name, out var direct) && direct.Contains("GuildWars."))
|
|
return direct;
|
|
// Try struct-prefixed key (for collision cases like Attribute struct vs enum)
|
|
if (typeMap.TryGetValue("struct " + name, out var structAlt))
|
|
return structAlt;
|
|
// Unmapped struct -> nint
|
|
return "nint";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a pointer inner type (the pointee) for struct or enum types,
|
|
/// handling name collisions between structs and enums.
|
|
/// </summary>
|
|
private static string ResolveStructOrEnumPointerInner(string name, Dictionary<string, string> typeMap)
|
|
{
|
|
// Try direct lookup - if it's in GuildWars namespace, it's a struct
|
|
if (typeMap.TryGetValue(name, out var direct) && direct.Contains("GuildWars."))
|
|
return direct;
|
|
// Try struct-prefixed key (for collision cases like Attribute struct vs enum)
|
|
if (typeMap.TryGetValue("struct " + name, out var structAlt))
|
|
return structAlt;
|
|
// Fall back to direct lookup result (could be an enum) or just the name
|
|
return direct ?? name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a pointer type used as a template argument (e.g., Agent* in Array<Agent*>).
|
|
/// In unsafe C#, pointer types can be used as generic type arguments for unmanaged structs.
|
|
/// </summary>
|
|
private static string ResolvePointerTemplateArg(DemangledType arg, Dictionary<string, string> typeMap)
|
|
{
|
|
// arg.Name is the inner type (e.g., "Agent" for Agent*)
|
|
var innerName = arg.Name;
|
|
|
|
// Check if the inner type is mapped (prefer struct mapping for GuildWars types)
|
|
var resolved = ResolveStructOrEnumPointerInner(innerName, typeMap);
|
|
if (resolved != innerName)
|
|
return resolved + "*";
|
|
|
|
// Check for primitives
|
|
var primitive = MapPrimitiveType(innerName);
|
|
if (primitive != innerName)
|
|
return primitive + "*";
|
|
|
|
// Special cases
|
|
return innerName switch
|
|
{
|
|
"void" => "nint", // void* -> nint
|
|
_ => "nint", // unmapped pointer -> nint
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps C/C++ primitive type names to C# primitive types.
|
|
/// </summary>
|
|
private static string MapPrimitiveType(string cppType)
|
|
{
|
|
return cppType switch
|
|
{
|
|
"uint32_t" => "uint",
|
|
"int32_t" => "int",
|
|
"uint16_t" => "ushort",
|
|
"int16_t" => "short",
|
|
"uint8_t" => "byte",
|
|
"int8_t" => "sbyte",
|
|
"uint64_t" => "ulong",
|
|
"int64_t" => "long",
|
|
"size_t" => "nuint",
|
|
"uintptr_t" => "nuint",
|
|
"intptr_t" => "nint",
|
|
"unsigned int" or "unsigned" => "uint",
|
|
"unsigned short" => "ushort",
|
|
"unsigned long" => "uint",
|
|
"unsigned char" => "byte",
|
|
"char" => "byte",
|
|
"wchar_t" => "char",
|
|
"bool" => "byte",
|
|
// GWCA-specific typedefs
|
|
"AgentID" => "uint",
|
|
"PlayerNumber" => "uint",
|
|
"ItemID" => "uint",
|
|
"PlayerID" => "uint",
|
|
"Color" => "uint",
|
|
_ => cppType, // pass through already-correct types like int, uint, etc
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a comment annotation for unmapped struct/enum types, or null.
|
|
/// </summary>
|
|
private static string? TypeComment(DemangledType dt, Dictionary<string, string> typeMap)
|
|
{
|
|
switch (dt.Kind)
|
|
{
|
|
case TypeKind.Pointer:
|
|
if (dt.Name is "void" or "byte" or "ushort" or "short" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "bool")
|
|
return null;
|
|
if (typeMap.ContainsKey(dt.Name))
|
|
return null;
|
|
return "TODO: map struct " + (dt.QualifiedName ?? dt.Name);
|
|
|
|
case TypeKind.Enum:
|
|
if (typeMap.ContainsKey(dt.Name))
|
|
return null;
|
|
return "enum " + (dt.QualifiedName ?? dt.Name) + " (as " + (dt.UnderlyingType ?? "int") + ")";
|
|
|
|
case TypeKind.Struct:
|
|
if (typeMap.ContainsKey(dt.Name))
|
|
return null;
|
|
return "TODO: map struct " + (dt.QualifiedName ?? dt.Name);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Parameter naming
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private static string MakeParamName(
|
|
DemangledType dt, Dictionary<string, string> typeMap, int idx, int total)
|
|
{
|
|
string baseName;
|
|
if (dt.Kind is TypeKind.Pointer)
|
|
{
|
|
baseName = dt.Name is "void" or "byte" or "ushort" or "short" or "int" or "uint"
|
|
or "long" or "ulong" or "float" or "double" or "bool"
|
|
? "ptr"
|
|
: CamelCase(ShortName(typeMap.TryGetValue(dt.Name, out var mapped) ? mapped : dt.Name));
|
|
}
|
|
else if (dt.Kind is TypeKind.FuncPtr)
|
|
{
|
|
baseName = "callback";
|
|
}
|
|
else
|
|
{
|
|
var csType = ResolveCsType(dt, typeMap);
|
|
var shortType = ShortName(csType);
|
|
baseName = shortType switch
|
|
{
|
|
"uint" => "value",
|
|
"int" => "value",
|
|
"nint" => "ptr",
|
|
"bool" => "flag",
|
|
"float" => "value",
|
|
"double" => "value",
|
|
"short" => "value",
|
|
"ushort" => "value",
|
|
"byte" => "value",
|
|
"long" => "value",
|
|
"ulong" => "value",
|
|
_ => CamelCase(shortType),
|
|
};
|
|
}
|
|
|
|
return total > 1 ? baseName + (idx + 1) : baseName;
|
|
}
|
|
|
|
private static string CamelCase(string name)
|
|
{
|
|
if (name.Length == 0) return "arg";
|
|
return char.ToLowerInvariant(name[0]) + name.Substring(1);
|
|
}
|
|
|
|
/// <summary>Extracts the short type name from a potentially fully-qualified name.</summary>
|
|
private static string ShortName(string fqn)
|
|
{
|
|
var idx = fqn.LastIndexOf('.');
|
|
return idx >= 0 ? fqn.Substring(idx + 1) : fqn;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Internal types
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>Type mapping for the incremental pipeline. Implements
|
|
/// <see cref="IEquatable{T}"/> for proper caching.</summary>
|
|
internal readonly struct TypeMapping(string gwcaName, string csName, string csNamespace) : IEquatable<TypeMapping>
|
|
{
|
|
public readonly string GwcaName = gwcaName;
|
|
public readonly string CsName = csName;
|
|
public readonly string CsNamespace = csNamespace;
|
|
|
|
public bool Equals(TypeMapping other)
|
|
=> this.GwcaName == other.GwcaName && this.CsName == other.CsName && this.CsNamespace == other.CsNamespace;
|
|
|
|
public override bool Equals(object? obj)
|
|
=> obj is TypeMapping other && this.Equals(other);
|
|
|
|
public override int GetHashCode()
|
|
=> (this.GwcaName?.GetHashCode() ?? 0) * 397
|
|
^ (this.CsName?.GetHashCode() ?? 0) * 17
|
|
^ (this.CsNamespace?.GetHashCode() ?? 0);
|
|
}
|
|
|
|
/// <summary>A node in the namespace tree, representing a C++ namespace
|
|
/// as a nested static class.</summary>
|
|
internal sealed class NamespaceNode(string name)
|
|
{
|
|
public string Name { get; } = name;
|
|
public Dictionary<string, NamespaceNode> Children { get; } = new(StringComparer.Ordinal);
|
|
public List<ExportEntry> Functions { get; } = [];
|
|
}
|
|
|
|
/// <summary>A single mangled export with its demangled info.</summary>
|
|
internal sealed class ExportEntry(string mangledName, DemangledFunction info)
|
|
{
|
|
public string MangledName { get; } = mangledName;
|
|
public DemangledFunction Info { get; } = info;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Constants tree types (parsed from C++ headers)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
internal sealed class ConstantsNode(string name)
|
|
{
|
|
public string Name { get; } = name;
|
|
public Dictionary<string, ConstantsNode> Children { get; } = new(StringComparer.Ordinal);
|
|
public List<CppEnumDef> Enums { get; } = [];
|
|
public List<CppConstField> Constants { get; } = [];
|
|
public List<CppStructDef> Structs { get; } = [];
|
|
public List<CppTypeAlias> TypeAliases { get; } = [];
|
|
public int DebugPopLine { get; set; } // Debug: line number where this namespace was popped
|
|
|
|
public ConstantsNode GetOrCreateChild(string childName)
|
|
{
|
|
if (!this.Children.TryGetValue(childName, out var child))
|
|
{
|
|
child = new ConstantsNode(childName);
|
|
this.Children[childName] = child;
|
|
}
|
|
return child;
|
|
}
|
|
}
|
|
|
|
internal sealed class CppEnumDef
|
|
{
|
|
public string? Name { get; set; } // null for anonymous enums
|
|
public string? UnderlyingType { get; set; } // e.g. "uint32_t", null → int
|
|
public List<CppEnumMember> Members { get; } = [];
|
|
}
|
|
|
|
internal sealed class CppEnumMember
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string? Value { get; set; }
|
|
public string? Comment { get; set; }
|
|
}
|
|
|
|
internal sealed class CppConstField
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string CppType { get; set; } = "int";
|
|
public string Value { get; set; } = "0";
|
|
public string? Comment { get; set; }
|
|
}
|
|
|
|
internal sealed class CppStructDef
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string? BaseType { get; set; }
|
|
public int? Size { get; set; } // from static_assert(sizeof(...))
|
|
public List<CppStructField> Fields { get; } = [];
|
|
public string DebugNamespacePath { get; set; } = ""; // Debug: namespace path when parsed
|
|
}
|
|
|
|
internal sealed class CppStructField
|
|
{
|
|
public string Name { get; set; } = "";
|
|
public string CppType { get; set; } = "int";
|
|
public int? Offset { get; set; }
|
|
public string? ArraySize { get; set; } // e.g., "7", "0x10", "20"
|
|
public string? Comment { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a C++ typedef alias like: typedef Array<T> AliasName;
|
|
/// </summary>
|
|
internal sealed class CppTypeAlias
|
|
{
|
|
public string AliasName { get; set; } = "";
|
|
public string SourceType { get; set; } = ""; // e.g., "Array"
|
|
public string? TemplateArg { get; set; } // e.g., "Buff"
|
|
}
|
|
} |