Files
Ananta-ps/tools/transpiler/Program.cs
T

1600 lines
60 KiB
C#
Vendored

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace DrmkTranspiler;
public class GoCodeEmitter
{
private readonly StringBuilder _sb = new();
private int _indent = 0;
private readonly HashSet<string> _imports = new();
public void AddImport(string pkg) => _imports.Add(pkg);
public void Indent() => _indent++;
public void Unindent() { if (_indent > 0) _indent--; }
public void AppendLine(string line = "")
{
if (string.IsNullOrWhiteSpace(line))
{
_sb.AppendLine();
return;
}
_sb.Append(new string('\t', _indent));
_sb.AppendLine(line);
}
public void Append(string text) => _sb.Append(text);
public string Generate(string packageName)
{
var final = new StringBuilder();
final.AppendLine($"package {packageName}");
final.AppendLine();
if (_imports.Count > 0)
{
final.AppendLine("import (");
foreach (var imp in _imports.OrderBy(x => x))
{
final.AppendLine($"\t\"{imp}\"");
}
final.AppendLine(")");
final.AppendLine();
if (_imports.Contains("encoding/json"))
final.AppendLine("var _ = json.RawMessage{}");
if (_imports.Contains("sync"))
final.AppendLine("var _ = sync.Mutex{}");
if (_imports.Contains("context"))
final.AppendLine("var _ = context.Background");
if (_imports.Contains("os"))
final.AppendLine("var _ = os.ReadFile");
if (_imports.Contains("path/filepath"))
final.AppendLine("var _ = filepath.Join");
if (_imports.Contains("time"))
final.AppendLine("var _ = time.Now");
final.AppendLine();
}
final.Append(_sb);
return final.ToString();
}
}
public class CSharpToGoConverter
{
private static readonly HashSet<string> GoKeywords = new(StringComparer.OrdinalIgnoreCase)
{
"break", "default", "func", "interface", "select",
"case", "defer", "go", "map", "struct",
"chan", "else", "goto", "package", "switch",
"const", "fallthrough", "if", "range", "type",
"continue", "for", "import", "return", "var"
};
private static readonly HashSet<string> KnownPrimitives = new(StringComparer.OrdinalIgnoreCase)
{
"int32", "uint32", "int64", "uint64", "int16", "uint16",
"byte", "int8", "rune", "float32", "float64", "bool", "string",
"time.Time", "time.Duration", "context.Context", "context.CancelFunc",
"*GameSession", "*RpcContext", "error", "any", "[]byte", "Vec2", "Vec3", "Vec4", "Quat", "Transform"
};
public static string CleanParamName(string name)
{
name = name.Replace("@", "").Trim();
if (name.Equals("ctx", StringComparison.OrdinalIgnoreCase))
return "rpcCtx";
if (GoKeywords.Contains(name))
return name + "Val";
if (string.IsNullOrEmpty(name))
return "param";
return name;
}
public static string ConvertType(TypeSyntax? type, GoCodeEmitter emitter, bool simplifyExternal = false)
{
if (type == null) return "any";
string typeStr = type.ToString().Trim();
string converted = ConvertTypeString(typeStr, emitter);
if (simplifyExternal)
{
if (converted.StartsWith("[]") && converted != "[]byte")
return "[]any";
if (converted.StartsWith("map["))
return "map[string]any";
if (!KnownPrimitives.Contains(converted))
return "any";
}
return converted;
}
public static string ConvertTypeString(string typeStr, GoCodeEmitter emitter)
{
typeStr = typeStr.Trim();
if (typeStr.EndsWith("?"))
{
string inner = typeStr.Substring(0, typeStr.Length - 1).Trim();
if (inner.EndsWith("[]") || inner.Contains("List") || inner.Contains("Dictionary") || inner.Contains("Collection"))
return ConvertTypeString(inner, emitter);
string converted = ConvertTypeString(inner, emitter);
if (converted.StartsWith("[]") || converted.StartsWith("map["))
return converted;
return "*" + converted;
}
if (typeStr.EndsWith("[]"))
{
string elem = typeStr.Substring(0, typeStr.Length - 2);
return "[]" + ConvertTypeString(elem, emitter);
}
if (typeStr.StartsWith("(") && typeStr.EndsWith(")"))
{
return "any";
}
var genMatch = Regex.Match(typeStr, @"^(.*?)<(.+)>$");
if (genMatch.Success)
{
string baseName = genMatch.Groups[1].Value.Trim();
string argsStr = genMatch.Groups[2].Value.Trim();
var args = SplitGenericArgs(argsStr);
if (baseName.EndsWith("List") || baseName.EndsWith("Collection") || baseName.EndsWith("IReadOnlyList") ||
baseName.EndsWith("ICollection") || baseName.EndsWith("IReadOnlyCollection") || baseName.EndsWith("IEnumerable") ||
baseName.EndsWith("HashSet") || baseName.EndsWith("ISet") || baseName.EndsWith("ImmutableArray") || baseName.EndsWith("ReadOnlySpan"))
{
return "[]" + (args.Count > 0 ? ConvertTypeString(args[0], emitter) : "any");
}
if (baseName.EndsWith("Dictionary") || baseName.EndsWith("IReadOnlyDictionary") || baseName.EndsWith("IDictionary") || baseName.EndsWith("ConcurrentDictionary"))
{
string k = args.Count > 0 ? ConvertTypeString(args[0], emitter) : "string";
string v = args.Count > 1 ? ConvertTypeString(args[1], emitter) : "any";
return $"map[{k}]{v}";
}
if (baseName.EndsWith("Nullable"))
{
return "*" + (args.Count > 0 ? ConvertTypeString(args[0], emitter) : "any");
}
if (baseName.EndsWith("Task") || baseName.EndsWith("ValueTask"))
{
return args.Count > 0 ? ConvertTypeString(args[0], emitter) : "error";
}
if (baseName.EndsWith("ConditionalWeakTable"))
{
emitter.AddImport("sync");
return "*sync.Map";
}
if (baseName.EndsWith("Lazy"))
{
return args.Count > 0 ? ConvertTypeString(args[0], emitter) : "any";
}
return "any";
}
if (typeStr.Contains("."))
{
var parts = typeStr.Split('.');
typeStr = parts[^1];
}
return MapBaseType(typeStr, emitter);
}
private static List<string> SplitGenericArgs(string args)
{
var result = new List<string>();
int depth = 0;
int start = 0;
for (int i = 0; i < args.Length; i++)
{
if (args[i] == '<' || args[i] == '(') depth++;
else if (args[i] == '>' || args[i] == ')') depth--;
else if (args[i] == ',' && depth == 0)
{
result.Add(args.Substring(start, i - start).Trim());
start = i + 1;
}
}
if (start < args.Length)
result.Add(args.Substring(start).Trim());
return result;
}
public static string CleanLiteralValue(string val)
{
if (string.IsNullOrWhiteSpace(val)) return "0";
val = val.Trim();
// Strip float suffixes everywhere in expression e.g. 0.2f, 10.0f
val = Regex.Replace(val, @"(?<=[0-9_]\.[0-9_]+|[0-9_]+)[fFdDmM](?=[^a-zA-Z0-9_]|$)", "");
// Strip integer suffixes everywhere in expression e.g. 0xFFFFFFFFUL, 1u << 8, 100UL, 5000L
val = Regex.Replace(val, @"(?<=[0-9a-fA-F_])[uUlL]+(?=[^a-zA-Z0-9_]|$)", "");
return val;
}
public static string MapBaseType(string typeStr, GoCodeEmitter emitter)
{
switch (typeStr)
{
case "int": return "int32";
case "uint": return "uint32";
case "long": return "int64";
case "ulong": return "uint64";
case "short": return "int16";
case "ushort": return "uint16";
case "byte": return "byte";
case "sbyte": return "int8";
case "char": return "rune";
case "float": return "float32";
case "double": return "float64";
case "bool": return "bool";
case "string": return "string";
case "object": return "any";
case "void": return "";
case "CancellationToken":
emitter.AddImport("context");
return "context.Context";
case "CancellationTokenSource":
emitter.AddImport("context");
return "context.CancelFunc";
case "SemaphoreSlim": return "*sync.Mutex";
case "JsonElement": return "any";
case "JsonSerializerOptions": return "any";
case "JsonDocument": return "any";
case "Random": return "any";
case "WorldEntryState": return "any";
case "ServerLogger": return "any";
case "FileSystemWatcher": return "any";
case "Timer": return "any";
case "Task": return "any";
case "ValueTask": return "any";
case "PrivateServerConfig": return "any";
case "TcpSession": return "*GameSession";
case "RpcContext": return "*RpcContext";
case "DateTime":
case "DateTimeOffset":
emitter.AddImport("time");
return "time.Time";
case "TimeSpan":
emitter.AddImport("time");
return "time.Duration";
case "Vector3": return "Vec3";
case "Vector2": return "Vec2";
case "Vector4": return "Vec4";
case "Quaternion": return "Quat";
default: return CleanIdentifier(typeStr);
}
}
public static string ExportName(string name)
{
if (string.IsNullOrEmpty(name)) return "";
name = name.Replace("@", "");
if (name.StartsWith("_")) name = name.TrimStart('_');
if (name.Length == 0) return "Field";
return char.ToUpperInvariant(name[0]) + name.Substring(1);
}
public static string CleanIdentifier(string id)
{
id = id.Replace("@", "");
if (id.Contains("."))
{
var parts = id.Split('.');
id = parts[^1];
}
return id;
}
public static string ConvertTrivia(SyntaxTriviaList trivia)
{
var sb = new StringBuilder();
foreach (var t in trivia)
{
if (t.IsKind(SyntaxKind.SingleLineCommentTrivia))
{
sb.AppendLine(t.ToFullString().TrimEnd());
}
else if (t.IsKind(SyntaxKind.MultiLineCommentTrivia))
{
sb.AppendLine(t.ToFullString().TrimEnd());
}
else if (t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
{
string doc = t.ToFullString();
string cleaned = CleanXmlDoc(doc);
if (!string.IsNullOrWhiteSpace(cleaned))
sb.Append(cleaned);
}
}
return sb.ToString();
}
private static string CleanXmlDoc(string doc)
{
var lines = doc.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
var sb = new StringBuilder();
foreach (var line in lines)
{
string l = line.Trim();
if (l.StartsWith("///")) l = l.Substring(3).Trim();
if (string.IsNullOrWhiteSpace(l)) continue;
l = Regex.Replace(l, @"<summary>|</summary>|<remarks>|</remarks>|<see cref=""([^""]+)""\s*/>|<param[^>]*>|</param>|<returns>|</returns>|<c>|</c>|<b>|</b>", "$1");
sb.AppendLine("// " + l.Trim());
}
return sb.ToString();
}
}
class Program
{
static readonly HashSet<string> CollidingCatalogNames = new(StringComparer.OrdinalIgnoreCase)
{
"Snapshot", "Data"
};
static void Main(string[] args)
{
Console.WriteLine("Drmk C# to Go Transpiler v9.0 (Flawless Router Payload Typing)");
string repoRoot = @"C:\Users\Phitchayut\Desktop\ngp-Ananta-DEV5\ngp-Ananta-DEV5";
string sourceRoot = @"C:\Users\Phitchayut\Downloads\Ananta_PS_FullBuild\Ananta_PS_FullBuild";
CreateCommonTypes(repoRoot);
// 1. Catalogs (77 files)
ConvertCatalogs(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.Core\ClientData\Client4229938"),
Path.Combine(repoRoot, @"internal\catalogs"));
// 2. RpcTypes Wire Contracts (11 files)
ConvertRpcTypes(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.RpcTypes\Client4229938"),
Path.Combine(repoRoot, @"pkg\protocol\rpctypes"));
// 3. Rpc Methods (74 files)
ConvertRpcMethods(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.RpcTypes\Client4229938\Methods"),
Path.Combine(repoRoot, @"pkg\protocol\methods"));
// 4. Protocol Models (26 files)
ConvertProtocolModels(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.Core\Protocol\Client4229938"),
Path.Combine(repoRoot, @"pkg\protocol\models"));
// 5. Configuration (3 files)
ConvertConfiguration(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.Core\Configuration"),
Path.Combine(repoRoot, @"internal\config"));
// 6. Gameplay (3 files)
ConvertGameplay(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.Gameplay"),
Path.Combine(repoRoot, @"internal\gameplay"));
// 7. Routers (155 files)
ConvertRouters(
Path.Combine(sourceRoot, @"Drmk.Server\Drmk.Handlers\Game"),
Path.Combine(repoRoot, @"internal\gameserver\routers"));
Console.WriteLine("All conversions completed successfully!");
}
static void CreateCommonTypes(string repoRoot)
{
string catDir = Path.Combine(repoRoot, @"internal\catalogs");
Directory.CreateDirectory(catDir);
File.WriteAllText(Path.Combine(catDir, "common_types.go"),
@"package catalogs
type Vec2 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
}
type Vec3 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
}
type Vec4 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type Quat struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type Transform struct {
Position Vec3 `json:""position""`
Rotation Quat `json:""rotation""`
Scale Vec3 `json:""scale""`
}
type RosterNpcStaticSpot struct {
SpiritId uint32 `json:""spiritId""`
Raid uint32 `json:""raid""`
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
Note string `json:""note""`
}
type JiguanLandingOverride struct {
EntranceId uint32 `json:""entranceId""`
Raid uint32 `json:""raid""`
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
Yaw float32 `json:""yaw""`
}
", Encoding.UTF8);
string rpcDir = Path.Combine(repoRoot, @"pkg\protocol\rpctypes");
Directory.CreateDirectory(rpcDir);
File.WriteAllText(Path.Combine(rpcDir, "common_types.go"),
@"package rpctypes
type Vec2 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
}
type Vec4 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type Quat struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type GridIndex struct {
X int32 `json:""x""`
Y int32 `json:""y""`
}
type GridAOIDecrease struct {
X int32 `json:""x""`
Y int32 `json:""y""`
}
", Encoding.UTF8);
string modelsDir = Path.Combine(repoRoot, @"pkg\protocol\models");
Directory.CreateDirectory(modelsDir);
File.WriteAllText(Path.Combine(modelsDir, "common_types.go"),
@"package models
import ""time""
var _ = time.Now
type Vec2 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
}
type Vec3 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
}
type Vec4 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type Quat struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type CombatSettings struct {
MaxHp float32 `json:""maxHp""`
UltimateResourceId uint32 `json:""ultimateResourceId""`
UltimateResourceMax float32 `json:""ultimateResourceMax""`
AllResourceMaximums map[uint32]float32 `json:""allResourceMaximums""`
}
type ExtractionShooterBagMeta struct {
BagId uint32 `json:""bagId""`
Capacity uint32 `json:""capacity""`
Width int32 `json:""width""`
Height int32 `json:""height""`
}
type IndoorBuildInfo struct {
BuildId uint32 `json:""buildId""`
ThemeId uint32 `json:""themeId""`
FloorId uint32 `json:""floorId""`
}
type HouseWallInfo struct {
WallId uint32 `json:""wallId""`
ColorId uint32 `json:""colorId""`
}
type HouseFashionShowcaseData struct {
ShowcaseId uint32 `json:""showcaseId""`
FashionId uint32 `json:""fashionId""`
}
type PlayerSingleHouseConfiguration struct {
HouseId uint32 `json:""houseId""`
LayoutId uint32 `json:""layoutId""`
}
type PlayerFishingInfo struct {
RodId uint32 `json:""rodId""`
BaitId uint32 `json:""baitId""`
}
type PlayerPackItem struct {
ItemId uint32 `json:""itemId""`
Count uint32 `json:""count""`
}
type SpiritMobileSkinInfo struct {
SpiritId uint32 `json:""spiritId""`
SkinId uint32 `json:""skinId""`
}
type Unknown struct{}
", Encoding.UTF8);
string gameplayDir = Path.Combine(repoRoot, @"internal\gameplay");
Directory.CreateDirectory(gameplayDir);
File.WriteAllText(Path.Combine(gameplayDir, "settings_types.go"),
@"package gameplay
type WebTraversalSettings struct {
Enabled bool `json:""enabled""`
GrappleRearmDelayMs int32 `json:""grappleRearmDelayMs""`
ForbidStates []uint32 `json:""forbidStates""`
FeisuoAbilityLimitId uint32 `json:""feisuoAbilityLimitId""`
AssassinationAbilityLimitId uint32 `json:""assassinationAbilityLimitId""`
KillingAbilityLimitId uint32 `json:""killingAbilityLimitId""`
KillingSpiritId uint32 `json:""killingSpiritId""`
}
type CombatSettings struct {
Enabled bool `json:""enabled""`
MaxHp float32 `json:""maxHp""`
UltimateResourceId uint32 `json:""ultimateResourceId""`
UltimateResourceMax float32 `json:""ultimateResourceMax""`
}
", Encoding.UTF8);
string routersDir = Path.Combine(repoRoot, @"internal\gameserver\routers");
Directory.CreateDirectory(routersDir);
File.WriteAllText(Path.Combine(routersDir, "common_types.go"),
@"package routers
type Vec2 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
}
type Vec3 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
}
type Vec4 struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type Quat struct {
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
W float32 `json:""w""`
}
type TeleportVec3 = Vec3
type UxVector3 = Vec3
type Vector3 = Vec3
type Vector2 = Vec2
type Vector4 = Vec4
type Quaternion = Quat
type Transform struct {
Position Vec3 `json:""position""`
Rotation Quat `json:""rotation""`
Scale Vec3 `json:""scale""`
}
type GameSession struct{}
type GateSessionHub = any
type ChineseChessMove = any
type GameGroundZoneTurnChangePush = any
type TruckJobOrderWrap = any
type Mj = any
type T = any
type Connection = any
type UxRpcMessage = any
type RpcRouter = any
type RpcContext struct{}
type AetherSettings struct{}
type Table struct {
Count uint32 `json:""count""`
Records []any `json:""records""`
}
type Lane struct {
Id uint32 `json:""id""`
Speed float32 `json:""speed""`
Points []any `json:""points""`
}
type EnemyTerritorySpot4229938 struct {
Raid uint32 `json:""raid""`
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
}
type FunctionalNpcSpot4229938 struct {
NpcId uint32 `json:""npcId""`
Raid uint32 `json:""raid""`
X float32 `json:""x""`
Y float32 `json:""y""`
Z float32 `json:""z""`
}
type PlayerPackItem struct {
ItemId uint32 `json:""itemId""`
Count uint32 `json:""count""`
}
type TrafficIntersectionInitInfoDerived4229938 = any
type TrafficIntersectionInitInfoBaseFirst4229938 = any
type IntersectionRow = any
", Encoding.UTF8);
}
static void ConvertCatalogs(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} catalog files...");
foreach (var file in files)
{
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
emitter.AddImport("sync");
emitter.AddImport("encoding/json");
emitter.AddImport("os");
emitter.AddImport("path/filepath");
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix, CollidingCatalogNames);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
string cName = cls.Identifier.Text;
if (cName.EndsWith("Catalog4229938") || cName.EndsWith("Catalog") || cName.EndsWith("Repository") || cName.EndsWith("Store"))
{
TranspileCatalogClass(cls, emitter, prefix);
}
else
{
TranspileWireClass(cls, emitter, prefix, CollidingCatalogNames);
}
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix, CollidingCatalogNames);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix, CollidingCatalogNames);
}
}
File.WriteAllText(dstPath, emitter.Generate("catalogs"), Encoding.UTF8);
}
}
static void TranspileRecord(RecordDeclarationSyntax rec, GoCodeEmitter emitter, string prefix, HashSet<string>? colliders = null)
{
string rawName = rec.Identifier.Text;
string name = (colliders != null && colliders.Contains(rawName)) ? CSharpToGoConverter.ExportName(prefix + "_" + rawName) : CSharpToGoConverter.ExportName(rawName);
emitter.AppendLine($"type {name} struct {{");
emitter.Indent();
var declaredFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (rec.ParameterList != null)
{
foreach (var param in rec.ParameterList.Parameters)
{
string pName = CSharpToGoConverter.ExportName(param.Identifier.Text);
if (!declaredFields.Add(pName)) continue;
string pTrivia = CSharpToGoConverter.ConvertTrivia(param.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(pTrivia))
emitter.Append(pTrivia);
string pType = CSharpToGoConverter.ConvertType(param.Type, emitter);
string jsonName = ToJsonName(param.Identifier.Text);
emitter.AppendLine($"{pName} {pType} `json:\"{jsonName}\"`");
}
}
if (rec.Members != null)
{
foreach (var member in rec.Members)
{
if (member is PropertyDeclarationSyntax prop)
{
string propName = CSharpToGoConverter.ExportName(prop.Identifier.Text);
if (!declaredFields.Add(propName)) continue;
string propTrivia = CSharpToGoConverter.ConvertTrivia(prop.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(propTrivia))
emitter.Append(propTrivia);
string propType = CSharpToGoConverter.ConvertType(prop.Type, emitter);
string jsonName = ToJsonName(prop.Identifier.Text);
emitter.AppendLine($"{propName} {propType} `json:\"{jsonName}\"`");
}
}
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
static void TranspileStruct(StructDeclarationSyntax str, GoCodeEmitter emitter, string prefix, HashSet<string>? colliders = null)
{
string rawName = str.Identifier.Text;
string name = (colliders != null && colliders.Contains(rawName)) ? CSharpToGoConverter.ExportName(prefix + "_" + rawName) : CSharpToGoConverter.ExportName(rawName);
emitter.AppendLine($"type {name} struct {{");
emitter.Indent();
var declaredFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var member in str.Members)
{
if (member is FieldDeclarationSyntax field)
{
string fTrivia = CSharpToGoConverter.ConvertTrivia(field.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(fTrivia))
emitter.Append(fTrivia);
string fType = CSharpToGoConverter.ConvertType(field.Declaration.Type, emitter);
foreach (var v in field.Declaration.Variables)
{
string fName = CSharpToGoConverter.ExportName(v.Identifier.Text);
if (!declaredFields.Add(fName)) continue;
string jsonName = ToJsonName(v.Identifier.Text);
emitter.AppendLine($"{fName} {fType} `json:\"{jsonName}\"`");
}
}
else if (member is PropertyDeclarationSyntax prop)
{
string pName = CSharpToGoConverter.ExportName(prop.Identifier.Text);
if (!declaredFields.Add(pName)) continue;
string pTrivia = CSharpToGoConverter.ConvertTrivia(prop.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(pTrivia))
emitter.Append(pTrivia);
string pType = CSharpToGoConverter.ConvertType(prop.Type, emitter);
string jsonName = ToJsonName(prop.Identifier.Text);
emitter.AppendLine($"{pName} {pType} `json:\"{jsonName}\"`");
}
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
static void TranspileEnum(EnumDeclarationSyntax enm, GoCodeEmitter emitter, string prefix, HashSet<string>? colliders = null)
{
string rawName = enm.Identifier.Text;
string name = (colliders != null && colliders.Contains(rawName)) ? CSharpToGoConverter.ExportName(prefix + "_" + rawName) : CSharpToGoConverter.ExportName(rawName);
string baseType = "int32";
if (enm.BaseList != null && enm.BaseList.Types.Count > 0)
{
baseType = CSharpToGoConverter.ConvertType(enm.BaseList.Types[0].Type, emitter);
}
emitter.AppendLine($"type {name} {baseType}");
emitter.AppendLine();
emitter.AppendLine("const (");
emitter.Indent();
for (int i = 0; i < enm.Members.Count; i++)
{
var mem = enm.Members[i];
string memTrivia = CSharpToGoConverter.ConvertTrivia(mem.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(memTrivia))
emitter.Append(memTrivia);
string memName = name + "_" + CSharpToGoConverter.ExportName(mem.Identifier.Text);
if (mem.EqualsValue != null)
{
string val = CSharpToGoConverter.CleanLiteralValue(mem.EqualsValue.Value.ToString());
emitter.AppendLine($"{memName} {name} = {val}");
}
else
{
if (i == 0)
emitter.AppendLine($"{memName} {name} = iota");
else
emitter.AppendLine($"{memName}");
}
}
emitter.Unindent();
emitter.AppendLine(")");
emitter.AppendLine();
}
static void TranspileCatalogClass(ClassDeclarationSyntax cls, GoCodeEmitter emitter, string fileBase)
{
string rawClassName = cls.Identifier.Text;
string className = CollidingCatalogNames.Contains(rawClassName) ? CSharpToGoConverter.ExportName(fileBase + "_" + rawClassName) : CSharpToGoConverter.ExportName(rawClassName);
var consts = cls.Members.OfType<FieldDeclarationSyntax>()
.Where(f => f.Modifiers.Any(SyntaxKind.ConstKeyword)).ToList();
if (consts.Count > 0)
{
var constNames = consts.SelectMany(c => c.Declaration.Variables.Select(v => v.Identifier.Text)).ToHashSet();
emitter.AppendLine("const (");
emitter.Indent();
foreach (var f in consts)
{
string fTrivia = CSharpToGoConverter.ConvertTrivia(f.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(fTrivia))
emitter.Append(fTrivia);
string fType = CSharpToGoConverter.ConvertType(f.Declaration.Type, emitter);
foreach (var v in f.Declaration.Variables)
{
string fName = className + "_" + CSharpToGoConverter.ExportName(v.Identifier.Text);
string rawVal = v.Initializer != null ? v.Initializer.Value.ToString() : "0";
string fVal = CSharpToGoConverter.CleanLiteralValue(rawVal);
foreach (var cn in constNames)
{
fVal = Regex.Replace(fVal, $@"\b{cn}\b", $"{className}_{cn}");
}
emitter.AppendLine($"{fName} {fType} = {fVal}");
}
}
emitter.Unindent();
emitter.AppendLine(")");
emitter.AppendLine();
}
emitter.AppendLine($"var (");
emitter.Indent();
emitter.AppendLine($"{className}_cacheLock sync.RWMutex");
emitter.AppendLine($"{className}_cached = false");
emitter.AppendLine($"{className}_entries []any");
emitter.Unindent();
emitter.AppendLine(")");
emitter.AppendLine();
string jsonFileName = className.Replace("Catalog4229938", "Catalog.json")
.Replace("Catalog", "Catalog.json");
if (!jsonFileName.EndsWith(".json")) jsonFileName += ".json";
emitter.AppendLine($"// Load{className} loads catalog records from client config directory");
emitter.AppendLine($"func Load{className}(configDir string) ([]any, error) {{");
emitter.Indent();
emitter.AppendLine($"{className}_cacheLock.RLock()");
emitter.AppendLine($"if {className}_cached {{");
emitter.Indent();
emitter.AppendLine($"defer {className}_cacheLock.RUnlock()");
emitter.AppendLine($"return {className}_entries, nil");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine($"{className}_cacheLock.RUnlock()");
emitter.AppendLine();
emitter.AppendLine($"{className}_cacheLock.Lock()");
emitter.AppendLine($"defer {className}_cacheLock.Unlock()");
emitter.AppendLine($"if {className}_cached {{ return {className}_entries, nil }}");
emitter.AppendLine();
emitter.AppendLine($"filePath := filepath.Join(configDir, \"{jsonFileName}\")");
emitter.AppendLine("data, err := os.ReadFile(filePath)");
emitter.AppendLine("if err != nil {");
emitter.Indent();
emitter.AppendLine("return nil, err");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
emitter.AppendLine("var raw struct {");
emitter.Indent();
emitter.AppendLine("Records []json.RawMessage `json:\"records\"`");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine("if err := json.Unmarshal(data, &raw); err != nil {");
emitter.Indent();
emitter.AppendLine("return nil, err");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
emitter.AppendLine("var result []any");
emitter.AppendLine("for _, item := range raw.Records {");
emitter.Indent();
emitter.AppendLine("var m map[string]any");
emitter.AppendLine("if err := json.Unmarshal(item, &m); err == nil {");
emitter.Indent();
emitter.AppendLine("result = append(result, m)");
emitter.Unindent();
emitter.AppendLine("}");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
emitter.AppendLine($"{className}_entries = result");
emitter.AppendLine($"{className}_cached = true");
emitter.AppendLine("return result, nil");
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
var methodNames = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var method in cls.Members.OfType<MethodDeclarationSyntax>())
{
if (method.Identifier.Text == "Load") continue;
string mTrivia = CSharpToGoConverter.ConvertTrivia(method.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(mTrivia))
emitter.Append(mTrivia);
string mBase = CSharpToGoConverter.ExportName(method.Identifier.Text);
methodNames[mBase] = methodNames.GetValueOrDefault(mBase, 0) + 1;
string mName = methodNames[mBase] > 1 ? $"{className}_{mBase}_{methodNames[mBase]}" : $"{className}_{mBase}";
string retType = CSharpToGoConverter.ConvertType(method.ReturnType, emitter);
var paramList = new List<string>();
foreach (var p in method.ParameterList.Parameters)
{
string pClean = CSharpToGoConverter.CleanParamName(p.Identifier.Text);
paramList.Add($"{pClean} {CSharpToGoConverter.ConvertType(p.Type, emitter)}");
}
emitter.AppendLine($"func {mName}({string.Join(", ", paramList)}) {retType} {{");
emitter.Indent();
emitter.AppendLine($"// Catalog helper {mName}");
if (!string.IsNullOrEmpty(retType))
{
if (retType.StartsWith("[]") || retType.StartsWith("map") || retType.StartsWith("*"))
emitter.AppendLine("return nil");
else if (retType == "bool")
emitter.AppendLine("return false");
else if (retType == "string")
emitter.AppendLine("return \"\"");
else if (retType == "error")
emitter.AppendLine("return nil");
else if (retType.StartsWith("int") || retType.StartsWith("uint") || retType.StartsWith("float") || retType == "byte" || retType == "rune")
emitter.AppendLine("return 0");
else
emitter.AppendLine($"var zero {retType}; return zero");
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
}
static void ConvertRpcTypes(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} RpcTypes files...");
foreach (var file in files)
{
if (Path.GetFileName(file) == "AssemblyInfo.cs") continue;
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
emitter.AddImport("encoding/json");
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
TranspileWireClass(cls, emitter, prefix);
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("rpctypes"), Encoding.UTF8);
}
}
static void TranspileWireClass(ClassDeclarationSyntax cls, GoCodeEmitter emitter, string prefix, HashSet<string>? colliders = null)
{
string rawName = cls.Identifier.Text;
string name = (colliders != null && colliders.Contains(rawName)) ? CSharpToGoConverter.ExportName(prefix + "_" + rawName) : CSharpToGoConverter.ExportName(rawName);
emitter.AppendLine($"type {name} struct {{");
emitter.Indent();
var declaredFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var member in cls.Members)
{
if (member is FieldDeclarationSyntax field && !field.Modifiers.Any(SyntaxKind.ConstKeyword))
{
string fType = CSharpToGoConverter.ConvertType(field.Declaration.Type, emitter);
foreach (var v in field.Declaration.Variables)
{
string fName = CSharpToGoConverter.ExportName(v.Identifier.Text);
if (!declaredFields.Add(fName)) continue;
string fTrivia = CSharpToGoConverter.ConvertTrivia(field.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(fTrivia))
emitter.Append(fTrivia);
string jsonName = ToJsonName(v.Identifier.Text);
emitter.AppendLine($"{fName} {fType} `json:\"{jsonName}\"`");
}
}
else if (member is PropertyDeclarationSyntax prop)
{
string pName = CSharpToGoConverter.ExportName(prop.Identifier.Text);
if (!declaredFields.Add(pName)) continue;
string pTrivia = CSharpToGoConverter.ConvertTrivia(prop.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(pTrivia))
emitter.Append(pTrivia);
string pType = CSharpToGoConverter.ConvertType(prop.Type, emitter);
string jsonName = ToJsonName(prop.Identifier.Text);
emitter.AppendLine($"{pName} {pType} `json:\"{jsonName}\"`");
}
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
var consts = cls.Members.OfType<FieldDeclarationSyntax>().Where(f => f.Modifiers.Any(SyntaxKind.ConstKeyword)).ToList();
if (consts.Count > 0)
{
var constNames = consts.SelectMany(c => c.Declaration.Variables.Select(v => v.Identifier.Text)).ToHashSet();
emitter.AppendLine("const (");
emitter.Indent();
foreach (var c in consts)
{
string cType = CSharpToGoConverter.ConvertType(c.Declaration.Type, emitter);
foreach (var v in c.Declaration.Variables)
{
string cName = name + "_" + CSharpToGoConverter.ExportName(v.Identifier.Text);
string rawVal = v.Initializer != null ? v.Initializer.Value.ToString() : "0";
string val = CSharpToGoConverter.CleanLiteralValue(rawVal);
foreach (var cn in constNames)
{
val = Regex.Replace(val, $@"\b{cn}\b", $"{name}_{cn}");
}
emitter.AppendLine($"{cName} {cType} = {val}");
}
}
emitter.Unindent();
emitter.AppendLine(")");
emitter.AppendLine();
}
}
static void ConvertRpcMethods(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs", SearchOption.AllDirectories);
Console.WriteLine($"Transpiling {files.Length} RpcMethod files...");
foreach (var file in files)
{
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
TranspileMethodClass(cls, emitter, prefix);
}
else if (typeDecl is InterfaceDeclarationSyntax iface)
{
TranspileInterface(iface, emitter, prefix);
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("methods"), Encoding.UTF8);
}
}
static void TranspileMethodClass(ClassDeclarationSyntax cls, GoCodeEmitter emitter, string prefix)
{
string rawName = cls.Identifier.Text;
string name = CSharpToGoConverter.ExportName(rawName);
var consts = cls.Members.OfType<FieldDeclarationSyntax>().Where(f => f.Modifiers.Any(SyntaxKind.ConstKeyword)).ToList();
if (consts.Count > 0)
{
emitter.AppendLine("const (");
emitter.Indent();
foreach (var c in consts)
{
string cTrivia = CSharpToGoConverter.ConvertTrivia(c.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(cTrivia))
emitter.Append(cTrivia);
string cType = CSharpToGoConverter.ConvertType(c.Declaration.Type, emitter);
foreach (var v in c.Declaration.Variables)
{
string cName = name + "_" + CSharpToGoConverter.ExportName(v.Identifier.Text);
string rawVal = v.Initializer != null ? v.Initializer.Value.ToString() : "0";
string val = CSharpToGoConverter.CleanLiteralValue(rawVal);
emitter.AppendLine($"{cName} {cType} = {val}");
}
}
emitter.Unindent();
emitter.AppendLine(")");
emitter.AppendLine();
}
var methodNames = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var m in cls.Members.OfType<MethodDeclarationSyntax>())
{
string mTrivia = CSharpToGoConverter.ConvertTrivia(m.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(mTrivia))
emitter.Append(mTrivia);
string mBase = CSharpToGoConverter.ExportName(m.Identifier.Text);
methodNames[mBase] = methodNames.GetValueOrDefault(mBase, 0) + 1;
string mName = methodNames[mBase] > 1 ? $"{name}_{mBase}_{methodNames[mBase]}" : $"{name}_{mBase}";
string retType = CSharpToGoConverter.ConvertType(m.ReturnType, emitter);
var paramList = new List<string>();
foreach (var p in m.ParameterList.Parameters)
{
string pClean = CSharpToGoConverter.CleanParamName(p.Identifier.Text);
paramList.Add($"{pClean} {CSharpToGoConverter.ConvertType(p.Type, emitter)}");
}
emitter.AppendLine($"func {mName}({string.Join(", ", paramList)}) {retType} {{");
emitter.Indent();
if (!string.IsNullOrEmpty(retType))
{
if (retType.StartsWith("[]") || retType.StartsWith("map") || retType.StartsWith("*"))
emitter.AppendLine("return nil");
else if (retType == "bool")
emitter.AppendLine("return false");
else if (retType == "string")
emitter.AppendLine("return \"\"");
else if (retType == "error")
emitter.AppendLine("return nil");
else if (retType.StartsWith("int") || retType.StartsWith("uint") || retType.StartsWith("float") || retType == "byte" || retType == "rune")
emitter.AppendLine("return 0");
else
emitter.AppendLine($"var zero {retType}; return zero");
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
}
static void TranspileInterface(InterfaceDeclarationSyntax iface, GoCodeEmitter emitter, string prefix)
{
string rawName = iface.Identifier.Text;
string name = CSharpToGoConverter.ExportName(rawName);
emitter.AppendLine($"type {name} interface {{");
emitter.Indent();
foreach (var m in iface.Members.OfType<MethodDeclarationSyntax>())
{
string mTrivia = CSharpToGoConverter.ConvertTrivia(m.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(mTrivia))
emitter.Append(mTrivia);
string mName = CSharpToGoConverter.ExportName(m.Identifier.Text);
string retType = CSharpToGoConverter.ConvertType(m.ReturnType, emitter);
var paramList = new List<string>();
foreach (var p in m.ParameterList.Parameters)
{
string pClean = CSharpToGoConverter.CleanParamName(p.Identifier.Text);
paramList.Add($"{pClean} {CSharpToGoConverter.ConvertType(p.Type, emitter)}");
}
emitter.AppendLine($"{mName}({string.Join(", ", paramList)}) {retType}");
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
static void ConvertProtocolModels(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} Protocol model files...");
foreach (var file in files)
{
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
emitter.AddImport("encoding/json");
emitter.AddImport("sync");
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
TranspileWireClass(cls, emitter, prefix);
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("models"), Encoding.UTF8);
}
}
static void ConvertConfiguration(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} Configuration files...");
foreach (var file in files)
{
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
emitter.AddImport("encoding/json");
emitter.AddImport("sync");
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
TranspileWireClass(cls, emitter, prefix);
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("config"), Encoding.UTF8);
}
}
static void ConvertGameplay(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} Gameplay files...");
foreach (var file in files)
{
string fileBase = Path.GetFileNameWithoutExtension(file);
string goFileName = ToSnakeCase(fileBase) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = fileBase;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
TranspileWireClass(cls, emitter, prefix);
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("gameplay"), Encoding.UTF8);
}
}
static void ConvertRouters(string srcDir, string dstDir)
{
Directory.CreateDirectory(dstDir);
var files = Directory.GetFiles(srcDir, "*.cs");
Console.WriteLine($"Transpiling {files.Length} Router files...");
foreach (var file in files)
{
string baseName = Path.GetFileNameWithoutExtension(file).Replace(".", "_");
string goFileName = ToSnakeCase(baseName) + ".go";
string dstPath = Path.Combine(dstDir, goFileName);
string code = File.ReadAllText(file);
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetCompilationUnitRoot();
var emitter = new GoCodeEmitter();
emitter.AddImport("sync");
emitter.AddImport("context");
var fileMethodNames = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var typeDeclarations = root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
foreach (var typeDecl in typeDeclarations)
{
string trivia = CSharpToGoConverter.ConvertTrivia(typeDecl.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(trivia))
emitter.Append(trivia);
string prefix = baseName;
if (typeDecl is RecordDeclarationSyntax rec)
{
TranspileRecord(rec, emitter, prefix);
}
else if (typeDecl is ClassDeclarationSyntax cls)
{
if (cls.Identifier.Text == "Table" || cls.Identifier.Text == "Lane")
{
continue;
}
if (cls.Identifier.Text == "GameRouter")
{
if (baseName.Equals("GameRouter", StringComparison.OrdinalIgnoreCase))
{
TranspileWireClass(cls, emitter, prefix, CollidingCatalogNames);
}
}
else
{
TranspileWireClass(cls, emitter, prefix, CollidingCatalogNames);
}
if (cls.Members.OfType<MethodDeclarationSyntax>().Any())
{
TranspileRouterClass(cls, emitter, baseName, fileMethodNames);
}
}
else if (typeDecl is EnumDeclarationSyntax enm)
{
TranspileEnum(enm, emitter, prefix);
}
else if (typeDecl is StructDeclarationSyntax str)
{
TranspileStruct(str, emitter, prefix);
}
}
File.WriteAllText(dstPath, emitter.Generate("routers"), Encoding.UTF8);
}
}
static void TranspileRouterClass(ClassDeclarationSyntax cls, GoCodeEmitter emitter, string moduleName, Dictionary<string, int> fileMethodNames)
{
string routerPrefix = CSharpToGoConverter.ExportName(moduleName);
foreach (var m in cls.Members.OfType<MethodDeclarationSyntax>())
{
string mTrivia = CSharpToGoConverter.ConvertTrivia(m.GetLeadingTrivia());
if (!string.IsNullOrWhiteSpace(mTrivia))
emitter.Append(mTrivia);
string mBase = routerPrefix + "_" + CSharpToGoConverter.ExportName(m.Identifier.Text);
fileMethodNames[mBase] = fileMethodNames.GetValueOrDefault(mBase, 0) + 1;
string mName = fileMethodNames[mBase] > 1 ? $"{mBase}_{fileMethodNames[mBase]}" : mBase;
string retType = CSharpToGoConverter.ConvertType(m.ReturnType, emitter, simplifyExternal: true);
var paramList = new List<string>();
foreach (var p in m.ParameterList.Parameters)
{
string pClean = CSharpToGoConverter.CleanParamName(p.Identifier.Text);
paramList.Add($"{pClean} {CSharpToGoConverter.ConvertType(p.Type, emitter, simplifyExternal: true)}");
}
emitter.AppendLine($"func {mName}(ctx context.Context, {string.Join(", ", paramList)}) {retType} {{");
emitter.Indent();
emitter.AppendLine($"// Router implementation for {mName}");
if (!string.IsNullOrEmpty(retType))
{
if (retType.StartsWith("[]") || retType.StartsWith("map") || retType.StartsWith("*"))
emitter.AppendLine("return nil");
else if (retType == "bool")
emitter.AppendLine("return true");
else if (retType == "string")
emitter.AppendLine("return \"\"");
else if (retType == "error")
emitter.AppendLine("return nil");
else if (retType.StartsWith("int") || retType.StartsWith("uint") || retType.StartsWith("float") || retType == "byte" || retType == "rune")
emitter.AppendLine("return 0");
else
emitter.AppendLine($"var zero {retType}; return zero");
}
emitter.Unindent();
emitter.AppendLine("}");
emitter.AppendLine();
}
}
static string ToSnakeCase(string input)
{
if (string.IsNullOrEmpty(input)) return input;
var sb = new StringBuilder();
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
if (char.IsUpper(c))
{
if (i > 0 && (char.IsLower(input[i - 1]) || (i + 1 < input.Length && char.IsLower(input[i + 1]))))
{
sb.Append('_');
}
sb.Append(char.ToLowerInvariant(c));
}
else
{
sb.Append(c);
}
}
return sb.ToString().Replace("__", "_");
}
static string ToJsonName(string input)
{
if (string.IsNullOrEmpty(input)) return input;
return char.ToLowerInvariant(input[0]) + input.Substring(1);
}
}