`
This commit is contained in:
parent
de844d7274
commit
d0ff0dbfda
@ -13,7 +13,6 @@ namespace XNet.Business
|
|||||||
private const int TARGET_FPS = 30;
|
private const int TARGET_FPS = 30;
|
||||||
private const int FRAME_TIME_MS = 1000 / TARGET_FPS;
|
private const int FRAME_TIME_MS = 1000 / TARGET_FPS;
|
||||||
private readonly List<string> _instanceIds = new List<string>();
|
private readonly List<string> _instanceIds = new List<string>();
|
||||||
|
|
||||||
public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager)
|
public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager)
|
||||||
{
|
{
|
||||||
_navMeshManager = navMeshManager;
|
_navMeshManager = navMeshManager;
|
||||||
|
|||||||
@ -15,10 +15,10 @@ namespace XNet.Business
|
|||||||
{
|
{
|
||||||
public string InstanceId { get; }
|
public string InstanceId { get; }
|
||||||
public DtCrowd Crowd { get; set; }
|
public DtCrowd Crowd { get; set; }
|
||||||
public object SyncRoot { get; } = new object(); // 线程锁
|
//public object SyncRoot { get; } = new object(); // 线程锁
|
||||||
|
|
||||||
// 新增:存储该实例下所有 Agent 的索引(关键修复)
|
// 新增:存储该实例下所有 Agent 的索引(关键修复)
|
||||||
public List<int> AgentIndices { get; } = new List<int>();
|
public ConcurrentDictionary<int, bool> AgentIndices { get; } = new ConcurrentDictionary<int, bool>();
|
||||||
|
|
||||||
// 构造函数
|
// 构造函数
|
||||||
public CrowdInstance(string instanceId, DtCrowd crowd)
|
public CrowdInstance(string instanceId, DtCrowd crowd)
|
||||||
@ -30,22 +30,22 @@ namespace XNet.Business
|
|||||||
// 辅助:添加 Agent 时自动记录索引
|
// 辅助:添加 Agent 时自动记录索引
|
||||||
public void AddAgentIndex(int agentIdx)
|
public void AddAgentIndex(int agentIdx)
|
||||||
{
|
{
|
||||||
lock (SyncRoot)
|
//lock (SyncRoot)
|
||||||
{
|
//{
|
||||||
if (!AgentIndices.Contains(agentIdx))
|
//if (!AgentIndices.ContainsKey(agentIdx))
|
||||||
{
|
//{
|
||||||
AgentIndices.Add(agentIdx);
|
AgentIndices.TryAdd(agentIdx, true);
|
||||||
}
|
//}
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助:移除 Agent 时清理索引
|
// 辅助:移除 Agent 时清理索引
|
||||||
public void RemoveAgentIndex(int agentIdx)
|
public void RemoveAgentIndex(int agentIdx)
|
||||||
{
|
{
|
||||||
lock (SyncRoot)
|
//lock (SyncRoot)
|
||||||
{
|
//{
|
||||||
AgentIndices.Remove(agentIdx);
|
AgentIndices.TryRemove(agentIdx, out _);
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,45 +84,32 @@ namespace XNet.Business
|
|||||||
return syncList;
|
return syncList;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
|
//{
|
||||||
|
// 遍历实例内所有Agent索引(修复AgentIndices报错)
|
||||||
|
foreach (var kv in ci.AgentIndices)
|
||||||
{
|
{
|
||||||
// 遍历实例内所有Agent索引(修复AgentIndices报错)
|
int agentIdx = kv.Key;
|
||||||
foreach (var agentIdx in ci.AgentIndices)
|
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||||
|
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||||
{
|
{
|
||||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
continue;
|
||||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
}
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前状态
|
// 获取当前状态
|
||||||
Vector3 currPos = GetAgentPosition(instanceId, agentIdx);
|
Vector3 currPos = GetAgentPosition(instanceId, agentIdx);
|
||||||
float currRot = GetAgentRotation(instanceId, agentIdx);
|
float currRot = GetAgentRotation(instanceId, agentIdx);
|
||||||
|
|
||||||
// 对比上一帧状态
|
// 对比上一帧状态
|
||||||
if (lastStates.TryGetValue(agentIdx, out var lastState))
|
if (lastStates.TryGetValue(agentIdx, out var lastState))
|
||||||
{
|
{
|
||||||
// 判断是否超过阈值
|
// 判断是否超过阈值
|
||||||
bool posChanged = Vector3.Distance(currPos, lastState.Position) > POSITION_THRESHOLD;
|
bool posChanged = Vector3.Distance(currPos, lastState.Position) > POSITION_THRESHOLD;
|
||||||
bool rotChanged = Math.Abs(currRot - lastState.Rotation) > ROTATION_THRESHOLD;
|
bool rotChanged = Math.Abs(currRot - lastState.Rotation) > ROTATION_THRESHOLD;
|
||||||
|
|
||||||
if (posChanged || rotChanged)
|
if (posChanged || rotChanged)
|
||||||
{
|
|
||||||
// 加入同步列表
|
|
||||||
syncList.Add(new AgentPositionSyncMsg
|
|
||||||
{
|
|
||||||
InstanceId = instanceId,
|
|
||||||
AgentIdx = agentIdx,
|
|
||||||
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
|
||||||
Rotation = currRot
|
|
||||||
});
|
|
||||||
// 更新缓存
|
|
||||||
lastStates[agentIdx] = new AgentState { Position = currPos, Rotation = currRot };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// 首次同步,直接加入并缓存
|
// 加入同步列表
|
||||||
syncList.Add(new AgentPositionSyncMsg
|
syncList.Add(new AgentPositionSyncMsg
|
||||||
{
|
{
|
||||||
InstanceId = instanceId,
|
InstanceId = instanceId,
|
||||||
@ -130,17 +117,31 @@ namespace XNet.Business
|
|||||||
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
||||||
Rotation = currRot
|
Rotation = currRot
|
||||||
});
|
});
|
||||||
lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot });
|
// 更新缓存
|
||||||
|
lastStates[agentIdx] = new AgentState { Position = currPos, Rotation = currRot };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 首次同步,直接加入并缓存
|
||||||
|
syncList.Add(new AgentPositionSyncMsg
|
||||||
|
{
|
||||||
|
InstanceId = instanceId,
|
||||||
|
AgentIdx = agentIdx,
|
||||||
|
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
|
||||||
|
Rotation = currRot
|
||||||
|
});
|
||||||
|
lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//}
|
||||||
|
|
||||||
return syncList;
|
return syncList;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ===============================================
|
||||||
// 修复2:重写 GetAgentRotation(通过速度计算朝向)
|
// 修复2:重写 GetAgentRotation(通过速度计算朝向)
|
||||||
// ==========================================
|
// ===============================================
|
||||||
public float GetAgentRotation(string instanceId, int agentIdx)
|
public float GetAgentRotation(string instanceId, int agentIdx)
|
||||||
{
|
{
|
||||||
if (!_crowdInstances.TryGetValue(instanceId, out var ci))
|
if (!_crowdInstances.TryGetValue(instanceId, out var ci))
|
||||||
@ -148,28 +149,28 @@ namespace XNet.Business
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
|
//{
|
||||||
|
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||||
|
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||||
{
|
{
|
||||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
return 0f;
|
||||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
|
||||||
{
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 核心逻辑:通过Agent的速度向量计算绕Y轴的旋转角度(弧度)
|
|
||||||
// 速度向量 (vx, vz) -> 朝向角度 = atan2(vz, vx)
|
|
||||||
float vx = agent.vel.X;
|
|
||||||
float vz = -agent.vel.Z; // 和坐标转换一致,Z轴取反
|
|
||||||
float rotation = 0f;
|
|
||||||
|
|
||||||
// 只有速度大于阈值时才计算朝向(避免静止时角度抖动)
|
|
||||||
if (Math.Abs(vx) > 0.001f || Math.Abs(vz) > 0.001f)
|
|
||||||
{
|
|
||||||
rotation = (float)Math.Atan2(vz, vx); // 计算弧度(范围 -π ~ π)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rotation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 核心逻辑:通过Agent的速度向量计算绕Y轴的旋转角度(弧度)
|
||||||
|
// 速度向量 (vx, vz) -> 朝向角度 = atan2(vz, vx)
|
||||||
|
float vx = agent.vel.X;
|
||||||
|
float vz = -agent.vel.Z; // 和坐标转换一致,Z轴取反
|
||||||
|
float rotation = 0f;
|
||||||
|
|
||||||
|
// 只有速度大于阈值时才计算朝向(避免静止时角度抖动)
|
||||||
|
if (Math.Abs(vx) > 0.001f || Math.Abs(vz) > 0.001f)
|
||||||
|
{
|
||||||
|
rotation = (float)Math.Atan2(vz, vx); // 计算弧度(范围 -π ~ π)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rotation;
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -214,45 +215,45 @@ namespace XNet.Business
|
|||||||
throw new ArgumentException($"实例 {instanceId} 不存在");
|
throw new ArgumentException($"实例 {instanceId} 不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
{
|
//{
|
||||||
// 创建Agent(原有逻辑)
|
// 创建Agent(原有逻辑)
|
||||||
var agentParams = new DtCrowdAgentParams();
|
var agentParams = new DtCrowdAgentParams();
|
||||||
agentParams.radius = radius;
|
agentParams.radius = radius;
|
||||||
agentParams.height = height;
|
agentParams.height = height;
|
||||||
agentParams.maxAcceleration = 2.0f;
|
agentParams.maxAcceleration = 2.0f;
|
||||||
agentParams.maxSpeed = 3.0f;
|
agentParams.maxSpeed = 3.0f;
|
||||||
agentParams.collisionQueryRange = radius * 8.0f;
|
agentParams.collisionQueryRange = radius * 8.0f;
|
||||||
agentParams.pathOptimizationRange = radius * 32.0f;
|
agentParams.pathOptimizationRange = radius * 32.0f;
|
||||||
agentParams.separationWeight = 1.0f;
|
agentParams.separationWeight = 1.0f;
|
||||||
|
|
||||||
var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
|
var rcPos = new RcVec3f(position.X, position.Y, -position.Z); // 坐标转换(和之前一致)
|
||||||
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
|
int agentIdx = ci.Crowd.AddAgent(rcPos, agentParams).idx;
|
||||||
|
|
||||||
// 关键:记录Agent索引到实例的AgentIndices
|
// 关键:记录Agent索引到实例的AgentIndices
|
||||||
ci.AddAgentIndex(agentIdx);
|
ci.AddAgentIndex(agentIdx);
|
||||||
|
|
||||||
// 初始化状态缓存
|
// 初始化状态缓存
|
||||||
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
|
_agentLastState.GetOrAdd(instanceId, _ => new ConcurrentDictionary<int, AgentState>())
|
||||||
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = 0f });
|
.TryAdd(agentIdx, new AgentState { Position = position, Rotation = 0f });
|
||||||
|
|
||||||
return agentIdx;
|
return agentIdx;
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveAgent(string instanceId, int agentIdx)
|
public void RemoveAgent(string instanceId, int agentIdx)
|
||||||
{
|
{
|
||||||
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return;
|
if (!_crowdInstances.TryGetValue(instanceId, out var ci)) return;
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
|
//{
|
||||||
|
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||||
|
// 【修复】判断 Agent 是否有效,需要检查 state
|
||||||
|
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||||
{
|
{
|
||||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
ci.Crowd.RemoveAgent(agent);
|
||||||
// 【修复】判断 Agent 是否有效,需要检查 state
|
|
||||||
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
|
||||||
{
|
|
||||||
ci.Crowd.RemoveAgent(agent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination)
|
public bool AgentGoto(string instanceId, int agentIdx, Vector3 destination)
|
||||||
@ -267,14 +268,14 @@ namespace XNet.Business
|
|||||||
|
|
||||||
if (targetRef == 0) return false;
|
if (targetRef == 0) return false;
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
|
//{
|
||||||
|
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||||
|
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||||
{
|
{
|
||||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos);
|
||||||
if (agent != null && agent.state != DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
|
||||||
{
|
|
||||||
return ci.Crowd.RequestMoveTarget(agent, targetRef, realTargetPos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,17 +289,17 @@ namespace XNet.Business
|
|||||||
return Vector3.Zero;
|
return Vector3.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
|
//{
|
||||||
|
var agent = ci.Crowd.GetAgent(agentIdx);
|
||||||
|
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
||||||
{
|
{
|
||||||
var agent = ci.Crowd.GetAgent(agentIdx);
|
return Vector3.Zero;
|
||||||
if (agent == null || agent.state == DtCrowdAgentState.DT_CROWDAGENT_STATE_INVALID)
|
|
||||||
{
|
|
||||||
return Vector3.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 坐标转换(和AddAgent时一致:Z轴取反)
|
|
||||||
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 坐标转换(和AddAgent时一致:Z轴取反)
|
||||||
|
return new Vector3(agent.npos.X, agent.npos.Y, -agent.npos.Z);
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateAll(float deltaTime)
|
public void UpdateAll(float deltaTime)
|
||||||
@ -308,10 +309,10 @@ namespace XNet.Business
|
|||||||
MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数,避免过载
|
MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数,避免过载
|
||||||
}, ci =>
|
}, ci =>
|
||||||
{
|
{
|
||||||
lock (ci.SyncRoot)
|
//lock (ci.SyncRoot)
|
||||||
{
|
//{
|
||||||
ci.Crowd.Update(deltaTime, null);
|
ci.Crowd.Update(deltaTime, null);
|
||||||
}
|
//}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ namespace XNet.Business
|
|||||||
{
|
{
|
||||||
// ========== 原有核心字段 ==========
|
// ========== 原有核心字段 ==========
|
||||||
private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
|
private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
|
||||||
private readonly ConcurrentDictionary<string, HashSet<string>> _instanceSubscribers = new();
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, bool>> _instanceSubscribers = new();
|
||||||
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||||
|
|
||||||
// ========== 新增:对象池配置 ==========
|
// ========== 新增:对象池配置 ==========
|
||||||
@ -48,10 +48,7 @@ namespace XNet.Business
|
|||||||
{
|
{
|
||||||
foreach (var instanceId in _instanceSubscribers.Keys)
|
foreach (var instanceId in _instanceSubscribers.Keys)
|
||||||
{
|
{
|
||||||
lock (_instanceSubscribers[instanceId])
|
_instanceSubscribers[instanceId].TryRemove(connId, out _);
|
||||||
{
|
|
||||||
_instanceSubscribers[instanceId].Remove(connId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Console.WriteLine($"[WS .NET 10] 连接断开:{connId},当前连接数:{_connections.Count}");
|
Console.WriteLine($"[WS .NET 10] 连接断开:{connId},当前连接数:{_connections.Count}");
|
||||||
}
|
}
|
||||||
@ -65,17 +62,15 @@ namespace XNet.Business
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_instanceSubscribers.GetOrAdd(instanceId, _ => new HashSet<string>());
|
_instanceSubscribers.GetOrAdd(instanceId, _ => new ConcurrentDictionary<string, bool>());
|
||||||
lock (_instanceSubscribers[instanceId])
|
|
||||||
{
|
_instanceSubscribers[instanceId].TryAdd(connId, true);
|
||||||
_instanceSubscribers[instanceId].Add(connId);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendAgentPositionBatchAsync(List<AgentPositionSyncMsg> syncMsgs)
|
public async Task SendAgentPositionBatchAsync(List<AgentPositionSyncMsg> syncMsgs)
|
||||||
{
|
{
|
||||||
if(syncMsgs.Count == 0) return;
|
if (syncMsgs.Count == 0) return;
|
||||||
|
|
||||||
foreach (var group in syncMsgs.GroupBy(m => m.InstanceId))
|
foreach (var group in syncMsgs.GroupBy(m => m.InstanceId))
|
||||||
{
|
{
|
||||||
@ -109,7 +104,7 @@ namespace XNet.Business
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
msgBytes = tempMs.ToArray();
|
msgBytes = tempMs.ToArray();// 超出池化数组长度,使用新分配的数组
|
||||||
needReturnPooledBytes = false;
|
needReturnPooledBytes = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,21 +125,19 @@ namespace XNet.Business
|
|||||||
var deadConnIds = _deadConnListPool.Get();
|
var deadConnIds = _deadConnListPool.Get();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lock (subscriberConnIds)
|
foreach (var connKv in subscriberConnIds)
|
||||||
{
|
{
|
||||||
foreach (var connId in subscriberConnIds)
|
if (_connections.TryGetValue(connKv.Key, out var socket))
|
||||||
{
|
{
|
||||||
if (_connections.TryGetValue(connId, out var socket))
|
if (needReturnPooledBytes)
|
||||||
{
|
{
|
||||||
// 3. 发送时传递有效长度
|
// 3. 发送时传递有效长度
|
||||||
if (needReturnPooledBytes)
|
_ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connKv.Key, deadConnIds);
|
||||||
{
|
}
|
||||||
_ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connId, deadConnIds);
|
else
|
||||||
}
|
{
|
||||||
else
|
// 3. 发送整串字节数组
|
||||||
{
|
_ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connKv.Key, deadConnIds);
|
||||||
_ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connId, deadConnIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user