diff --git a/GameFrameX.Core/Abstractions/Attribute/TimeOutAttribute.cs b/GameFrameX.Core/Abstractions/Attribute/TimeOutAttribute.cs
index 4ff7dd1c8..07dfac110 100644
--- a/GameFrameX.Core/Abstractions/Attribute/TimeOutAttribute.cs
+++ b/GameFrameX.Core/Abstractions/Attribute/TimeOutAttribute.cs
@@ -45,10 +45,10 @@ public TimeOutAttribute(int timeOut)
}
///
- /// 超时时间
+ /// 超时时间(取 TimeSpan 的总毫秒数,而非 0-999 的毫秒分量)
///
///
- public TimeOutAttribute(TimeSpan timeOut) : this(timeOut.Milliseconds)
+ public TimeOutAttribute(TimeSpan timeOut) : this((int)timeOut.TotalMilliseconds)
{
}
diff --git a/GameFrameX.Core/Actors/Actor.cs b/GameFrameX.Core/Actors/Actor.cs
index afa827d25..b10be3daa 100644
--- a/GameFrameX.Core/Actors/Actor.cs
+++ b/GameFrameX.Core/Actors/Actor.cs
@@ -323,13 +323,13 @@ public Task OnRecycleAsync()
{
try
{
- while (_onceRecycleCallbacks.First != null)
+ // ConcurrentQueue 排空:注册(AddOnceRecycleCallback)可在任意线程并发进行,
+ // 取出即执行,避免 LinkedList 并发读写的未定义行为
+ while (_onceRecycleCallbacks.TryDequeue(out var callback))
{
- var first = _onceRecycleCallbacks.First;
- _onceRecycleCallbacks.RemoveFirst();
try
{
- first.Value?.Invoke();
+ callback?.Invoke();
}
catch (Exception ex)
{
@@ -345,15 +345,11 @@ public Task OnRecycleAsync()
// Localization: Core.Actor.RecycleFailed - Actor回收过程异常 actorId:{0} actorType:{1} 异常:\n{2}
LogHelper.Error(ex, LocalizationService.GetString(Localization.Keys.Core.Actor.RecycleFailed, Id, Type, ex));
}
- finally
- {
- _onceRecycleCallbacks.Clear();
- }
return Task.CompletedTask;
}
- private readonly LinkedList _onceRecycleCallbacks = new LinkedList();
+ private readonly ConcurrentQueue _onceRecycleCallbacks = new ConcurrentQueue();
///
/// 添加一个在Actor回收时执行一次的回调事件
@@ -368,7 +364,7 @@ public Task OnRecycleAsync()
///
public void AddOnceRecycleCallback(Action action)
{
- _onceRecycleCallbacks.AddLast(action);
+ _onceRecycleCallbacks.Enqueue(action);
}
///
diff --git a/GameFrameX.Core/Actors/Impl/WorkerActor.cs b/GameFrameX.Core/Actors/Impl/WorkerActor.cs
index 0c607e3c1..a2ed9075f 100644
--- a/GameFrameX.Core/Actors/Impl/WorkerActor.cs
+++ b/GameFrameX.Core/Actors/Impl/WorkerActor.cs
@@ -218,7 +218,8 @@ private async Task InnerRunAsync(WorkWrapper wrapper)
private static InvalidOperationException CreateQueueRejectedException(WorkWrapper wrapper)
{
- return new InvalidOperationException($"WorkerActor queue rejected work. actorId: {wrapper.Owner?.Id}, trace: {GetTraceSafely(wrapper)}");
+ // Localization: CoreExceptions.WorkerActor.QueueRejected - WorkerActor 队列拒绝了工作单元。actorId:{0} trace:{1}
+ return new InvalidOperationException(LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.QueueRejected, wrapper.Owner?.Id, GetTraceSafely(wrapper)));
}
private static string GetTraceSafely(WorkWrapper wrapper)
@@ -229,7 +230,8 @@ private static string GetTraceSafely(WorkWrapper wrapper)
}
catch (Exception exception)
{
- return $"trace unavailable: {exception.Message}";
+ // Localization: CoreExceptions.WorkerActor.TraceUnavailable - trace不可用:{0}
+ return LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.TraceUnavailable, exception.Message);
}
}
@@ -284,7 +286,8 @@ private static void CompleteSendResult(Task sendTask, WorkWrapper wrapper,
if (sendTask.IsFaulted)
{
- CompleteFailedPost(wrapper, sendTask.Exception?.GetBaseException() ?? new InvalidOperationException("WorkerActor queue post failed."), completeOnFailure);
+ // Localization: CoreExceptions.WorkerActor.QueueFailed - WorkerActor 队列投递失败。actorId:{0} trace:{1}
+ CompleteFailedPost(wrapper, sendTask.Exception?.GetBaseException() ?? new InvalidOperationException(LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.QueueFailed, wrapper.Owner?.Id, GetTraceSafely(wrapper))), completeOnFailure);
return;
}
@@ -303,7 +306,8 @@ private static void CompleteRejectedPost(WorkWrapper wrapper, bool completeOnFai
return;
}
- Log.Warning(exception, "WorkerActor queue rejected fire-and-forget work");
+ // Localization: CoreExceptions.WorkerActor.QueueRejected - WorkerActor 队列拒绝了工作单元。actorId:{0} trace:{1}
+ Log.Warning(exception, LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.QueueRejected, wrapper.Owner?.Id, GetTraceSafely(wrapper)));
wrapper.ForceSetResult();
}
@@ -315,7 +319,8 @@ private static void CompleteCanceledPost(WorkWrapper wrapper, CancellationToken
return;
}
- Log.Warning("WorkerActor queue post was cancelled. actorId: {ActorId}, trace: {Trace}", wrapper.Owner?.Id, GetTraceSafely(wrapper));
+ // Localization: CoreExceptions.WorkerActor.QueueCancelled - WorkerActor 队列投递被取消。actorId:{0} trace:{1}
+ Log.Warning(LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.QueueCancelled, wrapper.Owner?.Id, GetTraceSafely(wrapper)));
wrapper.ForceSetResult();
}
@@ -327,7 +332,8 @@ private static void CompleteFailedPost(WorkWrapper wrapper, Exception exception,
return;
}
- Log.Warning(exception, "WorkerActor queue post failed. actorId: {ActorId}, trace: {Trace}", wrapper.Owner?.Id, GetTraceSafely(wrapper));
+ // Localization: CoreExceptions.WorkerActor.QueueFailed - WorkerActor 队列投递失败。actorId:{0} trace:{1}
+ Log.Warning(exception, LocalizationService.GetString(Localization.Keys.CoreExceptions.WorkerActor.QueueFailed, wrapper.Owner?.Id, GetTraceSafely(wrapper)));
wrapper.ForceSetResult();
}
diff --git a/GameFrameX.Core/BaseHandler/RPC/BaseRpcComponentHandler.cs b/GameFrameX.Core/BaseHandler/RPC/BaseRpcComponentHandler.cs
index e0e905d05..a978efb5c 100644
--- a/GameFrameX.Core/BaseHandler/RPC/BaseRpcComponentHandler.cs
+++ b/GameFrameX.Core/BaseHandler/RPC/BaseRpcComponentHandler.cs
@@ -150,13 +150,6 @@ private async Task ExecuteWorkAsync(CancellationToken cancellationToken)
await InnerActionCoreAsync(RequestMessage, response, cancellationToken);
response?.SetUniqueId(requestId);
}
- catch (RpcException e)
- {
- if (response != null)
- {
- response.ErrorCode = e.ErrorCode;
- }
- }
catch (Exception e)
{
// Localization: Core.MessageHandler.InnerActionError - 内部操作错误: {0} {1}
diff --git a/GameFrameX.Core/Config/BaseDataTable.cs b/GameFrameX.Core/Config/BaseDataTable.cs
index 065aec914..c22601400 100644
--- a/GameFrameX.Core/Config/BaseDataTable.cs
+++ b/GameFrameX.Core/Config/BaseDataTable.cs
@@ -299,12 +299,19 @@ public void ForEach(Action func)
///
///
/// Computes the projection value for all objects using the specified function and returns the maximum value.
+ /// 空表返回 default(Tk),不抛异常。
+ /// An empty table returns default(Tk) instead of throwing.
///
/// 投影值的类型,必须实现 IComparable 接口 / The type of the projection value, must implement IComparable
/// 应用于每个对象的投影函数 / The projection function to apply to each object
- /// 所有对象投影值中的最大值 / The maximum value among all projected values
+ /// 所有对象投影值中的最大值;空表返回 default(Tk) / The maximum value among all projected values; default(Tk) for an empty table
public Tk Max(Func func)
{
+ if (DataList.Count == 0)
+ {
+ return default;
+ }
+
return DataList.Max(func);
}
@@ -313,12 +320,19 @@ public Tk Max(Func func)
///
///
/// Computes the projection value for all objects using the specified function and returns the minimum value.
+ /// 空表返回 default(Tk),不抛异常。
+ /// An empty table returns default(Tk) instead of throwing.
///
/// 投影值的类型,必须实现 IComparable 接口 / The type of the projection value, must implement IComparable
/// 应用于每个对象的投影函数 / The projection function to apply to each object
- /// 所有对象投影值中的最小值 / The minimum value among all projected values
+ /// 所有对象投影值中的最小值;空表返回 default(Tk) / The minimum value among all projected values; default(Tk) for an empty table
public Tk Min(Func func)
{
+ if (DataList.Count == 0)
+ {
+ return default;
+ }
+
return DataList.Min(func);
}
@@ -405,6 +419,8 @@ private void EnsureFirstLastCache()
if (DataList.Count == 0)
{
+ // 空表也置位缓存标志:首尾缓存结果(null)一次落定,不再每次访问重算
+ _cacheInitialized = true;
return;
}
@@ -444,14 +460,9 @@ private void EnsureCountCache()
return;
}
- var count = Math.Max(LongDataMaps.Count, StringDataMaps.Count);
- if (count == 0)
- {
- _countCache = 0;
- return;
- }
-
- _countCache = count;
+ // 条目数事实源是 DataList:两键字典可能只登记部分键型,取字典 Max 会低估条目数;
+ // 空表同样置位标志,避免每次访问重算
+ _countCache = DataList.Count;
_countCacheInitialized = true;
}
diff --git a/GameFrameX.Core/Config/ByteBuf.cs b/GameFrameX.Core/Config/ByteBuf.cs
index 691e7b091..ddb0876d1 100644
--- a/GameFrameX.Core/Config/ByteBuf.cs
+++ b/GameFrameX.Core/Config/ByteBuf.cs
@@ -1877,6 +1877,15 @@ public void ReadSegment(out int startIndex, out int segmentSize)
{
EnsureRead(1);
segmentSize = ((h & 0x3f) << 8) | Bytes[ReaderIndex];
+
+ // 段长来自数据本身:先校验可读范围再搬运尾部字节(long 算术防 int 溢出出负),
+ // 损坏数据统一抛 SerializationException 而非越界异常
+ // Localization: CoreConfig.ByteBuf.SegmentDataNotEnough - 段数据不足
+ if ((long)ReaderIndex + segmentSize + 1 > WriterIndex)
+ {
+ throw new SerializationException(LocalizationService.GetString(Localization.Keys.CoreConfig.SegmentDataNotEnough));
+ }
+
var endPos = ReaderIndex + segmentSize;
Bytes[ReaderIndex] = Bytes[endPos];
ReaderIndex += segmentSize + 1;
@@ -1885,6 +1894,15 @@ public void ReadSegment(out int startIndex, out int segmentSize)
{
EnsureRead(2);
segmentSize = ((h & 0x1f) << 16) | ((int)Bytes[ReaderIndex] << 8) | Bytes[ReaderIndex + 1];
+
+ // 段长来自数据本身:先校验可读范围再搬运尾部字节(long 算术防 int 溢出出负),
+ // 损坏数据统一抛 SerializationException 而非越界异常
+ // Localization: CoreConfig.ByteBuf.SegmentDataNotEnough - 段数据不足
+ if ((long)ReaderIndex + segmentSize + 2 > WriterIndex)
+ {
+ throw new SerializationException(LocalizationService.GetString(Localization.Keys.CoreConfig.SegmentDataNotEnough));
+ }
+
var endPos = ReaderIndex + segmentSize;
Bytes[ReaderIndex] = Bytes[endPos];
Bytes[ReaderIndex + 1] = Bytes[endPos + 1];
@@ -1894,6 +1912,15 @@ public void ReadSegment(out int startIndex, out int segmentSize)
{
EnsureRead(3);
segmentSize = ((h & 0x0f) << 24) | ((int)Bytes[ReaderIndex] << 16) | ((int)Bytes[ReaderIndex + 1] << 8) | Bytes[ReaderIndex + 2];
+
+ // 段长来自数据本身:先校验可读范围再搬运尾部字节(long 算术防 int 溢出出负),
+ // 损坏数据统一抛 SerializationException 而非越界异常
+ // Localization: CoreConfig.ByteBuf.SegmentDataNotEnough - 段数据不足
+ if ((long)ReaderIndex + segmentSize + 3 > WriterIndex)
+ {
+ throw new SerializationException(LocalizationService.GetString(Localization.Keys.CoreConfig.SegmentDataNotEnough));
+ }
+
var endPos = ReaderIndex + segmentSize;
Bytes[ReaderIndex] = Bytes[endPos];
Bytes[ReaderIndex + 1] = Bytes[endPos + 1];
@@ -1906,6 +1933,7 @@ public void ReadSegment(out int startIndex, out int segmentSize)
throw new SerializationException(LocalizationService.GetString(Localization.Keys.CoreConfig.ExceedMaxSize));
}
+ // 前置校验后此断言理论上不再触发,保留兜底(单字节段分支仍依赖它)
if (ReaderIndex > WriterIndex)
{
// Localization: CoreConfig.ByteBuf.SegmentDataNotEnough - 段数据不足
diff --git a/GameFrameX.Core/Config/StringUtil.cs b/GameFrameX.Core/Config/StringUtil.cs
index d7c6e090b..15341fb1c 100644
--- a/GameFrameX.Core/Config/StringUtil.cs
+++ b/GameFrameX.Core/Config/StringUtil.cs
@@ -40,10 +40,10 @@ namespace GameFrameX.Core.Config;
public static class StringUtil
{
///
- /// 将对象转换为字符串表示形式。
+ /// 将对象转换为字符串表示形式。调试辅助方法:反射遍历全部字段与属性,避免用于热路径。
///
///
- /// Converts an object to its string representation.
+ /// Converts an object to its string representation. Debug helper: reflects over all fields and properties; avoid hot paths.
///
/// 要转换的对象 / The object to convert
/// 对象的字符串表示形式 / The string representation of the object
@@ -53,10 +53,12 @@ public static string ToStr(object o)
}
///
- /// 将对象转换为字符串表示形式,并使用提供的StringBuilder进行构建。
+ /// 将对象转换为字符串表示形式,并使用提供的StringBuilder进行构建。调试辅助方法:反射遍历全部字段与属性,避免用于热路径。
///
///
- /// Converts an object to its string representation using the provided StringBuilder.
+ /// Converts an object to its string representation using the provided StringBuilder. Debug helper: reflects over all fields and properties; avoid hot paths.
+ /// 无 getter 或带参数的索引器属性会被跳过,不抛异常。
+ /// Properties without a getter or indexer properties are skipped instead of throwing.
///
/// 要转换的对象 / The object to convert
/// 用于构建字符串的StringBuilder / The StringBuilder used to build the string
@@ -70,6 +72,12 @@ public static string ToStr(object o, StringBuilder sb)
foreach (var p in o.GetType().GetProperties())
{
+ // 无 getter 或索引器属性不可无参取值,跳过而非抛异常
+ if (!p.CanRead || p.GetIndexParameters().Length > 0)
+ {
+ continue;
+ }
+
sb.Append($"{p.Name} = {p.GetValue(o)},");
}
diff --git a/GameFrameX.Core/Events/EventDispatcher.cs b/GameFrameX.Core/Events/EventDispatcher.cs
index bfb7051cf..55baa2c53 100644
--- a/GameFrameX.Core/Events/EventDispatcher.cs
+++ b/GameFrameX.Core/Events/EventDispatcher.cs
@@ -142,8 +142,8 @@ internal static async Task DispatchToActorAsync(IActor actor, long actorId, Game
var listeners = HotfixManager.Resolver.FindListeners(actor.Type, eventArgs.GetType());
if (listeners.IsNullOrEmpty())
{
- // Localization: Core.EventDispatcher.DispatchNoListeners - EventDispatcher.Dispatch Actor {0} {1} 事件类型:{2} 没有找到任何监听者
- LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Core.EventDispatcher.DispatchNoListeners, actorId, eventArgs.GetType().Name, eventArgs.GetType().Name));
+ // Localization: Core.EventDispatcher.DispatchNoListeners - EventDispatcher.Dispatch Actor {0} 类型:{1} 事件类型:{2} 没有找到任何监听者
+ LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Core.EventDispatcher.DispatchNoListeners, actorId, actor.Type, eventArgs.GetType().Name));
return;
}
@@ -167,8 +167,8 @@ private static void DispatchGlobal(long actorId, GameEventArgs eventArgs)
var listeners = HotfixManager.Resolver.FindListeners(eventArgs.GetType());
if (listeners.IsNullOrEmpty())
{
- // Localization: Core.EventDispatcher.DispatchNoListeners - EventDispatcher.Dispatch Actor {0} {1} 事件类型:{2} 没有找到任何监听者
- LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Core.EventDispatcher.DispatchNoListeners, actorId, eventArgs.GetType().Name, eventArgs.GetType().Name));
+ // Localization: Core.EventDispatcher.DispatchNoListeners - EventDispatcher.Dispatch Actor {0} 类型:{1} 事件类型:{2} 没有找到任何监听者
+ LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Core.EventDispatcher.DispatchNoListeners, actorId, ActorIdGenerator.GetActorType(actorId), eventArgs.GetType().Name));
return;
}
@@ -222,6 +222,14 @@ private static async Task DispatchListenerInActorAsync(IActor actor, IEventListe
{
// 获取监听器对应的组件代理
var componentAgent = await actor.GetComponentAgentAsync(listener.AgentType, false);
+
+ if (componentAgent == null)
+ {
+ // 组件未注册到该 Actor(isNew:false 未命中):显式跳过并告警,绝不以 null 代理调用监听器
+ // Localization: Core.EventDispatcher.DispatchOwnerActorNotLoaded - EventDispatcher.Dispatch Actor {0} 事件类型:{1} 监听器 {2} 所属 Actor 不在内存中,已跳过
+ LogHelper.Warning(LocalizationService.GetString(Localization.Keys.Core.EventDispatcher.DispatchOwnerActorNotLoaded, actorId, eventArgs.GetType().Name, listener.GetType().FullName));
+ return;
+ }
try
{
// 调用监听器的事件处理方法
diff --git a/GameFrameX.Core/Hotfix/HotfixLoadOptions.cs b/GameFrameX.Core/Hotfix/HotfixLoadOptions.cs
index 08bce5849..3717709ac 100644
--- a/GameFrameX.Core/Hotfix/HotfixLoadOptions.cs
+++ b/GameFrameX.Core/Hotfix/HotfixLoadOptions.cs
@@ -27,6 +27,8 @@
// Official Documentation: https://gameframex.doc.alianblank.com/
// ==========================================================================================
+using GameFrameX.Foundation.Localization.Core;
+
namespace GameFrameX.Core.Hotfix;
///
@@ -103,7 +105,8 @@ public string ResolveAssemblyFilePath()
// The version flows in from HTTP rollback traffic: reject separators and ".." so the load path cannot be escaped
if (Version.IndexOfAny(ForbiddenVersionChars) >= 0 || Version.Contains("..", StringComparison.Ordinal))
{
- throw new ArgumentException($"Version 含非法路径字符: {Version} / Version contains forbidden path characters: {Version}", nameof(Version));
+ // Localization: CoreExceptions.Hotfix.InvalidVersionChars - Version 含非法路径字符:{0}
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.CoreExceptions.HotfixModule.InvalidVersionChars, Version), nameof(Version));
}
directory = Version;
diff --git a/GameFrameX.Core/Hotfix/HotfixModule.cs b/GameFrameX.Core/Hotfix/HotfixModule.cs
index 1b64068a4..5df6182fb 100644
--- a/GameFrameX.Core/Hotfix/HotfixModule.cs
+++ b/GameFrameX.Core/Hotfix/HotfixModule.cs
@@ -616,15 +616,15 @@ internal BaseHttpHandler GetHttpHandler(string cmd)
internal List GetListHttpHandler()
{
var values = _httpHandlerMap.Values;
+ var seenHandlers = new HashSet();
var list = new List(values.Count / 2);
foreach (var handler in values)
{
- if (list.Contains(handler))
+ // 同一处理器实例可注册多个 cmd,引用去重线性完成
+ if (seenHandlers.Add(handler))
{
- continue;
+ list.Add(handler);
}
-
- list.Add(handler);
}
return list;
@@ -693,7 +693,21 @@ internal List FindListeners(Type eventArgsType)
/// 实例对象。
internal T GetInstance(string typeName)
{
- return (T)_typeCacheMap.GetOrAdd(typeName, k => HotfixAssembly.CreateInstance(k));
+ if (!_typeCacheMap.TryGetValue(typeName, out var value))
+ {
+ value = HotfixAssembly.CreateInstance(typeName);
+ if (value == null)
+ {
+ // ConcurrentDictionary 不接受 null 值,工厂返回 null 会抛脱离语境的 ArgumentNullException;
+ // 此处显式抛带类型名的契约异常
+ // Localization: CoreExceptions.Hotfix.TypeNotFound - 未找到类型:{0}
+ throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.CoreExceptions.HotfixModule.TypeNotFound, typeName));
+ }
+
+ _typeCacheMap[typeName] = value;
+ }
+
+ return (T)value;
}
///
diff --git a/GameFrameX.Core/Session/PlayerSessionManager.cs b/GameFrameX.Core/Session/PlayerSessionManager.cs
index ae15dc33c..32f04ead3 100644
--- a/GameFrameX.Core/Session/PlayerSessionManager.cs
+++ b/GameFrameX.Core/Session/PlayerSessionManager.cs
@@ -35,6 +35,8 @@
using GameFrameX.Foundation.Logger;
using GameFrameX.Network.Abstractions;
using GameFrameX.Utility.Setting;
+using GameFrameX.Foundation.Localization.Core;
+
namespace GameFrameX.Core.Session;
@@ -153,13 +155,7 @@ public void KickOffLineByPlayerId(long playerId)
/// 对应的会话对象,如果不存在则返回null。
public IPlayerSession GetByPlayerId(long playerId)
{
- var roleSession = Get(m => m.PlayerId == playerId);
- if (roleSession != null && ActorManager.HasActor(roleSession.PlayerId))
- {
- return roleSession;
- }
-
- return roleSession;
+ return Get(m => m.PlayerId == playerId);
}
///
@@ -487,7 +483,8 @@ private static async Task SyncUpsertSafeAsync(IPlayerRouteSyncTarget syncTarget,
}
catch (Exception exception)
{
- LogHelper.Warning(exception, "[PlayerSessionManager] player_route upsert hook failed for player {playerId}; the local PlayerRouteMap stays authoritative and the next SetOnline converges", playerId);
+ // Localization: Core.Session.RouteSyncUpsertFailed - [PlayerSessionManager] 玩家路由 upsert 同步钩子失败,玩家:{0};本地 PlayerRouteMap 仍为事实源,下次 SetOnline 会重新收敛
+ LogHelper.Warning(exception, LocalizationService.GetString(Localization.Keys.Core.Session.RouteSyncUpsertFailed, playerId));
}
}
@@ -521,7 +518,8 @@ private static async Task SyncDeleteSafeAsync(IPlayerRouteSyncTarget syncTarget,
}
catch (Exception exception)
{
- LogHelper.Warning(exception, "[PlayerSessionManager] player_route delete hook failed for player {playerId}", playerId);
+ // Localization: Core.Session.RouteSyncDeleteFailed - [PlayerSessionManager] 玩家路由 delete 同步钩子失败,玩家:{0}
+ LogHelper.Warning(exception, LocalizationService.GetString(Localization.Keys.Core.Session.RouteSyncDeleteFailed, playerId));
}
}
}
\ No newline at end of file
diff --git a/GameFrameX.Core/Timer/GlobalTimer.cs b/GameFrameX.Core/Timer/GlobalTimer.cs
index fd936ee7e..dd04bf248 100644
--- a/GameFrameX.Core/Timer/GlobalTimer.cs
+++ b/GameFrameX.Core/Timer/GlobalTimer.cs
@@ -138,7 +138,9 @@ private static long NextSaveTime()
///
public static async Task StopAsync()
{
- Console.WriteLine(LocalizationService.GetString(Localization.Keys.Core.Timer.GlobalTimerStopStart));
+ // Localization: Core.Timer.GlobalTimerStopStart - 全局定时器开始停止
+ LogHelper.Info(LocalizationService.GetString(Localization.Keys.Core.Timer.GlobalTimerStopStart));
+
IsWorking = false;
if (_loopTask != null)
{
@@ -147,6 +149,7 @@ public static async Task StopAsync()
await StateComponent.SaveAllAsync(true);
await GameDb.CloseAsync();
- Console.WriteLine(LocalizationService.GetString(Localization.Keys.Core.Timer.GlobalTimerStopComplete));
+ // Localization: Core.Timer.GlobalTimerStopComplete - 全局定时器停止完成
+ LogHelper.Info(LocalizationService.GetString(Localization.Keys.Core.Timer.GlobalTimerStopComplete));
}
}
\ No newline at end of file
diff --git a/GameFrameX.Localization/Keys.Core.cs b/GameFrameX.Localization/Keys.Core.cs
index e68853ec9..bd9f188e0 100644
--- a/GameFrameX.Localization/Keys.Core.cs
+++ b/GameFrameX.Localization/Keys.Core.cs
@@ -778,12 +778,12 @@ public static class HotfixModule
public static class EventDispatcher
{
///
- /// EventDispatcher.Dispatch Actor {0} {1} 事件类型:{2} 没有找到任何监听者
+ /// EventDispatcher.Dispatch Actor {0} 类型:{1} 事件类型:{2} 没有找到任何监听者
///
///
/// 键名: Core.EventDispatcher.DispatchNoListeners
/// 用途: 事件分发未找到监听者时记录
- /// 参数: {0} - Actor ID, {1} - 事件参数类型, {2} - 事件参数类型
+ /// 参数: {0} - Actor ID, {1} - Actor 类型, {2} - 事件参数类型
///
public const string DispatchNoListeners = "Core.EventDispatcher.DispatchNoListeners";
@@ -807,6 +807,32 @@ public static class EventDispatcher
///
public const string DispatchOwnerActorNotLoaded = "Core.EventDispatcher.DispatchOwnerActorNotLoaded";
}
+
+ ///
+ /// 会话管理相关资源键
+ ///
+ public static class Session
+ {
+ ///
+ /// [PlayerSessionManager] 玩家路由 upsert 同步钩子失败,玩家:{0};本地 PlayerRouteMap 仍为事实源,下次 SetOnline 会重新收敛
+ ///
+ ///
+ /// 键名: Core.Session.RouteSyncUpsertFailed
+ /// 用途: 会话路由同步 upsert 钩子(控制库)失败时记录
+ /// 参数: {0} - 玩家 ID
+ ///
+ public const string RouteSyncUpsertFailed = "Core.Session.RouteSyncUpsertFailed";
+
+ ///
+ /// [PlayerSessionManager] 玩家路由 delete 同步钩子失败,玩家:{0}
+ ///
+ ///
+ /// 键名: Core.Session.RouteSyncDeleteFailed
+ /// 用途: 会话路由同步 delete 钩子(控制库)失败时记录
+ /// 参数: {0} - 玩家 ID
+ ///
+ public const string RouteSyncDeleteFailed = "Core.Session.RouteSyncDeleteFailed";
+ }
}
///
@@ -859,6 +885,36 @@ public static class WorkerActor
/// 参数: {0} - 获取追踪信息时发生的异常消息
///
public const string TraceUnavailable = "CoreExceptions.WorkerActor.TraceUnavailable";
+
+ ///
+ /// WorkerActor 队列拒绝了工作单元。actorId:{0} trace:{1}
+ ///
+ ///
+ /// 键名: CoreExceptions.WorkerActor.QueueRejected
+ /// 用途: 队列拒绝工作单元(邮箱满/停止)时作为异常消息与告警日志
+ /// 参数: {0} - Actor ID, {1} - 调用链追踪信息
+ ///
+ public const string QueueRejected = "CoreExceptions.WorkerActor.QueueRejected";
+
+ ///
+ /// WorkerActor 队列投递被取消。actorId:{0} trace:{1}
+ ///
+ ///
+ /// 键名: CoreExceptions.WorkerActor.QueueCancelled
+ /// 用途: 队列投递工作单元被取消时记录
+ /// 参数: {0} - Actor ID, {1} - 调用链追踪信息
+ ///
+ public const string QueueCancelled = "CoreExceptions.WorkerActor.QueueCancelled";
+
+ ///
+ /// WorkerActor 队列投递失败。actorId:{0} trace:{1}
+ ///
+ ///
+ /// 键名: CoreExceptions.WorkerActor.QueueFailed
+ /// 用途: 队列投递工作单元抛出异常时作为后备异常消息与告警日志
+ /// 参数: {0} - Actor ID, {1} - 调用链追踪信息
+ ///
+ public const string QueueFailed = "CoreExceptions.WorkerActor.QueueFailed";
}
///
/// 定时器相关异常
@@ -1069,6 +1125,26 @@ public static class HotfixModule
/// 参数: {0} - 实现类型全名, {1} - 服务类型全名
///
public const string MissingParameterlessConstructor = "CoreExceptions.Hotfix.MissingParameterlessConstructor";
+
+ ///
+ /// Version 含非法路径字符:{0}
+ ///
+ ///
+ /// 键名: CoreExceptions.Hotfix.InvalidVersionChars
+ /// 用途: 热更版本号含路径分隔符或「..」段被拒绝装载时抛出异常
+ /// 参数: {0} - 被拒绝的版本号
+ ///
+ public const string InvalidVersionChars = "CoreExceptions.Hotfix.InvalidVersionChars";
+
+ ///
+ /// 未找到类型:{0}
+ ///
+ ///
+ /// 键名: CoreExceptions.Hotfix.TypeNotFound
+ /// 用途: 按类型全名在热更程序集创建实例未命中时抛出异常
+ /// 参数: {0} - 类型全名
+ ///
+ public const string TypeNotFound = "CoreExceptions.Hotfix.TypeNotFound";
}
///
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.resx b/GameFrameX.Localization/Localization/Messages/Resources.resx
index a048fd266..bb9d5f377 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.resx
@@ -548,6 +548,18 @@
trace unavailable: {0}
+
+ WorkerActor queue rejected work. actorId:{0} trace:{1}
+
+
+
+ WorkerActor queue post was cancelled. actorId:{0} trace:{1}
+
+
+
+ WorkerActor queue post failed. actorId:{0} trace:{1}
+
+
component:[{0}] the actor type is not bound
@@ -603,6 +615,14 @@
{0} must be a non-abstract closed type implementing {1} with a public parameterless constructor
+
+ Version contains forbidden path characters: {0}
+
+
+
+ Type not found: {0}
+
+
Invalid component type {0}, value should be greater than {1} and less than {2} and not equal to {3}
@@ -1696,7 +1716,15 @@
- EventDispatcher.Dispatch Actor {0} {1} No listeners found for event type: {2}
+ EventDispatcher.Dispatch Actor {0} type: {1} no listeners found for event type: {2}
+
+
+
+ [PlayerSessionManager] player_route upsert hook failed for player {0}; the local PlayerRouteMap stays authoritative and the next SetOnline converges
+
+
+
+ [PlayerSessionManager] player_route delete hook failed for player {0}
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
index f6ec046d9..191bdd128 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
@@ -566,6 +566,18 @@
trace不可用:{0}
+
+ WorkerActor 队列拒绝了工作单元。actorId:{0} trace:{1}
+
+
+
+ WorkerActor 队列投递被取消。actorId:{0} trace:{1}
+
+
+
+ WorkerActor 队列投递失败。actorId:{0} trace:{1}
+
+
组件:[{0}] Actor类型未绑定
@@ -624,6 +636,14 @@
{0} 必须是实现 {1} 的非抽象封闭类型,且具有公共无参构造函数
+
+ Version 含非法路径字符:{0}
+
+
+
+ 未找到类型:{0}
+
+
无效的组件类型 {0},值应大于{1}且小于{2}和不为{3}
@@ -1690,7 +1710,15 @@
- EventDispatcher.Dispatch Actor {0} {1} 事件类型:{2} 没有找到任何监听者
+ EventDispatcher.Dispatch Actor {0} 类型:{1} 事件类型:{2} 没有找到任何监听者
+
+
+
+ [PlayerSessionManager] 玩家路由 upsert 同步钩子失败,玩家:{0};本地 PlayerRouteMap 仍为事实源,下次 SetOnline 会重新收敛
+
+
+
+ [PlayerSessionManager] 玩家路由 delete 同步钩子失败,玩家:{0}
diff --git a/Tests/GameFrameX.Hotfix.Tests/ActorRecycleCallbackConcurrencyTests.cs b/Tests/GameFrameX.Hotfix.Tests/ActorRecycleCallbackConcurrencyTests.cs
new file mode 100644
index 000000000..b00e9c54b
--- /dev/null
+++ b/Tests/GameFrameX.Hotfix.Tests/ActorRecycleCallbackConcurrencyTests.cs
@@ -0,0 +1,162 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// 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.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// 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 legal rights and interests of others, as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or 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/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+using GameFrameX.Apps.Player.Player.Component;
+using GameFrameX.Core.Actors;
+using GameFrameX.Core.Components;
+using GameFrameX.Foundation.Localization.Core;
+using GameFrameX.Foundation.Localization.Providers;
+using GameFrameX.Utility;
+using GameFrameX.Utility.Setting;
+using Xunit;
+
+namespace GameFrameX.Hotfix.Tests;
+
+///
+/// Actor 一次性回收回调线程安全回归单测(GFX-1178 / CR-L09):
+/// 是可在任意线程调用的公共 API,
+/// 与 排空并发时必须无异常且每个回调恰执行一次——
+/// 旧实现的非线程安全 LinkedList 在并发注册/回收下是未定义行为。
+///
+///
+/// 沿 ActorManagerRemoveRecycleCallbackTests 先例挂静态状态串行 Collection(进程级共享状态)。
+///
+[Collection(HotfixManagerStaticStateCollection.Name)]
+public sealed class ActorRecycleCallbackConcurrencyTests
+{
+ ///
+ /// 探针Actor类型:每服单例段(type ≥ 128)且未注册任何组件,回调行为只反映回调队列本身。
+ ///
+ private const ushort ProbeActorType = 201;
+
+ private static bool _environmentInitialized;
+
+ public ActorRecycleCallbackConcurrencyTests()
+ {
+ if (GlobalSettings.CurrentSetting == null)
+ {
+ GlobalSettings.SetCurrentSetting(new AppSetting { ServerId = 1 });
+ }
+ }
+
+ private static async Task InitializeEnvironmentOnceAsync()
+ {
+ if (_environmentInitialized)
+ {
+ return;
+ }
+
+ await ComponentRegister.InitAsync(typeof(PlayerComponent).Assembly);
+ LocalizationService.RegisterProvider(new AssemblyResourceProvider(typeof(GameFrameX.Localization.Keys.CoreExceptions.HotfixModule).Assembly));
+ _environmentInitialized = true;
+ }
+
+ [Fact]
+ public async Task RecycleCallback_RegisteredThenRecycled_ExecutesEachExactlyOnce()
+ {
+ await InitializeEnvironmentOnceAsync();
+ var actor = new Actor(ActorIdGenerator.GetActorId(ProbeActorType, 9741), ProbeActorType);
+
+ var executedCount = 0;
+ actor.AddOnceRecycleCallback(() => Interlocked.Increment(ref executedCount));
+ actor.AddOnceRecycleCallback(() => Interlocked.Increment(ref executedCount));
+ actor.AddOnceRecycleCallback(() => throw new InvalidOperationException("callback-boom"));
+
+ await actor.OnRecycleAsync();
+ await actor.OnRecycleAsync();
+
+ // 含一个抛异常回调:异常被隔离,其余回调恰执行一次,二次回收不重复执行
+ Assert.Equal(2, executedCount);
+ }
+
+ [Fact]
+ public async Task RecycleCallback_ConcurrentRegisterAndRecycle_NoExceptionAllExecutedOnce()
+ {
+ await InitializeEnvironmentOnceAsync();
+ var actor = new Actor(ActorIdGenerator.GetActorId(ProbeActorType, 9742), ProbeActorType);
+
+ const int producerCount = 8;
+ const int perProducerCount = 1250;
+ var totalCallbacks = producerCount * perProducerCount;
+ var executedCount = 0;
+
+ var producers = Enumerable.Range(0, producerCount).Select(_ => Task.Run(() =>
+ {
+ for (var i = 0; i < perProducerCount; i++)
+ {
+ actor.AddOnceRecycleCallback(() => Interlocked.Increment(ref executedCount));
+ }
+ })).ToArray();
+
+ await Task.WhenAll(producers);
+
+ // 生产者全部入队后排空:每个回调恰执行一次
+ await actor.OnRecycleAsync();
+ Assert.Equal(totalCallbacks, executedCount);
+
+ await actor.OnRecycleAsync();
+ Assert.Equal(totalCallbacks, executedCount);
+ }
+
+ [Fact]
+ public async Task RecycleCallback_DrainRacingWithProducers_AllExecutedExactlyOnce()
+ {
+ await InitializeEnvironmentOnceAsync();
+ var actor = new Actor(ActorIdGenerator.GetActorId(ProbeActorType, 9743), ProbeActorType);
+
+ const int producerCount = 4;
+ const int perProducerCount = 2500;
+ var totalCallbacks = producerCount * perProducerCount;
+ var executedCount = 0;
+
+ var producers = Enumerable.Range(0, producerCount).Select(_ => Task.Run(async () =>
+ {
+ for (var i = 0; i < perProducerCount; i++)
+ {
+ actor.AddOnceRecycleCallback(() => Interlocked.Increment(ref executedCount));
+ if (i % 500 == 0)
+ {
+ await Task.Yield();
+ }
+ }
+ })).ToArray();
+
+ // 排空与注册并发进行:ConcurrentQueue 下取出即执行,回调不丢不重
+ while (executedCount < totalCallbacks || producers.Any(task => !task.IsCompleted))
+ {
+ await actor.OnRecycleAsync();
+ await Task.Yield();
+ }
+
+ await Task.WhenAll(producers);
+ await actor.OnRecycleAsync();
+
+ Assert.Equal(totalCallbacks, executedCount);
+ }
+}
diff --git a/Tests/GameFrameX.Hotfix.Tests/HotfixModuleGetInstanceTests.cs b/Tests/GameFrameX.Hotfix.Tests/HotfixModuleGetInstanceTests.cs
new file mode 100644
index 000000000..2ee9f3406
--- /dev/null
+++ b/Tests/GameFrameX.Hotfix.Tests/HotfixModuleGetInstanceTests.cs
@@ -0,0 +1,105 @@
+// ==========================================================================================
+// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
+// 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.
+// 本项目采用 Apache License 2.0 单协议分发,
+// This project is licensed solely under the Apache License 2.0,
+// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
+// 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 legal rights and interests of others, as prohibited by laws and regulations!
+// 因基于本项目二次开发所产生的一切法律纠纷与责任,
+// Any legal disputes or 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/GameFrameX
+// CNB 仓库:https://cnb.cool/GameFrameX
+// CNB Repository: https://cnb.cool/GameFrameX
+// 官方文档:https://gameframex.doc.alianblank.com/
+// Official Documentation: https://gameframex.doc.alianblank.com/
+// ==========================================================================================
+
+using GameFrameX.Apps.Player.Player.Component;
+using GameFrameX.Core.Components;
+using GameFrameX.Core.Hotfix;
+using GameFrameX.Foundation.Localization.Core;
+using GameFrameX.Foundation.Localization.Providers;
+using GameFrameX.Hotfix.Logic.Player.Login;
+using GameFrameX.Utility.Setting;
+using Xunit;
+
+namespace GameFrameX.Hotfix.Tests;
+
+///
+/// HotfixModule 实例解析与 HTTP 处理器列表回归单测(GFX-1178 / CR-L07):
+/// GetInstance 对未知类型名必须抛带类型上下文的 ,
+/// 而非旧实现经 ConcurrentDictionary.GetOrAdd 值工厂返回 null 抛出的脱离语境 ;
+/// GetListHttpHandler 对同一处理器实例的多 cmd 注册必须按引用去重。
+///
+public sealed class HotfixModuleGetInstanceTests
+{
+ private static bool _environmentInitialized;
+
+ private static async Task CreateModuleAsync()
+ {
+ if (!_environmentInitialized)
+ {
+ if (GlobalSettings.CurrentSetting == null)
+ {
+ GlobalSettings.SetCurrentSetting(new AppSetting { ServerId = 1 });
+ }
+
+ await ComponentRegister.InitAsync(typeof(PlayerComponent).Assembly);
+ LocalizationService.RegisterProvider(new AssemblyResourceProvider(typeof(GameFrameX.Localization.Keys.CoreExceptions.HotfixModule).Assembly));
+ _environmentInitialized = true;
+ }
+
+ return new HotfixModule(typeof(PlayerComponentAgent).Assembly);
+ }
+
+ [Fact]
+ public async Task GetInstance_UnknownTypeName_ThrowsInvalidOperationExceptionWithTypeContext()
+ {
+ var module = await CreateModuleAsync();
+ const string unknownTypeName = "GameFrameX.Hotfix.Tests.NoSuchTypeAnywhere";
+
+ var exception = Assert.Throws(() => module.GetInstance