Compare commits

...

8 Commits

Author SHA1 Message Date
gewuyou
3109beaa9b
Merge pull request #194 from GeWuYou/feat/docs-init-vitepress-config
feat(docs): 初始化 GFramework 文档网站配置
2026-04-07 14:34:30 +08:00
GeWuYou
ff4f92c6d7 feat(docs): 初始化 GFramework 文档网站配置
- 添加 VitePress 主题样式文件,自定义颜色、按钮、首页、自定义块等组件样式
- 配置深蓝色品牌色彩方案,包括文字、悬停和背景色
- 实现首页英雄区域渐变效果和响应式图像模糊滤镜
- 集成本地搜索功能,支持中文界面翻译和搜索提示
- 创建安全泛型转义插件,防止 Markdown 中的尖括号被误解析
- 设置多语言导航菜单,包含入门指南、Core、Game、Godot 等模块链接
- 构建完整的侧边栏结构,覆盖核心框架、游戏模块、源码生成器等所有功能区域
- 配置教程、最佳实践、API参考等学习资源分类
- 添加页脚版权信息、社交链接和返回顶部功能
- 优化移动端和桌面端的搜索框显示适配
2026-04-07 14:03:01 +08:00
gewuyou
d645e8a338
Merge pull request #193 from GeWuYou/chore/coderabbit-balanced-to-chill
chore(config): 调整 CodeRabbit 配置以简化审查设置
2026-04-07 12:54:34 +08:00
GeWuYou
ade735ed4a feat(config): 添加 GitHub Checks 工具配置
- 在 .coderabbit.yaml 中新增 tools 配置块
- 启用 github-checks 工具
- 设置 github-checks 超时时间为 90 秒
- 保留原有的 auto_review 配置设置
2026-04-07 12:50:08 +08:00
GeWuYou
ed6c13f151 chore(config): 调整 CodeRabbit 配置以简化审查设置
- 将审查配置文件从 balanced 更改为 chill 以降低严格度
- 移除 github-checks 工具配置
- 保持自动审查功能启用状态
- 维持现有的请求更改工作流程和摘要设置
2026-04-07 12:49:32 +08:00
gewuyou
b3e484632d
Merge pull request #191 from GeWuYou/feat/config-system-with-source-generator
feat(config): 添加游戏内容配置系统及源代码生成器
2026-04-07 09:18:28 +08:00
GeWuYou
0564428d69 chore(config): 更新 CodeRabbit 配置以优化代码审查设置
- 添加 YAML 语言服务器 schema 指定
- 设置语言为简体中文
- 配置审查参数包括高阶总结、状态展示及详细问题显示
- 开启请求修改工作流
- 配置工具与 GitHub 检查功能
- 设置超时时间为 90 秒
- 启用自动审查并排除草稿 PR
- 开启聊天自动回复功能
2026-04-07 08:59:28 +08:00
GeWuYou
ca82b2701c feat(config): 添加游戏内容配置系统及源代码生成器
- 实现基于 YAML 的静态游戏内容配置管理
- 集成 JSON Schema 结构描述与校验功能
- 提供一对象一文件的目录组织方式
- 支持运行时只读查询与类型安全访问
- 添加 Source Generator 自动生成配置类型和表包装
- 实现 VS Code 插件提供配置浏览与编辑功能
- 添加热重载支持开发期实时配置更新
- 提供官方启动帮助器简化初始化流程
- 支持跨表引用校验与关联表联动重载
- 实现强类型查询辅助方法提升开发效率
2026-04-07 08:38:06 +08:00
8 changed files with 425 additions and 72 deletions

View File

@ -1,5 +1,21 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "zh-CN"
early_access: false
reviews:
profile: "chill"
request_changes_workflow: true # 有问题时可以直接 request changes
high_level_summary: true # PR 总体总结
review_status: true # review 结果状态
review_details: true # 展示具体问题
poem: false # 关闭诗歌(基本没人用)
tools:
github-checks:
enabled: true
timeout_ms: 90000
timeout_ms: 90000
auto_review:
enabled: true
drafts: false # draft PR 不 review
chat:
auto_reply: true

View File

