Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
### Added

* Capability 架构护栏(C241):GFX0017 禁止 Apps/Hotfix 的直接及传递编译引用依赖实现包(保留 Online.Abstractions 及其合法 Hosting 契约闭包);GFX0018 以符号检查能力注册、builder 装配与三族桥 Publish/Retract 的所属边界,保留模块辅助注册、Launcher Wiring 与独立测试夹具。Capability 工程统一接入分析器;架构分析器 Roslyn 引用对齐 .NET 10 SDK 基线,防止 CS9057 跳过规则;Online.Persistence 针对 C249 域属行状态仅本工程豁免旧 GFX0001。静态检查不证明反射、动态加载、跨请求缓存或启停时序。
* Online 存储模式无声降级防护(C302,GFX-1015):`Configs/app_config.json` Game Role 的 Online 段补 `UseGameDbStores: true`(原先缺省走 InMemory,重启丢玩家身份/好友/资产/存储数据);启用 Online 能力族(双闸任一开)但未显式声明存储模式时组合根发出启动期 Warning(新本地化键 `Capability.Online.UseGameDbStoresUndeclared`,判定收敛为纯谓词 `OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared` 供单测断言);不 fail-fast,保护既有无键部署。
* 两层会话协同与真实断线恢复(C204):Core 物理连接 / Online 逻辑会话 / Online 对局成员三层职责与标识映射冻结——身份恢复凭据 = Online 会话 Token(`RespPlayerLogin` 新增 `SessionToken` 字段断线前交付;桥不可用为空、登录主流程不挂),新连接携凭据经新增 220-18/19 `ReqSessionResume`/`RespSessionResume`(Auth 族)恢复:验证(指纹/终态/到期/作用域)→ 连接替换(`OnlineSessionManager.MarkResumedAsync` 新面,不放松既有状态机)→ 原子轮换(旧凭据即刻失效,重放显式 TokenRevoked)→ Core 绑定(`UpdateSession` 顶号编排复用)→ 在线簿记还原 → 对局恢复(`IOnlineMatchDomain` 增 `NotifyPlayerDisconnectedAsync`/`TryResumeForPlayerAsync`——按票据→分配登记→运行时成员表逐级核对,服务端留存 rt- 令牌回灌 `ResumeAsync` 三重校验原样生效,快照+增量/回退,不重新匹配、不另开对局、不重复结算;无对局 = 空回执、超窗 NetworkTimeout、已结束 StateEnded、已离局 ScopeDenied)。`Capability.Online.Abstractions` 新增 `IOnlineSessionReconnectContract` + `OnlineSessionDomainBridgeHolder/Lease`(C246 匹配域桥同型);Foundation `OnlineSessionReconnectService` 实现(不另建第二凭据权威——Token 签发/校验/轮换全部委托既有 Token 服务)+ `session-reconnect-window-sweep` 清扫 tick;`PlayerSessionManager.Remove` 带路由下线守卫(旧断连回调迟到不清新路由);`OnDisconnected` 接入两层协同(会话域开窗 + 对局域标记断线成员,旧回调迟到守卫跳过);KCP 首消息白名单补 `ReqSessionResume`(与角色登录同为鉴权完成信号);Bot `--scenario=match` 扩展断线恢复演练(断线→携凭据恢复同身份同对局→旧凭据重放被拒→幂等重入同一局证据链)。宿主重启 = InMemory 会话/票据/对局失忆,恢复承诺终止于进程存活期(不宣称跨重启无损恢复)。


Expand Down Expand Up @@ -68,6 +69,7 @@

### Breaking

