XNet/XNet.Business/Tank/Manager/PlayerManager.cs
2025-12-31 18:29:29 +08:00

713 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using MessagePack;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using NanoidDotNet;
using System.Numerics;
using System.Threading.Tasks;
using XNet.Business.Dto;
using XNet.Business.Entity;
using XNet.Business.Net;
using XNet.Business.PathNavigation;
namespace XNet.Business.Tank.Manager
{
public class PlayerManager
{
private readonly WsConnectionManager _wsManager;
private readonly NavMeshManager _navMeshManager;
private readonly SceneAgent _sceneAgent;
private readonly List<string> RoleColor = new List<string>();
public PlayerManager(WsConnectionManager wsManager, NavMeshManager navMeshManager, SceneAgent sceneAgent)
{
_wsManager = wsManager;
_navMeshManager = navMeshManager;
_sceneAgent = sceneAgent;
RoleColor.AddRange(["#8CE929", "#FFBF5F", "#FFF45F", "#FF5F5F", "#5FECFF", "#EF5FFF",
"#497352","#736049","#6B7349","#734949","#495F73","#734972"]);
}
public async Task SubcribeRoom(string connId, byte[] data)
{
// 处理订阅实例请求
var subscribeReq = MessagePackSerializer.Deserialize<SubscribeRoom>(data);
if (subscribeReq != null)
{
//roomId为空时新创建一个唯一房间ID
if (string.IsNullOrWhiteSpace(subscribeReq.RoomId))
{
subscribeReq.RoomId = Nanoid.Generate();
}
await _wsManager.SendSerializeMessageToPointWsSocket(connId,
new BaseRoomMsg<string>
{
Type = WsMsgType.SUBSCRIBE_ROOM,
RoomId = subscribeReq.RoomId,
SenderId = connId,
Data = string.Empty
}
);
}
}
public async Task ChangeRoomKey(string connId, byte[] data)
{
// 处理订阅实例请求
var changeRoomReq = MessagePackSerializer.Deserialize<SubscribeInstanceReq>(data);
if (changeRoomReq != null && !string.IsNullOrWhiteSpace(changeRoomReq.RoomId))
{
string createRoomId = changeRoomReq.RoomId;
await _wsManager.SubscribeInstance(connId, changeRoomReq.MapKey, false, createRoomId, true);//Map_Forest Test
}
}
public async Task LockTarget(string connId, byte[] data)
{
var playerInfo = _wsManager.GetConnectionInfo(connId);
if (playerInfo == null) return;
// 处理锁定实例请求
var lockReq = MessagePackSerializer.Deserialize<LockTargetReq>(data);
await _wsManager.SendMessageToRoomBatchAsync(playerInfo.FirstRoomId, playerInfo.PlayerId, WsMsgType.ROOM_MSG, lockReq);
}
public async Task CreateOrJoinRoom(string connId, byte[] data)
{
var playerInfo = _wsManager.GetConnectionInfo(connId);
if (playerInfo == null) return;
// 处理订阅实例请求
var roomReq = MessagePackSerializer.Deserialize<CreateOrJoinRoomReq>(data);
if (roomReq != null)
{
//roomId为空时新创建一个唯一房间ID
if (string.IsNullOrWhiteSpace(roomReq.RoomId))
{
roomReq.RoomId = Nanoid.Generate();
}
string createRoomId = roomReq.RoomId;
createRoomId = (await _wsManager.SubscribeInstance(connId, roomReq.MapKey, true, createRoomId, false))!;//Map_Forest Test
if (playerInfo != null)
{
if (string.IsNullOrWhiteSpace(playerInfo.PlayerId))
{
playerInfo.PlayerId = connId;// string.IsNullOrWhiteSpace(roomReq.PlayerId) ? connId : roomReq.PlayerId;
playerInfo.IsAI = false;
}
playerInfo.HeadImageUrl = roomReq.HeadImageUrl;
playerInfo.NickName = roomReq.NickName;
}
var room = _wsManager.GetPlayerRoomInfo(roomReq.RoomId, connId);
if (room != null)
{
room.RoomName = roomReq.RoomName;
InitSets(room, connId, roomReq.AICount);
await _wsManager.SendBaseSingleMessage(connId, WsMsgType.PRIVATGE,
new EnterRoomReply
{
Type = WsMsgType.CHANGE_ROOM_KEY_NAME,
MapKey = room.MapKey
}
);
ControlPlayer? self = _wsManager.GetConnectionInfo(connId);
await _wsManager.SendMessageToRoomBatchAsync(room.RoomId, connId, WsMsgType.PRIVATGE, new RoomUserSync
{
Type = WsMsgType.CREATE_OR_JOIN_ROOM,
RoomId = room.RoomId,
PlayerId = connId,
HeadImageUrl = self?.HeadImageUrl,
SetIdx = self?.SetIdx ?? 0,
IsPublic = room.IsPublic
});
if (_wsManager.InstanceSubscribers.TryGetValue(room.RoomId, out var roomUsers))
{
foreach (var userKv in roomUsers)
{
var other = _wsManager.GetConnectionInfo(userKv.Key);
if (other != null && other.PlayerId != connId)
{
await _wsManager.SendBaseSingleMessage(connId, WsMsgType.PRIVATGE,
new RoomUserSync
{
Type = WsMsgType.CREATE_OR_JOIN_ROOM,
RoomId = room.RoomId,
PlayerId = connId,
NickName = other.NickName,
HeadImageUrl = other.HeadImageUrl,
SetIdx = other.SetIdx,
IsPublic = room.IsPublic
}
);
}
}
}
}
}
}
private void InitSets(PlayerRoomInfo<ControlPlayer> room, string connId, int aiCount)
{
ControlPlayer self = _wsManager.GetConnectionInfo(connId)!;
// 初始化座位数组长度为房间最大人数默认填充null
List<RoomSet?> sets = new List<RoomSet?>();
for (int i = 0; i < room.MaxPlayerCount; i++)
{
sets.Add(null);
}
// 初始化座位索引为-1无可用座位
int setIdx = -1;
// 如果已有座位数据,遍历填充座位数组并重新分配索引
if (room.Sets.Count > 0)
{
int tmpIndex = 0;
// 遍历所有已存在的座位键值对 (对应TS: state.sets.forEach)
foreach (var kvp in room.Sets)
{
string key = kvp.Key;
RoomSet set = kvp.Value;
// 从玩家集合中获取当前玩家 (对应TS: state.players.get(key))
ControlPlayer? playerItem = _wsManager.GetConnectionInfo(key);
if (playerItem == null)
{
sets[tmpIndex] = null;
}
else
{
sets[tmpIndex] = set;
playerItem.SetIdx = (byte)tmpIndex; // uint8对应byte强转
}
tmpIndex++;
}
// 查找第一个空座位 或 当前客户端sessionId对应的座位 (对应TS: sets.findIndex)
setIdx = sets.FindIndex(s => s == null || s.PlayerId == connId);
}
else
{
// 没有任何座位数据默认分配第0个座位
setIdx = 0;
}
// 如果获取到有效座位索引
if (setIdx != -1)
{
// 第一个进房间的玩家,将房主标识置空 (对应TS逻辑)
if (setIdx == 0)
{
room.Host = null;
}
// 给当前玩家赋值座位索引
self.SetIdx = (byte)setIdx;
// 更新/新增座位数据到状态集合中
if (room.Sets.ContainsKey(connId))
{
// 已存在该玩家的座位,更新信息
RoomSet roomSet = room.Sets[connId];
roomSet.PlayerId = connId;
roomSet.SetIdx = (byte)setIdx;
}
else
{
// 不存在则新建座位对象,添加到集合
RoomSet roomSet = new RoomSet();
roomSet.PlayerId = connId;
roomSet.SetIdx = (byte)setIdx;
room.Sets.Add(connId, roomSet);
}
}
// AI数量初始化逻辑消息中存在aiCount 且 未初始化过AI数量赋值
if (aiCount > 0 && room.AIPlayerCount == 0)
{
room.AIPlayerCount = aiCount;
}
}
public async Task SendMessageToRoomOthers(string connId, byte[] data)
{
var playerState = _wsManager.GetConnectionInfo(connId);
if (playerState != null)
{
await _wsManager.SendMessageToRoomBatchAsync(playerState.FirstRoomId, data, [connId]);
}
}
public async Task SendMessageToRoom(string connId, byte[] data)
{
var playerState = _wsManager.GetConnectionInfo(connId);
if (playerState != null)
{
await _wsManager.SendMessageToRoomBatchAsync(playerState.FirstRoomId, data);
}
}
public async Task SendMessageToSelf(string connId, byte[] data)
{
var playerState = _wsManager.GetConnectionInfo(connId);
if (playerState != null)
{
await _wsManager.SendBaseSingleMessage(connId, WsMsgType.TO_SELF, data);
}
}
public async Task RequestPath(string connId, byte[] data)
{
var posReq = MessagePackSerializer.Deserialize<PlayerPosReq>(data);
if (posReq != null)
{
var playerState = _wsManager.GetConnectionInfo(connId);
//if (playerState != null)
//{
// await _wsManager.SendMessageToRoomBatchAsync(playerState.FirstRoomId, data, [connId]);
//}
if (playerState != null && playerState.IsCanControl && posReq.EndPos != null)
{
var room = _wsManager.GetPlayerRoomInfo(playerState.FirstRoomId, connId);
if (room != null)
{
//客户端传上来的位置,角度等为了省流量,存成整形,这里处理成单精度浮点型要除以 LocationMultiply默认除以1000
var toPt = new Vector3(posReq.EndPos.X, posReq.EndPos.Y, posReq.EndPos.Z) / Global.LocationMultiply;
var closetPt = _navMeshManager.GetClosetPoint(room.RoomId!, toPt,out long startRef);
if (closetPt != null)
{
room.Crowd?.RequestMoveTarget(playerState.Agent, startRef, closetPt.Value);
}
}
}
}
await Task.CompletedTask;
}
public async Task RequestInit(string connId, byte[] data)
{
// 处理订阅实例请求
var message = MessagePackSerializer.Deserialize<PlayerInitReq>(data);
if (message != null)
{
var player = _wsManager.GetConnectionInfo(connId);
if (player == null) return;
var room = _wsManager.GetPlayerRoomInfo(player.FirstRoomId, connId);
if (room != null)
{
if (room.EndTime == null)
{
room.EndTime = DateTime.Now.AddMilliseconds(PlayerRoomInfo<ControlPlayer>.GameMillisecond);
}
if (room.WillEndTime == null)
{
room.WillEndTime = DateTime.Now.AddMilliseconds(PlayerRoomInfo<ControlPlayer>.GameMillisecond - PlayerRoomInfo<ControlPlayer>.GameWillEndMillisecond);
}
if (room.StartGameTime == null)
{
room.StartGameTime = DateTime.Now;
}
if (message.IsAI)
{
message.PlayerId = Nanoid.Generate();
}
else
{
message.PlayerId = player.PlayerId;
}
await RequestInitPlayer(room!, room?.MapKey!, player, message);
//可以移动了
await SetPlayerIsCanControl(room?.RoomId!, player, true, null);
//开始无敌
SetPlayerIsCanHit(connId, false);
_ = Task.Delay(5000).ContinueWith((t) =>
{
//5秒后会收到伤害
SetPlayerIsCanHit(connId, true);
});
var pos = player.CurrentPos * Global.LocationMultiply;
var convertPos = new Vec3((int)pos.X, (int)pos.Y, (int)pos.Z);
var euler = player.CurrentEuler * Global.LocationMultiply;
var convertEuler = new Vec3((int)euler.X, (int)euler.Y, (int)euler.Z);
await _wsManager.SendBaseSingleMessage(connId, WsMsgType.PRIVATGE, new RequestInitMsg
{
Type = WsMsgType.REQUEST_INIT,
PlayerId = player.PlayerId,
Pos = convertPos,
Euler = convertEuler,
IsAI = message.IsAI,
GameTime = (long)(PlayerRoomInfo<ControlPlayer>.GameMillisecond - (DateTime.Now - room!.StartGameTime.Value).TotalMilliseconds)
});
}
}
}
public async Task Login(string connId, byte[] data)
{
var loginReq = MessagePackSerializer.Deserialize<PlayerLoginReq>(data);
if (loginReq != null)
{
var player = _wsManager.GetConnectionInfo(connId);
if (player != null)
{
//await _wsManager.RemoveConnection(connId);
player.NickName = loginReq.NickName;
player.HeadImageUrl = loginReq.HeadImageUrl;
player.DeviceColor = loginReq.DeviceColor;
player.IsAI = false;
var room = _wsManager.GetPlayerRoomInfo(player.FirstRoomId, connId);
if (room != null)
{
player.PlayerId = connId;
player.IsAI = false;
room.Players[connId] = player;
if (room.Players.Count == 1)
{
room.Host = null;
}
float blood = await CheckLoginAndCreateHostAI(connId, room, loginReq.MapKey, loginReq);
await _wsManager.SendBaseSingleMessage(connId, WsMsgType.PRIVATGE, new LoginResultMsg
{
Type = WsMsgType.LOGIN,
PlayerId = player.PlayerId,
Result = LoginResult.Success,
Blood = player.Blood,
DeviceColor = player.DeviceColor
});
}
}
}
}
public void SetPlayerIsCanHit(string connId, bool isCanHit)
{
var player = _wsManager.GetConnectionInfo(connId);
if (player != null)
{
player.IsCanHit = isCanHit;
}
}
public bool GetPlayerIsCanHit(string connId)
{
var player = _wsManager.GetConnectionInfo(connId);
if (player != null)
{
return player.IsCanHit;
}
return false;
}
private async Task<float> CheckLoginAndCreateHostAI(string connId, PlayerRoomInfo<ControlPlayer> room, string mapKey, PlayerLoginReq loginReq)
{
bool isFirstPlayer = room.Host == null;
bool hasAI = room.Players.Any(p => p.Value.IsAI);
if (isFirstPlayer)
{
if (room.Players.TryGetValue(connId, out var loginClient))
{
room.Host = loginClient;
loginClient.BirthPositions.Clear();
if (loginReq.BirthPositions != null)
{
for (int i = 0; i < loginReq.BirthPositions.Length; i++)
{
loginClient.BirthPositions.Add(loginReq.BirthPositions[i]);
}
}
if (!hasAI && loginReq.HasAI)
{
var deviceInfo = DeviceManager.Instance.DeviceInfo;
for (int i = 0; i < Global.AICount; i++)
{
int bodyIdx = new Random().Next(0, Global.MaxPartKindCount);
int weaponIdx = new Random().Next(0, Global.MaxPartKindCount);
int armorIdx = new Random().Next(0, Global.MaxPartKindCount);
int chassisIdx = new Random().Next(0, Global.MaxPartKindCount);
var startPos = loginClient.BirthPositions[new Random().Next(0, loginClient.BirthPositions.Count)];
var message = new PlayerInitReq
{
IsAI = true,
PlayerId = Nanoid.Generate(),
BodyIdx = bodyIdx,
WeaponIdx = weaponIdx,
ArmorIdx = armorIdx,
ChassisIdx = chassisIdx,
DeviceColor = RoleColor[new Random().Next(0, RoleColor.Count)],
FindFaceDistance = deviceInfo.Weapon[weaponIdx].FindFaceDistance,
AttackPower = deviceInfo.Weapon[weaponIdx].AttackPower,
Armor = deviceInfo.Armor[armorIdx].Armor,
Speed = deviceInfo.Chassis[chassisIdx].Speed,
Radius = loginReq.Radius,
StartPos = startPos,
Euler = new Vec3(0, 0, 0)
//CurrentEuler = new Dto.Vec3(),
//NickName = loginReq.NickName,
};
var aiPlayer = new ControlPlayer
{
PlayerId = message.PlayerId,
IsAI = true,
IsEnterMap = false
};
await this.RequestInitPlayer(room, mapKey, aiPlayer, message);
//可以移动了
await SetPlayerIsCanControl(room.RoomId, aiPlayer, true, null);
//开始无敌
SetPlayerIsCanHit(connId, false);
_ = Task.Delay(5000).ContinueWith((t) =>
{
//5秒后会收到伤害
SetPlayerIsCanHit(connId, true);
});
}
}
return loginClient.MaxBlood;
}
}
return 0f;
}
private async Task RequestInitPlayer(PlayerRoomInfo<ControlPlayer> room, string mapKey, ControlPlayer loginPlayer, PlayerInitReq message)
{
if (loginPlayer == null || loginPlayer.IsEnterMap)
{
return;
}
loginPlayer.IsAI = message.IsAI;
loginPlayer.PlayerId = message.PlayerId;
loginPlayer.NickName = message.NickName;
var deviceInfo = DeviceManager.Instance.DeviceInfo;
var armorInfo = deviceInfo.Armor[message.ArmorIdx];
var weaponInfo = deviceInfo.Weapon[message.WeaponIdx];
var chassisInfo = deviceInfo.Chassis[message.ChassisIdx];
loginPlayer.MaxBlood = armorInfo.Blood;
loginPlayer.Blood = armorInfo.Blood;
loginPlayer.BodyIdx = message.BodyIdx;
loginPlayer.WeaponIdx = message.WeaponIdx;
loginPlayer.ArmorIdx = message.ArmorIdx;
loginPlayer.ChassisIdx = message.ChassisIdx;
loginPlayer.FindFaceDistance = weaponInfo.FindFaceDistance;
loginPlayer.AttackPower = weaponInfo.AttackPower;
loginPlayer.Armor = armorInfo.Armor;
loginPlayer.Speed = chassisInfo.Speed;
loginPlayer.Radius = message.Radius;
loginPlayer.DeviceColor = message.DeviceColor;
loginPlayer.CurrentPos = new Vector3(message.StartPos!.X, message.StartPos.Y, message.StartPos.Z)!;
loginPlayer.CurrentEuler = new Vector3(message.Euler!.X, message.Euler.Y, message.Euler.Z)!;
float speedScale = 1f;
if (loginPlayer.IsAI)
{
speedScale = 0.5f;//AI速度减为一半
loginPlayer.Skill1Level = (byte)new Random().Next(1, 4);
loginPlayer.Skill2Level = (byte)new Random().Next(1, 4);
loginPlayer.Skill3Level = (byte)new Random().Next(1, 4);
loginPlayer.Skill4Level = (byte)new Random().Next(1, 4);
}
var startPos = _navMeshManager.GetClosetPoint(room.RoomId, loginPlayer.CurrentPos / 1000, out long startRef);
if (startPos != null)
{
loginPlayer.CurrentPos = startPos.Value;
loginPlayer.Agent = _sceneAgent.AddAgent(room.RoomId, loginPlayer.CurrentPos, loginPlayer.Radius, loginPlayer.Radius, loginPlayer.PlayerId, loginPlayer.Speed * speedScale * 2, loginPlayer.Speed * speedScale);
}
loginPlayer.TmpPos = loginPlayer.CurrentPos;
_sceneAgent.AgentTeleport(room.RoomId, loginPlayer.Agent.idx, loginPlayer.CurrentPos);
loginPlayer.IsEnterMap = true;
if (!loginPlayer.IsAI)
{
//同步其他玩家的信息给自己
await SyncOtherPlayerInfoToSelf(room.RoomId, loginPlayer);
//向其他玩家发送自己的信息
await SyncSelfInfoToOtherPlayer(room.RoomId, loginPlayer);
//同步AI信息给登录玩家
await SyncAIToOthersPlayer(room.RoomId, loginPlayer);
}
}
/// <summary>
/// 同步其他玩家的信息给自己
/// </summary>
/// <param name="roomId"></param>
/// <param name="client"></param>
private async Task SyncOtherPlayerInfoToSelf(string roomId, ControlPlayer client)
{
var room = _wsManager.GetPlayerRoomInfo(roomId, client.PlayerId);
if (room != null)
{
foreach (var playerKv in room.Players)
{
var player = playerKv.Value;
if (!player.IsAI && player.IsEnterMap && player.PlayerId != client.PlayerId)
{
var pos = player.CurrentPos * Global.LocationMultiply;
var convertPos = new Vec3((int)pos.X, (int)pos.Y, (int)pos.Z);
var euler = player.CurrentEuler * Global.LocationMultiply;
var convertEuler = new Vec3((int)euler.X, (int)euler.Y, (int)euler.Z);
await _wsManager.SendBaseSingleMessage(client.PlayerId, WsMsgType.PRIVATGE, new PlayerInitSync
{
Type = WsMsgType.ADD_OR_UPDATE_PLAYER,
PlayerId = player.PlayerId,
IsAI = player.IsAI,
NickName = player.NickName,
Blood = player.MaxBlood,
BodyIdx = player.BodyIdx,
ArmorIdx = player.ArmorIdx,
ChassisIdx = player.ChassisIdx,
DeviceColor = player.DeviceColor,
HeadImageUrl = player.HeadImageUrl,
Position = convertPos,
EulerAngles = convertEuler,
});
}
}
}
}
/// <summary>
/// //向其他玩家发送自己的信息
/// </summary>
/// <param name="roomId"></param>
/// <param name="client"></param>
private async Task SyncSelfInfoToOtherPlayer(string roomId, ControlPlayer client)
{
var room = _wsManager.GetPlayerRoomInfo(roomId, client.PlayerId);
if (room != null)
{
foreach (var playerKv in room.Players)
{
var player = playerKv.Value;
if (!player.IsAI && player.IsEnterMap && player.PlayerId != client.PlayerId)
{
var pos = client.CurrentPos * Global.LocationMultiply;
var convertPos = new Vec3((int)pos.X, (int)pos.Y, (int)pos.Z);
var euler = client.CurrentEuler * Global.LocationMultiply;
var convertEuler = new Vec3((int)euler.X, (int)euler.Y, (int)euler.Z);
await _wsManager.SendBaseSingleMessage(player.PlayerId, WsMsgType.PRIVATGE, new PlayerInitSync
{
Type = WsMsgType.ADD_OR_UPDATE_PLAYER,
PlayerId = client.PlayerId,
IsAI = false,
NickName = client.NickName,
Blood = client.MaxBlood,
BodyIdx = client.BodyIdx,
ArmorIdx = client.ArmorIdx,
ChassisIdx = client.ChassisIdx,
DeviceColor = client.DeviceColor,
HeadImageUrl = client.HeadImageUrl,
Position = convertPos,
EulerAngles = convertEuler,
});
}
}
}
}
/// <summary>
/// 同步AI给登录玩家
/// </summary>
/// <param name="roomId"></param>
/// <param name="client"></param>
private async Task SyncAIToOthersPlayer(string roomId, ControlPlayer client)
{
var room = _wsManager.GetPlayerRoomInfo(roomId, client.PlayerId);
if (room != null)
{
foreach (var playerKv in room.Players)
{
var player = playerKv.Value;
if (player.IsAI && player.IsEnterMap)
{
var pos = client.CurrentPos * Global.LocationMultiply;
var convertPos = new Vec3((int)pos.X, (int)pos.Y, (int)pos.Z);
var euler = client.CurrentEuler * Global.LocationMultiply;
var convertEuler = new Vec3((int)euler.X, (int)euler.Y, (int)euler.Z);
await _wsManager.SendBaseSingleMessage(player.PlayerId, WsMsgType.PRIVATGE, new PlayerInitSync
{
Type = WsMsgType.ADD_OR_UPDATE_PLAYER,
PlayerId = client.PlayerId,
IsAI = false,
NickName = client.NickName,
Blood = client.MaxBlood,
BodyIdx = client.BodyIdx,
ArmorIdx = client.ArmorIdx,
ChassisIdx = client.ChassisIdx,
DeviceColor = client.DeviceColor,
HeadImageUrl = client.HeadImageUrl,
Position = convertPos,
EulerAngles = convertEuler,
});
}
}
}
}
public async Task SetPlayerIsCanControl(string roomId, ControlPlayer client, bool isCanControl, PlayerLocationSyncReq? locationSyncReq)
{
client.IsCanControl = true;
if (locationSyncReq != null)
{
var pos = locationSyncReq.Pos;
var convertPos = new Vector3((int)(pos!.X / Global.LocationMultiply), (int)(pos.Y / Global.LocationMultiply), (int)(pos.Z / Global.LocationMultiply));
var euler = client.CurrentEuler * Global.LocationMultiply;
var convertEuler = new Vector3((int)(euler!.X / Global.LocationMultiply), (int)(euler.Y / Global.LocationMultiply), (int)(euler.Z / Global.LocationMultiply));
var closetPos = _navMeshManager.GetClosetPoint(roomId, convertPos, out long startRef);
if (closetPos != null)
{
_sceneAgent.AgentTeleport(roomId, client.Agent.idx, closetPos.Value);
}
client.CurrentEuler = convertEuler;
}
}
}
}