@ -138,6 +138,135 @@ public class SchemaConfigGeneratorTests
});
}
/// <summary>
/// 验证 schema 顶层允许通过元数据覆盖默认配置目录,并会统一路径分隔符。
/// </summary>
[Test]
public void Run_Should_Use_Custom_Config_Path_Metadata_For_Generated_Registration()
{
const string source = """
using System;
using System.Collections.Generic;
namespace GFramework.Game.Abstractions.Config
{
public interface IConfigTable
{
Type KeyType { get; }
Type ValueType { get; }
int Count { get; }
}
public interface IConfigTable<TKey, TValue> : IConfigTable
where TKey : notnull
{
TValue Get(TKey key);
bool TryGet(TKey key, out TValue? value);
bool ContainsKey(TKey key);
IReadOnlyCollection<TValue> All();
}
public interface IConfigRegistry
{
IConfigTable<TKey, TValue> GetTable<TKey, TValue>(string name)
where TKey : notnull;
bool TryGetTable<TKey, TValue>(string name, out IConfigTable<TKey, TValue>? table)
where TKey : notnull;
}
}
namespace GFramework.Game.Config
{
public sealed class YamlConfigLoader
{
public YamlConfigLoader RegisterTable<TKey, TValue>(
string tableName,
string relativePath,
string schemaRelativePath,
Func<TValue, TKey> keySelector,
IEqualityComparer<TKey>? comparer = null)
where TKey : notnull
{
return this;
}
}
}
""";
const string schema = """
{
"type": "object",
"x-gframework-config-path": "config\\monster",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var generatedSources = result.Results
.Single()
.GeneratedSources
.ToDictionary(
static sourceResult => sourceResult.HintName,
static sourceResult => sourceResult.SourceText.ToString(),
StringComparer.Ordinal);
Assert.That(result.Results.Single().Diagnostics, Is.Empty);
Assert.That(generatedSources["MonsterConfigBindings.g.cs"],
Does.Contain("public const string ConfigRelativePath = \"config/monster\";"));
Assert.That(generatedSources["MonsterConfigBindings.g.cs"], Does.Contain("Metadata.ConfigRelativePath,"));
Assert.That(generatedSources["GeneratedConfigCatalog.g.cs"],
Does.Contain("MonsterConfigBindings.Metadata.ConfigRelativePath"));
}
/// <summary>
/// 验证 schema 顶层自定义配置目录元数据不能逃逸配置根目录。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Custom_Config_Path_Metadata_Is_Invalid()
{
const string source = """
namespace TestApp
{
public sealed class Dummy
{
}
}
""";
const string schema = """
{
"type": "object",
"x-gframework-config-path": "../monster",
"required": ["id"],
"properties": {
"id": { "type": "integer" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
("monster.schema.json", schema));
var diagnostic = result.Results.Single().Diagnostics.Single();
Assert.Multiple(() =>
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_007"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("x-gframework-config-path"));
Assert.That(diagnostic.GetMessage(), Does.Contain("relative"));
});
}
/// <summary>
/// 验证引用元数据成员名在不同路径规范化后发生碰撞时,生成器仍会分配全局唯一的成员名。
/// </summary>
@ -452,20 +581,33 @@ public class SchemaConfigGeneratorTests
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigCatalog"));
Assert.That(catalogSource, Does.Contain("public sealed class GeneratedConfigRegistrationOptions"));
Assert.That(catalogSource, Does.Contain("public static class GeneratedConfigRegistrationExtensions"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedConfigDomains { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IReadOnlyCollection<string>? IncludedTableNames { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"public global::System.Predicate<GeneratedConfigCatalog.TableMetadata>? TableFilter { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IEqualityComparer<string>? ItemComparer { get; init; }"));
Assert.That(catalogSource,
Does.Contain(
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))"));
Assert.That(catalogSource,
Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))"));
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);"));
Assert.That(catalogSource, Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
Assert.That(catalogSource,
Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
Assert.That(catalogSource,
Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
Assert.That(catalogSource, Does.Contain("private static bool ShouldRegisterTable("));
Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList("));
});

View File

@ -3,30 +3,31 @@
### New Rules
Rule ID | Category | Severity | Notes
-----------------------|------------------------------------|----------|-------------------------
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer
Rule ID | Category | Severity | Notes
----------------------------|------------------------------------|----------|--------------------------------
GF_Logging_001 | GFramework.Godot.logging | Warning | LoggerDiagnostics
GF_Rule_001 | GFramework.SourceGenerators.rule | Error | ContextAwareDiagnostic
GF_ContextGet_001 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_002 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_003 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_004 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_005 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_006 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_007 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_004 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_005 | GFramework.Priority | Error | PriorityDiagnostic
GF_Priority_Usage_001 | GFramework.Usage | Info | PriorityUsageAnalyzer

View File

@ -10,6 +10,7 @@ namespace GFramework.SourceGenerators.Config;
[Generator]
public sealed class SchemaConfigGenerator : IIncrementalGenerator
{
private const string ConfigPathMetadataKey = "x-gframework-config-path";
private const string GeneratedNamespace = "GFramework.Game.Config.Generated";
/// <inheritdoc />
@ -147,6 +148,12 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
}
var schemaBaseName = GetSchemaBaseName(file.Path);
var configRelativePath = ResolveConfigRelativePath(file.Path, root, schemaBaseName);
if (configRelativePath.Diagnostic is not null)
{
return SchemaParseResult.FromDiagnostic(configRelativePath.Diagnostic);
}
var schema = new SchemaFileSpec(
Path.GetFileName(file.Path),
entityName,
@ -156,7 +163,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
idProperty.TypeSpec.ClrType.TrimEnd('?'),
idProperty.PropertyName,
schemaBaseName,
schemaBaseName,
configRelativePath.Path!,
GetSchemaRelativePath(file.Path),
TryGetMetadataString(root, "title"),
TryGetMetadataString(root, "description"),
@ -991,7 +998,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Initializes one generated table metadata entry.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"configDomain\">Logical config domain derived from the schema base name.</param>");
builder.AppendLine(
" /// <param name=\"configDomain\">Logical config domain derived from the schema base name.</param>");
builder.AppendLine(" /// <param name=\"tableName\">Runtime registration name.</param>");
builder.AppendLine(" /// <param name=\"configRelativePath\">Relative YAML directory path.</param>");
builder.AppendLine(" /// <param name=\"schemaRelativePath\">Relative schema file path.</param>");
@ -1017,12 +1025,14 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" public string ConfigDomain { get; }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the runtime registration name used by <see cref=\"global::GFramework.Game.Config.YamlConfigLoader\" />.");
builder.AppendLine(
" /// Gets the runtime registration name used by <see cref=\"global::GFramework.Game.Config.YamlConfigLoader\" />.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public string TableName { get; }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets the relative directory that stores YAML files for the generated config table.");
builder.AppendLine(
" /// Gets the relative directory that stores YAML files for the generated config table.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" public string ConfigRelativePath { get; }");
builder.AppendLine();
@ -1033,7 +1043,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// Gets metadata for every generated config table in the current consumer project.");
builder.AppendLine(
" /// Gets metadata for every generated config table in the current consumer project.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
" public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> Tables { get; } = global::System.Array.AsReadOnly(new TableMetadata[]");
@ -1145,7 +1156,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
" /// Registers all generated config tables using schema-derived conventions so bootstrap code can stay one-line even as schemas grow.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"loader\">Target YAML config loader.</param>");
builder.AppendLine(" /// <returns>The same loader instance after all generated table registrations have been applied.</returns>");
builder.AppendLine(
" /// <returns>The same loader instance after all generated table registrations have been applied.</returns>");
builder.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"loader\"/> is null.</exception>");
builder.AppendLine(
@ -1167,7 +1179,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" /// <param name=\"loader\">Target YAML config loader.</param>");
builder.AppendLine(
" /// <param name=\"options\">Optional per-table overrides for aggregate registration; when null, all tables use their default comparer behavior.</param>");
builder.AppendLine(" /// <returns>The same loader instance after all generated table registrations have been applied.</returns>");
builder.AppendLine(
" /// <returns>The same loader instance after all generated table registrations have been applied.</returns>");
builder.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"loader\"/> is null.</exception>");
builder.AppendLine(
@ -1189,7 +1202,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(
$" if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], options))");
builder.AppendLine(" {");
builder.AppendLine($" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
builder.AppendLine(
$" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);");
builder.AppendLine(" }");
builder.AppendLine();
}
@ -1202,7 +1216,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
" /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"metadata\">Generated table metadata under consideration.</param>");
builder.AppendLine(" /// <param name=\"options\">Aggregate registration options supplied by the caller.</param>");
builder.AppendLine(
" /// <param name=\"options\">Aggregate registration options supplied by the caller.</param>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when the generated table should be registered; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(
@ -1212,7 +1227,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" {");
builder.AppendLine(
" // Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.");
builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
builder.AppendLine(
" if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
builder.AppendLine(" {");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
@ -1393,7 +1409,8 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" /// <param name=\"value\">The property value to match.</param>");
builder.AppendLine(
" /// <param name=\"result\">The first matching config entry when lookup succeeds; otherwise <see langword=\"null\" />.</param>");
builder.AppendLine(" /// <returns><see langword=\"true\" /> when a matching config entry is found; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when a matching config entry is found; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(" /// <remarks>");
builder.AppendLine(
" /// The generated helper walks the same snapshot exposed by <see cref=\"All\"/> and returns the first match in iteration order.");
@ -1767,6 +1784,98 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
return string.IsNullOrWhiteSpace(value) ? null : value;
}
/// <summary>
/// 解析 schema 顶层配置目录元数据。
/// </summary>
/// <param name="filePath">Schema 文件路径。</param>
/// <param name="element">Schema 顶层节点。</param>
/// <param name="defaultRelativePath">默认配置目录。</param>
/// <returns>最终使用的配置目录或诊断。</returns>
private static (string? Path, Diagnostic? Diagnostic) ResolveConfigRelativePath(
string filePath,
JsonElement element,
string defaultRelativePath)
{
if (!element.TryGetProperty(ConfigPathMetadataKey, out var configPathElement))
{
return (defaultRelativePath, null);
}
if (configPathElement.ValueKind != JsonValueKind.String)
{
return (
null,
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConfigRelativePathMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
ConfigPathMetadataKey,
$"Expected a JSON string but found '{configPathElement.ValueKind}'."));
}
var configuredPath = configPathElement.GetString();
if (string.IsNullOrWhiteSpace(configuredPath))
{
return (
null,
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConfigRelativePathMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
ConfigPathMetadataKey,
"Path cannot be null, empty, or whitespace."));
}
var normalizedPath = NormalizeConfigRelativePath(configuredPath!);
if (normalizedPath is null)
{
return (
null,
Diagnostic.Create(
ConfigSchemaDiagnostics.InvalidConfigRelativePathMetadata,
CreateFileLocation(filePath),
Path.GetFileName(filePath),
ConfigPathMetadataKey,
"Path must be relative and cannot contain '..' segments."));
}
return (normalizedPath, null);
}
/// <summary>
/// 标准化配置目录元数据,统一斜杠并拒绝逃逸配置根目录的写法。
/// </summary>
/// <param name="configuredPath">Schema 中声明的相对目录。</param>
/// <returns>标准化后的相对目录;无效时返回空。</returns>
private static string? NormalizeConfigRelativePath(string configuredPath)
{
var normalizedPath = configuredPath.Replace('\\', '/').Trim();
if (string.IsNullOrWhiteSpace(normalizedPath) ||
normalizedPath.StartsWith("/", StringComparison.Ordinal) ||
Path.IsPathRooted(normalizedPath))
{
return null;
}
var normalizedSegments = new List<string>();
foreach (var segment in normalizedPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))
{
if (string.Equals(segment, ".", StringComparison.Ordinal))
{
continue;
}
if (string.Equals(segment, "..", StringComparison.Ordinal))
{
return null;
}
normalizedSegments.Add(segment);
}
return normalizedSegments.Count == 0 ? null : string.Join("/", normalizedSegments);
}
/// <summary>
/// 为标量字段构建可直接生成到属性上的默认值初始化器。
/// </summary>