* `OnlineCapabilityOptions.UseGameDbStores` 由 `bool` 改三态 `bool?`(C302):null = 配置未声明(触发启动期告警后按 InMemory 处理)、true = 落库、false = 显式选择易失;消费方需按可空布尔判读(仓内调用点已同批迁移)。
* Capability 能力注入机制重设计(C294,GFX-998,破坏性一次性切换,能力未上线无兼容层):
- 模块契约改为三段生命周期:`ICapabilityModule` 新增 `ActivateAsync(CapabilityActivationContext, CancellationToken)` / `DeactivateAsync(CancellationToken)`;激活按拓扑序执行、中途失败逆序回滚,停止为停调度 → 逆序 Deactivate → 机制自动撤销全部桥。四桥发布、榜单预建、撮合通知订阅、旧好友数据迁移迁入所属能力模块。
- 桥机制化:泛型 `CapabilityBridge<T>.TryAcquire` / `CapabilityBridgeLease<T>`(撤销 / 替换后抛 `CapabilityBridgeUnavailableException`,族层映射 8002),发布只经 `CapabilityActivationContext.Bridges.Publish<T>`,同契约重复发布 fail-fast;删除 `Online.Abstractions/Bridges/` 四对 holder/lease 与 Launcher 四个 `*BridgeWiring`。
Expand Down
1 change: 1 addition & 0 deletions Configs/app_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"EnableRuntime": true,
"EnableAdminApi": true,
"EnableRoles": ["Game"],
"UseGameDbStores": true,
"Settings": {}
},
"MinModuleId": 10,
Expand Down
36 changes: 34 additions & 2 deletions GameFrameX.Launcher/Online/OnlineCapabilityCompositionRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
using GameFrameX.Capability.Online.Persistence.Social;
using GameFrameX.Capability.Online.Persistence.Storage;
using GameFrameX.Capability.Online.Social;
using GameFrameX.Foundation.Localization.Core;
using GameFrameX.Foundation.Logger;
using GameFrameX.Localization;
using GameFrameX.Utility.Setting;

namespace GameFrameX.Launcher.Online;
Expand Down Expand Up @@ -182,15 +184,20 @@ public static async Task ComposeIfEnabledAsync(AppSetting setting)
options.Override<IOnlineHotfixRollbackExecutor>(new HotfixManagerRollbackExecutor(setting));
options.Override<IOnlinePlayerDirectory>(new HostPlayerDirectory());

// GameDb 一级 store 覆盖(Online.UseGameDbStores=true 时预置五个持久化适配器;业务库已就绪由模板顺序保证)。
if (online.UseGameDbStores)
// GameDb 一级 store 覆盖(Online.UseGameDbStores 三态,C302):true 预置五个持久化适配器(业务库已就绪由模板顺序保证);
// 未声明(null)= 无声降级防护位——启用族但未声明存储模式时告警(不 fail-fast,保护既有无键部署),随后保持 InMemory 默认。
if (online.UseGameDbStores == true)
{
options.Override<IOnlineIdentityStore>(new GameDbOnlineIdentityStore());
options.Override<IOnlinePlayerStorageStore>(new GameDbOnlinePlayerStorageStore());
options.Override<IOnlineAssetStore>(new GameDbOnlineAssetStore());
options.Override<IOnlineAssetTransactionStore>(new GameDbOnlineAssetTransactionStore());
options.Override<IOnlineFriendStore>(new GameDbOnlineFriendStore());
}
else if (ShouldWarnStoreModeUndeclared(online))
{
LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Capability.OnlineModule.UseGameDbStoresUndeclared, setting.ServerType));
}

_runtime = CapabilityComposition.Compose(new OnlineCapabilityFamily(), options);
_composerServerType = setting.ServerType;
Expand All @@ -206,6 +213,31 @@ public static async Task ComposeIfEnabledAsync(AppSetting setting)
}
}

/// <summary>
/// 判定是否需要发出「存储模式未声明」启动期告警(C302 无声降级防护):
/// 族被启用(双闸任一开)但 <c>Online.UseGameDbStores</c> 未在配置中显式声明(null)时为 true。
/// <para>为什么不直接在组合点内联判定:三态口径(未声明 / 显式 true / 显式 false)是配置契约本身,
/// 收敛为纯谓词供单测直接断言,避免只能靠日志输出验证信号。</para>
/// <para>显式 false(有意选择 InMemory,如本地无库开发)与双闸全关(族未启用,零装配零日志)均不告警。</para>
/// </summary>
/// <remarks>
/// Determines whether the "storage mode undeclared" startup warning must fire (the C302
/// silent-degradation guard): true when the family is enabled (either gate on) yet
/// <c>Online.UseGameDbStores</c> was never explicitly declared in configuration (null). The
/// tri-state contract (undeclared / explicit true / explicit false) is collapsed into this pure
/// predicate so unit tests can assert it directly instead of scraping log output; an explicit
/// false (a deliberate in-memory choice, e.g. database-less local development) and both gates off
/// (family disabled: nothing assembled, nothing logged) never warn.
/// </remarks>
/// <param name="online">当前 Role 的 Online 配置段。</param>
/// <returns>未声明存储模式且族已启用时为 true;否则为 false。</returns>
public static bool ShouldWarnStoreModeUndeclared(OnlineCapabilityOptions online)
{
return online != null
&& (online.EnableRuntime || online.EnableAdminApi)
&& online.UseGameDbStores == null;
}

