diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c76c54eb..3ab77dc2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@
* 架构分析器守卫三项收口(C429/C430,GFX-1170,GitHub #916):GFX0016 候选集从仅 Class 放宽为全部命名类型种类——struct/interface/record 声明面引用 `MongoDbService`/`PostgreSqlDbService`/`MultiDbRegistry` 不再漏报(自行注册 NamedType 符号分析,实现层白名单与豁免不变);GFX0001 补抽象类豁免(与 GFX0005/0008/0009 一致,非 Apps 程序集的抽象中间状态类不再被误拦);生成 Wrapper 豁免改为「`GeneratedCodeAttribute` 标记(tool 名精确匹配)+ `.g.cs` 文件路径 + 仅 Hotfix 内名称后缀兜底」三段判定,Hotfix 之外手写 `ComponentAgentWrapper` 后缀类不再同时逃过 GFX0005/GFX0014;WrapperGenerator 生成类头追加 `GeneratedCodeAttribute` 标记(与分析器 `WrapperGeneratorToolName` 同值契约)。
* 入队与定时撮合解耦(C406,GFX-1104):删除每次入队的全量撮合及最新票据扫描,复用 `matchmaker-run` 按宿主调度节奏批量处理队列(默认 1000ms)。单轮撮合算法保持不变;桥测试覆盖四名玩家单批次形成两组及批次前取消不参与分配。
-* Hotfix Wrapper 生成器诊断体验、增量管道与低风险清理(C432,GFX-1172,GitHub #918,CODE_REVIEW CR-M06/M07/L03/L04/L05):①诊断改为静态描述符 + 稳定 ID `GFW0001`(契约违规 Error)/ `GFW0002`(契约警示 Warning),携带违规方法 `GetLocation()` 位置(错误列表可点击定位、可经 `.editorconfig` `dotnet_diagnostic_GFW000x.severity` 按 ID 抑制),消息统一引用 `Service`/`ThreadSafe`/`Discard`/`TimeOut` 真实特性常量(消灭不存在的【Api】/【AsyncApi】幻觉名);②增量管道按「代理类声明 → 输出」粒度注册(`WithComparer` 文本等值缓存 + `WithTrackingName`),替代 `Collect()` 全量折叠——单个代理类编辑只重算该代理 wrapper(生产 25 个代理全量生成产物逐字符不变,非 partial hintName 逐字节不变),partial 部件 hintName 序号改声明位置稳定键(FNV-1a 64,跨进程确定);语义调整:无基类列表的 partial 类不再按简单名并入生成面(生产零实例,需要拆分的代理应每部件自带相同基类列表);③`AgentTemplate` 删 `[ThreadStatic]` 静态 `StringBuilder`(局部实例参数化传递,消除矛盾注释);④`ComponentAgentBaseNames` 删除全仓不存在的 `QueryComponentAgent` 死条目。
+* Hotfix Wrapper 生成器诊断体验、增量管道与低风险清理(C432,GFX-1172,GitHub #918,CODE_REVIEW CR-M06/M07/L03/L04/L05):①诊断改为静态描述符 + 稳定 ID `GFW0001`(契约违规 Error)/ `GFW0002`(契约警示 Warning),携带违规方法 `GetLocation()` 位置(错误列表可点击定位、可经 `.editorconfig` `dotnet_diagnostic_GFW000x.severity` 按 ID 抑制),消息统一引用 `Service`/`ThreadSafe`/`Discard`/`TimeOut` 真实特性常量(消灭不存在的【Api】/【AsyncApi】幻觉名);②增量管道按「代理类声明 → 输出」粒度注册(`WithComparer` 文本等值缓存 + `WithTrackingName`),替代 `Collect()` 全量折叠——单个代理类编辑只重算该代理 wrapper(生产 25 个代理全量生成产物逐字符不变,非 partial hintName 逐字节不变),partial 部件 hintName 序号改声明位置稳定键(FNV-1a 64,跨进程确定);语义调整:无基类列表的 partial 类不再按简单名并入生成面(生产零实例,需要拆分的代理应每部件自带相同基类列表);③`AgentTemplate` 删 `[ThreadStatic]` 静态 `StringBuilder`(局部实例参数化传递,消除矛盾注释);④`ComponentAgentBaseNames` 删除全仓不存在的 `QueryComponentAgent` 死条目。与 C430 合并调停:`GeneratedCodeAttribute` 标记仅由非 partial 单体声明发射(逐声明独立缓存无法稳定选举唯一携带者,partial 分部一律省略标记以杜绝 CS0579,分析器豁免由 `.g.cs` 路径段与名称后缀兜底),标记常量移至 `HotfixWrapperGenerator`(`AgentTemplate` 保持零静态字段契约)。
* 撮合通知异步执行与停用退订(C407,GFX-1105,GitHub #733):`ICapabilityEventBus.Subscribe` 返回幂等 `IDisposable` 退订句柄(void → IDisposable 为二进制破坏性变更——包族消费方需重新编译;源码侧忽略返回值的既有调用不受影响);online-match 模块撮合分配事件回调改为纯入队(移除同步总线回调内 `ResolveAssignmentAsync` / `session.WriteAsync` 两处 `GetAwaiter().GetResult()` 死锁面),新增 `match-assignment-notification-tick` 每轮有限批次(64)异步解析并推送(条目级失败不阻断同批其余条目,异常交调度器既有日志路径);`DeactivateAsync` 停用收口——关闭入队、退订、清空待处理并等待在途批次完成,热更/停机后旧模块零通知。回归 13 例:总线退订契约 5 + 模块生命周期 8(含对旧同步实现确定性红的发布不同步解析 / 停用退订 / 停用清空 / 容错批次)。
* 举报入口频控与未结案去重(C405,GFX-1115):举报提交接入双固定窗口限流(`OnlineSocialOptions.Report.MaximumSubmissionsPerMinute=5` / `MaximumSubmissionsPerDay=50`,键=租户/App/举报人,超限 7001 且不落案件不发事件,分钟窗惰性清扫仅删双窗均过期的桶);`IOnlineReportStore.SaveIfAbsentAsync` 升级为两层原子唯一(案件标识重放 + 未结案组合键「举报人+被举报人+场景」,Submitted/Reviewing 占位、终态释放),重复举报返回原案件原证据且仅首建发 `ReportChanged`;`ReplaceAsync` 身份三要素不可改、终态释放与新占位归属在 CAS 同锁维护;`OnlineSocialDecisionService` 构造新增必填 `OnlineReportOptions`,宿主自定义举报存储必须遵循同一原子唯一性语义
diff --git a/GameFrameX.Hotfix.WrapperGenerator/Agent/AgentTemplate.cs b/GameFrameX.Hotfix.WrapperGenerator/Agent/AgentTemplate.cs
index fe61f2d23..80c6e5b19 100644
--- a/GameFrameX.Hotfix.WrapperGenerator/Agent/AgentTemplate.cs
+++ b/GameFrameX.Hotfix.WrapperGenerator/Agent/AgentTemplate.cs
@@ -39,23 +39,6 @@ namespace GameFrameX.Hotfix.WrapperGenerator.Agent;
///
public static class AgentTemplate
{
- ///
- /// 生成器标识:写入生成类 GeneratedCodeAttribute 的 tool 名。
- ///
- ///
- /// 架构分析器按此值精确匹配豁免生成产物(与 Architecture.Analyzers 侧
- /// ArchitectureAnalyzerConstants.WrapperGeneratorToolName 为同值契约,改名须双侧同步)。
- /// The tool name written into the GeneratedCodeAttribute on generated wrapper classes.
- ///
- private const string GeneratedCodeToolName = "GameFrameX.Hotfix.WrapperGenerator";
-
- ///
- /// 生成器版本:写入生成类 GeneratedCodeAttribute 的 version 参数。
- ///
- ///
- /// The version argument written into the GeneratedCodeAttribute on generated wrapper classes.
- ///
- private const string GeneratedCodeVersion = "1.0";
///
/// 根据代理信息生成包装器分部类的源代码。
///
@@ -81,7 +64,7 @@ public static string Run(AgentInfo info)
templateStringBuilder.AppendLine("{");
if (info.EmitGeneratedCodeMarker)
{
- templateStringBuilder.AppendLine("\t[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"" + GeneratedCodeToolName + "\", \"" + GeneratedCodeVersion + "\")]");
+ templateStringBuilder.AppendLine("\t[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"" + HotfixWrapperGenerator.GeneratedCodeToolName + "\", \"" + HotfixWrapperGenerator.GeneratedCodeVersion + "\")]");
}
templateStringBuilder.AppendLine("\tpublic partial class " + info.Name + " : " + info.Super);
templateStringBuilder.AppendLine("\t{");
diff --git a/GameFrameX.Hotfix.WrapperGenerator/Agent/HotfixWrapperGenerator.cs b/GameFrameX.Hotfix.WrapperGenerator/Agent/HotfixWrapperGenerator.cs
index 8ca69006a..580bbb94b 100644
--- a/GameFrameX.Hotfix.WrapperGenerator/Agent/HotfixWrapperGenerator.cs
+++ b/GameFrameX.Hotfix.WrapperGenerator/Agent/HotfixWrapperGenerator.cs
@@ -104,6 +104,25 @@ public class HotfixWrapperGenerator : IIncrementalGenerator
///
private const string TimeOutAttributeName = "TimeOut";
+ ///
+ /// 生成器标识:写入生成类 GeneratedCodeAttribute 的 tool 名。
+ ///
+ ///
+ /// 架构分析器按此值精确匹配豁免生成产物(与 Architecture.Analyzers 侧
+ /// ArchitectureAnalyzerConstants.WrapperGeneratorToolName 为同值契约,改名须双侧同步)。
+ /// 放在生成器而非 AgentTemplate:模板类契约「无任何静态字段」(见 HotfixWrapperGeneratorDiagnosticReportingTests)。
+ /// The tool name written into the GeneratedCodeAttribute on generated wrapper classes.
+ ///
+ internal const string GeneratedCodeToolName = "GameFrameX.Hotfix.WrapperGenerator";
+
+ ///
+ /// 生成器版本:写入生成类 GeneratedCodeAttribute 的 version 参数。
+ ///
+ ///
+ /// The version argument written into the GeneratedCodeAttribute on generated wrapper classes.
+ ///
+ internal const string GeneratedCodeVersion = "1.0";
+
///
/// 增量管线输出步骤的跟踪名:测试与诊断经 TrackedSteps[该名] 断言逐声明的缓存/重算行为。
///
diff --git a/GameFrameX.Localization/Keys.Utility.cs b/GameFrameX.Localization/Keys.Utility.cs
index d9ce57897..cba0d8444 100644
--- a/GameFrameX.Localization/Keys.Utility.cs
+++ b/GameFrameX.Localization/Keys.Utility.cs
@@ -386,6 +386,56 @@ public static class RandomHelper
/// 参数: {0} - 需求的数量, {1} - 数组长度
///
public const string CantRepeatRandomArgError = "Utility.RandomHelper.CantRepeatRandomArgError";
+
+ ///
+ /// 权重为负数
+ ///
+ ///
+ /// 键名: Utility.RandomHelper.NegativeWeight
+ /// 用途: 权重随机入参包含负权重时抛出异常
+ /// 参数: {0} - 非法权重值
+ ///
+ public const string NegativeWeight = "Utility.RandomHelper.NegativeWeight";
+
+ ///
+ /// 全部权重为 0
+ ///
+ ///
+ /// 键名: Utility.RandomHelper.ZeroTotalWeight
+ /// 用途: 参与随机的权重总和为 0(无可选目标)时抛出异常
+ /// 参数: 无
+ ///
+ public const string ZeroTotalWeight = "Utility.RandomHelper.ZeroTotalWeight";
+
+ ///
+ /// 权重总和超出 int 上限
+ ///
+ ///
+ /// 键名: Utility.RandomHelper.WeightSumOverflow
+ /// 用途: 权重求和超出随机源可表达的 32 位范围时抛出异常
+ /// 参数: {0} - 实际总权重
+ ///
+ public const string WeightSumOverflow = "Utility.RandomHelper.WeightSumOverflow";
+
+ ///
+ /// 权重索引越界
+ ///
+ ///
+ /// 键名: Utility.RandomHelper.WeightIndexOutOfRange
+ /// 用途: weightIndex 超出权重条目长度时抛出异常
+ /// 参数: {0} - 权重索引, {1} - 条目长度
+ ///
+ public const string WeightIndexOutOfRange = "Utility.RandomHelper.WeightIndexOutOfRange";
+
+ ///
+ /// 不可重复抽样时正权重条目不足
+ ///
+ ///
+ /// 键名: Utility.RandomHelper.InsufficientPositiveWeights
+ /// 用途: 不可重复抽取的目标数大于权重为正的条目数时抛出异常
+ /// 参数: {0} - 需求数量, {1} - 正权重条目数
+ ///
+ public const string InsufficientPositiveWeights = "Utility.RandomHelper.InsufficientPositiveWeights";
}
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.resx b/GameFrameX.Localization/Localization/Messages/Resources.resx
index dd45e1d34..3a8a65feb 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.resx
@@ -646,6 +646,26 @@
Cannot repeat random argument error, requested count: {0} is greater than ID count: {1}
+
+ Weight must not be negative, actual weight: {0}
+
+
+
+ Total weight must be greater than zero
+
+
+
+ Total weight {0} exceeds the supported 32-bit range
+
+
+
+ Weight index {0} is out of range for an entry with {1} element(s)
+
+
+
+ Cannot pick {0} distinct weighted items, only {1} item(s) have a positive weight
+
+
a = {0}
diff --git a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
index 4e85df89e..984828b44 100644
--- a/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
+++ b/GameFrameX.Localization/Localization/Messages/Resources.zh-CN.resx
@@ -667,6 +667,26 @@
不能重复随机参数错误,需求数量:{0} 大于ID数量:{1}
+
+ 权重不能为负数,实际权重:{0}
+
+
+
+ 权重总和必须大于 0
+
+
+
+ 权重总和 {0} 超出支持的 32 位范围
+
+
+
+ 权重索引 {0} 超出条目长度 {1}
+
+
+
+ 无法不重复抽取 {0} 个权重目标,只有 {1} 个条目的权重为正
+
+
a = {0}
diff --git a/GameFrameX.Utility/AssemblyHelper.cs b/GameFrameX.Utility/AssemblyHelper.cs
index 5d21ca107..180883e76 100644
--- a/GameFrameX.Utility/AssemblyHelper.cs
+++ b/GameFrameX.Utility/AssemblyHelper.cs
@@ -80,12 +80,19 @@ namespace GameFrameX.Utility;
///
public static class AssemblyHelper
{
+ ///
+ /// 类型缓存的最大条目数;达到上限后只返回解析结果、不再写入缓存。
+ ///
+ private const int MaxCachedTypeCount = 1024;
+
///
/// Type cache dictionary using type full name as key, thread-safe
- /// Uses StringComparer.Ordinal for optimal string comparison performance
+ /// Only successful resolutions are cached: a miss is never cached so that types
+ /// from assemblies loaded later (hotfix) can still be found.
///
///
/// 类型缓存字典,使用类型全名作为键,线程安全
+ /// 仅缓存命中结果:未命中不缓存,热更程序集后续加载后仍可解析
/// 使用 StringComparer.Ordinal 提供最佳性能的字符串比较
///
private static readonly ConcurrentDictionary CachedTypes = new(StringComparer.Ordinal);
@@ -219,6 +226,9 @@ public static void GetTypes(List results)
///
/// 获取已加载的程序集中的指定类型
/// 支持完全限定名和简单类型名,使用缓存机制提高查找性能
+ /// 仅缓存命中结果,未命中(miss)不缓存,热更程序集后续加载后仍可解析。
+ /// 安全约束:类型名必须是受信任的配置/代码常量;含逗号的程序集限定名会触发程序集加载,
+ /// 不得直接使用外部/玩家输入。
///
/// Type name to get, supports fully qualified names (e.g., "System.String") / 要获取的类型名,支持完全限定名(如 "System.String")
/// Specified type from loaded assemblies, returns null if not found / 已加载的程序集中的指定类型,如果未找到则返回 null
@@ -227,40 +237,65 @@ public static Type GetType(string typeName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(typeName);
- return CachedTypes.GetOrAdd(typeName, name =>
+ if (CachedTypes.TryGetValue(typeName, out var cachedType))
{
- // 首先尝试直接获取类型(适用于系统类型和当前程序集中的类型)
- var type = Type.GetType(name);
- if (type != null)
- {
- return type;
- }
+ return cachedType;
+ }
+
+ var resolvedType = ResolveType(typeName);
+ if (resolvedType == null)
+ {
+ // 未命中不入缓存:热更程序集可能在后续运行期加载,miss 缓存会给出永久错误答案。
+ return null;
+ }
+
+ if (CachedTypes.Count < MaxCachedTypeCount)
+ {
+ CachedTypes.TryAdd(typeName, resolvedType);
+ }
+
+ return resolvedType;
+ }
+
+ ///
+ /// Resolve a type by name from the loaded assemblies without touching the cache.
+ ///
+ ///
+ /// 不经过缓存解析类型:先按直接名称,再遍历已加载程序集。
+ ///
+ /// Type name to resolve / 要解析的类型名
+ /// Resolved type, or null if not found / 解析到的类型,未找到返回 null
+ private static Type ResolveType(string typeName)
+ {
+ // 首先尝试直接获取类型(适用于系统类型和当前程序集中的类型)
+ var type = Type.GetType(typeName);
+ if (type != null)
+ {
+ return type;
+ }
- // 如果直接获取失败,遍历所有程序集查找类型
- // 这种方式适用于跨程序集的类型查找
- var assemblies = GetCurrentAssemblies();
- foreach (var assembly in assemblies)
+ // 如果直接获取失败,遍历所有程序集查找类型
+ // 这种方式适用于跨程序集的类型查找
+ var assemblies = GetCurrentAssemblies();
+ foreach (var assembly in assemblies)
+ {
+ try
{
- try
- {
- // 使用程序集限定名称查找类型
- type = Type.GetType($"{name}, {assembly.FullName}");
- if (type != null)
- {
- return type;
- }
- }
- catch (Exception)
+ // 使用程序集限定名称查找类型
+ type = Type.GetType($"{typeName}, {assembly.FullName}");
+ if (type != null)
{
- // 忽略异常(如类型不存在、安全异常等)
- // 继续查找下一个程序集
- continue;
+ return type;
}
}
+ catch (Exception)
+ {
+ // 忽略异常(如类型不存在、安全异常等)
+ // 继续查找下一个程序集
+ }
+ }
- // 如果在所有程序集中都找不到该类型,返回 null
- return null;
- });
+ return null;
}
///
diff --git a/GameFrameX.Utility/GameFrameX.Utility.csproj b/GameFrameX.Utility/GameFrameX.Utility.csproj
index db2e606f8..3fcaf2b66 100644
--- a/GameFrameX.Utility/GameFrameX.Utility.csproj
+++ b/GameFrameX.Utility/GameFrameX.Utility.csproj
@@ -14,6 +14,15 @@
TRACE;FP
+
+
+
+
+
diff --git a/GameFrameX.Utility/LNumber.cs b/GameFrameX.Utility/LNumber.cs
index b968fa3a5..f7016982d 100644
--- a/GameFrameX.Utility/LNumber.cs
+++ b/GameFrameX.Utility/LNumber.cs
@@ -304,7 +304,8 @@ public string ToString(string str)
if (c > long.MinValue && c < long.MaxValue)
{
- r.Raw = long.Parse(c.ToString()); //未越界
+ // 上方范围检查已保证 c 在 long 范围内;LongValue 直接取低 64 位补码,零分配。
+ r.Raw = c.LongValue(); //未越界
}
else if ((lhs > 0 && rhs > 0) || (lhs < 0 && rhs < 0))
{
@@ -362,7 +363,8 @@ public string ToString(string str)
if (c > long.MinValue && c < long.MaxValue)
{
- r.Raw = long.Parse(c.ToString()); //未越界
+ // 上方范围检查已保证 c 在 long 范围内;LongValue 直接取低 64 位补码,零分配。
+ r.Raw = c.LongValue(); //未越界
}
else if ((lhs > 0 && rhs > 0) || (lhs < 0 && rhs < 0))
{
diff --git a/GameFrameX.Utility/Math/FP.Float.cs b/GameFrameX.Utility/Math/FP.Float.cs
index edce51c57..7513ea7a4 100644
--- a/GameFrameX.Utility/Math/FP.Float.cs
+++ b/GameFrameX.Utility/Math/FP.Float.cs
@@ -27,6 +27,14 @@
// Official Documentation: https://gameframex.doc.alianblank.com/
// ==========================================================================================
+// 警告:本文件是浮点版 FP 实现,整文件由 #if !FP 包裹,且已在 GameFrameX.Utility.csproj 中
+// 通过 显式排除——本仓(Debug/Release 均定义 FP)
+// 永不编译本文件,在此处的修改不会进入 GameFrameX.Utility 程序集。
+// 该实现供客户端共享工程(不定义 FP)使用;修改必须同步客户端仓并接受客户端编译验证。
+// WARNING: Float-based FP implementation. Wrapped in #if !FP and excluded from
+// GameFrameX.Utility.csproj; never compiled in this repository. Shared with
+// client-side projects that do not define FP - keep changes in sync there.
+
#if !FP
using System;
using System.IO;
diff --git a/GameFrameX.Utility/Math/FP.cs b/GameFrameX.Utility/Math/FP.cs
index f03719dd7..2e67c88dc 100644
--- a/GameFrameX.Utility/Math/FP.cs
+++ b/GameFrameX.Utility/Math/FP.cs
@@ -920,7 +920,7 @@ public static FP Sqrt(FP x)
{
// 我们无法像 Single 和 Double 一样表示无穷大,且 Sqrt 在 x < 0 时
// 数学上是未定义的。因此我们抛出异常。
- throw new ArgumentOutOfRangeException("x", LocalizationService.GetString(Keys.Utility.ExceptionMessages.SqrtValueNegative));
+ throw new ArgumentOutOfRangeException(nameof(x), LocalizationService.GetString(Keys.Utility.ExceptionMessages.SqrtValueNegative));
}
var num = (ulong)xl;
@@ -1005,10 +1005,20 @@ public static FP Sin(FP x)
// 这就是该函数在 x86 上性能下降的原因 - x64 是可以的
var rawIndex = FastMul(clamped, LutInterval);
var roundedIndex = Round(rawIndex);
- var indexError = 0;
+ // 与 Tan 同构:indexError ∈ [-0.5, 0.5](量化前),用于线性插值。
+ // SinLut 长度 == LUT_SIZE;ClampSinValue 保证 clamped ≤ PI_OVER_2 - 1,
+ // 故 rawIndex ≤ LUT_SIZE - 1 - ε,顶格时 indexError 必为负。
+ // 第二个下标仍显式夹取,避免未来改 LUT_SIZE / Round / LutInterval 后越界。
+ var indexError = FastSub(rawIndex, roundedIndex);
+
+ var nearestIndex = System.Math.Clamp(flipHorizontal ? SinLut.Length - 1 - (int)roundedIndex : (int)roundedIndex, 0, SinLut.Length - 1);
+ var secondNearestIndex = flipHorizontal
+ ? SinLut.Length - 1 - (int)roundedIndex - Sign(indexError)
+ : (int)roundedIndex + Sign(indexError);
+ secondNearestIndex = System.Math.Clamp(secondNearestIndex, 0, SinLut.Length - 1);
- var nearestValue = new FP(SinLut[flipHorizontal ? SinLut.Length - 1 - (int)roundedIndex : (int)roundedIndex]);
- var secondNearestValue = new FP(SinLut[flipHorizontal ? SinLut.Length - 1 - (int)roundedIndex - Sign(indexError) : (int)roundedIndex + Sign(indexError)]);
+ var nearestValue = new FP(SinLut[nearestIndex]);
+ var secondNearestValue = new FP(SinLut[secondNearestIndex]);
var delta = FastMul(indexError, FastAbs(FastSub(nearestValue, secondNearestValue)))._serializedValue;
var interpolatedValue = nearestValue._serializedValue + (flipHorizontal ? -delta : delta);
@@ -1280,7 +1290,7 @@ public static FP Acos(FP x)
{
if (x < -One || x > One)
{
- throw new ArgumentOutOfRangeException("x", LocalizationService.GetString(Keys.Utility.ExceptionMessages.ValueOutOfRange));
+ throw new ArgumentOutOfRangeException(nameof(x), LocalizationService.GetString(Keys.Utility.ExceptionMessages.ValueOutOfRange));
}
if (x.RawValue == 0)
diff --git a/GameFrameX.Utility/Math/FPMatrix.cs b/GameFrameX.Utility/Math/FPMatrix.cs
index 755341f29..a637553de 100644
--- a/GameFrameX.Utility/Math/FPMatrix.cs
+++ b/GameFrameX.Utility/Math/FPMatrix.cs
@@ -399,14 +399,12 @@ public static void Add(ref FPMatrix matrix1, ref FPMatrix matrix2, out FPMatrix
/// 计算给定矩阵的逆矩阵。
///
/// 要计算逆的矩阵。
- /// 逆矩阵。
-
- #region public static JMatrix Inverse(JMatrix matrix)
-
+ /// 逆矩阵;矩阵奇异(行列式为 0)时返回全部元素为 的哨兵矩阵。
+ [Obsolete("请使用 Invert。Inverse 的 1024 缩放会损失 10 位精度;本方法现委托 Invert,下个主版本将移除。")]
public static FPMatrix Inverse(FPMatrix matrix)
{
FPMatrix result;
- Inverse(ref matrix, out result);
+ Invert(ref matrix, out result);
return result;
}
@@ -424,10 +422,27 @@ public FP Determinant()
/// 计算给定矩阵的逆矩阵。
///
/// 要计算逆的矩阵。
- /// 逆矩阵。
+ /// 逆矩阵;矩阵奇异(行列式为 0)时填 哨兵。
public static void Invert(ref FPMatrix matrix, out FPMatrix result)
{
- var determinantInverse = 1 / matrix.Determinant();
+ var determinant = matrix.Determinant();
+ if (determinant == FP.Zero)
+ {
+ // 定点数没有 NaN/Inf 体系;与 FPMatrix4x4.Inverse 口径一致,
+ // 奇异矩阵输出全 PositiveInfinity 哨兵,避免 1/0 静默产出垃圾值。
+ result.M11 = FP.PositiveInfinity;
+ result.M12 = FP.PositiveInfinity;
+ result.M13 = FP.PositiveInfinity;
+ result.M21 = FP.PositiveInfinity;
+ result.M22 = FP.PositiveInfinity;
+ result.M23 = FP.PositiveInfinity;
+ result.M31 = FP.PositiveInfinity;
+ result.M32 = FP.PositiveInfinity;
+ result.M33 = FP.PositiveInfinity;
+ return;
+ }
+
+ var determinantInverse = FP.One / determinant;
var m11 = (matrix.M22 * matrix.M33 - matrix.M23 * matrix.M32) * determinantInverse;
var m12 = (matrix.M13 * matrix.M32 - matrix.M33 * matrix.M12) * determinantInverse;
var m13 = (matrix.M12 * matrix.M23 - matrix.M22 * matrix.M13) * determinantInverse;
@@ -458,55 +473,12 @@ public static void Invert(ref FPMatrix matrix, out FPMatrix result)
///
/// The matrix to invert.
/// The inverted JMatrix.
+ [Obsolete("请使用 Invert。Inverse 的 1024 缩放会损失 10 位精度;本方法现委托 Invert,下个主版本将移除。")]
public static void Inverse(ref FPMatrix matrix, out FPMatrix result)
{
- var det = 1024 * matrix.M11 * matrix.M22 * matrix.M33 -
- 1024 * matrix.M11 * matrix.M23 * matrix.M32 -
- 1024 * matrix.M12 * matrix.M21 * matrix.M33 +
- 1024 * matrix.M12 * matrix.M23 * matrix.M31 +
- 1024 * matrix.M13 * matrix.M21 * matrix.M32 -
- 1024 * matrix.M13 * matrix.M22 * matrix.M31;
-
- var num11 = 1024 * matrix.M22 * matrix.M33 - 1024 * matrix.M23 * matrix.M32;
- var num12 = 1024 * matrix.M13 * matrix.M32 - 1024 * matrix.M12 * matrix.M33;
- var num13 = 1024 * matrix.M12 * matrix.M23 - 1024 * matrix.M22 * matrix.M13;
-
- var num21 = 1024 * matrix.M23 * matrix.M31 - 1024 * matrix.M33 * matrix.M21;
- var num22 = 1024 * matrix.M11 * matrix.M33 - 1024 * matrix.M31 * matrix.M13;
- var num23 = 1024 * matrix.M13 * matrix.M21 - 1024 * matrix.M23 * matrix.M11;
-
- var num31 = 1024 * matrix.M21 * matrix.M32 - 1024 * matrix.M31 * matrix.M22;
- var num32 = 1024 * matrix.M12 * matrix.M31 - 1024 * matrix.M32 * matrix.M11;
- var num33 = 1024 * matrix.M11 * matrix.M22 - 1024 * matrix.M21 * matrix.M12;
-
- if (det == 0)
- {
- result.M11 = FP.PositiveInfinity;
- result.M12 = FP.PositiveInfinity;
- result.M13 = FP.PositiveInfinity;
- result.M21 = FP.PositiveInfinity;
- result.M22 = FP.PositiveInfinity;
- result.M23 = FP.PositiveInfinity;
- result.M31 = FP.PositiveInfinity;
- result.M32 = FP.PositiveInfinity;
- result.M33 = FP.PositiveInfinity;
- }
- else
- {
- result.M11 = num11 / det;
- result.M12 = num12 / det;
- result.M13 = num13 / det;
- result.M21 = num21 / det;
- result.M22 = num22 / det;
- result.M23 = num23 / det;
- result.M31 = num31 / det;
- result.M32 = num32 / det;
- result.M33 = num33 / det;
- }
+ Invert(ref matrix, out result);
}
- #endregion
-
///
/// 将矩阵乘以一个缩放因子。
///
@@ -806,15 +778,20 @@ public override bool Equals(object obj)
/// 当前矩阵的哈希代码。
public override int GetHashCode()
{
- return M11.GetHashCode() ^
- M12.GetHashCode() ^
- M13.GetHashCode() ^
- M21.GetHashCode() ^
- M22.GetHashCode() ^
- M23.GetHashCode() ^
- M31.GetHashCode() ^
- M32.GetHashCode() ^
- M33.GetHashCode();
+ unchecked
+ {
+ // 顺序混合(乘 397)而非 XOR:分量置换不再同哈希。
+ var hash = M11.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M12.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M13.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M21.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M22.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M23.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M31.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M32.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M33.RawValue.GetHashCode();
+ return hash;
+ }
}
///
diff --git a/GameFrameX.Utility/Math/FPMatrix4x4.cs b/GameFrameX.Utility/Math/FPMatrix4x4.cs
index 9f2750b1c..e7af51e53 100644
--- a/GameFrameX.Utility/Math/FPMatrix4x4.cs
+++ b/GameFrameX.Utility/Math/FPMatrix4x4.cs
@@ -830,22 +830,26 @@ public override bool Equals(object obj)
/// 当前矩阵的哈希代码。
public override int GetHashCode()
{
- return M11.GetHashCode() ^
- M12.GetHashCode() ^
- M13.GetHashCode() ^
- M14.GetHashCode() ^
- M21.GetHashCode() ^
- M22.GetHashCode() ^
- M23.GetHashCode() ^
- M24.GetHashCode() ^
- M31.GetHashCode() ^
- M32.GetHashCode() ^
- M33.GetHashCode() ^
- M34.GetHashCode() ^
- M41.GetHashCode() ^
- M42.GetHashCode() ^
- M43.GetHashCode() ^
- M44.GetHashCode();
+ unchecked
+ {
+ var hash = M11.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M12.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M13.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M14.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M21.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M22.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M23.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M24.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M31.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M32.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M33.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M34.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M41.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M42.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M43.RawValue.GetHashCode();
+ hash = (hash * 397) ^ M44.RawValue.GetHashCode();
+ return hash;
+ }
}
///
diff --git a/GameFrameX.Utility/Math/FPQuaternion.cs b/GameFrameX.Utility/Math/FPQuaternion.cs
index bd47aef38..58370ed56 100644
--- a/GameFrameX.Utility/Math/FPQuaternion.cs
+++ b/GameFrameX.Utility/Math/FPQuaternion.cs
@@ -791,6 +791,13 @@ public override bool Equals(object obj)
/// 当前四元数的哈希代码。
public override int GetHashCode()
{
- return x.GetHashCode() ^ y.GetHashCode() ^ z.GetHashCode() ^ w.GetHashCode();
+ unchecked
+ {
+ var hash = x.RawValue.GetHashCode();
+ hash = (hash * 397) ^ y.RawValue.GetHashCode();
+ hash = (hash * 397) ^ z.RawValue.GetHashCode();
+ hash = (hash * 397) ^ w.RawValue.GetHashCode();
+ return hash;
+ }
}
}
\ No newline at end of file
diff --git a/GameFrameX.Utility/Math/FPRandom.cs b/GameFrameX.Utility/Math/FPRandom.cs
index 185215397..7d0a93485 100644
--- a/GameFrameX.Utility/Math/FPRandom.cs
+++ b/GameFrameX.Utility/Math/FPRandom.cs
@@ -32,6 +32,13 @@ namespace GameFrameX.Utility.Math;
///
/// 生成基于确定性方法的随机数。
///
+///
+/// 梅森旋转(MT19937)伪随机:确定性可复现,但**非密码学安全**,
+/// 不得用于令牌、验证码、防作弊抽奖等安全决策;安全场景请使用
+/// System.Security.Cryptography.RandomNumberGenerator。
+/// 静态共享实例不保证线程安全(状态推进无锁);需要并发隔离时用
+/// 创建独立实例。
+///
public sealed class FPRandom
{
// 来源:http://www.codeproject.com/Articles/164087/Random-Number-Generation
@@ -46,20 +53,33 @@ public sealed class FPRandom
///
/// 使用种子 1 的 {@link TSRandom} 的静态实例。
///
+ ///
+ /// 首次访问静态成员时懒初始化(种子 1,确定性序列),无需调用方先调用 。
+ ///
private static FPRandom instance;
- private readonly uint[] mag01 = { 0x0U, MATRIX_A, }; // 变换矩阵
- private readonly uint[] mt = new uint[N]; // 梅森旋转状态向量
- private int mti = N + 1; // 当前状态向量索引
-
///
- /// 初始化一个新的实例,使用当前时间的毫秒数作为种子。
+ /// 静态成员使用的确定性实例;未初始化时按种子 1 创建(并发首次访问只发布一个实例)。
///
- private FPRandom()
+ private static FPRandom Instance
{
- init_genrand((uint)DateTime.UtcNow.Millisecond);
+ get
+ {
+ var current = Volatile.Read(ref instance);
+ if (current != null)
+ {
+ return current;
+ }
+
+ var created = New(1);
+ return Interlocked.CompareExchange(ref instance, created, null) ?? created;
+ }
}
+ private readonly uint[] mag01 = { 0x0U, MATRIX_A, }; // 变换矩阵
+ private readonly uint[] mt = new uint[N]; // 梅森旋转状态向量
+ private int mti = N + 1; // 当前状态向量索引
+
///
/// 使用指定的种子初始化一个新的实例。
///
@@ -97,20 +117,37 @@ public static int MaxRandomInt
///
public static FP value
{
- get { return instance.NextFP(); }
+ get { return Instance.NextFP(); }
}
///
/// 返回一个随机的 {@link TSVector},表示半径为 1 的球体内的一个点。
///
+ ///
+ /// 单位立方体拒绝采样:三个分量独立取 [-1, 1],模长超 1 则重采,
+ /// 结果在球体内均匀分布(旧实现返回的是立方体内的点)。
+ ///
public static FPVector3 insideUnitSphere
{
- get { return new FPVector3(value, value, value); }
+ get
+ {
+ while (true)
+ {
+ var candidate = new FPVector3(value * 2 - FP.One, value * 2 - FP.One, value * 2 - FP.One);
+ if (candidate.magnitude <= FP.One)
+ {
+ return candidate;
+ }
+ }
+ }
}
+ ///
+ /// 显式重置静态实例为种子 1 的新序列(测试用)。
+ ///
internal static void Init()
{
- instance = New(1);
+ Interlocked.Exchange(ref instance, New(1));
}
///
@@ -143,7 +180,7 @@ public int Next()
/// 生成的随机整数。
public static int CallNext()
{
- return instance.Next();
+ return Instance.Next();
}
///
@@ -151,7 +188,7 @@ public static int CallNext()
///
/// 最小值。
/// 最大值。
- /// 生成的随机整数。
+ /// 生成的随机整数。minValue == maxValue 时返回 minValue。
public int Next(int minValue, int maxValue)
{
if (minValue > maxValue)
@@ -159,9 +196,28 @@ public int Next(int minValue, int maxValue)
(maxValue, minValue) = (minValue, maxValue);
}
- var range = maxValue - minValue;
+ var range = (ulong)((long)maxValue - minValue);
+ if (range == 0)
+ {
+ return minValue;
+ }
+
+ // 拒绝采样消除模偏:Next() ∈ [0, 2^31 - 1],只接受落在 range 整数倍内的样本。
+ var drawCount = (ulong)MaxRandomInt + 1;
+ if (range < drawCount)
+ {
+ var limit = drawCount - drawCount % range;
+ int draw;
+ do
+ {
+ draw = Next();
+ } while ((ulong)draw >= limit);
+
+ return (int)(minValue + (long)((ulong)draw % range));
+ }
- return minValue + Next() % range;
+ // 跨度超过 31 位随机域(如 int.MinValue..int.MaxValue)时用 [0,1) 双精度缩放。
+ return (int)(minValue + (long)(NextDouble() * range));
}
///
@@ -191,7 +247,7 @@ public FP Next(float minValue, float maxValue)
/// 生成的随机整数。
public static int Range(int minValue, int maxValue)
{
- return instance.Next(minValue, maxValue);
+ return Instance.Next(minValue, maxValue);
}
///
@@ -202,7 +258,7 @@ public static int Range(int minValue, int maxValue)
/// 生成的随机 FP 值。
public static FP Range(float minValue, float maxValue)
{
- return instance.Next(minValue, maxValue);
+ return Instance.Next(minValue, maxValue);
}
///
diff --git a/GameFrameX.Utility/Math/FPVector2.cs b/GameFrameX.Utility/Math/FPVector2.cs
index 6462eb898..7fe540a6a 100644
--- a/GameFrameX.Utility/Math/FPVector2.cs
+++ b/GameFrameX.Utility/Math/FPVector2.cs
@@ -497,7 +497,11 @@ public bool Equals(FPVector2 other)
/// 当前 FPVector2 实例的哈希代码。
public override int GetHashCode()
{
- return (int)(x + y);
+ unchecked
+ {
+ // 分量和相同(如 (1,2)/(2,1)/(3,0))不再同桶:乘 397 顺序混合。
+ return (x.RawValue.GetHashCode() * 397) ^ y.RawValue.GetHashCode();
+ }
}
///
diff --git a/GameFrameX.Utility/Math/FPVector3.cs b/GameFrameX.Utility/Math/FPVector3.cs
index 2d77d842e..d2e0fc800 100644
--- a/GameFrameX.Utility/Math/FPVector3.cs
+++ b/GameFrameX.Utility/Math/FPVector3.cs
@@ -688,7 +688,13 @@ public static void Cross(ref FPVector3 vector1, ref FPVector3 vector2, out FPVec
/// 返回向量的哈希码。
public override int GetHashCode()
{
- return x.GetHashCode() ^ y.GetHashCode() ^ z.GetHashCode();
+ unchecked
+ {
+ var hash = x.RawValue.GetHashCode();
+ hash = (hash * 397) ^ y.RawValue.GetHashCode();
+ hash = (hash * 397) ^ z.RawValue.GetHashCode();
+ return hash;
+ }
}
///
diff --git a/GameFrameX.Utility/Math/FPVector4.cs b/GameFrameX.Utility/Math/FPVector4.cs
index 5ad517e6e..d64660f0a 100644
--- a/GameFrameX.Utility/Math/FPVector4.cs
+++ b/GameFrameX.Utility/Math/FPVector4.cs
@@ -588,7 +588,14 @@ public static void Subtract(ref FPVector4 value1, ref FPVector4 value2, out FPVe
/// 返回向量的哈希码。
public override int GetHashCode()
{
- return x.GetHashCode() ^ y.GetHashCode() ^ z.GetHashCode() ^ w.GetHashCode();
+ unchecked
+ {
+ var hash = x.RawValue.GetHashCode();
+ hash = (hash * 397) ^ y.RawValue.GetHashCode();
+ hash = (hash * 397) ^ z.RawValue.GetHashCode();
+ hash = (hash * 397) ^ w.RawValue.GetHashCode();
+ return hash;
+ }
}
///
diff --git a/GameFrameX.Utility/NetHelper.cs b/GameFrameX.Utility/NetHelper.cs
index 02d63f108..6a9ecec39 100644
--- a/GameFrameX.Utility/NetHelper.cs
+++ b/GameFrameX.Utility/NetHelper.cs
@@ -59,6 +59,16 @@ public static class NetHelper
///
private const int DefaultDnsTimeoutMilliseconds = 5000;
+ ///
+ /// 端口诊断子进程(lsof / netstat)的退出等待上限(毫秒)。
+ ///
+ private const int PortDiagnosticProcessTimeoutMilliseconds = 1500;
+
+ ///
+ /// 强制结束诊断子进程后,等待 stdout/stderr 读取完成的有界预算(毫秒)。
+ ///
+ private const int ProcessKillDrainTimeoutMilliseconds = 500;
+
///
/// 获取指定端口的占用进程信息(仅监听中的 TCP)。
///
@@ -85,6 +95,75 @@ public static List GetPortOccupyingProcesses(int port, int maxCount = 5)
return new List();
}
+ ///
+ /// 启动外部进程并读取其标准输出,超时后强制结束进程。
+ ///
+ ///
+ /// stdout 与 stderr 必须并发读取:先同步 ReadToEnd() 再 WaitForExit,
+ /// 在 RedirectStandardError 为 true 且子进程写满 stderr 管道(4-64KB)时,
+ /// 子进程阻塞在写 stderr、父进程阻塞在读 stdout,双方互等形成死锁。
+ ///
+ /// 进程启动参数,必须重定向 stdout 与 stderr
+ /// 等待进程退出的上限(毫秒)
+ /// 标准输出文本;进程无法启动时返回空串
+ private static string ReadProcessStandardOutput(ProcessStartInfo startInfo, int timeoutMilliseconds)
+ {
+ using (var process = Process.Start(startInfo))
+ {
+ if (process == null)
+ {
+ return string.Empty;
+ }
+
+ var outputTask = process.StandardOutput.ReadToEndAsync();
+ var errorTask = process.StandardError.ReadToEndAsync();
+
+ if (process.WaitForExit(timeoutMilliseconds))
+ {
+ Task.WhenAll(outputTask, errorTask).GetAwaiter().GetResult();
+ return outputTask.Result;
+ }
+
+ try
+ {
+ process.Kill(true);
+ }
+ catch (Exception)
+ {
+ // 进程已退出或无权结束:仍需按有界预算读取,绝不让调用线程无限阻塞。
+ }
+
+ // 强制结束后再给管道读取一个有界预算;kill 被权限/策略拒绝且管道未关闭时,
+ // 超时即返回(最多丢失诊断输出),保证 timeoutMilliseconds 的契约成立。
+ try
+ {
+ if (Task.WaitAll(new Task[] { outputTask, errorTask }, ProcessKillDrainTimeoutMilliseconds))
+ {
+ return outputTask.Result;
+ }
+ }
+ catch (Exception)
+ {
+ // 强制结束导致读取任务失败:退化为返回已成功读取的部分。
+ }
+
+ // 超时返回后 using 会释放 process,仍在进行的读取任务可能抛异常;
+ // 挂 OnlyOnFaulted 续体观察,避免 UnobservedTaskException 噪声。
+ ObserveFault(outputTask);
+ ObserveFault(errorTask);
+ return outputTask.IsCompletedSuccessfully ? outputTask.Result : string.Empty;
+ }
+ }
+
+ ///
+ /// 观察后台任务的异常,避免被回收时产生 。
+ ///
+ /// 待观察任务
+ private static void ObserveFault(Task task)
+ {
+ task.ContinueWith(static completed => _ = completed.Exception, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
+ }
+
///
/// 在 macOS/Linux 下通过 lsof 获取端口占用进程信息。
///
@@ -107,27 +186,18 @@ private static List GetPortOccupyingProcessesByLsof(int port, int maxCou
CreateNoWindow = true,
};
- using (var process = Process.Start(startInfo))
- {
- if (process == null)
- {
- return new List();
- }
+ var output = ReadProcessStandardOutput(startInfo, PortDiagnosticProcessTimeoutMilliseconds);
- var output = process.StandardOutput.ReadToEnd();
- process.WaitForExit(1500);
-
- if (string.IsNullOrWhiteSpace(output))
- {
- return new List();
- }
-
- return output
- .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
- .Where(line => !line.StartsWith("COMMAND", StringComparison.OrdinalIgnoreCase))
- .Take(maxCount)
- .ToList();
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ return new List();
}
+
+ return output
+ .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Where(line => !line.StartsWith("COMMAND", StringComparison.OrdinalIgnoreCase))
+ .Take(maxCount)
+ .ToList();
}
catch
{
@@ -155,45 +225,36 @@ private static List GetPortOccupyingProcessesByNetstat(int port, int max
CreateNoWindow = true,
};
- using (var process = Process.Start(startInfo))
+ var output = ReadProcessStandardOutput(startInfo, PortDiagnosticProcessTimeoutMilliseconds);
+ if (string.IsNullOrWhiteSpace(output))
+ {
+ return new List();
+ }
+
+ var result = new List();
+ var deDuplicate = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ foreach (var line in lines)
{
- if (process == null)
+ var item = TryBuildNetstatPortItem(line, port);
+ if (item == null)
{
- return new List();
+ continue;
}
- var output = process.StandardOutput.ReadToEnd();
- process.WaitForExit(1500);
- if (string.IsNullOrWhiteSpace(output))
+ if (!deDuplicate.Add(item))
{
- return new List();
+ continue;
}
- var result = new List();
- var deDuplicate = new HashSet(StringComparer.OrdinalIgnoreCase);
- var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- foreach (var line in lines)
+ result.Add(item);
+ if (result.Count >= maxCount)
{
- var item = TryBuildNetstatPortItem(line, port);
- if (item == null)
- {
- continue;
- }
-
- if (!deDuplicate.Add(item))
- {
- continue;
- }
-
- result.Add(item);
- if (result.Count >= maxCount)
- {
- break;
- }
+ break;
}
-
- return result;
}
+
+ return result;
}
catch
{
@@ -364,6 +425,11 @@ public static bool IsValidIpAddress(string ipAddress, out IPAddress value)
///
/// 获取第一个可用的端口号
///
+ ///
+ /// 一次性取本机连接快照()后逐候选做真实 bind 探测,
+ /// 避免对每个端口重复枚举全表。快照在循环期间可能过期;返回端口到调用方实际 bind
+ /// 之间仍存在竞态,调用方必须处理 bind 失败。
+ ///
/// 起始端口号,默认为667
/// 结束端口号,默认为65535
/// 第一个可用的端口号,如果没有可用端口号则返回-1
@@ -378,9 +444,16 @@ public static int GetFirstAvailablePort(int startPort = 667, int maxPort = 65535
ArgumentOutOfRangeException.ThrowIfGreaterThan(maxPort, 65535);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(startPort, maxPort);
+ // 一次性快照 + 逐候选真实 bind:避免对每个端口都重新枚举本机连接表。
+ var usedPorts = PortIsUsed().ToHashSet();
for (var i = startPort; i < maxPort; i++)
{
- if (PortIsAvailable(i))
+ if (usedPorts.Contains(i))
+ {
+ continue;
+ }
+
+ if (!IsPortBindConflict(i))
{
return i;
}
@@ -429,6 +502,15 @@ public static List PortIsUsed()
///
/// 检查指定端口是否可用
///
+ ///
+ /// 双重判定:先查本机 TCP/UDP 监听与活动连接快照,未命中再用 TcpListener
+ /// 对目标端口做真实 bind 试探(覆盖「其他进程正在 bind 但尚未出现在快照」的窗口)。
+ /// bind 探测为 TCP/IPv4 通配():UDP 与仅 IPv6 的占用
+ /// 由快照兜底。无权限(如 Unix 上 1024 以下端口)等无法判定的 bind 失败视为可用,
+ /// 由调用方实际 bind 时决定。因此返回 true 只表示「未检测到冲突」,
+ /// 不保证随后的 bind 一定成功;快照与 bind 之间仍存在 TOCTOU,真正可靠的占用判定
+ /// 是直接 bind。
+ ///
/// 要检查的端口号
/// 如果端口未被使用,返回true;否则返回false
/// 当port小于1或大于65535时抛出此异常
@@ -437,22 +519,43 @@ public static bool PortIsAvailable(int port)
ArgumentOutOfRangeException.ThrowIfLessThan(port, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThan(port, 65535);
- var isAvailable = true;
+ if (PortIsUsed().Contains(port))
+ {
+ return false;
+ }
- var portUsed = PortIsUsed();
+ return !IsPortBindConflict(port);
+ }
- foreach (var p in portUsed)
+ ///
+ /// 用真实 bind 试探目标端口是否被其他进程占用。
+ ///
+ ///
+ /// 仅把 AddressAlreadyInUse 视为占用;无权限、地址族不可用等其他 SocketException
+ /// 无法证明占用,返回 false(可用),避免把「本进程无权 bind」误报成「端口被占」。
+ ///
+ /// 目标端口号
+ /// 真实 bind 命中占用时返回 true
+ private static bool IsPortBindConflict(int port)
+ {
+ try
{
- if (p != port)
+ using (var listener = new TcpListener(IPAddress.Any, port))
{
- continue;
+ listener.Start();
+ listener.Stop();
}
- isAvailable = false;
- break;
+ return false;
+ }
+ catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
+ {
+ return true;
+ }
+ catch (SocketException)
+ {
+ return false;
}
-
- return isAvailable;
}
///
@@ -593,10 +696,7 @@ public static List GetMacAddresses()
if (!string.IsNullOrEmpty(macAddress))
{
// 格式化MAC地址为标准格式 (XX:XX:XX:XX:XX:XX)
- var formattedMac = string.Join(":",
- Enumerable.Range(0, macAddress.Length / 2)
- .Select(i => macAddress.Substring(i * 2, 2)));
- macAddresses.Add(formattedMac);
+ macAddresses.Add(FormatMacAddress(macAddress));
}
}
}
@@ -608,6 +708,29 @@ public static List GetMacAddresses()
return macAddresses;
}
+ ///
+ /// 将物理地址字符串格式化为 XX:XX:XX:XX:XX:XX 形式。
+ ///
+ ///
+ /// 奇数长度(部分虚拟/蓝牙网卡返回 5.5 字节形态)左侧补一个 0,
+ /// 保证最后一个字节不被静默丢弃。
+ ///
+ /// 物理地址原始十六进制字符串
+ /// 以冒号分隔的字节序列
+ internal static string FormatMacAddress(string macAddress)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(macAddress);
+
+ var paddedMacAddress = macAddress.Length % 2 == 0 ? macAddress : "0" + macAddress;
+ var groups = new string[paddedMacAddress.Length / 2];
+ for (var i = 0; i < groups.Length; i++)
+ {
+ groups[i] = paddedMacAddress.Substring(i * 2, 2);
+ }
+
+ return string.Join(":", groups);
+ }
+
///
/// 验证IP地址是否在指定的子网内
///
diff --git a/GameFrameX.Utility/RandomHelper.cs b/GameFrameX.Utility/RandomHelper.cs
index aba8b18c7..5ea5c3ae2 100644
--- a/GameFrameX.Utility/RandomHelper.cs
+++ b/GameFrameX.Utility/RandomHelper.cs
@@ -99,9 +99,9 @@ public static void SetSeed(int seed)
/// 一个大于等于零且小于 UInt32.MaxValue 的 32 位无符号整数。
public static uint NextUInt32()
{
- var bytes = new byte[4];
+ Span bytes = stackalloc byte[4];
RandomShared.NextBytes(bytes);
- return BitConverter.ToUInt32(bytes, 0);
+ return BitConverter.ToUInt32(bytes);
}
///
@@ -178,9 +178,9 @@ public static long NextInt64(long minValue, long maxValue)
/// 一个大于等于零且小于 UInt64.MaxValue 的 64 位无符号整数。
public static ulong NextUInt64()
{
- var bytes = new byte[8];
+ Span bytes = stackalloc byte[8];
RandomShared.NextBytes(bytes);
- return BitConverter.ToUInt64(bytes, 0);
+ return BitConverter.ToUInt64(bytes);
}
///
@@ -511,28 +511,29 @@ public static HashSet RandomSelect(int m, int n)
ArgumentOutOfRangeException.ThrowIfNegative(n);
ArgumentOutOfRangeException.ThrowIfGreaterThan(m, n);
- if (m == 0)
+ // 经典迭代抽样(Floyd 算法):从 n-m 递增到 n-1,每次在 [0, i] 取候选,
+ // 命中已选项则改取 i。与旧递归实现同分布,但无 m 层递归(大 m 不再栈溢出)。
+ var selected = new HashSet(m);
+ for (var i = n - m; i < n; i++)
{
- return new HashSet();
+ var candidate = ConcurrentCryptoRandom.Next(0, i + 1);
+ selected.Add(selected.Contains(candidate) ? i : candidate);
}
- var s = RandomSelect(m - 1, n - 1);
- var i = ConcurrentCryptoRandom.Next(0, n);
- s.Add(s.Contains(i) ? n - 1 : i);
- return s;
+ return selected;
}
///
/// 根据权重随机选取,如果需求数量超过权重和(除以权重最大公约数后的),那么按照权重比例加入id,剩余数量再进行随机
/// 不可重复随机num一定小于等于id数量
///
- /// 权重字符串,格式为"id1+weight1;id2+weight2;..."
+ /// 权重字符串;行内字段数以 weightIndex 为准(Ids 为 id+weight,Items 为 id+中间字段+weight)
/// 需要选取的数量
/// 权重索引
/// 是否允许重复选取
/// 包含随机选取的结果数组列表
/// 当weightStr为null时抛出此异常
- /// 当weightStr为空或格式错误,或不可重复选取且需求数量大于id数量时抛出此异常
+ /// 当weightStr为空或格式错误、权重非法,或不可重复抽取的正权重条目不足时抛出此异常
/// 当num或weightIndex小于0时抛出此异常
private static List RandomSelect(string weightStr, int num, int weightIndex, bool canRepeat = true)
{
@@ -554,7 +555,7 @@ private static List RandomSelect(string weightStr, int num, int weightInd
/// 是否允许重复选取
/// 包含随机选取的结果数组列表
/// 当array为null时抛出此异常
- /// 当array为空数组,或不可重复选取且需求数量大于id数量时抛出此异常
+ /// 当array为空数组、权重非法,或不可重复选取且需求数量大于正权重条目数时抛出此异常
/// 当num或weightIndex小于0时抛出此异常
private static List RandomSelect(IReadOnlyList array, int num, int weightIndex, bool canRepeat = true)
{
@@ -578,6 +579,110 @@ private static List RandomSelect(IReadOnlyList array, int num, int
return NoRepeatRandom(num, weightIndex, array);
}
+ ///
+ /// 读取并校验条目中的权重值。
+ ///
+ /// 权重条目。
+ /// 权重索引。
+ /// 校验通过的权重值。
+ /// 当条目为空、索引越界或权重为负数时抛出。
+ private static int ReadWeight(int[] entry, int weightIndex)
+ {
+ if (entry == null || weightIndex >= entry.Length)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.WeightIndexOutOfRange, weightIndex, entry == null ? 0 : entry.Length));
+ }
+
+ var weight = entry[weightIndex];
+ if (weight < 0)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.NegativeWeight, weight));
+ }
+
+ return weight;
+ }
+
+ ///
+ /// 校验权重数组并返回总和(拒绝负权重、全 0、超大总和与越界索引)。
+ ///
+ /// 权重数组,每个元素为[id, weight]。
+ /// 权重索引。
+ /// 校验通过的总权重。
+ /// 权重非法时抛出。
+ private static int ValidateAndSumWeights(IReadOnlyList array, int weightIndex)
+ {
+ long totalWeight = 0;
+ foreach (var entry in array)
+ {
+ totalWeight += ReadWeight(entry, weightIndex);
+ }
+
+ return EnsureTotalWeight(totalWeight);
+ }
+
+ ///
+ /// 校验纯权重数组并返回总和。
+ ///
+ /// 权重数组。
+ /// 校验通过的总权重。
+ /// 权重非法时抛出。
+ private static int ValidateAndSumWeightValues(int[] weights)
+ {
+ long totalWeight = 0;
+ foreach (var weight in weights)
+ {
+ if (weight < 0)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.NegativeWeight, weight));
+ }
+
+ totalWeight += weight;
+ }
+
+ return EnsureTotalWeight(totalWeight);
+ }
+
+ ///
+ /// 校验总权重:不能为 0,且必须落在 int 随机域内。
+ ///
+ /// 累加后的总权重。
+ /// int 范围内的总权重。
+ /// 总权重为 0 或超过 int 上限时抛出。
+ private static int EnsureTotalWeight(long totalWeight)
+ {
+ if (totalWeight == 0)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.ZeroTotalWeight));
+ }
+
+ if (totalWeight > int.MaxValue)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.WeightSumOverflow, totalWeight));
+ }
+
+ return (int)totalWeight;
+ }
+
+ ///
+ /// 统计权重为正的条目数量(不可重复抽样最多能选出这么多个目标)。
+ ///
+ /// 权重数组,每个元素为[id, weight]。
+ /// 权重索引。
+ /// 正权重条目数。
+ private static int CountPositiveWeights(IReadOnlyList array, int weightIndex)
+ {
+ var count = 0;
+ foreach (var entry in array)
+ {
+ if (ReadWeight(entry, weightIndex) > 0)
+ {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
///
/// 不可重复随机选取
///
@@ -585,8 +690,20 @@ private static List RandomSelect(IReadOnlyList array, int num, int
/// 权重索引
/// 权重数组,每个元素为[id, weight]
/// 包含随机选取的结果数组列表
+ /// 权重非法,或正权重条目数少于 num 时抛出。
private static List NoRepeatRandom(int num, int weightIndex, IReadOnlyList array)
{
+ // 先整表校验,避免非法权重在逐次抽样中被隐式放大成晦涩的库异常。
+ ValidateAndSumWeights(array, weightIndex);
+
+ // 0 权重条目永不被选中:不可重复抽样要求目标数不超过正权重条目数,
+ // 否则后续轮次的子集权重和会耗尽(旧实现会返回重复项,属静默错误)。
+ var positiveWeightCount = CountPositiveWeights(array, weightIndex);
+ if (num > positiveWeightCount)
+ {
+ throw new ArgumentException(LocalizationService.GetString(Localization.Keys.Utility.RandomHelper.InsufficientPositiveWeights, num, positiveWeightCount));
+ }
+
var results = new List();
var idxSet = new HashSet();
for (var i = 0; i < num; i++)
@@ -600,6 +717,7 @@ private static List NoRepeatRandom(int num, int weightIndex, IReadOnlyLis
}
}
+ // positiveWeightCount 预检保证每一轮仍有正权重可选,子集和恒 > 0。
var r = ConcurrentCryptoRandom.Next(totalWeight);
var temp = 0;
var idx = 0;
@@ -632,11 +750,7 @@ private static List NoRepeatRandom(int num, int weightIndex, IReadOnlyLis
/// 包含随机选取的结果数组列表
private static List CanRepeatRandom(IReadOnlyList array, int num, int weightIndex)
{
- var totalWeight = 0;
- foreach (var arr in array)
- {
- totalWeight += arr[weightIndex];
- }
+ var totalWeight = ValidateAndSumWeights(array, weightIndex);
var results = new List(num);
for (var i = 0; i < num; i++)
@@ -673,7 +787,7 @@ private static int[] SingleRandom(IReadOnlyList array, int totalWeight, i
var temp = 0;
foreach (var arr in array)
{
- temp += arr[weightIndex];
+ temp += ReadWeight(arr, weightIndex);
if (temp > r)
{
return arr;
@@ -689,13 +803,13 @@ private static int[] SingleRandom(IReadOnlyList array, int totalWeight, i
/// 权重数组
/// 随机选取的id索引
/// 当weights为null时抛出此异常
- /// 当weights为空数组时抛出此异常
+ /// 当weights为空数组、含负权重或权重和非法时抛出此异常
public static int Idx(int[] weights)
{
ArgumentNullException.ThrowIfNull(weights);
ArgumentOutOfRangeException.ThrowIfZero(weights.Length, nameof(weights));
- var totalWight = weights.Sum();
+ var totalWight = ValidateAndSumWeightValues(weights);
var r = ConcurrentCryptoRandom.Next(totalWight);
var temp = 0;
for (var i = 0; i < weights.Length; i++)
@@ -717,7 +831,7 @@ public static int Idx(int[] weights)
/// 权重索引
/// 随机选取的id索引
/// 当array为null时抛出此异常
- /// 当array为空数组时抛出此异常
+ /// 当array为空数组、权重非法或 weightIndex 越界时抛出此异常
/// 当weightIndex小于0时抛出此异常
public static int Idx(IReadOnlyList array, int weightIndex = 1)
{
@@ -725,18 +839,14 @@ public static int Idx(IReadOnlyList array, int weightIndex = 1)
ArgumentOutOfRangeException.ThrowIfZero(array.Count, nameof(array));
ArgumentOutOfRangeException.ThrowIfNegative(weightIndex);
- var totalWeight = 0;
- foreach (var arr in array)
- {
- totalWeight += arr[weightIndex];
- }
+ var totalWeight = ValidateAndSumWeights(array, weightIndex);
var r = ConcurrentCryptoRandom.Next(totalWeight);
var temp = 0;
for (var i = 0; i < array.Count; i++)
{
var arr = array[i];
- temp += arr[weightIndex];
+ temp += ReadWeight(arr, weightIndex);
if (temp > r)
{
return i;
@@ -754,7 +864,7 @@ public static int Idx(IReadOnlyList array, int weightIndex = 1)
/// 是否允许重复选取
/// 包含随机选取的id列表
/// 当array为null时抛出此异常
- /// 当array为空数组时抛出此异常
+ /// 当array为空数组、权重非法(负权重/全 0/总和溢出/索引越界),或不可重复抽取的正权重条目不足时抛出此异常
/// 当num小于0时抛出此异常
public static List Ids(IReadOnlyList array, int num, bool isCanRepeat = true)
{
@@ -773,7 +883,7 @@ public static List Ids(IReadOnlyList array, int num, bool isCanRepea
/// 是否允许重复选取
/// 包含随机选取的id列表
/// 当str为null时抛出此异常
- /// 当str为空或格式错误时抛出此异常
+ /// 当str为空或格式错误、权重非法(负权重/全 0/总和溢出),或不可重复抽取的正权重条目不足时抛出此异常
/// 当num小于0时抛出此异常
public static List Ids(string str, int num, bool isCanRepeat = true)
{
@@ -786,12 +896,12 @@ public static List Ids(string str, int num, bool isCanRepeat = true)
///
/// 随机获取指定数量的项目
///
- /// 权重字符串,格式为"id1+weight1;id2+weight2;..."
+ /// 权重字符串,Items 格式为"id1+中间字段1+weight1;id2+中间字段2+weight2;..."
/// 需要选取的数量
/// 是否允许重复选取
/// 包含随机选取的项目列表
/// 当str为null时抛出此异常
- /// 当str为空或格式错误时抛出此异常
+ /// 当str为空或格式错误、权重非法(负权重/全 0/总和溢出),或不可重复抽取的正权重条目不足时抛出此异常
/// 当num小于0时抛出此异常
public static List Items(string str, int num, bool isCanRepeat = true)
{
@@ -804,12 +914,12 @@ public static List Items(string str, int num, bool isCanRepeat = true)
///
/// 随机获取指定数量的项目
///
- /// 数据列表,每个元素为[id, weight]
+ /// 数据列表,每个元素为[id, 中间字段, weight](权重取索引 2)
/// 需要选取的数量
/// 是否允许重复选取
/// 包含随机选取的项目列表
/// 当array为null时抛出此异常
- /// 当array为空数组时抛出此异常
+ /// 当array为空数组、权重非法(负权重/全 0/总和溢出/索引越界),或不可重复抽取的正权重条目不足时抛出此异常
/// 当num小于0时抛出此异常
public static List Items(IReadOnlyList array, int num, bool isCanRepeat = true)
{
diff --git a/GameFrameX.Utility/Setting/GameServerConst.cs b/GameFrameX.Utility/Setting/GameServerConst.cs
index d4cbb38e4..75c12e3d2 100644
--- a/GameFrameX.Utility/Setting/GameServerConst.cs
+++ b/GameFrameX.Utility/Setting/GameServerConst.cs
@@ -33,7 +33,10 @@ namespace GameFrameX.Utility.Setting;
/// 游戏服务器常量定义(名称 + 服务器ID)。
///
///
-/// Centralized service constants binding Name and Id together.
+/// 职责边界(CR-L04):本类只登记「服务角色」的 Name / Id 成对常量(按 ID 段分组),
+/// 不承载 ID 位布局、键名或运行期配置——前者归 ,
+/// 后者归 / 。
+/// 新增服务角色时按既有 ID 段归组,禁止把业务常量塞进本类。
///
public static class GameServerConst
{
diff --git a/GameFrameX.Utility/Setting/GlobalConst.cs b/GameFrameX.Utility/Setting/GlobalConst.cs
index e133255a1..e0b37429c 100644
--- a/GameFrameX.Utility/Setting/GlobalConst.cs
+++ b/GameFrameX.Utility/Setting/GlobalConst.cs
@@ -31,10 +31,16 @@
namespace GameFrameX.Utility.Setting;
///
-/// 全局常量类。
+/// 全局常量类:ID 位布局、Actor 类型、会话/唯一ID 键名与进程级固定值。
///
///
-/// Global constants class containing server IDs, actor types, and system configuration values.
+/// 职责边界(CR-L04):
+/// - 本类只放「与具体服务无关、编译期固定」的协议式常量:ID 位掩码与取值范围、ActorType、
+/// 键名后缀、全局计时器间隔。
+/// - 服务名称/服务ID 组合常量归 。
+/// - 运行期可配置项(端口、密钥、语言、数据库连接等)归 ,
+/// 由其仓储 装载。
+/// - 新增常量前先确认上述三类都不合适;无名意义的「魔法数字」不得入库。
///
public static class GlobalConst
{
@@ -281,13 +287,5 @@ public static class GlobalConst
///
internal const int SaveIntervalInMilliSeconds = 300_000; //300_000;
- ///
- /// 魔法数字
- ///
- ///
- /// Magic number constant (60).
- ///
- public const int Magic = 60;
-
#endregion
}
diff --git a/GameFrameX.Utility/Setting/GlobalSettings.cs b/GameFrameX.Utility/Setting/GlobalSettings.cs
index 17bb9f031..5499882f9 100644
--- a/GameFrameX.Utility/Setting/GlobalSettings.cs
+++ b/GameFrameX.Utility/Setting/GlobalSettings.cs
@@ -37,10 +37,12 @@
namespace GameFrameX.Utility.Setting;
///
-/// 全局设置
+/// 全局设置( 的进程级仓储)。
///
///
-/// Global settings class that manages application configuration.
+/// 职责边界(CR-L04):本类是配置「装载 / 当前实例 / 进程级字段同步」的唯一入口,
+/// 不承载常量(归 / ),
+/// 也不承载业务状态。改名为 AppSettingStore 属破坏性变更,留待主版本统一处理。
///
public static class GlobalSettings
{
diff --git a/Tests/GameFrameX.Tests/Hotfix/HotfixWrapperGeneratorGeneratedCodeMarkerTests.cs b/Tests/GameFrameX.Tests/Hotfix/HotfixWrapperGeneratorGeneratedCodeMarkerTests.cs
index c22221048..8767c14bf 100644
--- a/Tests/GameFrameX.Tests/Hotfix/HotfixWrapperGeneratorGeneratedCodeMarkerTests.cs
+++ b/Tests/GameFrameX.Tests/Hotfix/HotfixWrapperGeneratorGeneratedCodeMarkerTests.cs
@@ -71,7 +71,7 @@ public virtual Task QueryAsync()
}
}
- public partial class PartialComponentAgent
+ public partial class PartialComponentAgent : BaseComponentAgent
{
[Service]
public virtual Task LoadAsync()
@@ -93,11 +93,13 @@ public virtual Task LoadAsync()
.Skip(1)
.Where(tree => tree.ToString().Contains("public partial class PartialComponentAgentWrapper"))
.ToArray();
- // 分部代理的两个落盘分部都生成 Wrapper 声明,但标记只允许出现一次:
- // GeneratedCodeAttribute 的 AllowMultiple 为 false,重复特性会令宿主编译失败(CS0579)
+ // C432 逐声明管线:partial 代理须每个部件自带基类列表才会逐部件生成 Wrapper 分部;
+ // GeneratedCodeAttribute 的 AllowMultiple 为 false,逐声明独立缓存无法稳定选举唯一携带者,
+ // 故 partial 分部一律不发射标记(CS0579 绝不发生),分析器豁免由 .g.cs 路径段与
+ // ComponentAgentWrapper 名称后缀兜底(三段判定)。标记由非 partial 单体声明的测试另行钉住。
Assert.Equal(2, wrapperTrees.Length);
var markerCount = wrapperTrees.Sum(tree => CountMarkerOccurrences(tree.ToString()));
- Assert.Equal(1, markerCount);
+ Assert.Equal(0, markerCount);
}
[Fact]
@@ -114,7 +116,7 @@ public virtual Task UnmarkedAsync()
}
}
- public partial class PartialComponentAgent
+ public partial class PartialComponentAgent : BaseComponentAgent
{
[Service]
public virtual Task QueryAsync()
@@ -136,9 +138,10 @@ public virtual Task QueryAsync()
.Skip(1)
.Where(tree => tree.ToString().Contains("public partial class PartialComponentAgentWrapper"))
.ToArray();
- // 首个声明分部无可包装方法不落盘:标记必须随唯一落盘分部出现(按落盘计数而非声明序号)
+ // 首个声明分部无可包装方法不落盘:唯一落盘分部为 partial 部件,同样不携带标记(CS0579 防线),
+ // 豁免由 .g.cs 路径段兜底。
Assert.Single(wrapperTrees);
- Assert.Equal(1, CountMarkerOccurrences(wrapperTrees[0].ToString()));
+ Assert.Equal(0, CountMarkerOccurrences(wrapperTrees[0].ToString()));
}
///
diff --git a/Tests/GameFrameX.Tests/Utility/AssemblyHelperTests.cs b/Tests/GameFrameX.Tests/Utility/AssemblyHelperTests.cs
index d49827a3e..5de2d477b 100644
--- a/Tests/GameFrameX.Tests/Utility/AssemblyHelperTests.cs
+++ b/Tests/GameFrameX.Tests/Utility/AssemblyHelperTests.cs
@@ -400,4 +400,27 @@ public void GetTypes_Performance_ShouldBeReasonablyFast()
Assert.True(stopwatch.ElapsedMilliseconds < 1000,
$"Performance test failed: {stopwatch.ElapsedMilliseconds}ms for {iterations} iterations");
}
+
+ ///
+ /// 测试 GetType 方法 - 未命中结果不得进入缓存(热更程序集后续加载后仍可解析)
+ ///
+ [Fact]
+ public void GetType_WithUnknownName_DoesNotCacheMiss()
+ {
+ // Arrange
+ const BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Static;
+ var cacheField = typeof(AssemblyHelper).GetField("CachedTypes", flags);
+ Assert.NotNull(cacheField);
+ var cache = (System.Collections.Concurrent.ConcurrentDictionary)cacheField.GetValue(null);
+ var typeName = "GameFrameX.Tests.NoSuchType_" + Guid.NewGuid().ToString("N");
+
+ // Act
+ var firstLookup = AssemblyHelper.GetType(typeName);
+ var secondLookup = AssemblyHelper.GetType(typeName);
+
+ // Assert
+ Assert.Null(firstLookup);
+ Assert.Null(secondLookup);
+ Assert.False(cache.ContainsKey(typeName), "未命中的类型名不应被缓存");
+ }
}
\ No newline at end of file
diff --git a/Tests/GameFrameX.Tests/Utility/LNumberTests.cs b/Tests/GameFrameX.Tests/Utility/LNumberTests.cs
index 49d338079..3360a72c0 100644
--- a/Tests/GameFrameX.Tests/Utility/LNumberTests.cs
+++ b/Tests/GameFrameX.Tests/Utility/LNumberTests.cs
@@ -644,5 +644,29 @@ public void ContinuousOperations_ShouldMaintainPrecision()
}
#endregion
+
+ #region 大数路径测试(CR-L03)
+
+ ///
+ /// 超出 int 权重范围后走 BigInteger 路径的乘法:结果必须由数值转换得到,
+ /// 不得依赖 ToString/Parse 往返(正负对称)。
+ ///
+ [Fact]
+ public void Multiply_LargeValues_BigIntegerPathKeepsExactResult()
+ {
+ // Arrange
+ var large = LNumber.Create(40000, 0); // Raw = 2,621,440,000 > int.MaxValue
+ var factor = LNumber.Create(3, 0);
+
+ // Act
+ var positive = large * factor;
+ var negative = (large * LNumber.Create(-1, 0)) * factor;
+
+ // Assert
+ Assert.Equal(LNumber.Create(120000, 0).Raw, positive.Raw);
+ Assert.Equal(LNumber.Create(-120000, 0).Raw, negative.Raw);
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/Tests/GameFrameX.Tests/Utility/Math/FPHashCodeTests.cs b/Tests/GameFrameX.Tests/Utility/Math/FPHashCodeTests.cs
new file mode 100644
index 000000000..508de031e
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Utility/Math/FPHashCodeTests.cs
@@ -0,0 +1,110 @@
+// ==========================================================================================
+// 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 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.
+// 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 System.Collections.Generic;
+using GameFrameX.Utility.Math;
+
+namespace GameFrameX.Tests.Utility.Math;
+
+///
+/// 定点向量/矩阵哈希质量回归测试(CR-M13)。
+///
+public class FPHashCodeTests
+{
+ ///
+ /// 分量和相同的向量必须散列到不同桶(旧实现 (int)(x + y) 全碰撞)。
+ ///
+ [Fact]
+ public void Vector2_ComponentOrder_AffectsHash()
+ {
+ Assert.NotEqual(new FPVector2(1, 2).GetHashCode(), new FPVector2(2, 1).GetHashCode());
+ Assert.NotEqual(new FPVector2(1, 2).GetHashCode(), new FPVector2(3, 0).GetHashCode());
+ }
+
+ ///
+ /// XOR 交换不变:分量置换必须散列到不同桶。
+ ///
+ [Fact]
+ public void Vector3_ComponentPermutation_AffectsHash()
+ {
+ Assert.NotEqual(new FPVector3(1, 2, 3).GetHashCode(), new FPVector3(3, 2, 1).GetHashCode());
+ }
+
+ ///
+ /// 四维向量与四元数的分量置换必须改变哈希。
+ ///
+ [Fact]
+ public void Vector4AndQuaternion_ComponentPermutation_AffectsHash()
+ {
+ Assert.NotEqual(new FPVector4(1, 2, 3, 4).GetHashCode(), new FPVector4(4, 3, 2, 1).GetHashCode());
+ Assert.NotEqual(new FPQuaternion(1, 2, 3, 4).GetHashCode(), new FPQuaternion(4, 3, 2, 1).GetHashCode());
+ }
+
+ ///
+ /// 矩阵元素置换必须改变哈希。
+ ///
+ [Fact]
+ public void Matrix_Permutation_AffectsHash()
+ {
+ Assert.NotEqual(new FPMatrix(1, 2, 3, 4, 5, 6, 7, 8, 9).GetHashCode(),
+ new FPMatrix(9, 8, 7, 6, 5, 4, 3, 2, 1).GetHashCode());
+ Assert.NotEqual(new FPMatrix4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16).GetHashCode(),
+ new FPMatrix4x4(16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1).GetHashCode());
+ }
+
+ ///
+ /// 相等对象必须同哈希(Equals/GetHashCode 契约)。
+ ///
+ [Fact]
+ public void EqualValues_ShareHashCode()
+ {
+ Assert.Equal(new FPVector2(3, 4).GetHashCode(), new FPVector2(3, 4).GetHashCode());
+ Assert.Equal(new FPVector3(3, 4, 5).GetHashCode(), new FPVector3(3, 4, 5).GetHashCode());
+ Assert.Equal(new FPQuaternion(3, 4, 5, 6).GetHashCode(), new FPQuaternion(3, 4, 5, 6).GetHashCode());
+ Assert.Equal(new FPMatrix(1, 2, 3, 4, 5, 6, 7, 8, 9).GetHashCode(), new FPMatrix(1, 2, 3, 4, 5, 6, 7, 8, 9).GetHashCode());
+ }
+
+ ///
+ /// 1 万个随机 V2 的冲突率应低于 5%(旧实现按分量和分桶,冲突率远超该值)。
+ ///
+ [Fact]
+ public void Vector2_RandomSamples_CollisionRateBelowFivePercent()
+ {
+ const int sampleCount = 10000;
+ var random = new System.Random(1234);
+ var hashes = new HashSet(sampleCount);
+
+ for (var i = 0; i < sampleCount; i++)
+ {
+ hashes.Add(new FPVector2(random.Next(-1000, 1000), random.Next(-1000, 1000)).GetHashCode());
+ }
+
+ Assert.True(hashes.Count >= sampleCount * 95 / 100, $"哈希冲突过多:仅 {hashes.Count} 个不同哈希");
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Utility/Math/FPMathPrecisionTests.cs b/Tests/GameFrameX.Tests/Utility/Math/FPMathPrecisionTests.cs
new file mode 100644
index 000000000..712fc6d29
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Utility/Math/FPMathPrecisionTests.cs
@@ -0,0 +1,130 @@
+// ==========================================================================================
+// 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 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.
+// 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.Utility.Math;
+
+namespace GameFrameX.Tests.Utility.Math;
+
+///
+/// FP 三角函数精度回归测试(CR-M08:Sin 线性插值)。
+///
+public class FPMathPrecisionTests
+{
+ ///
+ /// 插值后的精度应远优于最近邻查表(最近邻误差约 4e-6,插值误差约 1e-9)。
+ ///
+ private static readonly FP Tolerance = (FP)0.000001m;
+
+ ///
+ /// Sin(π/6) 应精确逼近 0.5;最近邻查表实现会偏差约 2e-6。
+ ///
+ [Fact]
+ public void Sin_AtThirtyDegrees_MatchesHalfWithinInterpolationTolerance()
+ {
+ var angle = FP.Pi / (FP)6;
+
+ var actual = FP.Sin(angle);
+
+ Assert.True(FP.Abs(actual - (FP)0.5m) <= Tolerance, $"Sin(π/6) = {(decimal)actual},期望 0.5");
+ }
+
+ ///
+ /// 第一、二象限抽样对照 double 参考值,确认插值分支未破坏镜像路径。
+ ///
+ [Fact]
+ public void Sin_AcrossHalfPeriod_StaysWithinToleranceOfDoubleReference()
+ {
+ for (var i = 0; i <= 3141; i++)
+ {
+ var radians = i / 1000.0;
+ var actual = (double)(decimal)FP.Sin((FP)(decimal)radians);
+ var expected = System.Math.Sin(radians);
+
+ Assert.True(System.Math.Abs(actual - expected) <= 1e-6, $"角度 {radians}:actual={actual} expected={expected}");
+ }
+ }
+
+ ///
+ /// Sin 的翻转分支(横向镜像)应保持对称性与单调性。
+ ///
+ [Fact]
+ public void Sin_IsAntisymmetricAndMonotonicOnFirstQuadrant()
+ {
+ var previous = FP.Zero;
+ for (var i = 1; i <= 100; i++)
+ {
+ var angle = FP.PiOver2 / (FP)(101 * 2) * (FP)i;
+ var value = FP.Sin(angle);
+ Assert.True(value >= previous, $"Sin 在 {i} 步非单调:{ (decimal)value } < { (decimal)previous }");
+ Assert.True(FP.Abs(FP.Sin(-angle) + value) <= Tolerance, $"Sin 在 {i} 步不满足奇对称");
+ previous = value;
+ }
+ }
+
+ ///
+ /// 全周期角度扫描:Sin 不越界、不抛异常,结果恒在 [-1, 1]。
+ /// 覆盖 LUT 顶格、翻转边界与负角度镜像路径。
+ ///
+ [Fact]
+ public void Sin_OverFullPeriod_StaysBoundedAndDoesNotThrow()
+ {
+ for (var i = -20000; i <= 20000; i++)
+ {
+ var angle = (FP)(decimal)(i / 1000.0);
+
+ var value = FP.Sin(angle);
+ Assert.True(value >= -FP.One && value <= FP.One, $"Sin({i / 1000.0}) = {(decimal)value} 越界");
+ _ = FP.Cos(angle);
+ }
+ }
+
+ ///
+ /// Sqrt 负数入参:本地化消息必须进 Message、ParamName 必须是 "x"(CR-L05)。
+ ///
+ [Fact]
+ public void Sqrt_WithNegativeValue_ReportsParamNameAndMessage()
+ {
+ var exception = Assert.Throws(() => FP.Sqrt((FP)(-1)));
+
+ Assert.Equal("x", exception.ParamName);
+ Assert.DoesNotContain("{", exception.Message);
+ }
+
+ ///
+ /// Acos 越界入参:本地化消息必须进 Message、ParamName 必须是 "x"(CR-L05)。
+ ///
+ [Fact]
+ public void Acos_WithOutOfRangeValue_ReportsParamNameAndMessage()
+ {
+ var exception = Assert.Throws(() => FP.Acos((FP)2));
+
+ Assert.Equal("x", exception.ParamName);
+ Assert.DoesNotContain("{", exception.Message);
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Utility/Math/FPMatrixInverseTests.cs b/Tests/GameFrameX.Tests/Utility/Math/FPMatrixInverseTests.cs
new file mode 100644
index 000000000..bbcfd48e8
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Utility/Math/FPMatrixInverseTests.cs
@@ -0,0 +1,140 @@
+// ==========================================================================================
+// 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 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.
+// 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.Utility.Math;
+
+namespace GameFrameX.Tests.Utility.Math;
+
+///
+/// FPMatrix 求逆回归测试(CR-M09:单求逆实现 + 奇异哨兵)。
+///
+public class FPMatrixInverseTests
+{
+ ///
+ /// 单位矩阵求逆仍为单位矩阵。
+ ///
+ [Fact]
+ public void Invert_Identity_ReturnsIdentity()
+ {
+ var identity = FPMatrix.Identity;
+
+ FPMatrix.Invert(ref identity, out var inverse);
+
+ Assert.Equal(FP.One, inverse.M11);
+ Assert.Equal(FP.One, inverse.M22);
+ Assert.Equal(FP.One, inverse.M33);
+ Assert.Equal(FP.Zero, inverse.M12);
+ Assert.Equal(FP.Zero, inverse.M13);
+ Assert.Equal(FP.Zero, inverse.M21);
+ Assert.Equal(FP.Zero, inverse.M23);
+ Assert.Equal(FP.Zero, inverse.M31);
+ Assert.Equal(FP.Zero, inverse.M32);
+ }
+
+ ///
+ /// 对角矩阵求逆得到对角倒数(旧 Invert 无奇异检查,此处同时验证直除精度)。
+ ///
+ [Fact]
+ public void Invert_DiagonalMatrix_ReturnsReciprocals()
+ {
+ var matrix = new FPMatrix(2, 0, 0, 0, 4, 0, 0, 0, 8);
+
+ FPMatrix.Invert(ref matrix, out var inverse);
+
+ Assert.Equal((FP)0.5m, inverse.M11);
+ Assert.Equal((FP)0.25m, inverse.M22);
+ Assert.Equal((FP)0.125m, inverse.M33);
+ Assert.Equal(FP.Zero, inverse.M21);
+ }
+
+ ///
+ /// 奇异矩阵(第三行为前两行线性组合)应输出 PositiveInfinity 哨兵,而不是 1/0 产生的垃圾值。
+ ///
+ [Fact]
+ public void Invert_SingularMatrix_FillsPositiveInfinitySentinel()
+ {
+ var matrix = new FPMatrix(1, 2, 3, 2, 4, 6, 7, 8, 9);
+
+ FPMatrix.Invert(ref matrix, out var inverse);
+
+ Assert.Equal(FP.PositiveInfinity, inverse.M11);
+ Assert.Equal(FP.PositiveInfinity, inverse.M12);
+ Assert.Equal(FP.PositiveInfinity, inverse.M13);
+ Assert.Equal(FP.PositiveInfinity, inverse.M21);
+ Assert.Equal(FP.PositiveInfinity, inverse.M22);
+ Assert.Equal(FP.PositiveInfinity, inverse.M23);
+ Assert.Equal(FP.PositiveInfinity, inverse.M31);
+ Assert.Equal(FP.PositiveInfinity, inverse.M32);
+ Assert.Equal(FP.PositiveInfinity, inverse.M33);
+ }
+
+ ///
+ /// 已过时的 Inverse 与 Invert 必须是同一实现(不再有 1024 缩放的第二套真相)。
+ ///
+ [Fact]
+ public void Inverse_DelegatesToInvert()
+ {
+ var matrix = new FPMatrix(2, 1, 0, 1, 3, 1, 0, 1, 4);
+
+ FPMatrix.Invert(ref matrix, out var viaInvert);
+
+#pragma warning disable CS0618 // 本用例专门验证过时 API 的委托语义
+ var viaInverse = FPMatrix.Inverse(matrix);
+ FPMatrix.Inverse(ref matrix, out var viaInverseRef);
+#pragma warning restore CS0618
+
+ Assert.Equal(viaInvert.M11, viaInverse.M11);
+ Assert.Equal(viaInvert.M23, viaInverse.M23);
+ Assert.Equal(viaInvert.M33, viaInverse.M33);
+ Assert.Equal(viaInvert.M11, viaInverseRef.M11);
+ Assert.Equal(viaInvert.M33, viaInverseRef.M33);
+ }
+
+ ///
+ /// 非奇异矩阵求逆后与原矩阵相乘应逼近单位矩阵。
+ ///
+ [Fact]
+ public void Invert_MultipliedByOriginal_ApproximatesIdentity()
+ {
+ var matrix = new FPMatrix(4, 7, 2, 3, 6, 1, 2, 5, 3);
+
+ FPMatrix.Invert(ref matrix, out var inverse);
+ var product = FPMatrix.Multiply(matrix, inverse);
+
+ Assert.True(FP.Abs(product.M11 - FP.One) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M22 - FP.One) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M33 - FP.One) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M12) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M13) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M21) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M23) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M31) <= (FP)0.00001m);
+ Assert.True(FP.Abs(product.M32) <= (FP)0.00001m);
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Utility/Math/FPRandomTests.cs b/Tests/GameFrameX.Tests/Utility/Math/FPRandomTests.cs
new file mode 100644
index 000000000..18f81994b
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Utility/Math/FPRandomTests.cs
@@ -0,0 +1,145 @@
+// ==========================================================================================
+// 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 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.
+// 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.Utility.Math;
+
+namespace GameFrameX.Tests.Utility.Math;
+
+///
+/// FPRandom 回归测试(CR-M10:懒初始化、球内采样、Next 边界与模偏)。
+///
+public class FPRandomTests
+{
+ ///
+ /// 未显式调用 Init 时静态成员也必须可用(旧实现直接 NRE)。
+ ///
+ [Fact]
+ public void Value_BeforeExplicitInit_DoesNotThrow()
+ {
+ var value = (decimal)FPRandom.value;
+
+ Assert.InRange(value, 0m, 1m);
+ }
+
+ ///
+ /// 球内采样:全部样本模长不超过 1(旧实现返回立方体点,模长可达 √3)。
+ ///
+ [Fact]
+ public void InsideUnitSphere_AllSamplesStayWithinUnitBall()
+ {
+ for (var i = 0; i < 2000; i++)
+ {
+ var point = FPRandom.insideUnitSphere;
+ var magnitude = (decimal)point.magnitude;
+
+ Assert.True(magnitude <= 1.0001m, $"第 {i} 个样本模长 {magnitude} 超出单位球");
+ }
+ }
+
+ ///
+ /// 球内采样不应退化为原点或单点:应同时存在靠近中心与外层壳的样本。
+ ///
+ [Fact]
+ public void InsideUnitSphere_CoversInnerAndOuterRegions()
+ {
+ var hasInner = false;
+ var hasOuter = false;
+
+ for (var i = 0; i < 2000 && !(hasInner && hasOuter); i++)
+ {
+ var magnitude = (decimal)FPRandom.insideUnitSphere.magnitude;
+ hasInner |= magnitude < 0.25m;
+ hasOuter |= magnitude > 0.75m;
+ }
+
+ Assert.True(hasInner, "未采到靠近球心的样本");
+ Assert.True(hasOuter, "未采到靠近球壳的样本");
+ }
+
+ ///
+ /// min == max 时返回 min,而不是抛 DivideByZeroException。
+ ///
+ [Fact]
+ public void Next_WithEqualBounds_ReturnsBoundary()
+ {
+ var random = FPRandom.New(1);
+
+ Assert.Equal(7, random.Next(7, 7));
+ Assert.Equal(-3, random.Next(-3, -3));
+ }
+
+ ///
+ /// 三桶抽样应接近均匀(拒绝采样消除模偏);旧实现的模偏在 6 万次下可被稳定检出。
+ ///
+ [Fact]
+ public void Next_ThreeBuckets_AreEvenlyDistributed()
+ {
+ const int sampleCount = 60000;
+ var random = FPRandom.New(42);
+ var buckets = new int[3];
+
+ for (var i = 0; i < sampleCount; i++)
+ {
+ buckets[random.Next(0, 3)]++;
+ }
+
+ const int expected = sampleCount / 3;
+ const int tolerance = expected / 50; // 2%
+ foreach (var count in buckets)
+ {
+ Assert.InRange(count, expected - tolerance, expected + tolerance);
+ }
+ }
+
+ ///
+ /// 跨度超过 31 位随机域时不应溢出或抛异常(旧实现 range 溢出为负数)。
+ ///
+ [Fact]
+ public void Next_WithFullIntRange_ReturnsValueInRange()
+ {
+ var random = FPRandom.New(7);
+
+ for (var i = 0; i < 100; i++)
+ {
+ var value = random.Next(int.MinValue, int.MaxValue);
+ Assert.InRange(value, int.MinValue, int.MaxValue - 1);
+ }
+ }
+
+ ///
+ /// 静态 Range 在未显式 Init 时也可用。
+ ///
+ [Fact]
+ public void Range_WithoutExplicitInit_ReturnsValueInRange()
+ {
+ var value = FPRandom.Range(10, 20);
+
+ Assert.InRange(value, 10, 19);
+ }
+}
diff --git a/Tests/GameFrameX.Tests/Utility/NetHelperTests.cs b/Tests/GameFrameX.Tests/Utility/NetHelperTests.cs
index 35f78d614..04c5f89d7 100644
--- a/Tests/GameFrameX.Tests/Utility/NetHelperTests.cs
+++ b/Tests/GameFrameX.Tests/Utility/NetHelperTests.cs
@@ -804,5 +804,61 @@ public void GetAvailablePorts_ShouldReturnActuallyAvailablePorts()
}
#endregion
+
+ #region MAC 地址格式化与端口 bind 探测
+
+ ///
+ /// 偶数长度的物理地址按字节分组。
+ ///
+ [Fact]
+ public void FormatMacAddress_WithEvenLength_GroupsByByte()
+ {
+ Assert.Equal("A1:B2:C3:D4:E5:F6", NetHelper.FormatMacAddress("A1B2C3D4E5F6"));
+ }
+
+ ///
+ /// 奇数长度的物理地址左补 0,最后一位不得静默丢失。
+ ///
+ [Fact]
+ public void FormatMacAddress_WithOddLength_PreservesAllNibbles()
+ {
+ var formatted = NetHelper.FormatMacAddress("A1B2C3D4E");
+
+ Assert.Equal("0A:1B:2C:3D:4E", formatted);
+ Assert.Equal("0A1B2C3D4E", formatted.Replace(":", string.Empty));
+ }
+
+ ///
+ /// 已被监听占用的端口必须判定为不可用。
+ ///
+ [Fact]
+ public void PortIsAvailable_WithBoundPort_ReturnsFalse()
+ {
+ using (var listener = new TcpListener(IPAddress.Any, 0))
+ {
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+
+ Assert.False(NetHelper.PortIsAvailable(port));
+ }
+ }
+
+ ///
+ /// 已释放的端口必须判定为可用。
+ ///
+ [Fact]
+ public void PortIsAvailable_WithReleasedPort_ReturnsTrue()
+ {
+ int port;
+ using (var listener = new TcpListener(IPAddress.Any, 0))
+ {
+ listener.Start();
+ port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ }
+
+ Assert.True(NetHelper.PortIsAvailable(port));
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/Tests/GameFrameX.Tests/Utility/RandomHelperWeightTests.cs b/Tests/GameFrameX.Tests/Utility/RandomHelperWeightTests.cs
new file mode 100644
index 000000000..0c8eef65a
--- /dev/null
+++ b/Tests/GameFrameX.Tests/Utility/RandomHelperWeightTests.cs
@@ -0,0 +1,183 @@
+// ==========================================================================================
+// 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 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.
+// 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 System;
+using System.Collections.Generic;
+using GameFrameX.Utility;
+
+namespace GameFrameX.Tests.Utility;
+
+///
+/// RandomHelper 权重随机与无分配随机数回归测试(CR-M12 / CR-L07)。
+///
+public class RandomHelperWeightTests
+{
+ ///
+ /// 负权重必须在入口被拒绝,并指出非法权重值。
+ ///
+ [Fact]
+ public void Idx_WithNegativeWeight_ThrowsArgumentException()
+ {
+ var exception = Assert.Throws(() => RandomHelper.Idx(new[] { 1, -2, 3 }));
+
+ Assert.Contains("-2", exception.Message);
+ }
+
+ ///
+ /// 全 0 权重无可选目标,必须显式报错而非抛随机库内部异常。
+ ///
+ [Fact]
+ public void Idx_WithAllZeroWeights_ThrowsArgumentException()
+ {
+ Assert.Throws(() => RandomHelper.Idx(new[] { 0, 0, 0 }));
+ }
+
+ ///
+ /// 权重求和超出 int 上限时必须拒绝(旧实现静默回绕成错误分布)。
+ ///
+ [Fact]
+ public void Idx_WithWeightSumOverflow_ThrowsArgumentException()
+ {
+ Assert.Throws(() => RandomHelper.Idx(new[] { int.MaxValue, int.MaxValue }));
+ }
+
+ ///
+ /// weightIndex 超出条目长度时给出带索引与长度的明确异常。
+ ///
+ [Fact]
+ public void Idx_WithWeightIndexOutOfRange_ThrowsArgumentException()
+ {
+ var array = new[] { new[] { 1, 2 }, new[] { 3, 4 } };
+
+ var exception = Assert.Throws(() => RandomHelper.Idx(array, 5));
+
+ Assert.Contains("5", exception.Message);
+ }
+
+ ///
+ /// Ids 的权重重载同样受权重校验保护。
+ ///
+ [Fact]
+ public void Ids_WithNegativeWeight_ThrowsArgumentException()
+ {
+ var array = new[] { new[] { 1, 1 }, new[] { 2, -1 } };
+
+ // 显式转成 IReadOnlyList,避免命中泛型 Ids(T[], int) 的「按普通元素抽样」重载。
+ Assert.Throws(() => RandomHelper.Ids((IReadOnlyList)array, 1));
+ }
+
+ ///
+ /// 合法权重返回合法索引。
+ ///
+ [Fact]
+ public void Idx_WithValidWeights_ReturnsIndexInRange()
+ {
+ var weights = new[] { 1, 2, 3 };
+
+ for (var i = 0; i < 1000; i++)
+ {
+ Assert.InRange(RandomHelper.Idx(weights), 0, weights.Length - 1);
+ }
+ }
+
+ ///
+ /// 不可重复抽取的正权重条目不足时必须 fail fast(0 权重条目永不被选中)。
+ ///
+ [Fact]
+ public void Items_NoRepeat_WithInsufficientPositiveWeights_ThrowsArgumentException()
+ {
+ var array = new[] { new[] { 1, 0, 5 }, new[] { 2, 0, 0 } };
+
+ var exception = Assert.Throws(() => RandomHelper.Items((IReadOnlyList)array, 2, false));
+
+ Assert.Contains("1", exception.Message);
+ }
+
+ ///
+ /// 正权重条目足够时,0 权重条目不会被选中。
+ ///
+ [Fact]
+ public void Items_NoRepeat_WithZeroWeightEntry_SelectsOnlyPositiveWeights()
+ {
+ var array = new[] { new[] { 1, 0, 5 }, new[] { 2, 0, 0 }, new[] { 3, 0, 5 } };
+
+ var selected = RandomHelper.Items((IReadOnlyList)array, 2, false);
+
+ Assert.Equal(2, selected.Count);
+ Assert.DoesNotContain(selected, entry => entry[0] == 2);
+ Assert.NotEqual(selected[0][0], selected[1][0]);
+ }
+
+ ///
+ /// 大 m 抽样不再线性递归,不触发栈溢出。
+ ///
+ [Fact]
+ public void RandomSelect_LargeCount_DoesNotOverflowStack()
+ {
+ var selected = RandomHelper.RandomSelect(100_000, 100_001);
+
+ Assert.Equal(100_000, selected.Count);
+ Assert.All(selected, value => Assert.InRange(value, 0, 100_000));
+ }
+
+ ///
+ /// 小规模抽样仍保持互异且落在 [0, n) 内。
+ ///
+ [Fact]
+ public void RandomSelect_ReturnsDistinctValuesInRange()
+ {
+ var selected = RandomHelper.RandomSelect(5, 10);
+
+ Assert.Equal(5, selected.Count);
+ Assert.All(selected, value => Assert.InRange(value, 0, 9));
+ }
+
+ ///
+ /// NextUInt32/NextUInt64 必须零堆分配(旧实现每次 new byte[])。
+ ///
+ [Fact]
+ public void NextUInt32AndNextUInt64_DoNotAllocatePerCall()
+ {
+ for (var i = 0; i < 2000; i++)
+ {
+ _ = RandomHelper.NextUInt32();
+ _ = RandomHelper.NextUInt64();
+ }
+
+ var before = GC.GetAllocatedBytesForCurrentThread();
+ for (var i = 0; i < 1000; i++)
+ {
+ _ = RandomHelper.NextUInt32();
+ _ = RandomHelper.NextUInt64();
+ }
+
+ var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
+ Assert.True(allocated < 1024, $"1000 次无符号随机分配了 {allocated} 字节");
+ }
+}