View File

@ -74,4 +74,15 @@ public static class ConfigSchemaDiagnostics
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
}
/// <summary>
/// schema 顶层自定义配置目录元数据无效。
/// </summary>
public static readonly DiagnosticDescriptor InvalidConfigRelativePathMetadata = new(
"GF_ConfigSchema_007",
"Config schema uses invalid custom config path metadata",
"Schema file '{0}' uses invalid '{1}' metadata: {2}",
SourceGeneratorsConfigCategory,
DiagnosticSeverity.Error,
true);
}

View File

@ -1,5 +1,26 @@
import { defineConfig } from 'vitepress'
const localSearch = {
provider: 'local' as const,
options: {
translations: {
button: {
buttonText: '搜索',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭'
}
}
}
}
}
function safeGenericEscapePlugin() {
return {
name: 'safe-generic-escape',
@ -62,6 +83,10 @@ export default defineConfig({
chunkSizeWarningLimit: 1000
}
},
themeConfig: {
// 在顶层保留搜索配置,避免构建期只读取站点级配置时把搜索入口裁掉。
search: localSearch
},
/** 多语言 */
locales: {
root: {
@ -71,41 +96,21 @@ export default defineConfig({
themeConfig: {
logo: '/logo-icon.png',
search: {
provider: 'local',
options: {
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
noResultsText: '无法找到相关结果',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭'
}
}
}
}
},
search: localSearch,
nav: [
{ text: '首页', link: '/zh-CN/' },
{ text: '入门指南', link: '/zh-CN/getting-started' },
{ text: 'Core', link: '/zh-CN/core/' },
{ text: 'ECS', link: '/zh-CN/ecs/' },
{ text: 'Game', link: '/zh-CN/game/' },
{ text: 'Godot', link: '/zh-CN/godot/' },
{ text: '源码生成器', link: '/zh-CN/source-generators' },
{ text: '教程', link: '/zh-CN/tutorials/' },
{ text: '最佳实践', link: '/zh-CN/best-practices/' },
{
text: '更多',
items: [
{ text: 'ECS', link: '/zh-CN/ecs/' },
{ text: '源码生成器', link: '/zh-CN/source-generators' },
{ text: '最佳实践', link: '/zh-CN/best-practices/' },
{ text: 'API 参考', link: '/zh-CN/api-reference' },
{ text: '常见问题', link: '/zh-CN/faq' },
{ text: '故障排查', link: '/zh-CN/troubleshooting' },

View File

@ -132,3 +132,46 @@
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Navigation
* -------------------------------------------------------------------------- */
@media (min-width: 768px) and (max-width: 1279px) {
/* Keep the search entry visible on medium desktop widths where nav links are the tightest. */
.VPNavBarSearch {
flex: 0 0 auto;
padding-left: 16px;
}
.VPNavBarSearch .VPNavBarSearchButton {
min-width: 0;
padding: 8px 10px;
}
.VPNavBarSearch .keys {
display: none;
}
.VPNavBarMenu {
min-width: 0;
}
.VPNavBarMenu .VPNavBarMenuLink,
.VPNavBarMenu .VPFlyout .button {
padding-left: 10px;
padding-right: 10px;
font-size: 13px;
}
}
@media (min-width: 1280px) {
/* Reserve stable room for the search button before appearance and social actions appear. */
.VPNavBarSearch {
min-width: 176px;
}
.VPNavBarSearch .VPNavBarSearchButton {
justify-content: space-between;
min-width: 176px;
}
}

View File

@ -37,6 +37,7 @@ GameProject/
{
"title": "Monster Config",
"description": "定义怪物静态配置。",
"x-gframework-config-path": "config/monster",
"type": "object",
"required": ["id", "name"],
"properties": {
@ -71,6 +72,31 @@ GameProject/
}
```
顶层可选元数据:
- `x-gframework-config-path`:覆盖生成器默认的配置目录。未声明时,默认使用 schema 基名,例如
`monster.schema.json -> monster`
例如项目希望继续把 YAML 放在 `config/monster/*.yaml` 下,而不是根目录 `monster/*.yaml`,可以这样声明:
```json
{
"type": "object",
"x-gframework-config-path": "config/monster",
"required": ["id"],
"properties": {
"id": { "type": "integer" }
}
}
```
约束如下:
- 必须是 JSON 字符串
- 必须是相对路径
- 不允许包含 `..`
- 生成器会把反斜杠标准化为 `/`
## YAML 示例
```yaml