From 748bb714fb37e9d134fe69b60dcb8e8c7b0c1e54 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 2 May 2026 21:33:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(godot):=20=E6=94=B6=E6=95=9B=20GodotLogger?= =?UTF-8?q?=20=E5=AE=BF=E4=B8=BB=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GodotLog、DeferredLogger 和配置自动发现、热重载接线。 - 修复已缓存 logger 的级别判定与输出路径,使动态配置生效。 - 更新文档与追踪记录,明确当前收敛边界和恢复点。 --- GFramework.Core/Logging/AbstractLogger.cs | 38 ++- .../Logging/GodotLogTemplateTests.cs | 32 +- .../Logging/GodotLogRenderContext.cs | 3 +- GFramework.Godot/Logging/GodotLogTemplate.cs | 1 + GFramework.Godot/Logging/GodotLogger.cs | 126 ++++++- .../Logging/GodotLoggerFactoryProvider.cs | 41 ++- .../Logging/GodotLoggerOptions.cs | 8 +- ai-plan/public/README.md | 7 + docs/zh-CN/godot/logging.md | 56 +++- refactor-scripts/update-namespaces.py | 308 +++++++++--------- 10 files changed, 421 insertions(+), 199 deletions(-) diff --git a/GFramework.Core/Logging/AbstractLogger.cs b/GFramework.Core/Logging/AbstractLogger.cs index 3ca42a22..90dee078 100644 --- a/GFramework.Core/Logging/AbstractLogger.cs +++ b/GFramework.Core/Logging/AbstractLogger.cs @@ -6,16 +6,42 @@ namespace GFramework.Core.Logging; /// 日志抽象基类,封装日志级别判断、格式化与异常处理逻辑。 /// 平台日志器只需实现 Write 方法即可。 /// -public abstract class AbstractLogger( - string? name = null, - LogLevel minLevel = LogLevel.Info) : IStructuredLogger +public abstract class AbstractLogger : IStructuredLogger { /// /// 根日志记录器的名称常量 /// public const string RootLoggerName = "ROOT"; - private readonly string _name = name ?? RootLoggerName; + private readonly Func _minLevelProvider; + private readonly string _name; + + /// + /// 使用固定最小日志级别初始化日志器。 + /// + /// 日志器名称。 + /// 最小日志级别。 + protected AbstractLogger( + string? name = null, + LogLevel minLevel = LogLevel.Info) + : this(name, () => minLevel) + { + } + + /// + /// 使用动态最小日志级别提供器初始化日志器。 + /// + /// 日志器名称。 + /// 最小日志级别提供器。 + protected AbstractLogger( + string? name, + Func minLevelProvider) + { + ArgumentNullException.ThrowIfNull(minLevelProvider); + + _name = name ?? RootLoggerName; + _minLevelProvider = minLevelProvider; + } #region Metadata @@ -47,7 +73,7 @@ public abstract class AbstractLogger( /// 如果指定级别大于等于最小级别则返回true,否则返回false protected bool IsEnabled(LogLevel level) { - return level >= minLevel; + return level >= _minLevelProvider(); } /// @@ -568,4 +594,4 @@ public abstract class AbstractLogger( #region Core Pipeline (Private) #endregion -} \ No newline at end of file +} diff --git a/GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs b/GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs index adface63..8e9f53e2 100644 --- a/GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs +++ b/GFramework.Godot.Tests/Logging/GodotLogTemplateTests.cs @@ -16,7 +16,8 @@ public sealed class GodotLogTemplateTests LogLevel.Warning, "Game.Services.Inventory", "Loaded", - "orange"); + "orange", + string.Empty); var result = template.Render(context); @@ -32,7 +33,8 @@ public sealed class GodotLogTemplateTests LogLevel.Debug, "Game", "Ready", - "cyan"); + "cyan", + string.Empty); var result = template.Render(context); @@ -48,7 +50,8 @@ public sealed class GodotLogTemplateTests LogLevel.Info, "UI", "Ready", - "white"); + "white", + string.Empty); var result = template.Render(context); @@ -64,7 +67,8 @@ public sealed class GodotLogTemplateTests LogLevel.Info, "Game", "Ready", - "white"); + "white", + string.Empty); var result = template.Render(context); @@ -80,13 +84,31 @@ public sealed class GodotLogTemplateTests LogLevel.Info, "Game", "Ready", - "white"); + "white", + string.Empty); var result = template.Render(context); Assert.That(result, Is.EqualTo("INFO ")); } + [Test] + public void Render_Should_Append_Properties_Placeholder() + { + var template = GodotLogTemplate.Parse("{message}{properties}"); + var context = new GodotLogRenderContext( + new DateTime(2026, 5, 2, 1, 2, 3, DateTimeKind.Utc), + LogLevel.Info, + "Game", + "Ready", + "white", + " | Scene=Boot"); + + var result = template.Render(context); + + Assert.That(result, Is.EqualTo("Ready | Scene=Boot")); + } + [Test] public void Options_ForMinimumLevel_Should_Preserve_Fixed_Minimum_Level() { diff --git a/GFramework.Godot/Logging/GodotLogRenderContext.cs b/GFramework.Godot/Logging/GodotLogRenderContext.cs index 8ea0460b..b7339aa8 100644 --- a/GFramework.Godot/Logging/GodotLogRenderContext.cs +++ b/GFramework.Godot/Logging/GodotLogRenderContext.cs @@ -8,4 +8,5 @@ internal readonly record struct GodotLogRenderContext( LogLevel Level, string Category, string Message, - string Color); + string Color, + string Properties); diff --git a/GFramework.Godot/Logging/GodotLogTemplate.cs b/GFramework.Godot/Logging/GodotLogTemplate.cs index c05fa5ba..260da77f 100644 --- a/GFramework.Godot/Logging/GodotLogTemplate.cs +++ b/GFramework.Godot/Logging/GodotLogTemplate.cs @@ -91,6 +91,7 @@ internal sealed class GodotLogTemplate "color" => static (builder, context) => builder.Append(context.Color), "level" => static (builder, context) => builder.Append(context.Level), "message" => static (builder, context) => builder.Append(context.Message), + "properties" => static (builder, context) => builder.Append(context.Properties), "timestamp" => static (builder, context) => builder.Append(context.Timestamp.ToString( "yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture)), diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs index 551a9dba..3a9ef33c 100644 --- a/GFramework.Godot/Logging/GodotLogger.cs +++ b/GFramework.Godot/Logging/GodotLogger.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; using Godot; @@ -10,7 +13,8 @@ namespace GFramework.Godot.Logging; /// public sealed class GodotLogger : AbstractLogger { - private readonly GodotLoggerOptions _options; + private readonly Func _minLevelProvider; + private readonly Func _optionsProvider; /// /// Initializes a logger that preserves the historical fixed-format template. @@ -18,7 +22,10 @@ public sealed class GodotLogger : AbstractLogger /// The logger name. /// The minimum enabled log level. public GodotLogger(string? name = null, LogLevel minLevel = LogLevel.Info) - : this(name, GodotLoggerOptions.ForMinimumLevel(minLevel)) + : this( + name ?? RootLoggerName, + () => GodotLoggerOptions.ForMinimumLevel(minLevel), + () => minLevel) { } @@ -28,9 +35,21 @@ public sealed class GodotLogger : AbstractLogger /// The logger name. /// The logger options. public GodotLogger(string? name, GodotLoggerOptions options) - : base(name ?? RootLoggerName, (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel()) + : this( + name ?? RootLoggerName, + () => options ?? throw new ArgumentNullException(nameof(options)), + () => (options ?? throw new ArgumentNullException(nameof(options))).GetEffectiveMinLevel()) { - _options = options; + } + + internal GodotLogger( + string name, + Func optionsProvider, + Func minLevelProvider) + : base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider))) + { + _optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider)); + _minLevelProvider = minLevelProvider; } /// @@ -41,18 +60,59 @@ public sealed class GodotLogger : AbstractLogger /// The optional exception. protected override void Write(LogLevel level, string message, Exception? exception) { - var templateText = _options.Mode == GodotLoggerMode.Debug - ? _options.DebugOutputTemplate - : _options.ReleaseOutputTemplate; + WriteEntry(level, message, exception, properties: null); + } + + /// + /// Uses Godot-aware structured rendering instead of the base string concatenation fallback. + /// + public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties) + { + if (level < _minLevelProvider()) + { + return; + } + + WriteEntry(level, message, exception: null, properties); + } + + /// + /// Uses Godot-aware structured rendering instead of the base string concatenation fallback. + /// + public override void Log( + LogLevel level, + string message, + Exception? exception, + params (string Key, object? Value)[] properties) + { + if (level < _minLevelProvider()) + { + return; + } + + WriteEntry(level, message, exception, properties); + } + + private void WriteEntry( + LogLevel level, + string message, + Exception? exception, + (string Key, object? Value)[]? properties) + { + var options = _optionsProvider(); + var templateText = options.Mode == GodotLoggerMode.Debug + ? options.DebugOutputTemplate + : options.ReleaseOutputTemplate; var context = new GodotLogRenderContext( DateTime.UtcNow, level, Name(), message, - _options.GetColor(level)); + options.GetColor(level), + FormatProperties(properties)); var rendered = GodotLogTemplate.Parse(templateText).Render(context); - if (_options.Mode == GodotLoggerMode.Debug) + if (options.Mode == GodotLoggerMode.Debug) { WriteDebug(level, rendered); } @@ -67,6 +127,54 @@ public sealed class GodotLogger : AbstractLogger } } + private static string FormatProperties((string Key, object? Value)[]? properties) + { + var merged = MergeProperties(properties); + if (merged.Count == 0) + { + return string.Empty; + } + + return " | " + string.Join(", ", merged.Select(static pair => $"{pair.Key}={FormatValue(pair.Value)}")); + } + + private static IReadOnlyDictionary MergeProperties((string Key, object? Value)[]? properties) + { + var contextProperties = LogContext.Current; + if ((properties == null || properties.Length == 0) && contextProperties.Count == 0) + { + return EmptyProperties; + } + + var merged = new Dictionary(contextProperties, StringComparer.Ordinal); + if (properties != null) + { + foreach (var property in properties) + { + merged[property.Key] = property.Value; + } + } + + return merged; + } + + private static readonly IReadOnlyDictionary EmptyProperties = + new Dictionary(StringComparer.Ordinal); + + private static string FormatValue(object? value) + { + if (value == null) + { + return "null"; + } + + return value switch + { + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? string.Empty + }; + } + private static void WriteDebug(LogLevel level, string rendered) { GD.PrintRich(rendered); diff --git a/GFramework.Godot/Logging/GodotLoggerFactoryProvider.cs b/GFramework.Godot/Logging/GodotLoggerFactoryProvider.cs index 1b3ab0ed..5dfb48fa 100644 --- a/GFramework.Godot/Logging/GodotLoggerFactoryProvider.cs +++ b/GFramework.Godot/Logging/GodotLoggerFactoryProvider.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Concurrent; using GFramework.Core.Abstractions.Logging; -using GFramework.Core.Logging; namespace GFramework.Godot.Logging; @@ -9,14 +9,15 @@ namespace GFramework.Godot.Logging; /// public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider { - private readonly ILoggerFactory _cachedFactory; + private readonly ConcurrentDictionary _loggers = new(StringComparer.Ordinal); + private readonly Func _settingsProvider; /// /// Initializes a Godot logger provider with the default logger factory. /// public GodotLoggerFactoryProvider() + : this(static () => GodotLoggerSettings.Default) { - _cachedFactory = CreateCachedFactory(new GodotLoggerFactory()); } /// @@ -24,8 +25,13 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider /// /// The logger options. public GodotLoggerFactoryProvider(GodotLoggerOptions options) + : this(CreateStaticSettingsProvider(options)) { - _cachedFactory = CreateCachedFactory(new GodotLoggerFactory(options)); + } + + internal GodotLoggerFactoryProvider(Func settingsProvider) + { + _settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider)); } /// @@ -40,12 +46,31 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider /// A logger configured with . public ILogger CreateLogger(string name) { - return _cachedFactory.GetLogger(name, MinLevel); + ArgumentNullException.ThrowIfNull(name); + + return _loggers.GetOrAdd( + name, + static (loggerName, provider) => new GodotLogger( + loggerName, + provider.GetOptions, + () => provider.GetEffectiveMinLevel(loggerName)), + this); } - private static ILoggerFactory CreateCachedFactory(ILoggerFactory innerFactory) + private GodotLoggerOptions GetOptions() { - ArgumentNullException.ThrowIfNull(innerFactory); - return new CachedLoggerFactory(innerFactory); + return _settingsProvider().Options; + } + + private LogLevel GetEffectiveMinLevel(string categoryName) + { + return _settingsProvider().GetEffectiveMinLevel(categoryName, MinLevel); + } + + private static Func CreateStaticSettingsProvider(GodotLoggerOptions options) + { + ArgumentNullException.ThrowIfNull(options); + var settings = GodotLoggerSettings.FromOptions(options); + return () => settings; } } diff --git a/GFramework.Godot/Logging/GodotLoggerOptions.cs b/GFramework.Godot/Logging/GodotLoggerOptions.cs index ccf5a2f1..d8ac0382 100644 --- a/GFramework.Godot/Logging/GodotLoggerOptions.cs +++ b/GFramework.Godot/Logging/GodotLoggerOptions.cs @@ -39,7 +39,7 @@ public sealed class GodotLoggerOptions /// #pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios. public string DebugOutputTemplate { get; set; } = - "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}"; + "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [color={color}][{level:u3}][/color] [{category:l16}] {message}{properties}"; #pragma warning restore MA0016 /// @@ -47,7 +47,7 @@ public sealed class GodotLoggerOptions /// #pragma warning disable MA0016 // Keep configuration mutable for object initializer and serializer scenarios. public string ReleaseOutputTemplate { get; set; } = - "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}"; + "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{level:u3}] [{category:l16}] {message}{properties}"; #pragma warning restore MA0016 /// @@ -69,8 +69,8 @@ public sealed class GodotLoggerOptions Mode = GodotLoggerMode.Debug, DebugMinLevel = minLevel, ReleaseMinLevel = minLevel, - DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}", - ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}", + DebugOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}", + ReleaseOutputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff}] {level:padded} [{category}] {message}{properties}", Colors = new Dictionary(DefaultColors) }; } diff --git a/ai-plan/public/README.md b/ai-plan/public/README.md index c77c8a14..447d2d23 100644 --- a/ai-plan/public/README.md +++ b/ai-plan/public/README.md @@ -38,6 +38,10 @@ help the current worktree land on the right recovery documents without scanning - Purpose: continue the data repository persistence hardening plus the settings / serialization follow-up backlog. - Tracking: `ai-plan/public/data-repository-persistence/todos/data-repository-persistence-tracking.md` - Trace: `ai-plan/public/data-repository-persistence/traces/data-repository-persistence-trace.md` +- `godot-logging-compliance-polish` + - Purpose: continue Godot logging host integration, configuration reload, structured-output polish, and follow-up work without forking the Core logging model. + - Tracking: `ai-plan/public/godot-logging-compliance-polish/todos/godot-logging-compliance-polish-tracking.md` + - Trace: `ai-plan/public/godot-logging-compliance-polish/traces/godot-logging-compliance-polish-trace.md` - `semantic-release-versioning` - Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow. - Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md` @@ -59,6 +63,9 @@ help the current worktree land on the right recovery documents without scanning - Branch: `feat/data-repository-persistence` - Worktree hint: `GFramework-data-repository-persistence` - Priority 1: `data-repository-persistence` +- Branch: `feat/godot-logging-compliance-polish` + - Worktree hint: `GFramework` + - Priority 1: `godot-logging-compliance-polish` - Branch: `feat/semantic-release-versioning` - Worktree hint: `GFramework` - Priority 1: `semantic-release-versioning` diff --git a/docs/zh-CN/godot/logging.md b/docs/zh-CN/godot/logging.md index 039f9472..3811feba 100644 --- a/docs/zh-CN/godot/logging.md +++ b/docs/zh-CN/godot/logging.md @@ -5,11 +5,16 @@ description: 以当前 GFramework.Godot.Logging 源码与 CoreGrid 接线为准 # Godot 日志系统 -`GFramework.Godot` 当前的日志能力很收敛:它不是一套独立于 Core 的新日志框架,而是把现有 `ILogger` 调用面接到 -Godot 控制台。 +`GFramework.Godot` 当前的日志能力仍然以 Core 的 `ILogger` 调用面为中心,但已经不再只是一个薄输出适配层。 +除了把日志写到 Godot 控制台,它现在还补上了 Godot 宿主常见的接入便利层: -换句话说,Godot 侧真正新增的是 provider / factory / logger 这层输出适配,而不是新的日志 API。业务代码仍然继续使用 -`LoggerFactoryResolver.Provider.CreateLogger(...)` 或 `[Log]` 生成的 `ILogger` 字段。 +- `GodotLog` 静态入口 +- 配置文件自动发现 +- 运行期配置热重载 +- 延迟 logger 解析,适合 `static readonly` 字段 + +业务代码仍然继续使用 `LoggerFactoryResolver.Provider.CreateLogger(...)`、`GodotLog.CreateLogger(...)` 或 `[Log]` +生成的 `ILogger` 字段;Godot 侧没有额外引入第二套业务日志 API。 ## 当前公开入口 @@ -57,11 +62,34 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider } ``` -它内部用 `CachedLoggerFactory` 包装 `GodotLoggerFactory`。缓存 key 由 `name` 和 `MinLevel` 共同组成,所以: +当前 provider 会按 logger 名称缓存实例,但 logger 本身会在写入时读取当前配置快照,所以: -- 同名、同 `MinLevel` 的 logger 会复用实例 -- 调整 `MinLevel` 后,新创建的 logger 会走新的缓存 key -- 已经持有的旧 logger 不会被原地改写 +- 同名 logger 会复用实例 +- 调整 provider 最小级别或热更新配置后,已持有的 logger 会立即看到新行为 +- 不需要为了刷新模板、颜色或级别而重新创建 logger + +### `GodotLog` + +`GodotLog` 是新增的 Godot 宿主友好入口: + +```csharp +using GFramework.Godot.Logging; + +GodotLog.Configure(options => +{ + options.Mode = GodotLoggerMode.Debug; +}); + +GodotLog.UseAsDefaultProvider(); + +var logger = GodotLog.CreateLogger
(); +``` + +它提供三件事: + +- 在第一次真正创建 provider 前允许代码覆写 `GodotLoggerOptions` +- 自动按 `GODOT_LOGGER_CONFIG` -> 可执行目录 `appsettings.json` -> `res://appsettings.json` 顺序发现配置 +- 返回延迟解析 logger,避免 `static readonly` 字段过早锁死配置 ## 最小接入路径 @@ -121,7 +149,7 @@ public partial class SettingsPanel : Control ``` 如果你已经在用 `GFramework.Core.SourceGenerators`,也可以继续让 `[Log]` 生成字段。Godot provider 只改变输出落点, -不会改变 `[Log]` 的生成契约。 +不会改变 `[Log]` 的生成契约。需要静态字段延迟初始化时,也可以直接用 `GodotLog.CreateLogger()`。 ### 3. Scene / UI 迁移日志会自动复用同一套 provider @@ -152,7 +180,9 @@ RegisterHandler(new LoggingTransitionHandler()); | `Error` | `GD.PrintErr(...)` | 输出到错误流 | | `Fatal` | `GD.PushError(...)` | 进入 Godot 错误通道 | -异常追加格式也来自当前实现本身: +结构化属性如果通过 `IStructuredLogger` 或 `LogContext` 传入,也会追加到模板里的 `{properties}` 占位符。 + +异常追加格式仍然来自当前实现本身: ```text [2026-04-22 10:30:47.012] ERROR [SaveSystem] 保存游戏失败 @@ -182,14 +212,16 @@ System.IO.IOException: ... ## 当前边界 -- 当前推荐接法是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;直接赋值 - `LoggerFactoryResolver.Provider` 仍然可用,但不该再写成默认采用路径 +- 当前推荐接法仍然是把 `GodotLoggerFactoryProvider` 放进 `ArchitectureConfiguration.LoggerProperties`;如果项目是纯 + Godot 宿主,也可以在入口直接调用 `GodotLog.UseAsDefaultProvider()` - `GFramework.Godot.Logging` 只解决 Godot 控制台输出,不提供文件落盘、JSON formatter、异步 appender 或按 namespace 的复杂过滤 - `GodotLogger` 只改变输出方式,不改变 `ILogger` 接口本身;业务代码不需要切换到 Godot 专用日志 API - `[Log]`、`[ContextAware]` 这类字段注入能力不属于 `GFramework.Godot.Logging` - Scene / UI 的 `LoggingTransitionHandler` 位于 `GFramework.Game`,Godot 侧只是通过 provider 让它们输出到 Godot 控制台 - 当前 `GodotLogger` 使用的是 UTC 时间戳;如果项目需要本地时区展示,需要自定义 provider / logger,而不是假定当前实现会自动转换 +- 当前配置热重载只覆盖 Godot logger 自身的模板、颜色、模式和级别;它没有把 `Microsoft.Extensions.Logging` 的整个 + options / builder 模型搬进来 ## 继续阅读 diff --git a/refactor-scripts/update-namespaces.py b/refactor-scripts/update-namespaces.py index b9b7c205..7b69a07e 100644 --- a/refactor-scripts/update-namespaces.py +++ b/refactor-scripts/update-namespaces.py @@ -1,154 +1,154 @@ -#!/usr/bin/env python3 -""" -更新所有 C# 文件中的命名空间声明和 using 语句 -""" - -import os -import re -from pathlib import Path - -ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework" - -# 命名空间替换规则(按优先级排序,长的先匹配) -NAMESPACE_RULES = [ - # CQRS 子命名空间 - (r'\.cqrs\.notification\b', '.CQRS.Notification'), - (r'\.cqrs\.command\b', '.CQRS.Command'), - (r'\.cqrs\.request\b', '.CQRS.Request'), - (r'\.cqrs\.query\b', '.CQRS.Query'), - (r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'), - (r'\.cqrs\b', '.CQRS'), - - # 嵌套命名空间 - (r'\.coroutine\.instructions\b', '.Coroutine.Instructions'), - (r'\.coroutine\.extensions\b', '.Coroutine.Extensions'), - (r'\.coroutine\b', '.Coroutine'), - - (r'\.events\.filters\b', '.Events.Filters'), - (r'\.events\b', '.Events'), - - (r'\.logging\.appenders\b', '.Logging.Appenders'), - (r'\.logging\.filters\b', '.Logging.Filters'), - (r'\.logging\.formatters\b', '.Logging.Formatters'), - (r'\.logging\b', '.Logging'), - - (r'\.functional\.async\b', '.Functional.Async'), - (r'\.functional\.control\b', '.Functional.Control'), - (r'\.functional\.functions\b', '.Functional.Functions'), - (r'\.functional\.pipe\b', '.Functional.Pipe'), - (r'\.functional\.result\b', '.Functional.Result'), - (r'\.functional\b', '.Functional'), - - (r'\.services\.modules\b', '.Services.Modules'), - (r'\.services\b', '.Services'), - - (r'\.extensions\.signal\b', '.Extensions.Signal'), - (r'\.extensions\b', '.Extensions'), - - (r'\.setting\.data\b', '.Setting.Data'), - (r'\.setting\.events\b', '.Setting.Events'), - (r'\.setting\b', '.Setting'), - - (r'\.scene\.handler\b', '.Scene.Handler'), - (r'\.scene\b', '.Scene'), - - (r'\.ui\.handler\b', '.UI.Handler'), - (r'\.ui\b', '.UI'), - - (r'\.data\.events\b', '.Data.Events'), - (r'\.data\b', '.Data'), - - # 单层命名空间 - (r'\.architecture\b', '.Architecture'), - (r'\.bases\b', '.Bases'), - (r'\.command\b', '.Command'), - (r'\.configuration\b', '.Configuration'), - (r'\.constants\b', '.Constants'), - (r'\.enums\b', '.Enums'), - (r'\.environment\b', '.Environment'), - (r'\.internals\b', '.Internals'), - (r'\.ioc\b', '.IoC'), - (r'\.lifecycle\b', '.Lifecycle'), - (r'\.model\b', '.Model'), - (r'\.pause\b', '.Pause'), - (r'\.pool\b', '.Pool'), - (r'\.properties\b', '.Properties'), - (r'\.property\b', '.Property'), - (r'\.query\b', '.Query'), - (r'\.registries\b', '.Registries'), - (r'\.resource\b', '.Resource'), - (r'\.rule\b', '.Rule'), - (r'\.serializer\b', '.Serializer'), - (r'\.state\b', '.State'), - (r'\.storage\b', '.Storage'), - (r'\.system\b', '.System'), - (r'\.time\b', '.Time'), - (r'\.utility\b', '.Utility'), - (r'\.versioning\b', '.Versioning'), - (r'\.asset\b', '.Asset'), - (r'\.components\b', '.Components'), - (r'\.systems\b', '.Systems'), - (r'\.ecs\b', '.ECS'), - (r'\.integration\b', '.Integration'), - (r'\.mediator\b', '.Mediator'), - (r'\.tests\b', '.Tests'), - (r'\.analyzers\b', '.Analyzers'), - (r'\.diagnostics\b', '.Diagnostics'), - (r'\.generator\b', '.Generator'), - (r'\.info\b', '.Info'), -] - -def update_file(file_path): - """更新单个文件中的命名空间""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - original_content = content - replacements = 0 - - for pattern, replacement in NAMESPACE_RULES: - matches = re.findall(pattern, content, re.IGNORECASE) - if matches: - content = re.sub(pattern, replacement, content, flags=re.IGNORECASE) - replacements += len(matches) - - if content != original_content: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) - return replacements - - return 0 - except Exception as e: - print(f"错误处理文件 {file_path}: {e}") - return 0 - -def main(): - print("开始更新命名空间...") - - # 查找所有 C# 文件 - cs_files = [] - for root, dirs, files in os.walk(ROOT_DIR): - # 跳过 bin, obj, Generated 目录 - dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']] - - for file in files: - if file.endswith('.cs'): - cs_files.append(os.path.join(root, file)) - - print(f"找到 {len(cs_files)} 个 C# 文件") - - updated_files = 0 - total_replacements = 0 - - for file_path in cs_files: - replacements = update_file(file_path) - if replacements > 0: - updated_files += 1 - total_replacements += replacements - print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)") - - print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换") - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +""" +更新所有 C# 文件中的命名空间声明和 using 语句 +""" + +import os +import re +from pathlib import Path + +ROOT_DIR = "/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework" + +# 命名空间替换规则(按优先级排序,长的先匹配) +NAMESPACE_RULES = [ + # CQRS 子命名空间 + (r'\.cqrs\.notification\b', '.CQRS.Notification'), + (r'\.cqrs\.command\b', '.CQRS.Command'), + (r'\.cqrs\.request\b', '.CQRS.Request'), + (r'\.cqrs\.query\b', '.CQRS.Query'), + (r'\.cqrs\.behaviors\b', '.CQRS.Behaviors'), + (r'\.cqrs\b', '.CQRS'), + + # 嵌套命名空间 + (r'\.coroutine\.instructions\b', '.Coroutine.Instructions'), + (r'\.coroutine\.extensions\b', '.Coroutine.Extensions'), + (r'\.coroutine\b', '.Coroutine'), + + (r'\.events\.filters\b', '.Events.Filters'), + (r'\.events\b', '.Events'), + + (r'\.logging\.appenders\b', '.Logging.Appenders'), + (r'\.logging\.filters\b', '.Logging.Filters'), + (r'\.logging\.formatters\b', '.Logging.Formatters'), + (r'\.logging\b', '.Logging'), + + (r'\.functional\.async\b', '.Functional.Async'), + (r'\.functional\.control\b', '.Functional.Control'), + (r'\.functional\.functions\b', '.Functional.Functions'), + (r'\.functional\.pipe\b', '.Functional.Pipe'), + (r'\.functional\.result\b', '.Functional.Result'), + (r'\.functional\b', '.Functional'), + + (r'\.services\.modules\b', '.Services.Modules'), + (r'\.services\b', '.Services'), + + (r'\.extensions\.signal\b', '.Extensions.Signal'), + (r'\.extensions\b', '.Extensions'), + + (r'\.setting\.data\b', '.Setting.Data'), + (r'\.setting\.events\b', '.Setting.Events'), + (r'\.setting\b', '.Setting'), + + (r'\.scene\.handler\b', '.Scene.Handler'), + (r'\.scene\b', '.Scene'), + + (r'\.ui\.handler\b', '.UI.Handler'), + (r'\.ui\b', '.UI'), + + (r'\.data\.events\b', '.Data.Events'), + (r'\.data\b', '.Data'), + + # 单层命名空间 + (r'\.architecture\b', '.Architecture'), + (r'\.bases\b', '.Bases'), + (r'\.command\b', '.Command'), + (r'\.configuration\b', '.Configuration'), + (r'\.constants\b', '.Constants'), + (r'\.enums\b', '.Enums'), + (r'\.environment\b', '.Environment'), + (r'\.internals\b', '.Internals'), + (r'\.ioc\b', '.IoC'), + (r'\.lifecycle\b', '.Lifecycle'), + (r'\.model\b', '.Model'), + (r'\.pause\b', '.Pause'), + (r'\.pool\b', '.Pool'), + (r'\.properties\b', '.Properties'), + (r'\.property\b', '.Property'), + (r'\.query\b', '.Query'), + (r'\.registries\b', '.Registries'), + (r'\.resource\b', '.Resource'), + (r'\.rule\b', '.Rule'), + (r'\.serializer\b', '.Serializer'), + (r'\.state\b', '.State'), + (r'\.storage\b', '.Storage'), + (r'\.system\b', '.System'), + (r'\.time\b', '.Time'), + (r'\.utility\b', '.Utility'), + (r'\.versioning\b', '.Versioning'), + (r'\.asset\b', '.Asset'), + (r'\.components\b', '.Components'), + (r'\.systems\b', '.Systems'), + (r'\.ecs\b', '.ECS'), + (r'\.integration\b', '.Integration'), + (r'\.mediator\b', '.Mediator'), + (r'\.tests\b', '.Tests'), + (r'\.analyzers\b', '.Analyzers'), + (r'\.diagnostics\b', '.Diagnostics'), + (r'\.generator\b', '.Generator'), + (r'\.info\b', '.Info'), +] + +def update_file(file_path): + """更新单个文件中的命名空间""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + original_content = content + replacements = 0 + + for pattern, replacement in NAMESPACE_RULES: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + content = re.sub(pattern, replacement, content, flags=re.IGNORECASE) + replacements += len(matches) + + if content != original_content: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + return replacements + + return 0 + except Exception as e: + print(f"错误处理文件 {file_path}: {e}") + return 0 + +def main(): + print("开始更新命名空间...") + + # 查找所有 C# 文件 + cs_files = [] + for root, dirs, files in os.walk(ROOT_DIR): + # 跳过 bin, obj, Generated 目录 + dirs[:] = [d for d in dirs if d not in ['bin', 'obj', 'Generated', '.git', 'node_modules']] + + for file in files: + if file.endswith('.cs'): + cs_files.append(os.path.join(root, file)) + + print(f"找到 {len(cs_files)} 个 C# 文件") + + updated_files = 0 + total_replacements = 0 + + for file_path in cs_files: + replacements = update_file(file_path) + if replacements > 0: + updated_files += 1 + total_replacements += replacements + print(f"更新: {os.path.basename(file_path)} ({replacements} 处替换)") + + print(f"\n完成!更新了 {updated_files} 个文件,共 {total_replacements} 处替换") + +if __name__ == '__main__': + main()