feat(godot): 收敛 GodotLogger 宿主能力

- 新增 GodotLog、DeferredLogger 和配置自动发现、热重载接线。
- 修复已缓存 logger 的级别判定与输出路径,使动态配置生效。
- 更新文档与追踪记录,明确当前收敛边界和恢复点。
This commit is contained in:
gewuyou 2026-05-02 21:33:28 +08:00
parent 36e1ae5f32
commit 748bb714fb
10 changed files with 421 additions and 199 deletions

View File

@ -6,16 +6,42 @@ namespace GFramework.Core.Logging;
/// 日志抽象基类,封装日志级别判断、格式化与异常处理逻辑。
/// 平台日志器只需实现 Write 方法即可。
/// </summary>
public abstract class AbstractLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info) : IStructuredLogger
public abstract class AbstractLogger : IStructuredLogger
{
/// <summary>
/// 根日志记录器的名称常量
/// </summary>
public const string RootLoggerName = "ROOT";
private readonly string _name = name ?? RootLoggerName;
private readonly Func<LogLevel> _minLevelProvider;
private readonly string _name;
/// <summary>
/// 使用固定最小日志级别初始化日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <param name="minLevel">最小日志级别。</param>
protected AbstractLogger(
string? name = null,
LogLevel minLevel = LogLevel.Info)
: this(name, () => minLevel)
{
}
/// <summary>
/// 使用动态最小日志级别提供器初始化日志器。
/// </summary>
/// <param name="name">日志器名称。</param>
/// <param name="minLevelProvider">最小日志级别提供器。</param>
protected AbstractLogger(
string? name,
Func<LogLevel> minLevelProvider)
{
ArgumentNullException.ThrowIfNull(minLevelProvider);
_name = name ?? RootLoggerName;
_minLevelProvider = minLevelProvider;
}
#region Metadata
@ -47,7 +73,7 @@ public abstract class AbstractLogger(
/// <returns>如果指定级别大于等于最小级别则返回true否则返回false</returns>
protected bool IsEnabled(LogLevel level)
{
return level >= minLevel;
return level >= _minLevelProvider();
}
/// <summary>
@ -568,4 +594,4 @@ public abstract class AbstractLogger(
#region Core Pipeline (Private)
#endregion
}
}

View File

@ -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()
{

View File

@ -8,4 +8,5 @@ internal readonly record struct GodotLogRenderContext(
LogLevel Level,
string Category,
string Message,
string Color);
string Color,
string Properties);

View File

@ -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)),

View File

@ -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;
/// </summary>
public sealed class GodotLogger : AbstractLogger
{
private readonly GodotLoggerOptions _options;
private readonly Func<LogLevel> _minLevelProvider;
private readonly Func<GodotLoggerOptions> _optionsProvider;
/// <summary>
/// Initializes a logger that preserves the historical fixed-format template.
@ -18,7 +22,10 @@ public sealed class GodotLogger : AbstractLogger
/// <param name="name">The logger name.</param>
/// <param name="minLevel">The minimum enabled log level.</param>
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
/// <param name="name">The logger name.</param>
/// <param name="options">The logger options.</param>
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<GodotLoggerOptions> optionsProvider,
Func<LogLevel> minLevelProvider)
: base(name, minLevelProvider ?? throw new ArgumentNullException(nameof(minLevelProvider)))
{
_optionsProvider = optionsProvider ?? throw new ArgumentNullException(nameof(optionsProvider));
_minLevelProvider = minLevelProvider;
}
/// <summary>
@ -41,18 +60,59 @@ public sealed class GodotLogger : AbstractLogger
/// <param name="exception">The optional exception.</param>
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);
}
/// <summary>
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
/// </summary>
public override void Log(LogLevel level, string message, params (string Key, object? Value)[] properties)
{
if (level < _minLevelProvider())
{
return;
}
WriteEntry(level, message, exception: null, properties);
}
/// <summary>
/// Uses Godot-aware structured rendering instead of the base string concatenation fallback.
/// </summary>
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<string, object?> 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<string, object?>(contextProperties, StringComparer.Ordinal);
if (properties != null)
{
foreach (var property in properties)
{
merged[property.Key] = property.Value;
}
}
return merged;
}
private static readonly IReadOnlyDictionary<string, object?> EmptyProperties =
new Dictionary<string, object?>(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);

View File

@ -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;
/// </summary>
public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
{
private readonly ILoggerFactory _cachedFactory;
private readonly ConcurrentDictionary<string, ILogger> _loggers = new(StringComparer.Ordinal);
private readonly Func<GodotLoggerSettings> _settingsProvider;
/// <summary>
/// Initializes a Godot logger provider with the default logger factory.
/// </summary>
public GodotLoggerFactoryProvider()
: this(static () => GodotLoggerSettings.Default)
{
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory());
}
/// <summary>
@ -24,8 +25,13 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
/// </summary>
/// <param name="options">The logger options.</param>
public GodotLoggerFactoryProvider(GodotLoggerOptions options)
: this(CreateStaticSettingsProvider(options))
{
_cachedFactory = CreateCachedFactory(new GodotLoggerFactory(options));
}
internal GodotLoggerFactoryProvider(Func<GodotLoggerSettings> settingsProvider)
{
_settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider));
}
/// <summary>
@ -40,12 +46,31 @@ public sealed class GodotLoggerFactoryProvider : ILoggerFactoryProvider
/// <returns>A logger configured with <see cref="MinLevel"/>.</returns>
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<GodotLoggerSettings> CreateStaticSettingsProvider(GodotLoggerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var settings = GodotLoggerSettings.FromOptions(options);
return () => settings;
}
}

View File

@ -39,7 +39,7 @@ public sealed class GodotLoggerOptions
/// </summary>
#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
/// <summary>
@ -47,7 +47,7 @@ public sealed class GodotLoggerOptions
/// </summary>
#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
/// <summary>
@ -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<LogLevel, string>(DefaultColors)
};
}

View File

@ -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`

View File

@ -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<Main>();
```
它提供三件事:
- 在第一次真正创建 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<T>()`
### 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 模型搬进来
## 继续阅读

View File

@ -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()