/// <summary>
/// 组合失败路径的尽力停止:撤回已启动部分(桥 / 调度 / 管理面),停止自身的异常只记日志,
/// 不得覆盖触发回滚的原异常。
Expand Down
11 changes: 11 additions & 0 deletions GameFrameX.Localization/Keys.Capability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,17 @@ public static class Assembly
/// </summary>
public static class OnlineModule
{

/// <summary>
/// Online 存储模式未声明告警
/// </summary>
/// <remarks>
/// 键名: Capability.Online.UseGameDbStoresUndeclared
/// 用途: 启用 Online 能力族(双闸任一开)但未显式声明 UseGameDbStores 时组合根发出的启动期告警(C302 无声降级防护)
/// 参数: {0} - 组合 Role 的 ServerType
/// </remarks>
public const string UseGameDbStoresUndeclared = "Capability.Online.UseGameDbStoresUndeclared";

/// <summary>
/// online-match(对局)能力相关消息
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions GameFrameX.Localization/Localization/Messages/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,9 @@
<data name="Capability.Online.Match.OptionalAssetsAbsent" xml:space="preserve">
<value>match settlement reward granting (settlement results are still recorded)</value>
</data>
<data name="Capability.Online.UseGameDbStoresUndeclared" xml:space="preserve">
<value>Role {0} enables the Online capability family but never declares Online.UseGameDbStores: level-one stores will use the in-process in-memory implementations and data is lost on restart; declare true (persisted) or false (explicitly volatile) in configuration for production.</value>
</data>
<data name="Capability.Online.Match.OptionalMatchmakingAbsent" xml:space="preserve">
<value>matchmaking domain (420 enqueue / cancel / status query)</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,9 @@
<data name="Capability.Online.Match.OptionalAssetsAbsent" xml:space="preserve">
<value>对局结算发奖(结算结果照常记录)</value>
</data>
<data name="Capability.Online.UseGameDbStoresUndeclared" xml:space="preserve">
<value>角色 {0} 已启用 Online 能力族但未声明 Online.UseGameDbStores:一级 store 将使用进程内 InMemory 实现,重启即失数据;生产环境请在配置中显式声明 true(落库)或 false(明确选择易失)。</value>
</data>
<data name="Capability.Online.Match.OptionalMatchmakingAbsent" xml:space="preserve">
<value>匹配域(420 入队 / 取消 / 状态查询)</value>
</data>
Expand Down
20 changes: 12 additions & 8 deletions GameFrameX.Utility/Setting/OnlineCapabilityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,23 @@ public sealed class OnlineCapabilityOptions
public IReadOnlyList<string> Disable { get; set; }

/// <summary>
/// 是否预置 Online 一级 store 的 GameDb 持久化实现(C294 起语义为宿主组合根 Override 注入开关)
/// <para>true = 宿主经 <c>Override&lt;T&gt;</c> 预置身份 / 玩家云存储 / 资产账本与交易 / 好友关系 store
/// (重启数据不丢);false(缺省)= 包内 InMemory 默认不变</para>
/// 是否预置 Online 一级 store 的 GameDb 持久化实现(C294 起语义为宿主组合根 Override 注入开关;C302 起为三态)
/// <para><see langword="true"/> = 宿主经 <c>Override&lt;T&gt;</c> 预置身份 / 玩家云存储 / 资产账本与交易 / 好友关系 store
/// (重启数据不丢);<see langword="false"/> = 显式选择包内 InMemory 默认;<see langword="null"/>(缺省)= 配置未声明——
/// 组合根对此形态发出启动期告警(无声降级防护,见 C302)后按 InMemory 处理</para>
/// <para>前置约束:开启前数据库必须已完成初始化(GameDb 门面 Init);Session / Presence 保持易失(Redis 演进位)</para>
/// </summary>
/// <remarks>
/// Whether the host composition root pre-seeds the GameDb persistence adapters for the Online
/// level-one stores via <c>Override&lt;T&gt;</c> (C294 semantics): true overrides the identity /
/// player-storage / asset-ledger and transaction / friend stores (data survives restarts); false
/// (default) keeps the in-package in-memory defaults. The database must be initialized (GameDb
/// The tri-state host composition-root switch for the GameDb persistence adapters of the Online
/// level-one stores (C294 semantics; tri-state since C302): <see langword="true"/> overrides the
/// identity / player-storage / asset-ledger and transaction / friend stores via
/// <c>Override&lt;T&gt;</c> (data survives restarts); <see langword="false"/> explicitly keeps the
/// in-package in-memory defaults; <see langword="null"/> (default) means the configuration never
/// declared the storage mode — the composition root warns at startup (the silent-degradation guard,
/// C302) and then falls back to the in-memory defaults. The database must be initialized (GameDb
/// facade Init) before enabling; sessions and presence stay volatile.
/// </remarks>
public bool UseGameDbStores { get; set; }
public bool? UseGameDbStores { get; set; }

/// <summary>
/// 能力级参数原始 JSON 节点(键 = 能力 Id(如 online-social),值 = 该能力 Options 根类型的字段覆盖源;
Expand Down
111 changes: 111 additions & 0 deletions Tests/GameFrameX.Tests/Online/OnlineCapabilityStoreModeWarningTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// ==========================================================================================
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents and other related rights
// 均受中华人民共和国及相关国际法律法规保护。
// are protected by the laws of the People's Republic of China and relevant international regulations.
// 使用本项目须严格遵守相关法律法规及开源许可证之规定。
// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
// 本项目采用 Apache License 2.0 单协议分发,
// This project is licensed solely under the Apache License 2.0,
// 完整许可证文本请参见源代码根目录的 LICENSE 文件。
// please refer to the LICENSE file at the source code repository root for the full license text.
// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
// 侵犯他人合法权益等法律法规所禁止之行为!
// Any legal disputes and liabilities arising from secondary development based on this project
// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
// GitHub 仓库:https://github.com/GameFrameX
// GitHub Repository: https://github.com/GameFrameX
// Gitee 仓库:https://gitee.com/GameFrameX
// Gitee Repository: https://gitee.com/cnblanblank
// CNB 仓库:https://cnb.cool
// CNB Repository: https://www.cnb.cool/GameFrameX
// 官方文档:https://gameframex.doc.alianblank.com/
// Official Documentation: https://gameframex.doc.alianblank.com/
// ==========================================================================================


using GameFrameX.Launcher.Online;
using GameFrameX.Utility.Setting;
using Xunit;

namespace GameFrameX.Tests.Online;

/// <summary>
/// Online 存储模式未声明告警判定测试(C302 无声降级防护):组合根纯谓词
/// <c>OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared</c> 的三态口径——
/// 族启用(双闸任一开)且 <c>UseGameDbStores</c> 未声明(null)才告警;
/// 显式 true / 显式 false / 双闸全关 / 配置段缺失均不告警。
/// </summary>
public class OnlineCapabilityStoreModeWarningTests
{
/// <summary>
/// 双闸开 + 未声明(null)→ 告警(原缺陷形态:Game Role 启用 Online 但配置无 UseGameDbStores 键)。
/// </summary>
[Fact]
public void Warns_WhenFamilyEnabledAndUseGameDbStoresUndeclared()
{
var online = new OnlineCapabilityOptions
{
EnableRuntime = true,
EnableAdminApi = true,
};

Assert.True(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(online));
}

/// <summary>
/// 双闸只开其一(admin-only 形态)+ 未声明 → 同样告警。
/// </summary>
[Fact]
public void Warns_WhenOnlyAdminGateEnabledAndUndeclared()
{
var online = new OnlineCapabilityOptions
{
EnableRuntime = false,
EnableAdminApi = true,
};

Assert.True(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(online));
}

/// <summary>
/// 显式声明(true 落库 / false 有意易失)→ 不告警:声明即消歧。
/// </summary>
[Fact]
public void DoesNotWarn_WhenUseGameDbStoresExplicitlyDeclared()
{
var persisted = new OnlineCapabilityOptions
{
EnableRuntime = true,
EnableAdminApi = true,
UseGameDbStores = true,
};

var volatileExplicit = new OnlineCapabilityOptions
{
EnableRuntime = true,
EnableAdminApi = true,
UseGameDbStores = false,
};

Assert.False(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(persisted));
Assert.False(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(volatileExplicit));
}

/// <summary>
/// 双闸全关(族未启用,零装配零日志)与配置段缺失 → 不告警。
/// </summary>
[Fact]
public void DoesNotWarn_WhenFamilyDisabledOrSectionMissing()
{
var disabled = new OnlineCapabilityOptions
{
EnableRuntime = false,
EnableAdminApi = false,
};

Assert.False(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(disabled));
Assert.False(OnlineCapabilityCompositionRoot.ShouldWarnStoreModeUndeclared(null));
}
}
Loading
Loading