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
74 changes: 73 additions & 1 deletion GameFrameX.Apps/Common/Session/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
using GameFrameX.Core.Actors;
using GameFrameX.Core.Events;
using GameFrameX.Foundation.Localization.Core;
using GameFrameX.Foundation.Logger;
using GameFrameX.NetWork.Abstractions;
using GameFrameX.NetWork.RemoteMessaging.Routing;
using GameFrameX.Utility.Setting;

namespace GameFrameX.Apps.Common.Session;
Expand All @@ -47,6 +49,26 @@ public static class SessionManager
private static readonly ConcurrentDictionary<string, Session> SessionMap = new();
private static readonly ConcurrentDictionary<long, SessionRouteSnapshot> PlayerRouteMap = new();

/// <summary>
/// 玩家路由外发同步目标(C143e D21:控制库写入钩子)。
/// </summary>
/// <remarks>
/// The outbound player-route sync target (C143e D21). Wired by
/// <see cref="MongoPlayerRouteResolverBootstrap"/> once the control database
/// is registered; default is <see cref="NullPlayerRouteSyncTarget.Instance"/>
/// so the in-process-only launch flow keeps working with zero side effects.
/// Callers must catch and swallow any exception from the hook — the local
/// <see cref="PlayerRouteMap"/> stays the single source of truth for the
/// process and a failed Mongo write is recovered on the next SetOnline.
/// </remarks>
public static IPlayerRouteSyncTarget PlayerRouteSyncTarget
{
get { return _playerRouteSyncTarget; }
set { _playerRouteSyncTarget = value ?? NullPlayerRouteSyncTarget.Instance; }
}

private static IPlayerRouteSyncTarget _playerRouteSyncTarget = NullPlayerRouteSyncTarget.Instance;

/// <summary>
/// 获取当前在线玩家的数量。
/// </summary>
Expand Down Expand Up @@ -275,10 +297,13 @@ public static void SetPlayerRouteOnline(long playerId, string serverType = null,
: serverType;
var resolvedServerId = serverId ?? (GlobalSettings.CurrentSetting?.ServerId ?? GameServerConst.Game.Id);

PlayerRouteMap.AddOrUpdate(
var snapshot = PlayerRouteMap.AddOrUpdate(
playerId,
_ => SessionRouteSnapshot.Online(playerId, resolvedServerType, resolvedServerId, 1),
(_, old) => SessionRouteSnapshot.Online(playerId, resolvedServerType, resolvedServerId, old.Version + 1));

// C143e D21:内存态落定后再调控制库钩子;失败 swallow(warning),主链路不挂。
FireSyncUpsert(playerId, resolvedServerType, snapshot);
}

/// <summary>
Expand All @@ -296,6 +321,53 @@ public static void SetPlayerRouteOffline(long playerId)
playerId,
_ => SessionRouteSnapshot.Offline(playerId, 1),
(_, old) => SessionRouteSnapshot.Offline(playerId, old.Version + 1));

// C143e D21:内存态落定后再调控制库钩子;失败 swallow(warning),主链路不挂。
FireSyncDelete(playerId);
}

private static void FireSyncUpsert(long playerId, string resolvedServerType, SessionRouteSnapshot snapshot)
{
var syncTarget = _playerRouteSyncTarget;
if (syncTarget == null || ReferenceEquals(syncTarget, NullPlayerRouteSyncTarget.Instance))
{
return;
}

// 钩子实例 ID:进程未注册发现层时退化为主机名 + 时间戳(与 MongoEndpointRegistry.CreateSelfDescriptorFromEnvironment 同形态)。
var instanceId = ResolveSyncInstanceId(resolvedServerType);
try
{
_ = syncTarget.UpsertAsync(playerId, instanceId, resolvedServerType, snapshot.Version);
}
catch (Exception exception)
{
LogHelper.Warning(exception, "[SessionManager] player_route upsert hook failed for player {playerId}; the local PlayerRouteMap stays authoritative and the next SetOnline converges", playerId);
}
}

private static void FireSyncDelete(long playerId)
{
var syncTarget = _playerRouteSyncTarget;
if (syncTarget == null || ReferenceEquals(syncTarget, NullPlayerRouteSyncTarget.Instance))
{
return;
}

try
{
_ = syncTarget.DeleteAsync(playerId);
}
catch (Exception exception)
{
LogHelper.Warning(exception, "[SessionManager] player_route delete hook failed for player {playerId}", playerId);
}
}

private static string ResolveSyncInstanceId(string resolvedServerType)
{
// 优先用 Mongo 发现层实例 ID(运行时通过 PlayerRouteSyncTarget 注入点的静态字段不可见——保留退化路径)。
return $"{resolvedServerType}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds():x}";
}
}

