commit 31b4c68da300a98712508008db49e0f41ecc5623 Author: amizing25 Date: Tue Dec 30 07:57:34 2025 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71ec0d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,226 @@ +# The following command works for downloading when using Git for Windows: +# curl -LOf http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore +# +# Download this file using PowerShell v3 under Windows with the following comand: +# Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore +# +# or wget: +# wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ +# build folder is nowadays used for build scripts and should not be ignored +#build/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings +modulesbin/ +tempbin/ + +# EPiServer Site file (VPP) +AppData/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# vim +*.txt~ +*.swp +*.swo + +# Temp files when opening LibreOffice on ubuntu +.~lock.* + +# svn +.svn + +# CVS - Source Control +**/CVS/ + +# Remainings from resolving conflicts in Source Control +*.orig + +# SQL Server files +**/App_Data/*.mdf +**/App_Data/*.ldf +**/App_Data/*.sdf + + +#LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# OS generated files # +Icon? + +# Mac desktop service store files +.DS_Store + +# SASS Compiler cache +.sass-cache + +# Visual Studio 2014 CTP +**/*.sln.ide + +# Visual Studio temp something +.vs/ + +# dotnet stuff +project.lock.json + +# VS 2015+ +*.vc.vc.opendb +*.vc.db + +# Rider +.idea/ + +# Visual Studio Code +.vscode/ + +# Output folder used by Webpack or other FE stuff +**/node_modules/* +**/wwwroot/* + +# SpecFlow specific +*.feature.cs +*.feature.xlsx.* +*.Specs_*.html + +# UWP Projects +AppPackages/ + +##### +# End of core ignore list, below put you custom 'per project' settings (patterns or path) +##### \ No newline at end of file diff --git a/DynamicCodec.cs b/DynamicCodec.cs new file mode 100644 index 0000000..24fc98d --- /dev/null +++ b/DynamicCodec.cs @@ -0,0 +1,152 @@ +using Google.Protobuf; +using Google.Protobuf.Collections; + +namespace DynamicProtobuf.Runtime; + +public static class DynamicCodec +{ + public static FieldCodec ForString(string fullName, string fieldName) => ForString(fullName, fieldName, ""); + + public static FieldCodec ForBytes(string fullName, string fieldName) => ForBytes(fullName, fieldName, ByteString.Empty); + + public static FieldCodec ForBool(string fullName, string fieldName) => ForBool(fullName, fieldName, false); + + public static FieldCodec ForInt32(string fullName, string fieldName) => ForInt32(fullName, fieldName, 0); + + public static FieldCodec ForSInt32(string fullName, string fieldName) => ForSInt32(fullName, fieldName, 0); + + public static FieldCodec ForFixed32(string fullName, string fieldName) => ForFixed32(fullName, fieldName, 0); + + public static FieldCodec ForSFixed32(string fullName, string fieldName) => ForSFixed32(fullName, fieldName, 0); + + public static FieldCodec ForUInt32(string fullName, string fieldName) => ForUInt32(fullName, fieldName, 0); + + public static FieldCodec ForInt64(string fullName, string fieldName) => ForInt64(fullName, fieldName, 0); + + + public static FieldCodec ForSInt64(string fullName, string fieldName) => ForSInt64(fullName, fieldName, 0); + + public static FieldCodec ForFixed64(string fullName, string fieldName) => ForFixed64(fullName, fieldName, 0); + + public static FieldCodec ForSFixed64(string fullName, string fieldName) => ForSFixed64(fullName, fieldName, 0); + + public static FieldCodec ForUInt64(string fullName, string fieldName) => ForUInt64(fullName, fieldName, 0); + + public static FieldCodec ForFloat(string fullName, string fieldName) => ForFloat(fullName, fieldName, 0); + + public static FieldCodec ForDouble(string fullName, string fieldName) => ForDouble(fullName, fieldName, 0); + + public static FieldCodec ForEnum(string fullName, string fieldName, Func toInt32, Func fromInt32) => + ForEnum(fullName, fieldName, toInt32, fromInt32, default); + + public static FieldCodec ForString(string fullName, string fieldName, string defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForString(tag, defaultValue); + } + + public static FieldCodec ForBytes(string fullName, string fieldName, ByteString defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForBytes(tag, defaultValue); + } + + public static FieldCodec ForBool(string fullName, string fieldName, bool defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForBool(tag, defaultValue); + } + + public static FieldCodec ForInt32(string fullName, string fieldName, int defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForInt32(tag, defaultValue); + } + + public static FieldCodec ForSInt32(string fullName, string fieldName, int defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForSInt32(tag, defaultValue); + } + + public static FieldCodec ForFixed32(string fullName, string fieldName, uint defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForFixed32(tag); + } + + public static FieldCodec ForSFixed32(string fullName, string fieldName, int defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForSFixed32(tag, defaultValue); + } + + public static FieldCodec ForUInt32(string fullName, string fieldName, uint defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForUInt32(tag, defaultValue); + } + + public static FieldCodec ForInt64(string fullName, string fieldName, long defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForInt64(tag, defaultValue); + } + + public static FieldCodec ForSInt64(string fullName, string fieldName, long defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForSInt64(tag, defaultValue); + } + + public static FieldCodec ForFixed64(string fullName, string fieldName, ulong defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForFixed64(tag, defaultValue); + } + + public static FieldCodec ForSFixed64(string fullName, string fieldName, long defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForSFixed64(tag, defaultValue); + } + + public static FieldCodec ForUInt64(string fullName, string fieldName, ulong defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForUInt64(tag, defaultValue); + } + + + public static FieldCodec ForFloat(string fullName, string fieldName, float defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForFloat(tag, defaultValue); + } + + public static FieldCodec ForDouble(string fullName, string fieldName, double defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForDouble(tag, defaultValue); + } + + public static FieldCodec ForEnum(string fullName, string fieldName, Func toInt32, Func fromInt32, T defaultValue) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForEnum(tag, toInt32, fromInt32, defaultValue); + } + + public static MapField.Codec ForMap(string fullName, string fieldName, FieldCodec keyCodec, + FieldCodec valueCodec) + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return new MapField.Codec(keyCodec, valueCodec, tag); + } + + public static FieldCodec ForMessage(string fullName, string fieldName, MessageParser parser) + where T : class, IMessage + { + uint tag = DynamicFieldRegistry.GetTag(fullName, fieldName); + return FieldCodec.ForMessage(tag, parser); + } +} \ No newline at end of file diff --git a/DynamicFieldRegistry.cs b/DynamicFieldRegistry.cs new file mode 100644 index 0000000..1ee8bdb --- /dev/null +++ b/DynamicFieldRegistry.cs @@ -0,0 +1,163 @@ +using System.Text.Json; +using Google.Protobuf; + +namespace DynamicProtobuf.Runtime; + +public static class DynamicFieldRegistry +{ + #region Local State + private static FileSystemWatcher? _fsWatcher; + + private static ProtoJsonRegistry Registry { get; set; } = new(); + + private static HashSet<(string, string)> MissingFieldSet { get; set; } = []; + + private static Timer? _reloadTimer; + + private static readonly Lock _reloadLock = new(); + + #endregion Local State + + #region Actions + public static Action OnMissingField { get; set; } = msg => Console.Error.WriteLine("[DynamicProtobuf] [WARN]: " + msg); + + public static Action OnError { get; set; } = msg => Console.Error.WriteLine("[DynamicProtobuf] [ERROR]: " + msg); + + public static event Action? OnHotReloaded; + + #endregion Actions + + #region Codegen Helper + + public static uint GetXorConst(string fullName, string fieldName) + { + return GetField(fullName, fieldName)?.XorConst ?? 0; + } + + public static bool HasField(string fullName, string fieldName) + { + return GetField(fullName, fieldName) != null; + } + + public static int GetFieldNumber(string fullName, string fieldName) + { + return GetField(fullName, fieldName)?.FieldNumber ?? 0; + } + + public static uint GetTag(string fullName, string fieldName, bool packed = false) + { + MessageField? field = GetField(fullName, fieldName); + if (field == null) + return 0; + return (uint)((field.FieldNumber << 3) | (int)field.WireType); + } + + public static int GetTagSize(string fullName, string fieldName) + { + return CodedOutputStream.ComputeTagSize(GetFieldNumber(fullName, fieldName)); + } + + private static MessageField? GetField(string fullName, string fieldName) + { + if (Registry.Messages.TryGetValue(fullName, out var fields) && fields.TryGetValue(fieldName, out var field)) + { + return field; + } + + if (MissingFieldSet.Add((fullName, fieldName))) + { + OnMissingField?.Invoke($"no such field \"{fieldName}\" at message \"{fullName}\""); + } + + return null; + } + + #endregion Codegen Helper + + public static void ListenFromFile(string path) + { + LoadFromFile(path); + + var directory = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(directory)) directory = "."; + var fileName = Path.GetFileName(path); + + _fsWatcher = new FileSystemWatcher(directory, Path.GetFileName(path)) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size, + EnableRaisingEvents = true + }; + + _fsWatcher.Error += (sender, e) => + { + OnError?.Invoke(e.GetException().Message); + }; + + _fsWatcher.Changed += (sender, e) => + { + if (e.ChangeType != WatcherChangeTypes.Changed) + return; + + lock (_reloadLock) + { + _reloadTimer?.Dispose(); + + _reloadTimer = new Timer(_ => + { + try + { + while (IsFileLocked(path)) + Thread.Sleep(50); + + LoadFromFile(path); + } + catch (Exception ex) + { + OnError?.Invoke(ex.Message); + } + }, null, 250, Timeout.Infinite); + } + }; + } + + public static void LoadFromFile(string path) + { + if (!File.Exists(path)) + { + OnError?.Invoke($"proto json registry file not found: {path}"); + return; + } + + try + { + MissingFieldSet.Clear(); + + var json = File.ReadAllText(path); + var reg = JsonSerializer.Deserialize(json); + + if (reg != null) + { + Registry = reg; + OnHotReloaded?.Invoke(Registry); + } + } + catch (Exception ex) + { + OnError?.Invoke($"failed to load proto json registry: {ex.Message}"); + } + } + + private static bool IsFileLocked(string path) + { + try + { + using FileStream file = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + return false; + } + catch (Exception) + { + return true; + } + } + +} \ No newline at end of file diff --git a/DynamicProtobuf.Runtime.csproj b/DynamicProtobuf.Runtime.csproj new file mode 100644 index 0000000..8d4afe1 --- /dev/null +++ b/DynamicProtobuf.Runtime.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + DynamicProtobuf.Runtime 1.0.0 + amizing25 + Runtime helper for dynamic protobuf generated code + https://github.com/ex-RushiaLover/DynamicProtobuf.Runtime + dotnet;utility;helper + true + + + + + + diff --git a/ProtoJsonRegistry.cs b/ProtoJsonRegistry.cs new file mode 100644 index 0000000..44a285a --- /dev/null +++ b/ProtoJsonRegistry.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using Google.Protobuf; + +namespace DynamicProtobuf.Runtime; + +public class ProtoJsonRegistry +{ + [JsonPropertyName("messages")] + public Dictionary> Messages { get; set; } = []; + + [JsonPropertyName("command_ids")] + public Dictionary? CommandIds { get; set; } +} + +public class MessageField +{ + [JsonPropertyName("field_number")] + public int FieldNumber { get; set; } + + [JsonPropertyName("wire_type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WireFormat.WireType WireType { get; set; } + + [JsonPropertyName("xor_const")] + public uint? XorConst { get; set; } +} \ No newline at end of file