From cf82daad001ba5bee6ad6299a590f232ed2179bc Mon Sep 17 00:00:00 2001 From: rfi Date: Fri, 24 Nov 2023 11:02:41 +0700 Subject: [PATCH] TableReaderV2 finally, caching pending, please migrate carefully --- .gitignore | 5 +- AscNet.Common/Util/TableReader.cs | 93 +++++++++++++++++++ AscNet.Table/AscNet.Table.csproj | 4 +- AscNet.Table/TableGenerator.cs | 6 +- AscNet.Table/TableGeneratorV2.cs | 144 ++++++++++++++++++++++++++++++ AscNet/Program.cs | 2 + 6 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 AscNet.Table/TableGeneratorV2.cs diff --git a/.gitignore b/.gitignore index 9491a2f..dba3ee0 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Generator logs +generator_fails* \ No newline at end of file diff --git a/AscNet.Common/Util/TableReader.cs b/AscNet.Common/Util/TableReader.cs index 969fd8b..da1f26a 100644 --- a/AscNet.Common/Util/TableReader.cs +++ b/AscNet.Common/Util/TableReader.cs @@ -1,4 +1,5 @@ using AscNet.Logging; +using System.Reflection; namespace AscNet.Common.Util { @@ -28,5 +29,97 @@ namespace AscNet.Common.Util public abstract void Load(); } + + public interface ITable + { + abstract static string File { get; } + } + + public static class TableReaderV2 + { + private static readonly Logger c = new(typeof(TableReaderV2), nameof(TableReaderV2), LogLevel.DEBUG, LogLevel.DEBUG); + + public static List Parse() where T : ITable + { + List result = new(); + + try + { + using (var reader = new StreamReader(T.File)) + { + // Read the header line to get column names + string headerLine = reader.ReadLine()!; + string[] columnNames = headerLine.Split('\t'); + + // Read data lines and parse them into objects + while (!reader.EndOfStream) + { + string dataLine = reader.ReadLine()!; + if (string.IsNullOrEmpty(dataLine)) + break; + + string[] values = dataLine.Split('\t'); + + T obj = MapToObject(columnNames, values); + result.Add(obj); + } + } + } + catch (Exception ex) + { + c.Error($"An error occurred: {ex.Message}"); + } + + return result; + } + + static T MapToObject(string[] columnNames, string[] values) where T : ITable + { + T obj = Activator.CreateInstance(); + + for (int i = 0; i < Math.Min(columnNames.Length, values.Length); i++) + { + PropertyInfo? prop = typeof(T).GetProperty(columnNames[i].Split('[').First()); + if (prop != null) + { + if (prop.PropertyType == typeof(List)) + { + if (prop.GetValue(obj) is null) + { + prop.SetValue(obj, new List()); + } + string value = values[i]; + if (!string.IsNullOrEmpty(value)) + { + prop.PropertyType.GetMethod("Add").Invoke(prop.GetValue(obj), new object[] { int.Parse(value) }); + } + } + else if (prop.PropertyType == typeof(List)) + { + if (prop.GetValue(obj) is null) + { + prop.SetValue(obj, new List()); + } + string value = values[i]; + if (!string.IsNullOrEmpty(value)) + { + prop.PropertyType.GetMethod("Add").Invoke(prop.GetValue(obj), new object[] { value }); + } + } + else if (prop.PropertyType == typeof(int)) + { + // For int properties like GroupId + prop.SetValue(obj, int.Parse(values[i])); + } + else + { + prop.SetValue(obj, values[i]); + } + } + } + + return obj; + } + } } diff --git a/AscNet.Table/AscNet.Table.csproj b/AscNet.Table/AscNet.Table.csproj index 1df9f1f..094733d 100644 --- a/AscNet.Table/AscNet.Table.csproj +++ b/AscNet.Table/AscNet.Table.csproj @@ -14,7 +14,7 @@ - + @@ -22,7 +22,7 @@ - + \ No newline at end of file diff --git a/AscNet.Table/TableGenerator.cs b/AscNet.Table/TableGenerator.cs index d760f3a..78141a3 100644 --- a/AscNet.Table/TableGenerator.cs +++ b/AscNet.Table/TableGenerator.cs @@ -43,7 +43,7 @@ namespace AscNet.Table } types.Add("global::System.String"); } - resolvedTypes.Add(types.ToArray()); + resolvedTypes.Add([.. types]); } Dictionary properties = new(); @@ -152,11 +152,13 @@ namespace AscNet.Table{ns} { fails.Add(new { - msg = ex.Message, + msg = ex.ToString(), file = table.Path }); } } + + // File.WriteAllText("./generator_fails.json", JsonConvert.SerializeObject(fails, Formatting.Indented)); } public void Initialize(GeneratorInitializationContext context) { } diff --git a/AscNet.Table/TableGeneratorV2.cs b/AscNet.Table/TableGeneratorV2.cs new file mode 100644 index 0000000..6201052 --- /dev/null +++ b/AscNet.Table/TableGeneratorV2.cs @@ -0,0 +1,144 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System; +using System.IO; +// using Newtonsoft.Json; + +namespace AscNet.Table +{ + [Generator] + public class TableGeneratorV2 : ISourceGenerator + { + public void Execute(GeneratorExecutionContext context) + { + List fails = new(); + foreach (var table in context.AdditionalFiles.Where(x => x.Path.EndsWith(".tsv"))) + { + try + { + string ns = string.Join("", Path.GetDirectoryName(table.Path)?.Split(new string[] { "table" }, StringSplitOptions.None).Skip(1)!) + .Replace('\\', '/').Replace('/', '.'); + + string content = (table.GetText()?.ToString()) ?? throw new FormatException("File is empty!"); + List lines = content.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries).ToList(); + + List members = new(); + List membersRealCount = new(); + string headLine = lines.First(); + lines.RemoveAt(0); + + foreach (var head in headLine.Split('\t')) + { + if (head.Contains('[')) + { + if (members.Any(x => x.Name == head.Split('[').First())) + { + MemberType memberType = members.First(x => x.Name == head.Split('[').First()); + memberType.Span++; + } + else + { + members.Add(new MemberType + { + Name = head.Split('[').First(), + Nullable = false, + Span = 1 + }); + } + } + else + { + members.Add(new MemberType + { + Name = head, + Nullable = false, + Span = 1 + }); + } + membersRealCount.Add(members.First(x => x.Name == head.Split('[').First())); + } + + for (int x = 0; x < lines.Count; x++) + { + string[] values = lines[x].Split('\t'); + + for (int y = 0; y < values.Length; y++) + { + string value = values[y]; + MemberType memberType = membersRealCount[y]; + if (string.IsNullOrEmpty(value)) + { + memberType.Nullable = true; + } + else + { + if (int.TryParse(value, out int intVal)) + { + if (memberType.Type != typeof(string).FullName) + memberType.Type = typeof(int).FullName; + } + else + { + memberType.Type = typeof(string).FullName; + } + } + membersRealCount[y] = memberType; + } + } + + string propDefs = string.Empty; + foreach (MemberType memberType in membersRealCount.GroupBy(x => x.Name).Select(g => g.First())) + { + if (string.IsNullOrEmpty(memberType.Type) || string.IsNullOrEmpty(memberType.Name)) + continue; + + if (membersRealCount.Where(x => x.Name == memberType.Name).Count() > 1) + { + propDefs += $"public List<{memberType.Type}> {memberType.Name} {{ get; set; }}\r\n\t"; + } + else + { + propDefs += $"public {memberType.Type + (memberType.Nullable ? "?" : string.Empty)} {memberType.Name} {{ get; set; }}\r\n\t"; + } + } + + string file = $@"// +namespace AscNet.Table.V2{ns} +{{ + #nullable enable + #pragma warning disable CS8618, CS8602 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public class {Path.GetFileNameWithoutExtension(table.Path)}Table : global::AscNet.Common.Util.ITable + {{ + public static string File => ""{string.Join("", table.Path.Replace("\\", "/").Split(new string[] { "/Resources/" }, StringSplitOptions.None).Skip(1))}""; + + {propDefs}}} +}}"; + + context.AddSource($"V2{ns}.{Path.GetFileNameWithoutExtension(table.Path)}V2.g.cs", file); + } + catch (Exception ex) + { + + fails.Add(new + { + msg = ex.ToString(), + file = table.Path + }); + } + } + + // File.WriteAllText("./generator_fails_new.json", JsonConvert.SerializeObject(fails, Formatting.Indented)); + } + + public void Initialize(GeneratorInitializationContext context) { } + + struct MemberType + { + public string Name { get; set; } + public string Type { get; set; } + public bool Nullable { get; set; } + public int Span { get; set; } + } + } +} diff --git a/AscNet/Program.cs b/AscNet/Program.cs index 8e12ac1..a8909e9 100644 --- a/AscNet/Program.cs +++ b/AscNet/Program.cs @@ -2,6 +2,8 @@ using AscNet.GameServer.Handlers; using AscNet.GameServer.Commands; using AscNet.Logging; +using AscNet.Common.Util; +using Newtonsoft.Json; namespace AscNet {