Expand Down
44 changes: 44 additions & 0 deletions GameFrameX.Apps/Common/Session/SessionManagerFastPathAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// ==========================================================================================
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and 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.
// This project is licensed solely under the Apache License 2.0,
// please refer to the LICENSE file in the root directory of the source code for the full license text.
// ==========================================================================================


using GameFrameX.NetWork.RemoteMessaging.Routing;

namespace GameFrameX.Apps.Common.Session;

/// <summary>
/// SessionManager Tier 1 快路径适配器(C143e D21)。
/// </summary>
/// <remarks>
/// The Tier 1 player-route fast-path adapter (C143e D21). Wraps
/// <c>SessionManager.PlayerRouteMap</c> in the <see cref="IPlayerRouteFastPath"/>
/// contract so <see cref="MongoPlayerRouteResolver"/> can be activated from
/// inside <c>GameFrameX.NetWork.RemoteMessaging</c> without taking a reverse
/// project reference on <c>GameFrameX.Apps</c>.
/// </remarks>
public sealed class SessionManagerFastPathAdapter : IPlayerRouteFastPath
{
public static readonly SessionManagerFastPathAdapter Instance = new SessionManagerFastPathAdapter();

private SessionManagerFastPathAdapter()
{
}

/// <inheritdoc />
public bool TryGetOnline(long playerId, out PlayerRouteInfo info)
{
if (playerId > 0 && SessionManager.TryGetPlayerRoute(playerId, out var snapshot) && snapshot.IsOnline)
{
info = PlayerRouteInfo.Online(snapshot.ServerType, snapshot.ServerId, snapshot.Version);
return true;
}

info = PlayerRouteInfo.Offline();
return false;
}
}
13 changes: 7 additions & 6 deletions GameFrameX.Apps/GameFrameX.Apps.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\GameFrameX.Core\GameFrameX.Core.csproj" />
<ProjectReference Include="..\GameFrameX.DataBase\GameFrameX.DataBase.csproj" />
<ProjectReference Include="..\GameFrameX.Monitor\GameFrameX.Monitor.csproj" />
<ProjectReference Include="..\GameFrameX.Proto\GameFrameX.Proto.csproj" />
<ProjectReference Include="..\GameFrameX.Architecture.Analyzers\GameFrameX.Architecture.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ProjectReference Include="..\GameFrameX.Core\GameFrameX.Core.csproj" />
<ProjectReference Include="..\GameFrameX.DataBase\GameFrameX.DataBase.csproj" />
<ProjectReference Include="..\GameFrameX.Monitor\GameFrameX.Monitor.csproj" />
<ProjectReference Include="..\GameFrameX.Proto\GameFrameX.Proto.csproj" />
<ProjectReference Include="..\GameFrameX.NetWork.RemoteMessaging\GameFrameX.NetWork.RemoteMessaging.csproj" />
<ProjectReference Include="..\GameFrameX.Architecture.Analyzers\GameFrameX.Architecture.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<Folder Include="Account\" />
Expand Down
4 changes: 3 additions & 1 deletion GameFrameX.Launcher/StartUp/AppStartUpGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ public override async Task StartAsync()

// C143d D11-D15:控制库就绪后激活 Mongo 发现层——读侧 watcher + 写侧心跳(未配置广播端口时自动跳过)
// 并以真实 case 2/3 转发器重装跨 Role 路由缝(替换 C143c 占位)。幂等:多 Role 进程首个调用生效。
MongoDiscoveryRuntime.Activate(((MongoDbService)MultiDbRegistry.Get(MultiDbRegistry.ControlDatabaseName)).CurrentDatabase, RoleSet.Current);
// C143e D21:再激活玩家路由层(建 player_route 索引 + 装 SyncTarget),Tier 1 fast-path 注入 SessionManager 适配器。
MongoDiscoveryRuntime.Activate(((MongoDbService)MultiDbRegistry.Get(MultiDbRegistry.ControlDatabaseName)).CurrentDatabase, RoleSet.Current, GameFrameX.Apps.Common.Session.SessionManagerFastPathAdapter.Instance);
GameFrameX.Apps.Common.Session.SessionManager.PlayerRouteSyncTarget = GameFrameX.NetWork.RemoteMessaging.Routing.MongoPlayerRouteResolverBootstrap.SyncTarget;

var initResult = await GameDb.Init<MongoDbService>(Setting.DataBaseUrl, new DbOptions { Name = Setting.DataBaseName, IsUseTimeZone = Setting.IsUseTimeZone, });
if (initResult == false)
Expand Down
4 changes: 3 additions & 1 deletion GameFrameX.Launcher/StartUp/Social/AppStartUpSocial.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ public override async Task StartAsync()

