From e224f58a3b4de745b85c353fef3ce1f445a0609e Mon Sep 17 00:00:00 2001
From: Kybxd <627940450@qq.com>
Date: Wed, 30 Apr 2025 10:33:33 +0800
Subject: [PATCH 1/6] feat: init proj
feat: update go.mod and readme
feat: add some template code
feat: load from bin
feat: only support json and bin
feat: hub
feat: load methods
feat: add filter huboptions
feat: a simple example
feat: git ignore
feat: remove LoadMode
feat: update readme
feat: csharp ordered map
feat: add index
feat: encapsulate messager container
fix(index): correct csharp index logic and add test examples
feat: update index and version
feat: simplify some logic
feat: escapeIdentifier
feat: best practice
feat: go mod tidy
feat: simplify code and remove gen plugin as parameters
feat: use lower camel case for private variables
feat: OrderedMapValue not null
feat: add MessageParser instead of generic types
feat: add test case for load bin
feat: classes
feat: options and newline
feat: delete settings
feat: add bat scripts
feat: support ordered index
feat: update bin ext
fix: generate file path
feat: use *.pc.cs file ext
feat: thread-safe messager container
feat: use template to genearate hub
feat: update scripts
feat: add readonly
feat: orderedmap
feat: index
feat: revert
feat: ordered index
feat: ident
feat: IComparable
feat: map key
feat: map key
feat: use tuple
feat: leveld index
feat(helper): enhance C# class type parsing and namespace handling
---
.gitignore | 4 +
README.md | 13 +
cmd/protoc-gen-csharp-tableau-loader/embed.go | 34 +
.../embed/Load.pc.cs | 221 +++
.../embed/Util.pc.cs | 61 +
.../embed/templates/Hub.pc.cs.tpl | 146 ++
.../helper/helper.go | 278 ++++
.../helper/keyword.go | 117 ++
cmd/protoc-gen-csharp-tableau-loader/hub.go | 21 +
.../indexes/generator.go | 154 +++
.../indexes/index.go | 343 +++++
.../indexes/ordered_index.go | 340 +++++
cmd/protoc-gen-csharp-tableau-loader/main.go | 38 +
.../messager.go | 173 +++
.../orderedmap/ordered_map.go | 205 +++
internal/options/options.go | 1 +
test/csharp-tableau-loader/Loader.csproj | 15 +
test/csharp-tableau-loader/Program.cs | 107 ++
.../csharp-tableau-loader.sln | 24 +
test/csharp-tableau-loader/gen.bat | 83 ++
test/csharp-tableau-loader/gen.sh | 72 +
.../tableau/HeroConf.pc.cs | 176 +++
test/csharp-tableau-loader/tableau/Hub.pc.cs | 209 +++
.../tableau/IndexConf.pc.cs | 1215 +++++++++++++++++
.../tableau/ItemConf.pc.cs | 630 +++++++++
test/csharp-tableau-loader/tableau/Load.pc.cs | 228 ++++
.../tableau/PatchConf.pc.cs | 190 +++
.../tableau/TestConf.pc.cs | 889 ++++++++++++
test/csharp-tableau-loader/tableau/Util.pc.cs | 68 +
test/proto/hero_conf.proto | 2 +-
test/testdata/bin/HeroConf.binpb | 11 +
test/testdata/conf/ItemConf.json | 22 +-
32 files changed, 6087 insertions(+), 3 deletions(-)
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/embed.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/helper/helper.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/hub.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/indexes/index.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/main.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/messager.go
create mode 100644 cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go
create mode 100644 test/csharp-tableau-loader/Loader.csproj
create mode 100644 test/csharp-tableau-loader/Program.cs
create mode 100644 test/csharp-tableau-loader/csharp-tableau-loader.sln
create mode 100644 test/csharp-tableau-loader/gen.bat
create mode 100644 test/csharp-tableau-loader/gen.sh
create mode 100644 test/csharp-tableau-loader/tableau/HeroConf.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/Hub.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/IndexConf.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/ItemConf.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/Load.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/PatchConf.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/TestConf.pc.cs
create mode 100644 test/csharp-tableau-loader/tableau/Util.pc.cs
create mode 100644 test/testdata/bin/HeroConf.binpb
diff --git a/.gitignore b/.gitignore
index 9c0a1db2..de4ff9c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,7 +39,11 @@ node_modules
cmd/protoc-gen-cpp-tableau-loader/protoc-gen-cpp-tableau-loader
cmd/protoc-gen-go-tableau-loader/protoc-gen-go-tableau-loader
+cmd/protoc-gen-csharp-tableau-loader/protoc-gen-csharp-tableau-loader
test/go-tableau-loader/go-tableau-loader
_lab/ts/src/protoconf
coverage.txt
+!test/testdata/bin/
+test/csharp-tableau-loader/protoconf
+
diff --git a/README.md b/README.md
index 4ff8e00d..6136676f 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,19 @@ The official config loader for [Tableau](https://github.com/tableauio/tableau).
- [Protocol Buffers Go Reference](https://protobuf.dev/reference/go/)
+## C#
+
+### Requirements
+
+- dotnet-sdk-8.0
+
+### Test
+
+- Install: **dotnet-sdk-8.0**
+- Change dir: `cd test/csharp-tableau-loader`
+- Generate protoconf: `sh gen.sh`
+- Test: `dotnet run`
+
## TypeScript
### Requirements
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed.go b/cmd/protoc-gen-csharp-tableau-loader/embed.go
new file mode 100644
index 00000000..9961feda
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "embed"
+ "path"
+
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "google.golang.org/protobuf/compiler/protogen"
+)
+
+//go:embed embed/*
+var efs embed.FS
+
+// generateEmbed generates related registry files.
+func generateEmbed(gen *protogen.Plugin) {
+ entries, err := efs.ReadDir("embed")
+ if err != nil {
+ panic(err)
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+
+ g := gen.NewGeneratedFile(entry.Name(), "")
+ helper.GenerateFileHeader(gen, nil, g, version)
+ // refer: [embed: embed path on different OS cannot open file](https://github.com/golang/go/issues/45230)
+ content, err := efs.ReadFile(path.Join("embed", entry.Name()))
+ if err != nil {
+ panic(err)
+ }
+ g.P(string(content))
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
new file mode 100644
index 00000000..23406389
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
@@ -0,0 +1,221 @@
+using pb = global::Google.Protobuf;
+using pbr = global::Google.Protobuf.Reflection;
+namespace Tableau
+{
+ ///
+ /// Load provides functions for loading and parsing protobuf messages from files.
+ ///
+ public static class Load
+ {
+ ///
+ /// ReadFunc reads the config file and returns its content.
+ ///
+ public delegate byte[] ReadFunc(string path);
+ ///
+ /// LoadFunc defines a func which can load message's content based on the
+ /// given descriptor, path, format, and options.
+ ///
+ public delegate pb::IMessage? LoadFunc(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options);
+
+ ///
+ /// BaseOptions is the common options for both global-level and messager-level options.
+ ///
+ public class BaseOptions
+ {
+ ///
+ /// Whether to ignore unknown JSON fields during parsing.
+ ///
+ public bool? IgnoreUnknownFields { get; set; }
+ ///
+ /// You can specify custom read function to read a config file's content.
+ /// Default is File.ReadAllBytes.
+ ///
+ public ReadFunc? ReadFunc { get; set; }
+ ///
+ /// You can specify custom load function to load a messager's content.
+ /// Default is LoadMessager.
+ ///
+ public LoadFunc? LoadFunc { get; set; }
+ }
+
+ ///
+ /// Options is the global-level options, which contains both global-level and
+ /// messager-level options.
+ ///
+ public class Options : BaseOptions
+ {
+ ///
+ /// MessagerOptions maps each messager name to a MessagerOptions.
+ /// If specified, then the messager will be parsed with the given options directly.
+ ///
+ public Dictionary? MessagerOptions { get; set; }
+ ///
+ /// ParseMessagerOptionsByName parses messager options with both global-level and
+ /// messager-level options taken into consideration.
+ ///
+ public MessagerOptions ParseMessagerOptionsByName(string name)
+ {
+ var mopts = MessagerOptions?.TryGetValue(name, out var val) == true ? (MessagerOptions)val.Clone() : new MessagerOptions();
+ mopts.IgnoreUnknownFields ??= IgnoreUnknownFields;
+ mopts.ReadFunc ??= ReadFunc;
+ mopts.LoadFunc ??= LoadFunc;
+ return mopts;
+ }
+ }
+
+ ///
+ /// MessagerOptions defines the options for loading a messager.
+ ///
+ public class MessagerOptions : BaseOptions, ICloneable
+ {
+ ///
+ /// Path maps each messager name to a corresponding config file path.
+ /// If specified, then the main messager will be parsed from the file
+ /// directly, other than the specified load dir.
+ ///
+ public string? Path { get; set; }
+
+ public object Clone()
+ {
+ return new MessagerOptions
+ {
+ IgnoreUnknownFields = IgnoreUnknownFields,
+ ReadFunc = ReadFunc,
+ LoadFunc = LoadFunc,
+ Path = Path
+ };
+ }
+ }
+
+ ///
+ /// LoadMessager loads a protobuf message from the specified file path and format.
+ ///
+ public static pb::IMessage? LoadMessager(pbr::MessageDescriptor desc, string path, Format fmt, in MessagerOptions? options = null)
+ {
+ var readFunc = options?.ReadFunc ?? File.ReadAllBytes;
+ byte[] content;
+ try
+ {
+ content = readFunc(path);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to read {path}: {ex.Message}");
+ throw;
+ }
+ return Unmarshal(content, desc, fmt, options);
+ }
+
+ ///
+ /// LoadMessagerInDir loads a protobuf message from the specified directory and format.
+ /// It resolves the file path based on the message descriptor name.
+ ///
+ public static pb::IMessage? LoadMessagerInDir(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options = null)
+ {
+ string name = desc.Name;
+ string path = "";
+ if (options?.Path != null)
+ {
+ path = options.Path;
+ fmt = Util.GetFormat(path);
+ }
+ if (path == "")
+ {
+ string filename = name + Util.Format2Ext(fmt);
+ path = Path.Combine(dir, filename);
+ }
+ var loadFunc = options?.LoadFunc ?? LoadMessager;
+ return loadFunc(desc, path, fmt, options);
+ }
+
+ ///
+ /// Unmarshal parses the given byte content into a protobuf message based on the specified format.
+ ///
+ public static pb::IMessage? Unmarshal(byte[] content, pbr::MessageDescriptor desc, Format fmt, in MessagerOptions? options = null)
+ {
+ switch (fmt)
+ {
+ case Format.JSON:
+ try
+ {
+ var parser = new pb::JsonParser(
+ pb::JsonParser.Settings.Default.WithIgnoreUnknownFields(options?.IgnoreUnknownFields ?? false)
+ );
+ return parser.Parse(new StreamReader(new MemoryStream(content)), desc);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to parse {desc.Name}.json: {ex.Message}");
+ throw;
+ }
+ case Format.Bin:
+ try
+ {
+ return desc.Parser.ParseFrom(content);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to parse {desc.Name}.binpb: {ex.Message}");
+ throw;
+ }
+ default:
+ Util.SetErrMsg($"unknown format: {fmt}");
+ return null;
+ }
+ }
+ }
+
+ ///
+ /// IMessagerName is an interface that provides the static Name() method for messagers.
+ ///
+ public interface IMessagerName
+ {
+ static abstract string Name();
+ }
+
+ ///
+ /// Messager is the base class for all generated configuration messagers.
+ /// It is designed for three goals:
+ /// 1. Easy use: simple yet powerful accessors.
+ /// 2. Elegant API: concise and clean functions.
+ /// 3. Extensibility: Map, OrderedMap, Index, OrderedIndex...
+ ///
+ public abstract class Messager
+ {
+ ///
+ /// Stats contains statistics info about loading.
+ ///
+ public class Stats
+ {
+ /// Total load time consuming.
+ public TimeSpan Duration;
+ }
+
+ protected Stats LoadStats = new();
+
+ ///
+ /// GetStats returns the loading stats info.
+ ///
+ public ref readonly Stats GetStats() => ref LoadStats;
+
+ ///
+ /// Load fills message from file in the specified directory and format.
+ ///
+ public abstract bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null);
+
+ ///
+ /// Message returns the inner protobuf message data.
+ ///
+ public virtual pb::IMessage? Message() => null;
+
+ ///
+ /// ProcessAfterLoad is invoked after this messager loaded.
+ ///
+ protected virtual bool ProcessAfterLoad() => true;
+
+ ///
+ /// ProcessAfterLoadAll is invoked after all messagers loaded.
+ ///
+ public virtual bool ProcessAfterLoadAll(in Hub hub) => true;
+ }
+}
\ No newline at end of file
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs b/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs
new file mode 100644
index 00000000..e720eb81
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs
@@ -0,0 +1,61 @@
+namespace Tableau
+{
+ ///
+ /// Format specifies the format of the configuration file.
+ ///
+ public enum Format
+ {
+ Unknown,
+ JSON,
+ Bin
+ }
+
+ ///
+ /// Util provides common utility functions.
+ ///
+ public static class Util
+ {
+ [ThreadStatic] private static string? _errMsg;
+
+ ///
+ /// Get the last error message.
+ ///
+ public static string GetErrMsg() => _errMsg ?? "";
+
+ ///
+ /// Set the error message.
+ ///
+ public static void SetErrMsg(string msg) => _errMsg = msg;
+
+ private const string _unknownExt = ".unknown";
+ private const string _jsonExt = ".json";
+ private const string _binExt = ".binpb";
+
+ ///
+ /// GetFormat returns the Format type determined by the file extension of the given path.
+ ///
+ public static Format GetFormat(string path)
+ {
+ string ext = Path.GetExtension(path);
+ return ext switch
+ {
+ _jsonExt => Format.JSON,
+ _binExt => Format.Bin,
+ _ => Format.Unknown,
+ };
+ }
+
+ ///
+ /// Format2Ext returns the file extension corresponding to the given format.
+ ///
+ public static string Format2Ext(Format fmt)
+ {
+ return fmt switch
+ {
+ Format.JSON => _jsonExt,
+ Format.Bin => _binExt,
+ _ => _unknownExt,
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
new file mode 100644
index 00000000..4d08249f
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
@@ -0,0 +1,146 @@
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// MessagerContainer holds all messager instances and provides fast access.
+ ///
+ internal class MessagerContainer(in Dictionary? messagerMap = null)
+ {
+ public Dictionary MessagerMap = messagerMap ?? [];
+ public DateTime LastLoadedTime = DateTime.Now;
+{{ range . }} public {{ . }}? {{ . }} = InternalGet<{{ . }}>(messagerMap);
+{{ end }}
+ ///
+ /// Get returns the messager of type T from the container.
+ ///
+ public T? Get() where T : Messager, IMessagerName => InternalGet(MessagerMap);
+
+ private static T? InternalGet(in Dictionary? messagerMap) where T : Messager, IMessagerName =>
+ messagerMap?.TryGetValue(T.Name(), out var messager) == true ? (T)messager : null;
+ }
+
+ ///
+ /// Atomic provides a thread-safe wrapper for reference types.
+ ///
+ internal class Atomic where T : class
+ {
+ private T? _value;
+
+ public T? Value
+ {
+ get => Interlocked.CompareExchange(ref _value, null, null);
+ set => Interlocked.Exchange(ref _value, value);
+ }
+ }
+
+ ///
+ /// HubOptions is the options for Hub.
+ ///
+ public class HubOptions
+ {
+ ///
+ /// Filter can only filter in certain specific messagers based on the
+ /// condition that you provide.
+ ///
+ public Func? Filter { get; set; }
+ }
+
+ ///
+ /// Hub is the messager manager. It manages loading, accessing, and storing
+ /// all configuration messagers.
+ ///
+ public class Hub(HubOptions? options = null)
+ {
+ private readonly Atomic _messagerContainer = new();
+ private readonly HubOptions? _options = options;
+
+ ///
+ /// Load fills messages from files in the specified directory and format.
+ ///
+ public bool Load(string dir, Format fmt, in Load.Options? options = null)
+ {
+ var messagerMap = NewMessagerMap();
+ var opts = options ?? new Load.Options();
+ foreach (var kvs in messagerMap)
+ {
+ string name = kvs.Key;
+ if (!kvs.Value.Load(dir, fmt, opts.ParseMessagerOptionsByName(name)))
+ {
+ Console.Error.WriteLine($"load {name} failed: {Util.GetErrMsg()}");
+ return false;
+ }
+ }
+ var tmpHub = new Hub();
+ tmpHub.SetMessagerMap(messagerMap);
+ foreach (var messager in messagerMap)
+ {
+ if (!messager.Value.ProcessAfterLoadAll(tmpHub))
+ {
+ Console.Error.WriteLine($"hub call ProcessAfterLoadAll failed, messager: {messager.Key}");
+ return false;
+ }
+ }
+ SetMessagerMap(messagerMap);
+ return true;
+ }
+
+ ///
+ /// GetMessagerMap returns the current messager map.
+ ///
+ public IReadOnlyDictionary? GetMessagerMap() => _messagerContainer.Value?.MessagerMap;
+
+ ///
+ /// SetMessagerMap sets the messager map with thread-safe guarantee.
+ ///
+ public void SetMessagerMap(in Dictionary map) => _messagerContainer.Value = new MessagerContainer(map);
+
+ ///
+ /// Get returns the messager of type T from the hub.
+ ///
+ public T? Get() where T : Messager, IMessagerName => _messagerContainer.Value?.Get();
+{{ range . }}
+ public {{ . }}? Get{{ . }}() => _messagerContainer.Value?.{{ . }};
+{{ end }}
+ ///
+ /// GetLastLoadedTime returns the time when hub's messager container was last set.
+ ///
+ public DateTime? GetLastLoadedTime() => _messagerContainer.Value?.LastLoadedTime;
+
+ ///
+ /// NewMessagerMap creates a new MessagerMap based on the registered messagers.
+ ///
+ private Dictionary NewMessagerMap()
+ {
+ var messagerMap = new Dictionary();
+ foreach (var kv in Registry.Registrar)
+ {
+ if (_options?.Filter?.Invoke(kv.Key) ?? true)
+ {
+ messagerMap[kv.Key] = kv.Value();
+ }
+ }
+ return messagerMap;
+ }
+ }
+
+ ///
+ /// Registry manages the registration of all messager generators.
+ ///
+ public class Registry
+ {
+ internal static readonly Dictionary> Registrar = [];
+
+ ///
+ /// Register registers a messager generator for type T.
+ ///
+ public static void Register() where T : Messager, IMessagerName, new() => Registrar[T.Name()] = () => new T();
+
+ ///
+ /// Init registers all generated messagers.
+ ///
+ public static void Init()
+ {
+{{ range . }} Register<{{ . }}>();
+{{ end }} }
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go
new file mode 100644
index 00000000..e3d2cede
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go
@@ -0,0 +1,278 @@
+package helper
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/iancoleman/strcase"
+ "github.com/tableauio/tableau/proto/tableaupb"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/types/descriptorpb"
+)
+
+func GenerateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, version string) {
+ g.P("// ")
+ g.P("// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.")
+ g.P("// versions:")
+ g.P("// - protoc-gen-csharp-tableau-loader v", version)
+ g.P("// - protoc ", protocVersion(gen))
+ if file != nil {
+ if file.Proto.GetOptions().GetDeprecated() {
+ g.P("// ", file.Desc.Path(), " is a deprecated file.")
+ } else {
+ g.P("// source: ", file.Desc.Path())
+ }
+ }
+ g.P("// ")
+ g.P("#nullable enable")
+}
+
+func protocVersion(gen *protogen.Plugin) string {
+ v := gen.Request.GetCompilerVersion()
+ if v == nil {
+ return "(unknown)"
+ }
+ var suffix string
+ if s := v.GetSuffix(); s != "" {
+ suffix = "-" + s
+ }
+ return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix)
+}
+
+func ParseIndexFieldName(fd protoreflect.FieldDescriptor) string {
+ return ParseCsharpPropertyName(fd)
+}
+
+// ParseCsharpPropertyName converts a protobuf field descriptor to the corresponding
+// C# property name, matching the behavior of protoc's C# code generator.
+// When the PascalCase field name collides with its containing type name,
+// "Types", or "Descriptor", a trailing underscore is appended to avoid conflicts.
+// See: protobuf/src/google/protobuf/compiler/csharp/csharp_helpers.cc GetPropertyName()
+func ParseCsharpPropertyName(fd protoreflect.FieldDescriptor) string {
+ propertyName := strcase.ToCamel(string(fd.Name()))
+ containingName := string(fd.Parent().(protoreflect.MessageDescriptor).Name())
+ if propertyName == containingName || propertyName == "Types" || propertyName == "Descriptor" {
+ propertyName += "_"
+ }
+ return propertyName
+}
+
+func ParseIndexFieldNameAsKeyStructFieldName(fd protoreflect.FieldDescriptor) string {
+ if fd.IsList() {
+ opts := fd.Options().(*descriptorpb.FieldOptions)
+ fdOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions)
+ return strcase.ToCamel(fdOpts.GetName())
+ }
+ // NOTE: Do not use ParseCsharpPropertyName here, because key struct field names
+ // are defined in our own code (not protobuf-generated), so they are not subject
+ // to protoc's property name conflict resolution (appending "_" suffix).
+ return strcase.ToCamel(string(fd.Name()))
+}
+
+func ParseIndexFieldNameAsFuncParam(fd protoreflect.FieldDescriptor) string {
+ return escapeIdentifier(strcase.ToLowerCamel(ParseIndexFieldNameAsKeyStructFieldName(fd)))
+}
+
+// ParseCsharpType converts a FieldDescriptor to C# type string.
+func ParseCsharpType(fd protoreflect.FieldDescriptor) string {
+ switch fd.Kind() {
+ case protoreflect.BoolKind:
+ return "bool"
+ case protoreflect.EnumKind:
+ fullname := string(fd.Enum().FullName())
+ seps := strings.Split(fullname, ".")
+ seps[0] = strcase.ToCamel(seps[0])
+ for i := 2; i < len(seps); i++ {
+ seps[i] = "Types." + seps[i]
+ }
+ return strings.Join(seps, ".")
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+ return "int"
+ case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+ return "uint"
+ case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+ return "long"
+ case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+ return "ulong"
+ case protoreflect.FloatKind:
+ return "float"
+ case protoreflect.DoubleKind:
+ return "double"
+ case protoreflect.StringKind, protoreflect.BytesKind:
+ return "string"
+ case protoreflect.MessageKind:
+ return ParseCsharpClassType(fd.Message())
+ // case protoreflect.GroupKind:
+ // return "group"
+ default:
+ return fmt.Sprintf("", fd.Kind())
+ }
+}
+
+func ParseCsharpClassType(md protoreflect.MessageDescriptor) string {
+ ns := ParseCsharpNamespace(md.ParentFile())
+ name := getNestedName(md)
+ if ns != "" {
+ return ns + "." + name
+ }
+ return name
+}
+
+func getNestedName(md protoreflect.MessageDescriptor) string {
+ parent, ok := md.Parent().(protoreflect.MessageDescriptor)
+ if ok {
+ return getNestedName(parent) + ".Types." + string(md.Name())
+ }
+ return string(md.Name())
+}
+
+func ParseCsharpNamespace(fd protoreflect.FileDescriptor) string {
+ if opts, ok := fd.Options().(*descriptorpb.FileOptions); ok && opts != nil {
+ if ns := opts.GetCsharpNamespace(); ns != "" {
+ return ns
+ }
+ }
+ return strcase.ToCamel(string(fd.Package()))
+}
+
+// ParseOrderedIndexKeyType converts a FieldDescriptor to its treemap key type.
+// fd must be an ordered type, or a message which can be converted to an ordered type.
+func ParseOrderedIndexKeyType(fd protoreflect.FieldDescriptor) string {
+ switch fd.Kind() {
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+ return "int"
+ case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+ return "uint"
+ case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+ return "long"
+ case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+ return "ulong"
+ case protoreflect.FloatKind:
+ return "float"
+ case protoreflect.DoubleKind:
+ return "double"
+ case protoreflect.StringKind:
+ return "string"
+ case protoreflect.EnumKind:
+ fullname := string(fd.Enum().FullName())
+ seps := strings.Split(fullname, ".")
+ seps[0] = strcase.ToCamel(seps[0])
+ for i := 2; i < len(seps); i++ {
+ seps[i] = "Types." + seps[i]
+ }
+ return strings.Join(seps, ".")
+ case protoreflect.MessageKind:
+ switch fd.Message().FullName() {
+ case "google.protobuf.Timestamp", "google.protobuf.Duration":
+ return "long"
+ default:
+ }
+ fallthrough
+ default:
+ panic(fmt.Sprintf("unsupported kind: %d", fd.Kind()))
+ }
+}
+
+func ParseMapFieldNameAsKeyStructFieldName(fd protoreflect.FieldDescriptor) string {
+ opts := fd.Options().(*descriptorpb.FieldOptions)
+ fdOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions)
+ name := fdOpts.GetKey()
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ valueFd := fd.MapValue().Message().Fields().Get(0)
+ name = string(valueFd.Name())
+ }
+ return strcase.ToCamel(name)
+}
+
+func ParseMapFieldNameAsFuncParam(fd protoreflect.FieldDescriptor) string {
+ fieldName := ParseMapFieldNameAsKeyStructFieldName(fd)
+ if fieldName == "" {
+ return fieldName
+ }
+ return escapeIdentifier(fieldName)
+}
+
+func ParseMapKeyType(fd protoreflect.FieldDescriptor) string {
+ return ParseCsharpType(fd)
+}
+
+func ParseMapValueType(fd protoreflect.FieldDescriptor) string {
+ return ParseCsharpType(fd.MapValue())
+}
+
+func GetTypeEmptyValue(fd protoreflect.FieldDescriptor) string {
+ switch fd.Kind() {
+ case protoreflect.BoolKind:
+ return "false"
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
+ protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
+ protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind,
+ protoreflect.Uint64Kind, protoreflect.Fixed64Kind, protoreflect.EnumKind:
+ return "0"
+ case protoreflect.FloatKind, protoreflect.DoubleKind:
+ return "0.0"
+ case protoreflect.StringKind:
+ return `""`
+ case protoreflect.BytesKind, protoreflect.MessageKind:
+ return "null"
+ // case protoreflect.GroupKind:
+ // return "group"
+ default:
+ return fmt.Sprintf("", fd.Kind())
+ }
+}
+
+func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect.FieldDescriptor) string {
+ if mapFd.MapValue().Kind() == protoreflect.MessageKind {
+ localMsgProtoName := strings.TrimPrefix(string(mapFd.MapValue().Message().FullName()), string(md.FullName())+".")
+ return strings.ReplaceAll(localMsgProtoName, ".", "_")
+ }
+ return mapFd.MapValue().Kind().String()
+}
+
+type MapKey struct {
+ Type string
+ Name string
+ FieldName string // multi-colunm index only
+ Fd protoreflect.FieldDescriptor // the map field descriptor this key belongs to
+}
+
+type MapKeySlice []MapKey
+
+func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice {
+ if newKey.Name == "" {
+ newKey.Name = fmt.Sprintf("key%d", len(s)+1)
+ }
+ for _, key := range s {
+ if key.Name == newKey.Name {
+ // rewrite to avoid name confict
+ newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1)
+ break
+ }
+ }
+ return append(s, newKey)
+}
+
+// GenGetParams generates function parameters, which are the names listed in the function's definition.
+func (s MapKeySlice) GenGetParams() string {
+ return s.GenCustom(func(key MapKey) string { return key.Type + " " + key.Name }, ", ")
+}
+
+// GenGetArguments generates function arguments, which are the real values passed to the function.
+func (s MapKeySlice) GenGetArguments() string {
+ return s.GenCustom(func(key MapKey) string { return key.Name }, ", ")
+}
+
+func (s MapKeySlice) GenCustom(fn func(MapKey) string, sep string) string {
+ var params []string
+ for _, key := range s {
+ params = append(params, fn(key))
+ }
+ return strings.Join(params, sep)
+}
+
+func Indent(depth int) string {
+ return strings.Repeat(" ", depth)
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go b/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go
new file mode 100644
index 00000000..7599c731
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go
@@ -0,0 +1,117 @@
+package helper
+
+import (
+ "strings"
+ "unicode"
+
+ "github.com/iancoleman/strcase"
+)
+
+var csharpKeywords map[string]bool
+
+func escapeIdentifier(str string) string {
+ // Filter invalid runes
+ var result strings.Builder
+ for _, r := range str {
+ if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
+ result.WriteRune(r)
+ }
+ }
+ str = result.String()
+ // To camel case
+ str = strcase.ToLowerCamel(str)
+ // Csharp variables must not start with digits
+ if len(str) != 0 && unicode.IsDigit(rune(str[0])) {
+ str = "_" + str
+ }
+ // Avoid csharp keywords
+ if _, ok := csharpKeywords[str]; ok {
+ return str + "_"
+ }
+ return str
+}
+
+// Ref:
+//
+// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords
+func init() {
+ csharpKeywords = map[string]bool{
+ "abstract": true,
+ "as": true,
+ "base": true,
+ "bool": true,
+ "break": true,
+ "byte": true,
+ "case": true,
+ "catch": true,
+ "char": true,
+ "checked": true,
+ "class": true,
+ "const": true,
+ "continue": true,
+ "decimal": true,
+ "default": true,
+ "delegate": true,
+ "do": true,
+ "double": true,
+ "else": true,
+ "enum": true,
+ "event": true,
+ "explicit": true,
+ "extern": true,
+ "false": true,
+ "finally": true,
+ "fixed": true,
+ "float": true,
+ "for": true,
+ "foreach": true,
+ "goto": true,
+ "if": true,
+ "implicit": true,
+ "in": true,
+ "int": true,
+ "interface": true,
+ "internal": true,
+ "is": true,
+ "lock": true,
+ "long": true,
+ "namespace": true,
+ "new": true,
+ "null": true,
+ "object": true,
+ "operator": true,
+ "out": true,
+ "override": true,
+ "params": true,
+ "private": true,
+ "protected": true,
+ "public": true,
+ "readonly": true,
+ "ref": true,
+ "return": true,
+ "sbyte": true,
+ "sealed": true,
+ "short": true,
+ "sizeof": true,
+ "stackalloc": true,
+ "static": true,
+ "string": true,
+ "struct": true,
+ "switch": true,
+ "this": true,
+ "throw": true,
+ "true": true,
+ "try": true,
+ "typeof": true,
+ "uint": true,
+ "ulong": true,
+ "unchecked": true,
+ "unsafe": true,
+ "ushort": true,
+ "using": true,
+ "virtual": true,
+ "void": true,
+ "volatile": true,
+ "while": true,
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/hub.go b/cmd/protoc-gen-csharp-tableau-loader/hub.go
new file mode 100644
index 00000000..da474435
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/hub.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+ "text/template"
+
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/internal/extensions"
+ "google.golang.org/protobuf/compiler/protogen"
+)
+
+var tpl = template.Must(template.New("").ParseFS(efs, "embed/templates/*"))
+
+// generateHub generates related hub files.
+func generateHub(gen *protogen.Plugin) {
+ filename := "Hub." + extensions.PC + ".cs"
+ g := gen.NewGeneratedFile(filename, "")
+ helper.GenerateFileHeader(gen, nil, g, version)
+ if err := tpl.Lookup(filename+".tpl").Execute(g, messagers); err != nil {
+ panic(err)
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go
new file mode 100644
index 00000000..29f84787
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go
@@ -0,0 +1,154 @@
+package indexes
+
+import (
+ "fmt"
+
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/internal/index"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+type Generator struct {
+ g *protogen.GeneratedFile
+ descriptor *index.IndexDescriptor
+ message *protogen.Message
+
+ // level message
+ keys helper.MapKeySlice
+}
+
+func NewGenerator(g *protogen.GeneratedFile, descriptor *index.IndexDescriptor, message *protogen.Message) *Generator {
+ generator := &Generator{
+ g: g,
+ descriptor: descriptor,
+ message: message,
+ }
+ generator.initLevelMessage()
+ return generator
+}
+
+func (x *Generator) initLevelMessage() {
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ if fd := lm.FD; fd != nil && fd.IsMap() {
+ // Only collect map keys/fds when a deeper level has an index or ordered index,
+ // because these keys are used solely for building upper-level (leveled) containers.
+ if !lm.NextLevel.NeedGenAnyIndex() {
+ break
+ }
+ x.keys = x.keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseMapKeyType(fd.MapKey()),
+ Name: helper.ParseMapFieldNameAsFuncParam(fd),
+ FieldName: helper.ParseMapFieldNameAsKeyStructFieldName(fd),
+ Fd: fd,
+ })
+ }
+ }
+}
+
+func (x *Generator) NeedGenerate() bool {
+ return x.needGenerateIndex() || x.needGenerateOrderedIndex()
+}
+
+func (x *Generator) levelKeyType(mapFd protoreflect.FieldDescriptor) string {
+ return fmt.Sprintf("LevelIndex_%sKey", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd))
+}
+
+func (x *Generator) mapValueType(index *index.LevelIndex) string {
+ return helper.ParseCsharpClassType(index.MD)
+}
+
+func (x *Generator) fieldGetter(fd protoreflect.FieldDescriptor) string {
+ return fmt.Sprintf(".%s", helper.ParseIndexFieldName(fd))
+}
+
+func (x *Generator) parseKeyFieldNameAndSuffix(field *index.LevelField) (string, string) {
+ var fieldName, suffix string
+ needEmptyValue := len(field.LeveledFDList) > 1
+ for i, leveledFd := range field.LeveledFDList {
+ if i != 0 {
+ fieldName += "?"
+ }
+ fieldName += x.fieldGetter(leveledFd)
+ if i == len(field.LeveledFDList)-1 && leveledFd.Message() != nil {
+ switch leveledFd.Message().FullName() {
+ case "google.protobuf.Timestamp", "google.protobuf.Duration":
+ suffix = "?.Seconds ?? 0"
+ needEmptyValue = false
+ default:
+ }
+ }
+ }
+ if field.FD.IsList() {
+ fieldName += " ?? Enumerable.Empty<" + helper.ParseCsharpType(field.FD) + ">()"
+ } else if needEmptyValue {
+ fieldName += " ?? " + helper.GetTypeEmptyValue(field.FD)
+ }
+ return fieldName, suffix
+}
+
+func (x *Generator) GenIndexTypeDef() {
+ if !x.NeedGenerate() {
+ return
+ }
+ // Generate LevelIndex key structs for intermediate map levels.
+ //
+ // x.keys holds one entry per map level whose next level still needs an
+ // index (populated by initLevelMessage). For a 3-level map keyed by
+ // (k1, k2, k3) with an index at the deepest level, x.keys = [k1, k2, k3].
+ //
+ // Level containers at depth 1 are keyed by a single scalar (k1), so no
+ // composite key struct is needed. The deepest level (depth = len(keys))
+ // also does not need one, because its full key combination (all keys)
+ // is already represented by the index's own key struct generated
+ // separately. Only intermediate depths (2 ≤ depth < len(keys)) require
+ // a LevelIndex struct that bundles all ancestor keys up to that depth:
+ //
+ // keys = [k1, k2, k3] → struct for depth 2: {k1, k2}
+ // keys = [k1, k2, k3, k4] → struct for depth 2: {k1, k2}
+ // struct for depth 3: {k1, k2, k3}
+ //
+ // The loop starts at i=2 (depth 2) and creates a struct from keys[:i].
+ // It runs len(x.keys)-2 times (0 times when len ≤ 2).
+ for i := 2; i < len(x.keys); i++ {
+ if i == 2 {
+ x.g.P()
+ x.g.P(helper.Indent(2), "// LevelIndex keys.")
+ }
+ fd := x.keys[i-1].Fd
+ keyType := x.levelKeyType(fd)
+ x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IEquatable<", keyType, ">")
+ x.g.P(helper.Indent(2), "{")
+ keys := x.keys[:i]
+ for _, key := range keys {
+ x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }")
+ }
+ x.g.P()
+ x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")")
+ x.g.P(helper.Indent(3), "{")
+ for _, key := range keys {
+ x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";")
+ }
+ x.g.P(helper.Indent(3), "}")
+ x.g.P()
+ x.g.P(helper.Indent(3), "public bool Equals(", keyType, " other) =>")
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").Equals((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));")
+ x.g.P()
+ x.g.P(helper.Indent(3), "public override int GetHashCode() =>")
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").GetHashCode();")
+ x.g.P(helper.Indent(2), "}")
+ x.g.P()
+ }
+ x.genIndexTypeDef()
+ x.genOrderedIndexTypeDef()
+}
+
+func (x *Generator) GenIndexLoader() {
+ x.genIndexLoader()
+ x.genOrderedIndexLoader()
+}
+
+func (x *Generator) GenIndexFinders() {
+ x.genIndexFinders()
+ x.genOrderedIndexFinders()
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go
new file mode 100644
index 00000000..b0f9d496
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go
@@ -0,0 +1,343 @@
+package indexes
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/iancoleman/strcase"
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/internal/index"
+ "github.com/tableauio/loader/internal/loadutil"
+ "github.com/tableauio/loader/internal/options"
+)
+
+func (x *Generator) needGenerateIndex() bool {
+ return options.NeedGenIndex(x.message.Desc, options.LangCS)
+}
+
+func (x *Generator) indexMapType(index *index.LevelIndex) string {
+ return fmt.Sprintf("Index_%sMap", index.Name())
+}
+
+func (x *Generator) indexMapKeyType(index *index.LevelIndex) string {
+ if len(index.ColFields) == 1 {
+ // single-column index
+ field := index.ColFields[0] // just take first field
+ return helper.ParseCsharpType(field.FD)
+ } else {
+ // multi-column index
+ return fmt.Sprintf("Index_%sKey", index.Name())
+ }
+}
+
+func (x *Generator) indexContainerName(index *index.LevelIndex, i int) string {
+ if i == 0 {
+ return fmt.Sprintf("_index%sMap", strcase.ToCamel(index.Name()))
+ }
+ return fmt.Sprintf("_index%sMap%d", strcase.ToCamel(index.Name()), i)
+}
+
+func (x *Generator) indexKeys(index *index.LevelIndex) helper.MapKeySlice {
+ var keys helper.MapKeySlice
+ for _, field := range index.ColFields {
+ keys = keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseCsharpType(field.FD),
+ Name: helper.ParseIndexFieldNameAsFuncParam(field.FD),
+ FieldName: helper.ParseIndexFieldNameAsKeyStructFieldName(field.FD),
+ })
+ }
+ return keys
+}
+
+func (x *Generator) genIndexTypeDef() {
+ if !x.needGenerateIndex() {
+ return
+ }
+ var once sync.Once
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.Indexes {
+ once.Do(func() { x.g.P(helper.Indent(2), "// Index types.") })
+ x.g.P(helper.Indent(2), "// Index: ", index.Index)
+ mapType := x.indexMapType(index)
+ keyType := x.indexMapKeyType(index)
+ valueType := x.mapValueType(index)
+ keys := x.indexKeys(index)
+ if len(index.ColFields) != 1 {
+ // Generate key struct
+ x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IEquatable<", keyType, ">")
+ x.g.P(helper.Indent(2), "{")
+ for _, key := range keys {
+ x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }")
+ }
+ x.g.P()
+ x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")")
+ x.g.P(helper.Indent(3), "{")
+ for _, key := range keys {
+ x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";")
+ }
+ x.g.P(helper.Indent(3), "}")
+ x.g.P()
+ x.g.P(helper.Indent(3), "public bool Equals(", keyType, " other) =>")
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").Equals((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));")
+ x.g.P()
+ x.g.P(helper.Indent(3), "public override int GetHashCode() =>")
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").GetHashCode();")
+ x.g.P(helper.Indent(2), "}")
+ x.g.P()
+ }
+ x.g.P(helper.Indent(2), "public class ", mapType, " : Dictionary<", keyType, ", List<", valueType, ">>;")
+ x.g.P()
+
+ x.g.P(helper.Indent(2), "private ", mapType, " ", x.indexContainerName(index, 0), " = [];")
+ x.g.P()
+ for i := 1; i < lm.MapDepth; i++ {
+ if i == 1 {
+ x.g.P(helper.Indent(2), "private Dictionary<", x.keys[0].Type, ", ", mapType, "> ", x.indexContainerName(index, i), " = [];")
+ } else {
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(2), "private Dictionary<", levelIndexKeyType, ", ", mapType, "> ", x.indexContainerName(index, i), " = [];")
+ }
+ x.g.P()
+ }
+ }
+ }
+}
+
+func (x *Generator) genIndexLoader() {
+ if !x.needGenerateIndex() {
+ return
+ }
+ defer x.genIndexSorter()
+ x.g.P(helper.Indent(3), "// Index init.")
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.Indexes {
+ x.g.P(helper.Indent(3), x.indexContainerName(index, 0), ".Clear();")
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(3), x.indexContainerName(index, i), ".Clear();")
+ }
+ }
+ }
+ parentDataName := "_data"
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ itemName := fmt.Sprintf("item%d", lm.Depth)
+ if !lm.NeedGenIndex() {
+ break
+ }
+ x.g.P(helper.Indent(lm.Depth+2), "foreach (var ", itemName, " in ", parentDataName, x.fieldGetter(lm.FD), ")")
+ x.g.P(helper.Indent(lm.Depth+2), "{")
+ parentDataName = itemName
+ if lm.FD.IsMap() {
+ if lm.NeedMapKeyForIndex() {
+ x.g.P(helper.Indent(lm.Depth+3), "var k", lm.MapDepth, " = ", itemName, ".Key;")
+ }
+ parentDataName = itemName + ".Value"
+ }
+ defer x.g.P(helper.Indent(lm.Depth+2), "}")
+ for _, index := range lm.Indexes {
+ x.genOneCsharpIndexLoader(lm, index, parentDataName)
+ }
+ }
+}
+
+func (x *Generator) genOneCsharpIndexLoader(lm *index.LevelMessage, index *index.LevelIndex, parentDataName string) {
+ ident := lm.Depth + 1
+ x.g.P(helper.Indent(ident+2), "{")
+ x.g.P(helper.Indent(ident+3), "// Index: ", index.Index)
+ if len(index.ColFields) == 1 {
+ // single-column index
+ field := index.ColFields[0] // just take the first field
+ fieldName, _ := x.parseKeyFieldNameAndSuffix(field)
+ if field.FD.IsList() {
+ itemName := fmt.Sprintf("item%d", lm.MapDepth+1)
+ x.g.P(helper.Indent(ident+3), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")")
+ x.g.P(helper.Indent(ident+3), "{")
+ key := itemName
+ x.genLoader(lm, index, ident+4, key, parentDataName)
+ x.g.P(helper.Indent(ident+3), "}")
+ } else {
+ key := parentDataName + fieldName
+ x.g.P(helper.Indent(ident+3), "var key = ", key, ";")
+ x.genLoader(lm, index, ident+3, "key", parentDataName)
+ }
+ } else {
+ // multi-column index
+ x.generateOneMulticolumnIndex(lm, index, ident+2, parentDataName, nil)
+ }
+ x.g.P(helper.Indent(ident+2), "}")
+}
+
+func (x *Generator) generateOneMulticolumnIndex(lm *index.LevelMessage, index *index.LevelIndex, ident int, parentDataName string, keys helper.MapKeySlice) {
+ cursor := len(keys)
+ if cursor >= len(index.ColFields) {
+ keyType := x.indexMapKeyType(index)
+ x.g.P(helper.Indent(ident+1), "var key = new ", keyType, "(", keys.GenGetArguments(), ");")
+ x.genLoader(lm, index, ident+1, "key", parentDataName)
+ return
+ }
+ field := index.ColFields[cursor]
+ fieldName, _ := x.parseKeyFieldNameAndSuffix(field)
+ if field.FD.IsList() {
+ itemName := fmt.Sprintf("indexItem%d", cursor)
+ x.g.P(helper.Indent(ident+1), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")")
+ x.g.P(helper.Indent(ident+1), "{")
+ key := itemName
+ keys = keys.AddMapKey(helper.MapKey{Name: key})
+ x.generateOneMulticolumnIndex(lm, index, ident+1, parentDataName, keys)
+ x.g.P(helper.Indent(ident+1), "}")
+ } else {
+ key := parentDataName + fieldName
+ keys = keys.AddMapKey(helper.MapKey{Name: key})
+ x.generateOneMulticolumnIndex(lm, index, ident, parentDataName, keys)
+ }
+}
+
+func (x *Generator) genLoader(lm *index.LevelMessage, index *index.LevelIndex, ident int, key, parentDataName string) {
+ x.g.P(helper.Indent(ident), "{")
+ x.g.P(helper.Indent(ident+1), "var list = ", x.indexContainerName(index, 0), ".TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : ", x.indexContainerName(index, 0), "[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ x.g.P(helper.Indent(ident), "}")
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(ident), "{")
+ if i == 1 {
+ x.g.P(helper.Indent(ident+1), "var map = ", x.indexContainerName(index, i), ".TryGetValue(k1, out var existingMap) ?")
+ x.g.P(helper.Indent(ident+1), "existingMap : ", x.indexContainerName(index, i), "[k1] = [];")
+ x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ } else {
+ var fields []string
+ for j := 1; j <= i; j++ {
+ fields = append(fields, fmt.Sprintf("k%d", j))
+ }
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(ident+1), "var mapKey = new ", levelIndexKeyType, "(", strings.Join(fields, ", "), ");")
+ x.g.P(helper.Indent(ident+1), "var map = ", x.indexContainerName(index, i), ".TryGetValue(mapKey, out var existingMap) ?")
+ x.g.P(helper.Indent(ident+1), "existingMap : ", x.indexContainerName(index, i), "[mapKey] = [];")
+ x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ }
+ x.g.P(helper.Indent(ident), "}")
+ }
+}
+
+func (x *Generator) genIndexSorter() {
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.Indexes {
+ if len(index.SortedColFields) != 0 {
+ valueType := x.mapValueType(index)
+ x.g.P(helper.Indent(3), "// Index(sort): ", index.Index)
+ indexContainerName := x.indexContainerName(index, 0)
+ sorter := strings.TrimPrefix(indexContainerName, "_") + "Comparison"
+ x.g.P(helper.Indent(3), "Comparison<", valueType, "> ", sorter, " = (a, b) =>")
+ var keys helper.MapKeySlice
+ for _, field := range index.SortedColFields {
+ fieldName, _ := x.parseKeyFieldNameAndSuffix(field)
+ keys = keys.AddMapKey(helper.MapKey{Name: fieldName})
+ }
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return "a" + key.Name }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "b" + key.Name }, ", "), "));")
+ x.g.P(helper.Indent(3), "foreach (var itemList in ", indexContainerName, ".Values)")
+ x.g.P(helper.Indent(3), "{")
+ x.g.P(helper.Indent(4), "itemList.Sort(", sorter, ");")
+ x.g.P(helper.Indent(3), "}")
+ // Iterate all leveled containers.
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(3), "foreach (var itemDict in ", x.indexContainerName(index, i), ".Values)")
+ x.g.P(helper.Indent(3), "{")
+ x.g.P(helper.Indent(4), "foreach (var itemList in itemDict.Values)")
+ x.g.P(helper.Indent(4), "{")
+ x.g.P(helper.Indent(5), "itemList.Sort(", sorter, ");")
+ x.g.P(helper.Indent(4), "}")
+ x.g.P(helper.Indent(3), "}")
+ }
+ }
+ }
+ }
+}
+
+func (x *Generator) genIndexFinders() {
+ if !x.needGenerateIndex() {
+ return
+ }
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.Indexes {
+ mapType := x.indexMapType(index)
+ mapValueType := x.mapValueType(index)
+ indexContainerName := x.indexContainerName(index, 0)
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "// Index: ", index.Index)
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map finds the index: key(", index.Index, ") to value(", mapValueType, ") map.")
+ x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ref readonly ", mapType, " Find", index.Name(), "Map() => ref ", indexContainerName, ";")
+ x.g.P()
+
+ keyType := x.indexMapKeyType(index)
+ keys := x.indexKeys(index)
+ params := keys.GenGetParams()
+ args := keys.GenGetArguments()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), " finds a list of all values of the given key(s).")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), "(", params, ") =>")
+ if len(index.ColFields) == 1 {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", args, ", out var value) ? value : null;")
+ } else {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", keyType, "(", args, "), out var value) ? value : null;")
+ }
+ x.g.P()
+
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), " finds the first value of the given key(s),")
+ x.g.P(helper.Indent(2), "/// or null if no value found.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), "(", params, ") =>")
+ x.g.P(helper.Indent(3), "Find", index.Name(), "(", args, ")?.FirstOrDefault();")
+
+ for i := 1; i < lm.MapDepth; i++ {
+ indexContainerName := x.indexContainerName(index, i)
+ partKeys := x.keys[:i]
+ partParams := partKeys.GenGetParams()
+ partArgs := partKeys.GenGetArguments()
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map", i, " finds the index: key(", index.Index, ") to value(", mapValueType, "),")
+ x.g.P(helper.Indent(2), "/// which is the upper ", loadutil.Ordinal(i), "-level map specified by (", partArgs, ").")
+ x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapType, "? Find", index.Name(), "Map", i, "(", partParams, ") =>")
+ if len(partKeys) == 1 {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", partArgs, ", out var value) ? value : null;")
+ } else {
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", levelIndexKeyType, "(", partArgs, "), out var value) ? value : null;")
+ }
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), i, " finds a list of all values of the given key(s) in the upper ", loadutil.Ordinal(i), "-level map")
+ x.g.P(helper.Indent(2), "/// specified by (", partArgs, ").")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), i, "(", partParams, ", ", params, ") =>")
+ if len(index.ColFields) == 1 {
+ x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(", args, ", out var value) == true ? value : null;")
+ } else {
+ x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(new ", keyType, "(", args, "), out var value) == true ? value : null;")
+ }
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), i, " finds the first value of the given key(s) in the upper ", loadutil.Ordinal(i), "-level map")
+ x.g.P(helper.Indent(2), "/// specified by (", partArgs, "), or null if no value found.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), i, "(", partParams, ", ", params, ") =>")
+ x.g.P(helper.Indent(3), "Find", index.Name(), i, "(", partArgs, ", ", args, ")?.FirstOrDefault();")
+ }
+ }
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go
new file mode 100644
index 00000000..6a13f581
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go
@@ -0,0 +1,340 @@
+package indexes
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "github.com/iancoleman/strcase"
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/internal/index"
+ "github.com/tableauio/loader/internal/loadutil"
+ "github.com/tableauio/loader/internal/options"
+)
+
+func (x *Generator) needGenerateOrderedIndex() bool {
+ return options.NeedGenOrderedIndex(x.message.Desc, options.LangCS)
+}
+
+func (x *Generator) orderedIndexMapType(index *index.LevelIndex) string {
+ return fmt.Sprintf("OrderedIndex_%sMap", index.Name())
+}
+
+func (x *Generator) orderedIndexMapKeyType(index *index.LevelIndex) string {
+ if len(index.ColFields) == 1 {
+ // single-column index
+ field := index.ColFields[0] // just take first field
+ return helper.ParseOrderedIndexKeyType(field.FD)
+ } else {
+ // multi-column index
+ return fmt.Sprintf("OrderedIndex_%sKey", index.Name())
+ }
+}
+
+func (x *Generator) orderedIndexContainerName(index *index.LevelIndex, i int) string {
+ if i == 0 {
+ return fmt.Sprintf("_orderedIndex%sMap", strcase.ToCamel(index.Name()))
+ }
+ return fmt.Sprintf("_orderedIndex%sMap%d", strcase.ToCamel(index.Name()), i)
+}
+
+func (x *Generator) orderedIndexKeys(index *index.LevelIndex) helper.MapKeySlice {
+ var keys helper.MapKeySlice
+ for _, field := range index.ColFields {
+ keys = keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseOrderedIndexKeyType(field.FD),
+ Name: helper.ParseIndexFieldNameAsFuncParam(field.FD),
+ FieldName: helper.ParseIndexFieldNameAsKeyStructFieldName(field.FD),
+ })
+ }
+ return keys
+}
+
+func (x *Generator) genOrderedIndexTypeDef() {
+ if !x.needGenerateOrderedIndex() {
+ return
+ }
+ var once sync.Once
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.OrderedIndexes {
+ once.Do(func() { x.g.P(helper.Indent(2), "// OrderedIndex types.") })
+ x.g.P(helper.Indent(2), "// OrderedIndex: ", index.Index)
+ mapType := x.orderedIndexMapType(index)
+ keyType := x.orderedIndexMapKeyType(index)
+ valueType := x.mapValueType(index)
+ keys := x.orderedIndexKeys(index)
+ if len(index.ColFields) != 1 {
+ // Generate key struct
+ x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IComparable<", keyType, ">")
+ x.g.P(helper.Indent(2), "{")
+ for _, key := range keys {
+ x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }")
+ }
+ x.g.P()
+ x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")")
+ x.g.P(helper.Indent(3), "{")
+ for _, key := range keys {
+ x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";")
+ }
+ x.g.P(helper.Indent(3), "}")
+ x.g.P()
+ x.g.P(helper.Indent(3), "public int CompareTo(", keyType, " other) =>")
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));")
+ x.g.P(helper.Indent(2), "}")
+ x.g.P()
+ }
+ x.g.P(helper.Indent(2), "public class ", mapType, " : SortedDictionary<", keyType, ", List<", valueType, ">>;")
+ x.g.P()
+
+ x.g.P(helper.Indent(2), "private ", mapType, " ", x.orderedIndexContainerName(index, 0), " = [];")
+ x.g.P()
+ for i := 1; i < lm.MapDepth; i++ {
+ if i == 1 {
+ x.g.P(helper.Indent(2), "private Dictionary<", x.keys[0].Type, ", ", mapType, "> ", x.orderedIndexContainerName(index, i), " = [];")
+ } else {
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(2), "private Dictionary<", levelIndexKeyType, ", ", mapType, "> ", x.orderedIndexContainerName(index, i), " = [];")
+ }
+ x.g.P()
+ }
+ }
+ }
+}
+
+func (x *Generator) genOrderedIndexLoader() {
+ if !x.needGenerateOrderedIndex() {
+ return
+ }
+ defer x.genOrderedIndexSorter()
+ x.g.P(helper.Indent(3), "// OrderedIndex init.")
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.OrderedIndexes {
+ x.g.P(helper.Indent(3), x.orderedIndexContainerName(index, 0), ".Clear();")
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(3), x.orderedIndexContainerName(index, i), ".Clear();")
+ }
+ }
+ }
+ parentDataName := "_data"
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ itemName := fmt.Sprintf("item%d", lm.Depth)
+ if !lm.NeedGenOrderedIndex() {
+ break
+ }
+ x.g.P(helper.Indent(lm.Depth+2), "foreach (var ", itemName, " in ", parentDataName, x.fieldGetter(lm.FD), ")")
+ x.g.P(helper.Indent(lm.Depth+2), "{")
+ parentDataName = itemName
+ if lm.FD.IsMap() {
+ if lm.NeedMapKeyForOrderedIndex() {
+ x.g.P(helper.Indent(lm.Depth+3), "var k", lm.MapDepth, " = ", itemName, ".Key;")
+ }
+ parentDataName = itemName + ".Value"
+ }
+ defer x.g.P(helper.Indent(lm.Depth+2), "}")
+ for _, index := range lm.OrderedIndexes {
+ x.genOneCsharpOrderedIndexLoader(lm, index, parentDataName)
+ }
+ }
+}
+
+func (x *Generator) genOneCsharpOrderedIndexLoader(lm *index.LevelMessage, index *index.LevelIndex, parentDataName string) {
+ ident := lm.Depth + 1
+ x.g.P(helper.Indent(ident+2), "{")
+ x.g.P(helper.Indent(ident+3), "// OrderedIndex: ", index.Index)
+ if len(index.ColFields) == 1 {
+ // single-column index
+ field := index.ColFields[0] // just take the first field
+ fieldName, suffix := x.parseKeyFieldNameAndSuffix(field)
+ if field.FD.IsList() {
+ itemName := fmt.Sprintf("item%d", lm.MapDepth+1)
+ x.g.P(helper.Indent(ident+3), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")")
+ x.g.P(helper.Indent(ident+3), "{")
+ x.g.P(helper.Indent(ident+4), "var key = ", itemName, suffix, ";")
+ x.genOrderedIndexLoaderCommon(lm, index, ident+4, "key", parentDataName)
+ x.g.P(helper.Indent(ident+3), "}")
+ } else {
+ key := parentDataName + fieldName + suffix
+ x.g.P(helper.Indent(ident+3), "var key = ", key, ";")
+ x.genOrderedIndexLoaderCommon(lm, index, ident+3, "key", parentDataName)
+ }
+ } else {
+ // multi-column index
+ x.generateOneMulticolumnOrderedIndex(lm, index, ident+2, parentDataName, nil)
+ }
+ x.g.P(helper.Indent(ident+2), "}")
+}
+
+func (x *Generator) generateOneMulticolumnOrderedIndex(lm *index.LevelMessage, index *index.LevelIndex, ident int, parentDataName string, keys helper.MapKeySlice) {
+ cursor := len(keys)
+ if cursor >= len(index.ColFields) {
+ keyType := x.orderedIndexMapKeyType(index)
+ x.g.P(helper.Indent(ident+1), "var key = new ", keyType, "(", keys.GenGetArguments(), ");")
+ x.genOrderedIndexLoaderCommon(lm, index, ident+1, "key", parentDataName)
+ return
+ }
+ field := index.ColFields[cursor]
+ fieldName, suffix := x.parseKeyFieldNameAndSuffix(field)
+ if field.FD.IsList() {
+ itemName := fmt.Sprintf("indexItem%d", cursor)
+ x.g.P(helper.Indent(ident+1), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")")
+ x.g.P(helper.Indent(ident+1), "{")
+ key := itemName
+ keys = keys.AddMapKey(helper.MapKey{Name: key})
+ x.generateOneMulticolumnOrderedIndex(lm, index, ident+1, parentDataName, keys)
+ x.g.P(helper.Indent(ident+1), "}")
+ } else {
+ key := parentDataName + fieldName + suffix
+ keys = keys.AddMapKey(helper.MapKey{Name: key})
+ x.generateOneMulticolumnOrderedIndex(lm, index, ident, parentDataName, keys)
+ }
+}
+
+func (x *Generator) genOrderedIndexLoaderCommon(lm *index.LevelMessage, index *index.LevelIndex, ident int, key, parentDataName string) {
+ x.g.P(helper.Indent(ident), "{")
+ x.g.P(helper.Indent(ident+1), "var list = ", x.orderedIndexContainerName(index, 0), ".TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : ", x.orderedIndexContainerName(index, 0), "[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ x.g.P(helper.Indent(ident), "}")
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(ident), "{")
+ if i == 1 {
+ x.g.P(helper.Indent(ident+1), "var map = ", x.orderedIndexContainerName(index, i), ".TryGetValue(k1, out var existingMap) ?")
+ x.g.P(helper.Indent(ident+1), "existingMap : ", x.orderedIndexContainerName(index, i), "[k1] = [];")
+ x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ } else {
+ var fields []string
+ for j := 1; j <= i; j++ {
+ fields = append(fields, fmt.Sprintf("k%d", j))
+ }
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(ident+1), "var mapKey = new ", levelIndexKeyType, "(", strings.Join(fields, ", "), ");")
+ x.g.P(helper.Indent(ident+1), "var map = ", x.orderedIndexContainerName(index, i), ".TryGetValue(mapKey, out var existingMap) ?")
+ x.g.P(helper.Indent(ident+1), "existingMap : ", x.orderedIndexContainerName(index, i), "[mapKey] = [];")
+ x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?")
+ x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = [];")
+ x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");")
+ }
+ x.g.P(helper.Indent(ident), "}")
+ }
+}
+
+func (x *Generator) genOrderedIndexSorter() {
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.OrderedIndexes {
+ if len(index.SortedColFields) != 0 {
+ valueType := x.mapValueType(index)
+ x.g.P(helper.Indent(3), "// OrderedIndex(sort): ", index.Index)
+ indexContainerName := x.orderedIndexContainerName(index, 0)
+ sorter := strings.TrimPrefix(indexContainerName, "_") + "Comparison"
+ x.g.P(helper.Indent(3), "Comparison<", valueType, "> ", sorter, " = (a, b) =>")
+ var keys helper.MapKeySlice
+ for _, field := range index.SortedColFields {
+ fieldName, _ := x.parseKeyFieldNameAndSuffix(field)
+ keys = keys.AddMapKey(helper.MapKey{Name: fieldName})
+ }
+ x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return "a" + key.Name }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "b" + key.Name }, ", "), "));")
+ x.g.P(helper.Indent(3), "foreach (var itemList in ", indexContainerName, ".Values)")
+ x.g.P(helper.Indent(3), "{")
+ x.g.P(helper.Indent(4), "itemList.Sort(", sorter, ");")
+ x.g.P(helper.Indent(3), "}")
+ // Iterate all leveled containers.
+ for i := 1; i < lm.MapDepth; i++ {
+ x.g.P(helper.Indent(3), "foreach (var itemDict in ", x.orderedIndexContainerName(index, i), ".Values)")
+ x.g.P(helper.Indent(3), "{")
+ x.g.P(helper.Indent(4), "foreach (var itemList in itemDict.Values)")
+ x.g.P(helper.Indent(4), "{")
+ x.g.P(helper.Indent(5), "itemList.Sort(", sorter, ");")
+ x.g.P(helper.Indent(4), "}")
+ x.g.P(helper.Indent(3), "}")
+ }
+ }
+ }
+ }
+}
+
+func (x *Generator) genOrderedIndexFinders() {
+ if !x.needGenerateOrderedIndex() {
+ return
+ }
+ for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel {
+ for _, index := range lm.OrderedIndexes {
+ mapType := x.orderedIndexMapType(index)
+ mapValueType := x.mapValueType(index)
+ indexContainerName := x.orderedIndexContainerName(index, 0)
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "// OrderedIndex: ", index.Index)
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map finds the ordered index: key(", index.Index, ") to value(", mapValueType, ") sorted map.")
+ x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ref readonly ", mapType, " Find", index.Name(), "Map() => ref ", indexContainerName, ";")
+ x.g.P()
+
+ keyType := x.orderedIndexMapKeyType(index)
+ keys := x.orderedIndexKeys(index)
+ params := keys.GenGetParams()
+ args := keys.GenGetArguments()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), " finds a list of all values of the given key(s).")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), "(", params, ") =>")
+ if len(index.ColFields) == 1 {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", args, ", out var value) ? value : null;")
+ } else {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", keyType, "(", args, "), out var value) ? value : null;")
+ }
+ x.g.P()
+
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), " finds the first value of the given key(s),")
+ x.g.P(helper.Indent(2), "/// or null if no value found.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), "(", params, ") =>")
+ x.g.P(helper.Indent(3), "Find", index.Name(), "(", args, ")?.FirstOrDefault();")
+
+ for i := 1; i < lm.MapDepth; i++ {
+ indexContainerName := x.orderedIndexContainerName(index, i)
+ partKeys := x.keys[:i]
+ partParams := partKeys.GenGetParams()
+ partArgs := partKeys.GenGetArguments()
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map", i, " finds the ordered index: key(", index.Index, ") to value(", mapValueType, "),")
+ x.g.P(helper.Indent(2), "/// which is the upper ", loadutil.Ordinal(i), "-level sorted map specified by (", partArgs, ").")
+ x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapType, "? Find", index.Name(), "Map", i, "(", partParams, ") =>")
+ if len(partKeys) == 1 {
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", partArgs, ", out var value) ? value : null;")
+ } else {
+ levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd)
+ x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", levelIndexKeyType, "(", partArgs, "), out var value) ? value : null;")
+ }
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// Find", index.Name(), i, " finds a list of all values of the given key(s) in the upper ", loadutil.Ordinal(i), "-level sorted map")
+ x.g.P(helper.Indent(2), "/// specified by (", partArgs, ").")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), i, "(", partParams, ", ", params, ") =>")
+ if len(index.ColFields) == 1 {
+ x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(", args, ", out var value) == true ? value : null;")
+ } else {
+ x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(new ", keyType, "(", args, "), out var value) == true ? value : null;")
+ }
+
+ x.g.P()
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), i, " finds the first value of the given key(s) in the upper ", loadutil.Ordinal(i), "-level sorted map")
+ x.g.P(helper.Indent(2), "/// specified by (", partArgs, "), or null if no value found.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), i, "(", partParams, ", ", params, ") =>")
+ x.g.P(helper.Indent(3), "Find", index.Name(), i, "(", partArgs, ", ", args, ")?.FirstOrDefault();")
+ }
+ }
+ }
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/main.go b/cmd/protoc-gen-csharp-tableau-loader/main.go
new file mode 100644
index 00000000..bad35e59
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/main.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "github.com/tableauio/loader/internal/options"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/types/pluginpb"
+)
+
+const version = "0.1.0"
+
+func main() {
+ showVersion := flag.Bool("version", false, "print the version and exit")
+ flag.Parse()
+ if *showVersion {
+ fmt.Printf("protoc-gen-csharp-tableau-loader %v\n", version)
+ return
+ }
+
+ var flags flag.FlagSet
+
+ protogen.Options{
+ ParamFunc: flags.Set,
+ }.Run(func(gen *protogen.Plugin) error {
+ gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
+ for _, f := range gen.Files {
+ if !options.NeedGenFile(f) {
+ continue
+ }
+ generateMessager(gen, f)
+ }
+ generateHub(gen)
+ generateEmbed(gen)
+ return nil
+ })
+}
diff --git a/cmd/protoc-gen-csharp-tableau-loader/messager.go b/cmd/protoc-gen-csharp-tableau-loader/messager.go
new file mode 100644
index 00000000..d51949ae
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/messager.go
@@ -0,0 +1,173 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/iancoleman/strcase"
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/indexes"
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/orderedmap"
+ "github.com/tableauio/loader/internal/extensions"
+ "github.com/tableauio/loader/internal/index"
+ "github.com/tableauio/loader/internal/loadutil"
+ "github.com/tableauio/tableau/proto/tableaupb"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
+ "google.golang.org/protobuf/types/descriptorpb"
+)
+
+// golbal container for record all proto filenames and messager names
+var messagers []string
+
+// generateMessager generates a protoconf file corresponding to the protobuf file.
+// Each wrapped struct type implement the Messager interface.
+func generateMessager(gen *protogen.Plugin, file *protogen.File) {
+
+ filename := filepath.Join(strcase.ToCamel(file.GeneratedFilenamePrefix) + "." + extensions.PC + ".cs")
+ g := gen.NewGeneratedFile(filename, "")
+ helper.GenerateFileHeader(gen, file, g, version)
+ generateFileContent(gen, file, g)
+}
+
+// generateFileContent generates struct type definitions.
+func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) {
+ g.P(staticMessagerContent1)
+ var fileMessagers []string
+ firstMessager := true
+ for _, message := range file.Messages {
+ opts := message.Desc.Options().(*descriptorpb.MessageOptions)
+ worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions)
+ if worksheet != nil {
+ if !firstMessager {
+ g.P()
+ }
+ firstMessager = false
+ genMessage(gen, g, message)
+
+ messagerName := string(message.Desc.Name())
+ fileMessagers = append(fileMessagers, messagerName)
+ }
+ }
+ messagers = append(messagers, fileMessagers...)
+ g.P(staticMessagerContent2)
+}
+
+// genMessage generates a message definition.
+func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, message *protogen.Message) {
+ messagerName := string(message.Desc.Name())
+ indexDescriptor := index.ParseIndexDescriptor(message.Desc)
+
+ orderedMapGenerator := orderedmap.NewGenerator(g, message)
+ indexGenerator := indexes.NewGenerator(g, indexDescriptor, message)
+
+ g.P(helper.Indent(1), "/// ")
+ g.P(helper.Indent(1), "/// ", messagerName, " is a wrapper around protobuf message ", helper.ParseCsharpClassType(message.Desc), ".")
+ g.P(helper.Indent(1), "/// ")
+ g.P(helper.Indent(1), "public class ", messagerName, " : Messager, IMessagerName")
+ g.P(helper.Indent(1), "{")
+ // type definitions
+ orderedMapGenerator.GenOrderedMapTypeDef()
+ indexGenerator.GenIndexTypeDef()
+ g.P(helper.Indent(2), "private ", helper.ParseCsharpClassType(message.Desc), " _data = new();")
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// Name returns the ", messagerName, "'s message name.")
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "public static string Name() => ", helper.ParseCsharpClassType(message.Desc), ".Descriptor.Name;")
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// Load loads ", messagerName, "'s content in the given dir, based on format and messager options.")
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)")
+ g.P(helper.Indent(2), "{")
+ g.P(helper.Indent(3), "var start = DateTime.Now;")
+ g.P(helper.Indent(3), "try")
+ g.P(helper.Indent(3), "{")
+ g.P(helper.Indent(4), "_data = (", helper.ParseCsharpClassType(message.Desc), ")(")
+ g.P(helper.Indent(5), "Tableau.Load.LoadMessagerInDir(", helper.ParseCsharpClassType(message.Desc), ".Descriptor, dir, fmt, options)")
+ g.P(helper.Indent(5), "?? throw new InvalidOperationException()")
+ g.P(helper.Indent(4), ");")
+ g.P(helper.Indent(3), "}")
+ g.P(helper.Indent(3), "catch (Exception ex)")
+ g.P(helper.Indent(3), "{")
+ g.P(helper.Indent(4), "if (string.IsNullOrEmpty(Util.GetErrMsg()))")
+ g.P(helper.Indent(4), "{")
+ g.P(helper.Indent(5), "Util.SetErrMsg($\"failed to load ", messagerName, ": {ex.Message}\");")
+ g.P(helper.Indent(4), "}")
+ g.P(helper.Indent(4), "return false;")
+ g.P(helper.Indent(3), "}")
+ g.P(helper.Indent(3), "LoadStats.Duration = DateTime.Now - start;")
+ g.P(helper.Indent(3), "return ProcessAfterLoad();")
+ g.P(helper.Indent(2), "}")
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// Data returns the ", messagerName, "'s inner message data.")
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "public ref readonly ", helper.ParseCsharpClassType(message.Desc), " Data() => ref _data;")
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// Message returns the ", messagerName, "'s inner message data.")
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "public override pb::IMessage? Message() => _data;")
+
+ if orderedMapGenerator.NeedGenerate() || indexGenerator.NeedGenerate() {
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// ProcessAfterLoad runs after this messager is loaded.")
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "protected override bool ProcessAfterLoad()")
+ g.P(helper.Indent(2), "{")
+ orderedMapGenerator.GenOrderedMapLoader()
+ indexGenerator.GenIndexLoader()
+ g.P(helper.Indent(3), "return true;")
+ g.P(helper.Indent(2), "}")
+ }
+
+ // syntactic sugar for accessing map items
+ genMapGetters(gen, g, message.Desc, 1, nil, messagerName)
+ orderedMapGenerator.GenOrderedMapGetters()
+ indexGenerator.GenIndexFinders()
+ g.P(helper.Indent(1), "}")
+}
+
+func genMapGetters(gen *protogen.Plugin, g *protogen.GeneratedFile, md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice, messagerName string) {
+ for i := 0; i < md.Fields().Len(); i++ {
+ fd := md.Fields().Get(i)
+ if fd.IsMap() {
+ keys = keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseMapKeyType(fd.MapKey()),
+ Name: helper.ParseMapFieldNameAsFuncParam(fd),
+ })
+ getter := fmt.Sprintf("Get%v", depth)
+ g.P()
+ g.P(helper.Indent(2), "/// ")
+ g.P(helper.Indent(2), "/// ", getter, " finds value in the ", loadutil.Ordinal(depth), "-level map.")
+ g.P(helper.Indent(2), "/// It will return null if the key is not found.")
+ g.P(helper.Indent(2), "/// ")
+
+ lastKeyName := keys[len(keys)-1].Name
+ if depth == 1 {
+ g.P(helper.Indent(2), "public ", helper.ParseMapValueType(fd), "? ", getter, "(", keys.GenGetParams(), ") =>")
+ g.P(helper.Indent(3), "_data.", helper.ParseCsharpPropertyName(fd), "?.TryGetValue(", lastKeyName, ", out var val) == true ? val : null;")
+ } else {
+ prevKeys := keys[:len(keys)-1]
+ prevGetter := fmt.Sprintf("Get%v", depth-1)
+ g.P(helper.Indent(2), "public ", helper.ParseMapValueType(fd), "? ", getter, "(", keys.GenGetParams(), ") =>")
+ g.P(helper.Indent(3), prevGetter, "(", prevKeys.GenGetArguments(), ")?.", helper.ParseCsharpPropertyName(fd), "?.TryGetValue(", lastKeyName, ", out var val) == true ? val : null;")
+ }
+
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ genMapGetters(gen, g, fd.MapValue().Message(), depth+1, keys, messagerName)
+ }
+ break
+ }
+ }
+}
+
+const staticMessagerContent1 = `using pb = global::Google.Protobuf;
+namespace Tableau
+{`
+
+const staticMessagerContent2 = `}`
diff --git a/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go b/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go
new file mode 100644
index 00000000..6bcb2d2b
--- /dev/null
+++ b/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go
@@ -0,0 +1,205 @@
+package orderedmap
+
+import (
+ "fmt"
+
+ "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper"
+ "github.com/tableauio/loader/internal/loadutil"
+ "github.com/tableauio/loader/internal/options"
+ "google.golang.org/protobuf/compiler/protogen"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+type Generator struct {
+ g *protogen.GeneratedFile
+ message *protogen.Message
+}
+
+func NewGenerator(g *protogen.GeneratedFile, message *protogen.Message) *Generator {
+ return &Generator{
+ g: g,
+ message: message,
+ }
+}
+
+func (x *Generator) NeedGenerate() bool {
+ return options.NeedGenOrderedMap(x.message.Desc, options.LangCS)
+}
+
+func (x *Generator) mapType(mapFd protoreflect.FieldDescriptor) string {
+ return fmt.Sprintf("OrderedMap_%sMap", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd))
+}
+
+func (x *Generator) mapValueType(mapFd protoreflect.FieldDescriptor) string {
+ return fmt.Sprintf("OrderedMap_%sValue", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd))
+}
+
+func (x *Generator) mapValueFieldType(fd protoreflect.FieldDescriptor) string {
+ nextMapFD := getNextLevelMapFD(fd.MapValue())
+ if nextMapFD != nil {
+ return x.mapValueType(fd)
+ }
+ return helper.ParseMapValueType(fd)
+}
+
+func (x *Generator) GenOrderedMapTypeDef() {
+ if !x.NeedGenerate() {
+ return
+ }
+ x.genOrderedMapTypeDef(x.message.Desc, 1, nil)
+}
+
+func (x *Generator) genOrderedMapTypeDef(md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice) {
+ for i := 0; i < md.Fields().Len(); i++ {
+ fd := md.Fields().Get(i)
+ if fd.IsMap() {
+ if depth == 1 {
+ x.g.P(helper.Indent(2), "// OrderedMap types.")
+ }
+ nextKeys := keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseMapKeyType(fd.MapKey()),
+ Name: helper.ParseMapFieldNameAsKeyStructFieldName(fd),
+ })
+ keyType := nextKeys[len(nextKeys)-1].Type
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ x.genOrderedMapTypeDef(fd.MapValue().Message(), depth+1, nextKeys)
+ }
+ orderedMap := x.mapType(fd)
+ orderedMapValue := x.mapValueType(fd)
+ nextMapFD := getNextLevelMapFD(fd.MapValue())
+ if nextMapFD != nil {
+ currValueType := helper.ParseCsharpType(fd.MapValue())
+ nextOrderedMap := x.mapType(nextMapFD)
+ x.g.P(helper.Indent(2), "public class ", orderedMapValue, "(", nextOrderedMap, " item1, ", currValueType, " item2)")
+ x.g.P(helper.Indent(3), ": Tuple<", nextOrderedMap, ", ", currValueType, ">(item1, item2);")
+ }
+ x.g.P(helper.Indent(2), "public class ", orderedMap, " : SortedDictionary<", keyType, ", ", x.mapValueFieldType(fd), ">;")
+ x.g.P()
+ if depth == 1 {
+ x.g.P(helper.Indent(2), "private ", orderedMap, " _orderedMap = [];")
+ x.g.P()
+ }
+ break
+ }
+ }
+}
+
+func (x *Generator) GenOrderedMapLoader() {
+ if !x.NeedGenerate() {
+ return
+ }
+ x.genOrderedMapLoader(x.message.Desc, 1)
+}
+
+func (x *Generator) genOrderedMapLoader(md protoreflect.MessageDescriptor, depth int) {
+ for i := 0; i < md.Fields().Len(); i++ {
+ fd := md.Fields().Get(i)
+ if fd.IsMap() {
+ if depth == 1 {
+ x.g.P(helper.Indent(3), "// OrderedMap init.")
+ x.g.P(helper.Indent(3), "_orderedMap.Clear();")
+ }
+ orderedMapValue := x.mapValueType(fd)
+ keyName := fmt.Sprintf("key%d", depth)
+ valueName := fmt.Sprintf("value%d", depth)
+
+ tmpOrderedMapName := fmt.Sprintf("ordered_map%d", depth)
+
+ prevContainer := fmt.Sprintf("value%d", depth-1)
+ prevTmpOrderedMapName := fmt.Sprintf("ordered_map%d", depth-1)
+ if depth == 1 {
+ prevContainer = "_data"
+ prevTmpOrderedMapName = "_orderedMap"
+ }
+ x.g.P(helper.Indent(depth+2), "foreach (var (", keyName, ", ", valueName, ") in ", prevContainer, ".", helper.ParseCsharpPropertyName(fd), ")")
+ x.g.P(helper.Indent(depth+2), "{")
+ nextMapFD := getNextLevelMapFD(fd.MapValue())
+ if nextMapFD != nil {
+ nextOrderedMap := x.mapType(nextMapFD)
+ x.g.P(helper.Indent(depth+3), "var ", tmpOrderedMapName, " = new ", nextOrderedMap, "();")
+ }
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ x.genOrderedMapLoader(fd.MapValue().Message(), depth+1)
+ }
+
+ if nextMapFD != nil {
+ x.g.P(helper.Indent(depth+3), prevTmpOrderedMapName, "[", keyName, "] = new ", orderedMapValue, "(", tmpOrderedMapName, ", ", valueName, ");")
+ } else {
+ x.g.P(helper.Indent(depth+3), prevTmpOrderedMapName, "[", keyName, "] = ", valueName, ";")
+ }
+ x.g.P(helper.Indent(depth+2), "}")
+ break
+ }
+ }
+}
+
+func (x *Generator) GenOrderedMapGetters() {
+ if !x.NeedGenerate() {
+ return
+ }
+ x.genOrderedMapGetters(x.message.Desc, 1, nil)
+}
+
+func (x *Generator) genOrderedMapGetters(md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice) {
+ genGetterName := func(depth int) string {
+ getter := "GetOrderedMap"
+ if depth > 1 {
+ getter = fmt.Sprintf("GetOrderedMap%v", depth-1)
+ }
+ return getter
+ }
+ for i := 0; i < md.Fields().Len(); i++ {
+ fd := md.Fields().Get(i)
+ if fd.IsMap() {
+ x.g.P()
+ if depth == 1 {
+ x.g.P(helper.Indent(2), "// OrderedMap accessers.")
+ }
+ getter := genGetterName(depth)
+ orderedMap := x.mapType(fd)
+ if depth == 1 {
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// ", getter, " returns the ", loadutil.Ordinal(depth), "-level ordered map.")
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "public ref readonly ", orderedMap, " ", getter, "() => ref _orderedMap;")
+ } else {
+ x.g.P(helper.Indent(2), "/// ")
+ x.g.P(helper.Indent(2), "/// ", getter, " finds value in the ", loadutil.Ordinal(depth-1), "-level ordered map.")
+ x.g.P(helper.Indent(2), "/// It will return null if the key is not found.")
+ x.g.P(helper.Indent(2), "/// ")
+ lastKeyName := keys[len(keys)-1].Name
+ if depth == 2 {
+ x.g.P(helper.Indent(2), "public ", orderedMap, "? ", getter, "(", keys.GenGetParams(), ") =>")
+ x.g.P(helper.Indent(3), "_orderedMap.TryGetValue(", lastKeyName, ", out var value) ? value.Item1 : null;")
+ } else {
+ prevKeys := keys[:len(keys)-1]
+ prevGetter := genGetterName(depth - 1)
+ x.g.P(helper.Indent(2), "public ", orderedMap, "? ", getter, "(", keys.GenGetParams(), ") =>")
+ x.g.P(helper.Indent(3), prevGetter, "(", prevKeys.GenGetArguments(), ")?.TryGetValue(", lastKeyName, ", out var value) == true ? value.Item1 : null;")
+ }
+ }
+
+ nextKeys := keys.AddMapKey(helper.MapKey{
+ Type: helper.ParseMapKeyType(fd.MapKey()),
+ Name: helper.ParseMapFieldNameAsFuncParam(fd),
+ })
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ x.genOrderedMapGetters(fd.MapValue().Message(), depth+1, nextKeys)
+ }
+ break
+ }
+ }
+}
+
+func getNextLevelMapFD(fd protoreflect.FieldDescriptor) protoreflect.FieldDescriptor {
+ if fd.Kind() == protoreflect.MessageKind {
+ md := fd.Message()
+ for i := 0; i < md.Fields().Len(); i++ {
+ fd := md.Fields().Get(i)
+ if fd.IsMap() {
+ return fd
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/options/options.go b/internal/options/options.go
index c56d5b12..df5f801a 100644
--- a/internal/options/options.go
+++ b/internal/options/options.go
@@ -22,6 +22,7 @@ type Language = string
const (
LangCPP Language = "cpp"
LangGO Language = "go"
+ LangCS Language = "cs"
)
func NeedGenOrderedMap(md protoreflect.MessageDescriptor, lang Language) bool {
diff --git a/test/csharp-tableau-loader/Loader.csproj b/test/csharp-tableau-loader/Loader.csproj
new file mode 100644
index 00000000..500449ad
--- /dev/null
+++ b/test/csharp-tableau-loader/Loader.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ CS8981
+
+
+
+
+
+
+
diff --git a/test/csharp-tableau-loader/Program.cs b/test/csharp-tableau-loader/Program.cs
new file mode 100644
index 00000000..e539ea70
--- /dev/null
+++ b/test/csharp-tableau-loader/Program.cs
@@ -0,0 +1,107 @@
+class Program
+{
+ static void Main(string[] _)
+ {
+ Tableau.Registry.Init();
+
+ var options = new Tableau.HubOptions
+ {
+ Filter = name => name != "TaskConf"
+ };
+ var hub = new Tableau.Hub(options);
+ var loadOptions = new Tableau.Load.Options
+ {
+ IgnoreUnknownFields = true
+ };
+ if (!hub.Load("../testdata/conf", Tableau.Format.JSON, loadOptions))
+ {
+ Console.WriteLine("Failed to load configurations");
+ return;
+ }
+
+ var taskConf = hub.Get();
+ if (taskConf is null)
+ {
+ Console.WriteLine("TaskConf is null");
+ }
+ else
+ {
+ Console.WriteLine($"TaskConf: {taskConf.Data()}");
+ Console.WriteLine($"TaskConf Load duration: {taskConf.GetStats().Duration.TotalMilliseconds} ms");
+ }
+
+ var heroConf = hub.Get();
+ if (heroConf is null)
+ {
+ Console.WriteLine("HeroConf is null");
+ }
+ else
+ {
+ Console.WriteLine($"HeroConf: {heroConf.Data()}");
+ Console.WriteLine($"HeroConf Load duration: {heroConf.GetStats().Duration.TotalMilliseconds} ms");
+ // Traverse top-level OrderedMap (HeroOrderedMap)
+ var heroOrderedMap = heroConf.GetOrderedMap();
+ if (heroOrderedMap != null)
+ {
+ Console.WriteLine("Hero OrderedMap:");
+ foreach (var heroPair in heroOrderedMap)
+ {
+ Console.WriteLine($"Hero: {heroPair.Key}");
+ Console.WriteLine($" - Hero Data: {heroPair.Value.Item2}");
+ // Traverse nested Attr OrderedMap
+ var attrOrderedMap = heroPair.Value.Item1;
+ if (attrOrderedMap != null && attrOrderedMap.Count > 0)
+ {
+ Console.WriteLine(" Attributes:");
+ foreach (var attrPair in attrOrderedMap)
+ {
+ Console.WriteLine($" - {attrPair.Key}: {attrPair.Value}");
+ }
+ }
+ }
+ }
+ }
+
+ var itemConf = hub.Get();
+ if (itemConf is null)
+ {
+ Console.WriteLine("ItemConf is null");
+ }
+ else
+ {
+ Console.WriteLine($"ItemConf: {itemConf.Data()}");
+ Console.WriteLine($"ItemConf Load duration: {itemConf.GetStats().Duration.TotalMilliseconds} ms");
+ var itemConf2 = hub.GetItemConf();
+ Console.WriteLine($"hub.Get() returns same instance with hub.GetItemConf(): {ReferenceEquals(itemConf, itemConf2)}");
+ var itemInfoMap = itemConf.FindItemInfoMap();
+ if (itemInfoMap != null)
+ {
+ Console.WriteLine("ItemInfoMap Contents:");
+ foreach (var itemPair in itemInfoMap)
+ {
+ Console.WriteLine($" - {itemPair.Key}: ");
+ foreach (var element in itemPair.Value)
+ {
+ Console.WriteLine($" - {element}");
+ }
+ }
+ }
+ }
+
+ LoadBin();
+ }
+
+ static void LoadBin()
+ {
+ Console.WriteLine("LoadBin");
+ var heroConf = new Tableau.HeroConf();
+ if (heroConf.Load("../testdata/bin", Tableau.Format.Bin))
+ {
+ Console.WriteLine($"HeroConf: {heroConf.Data()}");
+ }
+ if (!heroConf.Load("../testdata/notexist", Tableau.Format.Bin))
+ {
+ Console.WriteLine("HeroConf not exist");
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/csharp-tableau-loader/csharp-tableau-loader.sln b/test/csharp-tableau-loader/csharp-tableau-loader.sln
new file mode 100644
index 00000000..d31764f3
--- /dev/null
+++ b/test/csharp-tableau-loader/csharp-tableau-loader.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Loader", "Loader.csproj", "{E8053C8C-98ED-7494-D874-8E12A72FA028}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E8053C8C-98ED-7494-D874-8E12A72FA028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8053C8C-98ED-7494-D874-8E12A72FA028}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8053C8C-98ED-7494-D874-8E12A72FA028}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8053C8C-98ED-7494-D874-8E12A72FA028}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {80988C2E-15FF-4FCD-B5F0-A75A1AD74321}
+ EndGlobalSection
+EndGlobal
diff --git a/test/csharp-tableau-loader/gen.bat b/test/csharp-tableau-loader/gen.bat
new file mode 100644
index 00000000..3a3d50f7
--- /dev/null
+++ b/test/csharp-tableau-loader/gen.bat
@@ -0,0 +1,83 @@
+@echo off
+setlocal
+setlocal enabledelayedexpansion
+
+for /f "delims=" %%i in ('git rev-parse --show-toplevel') do set repoRoot=%%i
+cd /d "%repoRoot%"
+
+REM Allow overriding protoc via environment variable.
+REM Default to locally compiled protoc for local development; fallback to system protoc.
+if not defined PROTOC (
+ if exist "%repoRoot%\third_party\_submodules\protobuf\cmake\build\protoc.exe" (
+ set "PROTOC=%repoRoot%\third_party\_submodules\protobuf\cmake\build\protoc.exe"
+ ) else (
+ where protoc >nul 2>nul
+ if !errorlevel! equ 0 (
+ for /f "delims=" %%p in ('where protoc') do set "PROTOC=%%p"
+ ) else (
+ echo Error: protoc not found. Please build protobuf submodule or install protoc. >&2
+ exit /b 1
+ )
+ )
+)
+REM Allow overriding protobuf include path via environment variable.
+REM Default to local submodule source; fallback to system include path.
+if not defined PROTOBUF_PROTO (
+ if exist "%repoRoot%\third_party\_submodules\protobuf\src\google\protobuf" (
+ set "PROTOBUF_PROTO=%repoRoot%\third_party\_submodules\protobuf\src"
+ ) else (
+ for /f "delims=" %%p in ('where protoc 2^>nul') do set "_PROTOC_DIR=%%~dpp"
+ if defined _PROTOC_DIR (
+ set "PROTOBUF_PROTO=!_PROTOC_DIR!..\include"
+ ) else (
+ set "PROTOBUF_PROTO=%repoRoot%\third_party\_submodules\protobuf\src"
+ )
+ )
+)
+set "TABLEAU_PROTO=%repoRoot%\third_party\_submodules\tableau\proto"
+set "ROOTDIR=%repoRoot%\test\csharp-tableau-loader"
+set "PLGUIN_DIR=%repoRoot%\cmd\protoc-gen-csharp-tableau-loader"
+set "PROTOCONF_IN=%repoRoot%\test\proto"
+set "PROTOCONF_OUT=%ROOTDIR%\protoconf"
+set "LOADER_OUT=%ROOTDIR%\tableau"
+
+REM remove old generated files
+rmdir /s /q "%PROTOCONF_OUT%" "%LOADER_OUT%" 2>nul
+mkdir "%PROTOCONF_OUT%" "%LOADER_OUT%"
+
+REM build protoc plugin of loader
+pushd "%PLGUIN_DIR%"
+go build
+popd
+
+set "PATH=%PATH%;%PLGUIN_DIR%"
+
+set protoFiles=
+pushd "%PROTOCONF_IN%"
+for /R %%f in (*.proto) do (
+ set protoFiles=!protoFiles! "%%f"
+)
+popd
+"%PROTOC%" ^
+--csharp_out="%PROTOCONF_OUT%" ^
+--csharp-tableau-loader_out="%LOADER_OUT%" ^
+--csharp-tableau-loader_opt=paths=source_relative ^
+--proto_path="%PROTOBUF_PROTO%" ^
+--proto_path="%TABLEAU_PROTO%" ^
+--proto_path="%PROTOCONF_IN%" ^
+!protoFiles!
+
+set "TABLEAU_IN=%TABLEAU_PROTO%\tableau\protobuf"
+set "TABLEAU_OUT=%ROOTDIR%\protoconf\tableau"
+REM remove old generated files
+rmdir /s /q "%TABLEAU_OUT%" 2>nul
+mkdir "%TABLEAU_OUT%"
+
+"%PROTOC%" ^
+--csharp_out="%TABLEAU_OUT%" ^
+--proto_path="%PROTOBUF_PROTO%" ^
+--proto_path="%TABLEAU_PROTO%" ^
+"%TABLEAU_IN%\tableau.proto" "%TABLEAU_IN%\wellknown.proto"
+
+endlocal
+endlocal
diff --git a/test/csharp-tableau-loader/gen.sh b/test/csharp-tableau-loader/gen.sh
new file mode 100644
index 00000000..95826453
--- /dev/null
+++ b/test/csharp-tableau-loader/gen.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+# set -eux
+set -e
+set -o pipefail
+
+cd "$(git rev-parse --show-toplevel)"
+
+# Allow overriding protoc via environment variable.
+# Default to locally compiled protoc for local development; fallback to system protoc.
+LOCAL_PROTOC="./third_party/_submodules/protobuf/cmake/build/protoc"
+if [ -z "$PROTOC" ]; then
+ if [ -x "$LOCAL_PROTOC" ]; then
+ PROTOC="$LOCAL_PROTOC"
+ else
+ PROTOC="$(which protoc 2>/dev/null || true)"
+ fi
+fi
+if [ -z "$PROTOC" ]; then
+ echo "Error: protoc not found. Please build protobuf submodule or install protoc." >&2
+ exit 1
+fi
+# Allow overriding protobuf include path via environment variable.
+# Default to local submodule source; fallback to system include path.
+LOCAL_PROTOBUF_PROTO="./third_party/_submodules/protobuf/src"
+if [ -z "$PROTOBUF_PROTO" ]; then
+ if [ -d "$LOCAL_PROTOBUF_PROTO/google/protobuf" ]; then
+ PROTOBUF_PROTO="$LOCAL_PROTOBUF_PROTO"
+ else
+ PROTOBUF_PROTO="$(pkg-config --variable=includedir protobuf 2>/dev/null || echo /usr/include)"
+ fi
+fi
+TABLEAU_PROTO="./third_party/_submodules/tableau/proto"
+ROOTDIR="./test/csharp-tableau-loader"
+PLUGIN_DIR="./cmd/protoc-gen-csharp-tableau-loader"
+PROTOCONF_IN="./test/proto"
+PROTOCONF_OUT="${ROOTDIR}/protoconf"
+LOADER_OUT="${ROOTDIR}/tableau"
+
+# remove old generated files
+rm -rfv "$PROTOCONF_OUT" "$LOADER_OUT"
+mkdir -p "$PROTOCONF_OUT" "$LOADER_OUT"
+
+# build
+cd "${PLUGIN_DIR}" && go build && cd -
+
+export PATH="$(pwd)/${PLUGIN_DIR}:${PATH}"
+
+# Collect all .proto files (use `find` for cross-platform compatibility).
+PROTO_FILES=$(find "$PROTOCONF_IN" -name "*.proto")
+
+${PROTOC} \
+ --csharp_out="$PROTOCONF_OUT" \
+ --csharp-tableau-loader_out="$LOADER_OUT" \
+ --csharp-tableau-loader_opt=paths=source_relative \
+ --proto_path="$PROTOBUF_PROTO" \
+ --proto_path="$TABLEAU_PROTO" \
+ --proto_path="$PROTOCONF_IN" \
+ $PROTO_FILES
+
+TABLEAU_IN="$TABLEAU_PROTO/tableau/protobuf"
+TABLEAU_OUT="${ROOTDIR}/protoconf/tableau"
+# remove old generated files
+rm -rfv "$TABLEAU_OUT"
+mkdir -p "$TABLEAU_OUT"
+
+${PROTOC} \
+ --csharp_out="$TABLEAU_OUT" \
+ --proto_path="$PROTOBUF_PROTO" \
+ --proto_path="$TABLEAU_PROTO" \
+ "$TABLEAU_IN/tableau.proto" \
+ "$TABLEAU_IN/wellknown.proto"
diff --git a/test/csharp-tableau-loader/tableau/HeroConf.pc.cs b/test/csharp-tableau-loader/tableau/HeroConf.pc.cs
new file mode 100644
index 00000000..0a5118e5
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/HeroConf.pc.cs
@@ -0,0 +1,176 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+// source: hero_conf.proto
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// HeroConf is a wrapper around protobuf message Protoconf.HeroConf.
+ ///
+ public class HeroConf : Messager, IMessagerName
+ {
+ // OrderedMap types.
+ public class OrderedMap_Hero_AttrMap : SortedDictionary;
+
+ public class OrderedMap_HeroValue(OrderedMap_Hero_AttrMap item1, Protoconf.HeroConf.Types.Hero item2)
+ : Tuple(item1, item2);
+ public class OrderedMap_HeroMap : SortedDictionary;
+
+ private OrderedMap_HeroMap _orderedMap = [];
+
+ private Protoconf.HeroConf _data = new();
+
+ ///
+ /// Name returns the HeroConf's message name.
+ ///
+ public static string Name() => Protoconf.HeroConf.Descriptor.Name;
+
+ ///
+ /// Load loads HeroConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.HeroConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.HeroConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load HeroConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the HeroConf's inner message data.
+ ///
+ public ref readonly Protoconf.HeroConf Data() => ref _data;
+
+ ///
+ /// Message returns the HeroConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // OrderedMap init.
+ _orderedMap.Clear();
+ foreach (var (key1, value1) in _data.HeroMap)
+ {
+ var ordered_map1 = new OrderedMap_Hero_AttrMap();
+ foreach (var (key2, value2) in value1.AttrMap)
+ {
+ ordered_map1[key2] = value2;
+ }
+ _orderedMap[key1] = new OrderedMap_HeroValue(ordered_map1, value1);
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.HeroConf.Types.Hero? Get1(string name) =>
+ _data.HeroMap?.TryGetValue(name, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.HeroConf.Types.Hero.Types.Attr? Get2(string name, string title) =>
+ Get1(name)?.AttrMap?.TryGetValue(title, out var val) == true ? val : null;
+
+ // OrderedMap accessers.
+ ///
+ /// GetOrderedMap returns the 1st-level ordered map.
+ ///
+ public ref readonly OrderedMap_HeroMap GetOrderedMap() => ref _orderedMap;
+
+ ///
+ /// GetOrderedMap1 finds value in the 1st-level ordered map.
+ /// It will return null if the key is not found.
+ ///
+ public OrderedMap_Hero_AttrMap? GetOrderedMap1(string name) =>
+ _orderedMap.TryGetValue(name, out var value) ? value.Item1 : null;
+ }
+
+ ///
+ /// HeroBaseConf is a wrapper around protobuf message Protoconf.HeroBaseConf.
+ ///
+ public class HeroBaseConf : Messager, IMessagerName
+ {
+ private Protoconf.HeroBaseConf _data = new();
+
+ ///
+ /// Name returns the HeroBaseConf's message name.
+ ///
+ public static string Name() => Protoconf.HeroBaseConf.Descriptor.Name;
+
+ ///
+ /// Load loads HeroBaseConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.HeroBaseConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.HeroBaseConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load HeroBaseConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the HeroBaseConf's inner message data.
+ ///
+ public ref readonly Protoconf.HeroBaseConf Data() => ref _data;
+
+ ///
+ /// Message returns the HeroBaseConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Base.Hero? Get1(string name) =>
+ _data.HeroMap?.TryGetValue(name, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Base.Item? Get2(string name, string id) =>
+ Get1(name)?.ItemMap?.TryGetValue(id, out var val) == true ? val : null;
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/Hub.pc.cs b/test/csharp-tableau-loader/tableau/Hub.pc.cs
new file mode 100644
index 00000000..c95fbebf
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/Hub.pc.cs
@@ -0,0 +1,209 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// MessagerContainer holds all messager instances and provides fast access.
+ ///
+ internal class MessagerContainer(in Dictionary? messagerMap = null)
+ {
+ public Dictionary MessagerMap = messagerMap ?? [];
+ public DateTime LastLoadedTime = DateTime.Now;
+ public ItemConf? ItemConf = InternalGet(messagerMap);
+ public ActivityConf? ActivityConf = InternalGet(messagerMap);
+ public ChapterConf? ChapterConf = InternalGet(messagerMap);
+ public ThemeConf? ThemeConf = InternalGet(messagerMap);
+ public TaskConf? TaskConf = InternalGet(messagerMap);
+ public FruitConf? FruitConf = InternalGet(messagerMap);
+ public Fruit2Conf? Fruit2Conf = InternalGet(messagerMap);
+ public Fruit3Conf? Fruit3Conf = InternalGet(messagerMap);
+ public Fruit4Conf? Fruit4Conf = InternalGet(messagerMap);
+ public Fruit5Conf? Fruit5Conf = InternalGet(messagerMap);
+ public HeroConf? HeroConf = InternalGet(messagerMap);
+ public HeroBaseConf? HeroBaseConf = InternalGet(messagerMap);
+ public PatchReplaceConf? PatchReplaceConf = InternalGet(messagerMap);
+ public PatchMergeConf? PatchMergeConf = InternalGet(messagerMap);
+ public RecursivePatchConf? RecursivePatchConf = InternalGet(messagerMap);
+
+ ///
+ /// Get returns the messager of type T from the container.
+ ///
+ public T? Get() where T : Messager, IMessagerName => InternalGet(MessagerMap);
+
+ private static T? InternalGet(in Dictionary? messagerMap) where T : Messager, IMessagerName =>
+ messagerMap?.TryGetValue(T.Name(), out var messager) == true ? (T)messager : null;
+ }
+
+ ///
+ /// Atomic provides a thread-safe wrapper for reference types.
+ ///
+ internal class Atomic where T : class
+ {
+ private T? _value;
+
+ public T? Value
+ {
+ get => Interlocked.CompareExchange(ref _value, null, null);
+ set => Interlocked.Exchange(ref _value, value);
+ }
+ }
+
+ ///
+ /// HubOptions is the options for Hub.
+ ///
+ public class HubOptions
+ {
+ ///
+ /// Filter can only filter in certain specific messagers based on the
+ /// condition that you provide.
+ ///
+ public Func? Filter { get; set; }
+ }
+
+ ///
+ /// Hub is the messager manager. It manages loading, accessing, and storing
+ /// all configuration messagers.
+ ///
+ public class Hub(HubOptions? options = null)
+ {
+ private readonly Atomic _messagerContainer = new();
+ private readonly HubOptions? _options = options;
+
+ ///
+ /// Load fills messages from files in the specified directory and format.
+ ///
+ public bool Load(string dir, Format fmt, in Load.Options? options = null)
+ {
+ var messagerMap = NewMessagerMap();
+ var opts = options ?? new Load.Options();
+ foreach (var kvs in messagerMap)
+ {
+ string name = kvs.Key;
+ if (!kvs.Value.Load(dir, fmt, opts.ParseMessagerOptionsByName(name)))
+ {
+ Console.Error.WriteLine($"load {name} failed: {Util.GetErrMsg()}");
+ return false;
+ }
+ }
+ var tmpHub = new Hub();
+ tmpHub.SetMessagerMap(messagerMap);
+ foreach (var messager in messagerMap)
+ {
+ if (!messager.Value.ProcessAfterLoadAll(tmpHub))
+ {
+ Console.Error.WriteLine($"hub call ProcessAfterLoadAll failed, messager: {messager.Key}");
+ return false;
+ }
+ }
+ SetMessagerMap(messagerMap);
+ return true;
+ }
+
+ ///
+ /// GetMessagerMap returns the current messager map.
+ ///
+ public IReadOnlyDictionary? GetMessagerMap() => _messagerContainer.Value?.MessagerMap;
+
+ ///
+ /// SetMessagerMap sets the messager map with thread-safe guarantee.
+ ///
+ public void SetMessagerMap(in Dictionary map) => _messagerContainer.Value = new MessagerContainer(map);
+
+ ///
+ /// Get returns the messager of type T from the hub.
+ ///
+ public T? Get() where T : Messager, IMessagerName => _messagerContainer.Value?.Get();
+
+ public ItemConf? GetItemConf() => _messagerContainer.Value?.ItemConf;
+
+ public ActivityConf? GetActivityConf() => _messagerContainer.Value?.ActivityConf;
+
+ public ChapterConf? GetChapterConf() => _messagerContainer.Value?.ChapterConf;
+
+ public ThemeConf? GetThemeConf() => _messagerContainer.Value?.ThemeConf;
+
+ public TaskConf? GetTaskConf() => _messagerContainer.Value?.TaskConf;
+
+ public FruitConf? GetFruitConf() => _messagerContainer.Value?.FruitConf;
+
+ public Fruit2Conf? GetFruit2Conf() => _messagerContainer.Value?.Fruit2Conf;
+
+ public Fruit3Conf? GetFruit3Conf() => _messagerContainer.Value?.Fruit3Conf;
+
+ public Fruit4Conf? GetFruit4Conf() => _messagerContainer.Value?.Fruit4Conf;
+
+ public Fruit5Conf? GetFruit5Conf() => _messagerContainer.Value?.Fruit5Conf;
+
+ public HeroConf? GetHeroConf() => _messagerContainer.Value?.HeroConf;
+
+ public HeroBaseConf? GetHeroBaseConf() => _messagerContainer.Value?.HeroBaseConf;
+
+ public PatchReplaceConf? GetPatchReplaceConf() => _messagerContainer.Value?.PatchReplaceConf;
+
+ public PatchMergeConf? GetPatchMergeConf() => _messagerContainer.Value?.PatchMergeConf;
+
+ public RecursivePatchConf? GetRecursivePatchConf() => _messagerContainer.Value?.RecursivePatchConf;
+
+ ///
+ /// GetLastLoadedTime returns the time when hub's messager container was last set.
+ ///
+ public DateTime? GetLastLoadedTime() => _messagerContainer.Value?.LastLoadedTime;
+
+ ///
+ /// NewMessagerMap creates a new MessagerMap based on the registered messagers.
+ ///
+ private Dictionary NewMessagerMap()
+ {
+ var messagerMap = new Dictionary();
+ foreach (var kv in Registry.Registrar)
+ {
+ if (_options?.Filter?.Invoke(kv.Key) ?? true)
+ {
+ messagerMap[kv.Key] = kv.Value();
+ }
+ }
+ return messagerMap;
+ }
+ }
+
+ ///
+ /// Registry manages the registration of all messager generators.
+ ///
+ public class Registry
+ {
+ internal static readonly Dictionary> Registrar = [];
+
+ ///
+ /// Register registers a messager generator for type T.
+ ///
+ public static void Register() where T : Messager, IMessagerName, new() => Registrar[T.Name()] = () => new T();
+
+ ///
+ /// Init registers all generated messagers.
+ ///
+ public static void Init()
+ {
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ Register();
+ }
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/IndexConf.pc.cs b/test/csharp-tableau-loader/tableau/IndexConf.pc.cs
new file mode 100644
index 00000000..149e4ce7
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/IndexConf.pc.cs
@@ -0,0 +1,1215 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+// source: index_conf.proto
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// FruitConf is a wrapper around protobuf message Protoconf.FruitConf.
+ ///
+ public class FruitConf : Messager, IMessagerName
+ {
+ // OrderedIndex types.
+ // OrderedIndex: Price
+ public class OrderedIndex_ItemMap : SortedDictionary>;
+
+ private OrderedIndex_ItemMap _orderedIndexItemMap = [];
+
+ private Dictionary _orderedIndexItemMap1 = [];
+
+ private Protoconf.FruitConf _data = new();
+
+ ///
+ /// Name returns the FruitConf's message name.
+ ///
+ public static string Name() => Protoconf.FruitConf.Descriptor.Name;
+
+ ///
+ /// Load loads FruitConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.FruitConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.FruitConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load FruitConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the FruitConf's inner message data.
+ ///
+ public ref readonly Protoconf.FruitConf Data() => ref _data;
+
+ ///
+ /// Message returns the FruitConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // OrderedIndex init.
+ _orderedIndexItemMap.Clear();
+ _orderedIndexItemMap1.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.ItemMap)
+ {
+ {
+ // OrderedIndex: Price
+ var key = item2.Value.Price;
+ {
+ var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexItemMap[key] = [];
+ list.Add(item2.Value);
+ }
+ {
+ var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _orderedIndexItemMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item2.Value);
+ }
+ }
+ }
+ }
+ // OrderedIndex(sort): Price
+ Comparison orderedIndexItemMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexItemMap.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ foreach (var itemDict in _orderedIndexItemMap1.Values)
+ {
+ foreach (var itemList in itemDict.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.FruitConf.Types.Fruit? Get1(int fruitType) =>
+ _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.FruitConf.Types.Fruit.Types.Item? Get2(int fruitType, int id) =>
+ Get1(fruitType)?.ItemMap?.TryGetValue(id, out var val) == true ? val : null;
+
+ // OrderedIndex: Price
+
+ ///
+ /// FindItemMap finds the ordered index: key(Price) to value(Protoconf.FruitConf.Types.Fruit.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap;
+
+ ///
+ /// FindItem finds a list of all values of the given key(s).
+ ///
+ public List? FindItem(int price) =>
+ _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null;
+
+ ///
+ /// FindFirstItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.FruitConf.Types.Fruit.Types.Item? FindFirstItem(int price) =>
+ FindItem(price)?.FirstOrDefault();
+
+ ///
+ /// FindItemMap1 finds the ordered index: key(Price) to value(Protoconf.FruitConf.Types.Fruit.Types.Item),
+ /// which is the upper 1st-level sorted map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public OrderedIndex_ItemMap? FindItemMap1(int fruitType) =>
+ _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType).
+ ///
+ public List? FindItem1(int fruitType, int price) =>
+ FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.FruitConf.Types.Fruit.Types.Item? FindFirstItem1(int fruitType, int price) =>
+ FindItem1(fruitType, price)?.FirstOrDefault();
+ }
+
+ ///
+ /// Fruit2Conf is a wrapper around protobuf message Protoconf.Fruit2Conf.
+ ///
+ public class Fruit2Conf : Messager, IMessagerName
+ {
+ // Index types.
+ // Index: CountryName
+ public class Index_CountryMap : Dictionary>;
+
+ private Index_CountryMap _indexCountryMap = [];
+
+ // Index: CountryItemAttrName
+ public class Index_AttrMap : Dictionary>;
+
+ private Index_AttrMap _indexAttrMap = [];
+
+ private Dictionary _indexAttrMap1 = [];
+
+ // OrderedIndex types.
+ // OrderedIndex: CountryItemPrice
+ public class OrderedIndex_ItemMap : SortedDictionary>;
+
+ private OrderedIndex_ItemMap _orderedIndexItemMap = [];
+
+ private Dictionary _orderedIndexItemMap1 = [];
+
+ private Protoconf.Fruit2Conf _data = new();
+
+ ///
+ /// Name returns the Fruit2Conf's message name.
+ ///
+ public static string Name() => Protoconf.Fruit2Conf.Descriptor.Name;
+
+ ///
+ /// Load loads Fruit2Conf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.Fruit2Conf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.Fruit2Conf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load Fruit2Conf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the Fruit2Conf's inner message data.
+ ///
+ public ref readonly Protoconf.Fruit2Conf Data() => ref _data;
+
+ ///
+ /// Message returns the Fruit2Conf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // Index init.
+ _indexCountryMap.Clear();
+ _indexAttrMap.Clear();
+ _indexAttrMap1.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.CountryList)
+ {
+ {
+ // Index: CountryName
+ var key = item2.Name;
+ {
+ var list = _indexCountryMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexCountryMap[key] = [];
+ list.Add(item2);
+ }
+ }
+ foreach (var item3 in item2.ItemMap)
+ {
+ foreach (var item4 in item3.Value.AttrList)
+ {
+ {
+ // Index: CountryItemAttrName
+ var key = item4.Name;
+ {
+ var list = _indexAttrMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexAttrMap[key] = [];
+ list.Add(item4);
+ }
+ {
+ var map = _indexAttrMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexAttrMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item4);
+ }
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex init.
+ _orderedIndexItemMap.Clear();
+ _orderedIndexItemMap1.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.CountryList)
+ {
+ foreach (var item3 in item2.ItemMap)
+ {
+ {
+ // OrderedIndex: CountryItemPrice
+ var key = item3.Value.Price;
+ {
+ var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexItemMap[key] = [];
+ list.Add(item3.Value);
+ }
+ {
+ var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _orderedIndexItemMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item3.Value);
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex(sort): CountryItemPrice
+ Comparison orderedIndexItemMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexItemMap.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ foreach (var itemDict in _orderedIndexItemMap1.Values)
+ {
+ foreach (var itemList in itemDict.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit? Get1(int fruitType) =>
+ _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null;
+
+ // Index: CountryName
+
+ ///
+ /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap;
+
+ ///
+ /// FindCountry finds a list of all values of the given key(s).
+ ///
+ public List? FindCountry(string name) =>
+ _indexCountryMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstCountry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) =>
+ FindCountry(name)?.FirstOrDefault();
+
+ // Index: CountryItemAttrName
+
+ ///
+ /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap;
+
+ ///
+ /// FindAttr finds a list of all values of the given key(s).
+ ///
+ public List? FindAttr(string name) =>
+ _indexAttrMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstAttr finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) =>
+ FindAttr(name)?.FirstOrDefault();
+
+ ///
+ /// FindAttrMap1 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr),
+ /// which is the upper 1st-level map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_AttrMap? FindAttrMap1(int fruitType) =>
+ _indexAttrMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindAttr1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType).
+ ///
+ public List? FindAttr1(int fruitType, string name) =>
+ FindAttrMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstAttr1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr1(int fruitType, string name) =>
+ FindAttr1(fruitType, name)?.FirstOrDefault();
+
+ // OrderedIndex: CountryItemPrice
+
+ ///
+ /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap;
+
+ ///
+ /// FindItem finds a list of all values of the given key(s).
+ ///
+ public List? FindItem(int price) =>
+ _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null;
+
+ ///
+ /// FindFirstItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) =>
+ FindItem(price)?.FirstOrDefault();
+
+ ///
+ /// FindItemMap1 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item),
+ /// which is the upper 1st-level sorted map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public OrderedIndex_ItemMap? FindItemMap1(int fruitType) =>
+ _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType).
+ ///
+ public List? FindItem1(int fruitType, int price) =>
+ FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem1(int fruitType, int price) =>
+ FindItem1(fruitType, price)?.FirstOrDefault();
+ }
+
+ ///
+ /// Fruit3Conf is a wrapper around protobuf message Protoconf.Fruit3Conf.
+ ///
+ public class Fruit3Conf : Messager, IMessagerName
+ {
+ // Index types.
+ // Index: CountryName
+ public class Index_CountryMap : Dictionary>;
+
+ private Index_CountryMap _indexCountryMap = [];
+
+ // Index: CountryItemAttrName
+ public class Index_AttrMap : Dictionary>;
+
+ private Index_AttrMap _indexAttrMap = [];
+
+ // OrderedIndex types.
+ // OrderedIndex: CountryItemPrice
+ public class OrderedIndex_ItemMap : SortedDictionary>;
+
+ private OrderedIndex_ItemMap _orderedIndexItemMap = [];
+
+ private Protoconf.Fruit3Conf _data = new();
+
+ ///
+ /// Name returns the Fruit3Conf's message name.
+ ///
+ public static string Name() => Protoconf.Fruit3Conf.Descriptor.Name;
+
+ ///
+ /// Load loads Fruit3Conf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.Fruit3Conf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.Fruit3Conf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load Fruit3Conf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the Fruit3Conf's inner message data.
+ ///
+ public ref readonly Protoconf.Fruit3Conf Data() => ref _data;
+
+ ///
+ /// Message returns the Fruit3Conf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // Index init.
+ _indexCountryMap.Clear();
+ _indexAttrMap.Clear();
+ foreach (var item1 in _data.FruitList)
+ {
+ foreach (var item2 in item1.CountryList)
+ {
+ {
+ // Index: CountryName
+ var key = item2.Name;
+ {
+ var list = _indexCountryMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexCountryMap[key] = [];
+ list.Add(item2);
+ }
+ }
+ foreach (var item3 in item2.ItemMap)
+ {
+ foreach (var item4 in item3.Value.AttrList)
+ {
+ {
+ // Index: CountryItemAttrName
+ var key = item4.Name;
+ {
+ var list = _indexAttrMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexAttrMap[key] = [];
+ list.Add(item4);
+ }
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex init.
+ _orderedIndexItemMap.Clear();
+ foreach (var item1 in _data.FruitList)
+ {
+ foreach (var item2 in item1.CountryList)
+ {
+ foreach (var item3 in item2.ItemMap)
+ {
+ {
+ // OrderedIndex: CountryItemPrice
+ var key = item3.Value.Price;
+ {
+ var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexItemMap[key] = [];
+ list.Add(item3.Value);
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex(sort): CountryItemPrice
+ Comparison orderedIndexItemMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexItemMap.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ return true;
+ }
+
+ // Index: CountryName
+
+ ///
+ /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap;
+
+ ///
+ /// FindCountry finds a list of all values of the given key(s).
+ ///
+ public List? FindCountry(string name) =>
+ _indexCountryMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstCountry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit3Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) =>
+ FindCountry(name)?.FirstOrDefault();
+
+ // Index: CountryItemAttrName
+
+ ///
+ /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap;
+
+ ///
+ /// FindAttr finds a list of all values of the given key(s).
+ ///
+ public List? FindAttr(string name) =>
+ _indexAttrMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstAttr finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) =>
+ FindAttr(name)?.FirstOrDefault();
+
+ // OrderedIndex: CountryItemPrice
+
+ ///
+ /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap;
+
+ ///
+ /// FindItem finds a list of all values of the given key(s).
+ ///
+ public List? FindItem(int price) =>
+ _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null;
+
+ ///
+ /// FindFirstItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) =>
+ FindItem(price)?.FirstOrDefault();
+ }
+
+ ///
+ /// Fruit4Conf is a wrapper around protobuf message Protoconf.Fruit4Conf.
+ ///
+ public class Fruit4Conf : Messager, IMessagerName
+ {
+
+ // LevelIndex keys.
+ public readonly struct LevelIndex_Fruit_CountryKey : IEquatable
+ {
+ public int FruitType { get; }
+ public int Id { get; }
+
+ public LevelIndex_Fruit_CountryKey(int fruitType, int id)
+ {
+ FruitType = fruitType;
+ Id = id;
+ }
+
+ public bool Equals(LevelIndex_Fruit_CountryKey other) =>
+ (FruitType, Id).Equals((other.FruitType, other.Id));
+
+ public override int GetHashCode() =>
+ (FruitType, Id).GetHashCode();
+ }
+
+ // Index types.
+ // Index: CountryName
+ public class Index_CountryMap : Dictionary>;
+
+ private Index_CountryMap _indexCountryMap = [];
+
+ private Dictionary _indexCountryMap1 = [];
+
+ // Index: CountryItemAttrName
+ public class Index_AttrMap : Dictionary>;
+
+ private Index_AttrMap _indexAttrMap = [];
+
+ private Dictionary _indexAttrMap1 = [];
+
+ private Dictionary _indexAttrMap2 = [];
+
+ // OrderedIndex types.
+ // OrderedIndex: CountryItemPrice
+ public class OrderedIndex_ItemMap : SortedDictionary>;
+
+ private OrderedIndex_ItemMap _orderedIndexItemMap = [];
+
+ private Dictionary _orderedIndexItemMap1 = [];
+
+ private Dictionary _orderedIndexItemMap2 = [];
+
+ private Protoconf.Fruit4Conf _data = new();
+
+ ///
+ /// Name returns the Fruit4Conf's message name.
+ ///
+ public static string Name() => Protoconf.Fruit4Conf.Descriptor.Name;
+
+ ///
+ /// Load loads Fruit4Conf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.Fruit4Conf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.Fruit4Conf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load Fruit4Conf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the Fruit4Conf's inner message data.
+ ///
+ public ref readonly Protoconf.Fruit4Conf Data() => ref _data;
+
+ ///
+ /// Message returns the Fruit4Conf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // Index init.
+ _indexCountryMap.Clear();
+ _indexCountryMap1.Clear();
+ _indexAttrMap.Clear();
+ _indexAttrMap1.Clear();
+ _indexAttrMap2.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.CountryMap)
+ {
+ var k2 = item2.Key;
+ {
+ // Index: CountryName
+ var key = item2.Value.Name;
+ {
+ var list = _indexCountryMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexCountryMap[key] = [];
+ list.Add(item2.Value);
+ }
+ {
+ var map = _indexCountryMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexCountryMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item2.Value);
+ }
+ }
+ foreach (var item3 in item2.Value.ItemMap)
+ {
+ foreach (var item4 in item3.Value.AttrList)
+ {
+ {
+ // Index: CountryItemAttrName
+ var key = item4.Name;
+ {
+ var list = _indexAttrMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexAttrMap[key] = [];
+ list.Add(item4);
+ }
+ {
+ var map = _indexAttrMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexAttrMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item4);
+ }
+ {
+ var mapKey = new LevelIndex_Fruit_CountryKey(k1, k2);
+ var map = _indexAttrMap2.TryGetValue(mapKey, out var existingMap) ?
+ existingMap : _indexAttrMap2[mapKey] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item4);
+ }
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex init.
+ _orderedIndexItemMap.Clear();
+ _orderedIndexItemMap1.Clear();
+ _orderedIndexItemMap2.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.CountryMap)
+ {
+ var k2 = item2.Key;
+ foreach (var item3 in item2.Value.ItemMap)
+ {
+ {
+ // OrderedIndex: CountryItemPrice
+ var key = item3.Value.Price;
+ {
+ var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexItemMap[key] = [];
+ list.Add(item3.Value);
+ }
+ {
+ var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _orderedIndexItemMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item3.Value);
+ }
+ {
+ var mapKey = new LevelIndex_Fruit_CountryKey(k1, k2);
+ var map = _orderedIndexItemMap2.TryGetValue(mapKey, out var existingMap) ?
+ existingMap : _orderedIndexItemMap2[mapKey] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item3.Value);
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex(sort): CountryItemPrice
+ Comparison orderedIndexItemMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexItemMap.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ foreach (var itemDict in _orderedIndexItemMap1.Values)
+ {
+ foreach (var itemList in itemDict.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ }
+ foreach (var itemDict in _orderedIndexItemMap2.Values)
+ {
+ foreach (var itemList in itemDict.Values)
+ {
+ itemList.Sort(orderedIndexItemMapComparison);
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit? Get1(int fruitType) =>
+ _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? Get2(int fruitType, int id) =>
+ Get1(fruitType)?.CountryMap?.TryGetValue(id, out var val) == true ? val : null;
+
+ ///
+ /// Get3 finds value in the 3rd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? Get3(int fruitType, int id, int id3) =>
+ Get2(fruitType, id)?.ItemMap?.TryGetValue(id3, out var val) == true ? val : null;
+
+ // Index: CountryName
+
+ ///
+ /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap;
+
+ ///
+ /// FindCountry finds a list of all values of the given key(s).
+ ///
+ public List? FindCountry(string name) =>
+ _indexCountryMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstCountry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) =>
+ FindCountry(name)?.FirstOrDefault();
+
+ ///
+ /// FindCountryMap1 finds the index: key(CountryName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country),
+ /// which is the upper 1st-level map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_CountryMap? FindCountryMap1(int fruitType) =>
+ _indexCountryMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindCountry1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType).
+ ///
+ public List? FindCountry1(int fruitType, string name) =>
+ FindCountryMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstCountry1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? FindFirstCountry1(int fruitType, string name) =>
+ FindCountry1(fruitType, name)?.FirstOrDefault();
+
+ // Index: CountryItemAttrName
+
+ ///
+ /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap;
+
+ ///
+ /// FindAttr finds a list of all values of the given key(s).
+ ///
+ public List? FindAttr(string name) =>
+ _indexAttrMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstAttr finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) =>
+ FindAttr(name)?.FirstOrDefault();
+
+ ///
+ /// FindAttrMap1 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr),
+ /// which is the upper 1st-level map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_AttrMap? FindAttrMap1(int fruitType) =>
+ _indexAttrMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindAttr1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType).
+ ///
+ public List? FindAttr1(int fruitType, string name) =>
+ FindAttrMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstAttr1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr1(int fruitType, string name) =>
+ FindAttr1(fruitType, name)?.FirstOrDefault();
+
+ ///
+ /// FindAttrMap2 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr),
+ /// which is the upper 2nd-level map specified by (fruitType, id).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_AttrMap? FindAttrMap2(int fruitType, int id) =>
+ _indexAttrMap2.TryGetValue(new LevelIndex_Fruit_CountryKey(fruitType, id), out var value) ? value : null;
+
+ ///
+ /// FindAttr2 finds a list of all values of the given key(s) in the upper 2nd-level map
+ /// specified by (fruitType, id).
+ ///
+ public List? FindAttr2(int fruitType, int id, string name) =>
+ FindAttrMap2(fruitType, id)?.TryGetValue(name, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstAttr2 finds the first value of the given key(s) in the upper 2nd-level map
+ /// specified by (fruitType, id), or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr2(int fruitType, int id, string name) =>
+ FindAttr2(fruitType, id, name)?.FirstOrDefault();
+
+ // OrderedIndex: CountryItemPrice
+
+ ///
+ /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap;
+
+ ///
+ /// FindItem finds a list of all values of the given key(s).
+ ///
+ public List? FindItem(int price) =>
+ _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null;
+
+ ///
+ /// FindFirstItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) =>
+ FindItem(price)?.FirstOrDefault();
+
+ ///
+ /// FindItemMap1 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item),
+ /// which is the upper 1st-level sorted map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public OrderedIndex_ItemMap? FindItemMap1(int fruitType) =>
+ _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType).
+ ///
+ public List? FindItem1(int fruitType, int price) =>
+ FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem1(int fruitType, int price) =>
+ FindItem1(fruitType, price)?.FirstOrDefault();
+
+ ///
+ /// FindItemMap2 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item),
+ /// which is the upper 2nd-level sorted map specified by (fruitType, id).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public OrderedIndex_ItemMap? FindItemMap2(int fruitType, int id) =>
+ _orderedIndexItemMap2.TryGetValue(new LevelIndex_Fruit_CountryKey(fruitType, id), out var value) ? value : null;
+
+ ///
+ /// FindItem2 finds a list of all values of the given key(s) in the upper 2nd-level sorted map
+ /// specified by (fruitType, id).
+ ///
+ public List? FindItem2(int fruitType, int id, int price) =>
+ FindItemMap2(fruitType, id)?.TryGetValue(price, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstItem2 finds the first value of the given key(s) in the upper 2nd-level sorted map
+ /// specified by (fruitType, id), or null if no value found.
+ ///
+ public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem2(int fruitType, int id, int price) =>
+ FindItem2(fruitType, id, price)?.FirstOrDefault();
+ }
+
+ ///
+ /// Fruit5Conf is a wrapper around protobuf message Protoconf.Fruit5Conf.
+ ///
+ public class Fruit5Conf : Messager, IMessagerName
+ {
+ // Index types.
+ // Index: CountryName
+ public class Index_CountryMap : Dictionary>;
+
+ private Index_CountryMap _indexCountryMap = [];
+
+ private Dictionary _indexCountryMap1 = [];
+
+ private Protoconf.Fruit5Conf _data = new();
+
+ ///
+ /// Name returns the Fruit5Conf's message name.
+ ///
+ public static string Name() => Protoconf.Fruit5Conf.Descriptor.Name;
+
+ ///
+ /// Load loads Fruit5Conf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.Fruit5Conf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.Fruit5Conf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load Fruit5Conf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the Fruit5Conf's inner message data.
+ ///
+ public ref readonly Protoconf.Fruit5Conf Data() => ref _data;
+
+ ///
+ /// Message returns the Fruit5Conf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // Index init.
+ _indexCountryMap.Clear();
+ _indexCountryMap1.Clear();
+ foreach (var item1 in _data.FruitMap)
+ {
+ var k1 = item1.Key;
+ foreach (var item2 in item1.Value.CountryMap)
+ {
+ {
+ // Index: CountryName
+ var key = item2.Value.Name;
+ {
+ var list = _indexCountryMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexCountryMap[key] = [];
+ list.Add(item2.Value);
+ }
+ {
+ var map = _indexCountryMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexCountryMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item2.Value);
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit5Conf.Types.Fruit? Get1(int fruitType) =>
+ _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? Get2(int fruitType, int id) =>
+ Get1(fruitType)?.CountryMap?.TryGetValue(id, out var val) == true ? val : null;
+
+ ///
+ /// Get3 finds value in the 3rd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Fruit5Conf.Types.Fruit.Types.Country.Types.Item? Get3(int fruitType, int id, int id3) =>
+ Get2(fruitType, id)?.ItemMap?.TryGetValue(id3, out var val) == true ? val : null;
+
+ // Index: CountryName
+
+ ///
+ /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit5Conf.Types.Fruit.Types.Country) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap;
+
+ ///
+ /// FindCountry finds a list of all values of the given key(s).
+ ///
+ public List? FindCountry(string name) =>
+ _indexCountryMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstCountry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) =>
+ FindCountry(name)?.FirstOrDefault();
+
+ ///
+ /// FindCountryMap1 finds the index: key(CountryName) to value(Protoconf.Fruit5Conf.Types.Fruit.Types.Country),
+ /// which is the upper 1st-level map specified by (fruitType).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_CountryMap? FindCountryMap1(int fruitType) =>
+ _indexCountryMap1.TryGetValue(fruitType, out var value) ? value : null;
+
+ ///
+ /// FindCountry1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType).
+ ///
+ public List? FindCountry1(int fruitType, string name) =>
+ FindCountryMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstCountry1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (fruitType), or null if no value found.
+ ///
+ public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? FindFirstCountry1(int fruitType, string name) =>
+ FindCountry1(fruitType, name)?.FirstOrDefault();
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/ItemConf.pc.cs b/test/csharp-tableau-loader/tableau/ItemConf.pc.cs
new file mode 100644
index 00000000..42497658
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/ItemConf.pc.cs
@@ -0,0 +1,630 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+// source: item_conf.proto
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// ItemConf is a wrapper around protobuf message Protoconf.ItemConf.
+ ///
+ public class ItemConf : Messager, IMessagerName
+ {
+ // OrderedMap types.
+ public class OrderedMap_ItemMap : SortedDictionary;
+
+ private OrderedMap_ItemMap _orderedMap = [];
+
+ // Index types.
+ // Index: Type
+ public class Index_ItemMap : Dictionary>;
+
+ private Index_ItemMap _indexItemMap = [];
+
+ // Index: Param@ItemInfo
+ public class Index_ItemInfoMap : Dictionary>;
+
+ private Index_ItemInfoMap _indexItemInfoMap = [];
+
+ // Index: Default@ItemDefaultInfo
+ public class Index_ItemDefaultInfoMap : Dictionary>;
+
+ private Index_ItemDefaultInfoMap _indexItemDefaultInfoMap = [];
+
+ // Index: ExtType@ItemExtInfo
+ public class Index_ItemExtInfoMap : Dictionary>;
+
+ private Index_ItemExtInfoMap _indexItemExtInfoMap = [];
+
+ // Index: (ID,Name)@AwardItem
+ public readonly struct Index_AwardItemKey : IEquatable
+ {
+ public uint Id { get; }
+ public string Name { get; }
+
+ public Index_AwardItemKey(uint id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+
+ public bool Equals(Index_AwardItemKey other) =>
+ (Id, Name).Equals((other.Id, other.Name));
+
+ public override int GetHashCode() =>
+ (Id, Name).GetHashCode();
+ }
+
+ public class Index_AwardItemMap : Dictionary>;
+
+ private Index_AwardItemMap _indexAwardItemMap = [];
+
+ // Index: (ID,Type,Param,ExtType)@SpecialItem
+ public readonly struct Index_SpecialItemKey : IEquatable
+ {
+ public uint Id { get; }
+ public Protoconf.FruitType Type { get; }
+ public int Param { get; }
+ public Protoconf.FruitType ExtType { get; }
+
+ public Index_SpecialItemKey(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType)
+ {
+ Id = id;
+ Type = type;
+ Param = param;
+ ExtType = extType;
+ }
+
+ public bool Equals(Index_SpecialItemKey other) =>
+ (Id, Type, Param, ExtType).Equals((other.Id, other.Type, other.Param, other.ExtType));
+
+ public override int GetHashCode() =>
+ (Id, Type, Param, ExtType).GetHashCode();
+ }
+
+ public class Index_SpecialItemMap : Dictionary>;
+
+ private Index_SpecialItemMap _indexSpecialItemMap = [];
+
+ // Index: PathDir@ItemPathDir
+ public class Index_ItemPathDirMap : Dictionary>;
+
+ private Index_ItemPathDirMap _indexItemPathDirMap = [];
+
+ // Index: PathName@ItemPathName
+ public class Index_ItemPathNameMap : Dictionary>;
+
+ private Index_ItemPathNameMap _indexItemPathNameMap = [];
+
+ // Index: PathFriendID@ItemPathFriendID
+ public class Index_ItemPathFriendIDMap : Dictionary>;
+
+ private Index_ItemPathFriendIDMap _indexItemPathFriendIdMap = [];
+
+ // Index: UseEffectType@UseEffectType
+ public class Index_UseEffectTypeMap : Dictionary>;
+
+ private Index_UseEffectTypeMap _indexUseEffectTypeMap = [];
+
+ // OrderedIndex types.
+ // OrderedIndex: ExtType@ExtType
+ public class OrderedIndex_ExtTypeMap : SortedDictionary>;
+
+ private OrderedIndex_ExtTypeMap _orderedIndexExtTypeMap = [];
+
+ // OrderedIndex: (Param,ExtType)@ParamExtType
+ public readonly struct OrderedIndex_ParamExtTypeKey : IComparable
+ {
+ public int Param { get; }
+ public Protoconf.FruitType ExtType { get; }
+
+ public OrderedIndex_ParamExtTypeKey(int param, Protoconf.FruitType extType)
+ {
+ Param = param;
+ ExtType = extType;
+ }
+
+ public int CompareTo(OrderedIndex_ParamExtTypeKey other) =>
+ (Param, ExtType).CompareTo((other.Param, other.ExtType));
+ }
+
+ public class OrderedIndex_ParamExtTypeMap : SortedDictionary>;
+
+ private OrderedIndex_ParamExtTypeMap _orderedIndexParamExtTypeMap = [];
+
+ private Protoconf.ItemConf _data = new();
+
+ ///
+ /// Name returns the ItemConf's message name.
+ ///
+ public static string Name() => Protoconf.ItemConf.Descriptor.Name;
+
+ ///
+ /// Load loads ItemConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.ItemConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.ItemConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load ItemConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the ItemConf's inner message data.
+ ///
+ public ref readonly Protoconf.ItemConf Data() => ref _data;
+
+ ///
+ /// Message returns the ItemConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // OrderedMap init.
+ _orderedMap.Clear();
+ foreach (var (key1, value1) in _data.ItemMap)
+ {
+ _orderedMap[key1] = value1;
+ }
+ // Index init.
+ _indexItemMap.Clear();
+ _indexItemInfoMap.Clear();
+ _indexItemDefaultInfoMap.Clear();
+ _indexItemExtInfoMap.Clear();
+ _indexAwardItemMap.Clear();
+ _indexSpecialItemMap.Clear();
+ _indexItemPathDirMap.Clear();
+ _indexItemPathNameMap.Clear();
+ _indexItemPathFriendIdMap.Clear();
+ _indexUseEffectTypeMap.Clear();
+ foreach (var item1 in _data.ItemMap)
+ {
+ {
+ // Index: Type
+ var key = item1.Value.Type;
+ {
+ var list = _indexItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexItemMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // Index: Param@ItemInfo
+ foreach (var item2 in item1.Value.ParamList ?? Enumerable.Empty())
+ {
+ {
+ var list = _indexItemInfoMap.TryGetValue(item2, out var existingList) ?
+ existingList : _indexItemInfoMap[item2] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ {
+ // Index: Default@ItemDefaultInfo
+ var key = item1.Value.Default;
+ {
+ var list = _indexItemDefaultInfoMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexItemDefaultInfoMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // Index: ExtType@ItemExtInfo
+ foreach (var item2 in item1.Value.ExtTypeList ?? Enumerable.Empty())
+ {
+ {
+ var list = _indexItemExtInfoMap.TryGetValue(item2, out var existingList) ?
+ existingList : _indexItemExtInfoMap[item2] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ {
+ // Index: (ID,Name)@AwardItem
+ var key = new Index_AwardItemKey(item1.Value.Id, item1.Value.Name);
+ {
+ var list = _indexAwardItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexAwardItemMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // Index: (ID,Type,Param,ExtType)@SpecialItem
+ foreach (var indexItem2 in item1.Value.ParamList ?? Enumerable.Empty())
+ {
+ foreach (var indexItem3 in item1.Value.ExtTypeList ?? Enumerable.Empty())
+ {
+ var key = new Index_SpecialItemKey(item1.Value.Id, item1.Value.Type, indexItem2, indexItem3);
+ {
+ var list = _indexSpecialItemMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexSpecialItemMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ }
+ {
+ // Index: PathDir@ItemPathDir
+ var key = item1.Value.Path?.Dir ?? "";
+ {
+ var list = _indexItemPathDirMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexItemPathDirMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // Index: PathName@ItemPathName
+ foreach (var item2 in item1.Value.Path?.NameList ?? Enumerable.Empty())
+ {
+ {
+ var list = _indexItemPathNameMap.TryGetValue(item2, out var existingList) ?
+ existingList : _indexItemPathNameMap[item2] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ {
+ // Index: PathFriendID@ItemPathFriendID
+ var key = item1.Value.Path?.Friend?.Id ?? 0;
+ {
+ var list = _indexItemPathFriendIdMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexItemPathFriendIdMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // Index: UseEffectType@UseEffectType
+ var key = item1.Value.UseEffect?.Type ?? 0;
+ {
+ var list = _indexUseEffectTypeMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexUseEffectTypeMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ // Index(sort): Param@ItemInfo
+ Comparison indexItemInfoMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _indexItemInfoMap.Values)
+ {
+ itemList.Sort(indexItemInfoMapComparison);
+ }
+ // Index(sort): (ID,Name)@AwardItem
+ Comparison indexAwardItemMapComparison = (a, b) =>
+ (a.Type, a.UseEffect?.Type ?? 0).CompareTo((b.Type, b.UseEffect?.Type ?? 0));
+ foreach (var itemList in _indexAwardItemMap.Values)
+ {
+ itemList.Sort(indexAwardItemMapComparison);
+ }
+ // OrderedIndex init.
+ _orderedIndexExtTypeMap.Clear();
+ _orderedIndexParamExtTypeMap.Clear();
+ foreach (var item1 in _data.ItemMap)
+ {
+ {
+ // OrderedIndex: ExtType@ExtType
+ foreach (var item2 in item1.Value.ExtTypeList ?? Enumerable.Empty())
+ {
+ var key = item2;
+ {
+ var list = _orderedIndexExtTypeMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexExtTypeMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ {
+ // OrderedIndex: (Param,ExtType)@ParamExtType
+ foreach (var indexItem0 in item1.Value.ParamList ?? Enumerable.Empty())
+ {
+ foreach (var indexItem1 in item1.Value.ExtTypeList ?? Enumerable.Empty())
+ {
+ var key = new OrderedIndex_ParamExtTypeKey(indexItem0, indexItem1);
+ {
+ var list = _orderedIndexParamExtTypeMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexParamExtTypeMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ }
+ }
+ // OrderedIndex(sort): (Param,ExtType)@ParamExtType
+ Comparison orderedIndexParamExtTypeMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexParamExtTypeMap.Values)
+ {
+ itemList.Sort(orderedIndexParamExtTypeMapComparison);
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.ItemConf.Types.Item? Get1(uint id) =>
+ _data.ItemMap?.TryGetValue(id, out var val) == true ? val : null;
+
+ // OrderedMap accessers.
+ ///
+ /// GetOrderedMap returns the 1st-level ordered map.
+ ///
+ public ref readonly OrderedMap_ItemMap GetOrderedMap() => ref _orderedMap;
+
+ // Index: Type
+
+ ///
+ /// FindItemMap finds the index: key(Type) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemMap FindItemMap() => ref _indexItemMap;
+
+ ///
+ /// FindItem finds a list of all values of the given key(s).
+ ///
+ public List? FindItem(Protoconf.FruitType type) =>
+ _indexItemMap.TryGetValue(type, out var value) ? value : null;
+
+ ///
+ /// FindFirstItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItem(Protoconf.FruitType type) =>
+ FindItem(type)?.FirstOrDefault();
+
+ // Index: Param@ItemInfo
+
+ ///
+ /// FindItemInfoMap finds the index: key(Param@ItemInfo) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemInfoMap FindItemInfoMap() => ref _indexItemInfoMap;
+
+ ///
+ /// FindItemInfo finds a list of all values of the given key(s).
+ ///
+ public List? FindItemInfo(int param) =>
+ _indexItemInfoMap.TryGetValue(param, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemInfo finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemInfo(int param) =>
+ FindItemInfo(param)?.FirstOrDefault();
+
+ // Index: Default@ItemDefaultInfo
+
+ ///
+ /// FindItemDefaultInfoMap finds the index: key(Default@ItemDefaultInfo) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemDefaultInfoMap FindItemDefaultInfoMap() => ref _indexItemDefaultInfoMap;
+
+ ///
+ /// FindItemDefaultInfo finds a list of all values of the given key(s).
+ ///
+ public List? FindItemDefaultInfo(string default_) =>
+ _indexItemDefaultInfoMap.TryGetValue(default_, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemDefaultInfo finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemDefaultInfo(string default_) =>
+ FindItemDefaultInfo(default_)?.FirstOrDefault();
+
+ // Index: ExtType@ItemExtInfo
+
+ ///
+ /// FindItemExtInfoMap finds the index: key(ExtType@ItemExtInfo) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemExtInfoMap FindItemExtInfoMap() => ref _indexItemExtInfoMap;
+
+ ///
+ /// FindItemExtInfo finds a list of all values of the given key(s).
+ ///
+ public List? FindItemExtInfo(Protoconf.FruitType extType) =>
+ _indexItemExtInfoMap.TryGetValue(extType, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemExtInfo finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemExtInfo(Protoconf.FruitType extType) =>
+ FindItemExtInfo(extType)?.FirstOrDefault();
+
+ // Index: (ID,Name)@AwardItem
+
+ ///
+ /// FindAwardItemMap finds the index: key((ID,Name)@AwardItem) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_AwardItemMap FindAwardItemMap() => ref _indexAwardItemMap;
+
+ ///
+ /// FindAwardItem finds a list of all values of the given key(s).
+ ///
+ public List? FindAwardItem(uint id, string name) =>
+ _indexAwardItemMap.TryGetValue(new Index_AwardItemKey(id, name), out var value) ? value : null;
+
+ ///
+ /// FindFirstAwardItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstAwardItem(uint id, string name) =>
+ FindAwardItem(id, name)?.FirstOrDefault();
+
+ // Index: (ID,Type,Param,ExtType)@SpecialItem
+
+ ///
+ /// FindSpecialItemMap finds the index: key((ID,Type,Param,ExtType)@SpecialItem) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_SpecialItemMap FindSpecialItemMap() => ref _indexSpecialItemMap;
+
+ ///
+ /// FindSpecialItem finds a list of all values of the given key(s).
+ ///
+ public List? FindSpecialItem(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType) =>
+ _indexSpecialItemMap.TryGetValue(new Index_SpecialItemKey(id, type, param, extType), out var value) ? value : null;
+
+ ///
+ /// FindFirstSpecialItem finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstSpecialItem(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType) =>
+ FindSpecialItem(id, type, param, extType)?.FirstOrDefault();
+
+ // Index: PathDir@ItemPathDir
+
+ ///
+ /// FindItemPathDirMap finds the index: key(PathDir@ItemPathDir) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemPathDirMap FindItemPathDirMap() => ref _indexItemPathDirMap;
+
+ ///
+ /// FindItemPathDir finds a list of all values of the given key(s).
+ ///
+ public List? FindItemPathDir(string dir) =>
+ _indexItemPathDirMap.TryGetValue(dir, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemPathDir finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemPathDir(string dir) =>
+ FindItemPathDir(dir)?.FirstOrDefault();
+
+ // Index: PathName@ItemPathName
+
+ ///
+ /// FindItemPathNameMap finds the index: key(PathName@ItemPathName) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemPathNameMap FindItemPathNameMap() => ref _indexItemPathNameMap;
+
+ ///
+ /// FindItemPathName finds a list of all values of the given key(s).
+ ///
+ public List? FindItemPathName(string name) =>
+ _indexItemPathNameMap.TryGetValue(name, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemPathName finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemPathName(string name) =>
+ FindItemPathName(name)?.FirstOrDefault();
+
+ // Index: PathFriendID@ItemPathFriendID
+
+ ///
+ /// FindItemPathFriendIDMap finds the index: key(PathFriendID@ItemPathFriendID) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ItemPathFriendIDMap FindItemPathFriendIDMap() => ref _indexItemPathFriendIdMap;
+
+ ///
+ /// FindItemPathFriendID finds a list of all values of the given key(s).
+ ///
+ public List? FindItemPathFriendID(uint id) =>
+ _indexItemPathFriendIdMap.TryGetValue(id, out var value) ? value : null;
+
+ ///
+ /// FindFirstItemPathFriendID finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstItemPathFriendID(uint id) =>
+ FindItemPathFriendID(id)?.FirstOrDefault();
+
+ // Index: UseEffectType@UseEffectType
+
+ ///
+ /// FindUseEffectTypeMap finds the index: key(UseEffectType@UseEffectType) to value(Protoconf.ItemConf.Types.Item) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_UseEffectTypeMap FindUseEffectTypeMap() => ref _indexUseEffectTypeMap;
+
+ ///
+ /// FindUseEffectType finds a list of all values of the given key(s).
+ ///
+ public List? FindUseEffectType(Protoconf.UseEffect.Types.Type type) =>
+ _indexUseEffectTypeMap.TryGetValue(type, out var value) ? value : null;
+
+ ///
+ /// FindFirstUseEffectType finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstUseEffectType(Protoconf.UseEffect.Types.Type type) =>
+ FindUseEffectType(type)?.FirstOrDefault();
+
+ // OrderedIndex: ExtType@ExtType
+
+ ///
+ /// FindExtTypeMap finds the ordered index: key(ExtType@ExtType) to value(Protoconf.ItemConf.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ExtTypeMap FindExtTypeMap() => ref _orderedIndexExtTypeMap;
+
+ ///
+ /// FindExtType finds a list of all values of the given key(s).
+ ///
+ public List? FindExtType(Protoconf.FruitType extType) =>
+ _orderedIndexExtTypeMap.TryGetValue(extType, out var value) ? value : null;
+
+ ///
+ /// FindFirstExtType finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstExtType(Protoconf.FruitType extType) =>
+ FindExtType(extType)?.FirstOrDefault();
+
+ // OrderedIndex: (Param,ExtType)@ParamExtType
+
+ ///
+ /// FindParamExtTypeMap finds the ordered index: key((Param,ExtType)@ParamExtType) to value(Protoconf.ItemConf.Types.Item) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ParamExtTypeMap FindParamExtTypeMap() => ref _orderedIndexParamExtTypeMap;
+
+ ///
+ /// FindParamExtType finds a list of all values of the given key(s).
+ ///
+ public List? FindParamExtType(int param, Protoconf.FruitType extType) =>
+ _orderedIndexParamExtTypeMap.TryGetValue(new OrderedIndex_ParamExtTypeKey(param, extType), out var value) ? value : null;
+
+ ///
+ /// FindFirstParamExtType finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ItemConf.Types.Item? FindFirstParamExtType(int param, Protoconf.FruitType extType) =>
+ FindParamExtType(param, extType)?.FirstOrDefault();
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/Load.pc.cs b/test/csharp-tableau-loader/tableau/Load.pc.cs
new file mode 100644
index 00000000..e64ca47c
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/Load.pc.cs
@@ -0,0 +1,228 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+using pbr = global::Google.Protobuf.Reflection;
+namespace Tableau
+{
+ ///
+ /// Load provides functions for loading and parsing protobuf messages from files.
+ ///
+ public static class Load
+ {
+ ///
+ /// ReadFunc reads the config file and returns its content.
+ ///
+ public delegate byte[] ReadFunc(string path);
+ ///
+ /// LoadFunc defines a func which can load message's content based on the
+ /// given descriptor, path, format, and options.
+ ///
+ public delegate pb::IMessage? LoadFunc(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options);
+
+ ///
+ /// BaseOptions is the common options for both global-level and messager-level options.
+ ///
+ public class BaseOptions
+ {
+ ///
+ /// Whether to ignore unknown JSON fields during parsing.
+ ///
+ public bool? IgnoreUnknownFields { get; set; }
+ ///
+ /// You can specify custom read function to read a config file's content.
+ /// Default is File.ReadAllBytes.
+ ///
+ public ReadFunc? ReadFunc { get; set; }
+ ///
+ /// You can specify custom load function to load a messager's content.
+ /// Default is LoadMessager.
+ ///
+ public LoadFunc? LoadFunc { get; set; }
+ }
+
+ ///
+ /// Options is the global-level options, which contains both global-level and
+ /// messager-level options.
+ ///
+ public class Options : BaseOptions
+ {
+ ///
+ /// MessagerOptions maps each messager name to a MessagerOptions.
+ /// If specified, then the messager will be parsed with the given options directly.
+ ///
+ public Dictionary? MessagerOptions { get; set; }
+ ///
+ /// ParseMessagerOptionsByName parses messager options with both global-level and
+ /// messager-level options taken into consideration.
+ ///
+ public MessagerOptions ParseMessagerOptionsByName(string name)
+ {
+ var mopts = MessagerOptions?.TryGetValue(name, out var val) == true ? (MessagerOptions)val.Clone() : new MessagerOptions();
+ mopts.IgnoreUnknownFields ??= IgnoreUnknownFields;
+ mopts.ReadFunc ??= ReadFunc;
+ mopts.LoadFunc ??= LoadFunc;
+ return mopts;
+ }
+ }
+
+ ///
+ /// MessagerOptions defines the options for loading a messager.
+ ///
+ public class MessagerOptions : BaseOptions, ICloneable
+ {
+ ///
+ /// Path maps each messager name to a corresponding config file path.
+ /// If specified, then the main messager will be parsed from the file
+ /// directly, other than the specified load dir.
+ ///
+ public string? Path { get; set; }
+
+ public object Clone()
+ {
+ return new MessagerOptions
+ {
+ IgnoreUnknownFields = IgnoreUnknownFields,
+ ReadFunc = ReadFunc,
+ LoadFunc = LoadFunc,
+ Path = Path
+ };
+ }
+ }
+
+ ///
+ /// LoadMessager loads a protobuf message from the specified file path and format.
+ ///
+ public static pb::IMessage? LoadMessager(pbr::MessageDescriptor desc, string path, Format fmt, in MessagerOptions? options = null)
+ {
+ var readFunc = options?.ReadFunc ?? File.ReadAllBytes;
+ byte[] content;
+ try
+ {
+ content = readFunc(path);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to read {path}: {ex.Message}");
+ throw;
+ }
+ return Unmarshal(content, desc, fmt, options);
+ }
+
+ ///
+ /// LoadMessagerInDir loads a protobuf message from the specified directory and format.
+ /// It resolves the file path based on the message descriptor name.
+ ///
+ public static pb::IMessage? LoadMessagerInDir(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options = null)
+ {
+ string name = desc.Name;
+ string path = "";
+ if (options?.Path != null)
+ {
+ path = options.Path;
+ fmt = Util.GetFormat(path);
+ }
+ if (path == "")
+ {
+ string filename = name + Util.Format2Ext(fmt);
+ path = Path.Combine(dir, filename);
+ }
+ var loadFunc = options?.LoadFunc ?? LoadMessager;
+ return loadFunc(desc, path, fmt, options);
+ }
+
+ ///
+ /// Unmarshal parses the given byte content into a protobuf message based on the specified format.
+ ///
+ public static pb::IMessage? Unmarshal(byte[] content, pbr::MessageDescriptor desc, Format fmt, in MessagerOptions? options = null)
+ {
+ switch (fmt)
+ {
+ case Format.JSON:
+ try
+ {
+ var parser = new pb::JsonParser(
+ pb::JsonParser.Settings.Default.WithIgnoreUnknownFields(options?.IgnoreUnknownFields ?? false)
+ );
+ return parser.Parse(new StreamReader(new MemoryStream(content)), desc);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to parse {desc.Name}.json: {ex.Message}");
+ throw;
+ }
+ case Format.Bin:
+ try
+ {
+ return desc.Parser.ParseFrom(content);
+ }
+ catch (Exception ex)
+ {
+ Util.SetErrMsg($"failed to parse {desc.Name}.binpb: {ex.Message}");
+ throw;
+ }
+ default:
+ Util.SetErrMsg($"unknown format: {fmt}");
+ return null;
+ }
+ }
+ }
+
+ ///
+ /// IMessagerName is an interface that provides the static Name() method for messagers.
+ ///
+ public interface IMessagerName
+ {
+ static abstract string Name();
+ }
+
+ ///
+ /// Messager is the base class for all generated configuration messagers.
+ /// It is designed for three goals:
+ /// 1. Easy use: simple yet powerful accessors.
+ /// 2. Elegant API: concise and clean functions.
+ /// 3. Extensibility: Map, OrderedMap, Index, OrderedIndex...
+ ///
+ public abstract class Messager
+ {
+ ///
+ /// Stats contains statistics info about loading.
+ ///
+ public class Stats
+ {
+ /// Total load time consuming.
+ public TimeSpan Duration;
+ }
+
+ protected Stats LoadStats = new();
+
+ ///
+ /// GetStats returns the loading stats info.
+ ///
+ public ref readonly Stats GetStats() => ref LoadStats;
+
+ ///
+ /// Load fills message from file in the specified directory and format.
+ ///
+ public abstract bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null);
+
+ ///
+ /// Message returns the inner protobuf message data.
+ ///
+ public virtual pb::IMessage? Message() => null;
+
+ ///
+ /// ProcessAfterLoad is invoked after this messager loaded.
+ ///
+ protected virtual bool ProcessAfterLoad() => true;
+
+ ///
+ /// ProcessAfterLoadAll is invoked after all messagers loaded.
+ ///
+ public virtual bool ProcessAfterLoadAll(in Hub hub) => true;
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/PatchConf.pc.cs b/test/csharp-tableau-loader/tableau/PatchConf.pc.cs
new file mode 100644
index 00000000..59383a89
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/PatchConf.pc.cs
@@ -0,0 +1,190 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+// source: patch_conf.proto
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// PatchReplaceConf is a wrapper around protobuf message Protoconf.PatchReplaceConf.
+ ///
+ public class PatchReplaceConf : Messager, IMessagerName
+ {
+ private Protoconf.PatchReplaceConf _data = new();
+
+ ///
+ /// Name returns the PatchReplaceConf's message name.
+ ///
+ public static string Name() => Protoconf.PatchReplaceConf.Descriptor.Name;
+
+ ///
+ /// Load loads PatchReplaceConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.PatchReplaceConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.PatchReplaceConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load PatchReplaceConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the PatchReplaceConf's inner message data.
+ ///
+ public ref readonly Protoconf.PatchReplaceConf Data() => ref _data;
+
+ ///
+ /// Message returns the PatchReplaceConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+ }
+
+ ///
+ /// PatchMergeConf is a wrapper around protobuf message Protoconf.PatchMergeConf.
+ ///
+ public class PatchMergeConf : Messager, IMessagerName
+ {
+ private Protoconf.PatchMergeConf _data = new();
+
+ ///
+ /// Name returns the PatchMergeConf's message name.
+ ///
+ public static string Name() => Protoconf.PatchMergeConf.Descriptor.Name;
+
+ ///
+ /// Load loads PatchMergeConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.PatchMergeConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.PatchMergeConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load PatchMergeConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the PatchMergeConf's inner message data.
+ ///
+ public ref readonly Protoconf.PatchMergeConf Data() => ref _data;
+
+ ///
+ /// Message returns the PatchMergeConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Item? Get1(uint id) =>
+ _data.ItemMap?.TryGetValue(id, out var val) == true ? val : null;
+ }
+
+ ///
+ /// RecursivePatchConf is a wrapper around protobuf message Protoconf.RecursivePatchConf.
+ ///
+ public class RecursivePatchConf : Messager, IMessagerName
+ {
+ private Protoconf.RecursivePatchConf _data = new();
+
+ ///
+ /// Name returns the RecursivePatchConf's message name.
+ ///
+ public static string Name() => Protoconf.RecursivePatchConf.Descriptor.Name;
+
+ ///
+ /// Load loads RecursivePatchConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.RecursivePatchConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.RecursivePatchConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load RecursivePatchConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the RecursivePatchConf's inner message data.
+ ///
+ public ref readonly Protoconf.RecursivePatchConf Data() => ref _data;
+
+ ///
+ /// Message returns the RecursivePatchConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.RecursivePatchConf.Types.Shop? Get1(uint shopId) =>
+ _data.ShopMap?.TryGetValue(shopId, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.RecursivePatchConf.Types.Shop.Types.Goods? Get2(uint shopId, uint goodsId) =>
+ Get1(shopId)?.GoodsMap?.TryGetValue(goodsId, out var val) == true ? val : null;
+
+ ///
+ /// Get3 finds value in the 3rd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.RecursivePatchConf.Types.Shop.Types.Goods.Types.Currency? Get3(uint shopId, uint goodsId, uint type) =>
+ Get2(shopId, goodsId)?.CurrencyMap?.TryGetValue(type, out var val) == true ? val : null;
+
+ ///
+ /// Get4 finds value in the 4th-level map.
+ /// It will return null if the key is not found.
+ ///
+ public int? Get4(uint shopId, uint goodsId, uint type, int key4) =>
+ Get3(shopId, goodsId, type)?.ValueList?.TryGetValue(key4, out var val) == true ? val : null;
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/TestConf.pc.cs b/test/csharp-tableau-loader/tableau/TestConf.pc.cs
new file mode 100644
index 00000000..413d6cd7
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/TestConf.pc.cs
@@ -0,0 +1,889 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+// source: test_conf.proto
+//
+#nullable enable
+using pb = global::Google.Protobuf;
+namespace Tableau
+{
+ ///
+ /// ActivityConf is a wrapper around protobuf message Protoconf.ActivityConf.
+ ///
+ public class ActivityConf : Messager, IMessagerName
+ {
+ // OrderedMap types.
+ public class OrderedMap_int32Map : SortedDictionary;
+
+ public class OrderedMap_protoconf_SectionValue(OrderedMap_int32Map item1, Protoconf.Section item2)
+ : Tuple(item1, item2);
+ public class OrderedMap_protoconf_SectionMap : SortedDictionary;
+
+ public class OrderedMap_Activity_ChapterValue(OrderedMap_protoconf_SectionMap item1, Protoconf.ActivityConf.Types.Activity.Types.Chapter item2)
+ : Tuple(item1, item2);
+ public class OrderedMap_Activity_ChapterMap : SortedDictionary;
+
+ public class OrderedMap_ActivityValue(OrderedMap_Activity_ChapterMap item1, Protoconf.ActivityConf.Types.Activity item2)
+ : Tuple(item1, item2);
+ public class OrderedMap_ActivityMap : SortedDictionary;
+
+ private OrderedMap_ActivityMap _orderedMap = [];
+
+
+ // LevelIndex keys.
+ public readonly struct LevelIndex_Activity_ChapterKey : IEquatable
+ {
+ public ulong ActivityId { get; }
+ public uint ChapterId { get; }
+
+ public LevelIndex_Activity_ChapterKey(ulong activityId, uint chapterId)
+ {
+ ActivityId = activityId;
+ ChapterId = chapterId;
+ }
+
+ public bool Equals(LevelIndex_Activity_ChapterKey other) =>
+ (ActivityId, ChapterId).Equals((other.ActivityId, other.ChapterId));
+
+ public override int GetHashCode() =>
+ (ActivityId, ChapterId).GetHashCode();
+ }
+
+ // Index types.
+ // Index: ActivityName
+ public class Index_ActivityMap : Dictionary>;
+
+ private Index_ActivityMap _indexActivityMap = [];
+
+ // Index: ChapterID
+ public class Index_ChapterMap : Dictionary>;
+
+ private Index_ChapterMap _indexChapterMap = [];
+
+ private Dictionary _indexChapterMap1 = [];
+
+ // Index: ChapterName@NamedChapter
+ public class Index_NamedChapterMap : Dictionary>;
+
+ private Index_NamedChapterMap _indexNamedChapterMap = [];
+
+ private Dictionary _indexNamedChapterMap1 = [];
+
+ // Index: SectionItemID@Award
+ public class Index_AwardMap : Dictionary>;
+
+ private Index_AwardMap _indexAwardMap = [];
+
+ private Dictionary _indexAwardMap1 = [];
+
+ private Dictionary _indexAwardMap2 = [];
+
+ private Protoconf.ActivityConf _data = new();
+
+ ///
+ /// Name returns the ActivityConf's message name.
+ ///
+ public static string Name() => Protoconf.ActivityConf.Descriptor.Name;
+
+ ///
+ /// Load loads ActivityConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.ActivityConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.ActivityConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load ActivityConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the ActivityConf's inner message data.
+ ///
+ public ref readonly Protoconf.ActivityConf Data() => ref _data;
+
+ ///
+ /// Message returns the ActivityConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // OrderedMap init.
+ _orderedMap.Clear();
+ foreach (var (key1, value1) in _data.ActivityMap)
+ {
+ var ordered_map1 = new OrderedMap_Activity_ChapterMap();
+ foreach (var (key2, value2) in value1.ChapterMap)
+ {
+ var ordered_map2 = new OrderedMap_protoconf_SectionMap();
+ foreach (var (key3, value3) in value2.SectionMap)
+ {
+ var ordered_map3 = new OrderedMap_int32Map();
+ foreach (var (key4, value4) in value3.SectionRankMap)
+ {
+ ordered_map3[key4] = value4;
+ }
+ ordered_map2[key3] = new OrderedMap_protoconf_SectionValue(ordered_map3, value3);
+ }
+ ordered_map1[key2] = new OrderedMap_Activity_ChapterValue(ordered_map2, value2);
+ }
+ _orderedMap[key1] = new OrderedMap_ActivityValue(ordered_map1, value1);
+ }
+ // Index init.
+ _indexActivityMap.Clear();
+ _indexChapterMap.Clear();
+ _indexChapterMap1.Clear();
+ _indexNamedChapterMap.Clear();
+ _indexNamedChapterMap1.Clear();
+ _indexAwardMap.Clear();
+ _indexAwardMap1.Clear();
+ _indexAwardMap2.Clear();
+ foreach (var item1 in _data.ActivityMap)
+ {
+ var k1 = item1.Key;
+ {
+ // Index: ActivityName
+ var key = item1.Value.ActivityName;
+ {
+ var list = _indexActivityMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexActivityMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ foreach (var item2 in item1.Value.ChapterMap)
+ {
+ var k2 = item2.Key;
+ {
+ // Index: ChapterID
+ var key = item2.Value.ChapterId;
+ {
+ var list = _indexChapterMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexChapterMap[key] = [];
+ list.Add(item2.Value);
+ }
+ {
+ var map = _indexChapterMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexChapterMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item2.Value);
+ }
+ }
+ {
+ // Index: ChapterName@NamedChapter
+ var key = item2.Value.ChapterName;
+ {
+ var list = _indexNamedChapterMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexNamedChapterMap[key] = [];
+ list.Add(item2.Value);
+ }
+ {
+ var map = _indexNamedChapterMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexNamedChapterMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item2.Value);
+ }
+ }
+ foreach (var item3 in item2.Value.SectionMap)
+ {
+ foreach (var item4 in item3.Value.SectionItemList)
+ {
+ {
+ // Index: SectionItemID@Award
+ var key = item4.Id;
+ {
+ var list = _indexAwardMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexAwardMap[key] = [];
+ list.Add(item4);
+ }
+ {
+ var map = _indexAwardMap1.TryGetValue(k1, out var existingMap) ?
+ existingMap : _indexAwardMap1[k1] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item4);
+ }
+ {
+ var mapKey = new LevelIndex_Activity_ChapterKey(k1, k2);
+ var map = _indexAwardMap2.TryGetValue(mapKey, out var existingMap) ?
+ existingMap : _indexAwardMap2[mapKey] = [];
+ var list = map.TryGetValue(key, out var existingList) ?
+ existingList : map[key] = [];
+ list.Add(item4);
+ }
+ }
+ }
+ }
+ }
+ }
+ // Index(sort): ChapterName@NamedChapter
+ Comparison indexNamedChapterMapComparison = (a, b) =>
+ (a.AwardId).CompareTo((b.AwardId));
+ foreach (var itemList in _indexNamedChapterMap.Values)
+ {
+ itemList.Sort(indexNamedChapterMapComparison);
+ }
+ foreach (var itemDict in _indexNamedChapterMap1.Values)
+ {
+ foreach (var itemList in itemDict.Values)
+ {
+ itemList.Sort(indexNamedChapterMapComparison);
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity? Get1(ulong activityId) =>
+ _data.ActivityMap?.TryGetValue(activityId, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity.Types.Chapter? Get2(ulong activityId, uint chapterId) =>
+ Get1(activityId)?.ChapterMap?.TryGetValue(chapterId, out var val) == true ? val : null;
+
+ ///
+ /// Get3 finds value in the 3rd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.Section? Get3(ulong activityId, uint chapterId, uint sectionId) =>
+ Get2(activityId, chapterId)?.SectionMap?.TryGetValue(sectionId, out var val) == true ? val : null;
+
+ ///
+ /// Get4 finds value in the 4th-level map.
+ /// It will return null if the key is not found.
+ ///
+ public int? Get4(ulong activityId, uint chapterId, uint sectionId, uint key4) =>
+ Get3(activityId, chapterId, sectionId)?.SectionRankMap?.TryGetValue(key4, out var val) == true ? val : null;
+
+ // OrderedMap accessers.
+ ///
+ /// GetOrderedMap returns the 1st-level ordered map.
+ ///
+ public ref readonly OrderedMap_ActivityMap GetOrderedMap() => ref _orderedMap;
+
+ ///
+ /// GetOrderedMap1 finds value in the 1st-level ordered map.
+ /// It will return null if the key is not found.
+ ///
+ public OrderedMap_Activity_ChapterMap? GetOrderedMap1(ulong activityId) =>
+ _orderedMap.TryGetValue(activityId, out var value) ? value.Item1 : null;
+
+ ///
+ /// GetOrderedMap2 finds value in the 2nd-level ordered map.
+ /// It will return null if the key is not found.
+ ///
+ public OrderedMap_protoconf_SectionMap? GetOrderedMap2(ulong activityId, uint chapterId) =>
+ GetOrderedMap1(activityId)?.TryGetValue(chapterId, out var value) == true ? value.Item1 : null;
+
+ ///
+ /// GetOrderedMap3 finds value in the 3rd-level ordered map.
+ /// It will return null if the key is not found.
+ ///
+ public OrderedMap_int32Map? GetOrderedMap3(ulong activityId, uint chapterId, uint sectionId) =>
+ GetOrderedMap2(activityId, chapterId)?.TryGetValue(sectionId, out var value) == true ? value.Item1 : null;
+
+ // Index: ActivityName
+
+ ///
+ /// FindActivityMap finds the index: key(ActivityName) to value(Protoconf.ActivityConf.Types.Activity) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ActivityMap FindActivityMap() => ref _indexActivityMap;
+
+ ///
+ /// FindActivity finds a list of all values of the given key(s).
+ ///
+ public List? FindActivity(string activityName) =>
+ _indexActivityMap.TryGetValue(activityName, out var value) ? value : null;
+
+ ///
+ /// FindFirstActivity finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity? FindFirstActivity(string activityName) =>
+ FindActivity(activityName)?.FirstOrDefault();
+
+ // Index: ChapterID
+
+ ///
+ /// FindChapterMap finds the index: key(ChapterID) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_ChapterMap FindChapterMap() => ref _indexChapterMap;
+
+ ///
+ /// FindChapter finds a list of all values of the given key(s).
+ ///
+ public List? FindChapter(uint chapterId) =>
+ _indexChapterMap.TryGetValue(chapterId, out var value) ? value : null;
+
+ ///
+ /// FindFirstChapter finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstChapter(uint chapterId) =>
+ FindChapter(chapterId)?.FirstOrDefault();
+
+ ///
+ /// FindChapterMap1 finds the index: key(ChapterID) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter),
+ /// which is the upper 1st-level map specified by (activityId).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_ChapterMap? FindChapterMap1(ulong activityId) =>
+ _indexChapterMap1.TryGetValue(activityId, out var value) ? value : null;
+
+ ///
+ /// FindChapter1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (activityId).
+ ///
+ public List? FindChapter1(ulong activityId, uint chapterId) =>
+ FindChapterMap1(activityId)?.TryGetValue(chapterId, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstChapter1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (activityId), or null if no value found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstChapter1(ulong activityId, uint chapterId) =>
+ FindChapter1(activityId, chapterId)?.FirstOrDefault();
+
+ // Index: ChapterName@NamedChapter
+
+ ///
+ /// FindNamedChapterMap finds the index: key(ChapterName@NamedChapter) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_NamedChapterMap FindNamedChapterMap() => ref _indexNamedChapterMap;
+
+ ///
+ /// FindNamedChapter finds a list of all values of the given key(s).
+ ///
+ public List? FindNamedChapter(string chapterName) =>
+ _indexNamedChapterMap.TryGetValue(chapterName, out var value) ? value : null;
+
+ ///
+ /// FindFirstNamedChapter finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstNamedChapter(string chapterName) =>
+ FindNamedChapter(chapterName)?.FirstOrDefault();
+
+ ///
+ /// FindNamedChapterMap1 finds the index: key(ChapterName@NamedChapter) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter),
+ /// which is the upper 1st-level map specified by (activityId).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_NamedChapterMap? FindNamedChapterMap1(ulong activityId) =>
+ _indexNamedChapterMap1.TryGetValue(activityId, out var value) ? value : null;
+
+ ///
+ /// FindNamedChapter1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (activityId).
+ ///
+ public List? FindNamedChapter1(ulong activityId, string chapterName) =>
+ FindNamedChapterMap1(activityId)?.TryGetValue(chapterName, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstNamedChapter1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (activityId), or null if no value found.
+ ///
+ public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstNamedChapter1(ulong activityId, string chapterName) =>
+ FindNamedChapter1(activityId, chapterName)?.FirstOrDefault();
+
+ // Index: SectionItemID@Award
+
+ ///
+ /// FindAwardMap finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_AwardMap FindAwardMap() => ref _indexAwardMap;
+
+ ///
+ /// FindAward finds a list of all values of the given key(s).
+ ///
+ public List? FindAward(uint id) =>
+ _indexAwardMap.TryGetValue(id, out var value) ? value : null;
+
+ ///
+ /// FindFirstAward finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.Section.Types.SectionItem? FindFirstAward(uint id) =>
+ FindAward(id)?.FirstOrDefault();
+
+ ///
+ /// FindAwardMap1 finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem),
+ /// which is the upper 1st-level map specified by (activityId).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_AwardMap? FindAwardMap1(ulong activityId) =>
+ _indexAwardMap1.TryGetValue(activityId, out var value) ? value : null;
+
+ ///
+ /// FindAward1 finds a list of all values of the given key(s) in the upper 1st-level map
+ /// specified by (activityId).
+ ///
+ public List? FindAward1(ulong activityId, uint id) =>
+ FindAwardMap1(activityId)?.TryGetValue(id, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstAward1 finds the first value of the given key(s) in the upper 1st-level map
+ /// specified by (activityId), or null if no value found.
+ ///
+ public Protoconf.Section.Types.SectionItem? FindFirstAward1(ulong activityId, uint id) =>
+ FindAward1(activityId, id)?.FirstOrDefault();
+
+ ///
+ /// FindAwardMap2 finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem),
+ /// which is the upper 2nd-level map specified by (activityId, chapterId).
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public Index_AwardMap? FindAwardMap2(ulong activityId, uint chapterId) =>
+ _indexAwardMap2.TryGetValue(new LevelIndex_Activity_ChapterKey(activityId, chapterId), out var value) ? value : null;
+
+ ///
+ /// FindAward2 finds a list of all values of the given key(s) in the upper 2nd-level map
+ /// specified by (activityId, chapterId).
+ ///
+ public List? FindAward2(ulong activityId, uint chapterId, uint id) =>
+ FindAwardMap2(activityId, chapterId)?.TryGetValue(id, out var value) == true ? value : null;
+
+ ///
+ /// FindFirstAward2 finds the first value of the given key(s) in the upper 2nd-level map
+ /// specified by (activityId, chapterId), or null if no value found.
+ ///
+ public Protoconf.Section.Types.SectionItem? FindFirstAward2(ulong activityId, uint chapterId, uint id) =>
+ FindAward2(activityId, chapterId, id)?.FirstOrDefault();
+ }
+
+ ///
+ /// ChapterConf is a wrapper around protobuf message Protoconf.ChapterConf.
+ ///
+ public class ChapterConf : Messager, IMessagerName
+ {
+ private Protoconf.ChapterConf _data = new();
+
+ ///
+ /// Name returns the ChapterConf's message name.
+ ///
+ public static string Name() => Protoconf.ChapterConf.Descriptor.Name;
+
+ ///
+ /// Load loads ChapterConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.ChapterConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.ChapterConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load ChapterConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the ChapterConf's inner message data.
+ ///
+ public ref readonly Protoconf.ChapterConf Data() => ref _data;
+
+ ///
+ /// Message returns the ChapterConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.ChapterConf.Types.Chapter? Get1(ulong id) =>
+ _data.ChapterMap?.TryGetValue(id, out var val) == true ? val : null;
+ }
+
+ ///
+ /// ThemeConf is a wrapper around protobuf message Protoconf.ThemeConf.
+ ///
+ public class ThemeConf : Messager, IMessagerName
+ {
+ private Protoconf.ThemeConf _data = new();
+
+ ///
+ /// Name returns the ThemeConf's message name.
+ ///
+ public static string Name() => Protoconf.ThemeConf.Descriptor.Name;
+
+ ///
+ /// Load loads ThemeConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.ThemeConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.ThemeConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load ThemeConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the ThemeConf's inner message data.
+ ///
+ public ref readonly Protoconf.ThemeConf Data() => ref _data;
+
+ ///
+ /// Message returns the ThemeConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.ThemeConf.Types.Theme? Get1(string name) =>
+ _data.ThemeMap?.TryGetValue(name, out var val) == true ? val : null;
+
+ ///
+ /// Get2 finds value in the 2nd-level map.
+ /// It will return null if the key is not found.
+ ///
+ public string? Get2(string name, string param) =>
+ Get1(name)?.ParamMap?.TryGetValue(param, out var val) == true ? val : null;
+ }
+
+ ///
+ /// TaskConf is a wrapper around protobuf message Protoconf.TaskConf.
+ ///
+ public class TaskConf : Messager, IMessagerName
+ {
+ // Index types.
+ // Index: ActivityID
+ public class Index_TaskMap : Dictionary>;
+
+ private Index_TaskMap _indexTaskMap = [];
+
+ // OrderedIndex types.
+ // OrderedIndex: Goal@OrderedTask
+ public class OrderedIndex_OrderedTaskMap : SortedDictionary>;
+
+ private OrderedIndex_OrderedTaskMap _orderedIndexOrderedTaskMap = [];
+
+ // OrderedIndex: Expiry@TaskExpiry
+ public class OrderedIndex_TaskExpiryMap : SortedDictionary>;
+
+ private OrderedIndex_TaskExpiryMap _orderedIndexTaskExpiryMap = [];
+
+ // OrderedIndex: Expiry@SortedTaskExpiry
+ public class OrderedIndex_SortedTaskExpiryMap : SortedDictionary>;
+
+ private OrderedIndex_SortedTaskExpiryMap _orderedIndexSortedTaskExpiryMap = [];
+
+ // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry
+ public readonly struct OrderedIndex_ActivityExpiryKey : IComparable
+ {
+ public long Expiry { get; }
+ public long ActivityId { get; }
+
+ public OrderedIndex_ActivityExpiryKey(long expiry, long activityId)
+ {
+ Expiry = expiry;
+ ActivityId = activityId;
+ }
+
+ public int CompareTo(OrderedIndex_ActivityExpiryKey other) =>
+ (Expiry, ActivityId).CompareTo((other.Expiry, other.ActivityId));
+ }
+
+ public class OrderedIndex_ActivityExpiryMap : SortedDictionary>;
+
+ private OrderedIndex_ActivityExpiryMap _orderedIndexActivityExpiryMap = [];
+
+ private Protoconf.TaskConf _data = new();
+
+ ///
+ /// Name returns the TaskConf's message name.
+ ///
+ public static string Name() => Protoconf.TaskConf.Descriptor.Name;
+
+ ///
+ /// Load loads TaskConf's content in the given dir, based on format and messager options.
+ ///
+ public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)
+ {
+ var start = DateTime.Now;
+ try
+ {
+ _data = (Protoconf.TaskConf)(
+ Tableau.Load.LoadMessagerInDir(Protoconf.TaskConf.Descriptor, dir, fmt, options)
+ ?? throw new InvalidOperationException()
+ );
+ }
+ catch (Exception ex)
+ {
+ if (string.IsNullOrEmpty(Util.GetErrMsg()))
+ {
+ Util.SetErrMsg($"failed to load TaskConf: {ex.Message}");
+ }
+ return false;
+ }
+ LoadStats.Duration = DateTime.Now - start;
+ return ProcessAfterLoad();
+ }
+
+ ///
+ /// Data returns the TaskConf's inner message data.
+ ///
+ public ref readonly Protoconf.TaskConf Data() => ref _data;
+
+ ///
+ /// Message returns the TaskConf's inner message data.
+ ///
+ public override pb::IMessage? Message() => _data;
+
+ ///
+ /// ProcessAfterLoad runs after this messager is loaded.
+ ///
+ protected override bool ProcessAfterLoad()
+ {
+ // Index init.
+ _indexTaskMap.Clear();
+ foreach (var item1 in _data.TaskMap)
+ {
+ {
+ // Index: ActivityID
+ var key = item1.Value.ActivityId;
+ {
+ var list = _indexTaskMap.TryGetValue(key, out var existingList) ?
+ existingList : _indexTaskMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ // Index(sort): ActivityID
+ Comparison indexTaskMapComparison = (a, b) =>
+ (a.Goal, a.Id).CompareTo((b.Goal, b.Id));
+ foreach (var itemList in _indexTaskMap.Values)
+ {
+ itemList.Sort(indexTaskMapComparison);
+ }
+ // OrderedIndex init.
+ _orderedIndexOrderedTaskMap.Clear();
+ _orderedIndexTaskExpiryMap.Clear();
+ _orderedIndexSortedTaskExpiryMap.Clear();
+ _orderedIndexActivityExpiryMap.Clear();
+ foreach (var item1 in _data.TaskMap)
+ {
+ {
+ // OrderedIndex: Goal@OrderedTask
+ var key = item1.Value.Goal;
+ {
+ var list = _orderedIndexOrderedTaskMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexOrderedTaskMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // OrderedIndex: Expiry@TaskExpiry
+ var key = item1.Value.Expiry?.Seconds ?? 0;
+ {
+ var list = _orderedIndexTaskExpiryMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexTaskExpiryMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // OrderedIndex: Expiry@SortedTaskExpiry
+ var key = item1.Value.Expiry?.Seconds ?? 0;
+ {
+ var list = _orderedIndexSortedTaskExpiryMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexSortedTaskExpiryMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ {
+ // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry
+ var key = new OrderedIndex_ActivityExpiryKey(item1.Value.Expiry?.Seconds ?? 0, item1.Value.ActivityId);
+ {
+ var list = _orderedIndexActivityExpiryMap.TryGetValue(key, out var existingList) ?
+ existingList : _orderedIndexActivityExpiryMap[key] = [];
+ list.Add(item1.Value);
+ }
+ }
+ }
+ // OrderedIndex(sort): Goal@OrderedTask
+ Comparison orderedIndexOrderedTaskMapComparison = (a, b) =>
+ (a.Id).CompareTo((b.Id));
+ foreach (var itemList in _orderedIndexOrderedTaskMap.Values)
+ {
+ itemList.Sort(orderedIndexOrderedTaskMapComparison);
+ }
+ // OrderedIndex(sort): Expiry@SortedTaskExpiry
+ Comparison orderedIndexSortedTaskExpiryMapComparison = (a, b) =>
+ (a.Goal, a.Id).CompareTo((b.Goal, b.Id));
+ foreach (var itemList in _orderedIndexSortedTaskExpiryMap.Values)
+ {
+ itemList.Sort(orderedIndexSortedTaskExpiryMapComparison);
+ }
+ return true;
+ }
+
+ ///
+ /// Get1 finds value in the 1st-level map.
+ /// It will return null if the key is not found.
+ ///
+ public Protoconf.TaskConf.Types.Task? Get1(long id) =>
+ _data.TaskMap?.TryGetValue(id, out var val) == true ? val : null;
+
+ // Index: ActivityID
+
+ ///
+ /// FindTaskMap finds the index: key(ActivityID) to value(Protoconf.TaskConf.Types.Task) map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly Index_TaskMap FindTaskMap() => ref _indexTaskMap;
+
+ ///
+ /// FindTask finds a list of all values of the given key(s).
+ ///
+ public List? FindTask(long activityId) =>
+ _indexTaskMap.TryGetValue(activityId, out var value) ? value : null;
+
+ ///
+ /// FindFirstTask finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.TaskConf.Types.Task? FindFirstTask(long activityId) =>
+ FindTask(activityId)?.FirstOrDefault();
+
+ // OrderedIndex: Goal@OrderedTask
+
+ ///
+ /// FindOrderedTaskMap finds the ordered index: key(Goal@OrderedTask) to value(Protoconf.TaskConf.Types.Task) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_OrderedTaskMap FindOrderedTaskMap() => ref _orderedIndexOrderedTaskMap;
+
+ ///
+ /// FindOrderedTask finds a list of all values of the given key(s).
+ ///
+ public List? FindOrderedTask(long goal) =>
+ _orderedIndexOrderedTaskMap.TryGetValue(goal, out var value) ? value : null;
+
+ ///
+ /// FindFirstOrderedTask finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.TaskConf.Types.Task? FindFirstOrderedTask(long goal) =>
+ FindOrderedTask(goal)?.FirstOrDefault();
+
+ // OrderedIndex: Expiry@TaskExpiry
+
+ ///
+ /// FindTaskExpiryMap finds the ordered index: key(Expiry@TaskExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_TaskExpiryMap FindTaskExpiryMap() => ref _orderedIndexTaskExpiryMap;
+
+ ///
+ /// FindTaskExpiry finds a list of all values of the given key(s).
+ ///
+ public List? FindTaskExpiry(long expiry) =>
+ _orderedIndexTaskExpiryMap.TryGetValue(expiry, out var value) ? value : null;
+
+ ///
+ /// FindFirstTaskExpiry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.TaskConf.Types.Task? FindFirstTaskExpiry(long expiry) =>
+ FindTaskExpiry(expiry)?.FirstOrDefault();
+
+ // OrderedIndex: Expiry@SortedTaskExpiry
+
+ ///
+ /// FindSortedTaskExpiryMap finds the ordered index: key(Expiry@SortedTaskExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_SortedTaskExpiryMap FindSortedTaskExpiryMap() => ref _orderedIndexSortedTaskExpiryMap;
+
+ ///
+ /// FindSortedTaskExpiry finds a list of all values of the given key(s).
+ ///
+ public List? FindSortedTaskExpiry(long expiry) =>
+ _orderedIndexSortedTaskExpiryMap.TryGetValue(expiry, out var value) ? value : null;
+
+ ///
+ /// FindFirstSortedTaskExpiry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.TaskConf.Types.Task? FindFirstSortedTaskExpiry(long expiry) =>
+ FindSortedTaskExpiry(expiry)?.FirstOrDefault();
+
+ // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry
+
+ ///
+ /// FindActivityExpiryMap finds the ordered index: key((Expiry,ActivityID)@ActivityExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map.
+ /// One key may correspond to multiple values, which are represented by a list.
+ ///
+ public ref readonly OrderedIndex_ActivityExpiryMap FindActivityExpiryMap() => ref _orderedIndexActivityExpiryMap;
+
+ ///
+ /// FindActivityExpiry finds a list of all values of the given key(s).
+ ///
+ public List? FindActivityExpiry(long expiry, long activityId) =>
+ _orderedIndexActivityExpiryMap.TryGetValue(new OrderedIndex_ActivityExpiryKey(expiry, activityId), out var value) ? value : null;
+
+ ///
+ /// FindFirstActivityExpiry finds the first value of the given key(s),
+ /// or null if no value found.
+ ///
+ public Protoconf.TaskConf.Types.Task? FindFirstActivityExpiry(long expiry, long activityId) =>
+ FindActivityExpiry(expiry, activityId)?.FirstOrDefault();
+ }
+}
diff --git a/test/csharp-tableau-loader/tableau/Util.pc.cs b/test/csharp-tableau-loader/tableau/Util.pc.cs
new file mode 100644
index 00000000..d522550f
--- /dev/null
+++ b/test/csharp-tableau-loader/tableau/Util.pc.cs
@@ -0,0 +1,68 @@
+//
+// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.
+// versions:
+// - protoc-gen-csharp-tableau-loader v0.1.0
+// - protoc v3.19.3
+//
+#nullable enable
+namespace Tableau
+{
+ ///
+ /// Format specifies the format of the configuration file.
+ ///
+ public enum Format
+ {
+ Unknown,
+ JSON,
+ Bin
+ }
+
+ ///
+ /// Util provides common utility functions.
+ ///
+ public static class Util
+ {
+ [ThreadStatic] private static string? _errMsg;
+
+ ///
+ /// Get the last error message.
+ ///
+ public static string GetErrMsg() => _errMsg ?? "";
+
+ ///
+ /// Set the error message.
+ ///
+ public static void SetErrMsg(string msg) => _errMsg = msg;
+
+ private const string _unknownExt = ".unknown";
+ private const string _jsonExt = ".json";
+ private const string _binExt = ".binpb";
+
+ ///
+ /// GetFormat returns the Format type determined by the file extension of the given path.
+ ///
+ public static Format GetFormat(string path)
+ {
+ string ext = Path.GetExtension(path);
+ return ext switch
+ {
+ _jsonExt => Format.JSON,
+ _binExt => Format.Bin,
+ _ => Format.Unknown,
+ };
+ }
+
+ ///
+ /// Format2Ext returns the file extension corresponding to the given format.
+ ///
+ public static string Format2Ext(Format fmt)
+ {
+ return fmt switch
+ {
+ Format.JSON => _jsonExt,
+ Format.Bin => _binExt,
+ _ => _unknownExt,
+ };
+ }
+ }
+}
diff --git a/test/proto/hero_conf.proto b/test/proto/hero_conf.proto
index 2437a682..ba636f4b 100644
--- a/test/proto/hero_conf.proto
+++ b/test/proto/hero_conf.proto
@@ -16,7 +16,7 @@ message HeroConf {
ordered_map: true
index: "Title"
lang_options: { key: "Index" value: "go" }
- lang_options: { key: "OrderedMap" value: "cpp" }
+ lang_options: { key: "OrderedMap" value: "cpp cs" }
};
map hero_map = 1 [(tableau.field) = { key: "Name" layout: LAYOUT_VERTICAL }];
message Hero {
diff --git a/test/testdata/bin/HeroConf.binpb b/test/testdata/bin/HeroConf.binpb
new file mode 100644
index 00000000..58e8445a
--- /dev/null
+++ b/test/testdata/bin/HeroConf.binpb
@@ -0,0 +1,11 @@
+
++
+venus"
+venus
+title2
+title2attr2
+)
+zeus!
+zeus
+title1
+title1attr1
\ No newline at end of file
diff --git a/test/testdata/conf/ItemConf.json b/test/testdata/conf/ItemConf.json
index a2b6e2c5..bc09a013 100644
--- a/test/testdata/conf/ItemConf.json
+++ b/test/testdata/conf/ItemConf.json
@@ -14,6 +14,11 @@
},
"expiry": "2021-01-01T18:00:00Z",
"type": "FRUIT_TYPE_APPLE",
+ "param_list": [
+ 1,
+ 2,
+ 3
+ ],
"extTypeList": [
"FRUIT_TYPE_APPLE",
"FRUIT_TYPE_ORANGE"
@@ -29,7 +34,12 @@
]
},
"expiry": "2022-01-01T08:00:00Z",
- "type": "FRUIT_TYPE_ORANGE"
+ "type": "FRUIT_TYPE_ORANGE",
+ "param_list": [
+ 4,
+ 5,
+ 6
+ ]
},
"3": {
"id": 3,
@@ -40,7 +50,15 @@
"icon.png"
]
},
- "expiry": "2023-01-01T08:00:00Z"
+ "expiry": "2023-01-01T08:00:00Z",
+ "param_list": [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6
+ ]
},
"2001": {
"id": 2001,
From 77863b50f899c538e03f02337fc275ed2127e3da Mon Sep 17 00:00:00 2001
From: Kybxd <627940450@qq.com>
Date: Fri, 13 Mar 2026 14:45:04 +0800
Subject: [PATCH 2/6] feat: add GitHub Actions workflows for release and
testing of C# loader
---
.github/workflows/release-csharp.yml | 62 +++++++++++++++++
.github/workflows/testing-csharp.yml | 69 +++++++++++++++++++
.../csharp-tableau-loader.sln | 24 -------
3 files changed, 131 insertions(+), 24 deletions(-)
create mode 100644 .github/workflows/release-csharp.yml
create mode 100644 .github/workflows/testing-csharp.yml
delete mode 100644 test/csharp-tableau-loader/csharp-tableau-loader.sln
diff --git a/.github/workflows/release-csharp.yml b/.github/workflows/release-csharp.yml
new file mode 100644
index 00000000..aa718903
--- /dev/null
+++ b/.github/workflows/release-csharp.yml
@@ -0,0 +1,62 @@
+name: Release C#
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+jobs:
+ release:
+ name: Release cmd/protoc-gen-csharp-tableau-loader
+ runs-on: ubuntu-latest
+ if: startsWith(github.event.release.tag_name, 'cmd/protoc-gen-csharp-tableau-loader/')
+ strategy:
+ matrix:
+ goos: [linux, darwin, windows]
+ goarch: [amd64, arm64]
+ exclude:
+ - goos: linux
+ goarch: arm64
+ - goos: windows
+ goarch: arm64
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.21.x"
+
+ - name: Download dependencies
+ run: |
+ cd cmd/protoc-gen-csharp-tableau-loader
+ go mod download
+ - name: Prepare build directory
+ run: |
+ mkdir -p build/
+ cp README.md build/
+ cp LICENSE build/
+ - name: Build
+ env:
+ GOOS: ${{ matrix.goos }}
+ GOARCH: ${{ matrix.goarch }}
+ run: |
+ cd cmd/protoc-gen-csharp-tableau-loader
+ go build -trimpath -o $GITHUB_WORKSPACE/build
+ - name: Create package
+ id: package
+ run: |
+ PACKAGE_NAME=protoc-gen-csharp-tableau-loader.${GITHUB_REF#refs/tags/cmd/protoc-gen-csharp-tableau-loader/}.${{ matrix.goos }}.${{ matrix.goarch }}.tar.gz
+ tar -czvf $PACKAGE_NAME -C build .
+ echo ::set-output name=name::${PACKAGE_NAME}
+ - name: Upload asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ github.event.release.upload_url }}
+ asset_path: ./${{ steps.package.outputs.name }}
+ asset_name: ${{ steps.package.outputs.name }}
+ asset_content_type: application/gzip
diff --git a/.github/workflows/testing-csharp.yml b/.github/workflows/testing-csharp.yml
new file mode 100644
index 00000000..b2c5fd5c
--- /dev/null
+++ b/.github/workflows/testing-csharp.yml
@@ -0,0 +1,69 @@
+name: Testing C#
+
+# Trigger on pushes, PRs (excluding documentation changes), and nightly.
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+ schedule:
+ - cron: 0 0 * * * # daily at 00:00
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-latest
+ gen_script: bash gen.sh
+ - os: windows-latest
+ gen_script: cmd /c gen.bat
+
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v6
+
+ - name: Install Go
+ uses: actions/setup-go@v6
+ with:
+ go-version: 1.21.x
+ cache: true
+
+ - name: Install .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Install dependencies (Ubuntu)
+ if: runner.os == 'Linux'
+ run: sudo apt-get update && sudo apt-get install -y cmake ninja-build
+
+ - name: Install dependencies (Windows)
+ if: runner.os == 'Windows'
+ run: choco install cmake ninja -y
+
+ - name: Setup MSVC (Windows)
+ if: runner.os == 'Windows'
+ uses: ilammy/msvc-dev-cmd@v1
+
+ - name: Init submodules and build protobuf
+ shell: bash
+ run: bash init.sh
+
+ - name: Generate protoconf
+ working-directory: test/csharp-tableau-loader
+ run: ${{ matrix.gen_script }}
+
+ - name: Build
+ working-directory: test/csharp-tableau-loader
+ run: dotnet build
+
+ - name: Run loader
+ working-directory: test/csharp-tableau-loader
+ run: dotnet run
diff --git a/test/csharp-tableau-loader/csharp-tableau-loader.sln b/test/csharp-tableau-loader/csharp-tableau-loader.sln
deleted file mode 100644
index d31764f3..00000000
--- a/test/csharp-tableau-loader/csharp-tableau-loader.sln
+++ /dev/null
@@ -1,24 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.5.2.0
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Loader", "Loader.csproj", "{E8053C8C-98ED-7494-D874-8E12A72FA028}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {E8053C8C-98ED-7494-D874-8E12A72FA028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E8053C8C-98ED-7494-D874-8E12A72FA028}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E8053C8C-98ED-7494-D874-8E12A72FA028}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E8053C8C-98ED-7494-D874-8E12A72FA028}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {80988C2E-15FF-4FCD-B5F0-A75A1AD74321}
- EndGlobalSection
-EndGlobal
From 2609033bff96d45936bd720945113ea006582cc5 Mon Sep 17 00:00:00 2001
From: Kybxd <627940450@qq.com>
Date: Fri, 13 Mar 2026 19:43:16 +0800
Subject: [PATCH 3/6] fix: format shell scripts
---
test/csharp-tableau-loader/gen.sh | 48 +++++++++++++++----------------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/test/csharp-tableau-loader/gen.sh b/test/csharp-tableau-loader/gen.sh
index 95826453..7e3d06a9 100644
--- a/test/csharp-tableau-loader/gen.sh
+++ b/test/csharp-tableau-loader/gen.sh
@@ -10,25 +10,25 @@ cd "$(git rev-parse --show-toplevel)"
# Default to locally compiled protoc for local development; fallback to system protoc.
LOCAL_PROTOC="./third_party/_submodules/protobuf/cmake/build/protoc"
if [ -z "$PROTOC" ]; then
- if [ -x "$LOCAL_PROTOC" ]; then
- PROTOC="$LOCAL_PROTOC"
- else
- PROTOC="$(which protoc 2>/dev/null || true)"
- fi
+ if [ -x "$LOCAL_PROTOC" ]; then
+ PROTOC="$LOCAL_PROTOC"
+ else
+ PROTOC="$(which protoc 2>/dev/null || true)"
+ fi
fi
if [ -z "$PROTOC" ]; then
- echo "Error: protoc not found. Please build protobuf submodule or install protoc." >&2
- exit 1
+ echo "Error: protoc not found. Please build protobuf submodule or install protoc." >&2
+ exit 1
fi
# Allow overriding protobuf include path via environment variable.
# Default to local submodule source; fallback to system include path.
LOCAL_PROTOBUF_PROTO="./third_party/_submodules/protobuf/src"
if [ -z "$PROTOBUF_PROTO" ]; then
- if [ -d "$LOCAL_PROTOBUF_PROTO/google/protobuf" ]; then
- PROTOBUF_PROTO="$LOCAL_PROTOBUF_PROTO"
- else
- PROTOBUF_PROTO="$(pkg-config --variable=includedir protobuf 2>/dev/null || echo /usr/include)"
- fi
+ if [ -d "$LOCAL_PROTOBUF_PROTO/google/protobuf" ]; then
+ PROTOBUF_PROTO="$LOCAL_PROTOBUF_PROTO"
+ else
+ PROTOBUF_PROTO="$(pkg-config --variable=includedir protobuf 2>/dev/null || echo /usr/include)"
+ fi
fi
TABLEAU_PROTO="./third_party/_submodules/tableau/proto"
ROOTDIR="./test/csharp-tableau-loader"
@@ -50,13 +50,13 @@ export PATH="$(pwd)/${PLUGIN_DIR}:${PATH}"
PROTO_FILES=$(find "$PROTOCONF_IN" -name "*.proto")
${PROTOC} \
- --csharp_out="$PROTOCONF_OUT" \
- --csharp-tableau-loader_out="$LOADER_OUT" \
- --csharp-tableau-loader_opt=paths=source_relative \
- --proto_path="$PROTOBUF_PROTO" \
- --proto_path="$TABLEAU_PROTO" \
- --proto_path="$PROTOCONF_IN" \
- $PROTO_FILES
+ --csharp_out="$PROTOCONF_OUT" \
+ --csharp-tableau-loader_out="$LOADER_OUT" \
+ --csharp-tableau-loader_opt=paths=source_relative \
+ --proto_path="$PROTOBUF_PROTO" \
+ --proto_path="$TABLEAU_PROTO" \
+ --proto_path="$PROTOCONF_IN" \
+ $PROTO_FILES
TABLEAU_IN="$TABLEAU_PROTO/tableau/protobuf"
TABLEAU_OUT="${ROOTDIR}/protoconf/tableau"
@@ -65,8 +65,8 @@ rm -rfv "$TABLEAU_OUT"
mkdir -p "$TABLEAU_OUT"
${PROTOC} \
- --csharp_out="$TABLEAU_OUT" \
- --proto_path="$PROTOBUF_PROTO" \
- --proto_path="$TABLEAU_PROTO" \
- "$TABLEAU_IN/tableau.proto" \
- "$TABLEAU_IN/wellknown.proto"
+ --csharp_out="$TABLEAU_OUT" \
+ --proto_path="$PROTOBUF_PROTO" \
+ --proto_path="$TABLEAU_PROTO" \
+ "$TABLEAU_IN/tableau.proto" \
+ "$TABLEAU_IN/wellknown.proto"
From 3a0a7ea645d079b927b46b31cbebb29341d21ee1 Mon Sep 17 00:00:00 2001
From: wenchy
Date: Sun, 15 Mar 2026 22:38:47 +0800
Subject: [PATCH 4/6] fix: make generated code compliant with Unity 2022.3 LTS
(C# 9)
---
README.md | 1 +
.../embed/Load.pc.cs | 4 +-
.../embed/templates/Hub.pc.cs.tpl | 42 ++++---
.../indexes/index.go | 17 +--
.../indexes/ordered_index.go | 17 +--
.../messager.go | 2 +-
.../orderedmap/ordered_map.go | 8 +-
test/csharp-tableau-loader/gen.sh | 0
.../tableau/HeroConf.pc.cs | 12 +-
test/csharp-tableau-loader/tableau/Hub.pc.cs | 100 ++++++++++------
.../tableau/IndexConf.pc.cs | 108 +++++++++---------
.../tableau/ItemConf.pc.cs | 52 ++++-----
test/csharp-tableau-loader/tableau/Load.pc.cs | 4 +-
.../tableau/PatchConf.pc.cs | 6 +-
.../tableau/TestConf.pc.cs | 88 +++++++-------
15 files changed, 260 insertions(+), 201 deletions(-)
mode change 100644 => 100755 test/csharp-tableau-loader/gen.sh
diff --git a/README.md b/README.md
index 6136676f..396a469c 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ The official config loader for [Tableau](https://github.com/tableauio/tableau).
### Requirements
+- Unity 2022.3 LTS (C# 9)
- dotnet-sdk-8.0
### Test
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
index 23406389..66a835ec 100644
--- a/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs
@@ -166,11 +166,11 @@ public object Clone()
}
///
- /// IMessagerName is an interface that provides the static Name() method for messagers.
+ /// IMessagerName is an interface that provides the Name() method for messagers.
///
public interface IMessagerName
{
- static abstract string Name();
+ string Name();
}
///
diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
index 4d08249f..3d0013c3 100644
--- a/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
+++ b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl
@@ -4,19 +4,30 @@ namespace Tableau
///
/// MessagerContainer holds all messager instances and provides fast access.
///
- internal class MessagerContainer(in Dictionary? messagerMap = null)
+ internal class MessagerContainer
{
- public Dictionary MessagerMap = messagerMap ?? [];
- public DateTime LastLoadedTime = DateTime.Now;
-{{ range . }} public {{ . }}? {{ . }} = InternalGet<{{ . }}>(messagerMap);
+ public Dictionary MessagerMap;
+ public DateTime LastLoadedTime;
+{{ range . }} public {{ . }}? {{ . }};
{{ end }}
+ public MessagerContainer(Dictionary? messagerMap = null)
+ {
+ MessagerMap = messagerMap ?? new Dictionary();
+ LastLoadedTime = DateTime.Now;
+{{ range . }} {{ . }} = InternalGet<{{ . }}>(messagerMap);
+{{ end }} }
+
///
/// Get returns the messager of type T from the container.
///
- public T? Get() where T : Messager, IMessagerName => InternalGet(MessagerMap);
+ public T? Get() where T : Messager, IMessagerName, new() => InternalGet(MessagerMap);
- private static T? InternalGet