mirror of
https://github.com/project-redbud/FunGame-Server.git
synced 2025-04-22 20:19:35 +08:00
647 lines
24 KiB
C#
647 lines
24 KiB
C#
using System.Collections;
|
||
using System.Data;
|
||
using Milimoe.FunGame.Core.Api.Transmittal;
|
||
using Milimoe.FunGame.Core.Api.Utility;
|
||
using Milimoe.FunGame.Core.Entity;
|
||
using Milimoe.FunGame.Core.Interface.Base;
|
||
using Milimoe.FunGame.Core.Library.Common.Addon;
|
||
using Milimoe.FunGame.Core.Library.Common.Network;
|
||
using Milimoe.FunGame.Core.Library.Constant;
|
||
using Milimoe.FunGame.Core.Library.SQLScript.Entity;
|
||
using Milimoe.FunGame.Server.Controller;
|
||
using Milimoe.FunGame.Server.Others;
|
||
using Milimoe.FunGame.Server.Utility;
|
||
|
||
namespace Milimoe.FunGame.Server.Model
|
||
{
|
||
public class ServerModel : IServerModel
|
||
{
|
||
/**
|
||
* Public
|
||
*/
|
||
public bool Running => _Running;
|
||
public ClientSocket? Socket => _Socket;
|
||
public string ClientName => _ClientName;
|
||
public User User => _User;
|
||
public Room Room
|
||
{
|
||
get => _Room;
|
||
set => _Room = value;
|
||
}
|
||
public MySQLHelper? SQLHelper { get; }
|
||
public MailSender? MailSender { get; }
|
||
public bool IsDebugMode { get; }
|
||
|
||
/**
|
||
* Private
|
||
*/
|
||
private GameModeServer? NowGamingServer = null;
|
||
|
||
private ClientSocket? _Socket = null;
|
||
private bool _Running = false;
|
||
private User _User = General.UnknownUserInstance;
|
||
private Room _Room = General.HallInstance;
|
||
private string _ClientName = "";
|
||
|
||
private Guid CheckLoginKey = Guid.Empty;
|
||
private int FailedTimes = 0; // 超过一定次数断开连接
|
||
private string UserName = "";
|
||
private DataSet DsUser = new();
|
||
private readonly Guid Token;
|
||
private readonly ServerSocket Server;
|
||
private readonly DataRequestController DataRequestController;
|
||
private long LoginTime;
|
||
private long LogoutTime;
|
||
private bool IsMatching;
|
||
|
||
public ServerModel(ServerSocket server, ClientSocket socket, bool running, bool isDebugMode)
|
||
{
|
||
Server = server;
|
||
_Socket = socket;
|
||
_Running = running;
|
||
Token = socket.Token;
|
||
this.IsDebugMode = isDebugMode;
|
||
if (Config.SQLMode) SQLHelper = new(this);
|
||
MailSender = SmtpHelper.GetMailSender();
|
||
DataRequestController = new(this);
|
||
}
|
||
|
||
public bool Read(ClientSocket socket)
|
||
{
|
||
// 接收客户端消息
|
||
try
|
||
{
|
||
// 禁止GameModeServer调用
|
||
if ((IServerModel)this is GameModeServer) throw new NotSupportedException("请勿在GameModeServer类中调用此方法");
|
||
|
||
SocketObject[] SocketObjects = socket.Receive();
|
||
if (SocketObjects.Length == 0)
|
||
{
|
||
ServerHelper.WriteLine(GetClientName() + " 发送了空信息。");
|
||
return false;
|
||
}
|
||
SocketObject SocketObject = SocketObjects[0];
|
||
|
||
SocketMessageType type = SocketObject.SocketType;
|
||
Guid token = SocketObject.Token;
|
||
object[] args = SocketObject.Parameters;
|
||
string msg = "";
|
||
|
||
// 验证Token
|
||
if (type != SocketMessageType.HeartBeat && token != Token)
|
||
{
|
||
ServerHelper.WriteLine(GetClientName() + " 使用了非法方式传输消息,服务器拒绝回应 -> [" + SocketSet.GetTypeString(type) + "]");
|
||
return false;
|
||
}
|
||
|
||
if (type == SocketMessageType.EndGame)
|
||
{
|
||
NowGamingServer = null;
|
||
return true;
|
||
}
|
||
|
||
if (type == SocketMessageType.DataRequest)
|
||
{
|
||
return DataRequestHandler(socket, SocketObject);
|
||
}
|
||
|
||
if (type == SocketMessageType.Gaming)
|
||
{
|
||
return GamingMessageHandler(socket, SocketObject);
|
||
}
|
||
|
||
if (type == SocketMessageType.HeartBeat)
|
||
{
|
||
return HeartBeat(socket);
|
||
}
|
||
|
||
switch (type)
|
||
{
|
||
case SocketMessageType.Disconnect:
|
||
ServerHelper.WriteLine("[" + SocketSet.GetTypeString(SocketMessageType.DataRequest) + "] " + GetClientName() + " -> Disconnect", InvokeMessageType.Core);
|
||
msg = "你已成功断开与服务器的连接: " + Config.ServerName + "。 ";
|
||
break;
|
||
}
|
||
return Send(socket, type, msg);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.WriteLine(GetClientName() + " 没有回应。");
|
||
ServerHelper.Error(e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public bool DataRequestHandler(ClientSocket socket, SocketObject SocketObject)
|
||
{
|
||
if (SQLHelper != null)
|
||
{
|
||
Hashtable result = [];
|
||
DataRequestType type = DataRequestType.UnKnown;
|
||
|
||
if (SocketObject.Parameters.Length > 0)
|
||
{
|
||
try
|
||
{
|
||
type = SocketObject.GetParam<DataRequestType>(0);
|
||
Hashtable data = SocketObject.GetParam<Hashtable>(1) ?? [];
|
||
|
||
result = DataRequestController.GetResultData(type, data);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.Error(e);
|
||
SQLHelper.Rollback();
|
||
return Send(socket, SocketMessageType.DataRequest, type, result);
|
||
}
|
||
}
|
||
|
||
return Send(socket, SocketMessageType.DataRequest, type, result);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool GamingMessageHandler(ClientSocket socket, SocketObject SocketObject)
|
||
{
|
||
if (NowGamingServer != null)
|
||
{
|
||
Hashtable result = [];
|
||
GamingType type = GamingType.None;
|
||
|
||
if (SocketObject.Parameters.Length > 0)
|
||
{
|
||
try
|
||
{
|
||
type = SocketObject.GetParam<GamingType>(0);
|
||
Hashtable data = SocketObject.GetParam<Hashtable>(1) ?? [];
|
||
|
||
result = NowGamingServer.GamingMessageHandler(UserName, type, data);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.Error(e);
|
||
return Send(socket, SocketMessageType.Gaming, type, result);
|
||
}
|
||
}
|
||
|
||
return Send(socket, SocketMessageType.Gaming, type, result);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool Send(ClientSocket socket, SocketMessageType type, params object[] objs)
|
||
{
|
||
// 发送消息给客户端
|
||
try
|
||
{
|
||
if (socket.Send(type, objs) == SocketResult.Success)
|
||
{
|
||
switch (type)
|
||
{
|
||
case SocketMessageType.ForceLogout:
|
||
RemoveUser();
|
||
break;
|
||
case SocketMessageType.Disconnect:
|
||
RemoveUser();
|
||
Close();
|
||
break;
|
||
case SocketMessageType.Chat:
|
||
return true;
|
||
}
|
||
object obj = objs[0];
|
||
if (obj.GetType() == typeof(string) && (string)obj != "")
|
||
ServerHelper.WriteLine("[" + SocketSet.GetTypeString(type) + "] " + GetClientName() + " <- " + obj, InvokeMessageType.Core);
|
||
return true;
|
||
}
|
||
throw new CanNotSendToClientException();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.WriteLine(GetClientName() + " 没有回应。");
|
||
ServerHelper.Error(e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public void Start()
|
||
{
|
||
if ((IServerModel)this is GameModeServer) throw new NotSupportedException("请勿在GameModeServer类中调用此方法"); // 禁止GameModeServer调用
|
||
Task StreamReader = Task.Factory.StartNew(CreateStreamReader);
|
||
Task PeriodicalQuerier = Task.Factory.StartNew(CreatePeriodicalQuerier);
|
||
}
|
||
|
||
public void SetClientName(string ClientName)
|
||
{
|
||
_ClientName = ClientName;
|
||
// 添加客户端到列表中
|
||
Server.AddClient(_ClientName, this);
|
||
Config.OnlinePlayerCount++;
|
||
GetUsersCount();
|
||
}
|
||
|
||
public string GetClientName()
|
||
{
|
||
return ServerHelper.MakeClientName(ClientName, User);
|
||
}
|
||
|
||
public void PreLogin(DataSet dsuser, string username, Guid checkloginkey)
|
||
{
|
||
DsUser = dsuser;
|
||
UserName = username;
|
||
CheckLoginKey = checkloginkey;
|
||
}
|
||
|
||
public void CheckLogin()
|
||
{
|
||
// 创建User对象
|
||
_User = Factory.GetUser(DsUser);
|
||
// 检查有没有重复登录的情况
|
||
KickUser();
|
||
// 添加至玩家列表
|
||
AddUser();
|
||
GetUsersCount();
|
||
// CheckLogin
|
||
LoginTime = DateTime.Now.Ticks;
|
||
SQLHelper?.Execute(UserQuery.Update_CheckLogin(UserName, _Socket?.ClientIP.Split(':')[0] ?? "127.0.0.1"));
|
||
}
|
||
|
||
public bool IsLoginKey(Guid key)
|
||
{
|
||
return key == CheckLoginKey;
|
||
}
|
||
|
||
public void LogOut()
|
||
{
|
||
// 从玩家列表移除
|
||
RemoveUser();
|
||
GetUsersCount();
|
||
CheckLoginKey = Guid.Empty;
|
||
}
|
||
|
||
public void ForceLogOut(string msg, string username = "")
|
||
{
|
||
ServerModel serverTask = (ServerModel)Server.GetUser(username == "" ? UserName : username);
|
||
if (serverTask.Socket != null)
|
||
{
|
||
serverTask.Room = General.HallInstance;
|
||
foreach (Room room in Config.RoomList.Cast<Room>())
|
||
{
|
||
QuitRoom(room.Roomid, room.RoomMaster.Id == User.Id);
|
||
}
|
||
serverTask.Send(serverTask.Socket, SocketMessageType.ForceLogout, msg);
|
||
}
|
||
}
|
||
|
||
public bool QuitRoom(string roomid, bool isMaster)
|
||
{
|
||
bool result;
|
||
|
||
Config.RoomList.CancelReady(roomid, User);
|
||
Config.RoomList.QuitRoom(roomid, User);
|
||
Room Room = Config.RoomList[roomid] ?? General.HallInstance;
|
||
// 是否是房主
|
||
if (isMaster)
|
||
{
|
||
List<User> users = Config.RoomList.GetPlayerList(roomid);
|
||
if (users.Count > 0) // 如果此时房间还有人,更新房主
|
||
{
|
||
User NewMaster = users[0];
|
||
Room.RoomMaster = NewMaster;
|
||
SQLHelper?.Execute(RoomQuery.Update_QuitRoom(roomid, User.Id, NewMaster.Id));
|
||
this.Room = General.HallInstance;
|
||
UpdateRoomMaster(Room, true);
|
||
result = true;
|
||
}
|
||
else // 没人了就解散房间
|
||
{
|
||
Config.RoomList.RemoveRoom(roomid);
|
||
SQLHelper?.Execute(RoomQuery.Delete_QuitRoom(roomid, User.Id));
|
||
this.Room = General.HallInstance;
|
||
ServerHelper.WriteLine("[ " + GetClientName() + " ] 解散了房间 " + roomid);
|
||
result = true;
|
||
}
|
||
}
|
||
// 不是房主直接退出房间
|
||
else
|
||
{
|
||
this.Room = General.HallInstance;
|
||
UpdateRoomMaster(Room);
|
||
result = true;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
public void Kick(string msg, string clientname = "")
|
||
{
|
||
// 将客户端踢出服务器
|
||
ServerModel serverTask = (ServerModel)Server.GetClient(clientname == "" ? ClientName : clientname);
|
||
if (serverTask.Socket != null)
|
||
{
|
||
serverTask.Room = General.HallInstance;
|
||
foreach (Room room in Config.RoomList.Cast<Room>())
|
||
{
|
||
QuitRoom(room.Roomid, room.RoomMaster.Id == User.Id);
|
||
}
|
||
RemoveUser();
|
||
serverTask.Send(serverTask.Socket, SocketMessageType.Disconnect, msg);
|
||
}
|
||
Close();
|
||
}
|
||
|
||
public void Chat(string msg)
|
||
{
|
||
ServerHelper.WriteLine(msg);
|
||
foreach (ServerModel Client in Server.ClientList.Cast<ServerModel>())
|
||
{
|
||
if (Room.Roomid == Client.Room.Roomid)
|
||
{
|
||
if (Client != null && User.Id != 0)
|
||
{
|
||
Client.Send(Client.Socket!, SocketMessageType.Chat, User.Username, DateTimeUtility.GetNowShortTime() + msg);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public void SendSystemMessage(ShowMessageType showtype, string msg, string title, int autoclose, params string[] usernames)
|
||
{
|
||
foreach (ServerModel serverTask in Server.UserList.Cast<ServerModel>().Where(model => usernames.Length > 0 && usernames.Contains(model.UserName)))
|
||
{
|
||
if (serverTask != null && serverTask.Socket != null)
|
||
{
|
||
serverTask.Send(serverTask.Socket, SocketMessageType.System, showtype, msg, title, autoclose);
|
||
}
|
||
}
|
||
}
|
||
|
||
public void StartGame(string roomid, List<User> users, params string[] usernames)
|
||
{
|
||
Room room = General.HallInstance;
|
||
if (roomid != "-1")
|
||
{
|
||
room = Config.RoomList[roomid];
|
||
}
|
||
if (room.Roomid == "-1") return;
|
||
// 启动服务器
|
||
TaskUtility.NewTask(() =>
|
||
{
|
||
if (Config.GameModeLoader != null && Config.GameModeLoader.ServerModes.ContainsKey(room.GameMode))
|
||
{
|
||
NowGamingServer = Config.GameModeLoader.GetServerMode(room.GameMode);
|
||
Dictionary<string, IServerModel> others = Server.UserList.Cast<IServerModel>().Where(model => usernames.Contains(model.User.Username) && model.User.Username != UserName).ToDictionary(k => k.User.Username, v => v);
|
||
if (NowGamingServer.StartServer(room.GameMode, room, users, this, others))
|
||
{
|
||
foreach (ServerModel serverTask in Server.UserList.Cast<ServerModel>().Where(model => usernames.Contains(model.User.Username)))
|
||
{
|
||
if (serverTask != null && serverTask.Socket != null)
|
||
{
|
||
Config.RoomList.CancelReady(roomid, serverTask.User);
|
||
serverTask.Send(serverTask.Socket, SocketMessageType.StartGame, room, users);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
public void IntoRoom(string roomid)
|
||
{
|
||
Room = Config.RoomList[roomid];
|
||
foreach (ServerModel Client in Server.ClientList.Cast<ServerModel>().Where(c => c != null && c.Socket != null && roomid == c.Room.Roomid))
|
||
{
|
||
if (User.Id != 0)
|
||
{
|
||
Client.Send(Client.Socket!, SocketMessageType.Chat, User.Username, DateTimeUtility.GetNowShortTime() + " [ " + User.Username + " ] 进入了房间。");
|
||
}
|
||
}
|
||
}
|
||
|
||
public void UpdateRoomMaster(Room Room, bool bolIsUpdateRoomMaster = false)
|
||
{
|
||
foreach (ServerModel Client in Server.ClientList.Cast<ServerModel>().Where(c => c != null && c.Socket != null && Room.Roomid == c.Room.Roomid))
|
||
{
|
||
if (User.Id != 0)
|
||
{
|
||
Client.Send(Client.Socket!, SocketMessageType.Chat, User.Username, DateTimeUtility.GetNowShortTime() + " [ " + User.Username + " ] 离开了房间。");
|
||
if (bolIsUpdateRoomMaster && Room.RoomMaster?.Id != 0 && Room.Roomid != "-1")
|
||
{
|
||
Client.Send(Client.Socket!, SocketMessageType.UpdateRoomMaster, Room);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public bool HeartBeat(ClientSocket socket)
|
||
{
|
||
return Send(socket, SocketMessageType.HeartBeat, "");
|
||
}
|
||
|
||
public void StartMatching(RoomType type, User user)
|
||
{
|
||
IsMatching = true;
|
||
ServerHelper.WriteLine(GetClientName() + " 开始匹配。类型:" + RoomSet.GetTypeString(type));
|
||
TaskUtility.NewTask(async () =>
|
||
{
|
||
if (IsMatching)
|
||
{
|
||
Room room = await MatchingRoom(type, user);
|
||
if (IsMatching && Socket != null)
|
||
{
|
||
Send(Socket, SocketMessageType.MatchRoom, room);
|
||
}
|
||
IsMatching = false;
|
||
}
|
||
}).OnError(e =>
|
||
{
|
||
ServerHelper.Error(e);
|
||
IsMatching = false;
|
||
});
|
||
}
|
||
|
||
public void StopMatching()
|
||
{
|
||
if (IsMatching)
|
||
{
|
||
ServerHelper.WriteLine(GetClientName() + " 取消了匹配。");
|
||
IsMatching = false;
|
||
}
|
||
}
|
||
|
||
private async Task<Room> MatchingRoom(RoomType roomtype, User user)
|
||
{
|
||
int i = 1;
|
||
int time = 0;
|
||
while (IsMatching)
|
||
{
|
||
// 先列出符合条件的房间
|
||
List<Room> targets = Config.RoomList.ListRoom.Where(r => r.RoomType == roomtype).ToList();
|
||
|
||
// 匹配Elo
|
||
foreach (Room room in targets)
|
||
{
|
||
// 计算房间平均Elo
|
||
List<User> players = Config.RoomList.GetPlayerList(room.Roomid);
|
||
if (players.Count > 0)
|
||
{
|
||
decimal avgelo = players.Sum(u => u.Statistics.EloStats.ContainsKey(0) ? u.Statistics.EloStats[0] : 0M) / players.Count;
|
||
decimal userelo = user.Statistics.EloStats.ContainsKey(0) ? user.Statistics.EloStats[0] : 0M;
|
||
if (userelo >= avgelo - (300 * i) && userelo <= avgelo + (300 * i))
|
||
{
|
||
return room;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!IsMatching) break;
|
||
|
||
// 等待10秒
|
||
await Task.Delay(10 * 1000);
|
||
time += 10 * 1000;
|
||
if (time >= 50 * 1000)
|
||
{
|
||
// 50秒后不再匹配Elo,直接返回第一个房间
|
||
if (targets.Count > 0)
|
||
{
|
||
return targets[0];
|
||
}
|
||
break;
|
||
}
|
||
i++;
|
||
}
|
||
|
||
return General.HallInstance;
|
||
}
|
||
|
||
private void KickUser()
|
||
{
|
||
if (User.Id != 0)
|
||
{
|
||
string user = User.Username;
|
||
if (Server.ContainsUser(user))
|
||
{
|
||
ServerHelper.WriteLine("OnlinePlayers: 玩家 " + user + " 重复登录!");
|
||
ForceLogOut("您的账号在别处登录,已强制下线。");
|
||
}
|
||
}
|
||
}
|
||
|
||
private bool AddUser()
|
||
{
|
||
if (User.Id != 0 && this != null)
|
||
{
|
||
Server.AddUser(User.Username, this);
|
||
UserName = User.Username;
|
||
ServerHelper.WriteLine("OnlinePlayers: 玩家 " + User.Username + " 已添加");
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private bool RemoveUser()
|
||
{
|
||
if (User.Id != 0 && this != null)
|
||
{
|
||
LogoutTime = DateTime.Now.Ticks;
|
||
int TotalMinutes = Convert.ToInt32((new DateTime(LogoutTime) - new DateTime(LoginTime)).TotalMinutes);
|
||
SQLHelper?.Execute(UserQuery.Update_GameTime(User.Username, TotalMinutes));
|
||
if (SQLHelper?.Result == SQLResult.Success)
|
||
{
|
||
ServerHelper.WriteLine("OnlinePlayers: 玩家 " + User.Username + " 本次已游玩" + TotalMinutes + "分钟");
|
||
}
|
||
else ServerHelper.WriteLine("OnlinePlayers: 无法更新玩家 " + User.Username + " 的游戏时长");
|
||
if (Server.RemoveUser(User.Username))
|
||
{
|
||
ServerHelper.WriteLine("OnlinePlayers: 玩家 " + User.Username + " 已移除");
|
||
_User = General.UnknownUserInstance;
|
||
return true;
|
||
}
|
||
else ServerHelper.WriteLine("OnlinePlayers: 移除玩家 " + User.Username + " 失败");
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private void GetUsersCount()
|
||
{
|
||
ServerHelper.WriteLine($"目前在线客户端数量: {Server.ClientCount}(已登录的玩家数量:{Server.UserCount})");
|
||
}
|
||
|
||
private void CreateStreamReader()
|
||
{
|
||
Thread.Sleep(20);
|
||
ServerHelper.WriteLine("Creating: StreamReader -> " + GetClientName() + " ...OK");
|
||
while (Running)
|
||
{
|
||
if (Socket != null)
|
||
{
|
||
if (!Read(Socket))
|
||
{
|
||
FailedTimes++;
|
||
if (FailedTimes >= Config.MaxConnectionFaileds)
|
||
{
|
||
RemoveUser();
|
||
Close();
|
||
ServerHelper.WriteLine(GetClientName() + " Error -> Too Many Faileds.");
|
||
ServerHelper.WriteLine(GetClientName() + " Close -> StreamReader is Closed.");
|
||
break;
|
||
}
|
||
}
|
||
else if (FailedTimes - 1 >= 0) FailedTimes--;
|
||
}
|
||
else
|
||
{
|
||
RemoveUser();
|
||
Close();
|
||
ServerHelper.WriteLine(GetClientName() + " Error -> Socket is Closed.");
|
||
ServerHelper.WriteLine(GetClientName() + " Close -> StringStream is Closed.");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CreatePeriodicalQuerier()
|
||
{
|
||
Thread.Sleep(20);
|
||
ServerHelper.WriteLine("Creating: PeriodicalQuerier -> " + GetClientName() + " ...OK");
|
||
while (Running)
|
||
{
|
||
// 每两小时触发一次SQL服务器的心跳查询,防止SQL服务器掉线
|
||
try
|
||
{
|
||
Thread.Sleep(2 * 1000 * 3600);
|
||
SQLHelper?.ExecuteDataSet(UserQuery.Select_IsExistUsername(UserName));
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.Error(e);
|
||
RemoveUser();
|
||
Close();
|
||
ServerHelper.WriteLine(GetClientName() + " Error -> Socket is Closed.");
|
||
ServerHelper.WriteLine(GetClientName() + " Close -> StringStream is Closed.");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void Close()
|
||
{
|
||
try
|
||
{
|
||
SQLHelper?.Close();
|
||
MailSender?.Dispose();
|
||
Socket?.Close();
|
||
_Socket = null;
|
||
_Running = false;
|
||
Server.RemoveClient(ClientName);
|
||
Config.OnlinePlayerCount--;
|
||
GetUsersCount();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
ServerHelper.Error(e);
|
||
}
|
||
}
|
||
}
|
||
}
|