// C143d D11-D15:控制库就绪后激活 Mongo 发现层——读侧 watcher + 写侧心跳(未配置广播端口时自动跳过)
// 并以真实 case 2/3 转发器重装跨 Role 路由缝(替换 C143c 占位)。幂等:多 Role 进程首个调用生效。
MongoDiscoveryRuntime.Activate(((MongoDbService)MultiDbRegistry.Get(MultiDbRegistry.ControlDatabaseName)).CurrentDatabase, RoleSet.Current);
// C143e D21:再激活玩家路由层(建 player_route 索引 + 装 SyncTarget),Tier 1 fast-path 注入 SessionManager 适配器。
MongoDiscoveryRuntime.Activate(((MongoDbService)MultiDbRegistry.Get(MultiDbRegistry.ControlDatabaseName)).CurrentDatabase, RoleSet.Current, GameFrameX.Apps.Common.Session.SessionManagerFastPathAdapter.Instance);
GameFrameX.Apps.Common.Session.SessionManager.PlayerRouteSyncTarget = GameFrameX.NetWork.RemoteMessaging.Routing.MongoPlayerRouteResolverBootstrap.SyncTarget;

var initResult = await GameDb.Init<MongoDbService>(Setting.DataBaseUrl, new DbOptions { Name = Setting.DataBaseName, IsUseTimeZone = Setting.IsUseTimeZone, });
if (initResult == false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ public static class MongoDiscoveryRuntime
/// </remarks>
/// <param name="controlDatabase">控制库(gameframex_control)/ The control database</param>
/// <param name="hostedRoleNames">本进程承载的 Role 名全集(RoleSet 快照)/ The full hosted role-name set (the RoleSet snapshot)</param>
/// <param name="playerRouteFastPath">Tier 1 玩家路由快路径提供方(apps 端 SessionManager 适配器;null 则跳过 Tier 1)/ Tier 1 fast path; null skips Tier 1</param>
/// <exception cref="ArgumentNullException">当 <paramref name="controlDatabase"/> 或 <paramref name="hostedRoleNames"/> 为 null 时抛出 / Thrown when controlDatabase or hostedRoleNames is null</exception>
public static void Activate(IMongoDatabase controlDatabase, IEnumerable<string> hostedRoleNames)
public static void Activate(IMongoDatabase controlDatabase, IEnumerable<string> hostedRoleNames, IPlayerRouteFastPath playerRouteFastPath = null)
{
ArgumentNullException.ThrowIfNull(controlDatabase, nameof(controlDatabase));
ArgumentNullException.ThrowIfNull(hostedRoleNames, nameof(hostedRoleNames));
Expand Down Expand Up @@ -118,6 +119,9 @@ public static void Activate(IMongoDatabase controlDatabase, IEnumerable<string>
}

RoleRouterHolder.Initialize(new InProcessRoleRouter(hostedRoles, null, new MongoDiscoveryRemoteRoleRouter(_watcher, new TcpEnvelopeForwarder())));
// C143e D21:玩家路由层装配(建索引 + 装 SyncTarget)。在路由缝激活后追加;
// 接收端 envelope 复投由 LocalEnvelopeDispatcher 承担,LocalEnvelopeDispatcher 在装配流程末尾(GameApp)按需注入,本类不直接重装 RoleRouterHolder。
MongoPlayerRouteResolverBootstrap.Attach(controlDatabase, playerRouteFastPath).GetAwaiter().GetResult();
}

/// <summary>
Expand Down
37 changes: 37 additions & 0 deletions GameFrameX.NetWork.RemoteMessaging/Routing/IPlayerRouteFastPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// ==========================================================================================
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and 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.
// This project is licensed solely under the Apache License 2.0,
// please refer to the LICENSE file in the root directory of the source code for the full license text.
// ==========================================================================================


namespace GameFrameX.NetWork.RemoteMessaging.Routing;

/// <summary>
/// 玩家路由 Tier 1 快路径提供方(C143e D21)。
/// </summary>
/// <remarks>
/// The Tier 1 player-route fast-path provider (C143e D21). The default
/// implementation in <c>GameFrameX.Apps</c> is the in-process
/// <c>SessionManager.PlayerRouteMap</c>; it is injected into
/// <see cref="MongoPlayerRouteResolver"/> at launch time so the resolver does
/// not depend on the host application layer. Returning <c>false</c> (or
/// <paramref name="info"/> with <see cref="PlayerRouteInfo.IsOnline"/> = false)

Check warning on line 21 in GameFrameX.NetWork.RemoteMessaging/Routing/IPlayerRouteFastPath.cs

View workflow job for this annotation

GitHub Actions / topology-equivalence

XML comment on 'IPlayerRouteFastPath' has a paramref tag for 'info', but there is no parameter by that name
/// makes the resolver fall through to the Tier 2 control-database lookup.
/// </remarks>
public interface IPlayerRouteFastPath
{
/// <summary>
/// 查询玩家是否在本进程在线。
/// </summary>
/// <remarks>
/// Checks whether the player is locally online. Returning false makes
/// the resolver fall through to Tier 2.
/// </remarks>
/// <param name="playerId">玩家 ID / The player id</param>
/// <param name="info">在线态返回本地缓存的路由信息 / The locally cached route info when online</param>
/// <returns>是否本地在线 / Whether the player is locally online</returns>
bool TryGetOnline(long playerId, out PlayerRouteInfo info);
}
36 changes: 36 additions & 0 deletions GameFrameX.NetWork.RemoteMessaging/Routing/IPlayerRouteResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// ==========================================================================================
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and 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.
// This project is licensed solely under the Apache License 2.0,
// please refer to the LICENSE file in the root directory of the source code for the full license text.
// ==========================================================================================


namespace GameFrameX.NetWork.RemoteMessaging.Routing;

/// <summary>
/// 玩家路由解析抽象(C143e D21)。
/// </summary>
/// <remarks>
/// The player-route resolver contract (C143e D21). Implementations resolve a
/// player's current logical location (which <c>role</c> + numeric <c>serverId</c>
/// holds them, or that they are offline) by looking through some set of tiers —
/// typically in-process memory first, then a shared control store. The default
/// Mongo-backed implementation is <see cref="MongoPlayerRouteResolver"/>; the
/// bootstrap swaps it in once the control database is registered. Returning a
/// cached or negative answer is allowed: the caller treats
/// <see cref="PlayerRouteInfo.IsOnline"/> as the binding signal.
/// </remarks>
public interface IPlayerRouteResolver
{
/// <summary>
/// 解析玩家当前路由位置。
/// </summary>
/// <remarks>
/// Resolves the player's current routing location.
/// </remarks>
/// <param name="playerId">玩家 ID / The player id</param>
/// <returns>玩家路由信息 — 在线时含 role + serverId;离线时仅 IsOnline=false / The route info — online carries role + serverId; offline returns IsOnline=false</returns>
Task<PlayerRouteInfo> ResolveAsync(long playerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// ==========================================================================================
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and 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.
// This project is licensed solely under the Apache License 2.0,
// please refer to the LICENSE file in the root directory of the source code for the full license text.
// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
// or infringe upon the legitimate rights and interests of others, as prohibited by laws and regulations!
// 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.
// ==========================================================================================


namespace GameFrameX.NetWork.RemoteMessaging.Routing;

/// <summary>
/// 玩家路由外发同步目标(C143e D21:SessionManager 钩子的可注入端)。
/// </summary>
/// <remarks>
/// The outbound sync target for the SessionManager player-route hooks (C143e D21).
/// The hook is async-by-design so the Mongo implementation can <c>await</c>
/// the CAS upsert; the NoOp default returns immediately. Callers (SessionManager)
/// must catch and swallow exceptions themselves — the inline hook contract is
/// "best effort, never throws". This interface deliberately exposes primitive
/// fields instead of a struct/DTO to keep GameFrameX.Apps free of Mongo types
/// (the SyncTarget type lives in RemoteMessaging, which Apps already references).
/// </remarks>
public interface IPlayerRouteSyncTarget
{
/// <summary>
/// 把 (playerId, instanceId, role, version) 原子写入控制库 player_route(version CAS)。
/// </summary>
/// <remarks>
/// Atomically writes the (playerId, instanceId, role, version) tuple into the
/// control-database <c>player_route</c> collection using <c>version</c> as the
/// CAS key. The Mongo implementation throws <see cref="PlayerRouteStaleException"/>
/// when the persisted version is already ahead of the supplied value.
/// </remarks>
/// <param name="playerId">玩家 ID / Player id</param>
/// <param name="instanceId">实例 ID / Instance id</param>
/// <param name="role">Role 名 / Role name</param>
/// <param name="version">顶号版本号 / Kick/relogin version</param>
/// <returns>异步任务 / Async task</returns>
Task UpsertAsync(long playerId, string instanceId, string role, long version);

/// <summary>
/// 从控制库 player_route 删除该玩家路由。
/// </summary>
/// <remarks>
/// Removes the player's route document from the control-database
/// <c>player_route</c> collection. Missing documents are silently ignored
/// (idempotent semantics; a stale cleanup is harmless).
/// </remarks>
/// <param name="playerId">玩家 ID / Player id</param>
/// <returns>异步任务 / Async task</returns>
Task DeleteAsync(long playerId);
}
Loading
Loading