From 8aec496fcb1c310c01849fb5f43813764c110d6d Mon Sep 17 00:00:00 2001 From: milimoe <110188673+milimoe@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:08:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=A1=A5=E5=85=A8?= =?UTF-8?q?=20API=20=E5=AE=9E=E7=8E=B0=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PayloadModel 添加 event 属性,添加 Room,Main 的 API 控制器 * 实现 SQLHelper 的自增 ID、异步版本功能 * 填充一些请求控制器的方法 * 添加报价的核心操作 * 涉及库存的物品获取应该使用 Guid 而不是 ItemId * 添加 InventoryController * 添加更新房间设置和用户房间的状态管理 * 优化 API Token 秘钥管理;修复服务器统一报错信息 BUG * 优雅的关闭服务器;补全了所有数据请求实现;API Token 加密方式修改;添加了服务器初始化时创建管理员账号的必要步骤 * 完善 Web API 控制器 --------- Co-authored-by: yeziuku --- .../Controllers/DataRequestController.cs | 1249 +++++++++++++++-- FunGame.Server/FunGame.Server.csproj | 11 +- FunGame.Server/Images/logo.ico | Bin 16958 -> 0 bytes FunGame.Server/Main.cs | 66 +- FunGame.Server/Models/ConsoleModel.cs | 96 +- FunGame.Server/Models/ServerModel.cs | 40 +- FunGame.Server/Services/DataRequestService.cs | 382 +++-- .../Services/DataUtility/MySQLHelper.cs | 237 +++- .../Services/DataUtility/SQLiteHelper.cs | 230 ++- FunGame.Server/Services/FunGameSystem.cs | 79 +- FunGame.Server/Services/General.cs | 10 +- FunGame.Server/logo.ico | Bin 16958 -> 67646 bytes .../Controllers/DataRequestController.cs | 1 + .../Controllers/GamingRequestController.cs | 1 + .../Controllers/InventoryController.cs | 437 ++++++ FunGame.WebAPI/Controllers/MainController.cs | 73 + FunGame.WebAPI/Controllers/RoomController.cs | 340 +++++ .../Controllers/UserCenterController.cs | 104 ++ FunGame.WebAPI/Controllers/UserController.cs | 72 +- FunGame.WebAPI/FunGame.WebAPI.csproj | 12 +- FunGame.WebAPI/Images/logo.ico | Bin 16958 -> 0 bytes FunGame.WebAPI/Models/PayloadModel.cs | 5 + FunGame.WebAPI/Program.cs | 39 +- .../Services/APIBearerTokenHandler.cs | 2 +- FunGame.WebAPI/logo.ico | Bin 0 -> 67646 bytes FunGameServer.sln | 6 + 26 files changed, 3064 insertions(+), 428 deletions(-) delete mode 100644 FunGame.Server/Images/logo.ico create mode 100644 FunGame.WebAPI/Controllers/InventoryController.cs create mode 100644 FunGame.WebAPI/Controllers/MainController.cs create mode 100644 FunGame.WebAPI/Controllers/RoomController.cs create mode 100644 FunGame.WebAPI/Controllers/UserCenterController.cs delete mode 100644 FunGame.WebAPI/Images/logo.ico create mode 100644 FunGame.WebAPI/logo.ico diff --git a/FunGame.Server/Controllers/DataRequestController.cs b/FunGame.Server/Controllers/DataRequestController.cs index 996a4a3..b760d61 100644 --- a/FunGame.Server/Controllers/DataRequestController.cs +++ b/FunGame.Server/Controllers/DataRequestController.cs @@ -4,13 +4,13 @@ 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.Event; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.SQLScript.Common; using Milimoe.FunGame.Core.Library.SQLScript.Entity; using Milimoe.FunGame.Server.Model; using Milimoe.FunGame.Server.Others; using Milimoe.FunGame.Server.Services; +using ProjectRedbud.FunGame.SQLQueryExtension; namespace Milimoe.FunGame.Server.Controller { @@ -51,6 +51,7 @@ namespace Milimoe.FunGame.Server.Controller { Dictionary result = []; _lastRequest = type; + ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); switch (type) { @@ -117,7 +118,8 @@ namespace Milimoe.FunGame.Server.Controller UpdatePassword(data, result); break; - case DataRequestType.Room_GetRoomSettings: + case DataRequestType.Room_UpdateRoomSettings: + UpdateRoomSettings(data, result); break; case DataRequestType.Room_GetRoomPlayerCount: @@ -125,6 +127,75 @@ namespace Milimoe.FunGame.Server.Controller break; case DataRequestType.Room_UpdateRoomMaster: + await UpdateRoomMaster(data, result); + break; + + case DataRequestType.UserCenter_UpdateUser: + UpdateUser(data, result); + break; + + case DataRequestType.UserCenter_UpdatePassword: + UpdatePassword(data, result); + break; + + case DataRequestType.UserCenter_DailySignIn: + DailySignIn(result); + break; + + case DataRequestType.Inventory_GetStore: + GetStore(data, result); + break; + + case DataRequestType.Inventory_GetMarket: + GetMarket(data, result); + break; + + case DataRequestType.Inventory_StoreBuy: + StoreBuy(data, result); + break; + + case DataRequestType.Inventory_MarketBuy: + MarketBuy(data, result); + break; + + case DataRequestType.Inventory_UpdateInventory: + UpdateInventory(data, result); + break; + + case DataRequestType.Inventory_Use: + Use(data, result); + break; + + case DataRequestType.Inventory_StoreSell: + StoreSell(data, result); + break; + + case DataRequestType.Inventory_MarketSell: + MarketSell(data, result); + break; + + case DataRequestType.Inventory_MarketDelist: + MarketDelist(data, result); + break; + + case DataRequestType.Inventory_UpdateMarketPrice: + UpdateMarketPrice(data, result); + break; + + case DataRequestType.Inventory_GetOffer: + GetOffer(data, result); + break; + + case DataRequestType.Inventory_MakeOffer: + MakeOffer(data, result); + break; + + case DataRequestType.Inventory_ReviseOffer: + ReviseOffer(data, result); + break; + + case DataRequestType.Inventory_RespondOffer: + RespondOffer(data, result); break; default: @@ -147,7 +218,6 @@ namespace Milimoe.FunGame.Server.Controller Guid key = Guid.Empty; if (requestData.Count >= 1) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); key = DataRequest.GetDictionaryJsonObject(requestData, "key"); if (Server.IsLoginKey(key)) { @@ -169,9 +239,8 @@ namespace Milimoe.FunGame.Server.Controller /// 获取公告 /// /// - private void GetServerNotice(Dictionary resultData) + private static void GetServerNotice(Dictionary resultData) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); resultData.Add("notice", Config.ServerNotice); } @@ -185,12 +254,12 @@ namespace Milimoe.FunGame.Server.Controller Room room = General.HallInstance; if (requestData.Count >= 3) { - RoomType type = DataRequest.GetDictionaryJsonObject(requestData, "roomtype"); - string gamemodule = DataRequest.GetDictionaryJsonObject(requestData, "gamemoduleserver") ?? ""; - string gamemap = DataRequest.GetDictionaryJsonObject(requestData, "gamemap") ?? ""; - bool isrank = DataRequest.GetDictionaryJsonObject(requestData, "isrank"); - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest) + " : " + RoomSet.GetTypeString(type) + " (" + string.Join(", ", [gamemodule, gamemap]) + ")", InvokeMessageType.DataRequest); - if (gamemodule == "" || gamemap == "" || FunGameSystem.GameModuleLoader is null || !FunGameSystem.GameModuleLoader.ModuleServers.ContainsKey(gamemodule) || !FunGameSystem.GameModuleLoader.Maps.ContainsKey(gamemap)) + RoomType type = DataRequest.GetDictionaryJsonObject(requestData, "roomType"); + string gameModule = DataRequest.GetDictionaryJsonObject(requestData, "moduleServer") ?? ""; + string gameMap = DataRequest.GetDictionaryJsonObject(requestData, "map") ?? ""; + bool isRank = DataRequest.GetDictionaryJsonObject(requestData, "isRank"); + ServerHelper.WriteLine("[CreateRoom] " + RoomSet.GetTypeString(type) + " (" + string.Join(", ", [gameModule, gameMap]) + ")", InvokeMessageType.DataRequest); + if (gameModule == "" || gameMap == "" || FunGameSystem.GameModuleLoader is null || !FunGameSystem.GameModuleLoader.ModuleServers.ContainsKey(gameModule) || !FunGameSystem.GameModuleLoader.Maps.ContainsKey(gameMap)) { ServerHelper.WriteLine("缺少对应的模组或地图,无法创建房间。"); resultData.Add("room", room); @@ -198,7 +267,7 @@ namespace Milimoe.FunGame.Server.Controller } User user = DataRequest.GetDictionaryJsonObject(requestData, "master") ?? Factory.GetUser(); string password = DataRequest.GetDictionaryJsonObject(requestData, "password") ?? ""; - int maxusers = DataRequest.GetDictionaryJsonObject(requestData, "maxusers"); + int maxusers = DataRequest.GetDictionaryJsonObject(requestData, "maxUsers"); if (user.Id != 0) { @@ -214,14 +283,14 @@ namespace Milimoe.FunGame.Server.Controller } if (roomid != "-1" && SQLHelper != null) { - SQLHelper.Execute(RoomQuery.Insert_CreateRoom(SQLHelper, roomid, user.Id, type, gamemodule, gamemap, isrank, password, maxusers)); + SQLHelper.Execute(RoomQuery.Insert_CreateRoom(SQLHelper, roomid, user.Id, type, gameModule, gameMap, isRank, password, maxusers)); if (SQLHelper.Result == SQLResult.Success) { ServerHelper.WriteLine("[CreateRoom] Master: " + user.Username + " RoomID: " + roomid); - SQLHelper.ExecuteDataSet(RoomQuery.Select_IsExistRoom(SQLHelper, roomid)); - if (SQLHelper.Result == SQLResult.Success && SQLHelper.DataSet.Tables[0].Rows.Count > 0) + DataRow? dr = SQLHelper.ExecuteDataRow(RoomQuery.Select_IsExistRoom(SQLHelper, roomid)); + if (dr != null) { - room = Factory.GetRoom(SQLHelper.DataSet.Tables[0].Rows[0], user); + room = Factory.GetRoom(dr, user); FunGameSystem.RoomList.AddRoom(room); } } @@ -235,9 +304,8 @@ namespace Milimoe.FunGame.Server.Controller /// 更新房间列表 /// /// - private void UpdateRoom(Dictionary resultData) + private static void UpdateRoom(Dictionary resultData) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); resultData.Add("rooms", FunGameSystem.RoomList.ListRoom); // 传RoomList } @@ -251,7 +319,6 @@ namespace Milimoe.FunGame.Server.Controller bool result = false; if (requestData.Count >= 2) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); string roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; bool isMaster = DataRequest.GetDictionaryJsonObject(requestData, "isMaster"); @@ -273,7 +340,6 @@ namespace Milimoe.FunGame.Server.Controller bool result = false; if (requestData.Count >= 1) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); string roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; if (roomid != "-1") @@ -285,6 +351,7 @@ namespace Milimoe.FunGame.Server.Controller { FunGameSystem.RoomList.IntoRoom(roomid, Server.User); Server.InRoom = FunGameSystem.RoomList[roomid]; + Server.User.OnlineState = OnlineState.InRoom; await Server.SendClients(Server.Listener.ClientList.Where(c => c != null && roomid == c.InRoom.Roomid && c.User.Id != 0), SocketMessageType.Chat, Server.User.Username, DateTimeUtility.GetNowShortTime() + " [ " + Server.User.Username + " ] 进入了房间。"); result = true; @@ -309,18 +376,18 @@ namespace Milimoe.FunGame.Server.Controller bool result = true; if (requestData.Count >= 1) { - bool iscancel = DataRequest.GetDictionaryJsonObject(requestData, "iscancel"); + bool iscancel = DataRequest.GetDictionaryJsonObject(requestData, "isCancel"); if (!iscancel) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest) + " : Start", InvokeMessageType.DataRequest); - RoomType type = DataRequest.GetDictionaryJsonObject(requestData, "roomtype"); + ServerHelper.WriteLine("[MatchRoom] Start", InvokeMessageType.DataRequest); + RoomType type = DataRequest.GetDictionaryJsonObject(requestData, "roomType"); User user = DataRequest.GetDictionaryJsonObject(requestData, "matcher") ?? Factory.GetUser(); StartMatching(type, user); } else { // 取消匹配 - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest) + " : Cancel", InvokeMessageType.DataRequest); + ServerHelper.WriteLine("[MatchRoom] Cancel", InvokeMessageType.DataRequest); StopMatching(); } } @@ -338,7 +405,6 @@ namespace Milimoe.FunGame.Server.Controller string roomid = "-1"; if (requestData.Count >= 1) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; User user = Server.User; @@ -364,7 +430,6 @@ namespace Milimoe.FunGame.Server.Controller string roomid = "-1"; if (requestData.Count >= 1) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; User user = Server.User; @@ -406,7 +471,6 @@ namespace Milimoe.FunGame.Server.Controller bool result = false; if (requestData.Count >= 2) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); string roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; bool isMaster = DataRequest.GetDictionaryJsonObject(requestData, "isMaster"); @@ -482,12 +546,15 @@ namespace Milimoe.FunGame.Server.Controller { if (FunGameSystem.GameModuleLoader != null && FunGameSystem.GameModuleLoader.ModuleServers.ContainsKey(room.GameModule)) { + room.RoomState = RoomState.Gaming; Server.NowGamingServer = FunGameSystem.GameModuleLoader.GetServerMode(room.GameModule); + Server.User.OnlineState = OnlineState.Gaming; Dictionary all = Server.Listener.UserList.Cast().ToDictionary(k => k.User.Username, v => v); // 给其他玩家赋值模组服务器 foreach (IServerModel model in all.Values.Where(s => s.User.Username != Server.User.Username)) { model.NowGamingServer = Server.NowGamingServer; + model.User.OnlineState = OnlineState.Gaming; } GamingObject obj = new(room, users, Server, all); if (Server.NowGamingServer.StartServer(obj)) @@ -522,7 +589,6 @@ namespace Milimoe.FunGame.Server.Controller bool success = false; if (requestData.Count >= 4) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); string username = DataRequest.GetDictionaryJsonObject(requestData, "username") ?? ""; string password = DataRequest.GetDictionaryJsonObject(requestData, "password") ?? ""; string email = DataRequest.GetDictionaryJsonObject(requestData, "email") ?? ""; @@ -549,7 +615,6 @@ namespace Milimoe.FunGame.Server.Controller /// private async Task Login(Dictionary requestData, Dictionary resultData) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); string msg = ""; User user = Factory.GetUser(); @@ -569,18 +634,6 @@ namespace Milimoe.FunGame.Server.Controller ServerHelper.WriteLine("客户端提供的参数不足。", InvokeMessageType.DataRequest, LogLevel.Warning); } - LoginEventArgs eventArgs = new(username, password, autokey); - FunGameSystem.ServerPluginLoader?.OnBeforeLoginEvent(this, eventArgs); - FunGameSystem.WebAPIPluginLoader?.OnBeforeLoginEvent(this, eventArgs); - if (eventArgs.Cancel) - { - msg = $"{DataRequestSet.GetTypeString(_lastRequest)} 请求已取消。{(eventArgs.EventMsg != "" ? $"原因:{eventArgs.EventMsg}" : "")}"; - ServerHelper.WriteLine(msg); - resultData.Add("msg", msg); - resultData.Add("user", user); - return; - } - // CheckLogin的情况 if (key != Guid.Empty) { @@ -606,8 +659,6 @@ namespace Milimoe.FunGame.Server.Controller } } - FunGameSystem.ServerPluginLoader?.OnAfterLoginEvent(this, eventArgs); - FunGameSystem.WebAPIPluginLoader?.OnAfterLoginEvent(this, eventArgs); resultData.Add("msg", msg); resultData.Add("user", user); } @@ -622,10 +673,9 @@ namespace Milimoe.FunGame.Server.Controller string msg = "无法找回您的密码,请稍后再试。"; // 返回的验证信息 if (requestData.Count >= 3) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); - string username = DataRequest.GetDictionaryJsonObject(requestData, ForgetVerifyCodes.Column_Username) ?? ""; - string email = DataRequest.GetDictionaryJsonObject(requestData, ForgetVerifyCodes.Column_Email) ?? ""; - string verifycode = DataRequest.GetDictionaryJsonObject(requestData, ForgetVerifyCodes.Column_ForgetVerifyCode) ?? ""; + string username = DataRequest.GetDictionaryJsonObject(requestData, "username") ?? ""; + string email = DataRequest.GetDictionaryJsonObject(requestData, "email") ?? ""; + string verifycode = DataRequest.GetDictionaryJsonObject(requestData, "forgetVerifyCode") ?? ""; // 客户端发来了验证码就进行验证,没有发就生成 if (verifycode.Trim() != "") @@ -633,11 +683,11 @@ namespace Milimoe.FunGame.Server.Controller // 先检查验证码 if (SQLHelper != null) { - SQLHelper.ExecuteDataSet(ForgetVerifyCodes.Select_ForgetVerifyCode(SQLHelper, username, email, verifycode)); - if (SQLHelper.Result == SQLResult.Success) + DataRow? dr = SQLHelper.ExecuteDataRow(ForgetVerifyCodes.Select_ForgetVerifyCode(SQLHelper, username, email, verifycode)); + if (dr != null) { // 检查验证码是否过期 - if (!DateTime.TryParse(SQLHelper.DataSet.Tables[0].Rows[0][ForgetVerifyCodes.Column_SendTime].ToString(), out DateTime SendTime)) + if (!DateTime.TryParse(dr[ForgetVerifyCodes.Column_SendTime].ToString(), out DateTime SendTime)) { SendTime = General.DefaultTime; } @@ -650,7 +700,7 @@ namespace Milimoe.FunGame.Server.Controller else { // 检查验证码是否正确 - if (verifycode.Equals(SQLHelper.DataSet.Tables[0].Rows[0][ForgetVerifyCodes.Column_ForgetVerifyCode])) + if (verifycode.Equals(dr[ForgetVerifyCodes.Column_ForgetVerifyCode])) { ServerHelper.WriteLine("[ForgerPassword] Username: " + username + " Email: " + email); SQLHelper.Execute(ForgetVerifyCodes.Delete_ForgetVerifyCode(SQLHelper, username, email)); @@ -675,8 +725,8 @@ namespace Milimoe.FunGame.Server.Controller else { // 检查验证码是否发送过和是否过期 - SQLHelper.ExecuteDataSet(ForgetVerifyCodes.Select_HasSentForgetVerifyCode(SQLHelper, username, email)); - if (SQLHelper.Result != SQLResult.Success || (DateTime.TryParse(SQLHelper.DataSet.Tables[0].Rows[0][ForgetVerifyCodes.Column_SendTime].ToString(), out DateTime SendTime) && (DateTime.Now - SendTime).TotalMinutes >= 10)) + DataRow? dr = SQLHelper.ExecuteDataRow(ForgetVerifyCodes.Select_HasSentForgetVerifyCode(SQLHelper, username, email)); + if (dr is null || (DateTime.TryParse(dr[ForgetVerifyCodes.Column_SendTime].ToString(), out DateTime SendTime) && (DateTime.Now - SendTime).TotalMinutes >= 10)) { // 发送验证码,需要先删除之前过期的验证码 SQLHelper.Execute(ForgetVerifyCodes.Delete_ForgetVerifyCode(SQLHelper, username, email)); @@ -688,8 +738,8 @@ namespace Milimoe.FunGame.Server.Controller { // 发送验证码 string ServerName = Config.ServerName; - string Subject = $"[{ServerName}] FunGame 找回密码验证码"; - string Body = $"亲爱的 {username},
您正在找回[{ServerName}]账号的密码,您的验证码是 {forgetVerify} ,10分钟内有效,请及时输入!

{ServerName}
{DateTimeUtility.GetDateTimeToString(TimeType.LongDateOnly)}"; + string Subject = $"[{ServerName}] 找回密码验证码"; + string Body = $"亲爱的 {username},
您正在找回 [{ServerName}] 账号的密码,您的验证码是 {forgetVerify} ,10分钟内有效,请及时输入!

{ServerName}
{DateTimeUtility.GetDateTimeToString(TimeType.LongDateOnly)}"; string[] To = [email]; if (MailSender.Send(MailSender.CreateMail(Subject, Body, System.Net.Mail.MailPriority.Normal, true, To)) == MailSendResult.Success) { @@ -704,7 +754,7 @@ namespace Milimoe.FunGame.Server.Controller } else // 不使用MailSender的情况 { - ServerHelper.WriteLine(Server.GetClientName() + $" 验证码为:{forgetVerify},请服务器管理员告知此用户"); + ServerHelper.WriteLine(Server.GetClientName() + $" 验证码为:{forgetVerify},但因 SMTP 服务未开启,请服务器管理员告知此用户"); msg = ""; } } @@ -712,7 +762,7 @@ namespace Milimoe.FunGame.Server.Controller else { // 发送过验证码且验证码没有过期 - string ForgetVerifyCode = (string)SQLHelper.DataSet.Tables[0].Rows[0][ForgetVerifyCodes.Column_ForgetVerifyCode]; + string ForgetVerifyCode = (string)dr[ForgetVerifyCodes.Column_ForgetVerifyCode]; ServerHelper.WriteLine(Server.GetClientName() + $" 十分钟内已向{email}发送过验证码:{ForgetVerifyCode}"); msg = ""; } @@ -723,54 +773,113 @@ namespace Milimoe.FunGame.Server.Controller resultData.Add("msg", msg); } - /// - /// 更新用户的密码 - /// - /// - /// - private void UpdatePassword(Dictionary requestData, Dictionary resultData) - { - string msg = "无法更新您的密码,请稍后再试。"; - if (requestData.Count >= 2) - { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); - string username = DataRequest.GetDictionaryJsonObject(requestData, UserQuery.Column_Username) ?? ""; - string password = DataRequest.GetDictionaryJsonObject(requestData, UserQuery.Column_Password) ?? ""; - if (username.Trim() != "" && password.Trim() != "") - { - FunGameSystem.UpdateUserKey(username); - password = password.Encrypt(FunGameSystem.GetUserKey(username)); - SQLHelper?.Execute(UserQuery.Update_Password(SQLHelper, username, password)); - if (SQLHelper?.Success ?? false) - { - // 更新成功返回空值 - msg = ""; - } - } - } - resultData.Add("msg", msg); - } - #endregion #region Room + /// + /// 更新房间设置 + /// + /// + /// + private void UpdateRoomSettings(Dictionary requestData, Dictionary resultData) + { + bool result = false; + string msg = ""; + string room = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; + RoomType newType = DataRequest.GetDictionaryJsonObject(requestData, "type"); + string newPassword = DataRequest.GetDictionaryJsonObject(requestData, "password") ?? ""; + int newMaxUsers = DataRequest.GetDictionaryJsonObject(requestData, "maxUsers"); + string newModule = DataRequest.GetDictionaryJsonObject(requestData, "module") ?? ""; + string newMap = DataRequest.GetDictionaryJsonObject(requestData, "map") ?? ""; + User user = Server.User; + if (room != "-1" && FunGameSystem.RoomList.IsExist(room)) + { + Room r = FunGameSystem.RoomList[room]; + if (user.Id != r.RoomMaster.Id) + { + msg = "更新失败,只有房主才可以更新房间设置。"; + } + else + { + result = true; + ServerHelper.WriteLine("[UpdateRoomSettings] User: " + user.Username + " RoomID: " + r.Roomid); + if (r.RoomState == RoomState.Created) + { + r.RoomType = newType; + r.Password = newPassword; + r.MaxUsers = newMaxUsers; + r.GameModule = newModule; + r.GameMap = newMap; + } + else + { + msg = "更新失败,只能在房间状态稳定时更新其设置。"; + } + } + } + resultData.Add("result", result); + resultData.Add("msg", msg); + resultData.Add("room", room); + } + /// /// 获取房间内玩家数量 /// /// /// - private void GetRoomPlayerCount(Dictionary requestData, Dictionary resultData) + private static void GetRoomPlayerCount(Dictionary requestData, Dictionary resultData) { string roomid = "-1"; if (requestData.Count >= 1) { - ServerHelper.WriteLine(Server.GetClientName() + " -> " + DataRequestSet.GetTypeString(_lastRequest), InvokeMessageType.DataRequest); roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; } resultData.Add("count", FunGameSystem.RoomList.GetUserCount(roomid)); } + /// + /// 更新房主 + /// + /// + /// + /// + private async Task UpdateRoomMaster(Dictionary requestData, Dictionary resultData) + { + bool result = false; + + if (requestData.Count >= 2) + { + string roomid = DataRequest.GetDictionaryJsonObject(requestData, "roomid") ?? "-1"; + User newMaster = DataRequest.GetDictionaryJsonObject(requestData, "newMaster") ?? Factory.GetUser(); + + if (roomid != "-1" && FunGameSystem.RoomList.IsExist(roomid) && newMaster.Id != 0) + { + Room room = FunGameSystem.RoomList[roomid]; + User oldMaster = room.RoomMaster; + room.RoomMaster = newMaster; + result = true; + + if (SQLHelper != null) + { + SQLHelper.UpdateRoomMaster(roomid, newMaster.Id); + if (SQLHelper.Result == SQLResult.Success) + { + await Server.SendClients(Server.Listener.ClientList.Where(c => c != null && c.InRoom.Roomid == roomid), SocketMessageType.UpdateRoomMaster, room); + ServerHelper.WriteLine($"[UpdateRoomMaster] RoomID: {roomid} 房主变更: {oldMaster.Username} -> {newMaster.Username}"); + } + } + } + } + else + { + ServerHelper.WriteLine("客户端提供的参数不足。", InvokeMessageType.DataRequest, LogLevel.Warning); + } + + // 返回结果 + resultData.Add("result", result); + } + /// /// 开始匹配 /// @@ -779,6 +888,7 @@ namespace Milimoe.FunGame.Server.Controller private void StartMatching(RoomType type, User user) { _isMatching = true; + if (user.OnlineState == OnlineState.Online) user.OnlineState = OnlineState.Matching; ServerHelper.WriteLine(Server.GetClientName() + " 开始匹配。类型:" + RoomSet.GetTypeString(type)); TaskUtility.NewTask(async () => { @@ -806,6 +916,7 @@ namespace Milimoe.FunGame.Server.Controller if (_isMatching) { ServerHelper.WriteLine(Server.GetClientName() + " 取消了匹配。"); + if (Server.User.OnlineState == OnlineState.Matching) Server.User.OnlineState = OnlineState.Online; _isMatching = false; } } @@ -822,18 +933,32 @@ namespace Milimoe.FunGame.Server.Controller double time = 0; // 已经匹配的时间 double expandInterval = 10; // 扩大匹配范围的间隔时间 double maxTime = 50; // 最大匹配时间 + bool isRefreshRoom = false; // 是否刷新房间列表 + + // 匹配房间类型(如果是All,则匹配所有房间) + List targets; + if (roomtype == RoomType.All) + { + targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching)]; + } + else + { + targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => (r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching) && r.RoomType == roomtype)]; + } while (_isMatching) { - // 匹配房间类型(如果是All,则匹配所有房间) - List targets; - if (roomtype == RoomType.All) + if (isRefreshRoom) { - targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching)]; - } - else - { - targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => (r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching) && r.RoomType == roomtype)]; + isRefreshRoom = false; + if (roomtype == RoomType.All) + { + targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching)]; + } + else + { + targets = [.. FunGameSystem.RoomList.ListRoom.Where(r => (r.RoomState == RoomState.Created || r.RoomState == RoomState.Matching) && r.RoomType == roomtype)]; + } } // 如果匹配停止,则退出 @@ -865,6 +990,8 @@ namespace Milimoe.FunGame.Server.Controller if (time >= expandInterval * i) { i++; + // 刷新房间列表 + isRefreshRoom = true; } // 达到最大匹配时间后不再匹配Elo,直接返回第一个房间 if (time >= maxTime) @@ -880,5 +1007,937 @@ namespace Milimoe.FunGame.Server.Controller } #endregion + + #region UserCenter + + /// + /// 更新用户(全部数据) + /// + /// + /// + private void UpdateUser(Dictionary requestData, Dictionary resultData) + { + string msg = "无法更新用户数据,请稍后再试。"; + if (SQLHelper != null && requestData.Count > 0) + { + User user = DataRequest.GetDictionaryJsonObject(requestData, "user") ?? Factory.GetUser(); + SQLHelper.UpdateUser(user); + if (SQLHelper.Success) + { + msg = ""; + } + } + resultData.Add("msg", msg); + } + + /// + /// 更新用户的密码 + /// + /// + /// + private void UpdatePassword(Dictionary requestData, Dictionary resultData) + { + string msg = "无法更新您的密码,请稍后再试。"; + if (requestData.Count >= 2) + { + string username = DataRequest.GetDictionaryJsonObject(requestData, "username") ?? ""; + string password = DataRequest.GetDictionaryJsonObject(requestData, "password") ?? ""; + if (username.Trim() != "" && password.Trim() != "") + { + FunGameSystem.UpdateUserKey(username); + password = password.Encrypt(FunGameSystem.GetUserKey(username)); + SQLHelper?.UpdatePassword(username, password); + if (SQLHelper?.Success ?? false) + { + // 更新成功返回空值 + msg = ""; + } + } + } + resultData.Add("msg", msg); + } + + /// + /// 每日签到 + /// + /// + private void DailySignIn(Dictionary resultData) + { + if (SQLHelper != null) + { + long userId = Server.User.Id; + if (userId != 0) + { + DataRow? dr = SQLHelper.ExecuteDataRow(UserSignIns.Select_GetUserSignIn(SQLHelper, userId)); + if (dr != null) + { + int days = Convert.ToInt32(dr[UserSignIns.Column_Days]) + 1; + bool isSigned = Convert.ToInt32(dr[UserSignIns.Column_IsSigned]) != 0; + if (dr[UserSignIns.Column_LastTime] != DBNull.Value && DateTime.TryParseExact(dr[UserSignIns.Column_LastTime].ToString(), General.GeneralDateTimeFormat, null, System.Globalization.DateTimeStyles.None, out DateTime dt)) + { + if (isSigned) + { + resultData.Add("msg", "今天已经签到过了,请明天再来。"); + return; + } + if ((DateTime.Now - dt).TotalDays > 1) + { + days = 1; + } + } + SQLHelper.Execute(UserSignIns.Update_UserSignIn(SQLHelper, userId, days)); + if (SQLHelper.Success) + { + resultData.Add("msg", $"签到成功!你已经连续签到 {days} 天!"); + return; + } + } + } + } + resultData.Add("msg", "签到失败!"); + } + + #endregion + + #region Inventory + + /// + /// 获取商店信息 + /// + /// + /// + private void GetStore(Dictionary requestData, Dictionary resultData) + { + List stores = []; + + if (requestData.Count > 0) + { + long[] ids = DataRequest.GetDictionaryJsonObject(requestData, "ids") ?? []; + stores = SQLHelper?.GetStoresWithGoods(ids) ?? []; + } + + resultData.Add("result", stores.Count > 0); + resultData.Add("stores", stores); + } + + /// + /// 获取市场信息 + /// + /// + /// + private void GetMarket(Dictionary requestData, Dictionary resultData) + { + List markets = []; + + if (requestData.Count > 0) + { + long[] users = DataRequest.GetDictionaryJsonObject(requestData, "users") ?? []; + MarketItemState state = DataRequest.GetDictionaryJsonObject(requestData, "state"); + + if (users.Length > 0) + { + foreach (long userid in users) + { + markets.AddRange(SQLHelper?.GetAllMarketsItem(userid, state) ?? []); + } + } + else + { + markets = SQLHelper?.GetAllMarketsItem(0, state) ?? []; + } + + long[] items = DataRequest.GetDictionaryJsonObject(requestData, "items") ?? []; + if (items.Length > 0) + { + markets = [.. markets.Where(i => items.Contains(i.Id))]; + } + } + + resultData.Add("result", markets.Count > 0); + resultData.Add("markets", markets); + } + + /// + /// 购买物品(商店) + /// + /// + /// + private void StoreBuy(Dictionary requestData, Dictionary resultData) + { + bool result = true; + double totalCost = 0; + List buyResult = []; + if (SQLHelper != null && requestData.Count > 0) + { + long storeid = DataRequest.GetDictionaryJsonObject(requestData, "storeid"); + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + string currency = DataRequest.GetDictionaryJsonObject(requestData, "currency") ?? ""; + Dictionary counts = DataRequest.GetDictionaryJsonObject>(requestData, "counts") ?? []; + bool ignore = DataRequest.GetDictionaryJsonObject(requestData, "ignore"); + + Store? store = SQLHelper.GetStore(storeid); + User? user = SQLHelper.GetUserById(userid, true); + if (store != null && user != null) + { + try + { + SQLHelper.NewTransaction(); + + foreach (long goodsId in counts.Keys) + { + Goods goods = store.Goods[goodsId]; + int count = counts[goodsId]; + if (count > goods.Stock) + { + result = false; + buyResult.Add($"购买失败,原因:库存不足,当前库存为:{goods.Stock},购买数量:{count}。"); + continue; + } + if (goods.GetPrice(currency, out double price)) + { + bool subResult = true; + bool useCredits = true; + double totalPrice = price * count; + if (currency == General.GameplayEquilibriumConstant.InGameCurrency && user.Inventory.Credits >= totalPrice) + { + user.Inventory.Credits -= totalPrice; + } + else + { + subResult = false; + buyResult.Add($"购买失败,原因:需要花费 {totalPrice} {General.GameplayEquilibriumConstant.InGameCurrency},但是您只有 {user.Inventory.Credits} {General.GameplayEquilibriumConstant.InGameCurrency}。"); + } + if (currency == General.GameplayEquilibriumConstant.InGameMaterial && user.Inventory.Materials >= totalPrice) + { + user.Inventory.Materials -= totalPrice; + useCredits = false; + } + else + { + subResult = false; + buyResult.Add($"购买失败,原因:需要花费 {totalPrice} {General.GameplayEquilibriumConstant.InGameMaterial},但是您只有 {user.Inventory.Credits} {General.GameplayEquilibriumConstant.InGameMaterial}。"); + } + if (subResult) + { + goods.Stock -= count; + totalCost += totalPrice; + ProcessStoreBuy(goods, useCredits, price, count, user); + buyResult.Add($"成功消费:{totalPrice} {currency},购买了 {count} 个 {goods.Name}。"); + } + else + { + result = false; + } + } + } + + if (result || (!result && ignore)) + { + SQLHelper.UpdateInventory(user.Inventory); + } + + if (SQLHelper.Success) + { + SQLHelper.Commit(); + } + else + { + SQLHelper.Rollback(); + } + } + catch (Exception e) + { + SQLHelper.Rollback(); + ServerHelper.Error(e); + buyResult.Add("暂时无法处理此购买,请稍后再试。"); + } + } + } + resultData.Add("result", result); + resultData.Add("msg", string.Join("\r\n", buyResult)); + } + + /// + /// 处理商店购买 + /// + /// + /// + /// + /// + /// + private static void ProcessStoreBuy(Goods goods, bool useCredits, double price, int count, User user) + { + foreach (Item item in goods.Items) + { + for (int i = 0; i < count; i++) + { + Item newItem = item.Copy(); + newItem.IsTradable = false; + newItem.NextTradableTime = DateTimeUtility.GetTradableTime(); + newItem.Price = useCredits ? Calculation.Round2Digits(price * 0.35) : Calculation.Round2Digits(price * 7); + newItem.User = user; + newItem.EntityState = EntityState.Added; + user.Inventory.Items.Add(newItem); + } + } + } + + /// + /// 购买物品(市场) + /// + /// + /// + private void MarketBuy(Dictionary requestData, Dictionary resultData) + { + bool result = false; + string msg = "无法购买此物品,请稍后再试。"; + if (SQLHelper != null && requestData.Count > 0) + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + + MarketItem? marketItem = SQLHelper.GetMarketItem(itemGuid); + if (marketItem != null) + { + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + double price = DataRequest.GetDictionaryJsonObject(requestData, "price"); + + try + { + User? buyer = SQLHelper.GetUserById(userid, true); + User? itemUser = SQLHelper.GetUserById(marketItem.User.Id, true); + if (itemUser != null && buyer != null && itemUser.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + if (buyer.Inventory.Credits >= price) + { + buyer.Inventory.Credits -= price; + double fee = Calculation.Round2Digits(price * 0.15); + itemUser.Inventory.Credits += price - fee; + result = true; + } + else + { + msg = $"购买失败,原因:需要花费 {price} {General.GameplayEquilibriumConstant.InGameCurrency},但是您只有 {buyer.Inventory.Credits} {General.GameplayEquilibriumConstant.InGameCurrency}。"; + } + + if (result) + { + SQLHelper.NewTransaction(); + + try + { + item.EntityState = EntityState.Deleted; + SQLHelper.DeleteMarketItem(itemGuid); + + Item newItem = item.Copy(); + newItem.IsTradable = false; + newItem.NextTradableTime = DateTimeUtility.GetTradableTime(); + newItem.User = buyer; + newItem.EntityState = EntityState.Added; + buyer.Inventory.Items.Add(newItem); + + SQLHelper.UpdateInventory(itemUser.Inventory); + SQLHelper.UpdateInventory(buyer.Inventory); + } + catch + { + result = false; + } + + if (result) + { + msg = $"成功消费:{price} {General.GameplayEquilibriumConstant.InGameCurrency},购买了 {itemUser.Username} 出售的 {item.Name}。"; + SQLHelper.Commit(); + } + else + { + SQLHelper.Rollback(); + } + } + } + } + catch (Exception e) + { + SQLHelper.Rollback(); + ServerHelper.Error(e); + msg = "暂时无法处理此购买,请稍后再试。"; + } + } + else + { + msg = "目标物品不存在。"; + } + } + resultData.Add("result", result); + resultData.Add("msg", msg); + } + + /// + /// 更新库存 + /// + /// + /// + private void UpdateInventory(Dictionary requestData, Dictionary resultData) + { + string msg = "无法更新库存数据,请稍后再试。"; + if (SQLHelper != null && requestData.Count > 0) + { + Inventory inventory = DataRequest.GetDictionaryJsonObject(requestData, "inventory") ?? Factory.GetInventory(); + bool fullUpdate = DataRequest.GetDictionaryJsonObject(requestData, "fullUpdate"); + SQLHelper.UpdateInventory(inventory, fullUpdate); + if (SQLHelper.Success) + { + msg = ""; + } + } + resultData.Add("msg", msg); + } + + /// + /// 使用物品 + /// + /// + /// + private void Use(Dictionary requestData, Dictionary resultData) + { + bool result = false; + string msg = "无法使用此物品,请稍后再试。"; + if (SQLHelper != null && requestData.Count > 0) + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + Character[] targets = DataRequest.GetDictionaryJsonObject(requestData, "targets") ?? []; + long useCount = DataRequest.GetDictionaryJsonObject(requestData, "useCount"); + User? user = SQLHelper.GetUserById(userid, true); + if (user != null && user.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + // 暂定标准实现是传这两个参数,作用目标和使用数量 + Dictionary args = new() + { + { "targets", targets }, + { "useCount", useCount } + }; + bool used = item.UseItem(args); + if (used) + { + SQLHelper.UpdateInventory(user.Inventory); + } + } + if (result) + { + msg = ""; + } + } + resultData.Add("result", result); + resultData.Add("msg", msg); + } + + /// + /// 出售物品(商店) + /// + /// + /// + private void StoreSell(Dictionary requestData, Dictionary resultData) + { + bool result = false; + string msg = "无法出售此物品,请稍后再试。"; + if (SQLHelper != null && requestData.Count > 0) + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + User? user = SQLHelper.GetUserById(userid, true); + if (user != null && user.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + if (!item.IsSellable) + { + msg = $"此物品无法出售{(item.NextSellableTime != DateTime.MinValue ? $",此物品将在 {item.NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售" : "")}。"; + } + else + { + double reward = item.Price; + user.Inventory.Credits += reward; + item.EntityState = EntityState.Deleted; + SQLHelper.UpdateInventory(user.Inventory); + } + } + if (result) + { + msg = ""; + } + } + resultData.Add("msg", msg); + } + + /// + /// 出售物品(市场) + /// + /// + /// + private void MarketSell(Dictionary requestData, Dictionary resultData) + { + string msg = "无法上架此物品,请稍后再试。"; + if (requestData.Count > 0) + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + double price = DataRequest.GetDictionaryJsonObject(requestData, "price"); + User? user = SQLHelper?.GetUserById(userid, true); + if (user != null && user.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + if (!item.IsSellable) + { + msg = $"此物品无法出售{(item.NextSellableTime != DateTime.MinValue ? $",此物品将在 {item.NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售" : "")}。"; + } + else + { + SQLHelper?.AddMarketItem(itemGuid, userid, price); + if (SQLHelper?.Success ?? false) + { + msg = ""; + } + } + } + } + resultData.Add("msg", msg); + } + + /// + /// 下架市场物品 + /// + /// + /// + private void MarketDelist(Dictionary requestData, Dictionary resultData) + { + string msg = "无法下架市场物品,请稍后再试。"; + if (requestData.Count > 0) + { + long userid = DataRequest.GetDictionaryJsonObject(requestData, "userid"); + if (userid != 0) + { + SQLHelper?.DeleteMarketItemByUserId(userid); + } + else + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + if (itemGuid != Guid.Empty) SQLHelper?.DeleteMarketItem(itemGuid); + } + if (SQLHelper?.Success ?? false) + { + msg = ""; + } + } + resultData.Add("msg", msg); + } + + /// + /// 更新市场价格 + /// + /// + /// + private void UpdateMarketPrice(Dictionary requestData, Dictionary resultData) + { + string msg = "无法更新市场价格,请稍后再试。"; + if (requestData.Count > 0) + { + Guid itemGuid = DataRequest.GetDictionaryJsonObject(requestData, "itemGuid"); + double price = DataRequest.GetDictionaryJsonObject(requestData, "price"); + SQLHelper?.UpdateMarketItemPrice(itemGuid, price); + if (SQLHelper?.Success ?? false) + { + msg = ""; + } + } + resultData.Add("msg", msg); + } + + /// + /// 获取交易报价 + /// + /// + /// + private void GetOffer(Dictionary requestData, Dictionary resultData) + { + string msg = "无法获取报价,请稍后再试。"; + if (SQLHelper != null && requestData.Count >= 1) + { + long offerId = DataRequest.GetDictionaryJsonObject(requestData, "id"); + bool apiQuery = DataRequest.GetDictionaryJsonObject(requestData, "apiQuery"); + Offer? offer = SQLHelper.GetOffer(offerId); + if (offer != null) + { + // 检查当前用户是否有权限查看(报价创建者或接收者)允许管理员使用 API 查询报价 + long userId = Server.User.Id; + if ((apiQuery && Server.User.IsAdmin) || offer.Offeror == userId || offer.Offeree == userId) + { + resultData.Add("offer", offer); + msg = ""; + } + else + { + msg = "您无权查看此报价。"; + } + } + else + { + msg = "报价不存在。"; + } + } + resultData.Add("msg", msg); + } + + /// + /// 创建交易报价 + /// + /// + /// + private void MakeOffer(Dictionary requestData, Dictionary resultData) + { + string msg = "无法创建报价,请稍后再试。"; + if (SQLHelper != null && requestData.Count >= 1) + { + long offeree = DataRequest.GetDictionaryJsonObject(requestData, "offeree"); + long offeror = Server.User.Id; + if (offeror != 0 && offeree != 0 && offeror != offeree) + { + SQLHelper.AddOffer(offeror, offeree); + if (SQLHelper.Success) + { + long offerId = SQLHelper.LastInsertId; + Offer? offer = SQLHelper.GetOffer(offerId); + if (offer != null) + { + resultData.Add("offer", offer); + msg = ""; + } + } + } + else + { + msg = "无效的用户ID。"; + } + } + resultData.Add("msg", msg); + } + + /// + /// 修改交易报价 + /// + /// + /// + private void ReviseOffer(Dictionary requestData, Dictionary resultData) + { + string msg = "无法修改报价,请稍后再试。"; + if (SQLHelper != null && requestData.Count >= 3) + { + long offerId = DataRequest.GetDictionaryJsonObject(requestData, "id"); + OfferActionType action = DataRequest.GetDictionaryJsonObject(requestData, "action"); + List offerorItems = DataRequest.GetDictionaryJsonObject>(requestData, "offerorItems") ?? []; + List offereeItems = DataRequest.GetDictionaryJsonObject>(requestData, "offereeItems") ?? []; + long userId = Server.User.Id; + + Offer? offer = SQLHelper.GetOffer(offerId); + if (offer != null && (offer.Offeror == userId || offer.Offeree == userId)) + { + try + { + SQLHelper.NewTransaction(); + + bool isOfferor = offer.Offeror == userId; + bool canProceed = false; + + // 根据 action 处理状态 + switch (action) + { + case OfferActionType.OfferorRevise: + if (isOfferor && (offer.Status == OfferState.Created || offer.Status == OfferState.Negotiating)) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.PendingOfferorConfirmation); + canProceed = true; + } + else msg = "当前状态不允许发起方修改。"; + break; + + case OfferActionType.OfferorConfirm: + if (isOfferor && offer.Status == OfferState.PendingOfferorConfirmation) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.OfferorConfirmed); + canProceed = true; + } + else msg = "当前状态不允许发起方确认。"; + break; + + case OfferActionType.OfferorSend: + if (isOfferor && offer.Status == OfferState.OfferorConfirmed) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.Sent); + canProceed = true; + } + else msg = "当前状态不允许发起方发送。"; + break; + + case OfferActionType.OfferorCancel: + if (isOfferor && offer.Status != OfferState.Completed && offer.Status != OfferState.Rejected && offer.Status != OfferState.Cancelled && offer.Status != OfferState.Expired) + { + SQLHelper.DeleteOfferItemsBackupByOfferId(offerId); + SQLHelper.UpdateOfferStatus(offerId, OfferState.Cancelled); + canProceed = true; + } + else msg = "当前状态不允许发起方取消。"; + break; + + case OfferActionType.OfferorAccept: + if (isOfferor && offer.Status == OfferState.Negotiating) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.NegotiationAccepted); + canProceed = true; + } + else msg = "当前状态不允许发起方同意。"; + break; + + case OfferActionType.OffereeRevise: + // 接收方修改报价 + if (!isOfferor && offer.NegotiatedTimes >= 3) + { + msg = "当前协商次数已达上限(3次),不允许接收方修改。"; + } + else if (!isOfferor && (offer.Status == OfferState.Sent || offer.Status == OfferState.NegotiationAccepted)) + { + // 备份 + SQLHelper.BackupOfferItem(offer); + SQLHelper.UpdateOfferStatus(offerId, OfferState.PendingOffereeConfirmation); + canProceed = true; + } + else msg = "当前状态不允许接收方修改。"; + break; + + case OfferActionType.OffereeConfirm: + if (!isOfferor && offer.Status == OfferState.PendingOffereeConfirmation) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.OffereeConfirmed); + canProceed = true; + } + else msg = "当前状态不允许接收方确认。"; + break; + + case OfferActionType.OffereeSend: + if (!isOfferor && (offer.Status == OfferState.OffereeConfirmed)) + { + if (offer.NegotiatedTimes < 3) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.Negotiating); + SQLHelper.UpdateOfferNegotiatedTimes(offerId, offer.NegotiatedTimes + 1); + canProceed = true; + } + else msg = "当前协商次数已达上限(3次),不允许接收方发送。"; + } + else msg = "当前状态不允许接收方修改。"; + break; + + default: + msg = "无效的操作类型。"; + break; + } + + if (canProceed) + { + // 更新物品,同时对物品进行检查 + User? offeree = SQLHelper.GetUserById(offer.Offeree, true); + User? offeror = SQLHelper.GetUserById(offer.Offeror, true); + if (offeree != null && offeror != null) + { + SQLHelper.DeleteOfferItemsByOfferId(offerId); + foreach (Guid itemGuid in offerorItems) + { + if (offeror.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + if (!item.IsTradable) + { + msg = $"此物品无法交易{(item.NextTradableTime != DateTime.MinValue ? $",此物品将在 {item.NextTradableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可交易" : "")}。"; + break; + } + else + { + SQLHelper.AddOfferItem(offerId, offer.Offeror, item.Guid); + } + } + } + foreach (Guid itemGuid in offereeItems) + { + if (offeree.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + if (!item.IsTradable) + { + msg = $"此物品无法交易{(item.NextTradableTime != DateTime.MinValue ? $",此物品将在 {item.NextTradableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可交易" : "")}。"; + break; + } + else + { + SQLHelper.AddOfferItem(offerId, offer.Offeree, item.Guid); + } + } + } + + if (msg != "") + { + offer = SQLHelper.GetOffer(offerId); + if (offer != null) + { + SQLHelper.Commit(); + resultData.Add("offer", offer); + msg = ""; + } + } + } + } + + if (msg != "") + { + SQLHelper.Rollback(); + } + } + catch (Exception e) + { + SQLHelper.Rollback(); + ServerHelper.Error(e); + msg = "修改报价时发生错误,请稍后再试。"; + } + } + else + { + msg = "报价不存在或您无权修改。"; + } + } + resultData.Add("msg", msg); + } + + /// + /// 回应交易报价 + /// + /// + /// + private void RespondOffer(Dictionary requestData, Dictionary resultData) + { + string msg = "无法回应报价,请稍后再试。"; + if (SQLHelper != null && requestData.Count >= 2) // 只需要 offerId 和 action + { + long offerId = DataRequest.GetDictionaryJsonObject(requestData, "id"); + OfferActionType action = DataRequest.GetDictionaryJsonObject(requestData, "action"); + long userId = Server.User.Id; + + Offer? offer = SQLHelper.GetOffer(offerId); + if (offer != null && offer.Offeree == userId) + { + bool canProceed = false; + bool isNegotiating = false; + + try + { + SQLHelper.NewTransaction(); + + // 根据 action 处理状态 + switch (action) + { + case OfferActionType.OffereeAccept: + if (offer.Status == OfferState.Sent || offer.Status == OfferState.Negotiating || offer.Status == OfferState.NegotiationAccepted) + { + if (offer.Status == OfferState.Negotiating) + { + isNegotiating = true; + } + SQLHelper.UpdateOfferStatus(offerId, OfferState.Completed); + SQLHelper.UpdateOfferFinishTime(offerId, DateTime.Now); + canProceed = true; + } + else msg = "当前状态不允许接受。"; + break; + + case OfferActionType.OffereeReject: + if (offer.Status == OfferState.Sent || offer.Status == OfferState.Negotiating || offer.Status == OfferState.NegotiationAccepted) + { + SQLHelper.UpdateOfferStatus(offerId, OfferState.Rejected); + SQLHelper.UpdateOfferFinishTime(offerId, DateTime.Now); + canProceed = true; + } + else msg = "当前状态不允许拒绝。"; + break; + + default: + msg = "无效的操作类型。"; + break; + } + + if (canProceed) + { + offer = SQLHelper.GetOffer(offerId, isNegotiating); + if (offer != null) + { + if (offer.Status == OfferState.Completed) + { + User? offeree = SQLHelper.GetUserById(offer.Offeree, true); + User? offeror = SQLHelper.GetUserById(offer.Offeror, true); + if (offeree != null && offeror != null) + { + foreach (Guid itemGuid in offer.OffereeItems) + { + if (offeree.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + item.EntityState = EntityState.Deleted; + + Item newItem = item.Copy(); + newItem.User = offeror; + newItem.IsSellable = false; + newItem.IsTradable = false; + newItem.NextSellableTime = DateTimeUtility.GetTradableTime(); + newItem.NextTradableTime = DateTimeUtility.GetTradableTime(); + newItem.EntityState = EntityState.Added; + offeror.Inventory.Items.Add(newItem); + } + } + foreach (Guid itemGuid in offer.OfferorItems) + { + if (offeror.Inventory.Items.FirstOrDefault(i => i.Guid == itemGuid) is Item item) + { + item.EntityState = EntityState.Deleted; + + Item newItem = item.Copy(); + newItem.User = offeree; + newItem.IsSellable = false; + newItem.IsTradable = false; + newItem.NextSellableTime = DateTimeUtility.GetTradableTime(); + newItem.NextTradableTime = DateTimeUtility.GetTradableTime(); + newItem.EntityState = EntityState.Added; + offeree.Inventory.Items.Add(newItem); + } + } + SQLHelper.UpdateInventory(offeror.Inventory); + SQLHelper.UpdateInventory(offeree.Inventory); + SQLHelper.Commit(); + resultData.Add("offer", offer); + msg = ""; + } + } + } + } + + if (msg != "") + { + SQLHelper.Rollback(); + } + } + catch (Exception e) + { + SQLHelper.Rollback(); + ServerHelper.Error(e); + msg = "回应报价时发生错误,请稍后再试。"; + } + } + else + { + msg = "报价不存在或您无权回应。"; + } + } + resultData.Add("msg", msg); + } + + #endregion } } diff --git a/FunGame.Server/FunGame.Server.csproj b/FunGame.Server/FunGame.Server.csproj index d2b577e..1b225d9 100644 --- a/FunGame.Server/FunGame.Server.csproj +++ b/FunGame.Server/FunGame.Server.csproj @@ -5,8 +5,6 @@ net9.0 enable enable - logo.ico - logo.ico ..\bin FunGameServer Milimoe @@ -17,6 +15,7 @@ FunGameServer Milimoe.$(MSBuildProjectName.Replace(" ", "_")) app.manifest + logo.ico @@ -47,14 +46,8 @@ + - - - True - \ - - - diff --git a/FunGame.Server/Images/logo.ico b/FunGame.Server/Images/logo.ico deleted file mode 100644 index 4082874accbeb80cd336bcb14ff9004ad35920ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16958 zcmeI4S94=|dfvxh!q>jk$58nzxXR_StSVhd)@~`6v|B2dEUz{v?ey$U=$=l~fz4)< zY~~CCB#4}I&KY3Nc^?1%hl3gJlzin&d$vv=-~a^9o1gc2U+f1T{5$^lhky71|Ne&$ z{@_1;@WH?T;DZnTfP+gP{D8w;xBmOXyVL!DZQhc+slII9vTU10xdK(?>)Ud;Gb*?C zmCSXOOf=N1@2TJ3RW?&sw^>!MT~@8$RJ*&YL4RBQZkMB>onA|O-IjJcP3>^>n{74f z4V7w5RZ0~l<5?Xap6mAdTJ_qtQt53~%G=uCKhp93iN3tK(dprty4*uH+mgecRwh|i zDp{1r9aSn;P`%tywbbJCx+<4CDsg`G&ZQ@#UVT2|)264aU}m8H!L`!)t_FjBou6IG zAI&ROXe(1_$lcyU%j=Z9= znsx_$^_n%c8+El>E%iEGj8Nh6b&Q`kk7(?WSrCmr*UZ<`B z^6zx2yw15-vvkm`)#P->)u;|MHR;jz;21w@DHiK!cjr`xd&fFIztqjuwFaGCZMP2; zk2JO2J5@5>RI9P8VyLP`l|D}k$7DZA>nUFups$M3>6SKK89CNd z@_Gx(XPR1E2qGu8{R>&n)|2U)@-GFuqpGHsxH;s^8dGsocXZ8KvVDbz8(*dq>%H6~8H~TI{G* z>Bz@@r?GQ5+LFs%)USU#tz^pBq_4yMW9&top$pqqNj6VL^Y)Y`7o*w?bu=;+l*e0C z1mEBE6_qG76~IS=@rI6$&s8t=k(uxC6#1?lRdYQh5-pYUCFC^rCLgrQlfzOHDrg#VbB;y%a&vkPHmi+y>+bp`w-O-%Wh|3ks$Qj7U?#*e9>waHeZdXp7#AYF5o!TCH*t+1yI{mlSqR$hrK5X5M{D><93JhHB-$LLu&X zE~Wj0Q~du>x7hw<|4g0guC~?_3J{0;y*(Z7o}rfuZEmGiA#ZiCz2Bc!JXTi0ivK2g zu&?K%9(=e%ZtH1tGi$x}%a2AhAkM?+DweFv&);m2+cv`m@?9Q(*~PvWT1Q{+mm|oW zASWhebHx>k@p`(VT)v|IrW^7u+xwr9!ZZFTogwdzQdhj;Y! z^`b^bSIO6Pe7P!bqNaGUt7xvRY>oRMzui!4#uj37Mmw592b%%nVm71#HLi%C1%fq2 zkT+lG>Ixej9bYLxUOGO$lyf7YTr#X;DyR)7_ltcZ=-3x3Yh+?enQTXebX`s2^4;qV zD|QaZ!`WwXjHdC)%A)>cvPU%%4A z%!H1QPRQL?R!zS=KeuXG&D1IUrBR~Bt|fJVZ4UAAH1;e~OHYrebIl#?^^d5PyXd?l z$7&LtQCB+D>(g^xkQ1WRd;5Ca@?}4=Z|39=)wN6=Yf>+E_znm8 z`nQjl^s^7A^{^>!hoFW$KoN;K7IgK4m5f&6adUnIWmVnZ}5`E7thRLtn z>h$*%0t;lRVWm=AZQ|SEEGigj$>uC6Roc-2`Dd16`sAryKYcK*AN_htKYK8%w=;eX z_6~J(eFMg;Xcv1J|1W^+mbS{uHV#$n9?BK1YRi=(-*mOQ8qwlvl+UkHR}2Pg5eFAK zKD{FM?kX9nXw#9f>bAjc=6(;zXHD)S8m{2u7u2>3^m3yw(Y5*RJUYw~8)fXci=F$_ z_6|O_1@4IWi|1#tM;`M~!zzUiw%^5< z&#c(Ey}1VOHt>m>3aOe7uxGEnW9g&O+_P{_t8Hv_t;_Rk@_a=tu*k{5vE`HXT36fT zkUn|{hO?@-cGTl}f;#$cK1B`OBL^I-e{?~t?`xJ?^1}z?dj8I#3;gf;`qsh|RpRdY zOZ*l2N9Gb%{IB@ya)zr4a9_?yRueN`WXP*VzO3T2X=HMP{d)MT(cO0clG?Mau(30K zeT!a<4vgJ8*wv4YEA$RIe8wLtTK9c^c1=z#kpIB%=&nir&XdpW%VC|9Q?IWtt=xLa zXPi)D(qK=6^$vE9l_BR9v3ZZ0(8KS0{bQxTN!#Sl9eg=f*p`!A-P{A~@w`KiQ3Gd2 z$YWmegR$8F`?|nN=JEBH_~e&gYS~rM>sfMth@K!+wRACS59;|FyBuJJc%-aKzJ>mq z)BZ?&`<#$NOTDZZDQ;PZitz>Xzks}bWVeBzRvcYAY-eaRi3JT~f6=a0ZfhuG>| zheuZy#>>%<+3}wTkEXQgj_dO3l3aPA5SX(|{0HbeS8QO9U|I1vIw5avIn#V!R4XfB zi_Hkw)AT6Rh;l=9>X*U&S>kEHcX#O3^2BP3T$`mfROlWDj;)+$d^|{%tTg zvi$z`_D+5ec>Mhu*goE&XEbp{J#=LC$+KyV(o?3X;Q{)=SgfQ3_cb*iP(Hpf~(Z$OSL_3%q_ zK<7yDOplztOFl2lL$9#zYvTJ|@Gky;b!+Ls*gsA!Hg^ZHH=Uu_3)|?ef|3mx7ir`9Y zHor-4kMGvE&$Jb3>(gfrIDxWqkil$GJ=Q%kDUiYh+P1Q#}&JO)Sl@Xdvd zn3lKbfl~Cek;Yg4N6jMt=Xh@pTxfDb1WeP!pQF^1HE_lJdWQR;U*@~kiNCq|u+>*b zsWYFyG(ATSEY;GwJw^=g!42RO)KRB1XYnb%NKcvSu~s%K`sBG!0s5XQJ&ujn{l1Jm z1BFEYEPBkuLv%Phv6ycmrXp`9XTy5r>GbB3HO;#H8nl$RDyaC3ZxaqAJX*m zJurU*KP+I2Iyo-~zi>%Dj?wqoxUUsoLy^ReocQhq7~uBHTk_wniuhgxKT45XTGW{m zy}>5;6-{^K=U9SA@Zm!aM^rD~IF$n@dJ{dp2OAq3rNM~?BRMy+n&Uo4CPUznyps8S zO)bG2_&XLp@l#)>=E7QC2j@^n-i~k5SM=l#6s%gizUHUaka2Rfi z8Wko#)`<5exOJRdV8;(r^l3hCTt)g7(;t+L+<7=T^pHoM6M76Uxog>#SFC)*=fD$y zvyI(N?3;6v_h-Ol!AK4qf36ZaDo8GL;m>7yj|lZ-VLhtF`4AX8WAQ?Zjv{s=S8+cn zazKFEWa>*e0-rD$!6y!>A?Pqi|Ct=XyEHZWCV-#h!BONRcSU3GeAJgXb?Zz!$2V|I z+i+N6dY&TkmsG2?$%%$L+_q|`hul~~rW$^*ycW~QTPMBJ9VciwiWcw7BTEH7q=-#= zV85u^P!)eE2ZHG`qT;|`|v2jujKLJI5?z=fBC=)9k9Eba|QjE@Q)3Do}SK0 zZv`*IeS86jmx5O;!#l>P$;OTzZ(UDEf?9Mo$-TQ;an{rXBQ4v@dh&%=^K(V{;8)*_ zcs08eqYpXIF4*`0*|(2wHQ?Mo1V1zm=;bQZs*I9`tHY*#bX_7oE9B84dCg5eUWMnH zhxZ8js5|&goxEq{uag79#Lsr`fOzR^d@iJF7d%04lqEK9Z@!V;%KvalIciRV7}(g# zz>zlK$H@8QJ`)=`Fxumhd3-poEo7fxB_8RqW8{?rwbgJjA!2tc(A0w`euXmo)cKm~ zJ@j2V(6bj_J%1I@+zi-f91euOVGm4jaCEKx<6G@=-ZQcvTxoE4tsdvy6S(2tnLN~% z)wLjb7tBFC8xACA_%puSK~MSknVp;oFIlCh;x&WO>fCP_j-%hfmekf<{YVWs(NL1y zM$a|0`|vzxi_y0egR@R8+2cxP2jr(Ly>*}1ENg7qripnMF^(VEGsJitpC*^UDTE_M z3#<5p;Kuc`UW~{1zEdUe`%>o&zNw~1FXMXjY(pE~D)L?7>(|J4qg`bF`Z&4O9@lm_ z573Eu-vQ4L(Ze=))h0Y~ak5bk(` zJmMu5(x&d?V;)z6JZ$n)QPT_f1v)6y>3MU^32KL6=m2;(t8x=OfNz^P-!XEYf)|n1 z9OhabU)@3GJ+AMaeyM|V{Nn6J2WPiBHpl7BUF>b8$PE>X_gkm0n_rC3|I(x3`%&&A zkNhS07RP3sz7YT4KUW}4E~FPK5c@{vq1?v)=J7IEVsSmDceB(pu+rP95SXulU5onk z$rLgyX>4YTdP_{wPldqiY4VcO8Kd680nsnbE=BR5B%C5OxwMb3b*OtM`tYGm>l@4i zj&8J%td_lv%p4}(jr``EqGl(kdxywbmKka?fui022$e#EhRNZ(5QdFe?v9Q5VO zL5+?3kTqxJ{unkl+^DI+336l~*@Mhhveg|G!O2J9(4o)Y*TK?8SW}CB&8`F$23tOU zvtjieVX)rAmrLLr7qK6J!%2~k=xa?bqSmag2dHB?`0T1)jJoKv3wpN{=R14gXzE&G zTVr!MxxLIq_TlvpusgEuV&l8x#*%Z_=!I)WKL^!T!MOpv)yE$q=P%r57w>)WZP>HxNTD`sL`@-qIVsK9_deLgba6Q*w%RkhEIlHM{ zv$n8i*CKGFC3rx%dan7wU@s>f^a3IBaS{2;Rxcie<112wH{4go4smXL-8|n%{sSKG6C1|o&0%z~$I&Av zR>-A=y2+4dN8hwTKN-mOG_g{ES1y3R4#=r{s&|ep84rw2k-cwpfb7QCO}rbKjlGX9IEVpr zZ3p>V`!~w9ZWJwC%b&ag%O7ZRDNSx~=;Oy5ngo+BfQhHz$X4htbJ)fQUzy17Sr{d2 z`lRab_q_MzFCc#yc^2pmy{39 zY+_)K+;84f=I@f#ONF!7^s!fb$E7}gx(TPzgyVFPx8Z5v^XBl|<;@JU8){PyUf7Y+ zIhvs?(~r3O`sQtuVP^%H@wgET3!G1XKUQc2{i~VuW+Tt_MOk) z+27RvJD>kXZleS8{>5EB?C;;wKO8cnIm5RP@#lST#|3hp>BU4?Bkx0cHR^`{r@wV( znC%rUE@yf*W^uG9@DoY$ahLh`?N{|~DEshiba2B_ulJM$W0|~B--E*j18*e+gzGk=D}}2YKHF{%6tE8WH*PIJs!dr=HL;`-<09bD)4Dl zd|}E)FP_;`V~5@dUZuT*{*f`tykwT#^YG~k`Z-sIUe(97O=eZz1U=d!wZzU$GI6Di z(3Qrf+T;V~TXy(M69-@dQwN4J5B4rBJbZvp7@V+wLe8hpo_qN~kACG6rG+;^>W^HrWC{~^DN9@P(Cj+xyTdOSD$ z*cx%r!uAFDts2~|$Mo%!UcDL%ssztz<|lb@Zk&8$^TDZZ)HO4k*B6gR+%(;A-*Pnrc%irtAKD|o-SIp_9o)%& zql?Q6dRJyE*fd{0)F~Y0HGJd+^DQ%|{|3hyd{DYqSa+*?VaJBj%OGVoCkoZEFsefcG+- z=rwub25zW<{$|;UnSswqvS$!rCTn&hMrWhQPj6><0cKJm{B9l0Vdh2#Yv#c?3G}c; zo%{_mx4-$jkM!W@AL%E5{YU!2cmI=~|Mb7>i=X|4{^@)FpdbF?HM!fT2M;&l!#l`- zsdD{7D_dQ-w>JHxiGg$WnxgO>%z7Fp@RY|EH}mKV7ct(2i)4ofo+6gnW@c4Hk2}=D zy!_fn|Hg%NCqcn3d{g=hl(e`(B#A@pT7XU3B$oUsWYG9vq|z~ z7QQJ7{}p1sm8!rs;3q-)E)TiW$F)W5@c3Pb9@4LW{_&K4{I65`&fk2b@BPy&II=Wc zONF^iQNR3fp7|@>88{+xRjoW|-dmtnm^zXt|JBX@ zfa&+?9SoNnp^sc;K5KR-Ch@sdzB@x*@g zg?oj=9-T^q+f(4SxYnKEY%o=|bfkCFAuT!z3eYop(VZ&>zr}eL|DRl@f5gxINp`ma z?A)=_*4e($GWTF+G~=`624-hA=KTw{1odvm>UrYyUq&V~?~F088`is_4vtSwz;N`g zMdrZ7Lzn!OB5xaRu*SU6vX9$=hVPlo#BhllbrA6W!< zyG@RuZ(P_+&<97EBSn!nr+4HhceJf3eAWjpnp{ZZ_euD}1NNOxz&B0391pX9Y38mq zupYY{%mZfGX_>Xbt+3az#@^c67P~&+unhWs2liZH{_<+ljV;nzgnu`^UzELtHR9IH zBg`Dp^!$0a(P7>*@*8<;b#@X4)c8^f?02J1@7T%%H`EFA9KizaVWnz@9nX3u+2`P*-_j(;59QBr$(YL zYqrn1&wRPVoHWO7S%$vG%z{?ElbH3BW+&cSUCgxYzryFN?!^Z1t%qEvM;(AX% z`W`;z*_cNkKey|*57(G+#HjH>UMB`7f_nK5z6HD(4mYgb)feL~y<-mIz{jTHJk1Pt zin-?^{xpw$XV;U&q1iv{5;Jkj{x9FIGh^`MQ(o|&2i)C2$5FWDz9oy{Fu(DA@Xpx@ zvq5IoM=iD3Umey(YZm#H{N}OQlZdA(%qzFy``D8+JBaWANn+fNzm3hfk+rN%;$xYa z*z0Kz&+%7B5c~z_H|Mo%`0=wf_AqSv@beXY{DmDq_rk9vkSnUEaKW1|p}?ui+CT6Pw6wWcT4OK4P=ZoODBV z_At)R$?42NzP3Gbb=cQDJYr9*U17I~odx{i+p=5NxIWm~w{l>dykBNd$ME2WkM|M> zHh9RFlRnwuzgCg^#pDL_!x%RAavyG9b6GRThc9h<#!l5|&ux11o?aci_>$eWC3Z<4 zz1)I#WQGNw^6HHn8z<=LJjk9S_s5yR#=!FMGV2k1KWgoAIK1#Vygt3m?kf5incPBe zLB11yojJ!k+}{*@U6Z71 zYvxmBA36cY5oa&WMQwOH18)d-zKZ={yDW_$x_i%-eU)!0tCdp4OYCuGvy>Mup zx%pfCWQw`P+i9QW3p1-JYbG>8FF!|)on@D3jC}ca-be0>fQ5*A`pYH5F`%FKi^M(q zD)Y8HGsPsI9R~B0OUM_S>>cZVV)KD)bCmQ^aWCtY$hfo3A7U96s)RVN?{Qv_k8I1D=zn>Xy$WF5O2JmCOmzF&;%9#y@ZEm=TEZ8*-_BQuZsDR)2n+P#`&a7i-Q-gO> znUuQxKB0{t+}DeFJbd5qZ0Uu+F*-PApD%0nn3%s;nW5#_RmoP_DW<&Q-edXw_==x6j*f#$Gi=!MJvL5opKs^z1^oW?TR4muxschc zlOFl)d`t`YLkdh5q=)tMcNV6!=QKK=0DrRA!A`E5++k+-DfT1MytmywLw07MVA=aJ z8ad*rq?P{~cz z{p7(2^=_S7%v@sD3-0l1#t}mYX}J<@y{CRh@tbN=ICjWZ_KEFdc9&c1qutef zBg37HUxM!n=!f}GrO374?+5S8fB*4NKgaNBHTuW~_`B9;w~BomFLgN0EHlfzd;$FT z2|n=Yi$!{;1oetqfF4%C2BY)LM8KBIo;os+Lre6I>=@YS8~yygV--0^-Upex!ad=? zQQn&8N zoCOauD*(f+kh4AD*hTsWH+)!tJ|>W7_m1S%PjCQ_XW92FzIP}` zquoJA)V}Qja+nzSCf^NZ{#sx7{O>>h(%MI?8D0fH%W{7q{5VW(TK5dcxQri+b zoghb=y{icC39-ku;$gpp`;U+_%sHCxsmLD5Dm|NvU85=Rz!JSy23bSwi6zj{8a;HF zIkM^R1NcO|a)kYvH-I_km{%@S7dP3Jh@(q8{Xsm{vv{y;YtF^}-;>|y!0@4oY(YB* zW){4c4(DTZ;pN^$?GQ91=%@qGedR2V=U9x z2bgCh(9Im!&<6&aW*2P=v%Is#})pc{f|JJOdzYVV6VLuQ4r@`+I zdi}e($WYGTxA`}9!CW`;U$N(1W4AC1Z@CP2x5`Y+$voZ`q_!~+p8&HjIN+n$At)iA z$;o;6(`=o6-7I@X#C{mrjr=iu+T^x5dY*7@TQkTXAdXBA=q4{X*}Ix!XK{i3u>f@; zP5zz6|CjNTIcn?jR-9eE9FJ4XZsFkZIaB9`vKSo z&GY-8F?>9H{Q#b*%DrUJ!36tF^IP;h%nz4cX9$plTy~vKs7(L+{bGF_yPd9kaXXaDobStT>RE$by!zlo+sqk;at-A+b{@(+yk?#cubbWH zVv*e-dLtKmFK_0(^gkPVJqZSWZ`U(=f#>56WMem#9GAo{S>9`6#tWC^fm_{*H`zC@ zz<01~!Q6hFe#pFk4&Pa44&?wR&49aK&V=NEJ6LA6YIY8vjXKdU`$Ovibe;eYf*

DfcfT*o|4Dvp4n%GkuzQ$cS7`V*Lz#!qhkrl3e*gI?zezH`12y|UD`3{=$o_a_ zP7hzr=;>R2cQft8M*LP2|Ie_0;EBK)!p+&p&vxYAU_al%+0Jh920hd~HFKJIP7;KC|?j~{>;Uz&bz(E~ON;0u2GK!YLpO#->i%s7mVhW9xm7kPiFOabXlMC+oLe? zoT7I7m?fJTvWI?uhCRVGxB|l)thkZOi|p9*;Ts3BP}7U|K6?KkwZyI8y;vbOvKoOm zm|9D~d(a=Q_=vNx-qU|Mn0cJBQ*Y*+W(V8c?@)%J-1mL|{_=ge=Unr7#Q-fMK)rH8NQt^Owori(E{S!YMX)ULOSG5Uc3HjG%D zjg>RzT>9+wDzP2W2zGrSb5RB>stFF_-xtUQs8+TeU^5UJ1oiUC{hn|3)NBEpMF!NI$fBJHseY#ole~KE3 zUg)L2;P*yT^Fh5w{s7p^{3h%E{oTL!p{?&<`@eZ?zUS-j;_)^8RhZgmW+P>Kka>EV zcT;ZqZ+c^TZx7hM22Or68=%&M5y|(4EAS9Ai}bXQk@3kJ;siOLjDnpxKOeJ^`||qT z^Eo|#y9!6LXz^c9Upd&jSl2il%4=p$W)JO*T5sy{(09Kr+tBvI^ZnQFUmNCt`>zil ze=RR@Wv-drX}yN;o8RcHQMWwQo-V($*Z|L)+4$rlzYV6gOu+&B110tc1L&ek%{Q2^ zYVlf*Aiu9!48e)b>(i(2;Y;TA6dk-6TZ11=;^RR+o8Mj1+mG{m=~w)IaDqLLPnda` mU9*#~`kQaxpLLJe#+>iV`0eMz*Ub6;{};gjfBgTR2L3nSWUmeY diff --git a/FunGame.Server/Main.cs b/FunGame.Server/Main.cs index 665eb49..662dfbb 100644 --- a/FunGame.Server/Main.cs +++ b/FunGame.Server/Main.cs @@ -1,5 +1,4 @@ -using Milimoe.FunGame; -using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Library.Common.Network; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Server.Controller; @@ -23,6 +22,28 @@ Console.CancelKeyPress += (sender, e) => Environment.Exit(0); // 退出程序 }; +FunGameSystem.CloseListener += async () => +{ + if (SocketListener != null) + { + foreach (ServerModel model in SocketListener.ClientList.Cast>()) + { + await model.Kick("服务器正在关闭。"); + } + SocketListener.Close(); + SocketListener = null; + } + if (WebSocketListener != null) + { + foreach (ServerModel model in WebSocketListener.ClientList.Cast>()) + { + await model.Kick("服务器正在关闭。"); + } + WebSocketListener.Close(); + WebSocketListener = null; + } +}; + while (Running) { string order = Console.ReadLine() ?? ""; @@ -30,21 +51,23 @@ while (Running) if (order != "" && Running) { order = order.ToLower(); + if (FunGameSystem.OrderList.TryGetValue(order, out Action? action) && action != null) + { + action(order); + } switch (order) { case OrderDictionary.Quit: case OrderDictionary.Exit: case OrderDictionary.Close: - Running = false; CloseServer(); break; case OrderDictionary.Restart: if (SocketListener is null || WebSocketListener is null) { - ServerHelper.WriteLine("重启服务器"); StartServer(); } - else ServerHelper.WriteLine("服务器正在运行,拒绝重启!"); + else ServerHelper.WriteLine("服务器正在运行,请手动结束服务器进程再启动!"); break; default: if (SocketListener != null) @@ -60,9 +83,6 @@ while (Running) } } -ServerHelper.WriteLine("服务器已关闭,按任意键退出程序。"); -Console.ReadKey(); - void StartServer() { TaskUtility.NewTask(async () => @@ -105,8 +125,8 @@ void StartServer() ServerHelper.WriteLine("请输入 help 来获取帮助,按下 Ctrl+C 关闭服务器。"); - // 初始化用户密钥列表 - FunGameSystem.InitUserKeys(); + // 初始化服务器其他配置文件 + FunGameSystem.InitOtherConfig(); ServerHelper.PrintFunGameTitle(); @@ -246,38 +266,18 @@ void StartServer() } catch (Exception e) { - if (e.Message.Equals(new ServerErrorException().Message)) - { - if (SocketListener != null) - { - SocketListener.Close(); - SocketListener = null; - } - if (WebSocketListener != null) - { - WebSocketListener.Close(); - WebSocketListener = null; - } - } ServerHelper.Error(e); + CloseServer(); } finally { - if (SocketListener != null) - { - SocketListener.Close(); - SocketListener = null; - } - if (WebSocketListener != null) - { - WebSocketListener.Close(); - WebSocketListener = null; - } + CloseServer(); } }); } void CloseServer() { + Running = false; FunGameSystem.CloseServer(); } diff --git a/FunGame.Server/Models/ConsoleModel.cs b/FunGame.Server/Models/ConsoleModel.cs index 411510f..ab19d95 100644 --- a/FunGame.Server/Models/ConsoleModel.cs +++ b/FunGame.Server/Models/ConsoleModel.cs @@ -1,7 +1,13 @@ -using Milimoe.FunGame.Core.Interface.Base; +using System.Text; +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.Constant; using Milimoe.FunGame.Server.Others; using Milimoe.FunGame.Server.Services; +using ProjectRedbud.FunGame.SQLQueryExtension; namespace Milimoe.FunGame.Server.Model { @@ -45,9 +51,6 @@ namespace Milimoe.FunGame.Server.Model case OrderDictionary.ShowUsers2: ShowUsers(server); break; - case OrderDictionary.Help: - ShowHelp(); - break; default: break; } @@ -109,9 +112,90 @@ namespace Milimoe.FunGame.Server.Model } } - public static void ShowHelp() + public static void FirstRunRegAdmin() { - ServerHelper.WriteLine("Milimoe -> 帮助"); + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper() ?? throw new SQLServiceException(); + ServerHelper.WriteLine("首次启动需要注册管理员账号,请按提示输入信息!", InvokeMessageType.Core); + string username, password, email; + ServerHelper.Write("请输入管理员用户名:", InvokeMessageType.Core); + while (true) + { + username = Console.ReadLine() ?? ""; + int usernameLength = NetworkUtility.GetUserNameLength(username); + if (usernameLength < 3 || usernameLength > 12) + { + ServerHelper.WriteLine("账号名长度不符合要求:3~12个字符数(一个中文2个字符)", InvokeMessageType.Error); + } + else + { + break; + } + } + ServerHelper.Write("请输入管理员邮箱:", InvokeMessageType.Core); + while (true) + { + email = Console.ReadLine() ?? ""; + if (!NetworkUtility.IsEmail(email)) + { + ServerHelper.WriteLine("这不是一个邮箱地址!", InvokeMessageType.Error); + } + else + { + break; + } + } + ServerHelper.Write("请输入管理员密码:", InvokeMessageType.Core); + while (true) + { + StringBuilder passwordBuilder = new(); + ConsoleKeyInfo key; + + do + { + key = Console.ReadKey(true); + + if (key.Key == ConsoleKey.Enter) + { + Console.WriteLine(); + break; + } + else if (key.Key == ConsoleKey.Backspace) + { + if (passwordBuilder.Length > 0) + { + passwordBuilder.Remove(passwordBuilder.Length - 1, 1); + Console.Write("\b \b"); + } + } + else if (!char.IsControl(key.KeyChar)) + { + passwordBuilder.Append(key.KeyChar); + Console.Write("*"); + } + } while (true); + + password = passwordBuilder.ToString(); + + if (password.Length < 6 || password.Length > 15) + { + ServerHelper.WriteLine("密码长度不符合要求:6~15个字符数", InvokeMessageType.Error); + } + else + { + break; + } + } + (string msg, RegInvokeType type, bool success) = DataRequestService.RegisterUser(sql, username, password, email, "localhost"); + ServerHelper.WriteLine(msg, InvokeMessageType.Core); + if (success) + { + User? user = sql.GetUserByUsernameAndEmail(username, email); + if (user != null) + { + user.IsAdmin = true; + sql.UpdateUser(user); + } + } } } } diff --git a/FunGame.Server/Models/ServerModel.cs b/FunGame.Server/Models/ServerModel.cs index 83ffd66..8237150 100644 --- a/FunGame.Server/Models/ServerModel.cs +++ b/FunGame.Server/Models/ServerModel.cs @@ -11,6 +11,7 @@ using Milimoe.FunGame.Core.Library.SQLScript.Entity; using Milimoe.FunGame.Server.Controller; using Milimoe.FunGame.Server.Others; using Milimoe.FunGame.Server.Services; +using ProjectRedbud.FunGame.SQLQueryExtension; namespace Milimoe.FunGame.Server.Model { @@ -82,6 +83,8 @@ namespace Milimoe.FunGame.Server.Model NowGamingServer.CloseAnonymousServer(this); } NowGamingServer = null; + User.OnlineState = OnlineState.InRoom; + if (User.Id == InRoom.RoomMaster.Id) InRoom.RoomState = RoomState.Created; return true; } @@ -382,44 +385,47 @@ namespace Milimoe.FunGame.Server.Model public async Task QuitRoom(string roomid, bool isMaster) { bool result; + SQLHelper?.NewTransaction(); FunGameSystem.RoomList.CancelReady(roomid, User); FunGameSystem.RoomList.QuitRoom(roomid, User); Room Room = FunGameSystem.RoomList[roomid] ?? General.HallInstance; + User.OnlineState = OnlineState.Online; // 是否是房主 - if (isMaster) + if (isMaster && Room.Roomid != "-1") { List users = [.. FunGameSystem.RoomList[roomid].UserAndIsReady.Keys]; - if (users.Count > 0) // 如果此时房间还有人,更新房主 + User? newRoomMaster = null; + if (users.Count > 0) newRoomMaster = users[0]; + SQLHelper?.QuitRoomByRoomMaster(roomid, User.Id, newRoomMaster?.Id); + if (newRoomMaster != null) { - User NewMaster = users[0]; - Room.RoomMaster = NewMaster; - SQLHelper?.Execute(RoomQuery.Update_QuitRoom(SQLHelper, roomid, User.Id, NewMaster.Id)); - this.InRoom = General.HallInstance; - await UpdateRoomMaster(Room, true); - result = true; + Room.RoomMaster = users[0]; + ServerHelper.WriteLine("[ " + GetClientName() + " ] 退出了房间 " + roomid + ",并更新房主为:" + newRoomMaster); + await NotifyQuitRoom(Room, true); } - else // 没人了就解散房间 + else { FunGameSystem.RoomList.RemoveRoom(roomid); - SQLHelper?.Execute(RoomQuery.Delete_QuitRoom(SQLHelper, roomid, User.Id)); - this.InRoom = General.HallInstance; ServerHelper.WriteLine("[ " + GetClientName() + " ] 解散了房间 " + roomid); - result = true; } + InRoom = General.HallInstance; + result = true; } // 不是房主直接退出房间 else { this.InRoom = General.HallInstance; - await UpdateRoomMaster(Room); + await NotifyQuitRoom(Room); result = true; } + SQLHelper?.Commit(); + return result; } - public async Task UpdateRoomMaster(Room room, bool isUpdateRoomMaster = false) + public async Task NotifyQuitRoom(Room room, bool isUpdateRoomMaster = false) { foreach (IServerModel Client in Listener.ClientList.Where(c => c != null && c.User.Id != 0 && room.Roomid == c.InRoom?.Roomid)) { @@ -473,6 +479,11 @@ namespace Milimoe.FunGame.Server.Model { // 创建User对象 User = Factory.GetUser(_dsUser); + if (SQLHelper?.GetUserById(User.Id, true, true) is User real) + { + User = real; + } + User.OnlineState = OnlineState.Online; // 检查有没有重复登录的情况 await ForceLogOutDuplicateLogonUser(); // 添加至玩家列表 @@ -499,6 +510,7 @@ namespace Milimoe.FunGame.Server.Model { if (User.Id != 0 && this != null) { + User.OnlineState = OnlineState.Offline; _checkLoginKey = Guid.Empty; _logoutTime = DateTime.Now.Ticks; int TotalMinutes = Convert.ToInt32((new DateTime(_logoutTime) - new DateTime(_loginTime)).TotalMinutes); diff --git a/FunGame.Server/Services/DataRequestService.cs b/FunGame.Server/Services/DataRequestService.cs index b0a5890..2c9d2ad 100644 --- a/FunGame.Server/Services/DataRequestService.cs +++ b/FunGame.Server/Services/DataRequestService.cs @@ -6,14 +6,25 @@ using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.SQLScript.Common; using Milimoe.FunGame.Core.Library.SQLScript.Entity; using Milimoe.FunGame.Server.Others; +using ProjectRedbud.FunGame.SQLQueryExtension; namespace Milimoe.FunGame.Server.Services { public class DataRequestService { + /// + /// 获取插件取消请求的原因 + /// + /// + /// + /// + private static string GetPluginCancelString(DataRequestType type, GeneralEventArgs e) => $"{DataRequestSet.GetTypeString(type)} 请求已取消。{(e.EventMsg != "" ? $"原因:{e.EventMsg}" : "")}"; + + #region Register + public static (string Msg, RegInvokeType RegInvokeType, bool Success) Reg(object sender, string username, string password, string email, string verifyCode, string clientIP = "") { - string msg = ""; + string msg; RegInvokeType type = RegInvokeType.None; bool success = false; string clientName = ServerHelper.MakeClientName(clientIP); @@ -23,131 +34,21 @@ namespace Milimoe.FunGame.Server.Services FunGameSystem.WebAPIPluginLoader?.OnBeforeRegEvent(sender, eventArgs); if (eventArgs.Cancel) { - msg = $"{DataRequestSet.GetTypeString(DataRequestType.Reg_Reg)} 请求已取消。{(eventArgs.EventMsg != "" ? $"原因:{eventArgs.EventMsg}" : "")}"; + msg = GetPluginCancelString(DataRequestType.Reg_Reg, eventArgs); ServerHelper.WriteLine(msg, InvokeMessageType.DataRequest, LogLevel.Warning); return (eventArgs.EventMsg, RegInvokeType.None, false); } using SQLHelper? sqlHelper = Factory.OpenFactory.GetSQLHelper(); using MailSender? mailSender = Factory.OpenFactory.GetMailSender(); + if (sqlHelper != null) { - // 如果没发验证码,就生成验证码 - if (verifyCode.Trim() == "") - { - // 先检查账号是否重复 - sqlHelper.ExecuteDataSet(UserQuery.Select_IsExistUsername(sqlHelper, username)); - if (sqlHelper.Result == SQLResult.Success) - { - ServerHelper.WriteLine(clientName + " 账号已被注册"); - msg = "此账号名已被使用!"; - type = RegInvokeType.DuplicateUserName; - } - else - { - // 检查邮箱是否重复 - sqlHelper.ExecuteDataSet(UserQuery.Select_IsExistEmail(sqlHelper, email)); - if (sqlHelper.Result == SQLResult.Success) - { - ServerHelper.WriteLine(clientName + " 邮箱已被注册"); - msg = "此邮箱已被注册!"; - type = RegInvokeType.DuplicateEmail; - } - else - { - // 检查验证码是否发送过 - sqlHelper.ExecuteDataSet(RegVerifyCodes.Select_HasSentRegVerifyCode(sqlHelper, username, email)); - if (sqlHelper.Result == SQLResult.Success && DateTime.TryParse(sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegTime].ToString(), out DateTime RegTime) && (DateTime.Now - RegTime).TotalMinutes < 10) - { - string RegVerifyCode = sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegVerifyCode].ToString() ?? ""; - ServerHelper.WriteLine(clientName + $" 十分钟内已向{email}发送过验证码:{RegVerifyCode}"); - type = RegInvokeType.InputVerifyCode; - } - else - { - // 发送验证码,需要先删除之前过期的验证码 - sqlHelper.NewTransaction(); - sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); - string regVerify = Verification.CreateVerifyCode(VerifyCodeType.NumberVerifyCode, 6); - sqlHelper.Execute(RegVerifyCodes.Insert_RegVerifyCode(sqlHelper, username, email, regVerify)); - if (sqlHelper.Result == SQLResult.Success) - { - sqlHelper.Commit(); - if (mailSender != null) - { - // 发送验证码 - string ServerName = Config.ServerName; - string Subject = $"[{ServerName}] FunGame 注册验证码"; - string Body = $"亲爱的 {username},
感谢您注册[{ServerName}],您的验证码是 {regVerify} ,10分钟内有效,请及时输入!

{ServerName}
{DateTimeUtility.GetDateTimeToString(TimeType.LongDateOnly)}"; - string[] To = [email]; - if (mailSender.Send(mailSender.CreateMail(Subject, Body, System.Net.Mail.MailPriority.Normal, true, To)) == MailSendResult.Success) - { - ServerHelper.WriteLine(clientName + $" 已向{email}发送验证码:{regVerify}"); - } - else - { - ServerHelper.WriteLine(clientName + " 无法发送验证码", InvokeMessageType.Error); - ServerHelper.WriteLine(mailSender.ErrorMsg, InvokeMessageType.Error); - } - } - else // 不使用MailSender的情况 - { - ServerHelper.WriteLine(clientName + $" 验证码为:{regVerify},请服务器管理员告知此用户"); - } - type = RegInvokeType.InputVerifyCode; - } - else sqlHelper.Rollback(); - } - } - } - } - else - { - // 先检查验证码 - sqlHelper.ExecuteDataSet(RegVerifyCodes.Select_RegVerifyCode(sqlHelper, username, email, verifyCode)); - if (sqlHelper.Result == SQLResult.Success) - { - if (!DateTime.TryParse(sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegTime].ToString(), out DateTime RegTime)) - { - RegTime = General.DefaultTime; - } - // 检查验证码是否过期 - if ((DateTime.Now - RegTime).TotalMinutes >= 10) - { - ServerHelper.WriteLine(clientName + " 验证码已过期"); - msg = "此验证码已过期,请重新注册。"; - sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); - type = RegInvokeType.None; - } - else - { - // 注册 - if (verifyCode.Equals(sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegVerifyCode])) - { - sqlHelper.NewTransaction(); - ServerHelper.WriteLine("[Reg] Username: " + username + " Email: " + email); - FunGameSystem.UpdateUserKey(username); - password = password.Encrypt(FunGameSystem.GetUserKey(username)); - sqlHelper.Execute(UserQuery.Insert_Register(sqlHelper, username, password, email, clientIP)); - if (sqlHelper.Result == SQLResult.Success) - { - success = true; - msg = "注册成功!请牢记您的账号与密码!"; - sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); - sqlHelper.Commit(); - } - else - { - sqlHelper.Rollback(); - msg = "服务器无法处理您的注册,注册失败!"; - } - } - else msg = "验证码不正确,请重新输入!"; - } - } - else if (sqlHelper.Result == SQLResult.NotFound) msg = "验证码不正确,请重新输入!"; - else msg = "服务器无法处理您的注册,注册失败!"; - } + (msg, type, success) = ProcessRegistration(sqlHelper, mailSender, username, password, email, verifyCode, clientIP, clientName); + } + else + { + msg = "服务器无法处理您的注册,注册失败!"; } eventArgs.Success = success; @@ -157,11 +58,152 @@ namespace Milimoe.FunGame.Server.Services return (msg, type, success); } + internal static (string Msg, RegInvokeType Type, bool Success) ProcessRegistration(SQLHelper sqlHelper, MailSender? mailSender, string username, string password, string email, string verifyCode, string clientIP, string clientName) + { + if (string.IsNullOrWhiteSpace(verifyCode)) + { + return HandleNoVerifyCode(sqlHelper, mailSender, username, email, clientName); + } + else + { + return HandleWithVerifyCode(sqlHelper, username, password, email, verifyCode, clientIP, clientName); + } + } + + internal static (string Msg, RegInvokeType Type, bool Success) HandleNoVerifyCode(SQLHelper sqlHelper, MailSender? mailSender, string username, string email, string clientName) + { + // 先检查账号是否重复 + sqlHelper.ExecuteDataSet(UserQuery.Select_IsExistUsername(sqlHelper, username)); + if (sqlHelper.Result == SQLResult.Success) + { + ServerHelper.WriteLine(clientName + " 账号已被注册"); + return ("此账号名已被使用!", RegInvokeType.DuplicateUserName, false); + } + + // 检查邮箱是否重复 + sqlHelper.ExecuteDataSet(UserQuery.Select_IsExistEmail(sqlHelper, email)); + if (sqlHelper.Result == SQLResult.Success) + { + ServerHelper.WriteLine(clientName + " 邮箱已被注册"); + return ("此邮箱已被注册!", RegInvokeType.DuplicateEmail, false); + } + + // 检查验证码是否发送过 + sqlHelper.ExecuteDataSet(RegVerifyCodes.Select_HasSentRegVerifyCode(sqlHelper, username, email)); + if (sqlHelper.Result == SQLResult.Success && DateTime.TryParse(sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegTime].ToString(), out DateTime RegTime) && (DateTime.Now - RegTime).TotalMinutes < 10) + { + string RegVerifyCode = sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegVerifyCode].ToString() ?? ""; + ServerHelper.WriteLine(clientName + $" 十分钟内已向{email}发送过验证码:{RegVerifyCode}"); + return ("", RegInvokeType.InputVerifyCode, false); + } + + // 发送验证码 + return SendVerificationCode(sqlHelper, mailSender, username, email, clientName); + } + + internal static (string Msg, RegInvokeType Type, bool Success) SendVerificationCode(SQLHelper sqlHelper, MailSender? mailSender, string username, string email, string clientName) + { + sqlHelper.NewTransaction(); + sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); + string regVerify = Verification.CreateVerifyCode(VerifyCodeType.NumberVerifyCode, 6); + sqlHelper.Execute(RegVerifyCodes.Insert_RegVerifyCode(sqlHelper, username, email, regVerify)); + + if (sqlHelper.Result == SQLResult.Success) + { + sqlHelper.Commit(); + + if (mailSender != null) + { + // 发送验证码 + string ServerName = Config.ServerName; + string Subject = $"[{ServerName}] 注册验证码"; + string Body = $"亲爱的 {username},
感谢您注册 [{ServerName}],您的验证码是 {regVerify} ,10分钟内有效,请及时输入!

{ServerName}
{DateTimeUtility.GetDateTimeToString(TimeType.LongDateOnly)}"; + string[] To = [email]; + if (mailSender.Send(mailSender.CreateMail(Subject, Body, System.Net.Mail.MailPriority.Normal, true, To)) == MailSendResult.Success) + { + ServerHelper.WriteLine(clientName + $" 已向{email}发送验证码:{regVerify}"); + } + else + { + ServerHelper.WriteLine(clientName + " 无法发送验证码", InvokeMessageType.Error); + ServerHelper.WriteLine(mailSender.ErrorMsg, InvokeMessageType.Error); + } + } + else // 不使用MailSender的情况 + { + ServerHelper.WriteLine(clientName + $" 验证码为:{regVerify},请服务器管理员告知此用户"); + } + return ("", RegInvokeType.InputVerifyCode, false); + } + else + { + sqlHelper.Rollback(); + return ("发送验证码失败!", RegInvokeType.None, false); + } + } + + internal static (string Msg, RegInvokeType Type, bool Success) HandleWithVerifyCode(SQLHelper sqlHelper, string username, string password, string email, string verifyCode, string clientIP, string clientName) + { + // 先检查验证码 + sqlHelper.ExecuteDataSet(RegVerifyCodes.Select_RegVerifyCode(sqlHelper, username, email, verifyCode)); + if (sqlHelper.Result == SQLResult.Success) + { + if (!DateTime.TryParse(sqlHelper.DataSet.Tables[0].Rows[0][RegVerifyCodes.Column_RegTime].ToString(), out DateTime RegTime)) + { + RegTime = General.DefaultTime; + } + + // 检查验证码是否过期 + if ((DateTime.Now - RegTime).TotalMinutes >= 10) + { + ServerHelper.WriteLine(clientName + " 验证码已过期"); + sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); + return ("此验证码已过期,请重新注册。", RegInvokeType.None, false); + } + + // 注册 + return RegisterUser(sqlHelper, username, password, email, clientIP); + } + else if (sqlHelper.Result == SQLResult.NotFound) + { + return ("验证码不正确,请重新输入!", RegInvokeType.None, false); + } + else + { + return ("服务器无法处理您的注册,注册失败!", RegInvokeType.None, false); + } + } + + internal static (string Msg, RegInvokeType Type, bool Success) RegisterUser(SQLHelper sqlHelper, string username, string password, string email, string clientIP) + { + sqlHelper.NewTransaction(); + ServerHelper.WriteLine("[Reg] Username: " + username + " Email: " + email); + FunGameSystem.UpdateUserKey(username); + password = password.Encrypt(FunGameSystem.GetUserKey(username)); + sqlHelper.RegisterUser(username, password, email, clientIP); + + if (sqlHelper.Result == SQLResult.Success) + { + sqlHelper.Execute(RegVerifyCodes.Delete_RegVerifyCode(sqlHelper, username, email)); + sqlHelper.Commit(); + return ("注册成功!请牢记您的账号与密码!", RegInvokeType.None, true); + } + else + { + sqlHelper.Rollback(); + return ("服务器无法处理您的注册,注册失败!", RegInvokeType.None, false); + } + } + + #endregion + + #region Login + public static (bool Success, DataSet DataSet, string Msg, Guid Key) PreLogin(object sender, string username, string password, string autokey = "") { bool success = false; DataSet dsUser = new(); - string msg = "用户名或密码不正确。"; + string msg; Guid key = Guid.Empty; LoginEventArgs eventArgs = new(username, password, autokey); @@ -169,43 +211,12 @@ namespace Milimoe.FunGame.Server.Services FunGameSystem.WebAPIPluginLoader?.OnBeforeLoginEvent(sender, eventArgs); if (eventArgs.Cancel) { - msg = $"{DataRequestSet.GetTypeString(DataRequestType.Login_Login)} 请求已取消。{(eventArgs.EventMsg != "" ? $"原因:{eventArgs.EventMsg}" : "")}"; + msg = GetPluginCancelString(DataRequestType.Login_Login, eventArgs); ServerHelper.WriteLine(msg, InvokeMessageType.DataRequest, LogLevel.Warning); return (success, dsUser, eventArgs.EventMsg, key); } - // 验证登录 - if (username != "" && password != "") - { - password = password.Encrypt(FunGameSystem.GetUserKey(username)); - ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] Username: " + username); - using SQLHelper? sqlHelper = Factory.OpenFactory.GetSQLHelper(); - if (sqlHelper != null) - { - sqlHelper.ExecuteDataSet(UserQuery.Select_Users_LoginQuery(sqlHelper, username, password)); - if (sqlHelper.Result == SQLResult.Success) - { - dsUser = sqlHelper.DataSet; - key = Guid.NewGuid(); - success = true; - msg = ""; - if (autokey.Trim() != "") - { - sqlHelper.ExecuteDataSet(UserQuery.Select_CheckAutoKey(sqlHelper, username, autokey)); - if (sqlHelper.Result == SQLResult.Success) - { - ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] AutoKey: 已确认"); - } - else - { - success = false; - msg = "AutoKey 不正确,拒绝自动登录!"; - ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] " + msg); - } - } - } - } - } + (success, dsUser, msg, key) = ProcessLogin(username, password, autokey); eventArgs.Success = success; FunGameSystem.ServerPluginLoader?.OnAfterLoginEvent(sender, eventArgs); @@ -214,5 +225,72 @@ namespace Milimoe.FunGame.Server.Services ServerHelper.WriteLine(msg, InvokeMessageType.Core); return (success, dsUser, msg, key); } + + internal static (bool Success, DataSet DataSet, string Msg, Guid Key) ProcessLogin(string username, string password, string autokey) + { + bool success = false; + DataSet dsUser = new(); + string msg = "用户名或密码不正确。"; + Guid key = Guid.Empty; + + if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) + { + password = password.Encrypt(FunGameSystem.GetUserKey(username)); + ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] Username: " + username); + + using SQLHelper? sqlHelper = Factory.OpenFactory.GetSQLHelper(); + if (sqlHelper != null) + { + (success, dsUser, msg, key) = ValidateLogin(sqlHelper, username, password, autokey); + } + else + { + msg = "无法连接到数据库,登录失败!"; + } + } + + return (success, dsUser, msg, key); + } + + internal static (bool Success, DataSet DataSet, string Msg, Guid Key) ValidateLogin(SQLHelper sqlHelper, string username, string password, string autokey) + { + bool success = false; + DataSet dsUser = new(); + string msg = "用户名或密码不正确。"; + Guid key = Guid.NewGuid(); + + sqlHelper.ExecuteDataSet(UserQuery.Select_Users_LoginQuery(sqlHelper, username, password)); + if (sqlHelper.Result == SQLResult.Success) + { + dsUser = sqlHelper.DataSet; + success = true; + msg = ""; + + if (!string.IsNullOrWhiteSpace(autokey)) + { + (success, msg) = CheckAutoKey(sqlHelper, username, autokey); + } + } + + return (success, dsUser, msg, key); + } + + internal static (bool Success, string Msg) CheckAutoKey(SQLHelper sqlHelper, string username, string autokey) + { + sqlHelper.ExecuteDataSet(UserQuery.Select_CheckAutoKey(sqlHelper, username, autokey)); + if (sqlHelper.Result == SQLResult.Success) + { + ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] AutoKey: 已确认"); + return (true, ""); + } + else + { + string msg = "AutoKey 不正确,拒绝自动登录!"; + ServerHelper.WriteLine("[" + DataRequestSet.GetTypeString(DataRequestType.Login_Login) + "] " + msg); + return (false, msg); + } + } + + #endregion } } diff --git a/FunGame.Server/Services/DataUtility/MySQLHelper.cs b/FunGame.Server/Services/DataUtility/MySQLHelper.cs index 54efb97..773bd62 100644 --- a/FunGame.Server/Services/DataUtility/MySQLHelper.cs +++ b/FunGame.Server/Services/DataUtility/MySQLHelper.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Model; @@ -9,14 +10,59 @@ namespace Milimoe.FunGame.Server.Services.DataUtility { public class MySQLHelper : SQLHelper { + /// + /// FunGame 类型 + /// public override FunGameInfo.FunGame FunGameType { get; } = FunGameInfo.FunGame.FunGame_Server; + + /// + /// 使用的数据库类型 + /// public override SQLMode Mode { get; } = SQLMode.MySQL; + + /// + /// SQL 脚本 + /// public override string Script { get; set; } = ""; + + /// + /// 命令类型 + /// public override CommandType CommandType { get; set; } = CommandType.Text; + + /// + /// 数据库事务 + /// + public override DbTransaction? Transaction => _transaction; + + /// + /// 执行结果 + /// public override SQLResult Result => _result; + + /// + /// SQL 服务器信息 + /// public override SQLServerInfo ServerInfo => _serverInfo ?? SQLServerInfo.Create(); - public override int UpdateRows => _updateRows; + + /// + /// 上一次执行命令影响的行数 + /// + public override int AffectedRows => _affectedRows; + + /// + /// 上一次执行的命令是 Insert 时,返回的自增 ID,大于 0 有效 + /// + public override long LastInsertId => _lastInsertId; + + /// + /// 上一次执行命令的查询结果集 + /// public override DataSet DataSet => _dataSet; + + /// + /// SQL 语句参数 + /// public override Dictionary Parameters { get; } = []; private readonly string _connectionString = ""; @@ -25,7 +71,8 @@ namespace Milimoe.FunGame.Server.Services.DataUtility private DataSet _dataSet = new(); private SQLResult _result = SQLResult.NotFound; private readonly SQLServerInfo? _serverInfo; - private int _updateRows = 0; + private int _affectedRows = 0; + private long _lastInsertId = 0; public MySQLHelper(string script = "", CommandType type = CommandType.Text) { @@ -69,7 +116,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 执行一个命令 + /// 执行现有命令() /// /// public override int Execute() @@ -104,14 +151,24 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } if (_transaction != null) command.Transaction = _transaction; - _updateRows = command.ExecuteNonQuery(); - _result = SQLResult.Success; + ReSet(); + _affectedRows = command.ExecuteNonQuery(); + if (_affectedRows > 0) + { + _result = SQLResult.Success; + if (script.Contains(Core.Library.SQLScript.Constant.Command_Insert, StringComparison.OrdinalIgnoreCase)) + { + _lastInsertId = command.LastInsertedId; + if (_lastInsertId < 0) _lastInsertId = 0; + } + } + else _result = SQLResult.Fail; if (localTransaction) Commit(); } catch (Exception e) { if (localTransaction) Rollback(); - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -119,11 +176,75 @@ namespace Milimoe.FunGame.Server.Services.DataUtility if (localTransaction) Close(); if (ClearParametersAfterExecute) Parameters.Clear(); } - return UpdateRows; + return AffectedRows; } /// - /// 查询DataSet + /// 异步执行现有命令() + /// + /// + public override async Task ExecuteAsync() + { + return await ExecuteAsync(Script); + } + + /// + /// 异步执行一个指定的命令 + /// + /// + /// + public override async Task ExecuteAsync(string script) + { + bool localTransaction = _transaction == null; + + try + { + if (localTransaction) + { + NewTransaction(); + } + + OpenConnection(); + Script = script; + ServerHelper.WriteLine("SQLQuery -> " + script, InvokeMessageType.Api); + using MySqlCommand command = new(script, _connection); + command.CommandType = CommandType; + foreach (KeyValuePair param in Parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value); + } + if (_transaction != null) command.Transaction = _transaction; + + ReSet(); + _affectedRows = await command.ExecuteNonQueryAsync(); + if (_affectedRows > 0) + { + _result = SQLResult.Success; + if (script.Contains(Core.Library.SQLScript.Constant.Command_Insert, StringComparison.OrdinalIgnoreCase)) + { + _lastInsertId = command.LastInsertedId; + if (_lastInsertId < 0) _lastInsertId = 0; + } + } + else _result = SQLResult.Fail; + if (localTransaction) Commit(); + } + catch (Exception e) + { + if (localTransaction) Rollback(); + _result = SQLResult.SQLError; + ServerHelper.Error(e); + } + finally + { + if (localTransaction) Close(); + if (ClearParametersAfterExecute) Parameters.Clear(); + } + return AffectedRows; + } + + /// + /// 执行现有命令()查询 DataSet /// /// public override DataSet ExecuteDataSet() @@ -132,7 +253,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 执行指定的命令查询DataSet + /// 执行指定的命令查询 DataSet /// /// /// @@ -161,12 +282,13 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } if (_transaction != null) command.Transaction = _transaction; + ReSet(); MySqlDataAdapter adapter = new() { SelectCommand = command }; _dataSet = new(); - adapter.Fill(_dataSet); + _affectedRows = adapter.Fill(_dataSet); if (localTransaction) Commit(); @@ -175,7 +297,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility catch (Exception e) { if (localTransaction) Rollback(); - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -187,26 +309,68 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 检查数据库是否存在 + /// 异步执行现有命令()查询 DataSet /// /// - public override bool DatabaseExists() + public override async Task ExecuteDataSetAsync() { + return await ExecuteDataSetAsync(Script); + } + + /// + /// 异步执行指定的命令查询 DataSet + /// + /// + /// + public override async Task ExecuteDataSetAsync(string script) + { + bool localTransaction = _transaction == null; + try { - ExecuteDataSet(Core.Library.SQLScript.Common.Configs.Select_GetConfig(this, "Initialization")); - return Success; + if (localTransaction) + { + NewTransaction(); + } + + OpenConnection(); + Script = script; + ServerHelper.WriteLine("SQLQuery -> " + script, InvokeMessageType.Api); + + using MySqlCommand command = new(script, _connection) + { + CommandType = CommandType + }; + foreach (KeyValuePair param in Parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value); + } + if (_transaction != null) command.Transaction = _transaction; + + ReSet(); + MySqlDataAdapter adapter = new() + { + SelectCommand = command + }; + _dataSet = new(); + _affectedRows = await adapter.FillAsync(_dataSet); + + if (localTransaction) Commit(); + + _result = _dataSet.Tables.Cast().Any(table => table.Rows.Count > 0) ? SQLResult.Success : SQLResult.NotFound; } catch (Exception e) { + if (localTransaction) Rollback(); + _result = SQLResult.SQLError; ServerHelper.Error(e); - _result = SQLResult.Fail; - return false; } finally { - Close(); + if (localTransaction) Close(); + if (ClearParametersAfterExecute) Parameters.Clear(); } + return _dataSet; } /// @@ -233,7 +397,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } catch (Exception e) { - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -254,7 +418,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } catch (Exception e) { - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -263,12 +427,35 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } } + /// + /// 检查数据库是否存在 + /// + /// + public override bool DatabaseExists() + { + try + { + ExecuteDataSet(Core.Library.SQLScript.Common.Configs.Select_GetConfig(this, "Initialization")); + return Success; + } + catch (Exception e) + { + ServerHelper.Error(e); + _result = SQLResult.SQLError; + return false; + } + finally + { + Close(); + } + } + private bool _isDisposed = false; /// /// 资源清理 /// - public void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { @@ -289,5 +476,13 @@ namespace Milimoe.FunGame.Server.Services.DataUtility Dispose(true); GC.SuppressFinalize(this); } + + private void ReSet() + { + _result = SQLResult.NotFound; + _affectedRows = 0; + _lastInsertId = 0; + DataSet.Clear(); + } } } diff --git a/FunGame.Server/Services/DataUtility/SQLiteHelper.cs b/FunGame.Server/Services/DataUtility/SQLiteHelper.cs index 57cce4f..510d396 100644 --- a/FunGame.Server/Services/DataUtility/SQLiteHelper.cs +++ b/FunGame.Server/Services/DataUtility/SQLiteHelper.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using Microsoft.Data.Sqlite; using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Library.Constant; @@ -9,14 +10,59 @@ namespace Milimoe.FunGame.Server.Services.DataUtility { public class SQLiteHelper : SQLHelper { + /// + /// FunGame 类型 + /// public override FunGameInfo.FunGame FunGameType { get; } = FunGameInfo.FunGame.FunGame_Server; + + /// + /// 使用的数据库类型 + /// public override SQLMode Mode { get; } = SQLMode.SQLite; + + /// + /// SQL 脚本 + /// public override string Script { get; set; } = ""; + + /// + /// 命令类型 + /// public override CommandType CommandType { get; set; } = CommandType.Text; + + /// + /// 数据库事务 + /// + public override DbTransaction? Transaction => _transaction; + + /// + /// 执行结果 + /// public override SQLResult Result => _result; + + /// + /// SQL 服务器信息 + /// public override SQLServerInfo ServerInfo => _serverInfo ?? SQLServerInfo.Create(); - public override int UpdateRows => _updateRows; + + /// + /// 上一次执行命令影响的行数 + /// + public override int AffectedRows => _affectedRows; + + /// + /// 上一次执行的命令是 Insert 时,返回的自增 ID,大于 0 有效 + /// + public override long LastInsertId => _lastInsertId; + + /// + /// 上一次执行命令的查询结果集 + /// public override DataSet DataSet => _dataSet; + + /// + /// SQL 语句参数 + /// public override Dictionary Parameters { get; } = []; private readonly string _connectionString = ""; @@ -25,7 +71,8 @@ namespace Milimoe.FunGame.Server.Services.DataUtility private DataSet _dataSet = new(); private SQLResult _result = SQLResult.NotFound; private readonly SQLServerInfo? _serverInfo; - private int _updateRows = 0; + private int _affectedRows = 0; + private long _lastInsertId = 0; public SQLiteHelper(string script = "", CommandType type = CommandType.Text) { @@ -67,7 +114,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 执行一个命令 + /// 执行现有命令() /// /// public override int Execute() @@ -102,14 +149,26 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } if (_transaction != null) command.Transaction = _transaction; - _updateRows = command.ExecuteNonQuery(); - _result = SQLResult.Success; + ReSet(); + _affectedRows = command.ExecuteNonQuery(); + if (_affectedRows > 0) + { + _result = SQLResult.Success; + if (script.Contains(Core.Library.SQLScript.Constant.Command_Insert, StringComparison.OrdinalIgnoreCase)) + { + using SqliteCommand idCommand = new("SELECT last_insert_rowid()", _connection); + if (_transaction != null) idCommand.Transaction = _transaction; + _lastInsertId = (long?)idCommand.ExecuteScalar() ?? 0; + if (_lastInsertId < 0) _lastInsertId = 0; + } + } + else _result = SQLResult.Fail; if (localTransaction) Commit(); } catch (Exception e) { if (localTransaction) Rollback(); - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -117,11 +176,77 @@ namespace Milimoe.FunGame.Server.Services.DataUtility if (localTransaction) Close(); if (ClearParametersAfterExecute) Parameters.Clear(); } - return UpdateRows; + return AffectedRows; } /// - /// 查询DataSet + /// 异步执行现有命令() + /// + /// + public override async Task ExecuteAsync() + { + return await ExecuteAsync(Script); + } + + /// + /// 异步执行一个指定的命令 + /// + /// + /// + public override async Task ExecuteAsync(string script) + { + bool localTransaction = _transaction == null; + + try + { + if (localTransaction) + { + NewTransaction(); + } + + OpenConnection(); + Script = script; + ServerHelper.WriteLine("SQLQuery -> " + script, InvokeMessageType.Api); + using SqliteCommand command = new(script, _connection); + command.CommandType = CommandType; + foreach (KeyValuePair param in Parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value); + } + if (_transaction != null) command.Transaction = _transaction; + + ReSet(); + _affectedRows = await command.ExecuteNonQueryAsync(); + if (_affectedRows > 0) + { + _result = SQLResult.Success; + if (script.Contains(Core.Library.SQLScript.Constant.Command_Insert, StringComparison.OrdinalIgnoreCase)) + { + using SqliteCommand idCommand = new("SELECT last_insert_rowid()", _connection); + if (_transaction != null) idCommand.Transaction = _transaction; + _lastInsertId = (long?)await idCommand.ExecuteScalarAsync() ?? 0; + if (_lastInsertId < 0) _lastInsertId = 0; + } + } + else _result = SQLResult.Fail; + if (localTransaction) Commit(); + } + catch (Exception e) + { + if (localTransaction) Rollback(); + _result = SQLResult.SQLError; + ServerHelper.Error(e); + } + finally + { + if (localTransaction) Close(); + if (ClearParametersAfterExecute) Parameters.Clear(); + } + return AffectedRows; + } + + /// + /// 执行现有命令()查询 DataSet /// /// public override DataSet ExecuteDataSet() @@ -130,7 +255,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 执行指定的命令查询DataSet + /// 执行指定的命令查询 DataSet /// /// /// @@ -158,6 +283,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } if (_transaction != null) command.Transaction = _transaction; + ReSet(); using SqliteDataReader reader = command.ExecuteReader(); _dataSet = new(); do @@ -174,7 +300,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility catch (Exception e) { if (localTransaction) Rollback(); - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -186,7 +312,73 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } /// - /// 创建一个SQL事务 + /// 异步执行现有命令()查询 DataSet + /// + /// + public override async Task ExecuteDataSetAsync() + { + return await ExecuteDataSetAsync(Script); + } + + /// + /// 异步执行指定的命令查询 DataSet + /// + /// + /// + public override async Task ExecuteDataSetAsync(string script) + { + bool localTransaction = _transaction == null; + + try + { + if (localTransaction) + { + NewTransaction(); + } + + OpenConnection(); + Script = script; + ServerHelper.WriteLine("SQLQuery -> " + script, InvokeMessageType.Api); + using SqliteCommand command = new(script, _connection) + { + CommandType = CommandType + }; + foreach (KeyValuePair param in Parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value); + } + if (_transaction != null) command.Transaction = _transaction; + + ReSet(); + using SqliteDataReader reader = await command.ExecuteReaderAsync(); + _dataSet = new(); + do + { + DataTable table = new(); + table.Load(reader); + _dataSet.Tables.Add(table); + } while (!reader.IsClosed && reader.NextResult()); + + if (localTransaction) Commit(); + + _result = _dataSet.Tables.Cast().Any(table => table.Rows.Count > 0) ? SQLResult.Success : SQLResult.NotFound; + } + catch (Exception e) + { + if (localTransaction) Rollback(); + _result = SQLResult.SQLError; + ServerHelper.Error(e); + } + finally + { + if (localTransaction) Close(); + if (ClearParametersAfterExecute) Parameters.Clear(); + } + return _dataSet; + } + + /// + /// 创建一个 SQL 事务 /// public override void NewTransaction() { @@ -209,7 +401,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } catch (Exception e) { - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -230,7 +422,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility } catch (Exception e) { - _result = SQLResult.Fail; + _result = SQLResult.SQLError; ServerHelper.Error(e); } finally @@ -253,7 +445,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility catch (Exception e) { ServerHelper.Error(e); - _result = SQLResult.Fail; + _result = SQLResult.SQLError; return false; } finally @@ -267,7 +459,7 @@ namespace Milimoe.FunGame.Server.Services.DataUtility /// /// 资源清理 /// - public void Dispose(bool disposing) + private void Dispose(bool disposing) { if (!_isDisposed) { @@ -288,5 +480,13 @@ namespace Milimoe.FunGame.Server.Services.DataUtility Dispose(true); GC.SuppressFinalize(this); } + + private void ReSet() + { + _result = SQLResult.NotFound; + _affectedRows = 0; + _lastInsertId = 0; + DataSet.Clear(); + } } } diff --git a/FunGame.Server/Services/FunGameSystem.cs b/FunGame.Server/Services/FunGameSystem.cs index 4b01e45..bf9f788 100644 --- a/FunGame.Server/Services/FunGameSystem.cs +++ b/FunGame.Server/Services/FunGameSystem.cs @@ -1,11 +1,11 @@ -using System.Collections; -using Milimoe.FunGame.Core.Api.Transmittal; +using Milimoe.FunGame.Core.Api.Transmittal; using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Library.Common.Addon; using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.SQLScript.Common; using Milimoe.FunGame.Core.Library.SQLScript.Entity; using Milimoe.FunGame.Core.Model; +using Milimoe.FunGame.Server.Model; using Milimoe.FunGame.Server.Others; using Milimoe.FunGame.Server.Services.DataUtility; @@ -13,10 +13,13 @@ namespace Milimoe.FunGame.Server.Services { public class FunGameSystem { + public delegate Task CloseListenerHandler(); + public static event CloseListenerHandler? CloseListener; + /// /// 服务器指令列表 /// - public static Hashtable OrderList { get; } = []; + public static Dictionary> OrderList { get; } = []; /// /// 在线房间列表 @@ -46,6 +49,11 @@ namespace Milimoe.FunGame.Server.Services /// /// 服务器配置 /// + public static PluginConfig LocalConfig { get; set; } = new("system", "local"); + + /// + /// 数据库配置 + /// public static PluginConfig SQLConfig { get; set; } = new("system", "sqlconfig"); /// @@ -53,6 +61,11 @@ namespace Milimoe.FunGame.Server.Services /// public const string FunGameWebAPITokenID = "fungame_web_api"; + /// + /// API Secret 文件名 + /// + public const string APISecretFileName = ".apisecret"; + /// /// 初始化数据库连接器 /// @@ -225,7 +238,7 @@ namespace Milimoe.FunGame.Server.Services /// public static void ServerLogin(SQLHelper sqlHelper) { - sqlHelper.Execute(ServerLoginLogs.Insert_ServerLoginLogs(sqlHelper, Config.ServerName, Config.ServerKey)); + sqlHelper.Execute(ServerLoginLogs.Insert_ServerLoginLog(sqlHelper, Config.ServerName, Config.ServerKey)); } /// @@ -237,10 +250,12 @@ namespace Milimoe.FunGame.Server.Services } /// - /// 初始化用户密钥列表 + /// 初始化服务器其他配置文件 /// - public static void InitUserKeys() + public static void InitOtherConfig() { + LocalConfig.LoadConfig(); + LocalConfig.SaveConfig(); UserKeys.LoadConfig(); UserKeys.SaveConfig(); } @@ -274,11 +289,12 @@ namespace Milimoe.FunGame.Server.Services /// 检查是否存在 API Secret Key /// /// - public static bool IsAPISecretKeyExist(string key) + public static bool APISecretKeyExists(string key) { using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); if (sql != null) { + key = Encryption.HmacSha256(key, Encryption.FileSha256(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, APISecretFileName))); sql.ExecuteDataSet(ApiTokens.Select_GetAPISecretKey(sql, key)); if (sql.Result == SQLResult.Success) { @@ -289,54 +305,42 @@ namespace Milimoe.FunGame.Server.Services } /// - /// 获取 API Secret Key - /// - /// - public static string GetAPISecretKey(string token) - { - using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); - if (sql != null) - { - sql.ExecuteDataSet(ApiTokens.Select_GetAPIToken(sql, token)); - if (sql.Result == SQLResult.Success) - { - return sql.DataSet.Tables[0].Rows[0][ApiTokens.Column_SecretKey].ToString() ?? ""; - } - } - return ""; - } - - /// - /// 设置 API Secret Key + /// 创建 API Secret Key /// /// /// /// - public static void SetAPISecretKey(string token, string reference1 = "", string reference2 = "", SQLHelper? sqlHelper = null) + public static string CreateAPISecretKey(string token, string reference1 = "", string reference2 = "", SQLHelper? sqlHelper = null) { bool useSQLHelper = sqlHelper != null; sqlHelper ??= Factory.OpenFactory.GetSQLHelper(); string key = Encryption.GenerateRandomString(); - if (sqlHelper != null) + string enKey = Encryption.HmacSha256(key, Encryption.FileSha256(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, APISecretFileName))); + if (sqlHelper != null && enKey != "") { sqlHelper.ExecuteDataSet(ApiTokens.Select_GetAPIToken(sqlHelper, token)); if (sqlHelper.Success) { - sqlHelper.Execute(ApiTokens.Update_APIToken(sqlHelper, token, key, reference1, reference2)); + sqlHelper.Execute(ApiTokens.Update_APIToken(sqlHelper, token, enKey, reference1, reference2)); } else { - sqlHelper.Execute(ApiTokens.Insert_APIToken(sqlHelper, token, key, reference1, reference2)); + sqlHelper.Execute(ApiTokens.Insert_APIToken(sqlHelper, token, enKey, reference1, reference2)); } } + else + { + ServerHelper.WriteLine($"API Secret Key '{token}' 创建失败,未连接到数据库或者找不到加密秘钥。", InvokeMessageType.Error); + } if (!useSQLHelper) { sqlHelper?.Dispose(); } + return key; } /// - /// 创建 SQL 服务后需要做的事 + /// 创建 SQL 服务后需要做的事,包括数据库初始化,API 初始化,首次建立管理员账户等等 /// /// public static void AfterCreateSQLService(SQLHelper sqlHelper) @@ -354,7 +358,12 @@ namespace Milimoe.FunGame.Server.Services { mysqlHelper.ExecuteSqlFile(AppDomain.CurrentDomain.BaseDirectory + "fungame.sql"); } - SetAPISecretKey(FunGameWebAPITokenID, sqlHelper: sqlHelper); + ConsoleModel.FirstRunRegAdmin(); + using StreamWriter sw = File.CreateText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, APISecretFileName)); + sw.WriteLine(Encryption.GenerateRandomString()); + sw.Flush(); + sw.Close(); + ServerHelper.WriteLine($"已生成一个默认的 API Token,Token ID: {FunGameWebAPITokenID}, Secret Key: 【{CreateAPISecretKey(FunGameWebAPITokenID, sqlHelper: sqlHelper)}】,请妥善保管方括号内的内容,仅显示一次。如遗忘需要使用管理员账号重置。"); sqlHelper.Execute(Configs.Insert_Config(sqlHelper, "Initialization", FunGameInfo.FunGame_Version, "SQL Service Installed.")); SQLConfig.Clear(); SQLConfig.Add("Initialized", true); @@ -367,6 +376,10 @@ namespace Milimoe.FunGame.Server.Services sqlHelper.Dispose(); } + /// + /// 数据库是否存在 + /// + /// public static bool DatabaseExists() { SQLConfig.LoadConfig(); @@ -403,6 +416,8 @@ namespace Milimoe.FunGame.Server.Services plugin.Controller.Close(); } } + // 停止所有正在运行的监听 + CloseListener?.Invoke(); } } } diff --git a/FunGame.Server/Services/General.cs b/FunGame.Server/Services/General.cs index a3201bc..efe0dcc 100644 --- a/FunGame.Server/Services/General.cs +++ b/FunGame.Server/Services/General.cs @@ -234,11 +234,11 @@ namespace Milimoe.FunGame.Server.Services public static void InitOrderList() { FunGameSystem.OrderList.Clear(); - FunGameSystem.OrderList.Add(OrderDictionary.Help, "Milimoe -> 帮助"); - FunGameSystem.OrderList.Add(OrderDictionary.Quit, "关闭服务器"); - FunGameSystem.OrderList.Add(OrderDictionary.Exit, "关闭服务器"); - FunGameSystem.OrderList.Add(OrderDictionary.Close, "关闭服务器"); - FunGameSystem.OrderList.Add(OrderDictionary.Restart, "重启服务器"); + FunGameSystem.OrderList.Add(OrderDictionary.Help, s => WriteLine("Milimoe -> 帮助")); + FunGameSystem.OrderList.Add(OrderDictionary.Quit, s => WriteLine("关闭服务器")); + FunGameSystem.OrderList.Add(OrderDictionary.Exit, s => WriteLine("关闭服务器")); + FunGameSystem.OrderList.Add(OrderDictionary.Close, s => WriteLine("关闭服务器")); + FunGameSystem.OrderList.Add(OrderDictionary.Restart, s => WriteLine("重启服务器")); } } diff --git a/FunGame.Server/logo.ico b/FunGame.Server/logo.ico index 4082874accbeb80cd336bcb14ff9004ad35920ff..b40ed3e35a11aa0b8b5bce71fbaaa64a7344b587 100644 GIT binary patch literal 67646 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 literal 16958 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 diff --git a/FunGame.WebAPI/Controllers/DataRequestController.cs b/FunGame.WebAPI/Controllers/DataRequestController.cs index 4da774d..d8ddfcf 100644 --- a/FunGame.WebAPI/Controllers/DataRequestController.cs +++ b/FunGame.WebAPI/Controllers/DataRequestController.cs @@ -19,6 +19,7 @@ namespace Milimoe.FunGame.WebAPI.Controllers { PayloadModel response = new() { + Event = "data_request", RequestType = payload.RequestType }; diff --git a/FunGame.WebAPI/Controllers/GamingRequestController.cs b/FunGame.WebAPI/Controllers/GamingRequestController.cs index 28e6247..2fb71c9 100644 --- a/FunGame.WebAPI/Controllers/GamingRequestController.cs +++ b/FunGame.WebAPI/Controllers/GamingRequestController.cs @@ -19,6 +19,7 @@ namespace Milimoe.FunGame.WebAPI.Controllers { PayloadModel response = new() { + Event = "gaming_request", RequestType = payload.RequestType }; if (model.RequestID == Guid.Empty) diff --git a/FunGame.WebAPI/Controllers/InventoryController.cs b/FunGame.WebAPI/Controllers/InventoryController.cs new file mode 100644 index 0000000..d9daf44 --- /dev/null +++ b/FunGame.WebAPI/Controllers/InventoryController.cs @@ -0,0 +1,437 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Library.SQLScript.Entity; +using Milimoe.FunGame.WebAPI.Models; + +namespace Milimoe.FunGame.WebAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize] + public class InventoryController(RESTfulAPIModel model, ILogger logger) : ControllerBase + { + private readonly ILogger _logger = logger; + + /// + /// ȡ̵ + /// + [HttpGet("getstore")] + public async Task GetStore(long[] ids) + { + PayloadModel response = new() + { + Event = "inventory_getstore", + RequestType = DataRequestType.Inventory_GetStore + }; + + try + { + Dictionary data = new() + { + { "ids", ids } + }; + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_GetStore, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ȡг + /// + [HttpGet("getmarket")] + public async Task GetMarket(long[]? users = null, MarketItemState state = MarketItemState.Listed, long[]? items = null) + { + PayloadModel response = new() + { + Event = "inventory_getmarket", + RequestType = DataRequestType.Inventory_GetMarket + }; + + try + { + Dictionary data = new() + { + { "users", users ?? [] }, + { "state", state }, + { "items", items ?? [] } + }; + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_GetMarket, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ̵깺Ʒ + /// + [HttpPost("storebuy")] + public async Task StoreBuy([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_storebuy", + RequestType = DataRequestType.Inventory_StoreBuy + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_StoreBuy, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// гƷ + /// + [HttpPost("marketbuy")] + public async Task MarketBuy([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_marketbuy", + RequestType = DataRequestType.Inventory_MarketBuy + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_MarketBuy, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ¿ + /// + [HttpPost("updateinventory")] + public async Task UpdateInventory([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_updateinventory", + RequestType = DataRequestType.Inventory_UpdateInventory + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_UpdateInventory, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ʹÿƷ + /// + [HttpPost("useitem")] + public async Task Use([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_use", + RequestType = DataRequestType.Inventory_Use + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_Use, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ̵Ʒ + /// + [HttpPost("storesell")] + public async Task StoreSell([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_storesell", + RequestType = DataRequestType.Inventory_StoreSell + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_StoreSell, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// гƷ + /// + [HttpPost("marketsell")] + public async Task MarketSell([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_marketsell", + RequestType = DataRequestType.Inventory_MarketSell + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_MarketSell, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ¼гƷ + /// + [HttpPost("marketdelist")] + public async Task MarketDelist([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_marketdelist", + RequestType = DataRequestType.Inventory_MarketDelist + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_MarketDelist, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// г۸ + /// + [HttpPost("updatemarketprice")] + public async Task UpdateMarketPrice([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_updatemarketprice", + RequestType = DataRequestType.Inventory_UpdateMarketPrice + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_UpdateMarketPrice, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ȡױ + /// + [HttpGet("getoffer")] + public async Task GetOffer([FromQuery] long offerId) + { + PayloadModel response = new() + { + Event = "inventory_getoffer", + RequestType = DataRequestType.Inventory_GetOffer + }; + + try + { + Dictionary data = new() + { + { OffersQuery.Column_Id, offerId }, + { "apiQuery", true } + }; + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_GetOffer, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ױ + /// + [HttpPost("makeoffer")] + public async Task MakeOffer([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_makeoffer", + RequestType = DataRequestType.Inventory_MakeOffer + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_MakeOffer, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ޸Ľױ + /// + [HttpPost("reviseoffer")] + public async Task ReviseOffer([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_reviseoffer", + RequestType = DataRequestType.Inventory_ReviseOffer + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_ReviseOffer, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// Ӧױ + /// + [HttpPost("respondoffer")] + public async Task RespondOffer([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "inventory_respondoffer", + RequestType = DataRequestType.Inventory_RespondOffer + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Inventory_RespondOffer, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + } +} diff --git a/FunGame.WebAPI/Controllers/MainController.cs b/FunGame.WebAPI/Controllers/MainController.cs new file mode 100644 index 0000000..b5bf73a --- /dev/null +++ b/FunGame.WebAPI/Controllers/MainController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.WebAPI.Models; + +namespace Milimoe.FunGame.WebAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize] + public class MainController(RESTfulAPIModel model, ILogger logger) : ControllerBase + { + private readonly ILogger _logger = logger; + + /// + /// ȡ + /// + [HttpGet("notice")] + public async Task GetNotice() + { + PayloadModel response = new() + { + Event = "main_getnotice", + RequestType = DataRequestType.Main_GetNotice + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_GetNotice, []); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// Ϣ + /// + [HttpPost("chat")] + public async Task Chat([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "main_chat", + RequestType = DataRequestType.Main_Chat + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_Chat, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + } +} diff --git a/FunGame.WebAPI/Controllers/RoomController.cs b/FunGame.WebAPI/Controllers/RoomController.cs new file mode 100644 index 0000000..bf75009 --- /dev/null +++ b/FunGame.WebAPI/Controllers/RoomController.cs @@ -0,0 +1,340 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.WebAPI.Models; + +namespace Milimoe.FunGame.WebAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize] + public class RoomController(RESTfulAPIModel model, ILogger logger) : ControllerBase + { + private readonly ILogger _logger = logger; + + /// + /// + /// + [HttpPost("create")] + public async Task CreateRoom([FromBody] Dictionary data) + { + string msg = "ʱ޷"; + PayloadModel response = new() + { + Event = "room_create", + RequestType = DataRequestType.Main_CreateRoom + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_CreateRoom, data); + if (result.TryGetValue("room", out object? value) && value is Room room && room.Roomid != "-1") + { + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + msg = "ʧܣ"; + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = msg; + return StatusCode(500, response); + } + + /// + /// ȡ/·б + /// + [HttpGet("list")] + public async Task GetRoomList() + { + PayloadModel response = new() + { + Event = "room_list", + RequestType = DataRequestType.Main_UpdateRoom + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_UpdateRoom, []); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// 뷿 + /// + [HttpPost("join")] + public async Task IntoRoom([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_join", + RequestType = DataRequestType.Main_IntoRoom + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_IntoRoom, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ˳ + /// + [HttpPost("quit")] + public async Task QuitRoom([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_quit", + RequestType = DataRequestType.Main_QuitRoom + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_QuitRoom, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ƥ䷿ + /// + [HttpPost("match")] + public async Task MatchRoom([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_match", + RequestType = DataRequestType.Main_MatchRoom + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_MatchRoom, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ׼״̬ + /// + [HttpPost("ready")] + public async Task SetReady([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_ready", + RequestType = DataRequestType.Main_Ready + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_Ready, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ȡ׼״̬ + /// + [HttpPost("cancelready")] + public async Task CancelReady([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_cancelready", + RequestType = DataRequestType.Main_CancelReady + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_CancelReady, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ʼϷ + /// + [HttpPost("start")] + public async Task StartGame([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_start", + RequestType = DataRequestType.Main_StartGame + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Main_StartGame, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// · + /// + [HttpPost("updateroomsettings")] + public async Task UpdateRoomSettings([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_updatesettings", + RequestType = DataRequestType.Room_UpdateRoomSettings + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Room_UpdateRoomSettings, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ȡ + /// + [HttpPost("playercount")] + public async Task GetRoomPlayerCount([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_playercount", + RequestType = DataRequestType.Room_GetRoomPlayerCount + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Room_GetRoomPlayerCount, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// · + /// + [HttpPost("updatemaster")] + public async Task UpdateRoomMaster([FromBody] Dictionary data) + { + PayloadModel response = new() + { + Event = "room_updatemaster", + RequestType = DataRequestType.Room_UpdateRoomMaster + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.Room_UpdateRoomMaster, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + } +} diff --git a/FunGame.WebAPI/Controllers/UserCenterController.cs b/FunGame.WebAPI/Controllers/UserCenterController.cs new file mode 100644 index 0000000..49cc9a1 --- /dev/null +++ b/FunGame.WebAPI/Controllers/UserCenterController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Library.SQLScript.Entity; +using Milimoe.FunGame.WebAPI.Models; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Milimoe.FunGame.WebAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize] + public class UserCenterController(RESTfulAPIModel model, ILogger logger) : ControllerBase + { + private readonly ILogger _logger = logger; + + /// + /// û + /// + [HttpPut("updateuser")] + public async Task UpdateUser(Dictionary data) + { + PayloadModel response = new() + { + Event = "user_update", + RequestType = DataRequestType.UserCenter_UpdateUser + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.UserCenter_UpdateUser, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// + /// + [HttpPut("updatepassword")] + public async Task UpdatePassword(Dictionary data) + { + PayloadModel response = new() + { + Event = "user_updatepassword", + RequestType = DataRequestType.UserCenter_UpdatePassword + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.UserCenter_UpdatePassword, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + + /// + /// ÿǩ + /// + [HttpPost("dailysignin")] + public async Task DailySignIn(Dictionary data) + { + PayloadModel response = new() + { + Event = "user_dailysignin", + RequestType = DataRequestType.UserCenter_DailySignIn + }; + + try + { + Dictionary result = await model.DataRequestController.GetResultData(DataRequestType.UserCenter_DailySignIn, data); + response.StatusCode = 200; + response.Data = result; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError("Error: {e}", e); + } + + response.StatusCode = 500; + response.Message = "ʱ޷"; + return StatusCode(500, response); + } + } +} diff --git a/FunGame.WebAPI/Controllers/UserController.cs b/FunGame.WebAPI/Controllers/UserController.cs index f5cdf20..626688d 100644 --- a/FunGame.WebAPI/Controllers/UserController.cs +++ b/FunGame.WebAPI/Controllers/UserController.cs @@ -28,6 +28,13 @@ namespace Milimoe.FunGame.WebAPI.Controllers [Authorize(AuthenticationSchemes = "APIBearer")] public IActionResult Register([FromBody] RegDTO dto) { + string msg = "服务器暂时无法处理注册请求。"; + PayloadModel response = new() + { + Event = "user_register", + RequestType = DataRequestType.Reg_Reg + }; + try { string clientIP = HttpContext.Connection.RemoteIpAddress?.ToString() + ":" + HttpContext.Connection.RemotePort; @@ -37,39 +44,40 @@ namespace Milimoe.FunGame.WebAPI.Controllers string email = dto.Email; string verifycode = dto.VerifyCode; - (string msg, RegInvokeType type, bool success) = DataRequestService.Reg(apiListener, username, password, email, verifycode, clientIP); + (msg, RegInvokeType type, bool success) = DataRequestService.Reg(apiListener, username, password, email, verifycode, clientIP); - return Ok(new PayloadModel() + response.StatusCode = 200; + response.Message = msg; + response.Data = new() { - RequestType = DataRequestType.Reg_Reg, - StatusCode = 200, - Message = msg, - Data = new() - { - { "success", success }, - { "type", type } - } - }); + { "success", success }, + { "type", type } + }; + return Ok(response); } catch (Exception e) { logger.LogError("Error: {e}", e); } - return BadRequest("服务器暂时无法处理注册请求。"); + + response.StatusCode = 500; + response.Message = msg; + return StatusCode(500, response); } [HttpPost("login")] public async Task Login([FromBody] LoginDTO dto) { + string msg = "服务器暂时无法处理登录请求。"; Config.ConnectingPlayerCount++; + PayloadModel response = new() + { + Event = "user_login", + RequestType = DataRequestType.Login_Login + }; + try { - PayloadModel response = new() - { - RequestType = DataRequestType.Login_Login - }; - string msg = "服务器暂时无法处理登录请求。"; - string clientIP = HttpContext.Connection.RemoteIpAddress?.ToString() + ":" + HttpContext.Connection.RemotePort; ServerHelper.WriteLine(ServerHelper.MakeClientName(clientIP) + " 通过 RESTful API 连接至服务器,正在登录 . . .", InvokeMessageType.Core); string username = dto.Username; @@ -109,21 +117,26 @@ namespace Milimoe.FunGame.WebAPI.Controllers Config.ConnectingPlayerCount--; logger.LogError("Error: {e}", e); } - return BadRequest(); + + response.StatusCode = 500; + response.Message = msg; + return StatusCode(500, response); } [HttpPost("logout")] [Authorize] public async Task LogOut() { + string msg = "退出登录失败!服务器可能暂时无法处理此请求。"; + PayloadModel response = new() + { + Event = "user_logout", + RequestType = DataRequestType.RunTime_Logout + }; + try { - PayloadModel response = new() - { - RequestType = DataRequestType.RunTime_Logout - }; string username = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? ""; - if (apiListener.UserList.ContainsKey(username)) { RESTfulAPIModel model = (RESTfulAPIModel)apiListener.UserList[username]; @@ -134,16 +147,15 @@ namespace Milimoe.FunGame.WebAPI.Controllers response.StatusCode = 200; return Ok(response); } - - response.Message = "退出登录失败!"; - response.StatusCode = 400; - return BadRequest(response); } catch (Exception e) { logger.LogError("Error: {e}", e); } - return BadRequest(); + + response.StatusCode = 500; + response.Message = msg; + return StatusCode(500, response); } [HttpPost("refresh")] @@ -163,7 +175,7 @@ namespace Milimoe.FunGame.WebAPI.Controllers { logger.LogError("Error: {e}", e); } - return BadRequest(); + return StatusCode(500); } private async Task CheckConnection(string username, string clientIP) diff --git a/FunGame.WebAPI/FunGame.WebAPI.csproj b/FunGame.WebAPI/FunGame.WebAPI.csproj index e86a67e..0c5b16e 100644 --- a/FunGame.WebAPI/FunGame.WebAPI.csproj +++ b/FunGame.WebAPI/FunGame.WebAPI.csproj @@ -7,8 +7,8 @@ Milimoe.$(MSBuildProjectName.Replace(" ", "_")) ..\bin\ FunGameWebAPI - Images\logo.ico Milimoe + logo.ico @@ -20,7 +20,7 @@ - + @@ -32,17 +32,11 @@ + - - - \ - True - - - diff --git a/FunGame.WebAPI/Images/logo.ico b/FunGame.WebAPI/Images/logo.ico deleted file mode 100644 index 4082874accbeb80cd336bcb14ff9004ad35920ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16958 zcmeI4S94=|dfvxh!q>jk$58nzxXR_StSVhd)@~`6v|B2dEUz{v?ey$U=$=l~fz4)< zY~~CCB#4}I&KY3Nc^?1%hl3gJlzin&d$vv=-~a^9o1gc2U+f1T{5$^lhky71|Ne&$ z{@_1;@WH?T;DZnTfP+gP{D8w;xBmOXyVL!DZQhc+slII9vTU10xdK(?>)Ud;Gb*?C zmCSXOOf=N1@2TJ3RW?&sw^>!MT~@8$RJ*&YL4RBQZkMB>onA|O-IjJcP3>^>n{74f z4V7w5RZ0~l<5?Xap6mAdTJ_qtQt53~%G=uCKhp93iN3tK(dprty4*uH+mgecRwh|i zDp{1r9aSn;P`%tywbbJCx+<4CDsg`G&ZQ@#UVT2|)264aU}m8H!L`!)t_FjBou6IG zAI&ROXe(1_$lcyU%j=Z9= znsx_$^_n%c8+El>E%iEGj8Nh6b&Q`kk7(?WSrCmr*UZ<`B z^6zx2yw15-vvkm`)#P->)u;|MHR;jz;21w@DHiK!cjr`xd&fFIztqjuwFaGCZMP2; zk2JO2J5@5>RI9P8VyLP`l|D}k$7DZA>nUFups$M3>6SKK89CNd z@_Gx(XPR1E2qGu8{R>&n)|2U)@-GFuqpGHsxH;s^8dGsocXZ8KvVDbz8(*dq>%H6~8H~TI{G* z>Bz@@r?GQ5+LFs%)USU#tz^pBq_4yMW9&top$pqqNj6VL^Y)Y`7o*w?bu=;+l*e0C z1mEBE6_qG76~IS=@rI6$&s8t=k(uxC6#1?lRdYQh5-pYUCFC^rCLgrQlfzOHDrg#VbB;y%a&vkPHmi+y>+bp`w-O-%Wh|3ks$Qj7U?#*e9>waHeZdXp7#AYF5o!TCH*t+1yI{mlSqR$hrK5X5M{D><93JhHB-$LLu&X zE~Wj0Q~du>x7hw<|4g0guC~?_3J{0;y*(Z7o}rfuZEmGiA#ZiCz2Bc!JXTi0ivK2g zu&?K%9(=e%ZtH1tGi$x}%a2AhAkM?+DweFv&);m2+cv`m@?9Q(*~PvWT1Q{+mm|oW zASWhebHx>k@p`(VT)v|IrW^7u+xwr9!ZZFTogwdzQdhj;Y! z^`b^bSIO6Pe7P!bqNaGUt7xvRY>oRMzui!4#uj37Mmw592b%%nVm71#HLi%C1%fq2 zkT+lG>Ixej9bYLxUOGO$lyf7YTr#X;DyR)7_ltcZ=-3x3Yh+?enQTXebX`s2^4;qV zD|QaZ!`WwXjHdC)%A)>cvPU%%4A z%!H1QPRQL?R!zS=KeuXG&D1IUrBR~Bt|fJVZ4UAAH1;e~OHYrebIl#?^^d5PyXd?l z$7&LtQCB+D>(g^xkQ1WRd;5Ca@?}4=Z|39=)wN6=Yf>+E_znm8 z`nQjl^s^7A^{^>!hoFW$KoN;K7IgK4m5f&6adUnIWmVnZ}5`E7thRLtn z>h$*%0t;lRVWm=AZQ|SEEGigj$>uC6Roc-2`Dd16`sAryKYcK*AN_htKYK8%w=;eX z_6~J(eFMg;Xcv1J|1W^+mbS{uHV#$n9?BK1YRi=(-*mOQ8qwlvl+UkHR}2Pg5eFAK zKD{FM?kX9nXw#9f>bAjc=6(;zXHD)S8m{2u7u2>3^m3yw(Y5*RJUYw~8)fXci=F$_ z_6|O_1@4IWi|1#tM;`M~!zzUiw%^5< z&#c(Ey}1VOHt>m>3aOe7uxGEnW9g&O+_P{_t8Hv_t;_Rk@_a=tu*k{5vE`HXT36fT zkUn|{hO?@-cGTl}f;#$cK1B`OBL^I-e{?~t?`xJ?^1}z?dj8I#3;gf;`qsh|RpRdY zOZ*l2N9Gb%{IB@ya)zr4a9_?yRueN`WXP*VzO3T2X=HMP{d)MT(cO0clG?Mau(30K zeT!a<4vgJ8*wv4YEA$RIe8wLtTK9c^c1=z#kpIB%=&nir&XdpW%VC|9Q?IWtt=xLa zXPi)D(qK=6^$vE9l_BR9v3ZZ0(8KS0{bQxTN!#Sl9eg=f*p`!A-P{A~@w`KiQ3Gd2 z$YWmegR$8F`?|nN=JEBH_~e&gYS~rM>sfMth@K!+wRACS59;|FyBuJJc%-aKzJ>mq z)BZ?&`<#$NOTDZZDQ;PZitz>Xzks}bWVeBzRvcYAY-eaRi3JT~f6=a0ZfhuG>| zheuZy#>>%<+3}wTkEXQgj_dO3l3aPA5SX(|{0HbeS8QO9U|I1vIw5avIn#V!R4XfB zi_Hkw)AT6Rh;l=9>X*U&S>kEHcX#O3^2BP3T$`mfROlWDj;)+$d^|{%tTg zvi$z`_D+5ec>Mhu*goE&XEbp{J#=LC$+KyV(o?3X;Q{)=SgfQ3_cb*iP(Hpf~(Z$OSL_3%q_ zK<7yDOplztOFl2lL$9#zYvTJ|@Gky;b!+Ls*gsA!Hg^ZHH=Uu_3)|?ef|3mx7ir`9Y zHor-4kMGvE&$Jb3>(gfrIDxWqkil$GJ=Q%kDUiYh+P1Q#}&JO)Sl@Xdvd zn3lKbfl~Cek;Yg4N6jMt=Xh@pTxfDb1WeP!pQF^1HE_lJdWQR;U*@~kiNCq|u+>*b zsWYFyG(ATSEY;GwJw^=g!42RO)KRB1XYnb%NKcvSu~s%K`sBG!0s5XQJ&ujn{l1Jm z1BFEYEPBkuLv%Phv6ycmrXp`9XTy5r>GbB3HO;#H8nl$RDyaC3ZxaqAJX*m zJurU*KP+I2Iyo-~zi>%Dj?wqoxUUsoLy^ReocQhq7~uBHTk_wniuhgxKT45XTGW{m zy}>5;6-{^K=U9SA@Zm!aM^rD~IF$n@dJ{dp2OAq3rNM~?BRMy+n&Uo4CPUznyps8S zO)bG2_&XLp@l#)>=E7QC2j@^n-i~k5SM=l#6s%gizUHUaka2Rfi z8Wko#)`<5exOJRdV8;(r^l3hCTt)g7(;t+L+<7=T^pHoM6M76Uxog>#SFC)*=fD$y zvyI(N?3;6v_h-Ol!AK4qf36ZaDo8GL;m>7yj|lZ-VLhtF`4AX8WAQ?Zjv{s=S8+cn zazKFEWa>*e0-rD$!6y!>A?Pqi|Ct=XyEHZWCV-#h!BONRcSU3GeAJgXb?Zz!$2V|I z+i+N6dY&TkmsG2?$%%$L+_q|`hul~~rW$^*ycW~QTPMBJ9VciwiWcw7BTEH7q=-#= zV85u^P!)eE2ZHG`qT;|`|v2jujKLJI5?z=fBC=)9k9Eba|QjE@Q)3Do}SK0 zZv`*IeS86jmx5O;!#l>P$;OTzZ(UDEf?9Mo$-TQ;an{rXBQ4v@dh&%=^K(V{;8)*_ zcs08eqYpXIF4*`0*|(2wHQ?Mo1V1zm=;bQZs*I9`tHY*#bX_7oE9B84dCg5eUWMnH zhxZ8js5|&goxEq{uag79#Lsr`fOzR^d@iJF7d%04lqEK9Z@!V;%KvalIciRV7}(g# zz>zlK$H@8QJ`)=`Fxumhd3-poEo7fxB_8RqW8{?rwbgJjA!2tc(A0w`euXmo)cKm~ zJ@j2V(6bj_J%1I@+zi-f91euOVGm4jaCEKx<6G@=-ZQcvTxoE4tsdvy6S(2tnLN~% z)wLjb7tBFC8xACA_%puSK~MSknVp;oFIlCh;x&WO>fCP_j-%hfmekf<{YVWs(NL1y zM$a|0`|vzxi_y0egR@R8+2cxP2jr(Ly>*}1ENg7qripnMF^(VEGsJitpC*^UDTE_M z3#<5p;Kuc`UW~{1zEdUe`%>o&zNw~1FXMXjY(pE~D)L?7>(|J4qg`bF`Z&4O9@lm_ z573Eu-vQ4L(Ze=))h0Y~ak5bk(` zJmMu5(x&d?V;)z6JZ$n)QPT_f1v)6y>3MU^32KL6=m2;(t8x=OfNz^P-!XEYf)|n1 z9OhabU)@3GJ+AMaeyM|V{Nn6J2WPiBHpl7BUF>b8$PE>X_gkm0n_rC3|I(x3`%&&A zkNhS07RP3sz7YT4KUW}4E~FPK5c@{vq1?v)=J7IEVsSmDceB(pu+rP95SXulU5onk z$rLgyX>4YTdP_{wPldqiY4VcO8Kd680nsnbE=BR5B%C5OxwMb3b*OtM`tYGm>l@4i zj&8J%td_lv%p4}(jr``EqGl(kdxywbmKka?fui022$e#EhRNZ(5QdFe?v9Q5VO zL5+?3kTqxJ{unkl+^DI+336l~*@Mhhveg|G!O2J9(4o)Y*TK?8SW}CB&8`F$23tOU zvtjieVX)rAmrLLr7qK6J!%2~k=xa?bqSmag2dHB?`0T1)jJoKv3wpN{=R14gXzE&G zTVr!MxxLIq_TlvpusgEuV&l8x#*%Z_=!I)WKL^!T!MOpv)yE$q=P%r57w>)WZP>HxNTD`sL`@-qIVsK9_deLgba6Q*w%RkhEIlHM{ zv$n8i*CKGFC3rx%dan7wU@s>f^a3IBaS{2;Rxcie<112wH{4go4smXL-8|n%{sSKG6C1|o&0%z~$I&Av zR>-A=y2+4dN8hwTKN-mOG_g{ES1y3R4#=r{s&|ep84rw2k-cwpfb7QCO}rbKjlGX9IEVpr zZ3p>V`!~w9ZWJwC%b&ag%O7ZRDNSx~=;Oy5ngo+BfQhHz$X4htbJ)fQUzy17Sr{d2 z`lRab_q_MzFCc#yc^2pmy{39 zY+_)K+;84f=I@f#ONF!7^s!fb$E7}gx(TPzgyVFPx8Z5v^XBl|<;@JU8){PyUf7Y+ zIhvs?(~r3O`sQtuVP^%H@wgET3!G1XKUQc2{i~VuW+Tt_MOk) z+27RvJD>kXZleS8{>5EB?C;;wKO8cnIm5RP@#lST#|3hp>BU4?Bkx0cHR^`{r@wV( znC%rUE@yf*W^uG9@DoY$ahLh`?N{|~DEshiba2B_ulJM$W0|~B--E*j18*e+gzGk=D}}2YKHF{%6tE8WH*PIJs!dr=HL;`-<09bD)4Dl zd|}E)FP_;`V~5@dUZuT*{*f`tykwT#^YG~k`Z-sIUe(97O=eZz1U=d!wZzU$GI6Di z(3Qrf+T;V~TXy(M69-@dQwN4J5B4rBJbZvp7@V+wLe8hpo_qN~kACG6rG+;^>W^HrWC{~^DN9@P(Cj+xyTdOSD$ z*cx%r!uAFDts2~|$Mo%!UcDL%ssztz<|lb@Zk&8$^TDZZ)HO4k*B6gR+%(;A-*Pnrc%irtAKD|o-SIp_9o)%& zql?Q6dRJyE*fd{0)F~Y0HGJd+^DQ%|{|3hyd{DYqSa+*?VaJBj%OGVoCkoZEFsefcG+- z=rwub25zW<{$|;UnSswqvS$!rCTn&hMrWhQPj6><0cKJm{B9l0Vdh2#Yv#c?3G}c; zo%{_mx4-$jkM!W@AL%E5{YU!2cmI=~|Mb7>i=X|4{^@)FpdbF?HM!fT2M;&l!#l`- zsdD{7D_dQ-w>JHxiGg$WnxgO>%z7Fp@RY|EH}mKV7ct(2i)4ofo+6gnW@c4Hk2}=D zy!_fn|Hg%NCqcn3d{g=hl(e`(B#A@pT7XU3B$oUsWYG9vq|z~ z7QQJ7{}p1sm8!rs;3q-)E)TiW$F)W5@c3Pb9@4LW{_&K4{I65`&fk2b@BPy&II=Wc zONF^iQNR3fp7|@>88{+xRjoW|-dmtnm^zXt|JBX@ zfa&+?9SoNnp^sc;K5KR-Ch@sdzB@x*@g zg?oj=9-T^q+f(4SxYnKEY%o=|bfkCFAuT!z3eYop(VZ&>zr}eL|DRl@f5gxINp`ma z?A)=_*4e($GWTF+G~=`624-hA=KTw{1odvm>UrYyUq&V~?~F088`is_4vtSwz;N`g zMdrZ7Lzn!OB5xaRu*SU6vX9$=hVPlo#BhllbrA6W!< zyG@RuZ(P_+&<97EBSn!nr+4HhceJf3eAWjpnp{ZZ_euD}1NNOxz&B0391pX9Y38mq zupYY{%mZfGX_>Xbt+3az#@^c67P~&+unhWs2liZH{_<+ljV;nzgnu`^UzELtHR9IH zBg`Dp^!$0a(P7>*@*8<;b#@X4)c8^f?02J1@7T%%H`EFA9KizaVWnz@9nX3u+2`P*-_j(;59QBr$(YL zYqrn1&wRPVoHWO7S%$vG%z{?ElbH3BW+&cSUCgxYzryFN?!^Z1t%qEvM;(AX% z`W`;z*_cNkKey|*57(G+#HjH>UMB`7f_nK5z6HD(4mYgb)feL~y<-mIz{jTHJk1Pt zin-?^{xpw$XV;U&q1iv{5;Jkj{x9FIGh^`MQ(o|&2i)C2$5FWDz9oy{Fu(DA@Xpx@ zvq5IoM=iD3Umey(YZm#H{N}OQlZdA(%qzFy``D8+JBaWANn+fNzm3hfk+rN%;$xYa z*z0Kz&+%7B5c~z_H|Mo%`0=wf_AqSv@beXY{DmDq_rk9vkSnUEaKW1|p}?ui+CT6Pw6wWcT4OK4P=ZoODBV z_At)R$?42NzP3Gbb=cQDJYr9*U17I~odx{i+p=5NxIWm~w{l>dykBNd$ME2WkM|M> zHh9RFlRnwuzgCg^#pDL_!x%RAavyG9b6GRThc9h<#!l5|&ux11o?aci_>$eWC3Z<4 zz1)I#WQGNw^6HHn8z<=LJjk9S_s5yR#=!FMGV2k1KWgoAIK1#Vygt3m?kf5incPBe zLB11yojJ!k+}{*@U6Z71 zYvxmBA36cY5oa&WMQwOH18)d-zKZ={yDW_$x_i%-eU)!0tCdp4OYCuGvy>Mup zx%pfCWQw`P+i9QW3p1-JYbG>8FF!|)on@D3jC}ca-be0>fQ5*A`pYH5F`%FKi^M(q zD)Y8HGsPsI9R~B0OUM_S>>cZVV)KD)bCmQ^aWCtY$hfo3A7U96s)RVN?{Qv_k8I1D=zn>Xy$WF5O2JmCOmzF&;%9#y@ZEm=TEZ8*-_BQuZsDR)2n+P#`&a7i-Q-gO> znUuQxKB0{t+}DeFJbd5qZ0Uu+F*-PApD%0nn3%s;nW5#_RmoP_DW<&Q-edXw_==x6j*f#$Gi=!MJvL5opKs^z1^oW?TR4muxschc zlOFl)d`t`YLkdh5q=)tMcNV6!=QKK=0DrRA!A`E5++k+-DfT1MytmywLw07MVA=aJ z8ad*rq?P{~cz z{p7(2^=_S7%v@sD3-0l1#t}mYX}J<@y{CRh@tbN=ICjWZ_KEFdc9&c1qutef zBg37HUxM!n=!f}GrO374?+5S8fB*4NKgaNBHTuW~_`B9;w~BomFLgN0EHlfzd;$FT z2|n=Yi$!{;1oetqfF4%C2BY)LM8KBIo;os+Lre6I>=@YS8~yygV--0^-Upex!ad=? zQQn&8N zoCOauD*(f+kh4AD*hTsWH+)!tJ|>W7_m1S%PjCQ_XW92FzIP}` zquoJA)V}Qja+nzSCf^NZ{#sx7{O>>h(%MI?8D0fH%W{7q{5VW(TK5dcxQri+b zoghb=y{icC39-ku;$gpp`;U+_%sHCxsmLD5Dm|NvU85=Rz!JSy23bSwi6zj{8a;HF zIkM^R1NcO|a)kYvH-I_km{%@S7dP3Jh@(q8{Xsm{vv{y;YtF^}-;>|y!0@4oY(YB* zW){4c4(DTZ;pN^$?GQ91=%@qGedR2V=U9x z2bgCh(9Im!&<6&aW*2P=v%Is#})pc{f|JJOdzYVV6VLuQ4r@`+I zdi}e($WYGTxA`}9!CW`;U$N(1W4AC1Z@CP2x5`Y+$voZ`q_!~+p8&HjIN+n$At)iA z$;o;6(`=o6-7I@X#C{mrjr=iu+T^x5dY*7@TQkTXAdXBA=q4{X*}Ix!XK{i3u>f@; zP5zz6|CjNTIcn?jR-9eE9FJ4XZsFkZIaB9`vKSo z&GY-8F?>9H{Q#b*%DrUJ!36tF^IP;h%nz4cX9$plTy~vKs7(L+{bGF_yPd9kaXXaDobStT>RE$by!zlo+sqk;at-A+b{@(+yk?#cubbWH zVv*e-dLtKmFK_0(^gkPVJqZSWZ`U(=f#>56WMem#9GAo{S>9`6#tWC^fm_{*H`zC@ zz<01~!Q6hFe#pFk4&Pa44&?wR&49aK&V=NEJ6LA6YIY8vjXKdU`$Ovibe;eYf*

DfcfT*o|4Dvp4n%GkuzQ$cS7`V*Lz#!qhkrl3e*gI?zezH`12y|UD`3{=$o_a_ zP7hzr=;>R2cQft8M*LP2|Ie_0;EBK)!p+&p&vxYAU_al%+0Jh920hd~HFKJIP7;KC|?j~{>;Uz&bz(E~ON;0u2GK!YLpO#->i%s7mVhW9xm7kPiFOabXlMC+oLe? zoT7I7m?fJTvWI?uhCRVGxB|l)thkZOi|p9*;Ts3BP}7U|K6?KkwZyI8y;vbOvKoOm zm|9D~d(a=Q_=vNx-qU|Mn0cJBQ*Y*+W(V8c?@)%J-1mL|{_=ge=Unr7#Q-fMK)rH8NQt^Owori(E{S!YMX)ULOSG5Uc3HjG%D zjg>RzT>9+wDzP2W2zGrSb5RB>stFF_-xtUQs8+TeU^5UJ1oiUC{hn|3)NBEpMF!NI$fBJHseY#ole~KE3 zUg)L2;P*yT^Fh5w{s7p^{3h%E{oTL!p{?&<`@eZ?zUS-j;_)^8RhZgmW+P>Kka>EV zcT;ZqZ+c^TZx7hM22Or68=%&M5y|(4EAS9Ai}bXQk@3kJ;siOLjDnpxKOeJ^`||qT z^Eo|#y9!6LXz^c9Upd&jSl2il%4=p$W)JO*T5sy{(09Kr+tBvI^ZnQFUmNCt`>zil ze=RR@Wv-drX}yN;o8RcHQMWwQo-V($*Z|L)+4$rlzYV6gOu+&B110tc1L&ek%{Q2^ zYVlf*Aiu9!48e)b>(i(2;Y;TA6dk-6TZ11=;^RR+o8Mj1+mG{m=~w)IaDqLLPnda` mU9*#~`kQaxpLLJe#+>iV`0eMz*Ub6;{};gjfBgTR2L3nSWUmeY diff --git a/FunGame.WebAPI/Models/PayloadModel.cs b/FunGame.WebAPI/Models/PayloadModel.cs index ab9a777..711c61e 100644 --- a/FunGame.WebAPI/Models/PayloadModel.cs +++ b/FunGame.WebAPI/Models/PayloadModel.cs @@ -2,6 +2,11 @@ { public class PayloadModel where T : struct, Enum { + /// + /// 业务事件 + /// + public string Event { get; set; } = ""; + /// /// 请求类型 /// diff --git a/FunGame.WebAPI/Program.cs b/FunGame.WebAPI/Program.cs index 9192c7b..e7bf6f5 100644 --- a/FunGame.WebAPI/Program.cs +++ b/FunGame.WebAPI/Program.cs @@ -62,8 +62,8 @@ try // 读取 Server 插件 FunGameSystem.GetServerPlugins(); - // 初始化用户密钥列表 - FunGameSystem.InitUserKeys(); + // 初始化服务器其他配置文件 + FunGameSystem.InitOtherConfig(); // Add services to the container. WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -215,12 +215,17 @@ try IExceptionHandlerFeature? contextFeature = context.Features.Get(); if (contextFeature != null) { - await context.Response.WriteAsync(new + await context.Response.WriteAsync(NetworkUtility.JsonSerialize(new PayloadModel() { - context.Response.StatusCode, + Event = "system_error", + RequestType = DataRequestType.UnKnown, + StatusCode = context.Response.StatusCode, Message = "Internal Server Error.", - Detailed = contextFeature.Error.Message - }.ToString() ?? ""); + Data = new() + { + { "msg", contextFeature.Error.Message } + } + })); } }); }); @@ -242,6 +247,16 @@ try // 开始监听连接 listener.BannedList.AddRange(Config.ServerBannedList); + FunGameSystem.CloseListener += async () => + { + foreach (ServerModel model in listener.ClientList.Cast>()) + { + await model.Kick("服务器正在关闭。"); + } + listener.Close(); + await app.StopAsync(); + }; + Task order = Task.Factory.StartNew(GetConsoleOrder); app.Run(); @@ -260,8 +275,20 @@ async Task GetConsoleOrder() if (order != "") { order = order.ToLower(); + if (FunGameSystem.OrderList.TryGetValue(order, out Action? action) && action != null) + { + action(order); + } switch (order) { + case OrderDictionary.Quit: + case OrderDictionary.Exit: + case OrderDictionary.Close: + CloseServer(); + break; + case OrderDictionary.Restart: + ServerHelper.WriteLine("服务器正在运行,请手动结束服务器进程再启动!"); + break; default: await ConsoleModel.Order(listener, order); break; diff --git a/FunGame.WebAPI/Services/APIBearerTokenHandler.cs b/FunGame.WebAPI/Services/APIBearerTokenHandler.cs index d12bc51..92d93b3 100644 --- a/FunGame.WebAPI/Services/APIBearerTokenHandler.cs +++ b/FunGame.WebAPI/Services/APIBearerTokenHandler.cs @@ -25,7 +25,7 @@ namespace Milimoe.FunGame.WebAPI.Services string key = authorizationHeader["Bearer ".Length..].Trim(); // 验证 API Bearer Token - if (key == "" || !FunGameSystem.IsAPISecretKeyExist(key)) + if (key == "" || !FunGameSystem.APISecretKeyExists(key)) { await Task.CompletedTask; return AuthenticateResult.Fail("Invalid Token."); diff --git a/FunGame.WebAPI/logo.ico b/FunGame.WebAPI/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..b40ed3e35a11aa0b8b5bce71fbaaa64a7344b587 GIT binary patch literal 67646 zcmeEv1$@=_x&A>aRMtt`bzQfvtFG=!ON+a^YX}k{LK27)ECiAePlyphT!_27D{&+4 z?!xnbz9-O9xs~niZvT7Nect`f`JEFx&-*^Ff00P0@qfCy68`^RB(FR;P4c8fB6)?v zTjHklzIZ4e{qA9jRHmN)@BaTnvwBIneeLv=b9JxA>D4ceJzevxv1`fy?mzrL zf1M=Xv|Unc-SX>9gEq|w)%q%*gB3&GvgPR^YL&CEZD^2WIp+W8Q~F={31nN=N^V{n zmgJkaKbv*2(=FOzwi_IzxyY}o1{qc zO!_1_rfqLzUF?dBR&TlOzM~R$YYJhuI2*4GtxQo5G{-gh;n-ofl&J%m1_K&kJb{8hUX%-yB?YkGRD&BwnyTY;>t2u1Z1_ii{C~dwU-${6ookk)oJpRR zX3(}Y-Jq*1Nv{jRaxL)QTL;hGwNf3pwxt4B)|9}M*qbcQhwZv5xb0~~h;rk_=5nuT zvFHDj5Bv)u{JVa1ne%g!)N`GZtc&#zr=0GRPuA~ijMpai3N7&2*8s0QQ~0|}?O)(u z${3IwKpn7NU5=~U>fv*w;Z}rB^=o08|A_d=vB;_`v(TbvGWAR+g5+9h_f6#ddde+$ z2<*32;p)b6*sU#v^{NtBtSEumGUkFSO5w1XvElA!+>md~iPo!pCQ`fVXa63*7h4DEsni^^UApgi){3N$+!7G{ob+!HA$Y~D0BMyhi{s9EGx0> z@-4RP9x62JLatFOQuG@UbhJT=z5C8uxNWb33*&yL4P~&ugS{28x1?CCC?o!5xU!)d zF1uUdCEGF@tX8)s;8^o7`i!lP>PhaE?|$*jCrl;vSL7vG#>!7snjJk>X0)rI=J;)k`0Qc$LrNSAFJK6KSjSYu+XG?;HGI8 z3QgLPbD;_8r*9!yw+2xvx5)XmQkxg?zT^5*`ut+pth`Cwg%nfDV700OmK2+{m2lbB z0+0Re2vlltNwQ44FG9cQo}d2X5AVfZJihn85G_Rsut!p6uJUn}*`cVai`yo1v=$&% z@jb*Hdk6Wa=A+VVk59GZi9gg_QMmU@|4%+nG7Xm`oBK<$&9fdzH>jMIc)IlW3Hr64 z#OpSx#GUMLjM3<*3_spE5uw_N^fPV97Pz0SL-MH_Buvjke7e0WvudAliz?%49-Ps7w1KqeG*U}tvqUw!cjek52Of%GyxSX_| zUSY1V@s^oPP3id!NLT*~(Q@x1{P63DSAGu}CqG5$g{8PiI`29W?*H#NF$( zt>j+$ul|UNYjKile*;N@dE+D57d!r(e!lg^bc43{(hM4xWEkE$n0l^JDfvwE*<}5u z&?KF<_;}6Mn{g-GJEPP(Z-*;)AxOR*ezGF2Z)S|wKzpyHOyM4Pq72b$#fVTYg8#un zX|2zawI1_jl%)l*SXM~RFP36&OYH47)xmyq1FmeWg~Qf*cpvP6-{FqQK$YqP0Y|&P z{u4a)Z6x=~*Dw6YgDfLON!V$n8I=}_J8CcQYb`##3aQ7xMAXsu5qju##3;UtEUhn4 zpg#|VXXc^$^19J_dwHEYpX)Pkxt#rx@BHuelFvCCi4foOHb04fgie{nPBCIyoNnW% z$!A(4QuI4&6Ls4gDP0LV?IQ`=ZMWmK+7WxQ4bkfDh*W7u_^~!=Ebo7anD1|b2jh9y z9k<}Rt&aKpExxA;@tS3b)hI@kY9YdoC+I`urT2Ey-h^mnY5l&1wIZt4e9} zRmA=loVGRL>W&sTZf}Mia{$kSUGO{71ApaP9-)^#@AKDp zr>0j}Dr~Pc-P=*9w-PBTUm{lSBZM7%6Ol*WLfVPhC^^3bl}0O2c5w;HDfQOdJL?_Q zmUr5%kW@Nc_^BWBZ}IwF{CS-w9^d7FANu9O)oUdY8nsUc9jnjvm92%hObvWwYH82) zxFOpJ|2rZc`^hxIcYgzGHzJ0wWBzuFv7GSf)ws5`3fHz!Hdm1c%Bcf45w4sk#XaCi zCT<)`hp$W;^(2EfpCiTJOxXTP+Wy)K*8FO5b$cUYzBah-?PLwO6He3t*M05qm+ObW zd|Q67TJ_^W$7{d$XP6(+mE4u@y~rCm7gQw(`&?3Prm(iwY+q~9sntkU7TAA+aM`yh zZz4hEL*$=cfLhZvXtLgjI?J`FGG0LrScCd2GI`Yw+ArR+SNWlD|8IBi9{i;|@MFJV zq(+k@Mzi^afFpGc)+_U1y`lh)8_SrNS4m?v*X`AC+ff7eJL0yz2ChPgxp>al?5a%|y%XGo}a3B1R4#H1vkdOOueP0KB>1LihJ;T=cd%wfK!;O2f z7mt6=e{rWOCDB@WGZRjAx&$fKg$Yz?1hp4s{?%d4SSsn;U8|EnM&C)q>K`9F-)WR(Q0+ z;d#>$7r_**ckq-p^ zx6Id}-tJ&~`4!E@gIBjo;?1pp&d2*d_MW>we(E~K0YCSL$LMxQV)UA~#OicR*sjfi z5&Q5$2dq~W!Dcmcwbit3sqi`PSxfDlvHQ|om@lR;Uzi2sc^SC$RT?hLNrA!V$uRgV znelxx+;$5NNP~}T2K=R3oAkSF;@Mj2? zc^4rE-$LxM50RnuC5p~1CeN=%qxA;b{8n^b-9E(uytdkHMm;&8ntq_pax-pSIZ{+? ztNBu??TMfJ@&1pz@7wtQvYu`Di8@J`#;w`$r#c6`WGY}dHw8uuv&jXyFkMW$W~?s6 zl+VSz%ZrFPd-cY`*5_v6BJKUcoKz|9=VvG3+-He6Py7wPOosE;bjARg^xruMQ7S-$ zS_xt_D-n0H4!(zp{lRYbd5FCN?VdIttU88JwQ+=<7)Llo=z;f5K)eNk<+rzKx&1+kw?yZ;=aGVdYiz?+DYdmm{UpG#vtVf!`Y z{szWjWj+Z?bt<_o+q1Mbfj5HV$(@XQRU^I_7e=U4IS4bvpJza{s;T*pA3)=nX0)N{6 zY}&pMJ{GUptj=QmUVt#wn}}j9AWpjqN&0n2JKKzy6Wzph05=qe*ys5M`*4kML}-p9 zQj0opa-4o(m^#oa@b3-RYMdLVds|{AqkH$)|K2s@w5%lM)ZyQiUfLgVQ-3{@6y_uL z$QKBc`3Qmg-ejHUT_mf`My}p`lo%{UC2jwf#X2ee&9wJc#(n~K;S0p$CfkjurG8Xg zT82vFm3-f}v4$%$XUnWrXI0rKeDCM~vu}JizQ0!o{+^#5sCHB0r=0(Am}*me0%tR9 z*5tr|_Iy#`OuR3Bl`Xvr?8S2_{(SAC)b?rrUuMwO(}_1lT>1RsSLBBIIno->jiaTA z)v83|sam9-Ye0r!3v!G*dF@4*+93IURBGp;>SI!yk0j<%+T)1Unc#H-A!?)WIogLH z)jmXMH(ZKT!heZ>g!(>-mF$7(`DYKDC_TGvB28r}Vh?|Xh=ZRZaPQlU_uoRi$|uOu znTwm}nByC*r0uUIw{JkB%_cNm*^Fj#fZzeC@OhK%Ch7-s0b*ZrVHqlnHt_v>hHI^l zo-eUF{!qD<(%O(IrrLVE9pBK-qnfG~~b>*u$unQpz?>HDAG z1G^>Jr*}zGb$5SIbY^>Vw)!f>A6|gSgI^$M@4Mvvw-9seBc#*k7oJ&!Qu2Nk^Zi=p zdG+M}Mq7dPW;9AMpL#5?Z)6N8^rPnTT9g>BLBYB8D7m-=wdVUqZduD3mRZU_Qeh?c z{om)GbjRnct>muw8Cw6t_={iDU#>yoceGM5hPA#MN2_sxzWXBi-(+Dv{eA(=*q66p zf8LVy8Y}i{Emz(vQ}?C!_?p$~GWKsOV7p$_^lEX9abB!;D{^V`1s2`7Y2A-f+d-7s z4I%gPC?Yj!_a~=t{s#LA#OY5;C5pNbs4~L%kTHN-Q?jRO=p!D=k>C4t!_{|7Zm8^g zAXjhu^?a>$NRnHGn1gc>y!S)+?_sVd_W@E*e1Sau1t>LKPJd49&56Ag^LrcOeT7_q zSA-ti!M?#t*!w2bm~N%c>_py~&B)PTi{cBLQDeS)q}uX;Rk5kuAM335Nir|}d&Py{ z`;h*N8$O3>Bwn(m+d_}GOopm9;L?2JKR=7{9^-u0=WW(7U)xa0zI+vDGpgaVmATv2 z8aQmZM>f~MeoHOy*Rgh2FYVu4+ugz(z8xW|-6$~YLy7GG%It?w;W&&6r%{w#8AHNp z;p@ruLdg3ux)X>!HG%lkLMEj=5JHadV;wN$cuTGC@s#I$kEed;FF7X3(Ap$P)Y!B# zPisR@+OcJbJ2W5R`#*#K?za#k`wkM1&t|SSA2$ui;q>`6#J-N$H`;6^?%T-w+t4Hx z#(dO+2I_#&i+bzLsI}USD)U1qHIkwI??Be6^~lm&E#-m=<88MqP4~u>naaMGW~4Gb z$M}CfCiu>W@r$2&94ME#?<@V%U%p`|@?;BZ^J%pIOxEd`^Q~pxPn=y@^YuKy`Y&t2 z-m;wKIM~eGjk3QP?)zKdzOMyt`&%jPa6iz&*q*gK*&cW?zV|10XPXS6+-U@rS4UCl zJT@g)$B}7#$Jf)&qltYCIX_N+0tsiwk$85B2f`T--e65AK(Tee?|8vyH;#Y17aXdv zQF7zRrpMCN*M(%Nu0-6Cg@}}yL+sx}z<%a>%AX*UKEH@Qzs!iSyxBV3BKGw*w0UCQ zY`+674m-b=X8Y~bfo)Q0xU!GEKt)tp97EaVBPcT5kKEJSkfF1dzGMY*POYLp*o+F( zJ@w`0vM2J4kX;AbUK1 zN1G9>)P_*i4#x8xjN3Yxmvfd^p_AO)MSR$+mG5CL$9ztam@5uY28sO;bGuV4AoO4oQ96_SUPH_k+s1v46|W0Wx%KtO zWwL3#B;)vUNwngs-RY_;$CBiiAo}1u1nv14H)#7(-ak19`SkfE^!b&hYw7bh68o)a zxU!wt??Q{?9<(|M*(<%aIPONXgOG!0ayZV~k2-3sRZ(HCh!Ug2C^XoIY<%NTNc zwpSlTu-X`Nx-k^ljHAY70@W_m0dXb%6;9(wxiBt`^#tz=zaLM`lP*jm#gKYnKp#Mz z2x5-tAwzySn5Pk@H1;+AH)NJcy!S1CI$nNha*E;-#K7;FdM{{mKqB+3zOy`_Ojv07V9E&UZvSzAA@y7frM}>!Q(73w2jcpvFoS z>?5Jr=nx8o4xHYOblr_e(^`r2lXDqcEMos~9pj2^%~j?*O^Pq?e=p;l+Kl{*|K2{q zRVGg2b@beHm%Zgzf>rn#?<tpFKdF z2Z`-4xqFnjjUoBM1X3-jxWFHLFH&Jf!WWwRah}b_5 zH+Fsizuj*lTJd9KXwRjezRUaT>C5TUZ3Xti)(@cFSr#4F4x_{68|l2Rgf90}=yX4a z_Uot7;;e@zM{TJNR9mT_f;u4OfeSK>2X~SOmLpl?GbE{hHkDtTWo~(CJt{75Yp68a z=U8E@yuQ}e;L&bFiKNmm@N3=usR!PtnBUzaKlT29~?UEU@AQ?Kn>B(dGT;E_<-g@Fl&mm>PWA_VQ44e#x5BKXj|NYP-te_HVVO7i|X z)UbwIXCtuRkJhWi`?>W8!op_6@t0H>bkE74~EPA{zqTADuI&dECu2VYDD0IM% zJYansmF7x}2jrLs>_o2qVx+N$n8F!?M72*SpCXMuN8B$pWG~rl$6$@MOlY;e%I8gi z4m0cA4Sp6>cX~S0ygB3Wb9*{J>l^>Tn_Tu5Nu2lO&+|Hbt54(r#*0#MiFIC^jTP+8 zH6ipkxnGyIuRlaR7-pO{BDMM0(_=_88AslgN!)avM2YJpO5LWU$Z3)~K^rEQi+nzk z^_=X>g7@#>&ubMqpv;jzz+hY&^9#;MGMwP|oIsZOBr?tD3&kE0KS8G5yQ}sr^g!K>brBQc_NQE{#14_rU}~jNTZr5!g?l^!g;q+^6s_p?#N94@$00qQH)JY(icqhYS4UnD3Q32;L|5-Uh?F>phRqC?R}zn>OG z2ZI3@F&Jpht2z1u%+N$p9reCO+ z&AixnIOAf&rc|T)zol7LK58<%P|{ag|AXJk<6ySLT_*E`pktK{K1a)-_hAIie3nEG z;7rIK&Vb2vG0q=ExCUc!FwV~)#=`&SSl!`p#_=M* z7d@TCGh-+r2UIX0EWaZ+$svjK0RsON;-76ry`n#<@VJd!+ez}qB(Cph#<@@9A~n7| z^I*v4c@o<-@2LB2n~l(23&{JQzSW_3M57dQ0e^ctq1@w9xXRL4tUDpqy(?tdy z&Wt0k?n68C$2R7Xccq<=JFf0U=e2!&?|$@n97JD;8oFbRG?BJPQsaO92XXkNH@(AD zB?;P{63*g36{FLp5qqjVFJ7->AVR4DH};mncUS3HfJ{Ym*s+?JD2-dDai?oHq?y(~ zk?T@BBisHPU2xtPFFCXF>U2N3T(j`w)j0n}Bu=~;f(xt#+HB-r<^j%QF}DvsKE!x_ z%KkIW$>TngXbhV~eaIxrsSDZGjN>k_wr4OV#Zts_!u|#RA`cM#y*$e?lrr}dQbrvp zC1;2jGJ!gfO8f=yS9tJy22F85JmZ3EoE19tZbYooN2U*+U-0U099O@c60rR%_*2|A zz5}0KZ?e`SY@hkwh2^w;5$|s&SId&~)i5Hk4dkjZ8VkDXp>41?4JHAfc2sXu_uQmp}j-lUE5q+Kt==V^NUVA;rEgo`wEQfxt zqZsf$ia{TF4EZTwIN%t+Rv9Bv+PCpwAwMTrQ_OKr`9}9&3pF&~ydJJsmr4D)REzA#7DKkmXDgGeY zJmnD&y$jR4G_586^4StUxr{d=)hchon!=}apvw0)a%^uSmNp))%@|RCjCrB(`{aN6f6?a&VUIuAU=+o6f(L{i zjG>HrkVRjTz?x60@q{#co~;${-&PihKU* z7#0#BbWIJTA?g?nQ^#0@21aAFFp^=2p?v$xok@{1dow(L3f8aZ`F`w;$>*9R$!8m$ z;J&S2${E&Xv^rU*>Ez6P8!o&O1(oSuP@U!rm1#auz9YwH`atuyH(>B$*lpV{6570X zWhdTH%sCWec=Puu$?DUVNmQnVoG3mPpqM|Z`MM8|J?90zkK=KfI~z_rxXX38i!<4* z^=OVt<3ADa)d$lD&{hThdG@yveR>oDssjjQk2gYVOp3jz1B9rLvzI%@IA9d{R-?FS z$3B420mm^uXY42H2LgZQiGoiC@+MImM7^M2uwP#Z)xWw%$-KV*VUsWZ;^?;S1Nd(F z64%$g1NW`W_u1de)MK5OwqJ3X^?mYw$8~vfy%z0HkKBENvA-N+f&KIcDi{bb#c&wy zUEnV4JjNL#0{847j2A>=q96hjxj`69^WtmP)Dr`~{~Wo15`LBpv6Qq67|yrL?kjM6xv#+G#~}ULuO(b8k>u+^l6JoHc&6dN?HnWa zRP{Sqt7}ArdK)>Q2}ZBR;`j_NVo#wie60r(A1F=phRS{3)CGSSydE)Xw>mM)X;;`0 zr-R;4hn>#;Risv-zvZ$>9QwT-6rc6PDe8bJ_qCk3o8xt~2Z8hh(Wl1fpC_aoP(dCj zalOsl|2AS+@AYC`*!>85ic0huY9rEGV2~>Hg0;LL_ViK>M^I=pBGrLn`!U?S!aR^Q zq7?Q7@)#d9MNVL}n!FOjUeNh*&Yl%R@fn9W<+tB_!}yCoS9omr6z&w~wXegEJ>Eq1 z&*}G>@6+~0%-7;Xf9s!xTf9#Fv7h5E07n@(mFzP8Sv>3)st05$v z<}B_0GR|0*AW*&$;fhVL<&2Ix{eY6hV@eN*yRt-x-~=z~fhUxx8^<5^#_8t*ZeMyk zFvRThpoP(=az6Gsm{xxLC1>n@!~*iqdPsS|j5u7~-3%`|;>uVs=^|@54kGSnyhp#C zV|5#GtOa@;ZigdxLR{D*@@4;4%<_ntJ~8uiLt%&xkqNGtQnJdD1#c_4Oy!%;r3I+hhG_a9;f`TsFRj&?6r*&zg(E z^Njgv`we!x$@_}5aZTp(8m!;RvIew;wU}M#bk%1(XGPl;cJG9dIP!hQ4U8AZVWJ@y z6K%zqXuZkDIT$aG!B~MG#`3%|p6^Ru;QMZp6G}rdUJ-%u$|wr2Wx@PoML6>;q^IvrfmHow-2L#W5r@ z_KV?chx?&sm~Y^Y56}2<-rEGP!#&b@KG72tv)%!WCDRNCkZV4GJkEpUTMse@WUuJL zC}YTRG}8XZ+i#;Wi8W;HAsBv|1exES&sTl*H*wbUU&Qq_AHja*Yw%#bFGl$@_GK5M zn6_VI$@wkTbO-#@$@#>d@n5O&2J*s2>H%v$zGjRCgzYoFizW7H#J40C<4yS(@2SE> zUp2-%OEBJ$jOQ;jf`ZFjvp+v`_-*=|1@-=BJ8o<+MP&!Y9wT(iFDeA<4##USGKn&GnZ zCg*W7IFpyoSxnC5v4&&1G=qF_lQY=dt6Y%>!@ot))`iUrKJfg80|XC9s2>!e6Dl*k zZfiW|HG1Y1f5<&~1?y($W8aZ-U6}3(Rq}%J zgYMWp(*!G~X=3&LdXRa_4kzCYh0(G+IPPvBX3RO*|MOO4jdoim?LHP476(J~W9rzO z&QO1s@x$y;oc$sSr)Nh%?~@Q{edv#KUwOl3i#u$$QntCne4{rEmIT6RWhm@+#UMZ@ z6`raoxV$nJ3cq*4-iNnA<<*B^vG^5OEqMhFYhOkPcR177=P5E=LEGOZ?dA0OsG^lQ ze-UT4GWA#oG-mzhx&dpujQ2xDoKNhNy@(ZKyjo)0L0pC13+&tTG1inyJ`lL4VVw3p z-c^9{e%ktIJ@M|M3}a$^l-CjJK`$l;sxaP6eJSx_T)aHD@Tj>I}t~uR`GkCn*2z3eL>4!1*PnIJ@{V^yZr5_p$Sme2e>Q1Jd$j3#(sJexkdB8j+>_dChu-DTwVT#Pr8;~T|&>OgA- z#yfbgmmEJtjK>+wPTVxGucR8A@-vQX6R*op*K*!yeHCZwqy<@xBRdE z7@WWOwQQ4CNuF8B^c<7^YuQGFNIuie9m!hQuH}p-=khOoDTI3&oZB>8TFCwS66v{0 z*4($V6mDtZMPL+teN~>pFkfUPu@=;{=}5{ zPjuv9vb%^lmtvx~l>AVL!P;c>m4=}=+XHP8rfBqFPsmZ8{Ii#_r(8vs@0o=I8zhpp z(EncE7s`@l+{LpLyB^NEG?bWrVURl&oVnuOtvN9?61zCGe=)Bu=J%|)Uv0r7C8)En4m$3b@^Vs;XKGx0B#-_*Bu=9nZ*!qGjHvi=?Rz0SSWe=%i#lt7D z>d})}IqL*gPS?PCi7IwJ@&=B+JQIpfxK1_Z73nG<1Mb4R}{Oq~2<9wKS@09XE zD$n((G?8Q9jx~E9GYke_k@Eg{iiqRM`?URudUAUcW4Aimd@bXEThxhLj0NlBr~`@g z{d~`@cnlQ5G#J-i-yYW8M%QJU5^S!rCIGD{3-!W|a^;z1sB^3T z6|8+wi!r1gV+c(wo}r3)_aDQ;CzP?_Jsqr{dlqY#8ez?9TddsZgykErV(B`2ELv=X z1s|JY>6_=U;msA;{pQoy|MG*7ef55v{Pfqjw&OM8P5iBPQT8C~^fDxz_zDH*cd_Sp zmi^ny?Bg)zi*UkNqURLvGv1%5CcocGXIxLbE5oLwDxCR$I41afw7>^l(H5w=u8!Ob zn~ufiQ!k8+#OX1wR-i3!B&QQI5&H=-p})7}nR@o?DybU{tO>LeZ=RYhj4^gTaREDiYk-wAPvXmIhcQ=j1dFCCV*amG zvFIH`tXzK$>l6a8=42S=tNY_)C3n1Y)ETeII^e|vws>y86`tK^iRboO;>EpIcypgQ zKHaHDy^zKBufBxrdrv~=i^uu*y^Gv)jP*_}=h+i;kfz5x&{~_e&;G5iC3}1Jjz-Ns>M!xfcB=Qw)aL zi)BuI=oaU6^QU(5z7pI|%(#;w&Q)^Q$nzPPZ+Xac!k0E5z}ic&^3={vnAo*rJyu-B z-d*r<)@HenBW5;zS=$l1Ao4V;wWX~8i`~$4m@>XF<$loF*CV0)m?z|?J40@o6ZXw8 z$HJM4_;ltTEP3DBDw>Y=@_)2fy26ir;NF!sFYF@yu>> zys^&=3wCN^*XotfU_Rim{dMjbe}HK2_}?^AK!c+Jd-i6uee!;!Gi!3*n8*%cejY`v z$@Q$`Ocan0^4&3>bsdATmZ)c4IQz^R_L06oFl$Oa``@PhyvlPbULf9o!kI6hg7znm zGp_wDPJj6{XCppC^vR9xsfO~;+R4Yya@rg9q~rFu=dW!_dXeWIeC4l{demPrYmc8y z(N|$g)pO$YYCp`fZ2Ch}de5}V(6(P3`*no%OU5!fX5Mli_RTaJVlTFnx?jb44d#8) zJ=_fLUuVITeS0hJX*h1?86Bcl$9ZfqLm$Xl+)&ov1?~|i*pENSoc)eO@EXp0VjoA; z>BTOtsMUFrH=G$aSg)(#8LfHT4a^`Xr19q@K<7n&$j@@ZVTnBsPzM&>r-=7wZoyYG zWwGi(HDZ4POYS4jKd6L7ADqVqMSrZ+jl#Q%Zuk>1{KH-gJa^0m?-)ekONRn1@v6l# z|3<6`YQ?IMc1jyo2DW0kUjyd5l;T5^WW1>EgQuwzkMF#UCw5%M%hZb{dvqYTZXrz8 z{S9GCt7*HMXt>H5E-y>^eUa}m=1XDiE;|$x`Q-W>AB<;GCs@ZFj50x^iz@xb8bm05 z%Ck=1A*a92xamchF8edi&3y`5AOD8&%tJWJ*i-J+2U-7k8V+j~!F#vrq}f+yPUqk9 zvb*@9U$Wu*frSR|1vZ>}Be3zp8^L`S-U)`mJ0X(>Z-osReGollJwLJ6bxT%$jC!?B z@s;*Bid^dMD|Gz%=Nz{Lc1hZTx*zQb8cr#9;{2t0EB#-Yn2VwB=iVmm--N#3lxOMM z@LW|V_UzpDwo19*UtxflPVHO??4vZ$YCh zgR486$Oq+|3(3NTuM)ZI7lSh&2IJThS0OXQ61%1wVgCJccx%Qwd^CL<_22*&{$361 zH{*4?VVzhX+mChegIN2u4Dm4~VG!#n8}3}!#|>aj zL=ToyA3nCo!1Kqw@Z=r~JVRahP}U3w_shXXb_?g!6j5VufG%(L?nBwri=mAt`(ZrY zAEPO(^~bTVA8w4M>uSj6PFMJ`**vS{J=*#kJfGr4Tw3@i=*@l{YVZ6C@~=FAgU`*t zzCX@_>@&|m=}&W@^Ji7)K5d1QzrG6fhp$8ZArGiMb@pRpbU9nY z*}OQ;*9Z}FHsX0K??>xU7qrL;_oXL8@Zvrz{N;!PJ~dCns^B(kh#kO&xIwHVrW=X> zR^oafvUbhPr1Xj(fL2DXLWYHdjs71$Q9g*ylW( z<{h3r^(rjaya0m*e`3w*2`Inu2oAq+KXcYukon!8ap;kSIQpO*lpipG$_&;pm=7J3 z2od#?J2jKLIkI@|j)WMO82|r_^;h@;k|G~o~UKbzGJM|p~JNgx_rc-{&-A3cxdGf!at8z-?*EeM~f`rz5!W_Vve1S|X+ zi8=8jhU;nTJBj%T-)3A3>Ojnmb`%loy3k%Ug!G_^(h%H(TcLf(^Y1{2e>)t4I-wfS zjxA9=Sf4nAbxHg_)P>c7ZTQ$I8Gn^`!OKUTuujDm_9ryBccg=c>t^Wiu|%8OInHjX zqu5vmF{%sTwfAjY-9p?~zkqY|pJC17amW*QnZMqTeNR1#gTHwXau4sJ-5Rj|B`|j- zuB?fOx(H)mA;%=H(wdp-gPu6?m@o8LJ30442#nv1gw;oJaG0BhYfE$Bwz>eW>+;~v zns_j4-ifDLIqTfYnfE5nL-wLMXxO78rtep6k^Mia1ARe5lAf^s3!Q;|XmD*sfk_$S zPiFFbtyG@RL7&X{%!Iz*g0*@3%{-?`^j!{dPe;t{s4`wSF-p51r;W3xsxL6-d@g6R zQVefP=k3!j-A206ZKQGbK9%>94Q_LepZz?(H=247PQM^x1EB++?h0tLx6D9v<%>MU2*M10vp3CEQ$Q*d=zI_D2E*nci$9lDg~o|Pk9 zt&->ERUw8FsZqsVd=>q46=&xv7=M*B_bwqWJZEPc^+L871=hW23mO~gj2ha}9Wf!P z3u*m99cT)zlhj9)P45cpcWLwQM$Oem&ST- zSN2|gj&yMctDiCcF#S5`wm7#VcI>3sb2dAT^I2lv_U^o1t~F=wC^?q5IfG4o;QX%G z=}0(7pFq9{r|$?nK1Ta*f<9-&l>g>Nzi5jczc$3`S?mwIWQdK#{vA12{PnN{<~tW- z6YYIn@(?z~_2E2m%cV_sL=H$X{Vv`=^}IJ`MEZ4Y5d%mjM`(p~U~A$awxo_q^y91#LHs)-+9jQl9gla1_GP#FcA&zphG(tjAo_R``}_$AQ=rHvBa~o;x{ z#GbyIJbjqhdxms!R`+*l8U=8|@kbQ)>-+!95ZhPWR%pbW9-M{$2 zoVx9^E0^b&-$bZNDfb_XStH43OqIcWJAv3n!*E_W>(HS%^EqX982eihd>)MpbK`Jv zK_ZM6r%YkJg7ug+JXdKw&&}G*Gqkoo8&;!+l3O0u>4H-9Q&pr>O2pF^BdiqcNj<>RU3%A{e$>V#wAE5;$jXB zgqEV-wE+c}i;$q5!t=G`8OI7qpe`iy{M9U;#ax7Zqe|Q~t><~%ZOAa_VNNtizdtJ7 z!ARtsrQm(BpHslu9kIVr?aNs!V%;7)iSCq1^k+_DFn3D&vZw9|ohZ3B0oNlpp)=c? zy%-19^Nlzgpo8Tv8)5G8FuZcu8DCjv5&L0mOBunzs4gVX*9)9Fqd4c`nvNLDYkd4) z>%hJEei#3HU%U5lH)Dg4;4aA07wpI+52TM`{`CsHs}qg|3KrP%>a*DX0RxX!2?U_7-K7r?8h2zYp0nmBx2DIPxhwl49(Em7` zbI~z8A0df7G@h5YDu;EELR=x%4qJH^CjV^%7lHNuMsjZpYbX4-t`2vQgW02F3>K)& zv&>YOhjWfTRDGE77HBkdmR17KTa878QXC?c6L{8k3KDg)k!4Vf!pqetwW>$um1fj9bfC(ihv#n$ zFeV-$_G8l7-5l!)>As%8ztU@hbNmx%iy`(IlNc?YWG#FW6E&1-O2q`mc&{yL1U@Qh zIK9vfaxYq8%R{HJ^kFS5*&B%06g=_4nNVz{z3(6oA4%>#l4p>3)J z?2&UW2yq&5wEtN4z+$C55O*R4srtFdy;zEy%ni$J>QQCij9XXR$Qks#9?Y#g2T|+F zGdQks=F*YkG$FNV>E4zbcXNFw&`d7s&nDg#Q}~Y)`{9B~^dxg0{~FKrI1+|StKD$y zH4ChNSR0GpzKl=R!|}3`2Ugyw$1d95fwW-+M|Yt+x|`SwTOUA6NG_V4x1leHyEHL< zl%ad%=i_`=2Y!efTImaHBf7Ak`0wHT@FxBab1tRfE5@YTpR<7aU!7pPA_QJCNwn1j z_D$)tKMf+*KG67^H%`3nM|%&#=}#ib?c{Y~=k(oEn6sWP*U>U_)CUU{O1oR`3&~3o>60SmiwhT+&jL){&$#E2V&F`c;0?0GS229pLt=4 zRSjdrdek^Iqt2xr4ep(2_U=LZjec|n4WTP+44vWQXbTxfoga5`h<%~sIOq2{(<^54 zxtm)TIL^N31p3GaeW~2X@E=2xeW&CV|qt9Q$razp)N|`{suIz=`W+~W7&fk?j z0)v=Nv_`cN`%X%iR0o=ZQ_yyO7x!SU5%>N(GQg{lLCRfy`1!gZbl|Q&h^z23#jF=; zrVLvrhNC8^w#*`$La)ne$&TPG>L8 zVio(R8+fkqR${)BoW7U7`vCu4l0$;er!Wsz?IVv5NcZBRxeFb8YJ_!@QRd9tjU>j& zJljCvo60?CvEP!$-IjFv-E`wA5&O;JaRKA1_Rt9orH;ot?&`wdrw>wHU@Q>Kdc%?YaU3q1z^=qTd}Wcto>Kx; zK4tCZFAk9V1M|E;xk2p}KXN?dIOg-0m*uk8!4AnxVy`=OY11UM};5n zWi3VIQ-jQ>hKM<5R?m$PXMR4$W60o6OqLn#$dbE`R@`NxkNsL~#AD7#^Pbq3&$1AE z&J(!FdFRe>a%|do-0i&aSrggc;NO!NB56%do-vXZ;5ZcLfhIRo{~UdKGIk=Qp;nkg-)@pNQQmb2O6aeOW~VTv1iV!DaD z(1m;Tp&z}mgY+MF_2Dizi0ij){=3hmd0}`j&X5C+-kiX}f-x+2tHL6~ENuD21^a(z z1Es%uF}4#NpGMr7o3UPQ&vUHA`Bl!Wo4V}b8T35U$m3uKV>q5yDc8$hIcKby`-Y$7 z4!rINYj~VlHylGIG0(B!+&b|sv?rdzr#drM=5DNrt?r6=E^;lAQ;Yppk$VXpDRrj( zMNJa_v8d79{s%|0hQ8+ip|l{$P6(Xm zzLg?(kM(&jfeWgv<56Q*fm){q*1((5bFfT8sZ?ES0Wjwo#`%R)>?!wu2xBWbmUWz??r}F%FD2RP9cbdXA#JLyb zdG5HSaMnH3l(^G>3)}_fqJC1vU6>l?Q@3tV{6$`UNBF$jXKJ@m=t3d)9`mnohthol zy>X2H(#M=zGIOW(BzJs`|7e=O;Qr4>lD+$Tg6vRXrN^08Ii4+WkoX@U2gq>0{1DHg zQs92MI(HmS@r)A_&b@n}*(DyW?!{>Jsz--kJN-WGE4&{AticZ@k8%ziY7wn=%Tm^BC8>>(&rPC~YTb)joj*r=6-lXIBE zaR$}0j!%ao^NKp+!9HlZHudT-pKnxCn{NQDP}G$Vy;@o-2KVq z|IAUfvCMDh{}UNEB%|s6TSpSzCpvsBP-3D%u9qeD`ziaV2M46jb5ARB|KTL6El;D~ z-ViO<*!OU^<=GalJj);yeSsP13$4OHR0oFQ2Qb2V=@{`J7jZs0V1)P&kQ2J2M$sBH zjC$`u;@?mFdy#M1jf{(;kDS0BnI%--IfvcPXkf!i_VctN@X>`>?9Lp`SJ;D3TVPw zvtp=oPWcjhkb(EH9)>7i>PZL=?5q_1@nZSF@ubZd?wd98~ zSCM-$?h2O1U?Zd1BTGlKzsdbNqJ1Qiryv>2ykRz+a2+ikMkq8S_iG&>_Ir8e&|dDm z?nj1>EcafOP->#Zxu-K|aJ-1t>*kz&v*o!3j+}vU!C;6NXW#-c8XJevM9ws))?h5N zownbP(aa(Go)PrNkI?^*pfzxa{6D~Rdisce5Ap9J{@nMGjluc(X2kzA_WVT+oA!9& z9rkt?*yllp*dNN_d0(tg4aVdW_X2W2A+OYh*ka;ez#NqOw;o6NIGflDUbxE*HGEGU z8UooLGtlAr0lUy2ZHJ+xF!YBRqRIaX@fTbn{KU6?#l5mU@UO=*ebWbHucG1FdCs|! z`%mscvifeRq@LWzvqcW^TsmdWJ!|8Z?O8Nky~G|j@%N(r`#I9~iGQdEcO88(5+BID zG|oAu#$Yr(kv<@uJ5ae8Nh!u)JbiXVJvwP8&0cM|<P;&d<9{ z{PnTxd37w4@y6?#fmr2Ti9?LDl{5QM7?zEp*h~z>W>a#Ae=e{2j5$+L@1nrp=@@az z;{8(MFQlBvs7hil;>f93aw>M1(uF?a zdMkkE%bRNP`&?okT|}N>pMg0*5@QOD>ItaUP2zAS=LfFUK!f%CtGnwFpwh>F#xTzt z8)YocGwQf^T6TRxYRBR%lcKAlKFWTW%~XFZjlHmB=5r!{7x+u_dFn$lbs&@RU(r?i za@uthdA>{3P+2z}WDQ~{e@a9>wJ(i(?Xk?I1BDM5r@x@jB~OfGj~9*P4gY>5claCp zvu_Z8|7XWCeQpgz*)#Uj<4pTOa{eyb|4#b;T}ad1N8f)K#fGXpr&5RK6`tprWk#HN zH|P8d@xMX)46y1K|j2)s; ze>D-6w*Rla^8j!2yzV^|rU*IS+9Q9dL6>7}xeUk2tCtV(o2 z6}PoAosq+Py@D><0ru6b8?s?4vsMaEA0QX{40}gm-@?AYWhh%KdT%UOON&SC6IZ~Dl#jx5RgW~)3FJt*5fnOF5P zGt8)oO4(*X##<#HU#I*OvDhF7yfbcjULWN4)fCS~1%1gS*!qg+E^iNcbnO2eeEwXx zunO^$%kljhgBAWxQLClS0It7C{XReav}Rk^QNnm1lx`HcRaRurJFJTXDKr@>0Z{ znLvDw+UIzB8)714H2jhb1&2x3`9LZ3+(qoEXXUQ|zZ0kA} z+VG(eBG&<8E>bc> z*$wSrsF&F^U-F}SWIC$`Je5yz-LFvggL^+TgeOWvm=`u}oEQrSc%8z3lIN$SBWq0d zcGzUw*ph5$v&mC&BeHc@k6ggFk5YA!aF!-x*JpUp&%Td-8hTKx&bVV9B}d4aP-CCt zP5#V-SkDgZKsYaxH`D*F)|Po5BgVa(-s@v<8sNwGClSj*&zm{x3Uf!dik~mh8@MD| zCwqd&XL)`&#VG^C-CVN2YH`t4r#)k@TYLw5b;nkppj)U-c-&SUFTn`dsIZm(RIrytMOUWU~{s?$Yz`p?eOW-#s zrv|bz3+!1HS@gnXxa5K*H$lu9amd_gnTd~-$>@vVA3|?SkaYX|GSk%yZm37)(9iCX z4L`U?ezEPcd}VKrJYO;^Z+0!o(Q4)Q=YW4MnCG$bS%s_;8BMr=P5QLVr=169?1Si54TTo!>=&q_hCy{vVY+7$v%z! z+RST|o#6H>lx*lC4t21@DjpL{vbobPs}siL^*0-3-}k7M{xNgtehFv!gWdRmJrYC? zC-@m+`_HPLRSN>#ZNt~nXff4pXFFrnTbQk`q4(uIde-XBKtA5zhV33`X19Q zH6#D)6Smo_qs5SXQaaDREfro*q5mHR|HqL1Pr^I*Jp86>>2KcxFP)c+g&lye4t*JW z7Fo-AN&)}OaAbcpTqbZFl!AW+eX5n{0JX}~!9NwdAW1Aa@nTMok-6kZ@V^B9q2M1R zLm}spJ?Et1@IHxp{xLcB^B>5Td+wHdw}i=875=3&*w{Mw;z!SR=6HpK&5FOxl?>;_~*UM*PHbAGYm6Mf*!dRwM4 za(PVo6gT{eIedz$3x3iY4Yy8F8v0_nY#8KchowDhP!4ok#cO&=wszU&IqHd@f2IH~ zff!l;ck!~}lL@lv&Lr9TxfI#{FR9{5U+eDgkQ4Y}z8w7-F@j%K$|-v4&(dFao__Yg zC)EC6F4`-DF8tG&Z(mG|v=P10hu=4YoxcFa3j4)j)+B3oQI@9J14C~7X9|h2f&WAX zIbUM0_Bv;yrPlJdmSu6DiJ}A~0JeWaLV&&MvdktNm-f?dg8$=Ey7Ljv{wih|Jt6gb zo)!I(wea0+#_!vO?%$8@$Ns$nZ&@OBuBod2k($>$@Gqj*y(}5r(^zT`q@xSKz9iWt z8?4!}VoJYE%_RBR_)wXO=FEosBe#8JAn1&=pYoBUH&*j~+$A1A`IbDm{)~KOPmVlY zFoBINvaj7FGr~zwXEUi}cW3J@!Pvjy`_TP|>3dOqPpbYo2-$y0 zZ28fmm2m#c>2s+>22^1OaBDA50{;Z?zry*C;{0F2U%=mv50dE^f0>9x|6V+UU2&4% zaaam>ye8)#y-W7}?5pzh+Wqpi-P!V3#-K3fTfBR$GEkbs?}`U=aLz}juoo8c$yq1j zk6_E0^UyCjX;OM_i}XjlirhgyxpYMVw@O!Bw;j}eD3!UCBYYM(TC=cCTy}%%C;{i5X&a- zM~(ije*^Pxq{DGT&i{M(ICq2lkBj9bT=*)-AMhC5N2@!L``}LPOBx&p`FqXSgm(M` zWy7nyIJP}l+S@t%11xp!74{BrU*y)t8b>w|Bby|~I!^A*NNhl3zc?Eg*Et(*iTku< z=$!iO4?CKZ3)K0ajz27|C)a}iBU0)C{+HT=^J2UgSHzDD}bZ$$Tdp!>0viL=aI zJ%#L7J+I)OL%(|=Tvpgen(Be7qW7hm{%3BriX?1;c%F|HOD^Xq^OBg-La_mY#gKGC zW)gzP8wASK)j;BRLGbt=rk4CcIse!y68%A_*rIC0Z$Vl8IR;$N7s6-@Y<*KG&2AI!OWNv;}`1M^FA$h;^Un=SJC z)oJm3s$6`2R7j0hDZLET@HxeQ+fTjo2sN+p-|v{k z-ZzmGvrs!_C+0)G{E8Es7keLH-->;&bi6v}&Uu!d1@^85{>lb0SA%~JG4^cietbbk z4xrCwx8CVs?%E1KkR5p0{=+x_n~%q4fsC{{`bRq@E}}wtLSfkhM4~v z<`is%@4^G!zn8w3!^nQ{PX+(1AY?!I7lA*$ud4UiS-lK@uI**YE{wwm05g`Ql=I8u zmO5m&K3^<76=EH%5!+C;SoK-bdVZ6{y?U1%c=b8C+bdOm;9o5pkgZ#WndQ=05B`zh z9>r4oAO;+xWgLE0W5!__Ack3a=6Pa~yWq&6-wxSutFBP{LD>%Gyew)KcH+-GOUyV1 z{EN{YCB#myN^Rg4e1#C~27Cnh#O`LX;TGyR%lJS};tWou>n%&N=;X|+v#%D{4$g?F9RDE4DKqE*CHoys zi~aUS+np>0`#b;4^@%!jL-L0;eNv&l`WpTxUv=SL=5Zgl4@m8vRm6Rsq4#|awjcP1 z?L_y3KRl(Tq?5>gKNp{&Ek7K;FUG}nAe|*Lvt$06MrL==i^ydd0y#?M3%A}uJ4C*;)to_Exh3J zU?yiq6z`jd9!QsRe|Y(?!s%C<#-1o)U5_~^+d=sjRXkQL{n^yQ51Qn(hHpBn$+{A= zc%B}VWA`zOnptN7U?1{CD>a=R%sA<$_6xrAH3M*<50mQ~CD%7W{&R|&c>~x}!+6LH zS24M8a-;3UZ3nJ$_Q7%<8_wDcme|0H#2FS1;J^IrEBw(vYJV7uoy4WF`?+<={w8Ox zrQZAzOa0Atzvj9`-9kh1M;xumh4z{#>>%Vn=e&a1BV}slfY<%ZB3cFC-D=`~;2*dV z+s}iZ$9?F2@K1*CB7?qX#ba3*$r+F3>?8XX=5^p*kNww3|7%MgeXw9YKrcMJSC07} zxGo0ax?tIR#bIuPAA>VJUM1Qf@xsqzw8J4NDFmb?GnzpcVYi(KYg-8I^Rxo{GyWm{7&cMwWVwTY(ZNydj;En7X5E3bJ_hW=W{|j zMK@bD_0HK>cdRWbth5h1+L8;jdi?REBiR4s`n}zAN28#VOkxY_dCPR(9P(4)ZIO zvj5xl%qwiBSFTM0A7SP-wR|!7bn&jUKSs^;gk{s|Z+R}7dTl$Bc?I{IXL#!B5j!Wmc*Wo&N2-$xhali+#{Z^6lTTRYy4cs^29}fO8 z`|$hWGEX~0ESKKrf{Xb4$o?vPzdFvo>VMUPeJA`Deej>ad*+;l+h_r9BW)7gXW_Rp z@R`TN2``SFpEH(3le;@9%||y$!RF^Aam}Ojy+0s+kF1pK|M5Nf<@QindA3U4Xf}x_ zw$IKUn-muiS0tWi&&U4Azra#^;a&DazVc0!zHsjc>rMM1Ru&4v@EZ;hCk>L8%V*#j zKZG1XW{}6Yp(~W1c|GQU?U0)#IVH@mGih>h$ss2u*_YHKo(veHHou-3rHyc7!HIXb z9!{ioW}bB+4|=Kn>BsIPzVp&JHC|KH^ueX-f$ZPM**{9{>=}zR2il}FTqC!S|4vSK zj&rQy`;KcqpA*hHCw9L>;m_V%RC)ma-_k~`k8|G_O?)}TPM$#{{)7I=w!JWsb+hfK z@L%lA)-84Aet5AnwZPGQMds5_N%#5n)c8Dt4q)!ko(Hh~R$=?EhUXakLpEajd7}IG z6Z0hpk`C8Z?gi{V_{b{YvaLb(H{kcT(Er{JzezXTCjFd!xX;`iIIZ|Bt)d|d?Cc9G zGGe~)GWBL>WH4COm24-@xSseUbw{3SrI?zNtj*8E0rv}e^L6_B4;9FxDShHK;uJ6T zz=@_w8O@DA51hZY4+6PWYx#aqIzp{w^5NF~P>k)62M(r_Zu#K0 z8{E1=?FTDyhk@KYIoD&5P~>lzrpfL$yBsVu$tilY6o;vrKd<`P&p+5B=kDu;14pes ze7yml8^$Iaqn~FI`+pkVlv(zIyS6irIB)q;8?{~V7hkmVnKW`en#`6W&+8pxX{Vm5 z&ms0PWcxI03cY|#oTI*~|0?-`Kvf^6xpGY^&xHPPj%6-9I-c<^=f5>i=V&Q@zoRoX z-l2~dQx4w&GQMvAqhN3^GpLwBcj!@Yc^2FMHR?S!V*7cJ?>&I-htEFq9Ckl^MB!bG}yIZkm^U{O#T&4v8!$hD$C_EARt% zwGZG>buA?q-21`yEBoP=y};+ORg#NHKPwYyC$V9b?ZCpXSgtGFc0&Srq9lo~?U6ua ze%z8K=O*m3ug)sI!J~4L+HKzlJFxq@srO-);Zpy_jT>nk>?f!Lo8o$w zS|9^HAe=dHrzCE)-~&nwgdOugrkb22(a>m1H)Xh*+H>c|8ny9B^FId~$b=0Hi z3Dq}IOHH8S%&zkz$BgeVn(DAj>?yodwtfm`8- zj9d(p?x0esJlZc;H}}il&3gIi;YxYzYPWcgYqEFNA$zd7QdK-B*N;8m3+C(twHN5W zcdd8!f_p#Q*bCS7#WmSb98Rqwz6ATghCH}#J1o}~Hui%tCswYIKRPpOmlUv%TCj_c z-X_8D2l&1U*VX;4oO@#ZkM$zw;iG(Jh#K%wYQ4tk^_iq6hhAwl(~g>-gtw^qqGv

`)cr4wE~U)_zv;d0%e-T-eg~CZ@iK7ccZV<#T+=RgIr8!>&kdt>qOU8gi^=p zDNRR-`R~0SS^OYzzsHIDJ|{zgYw?jbq5Hv~c!@dn1b!d9_J!Df?zCXG_*<%*`1cm=R^G^&{PrAg&#de?)+s+bS1YenjLSZQLk{2v?ddj4ULm=lclLnV2iMoK{h;-pIRorSSK@qiu{@4TOz#06nFSQq3_pj{*B_rNVR}>->3Y1*9SvJ_&53}(5 zWR-&bxJj}d)b420vib%;UfeSAN2BY0A0ywJh9??LDH ztA+gUN_LnqxvmOTx;wPP46_@_4d2$`d`Ig$pL$JwpyQY^kxk>^jA59 z$H@V|gxqHiHGIZBr=N!B{w281*THMOg?T2-Ga_GRNk1tzzH56iwmz^bZXp{L;Vpj$D zFL^BG%Oc+v*zdL$rx=KXrH9SRV@Dd~p~yDz=r+pn1&16pYqDq1BB5o}faPG1vIpGy z;5zopZe6lL?Sbp--F1b%AP7#6Kp7@y5nfs^XQvk=S7ROp*k?@H=uuxms$2nuDngqE>4l zQQ7SLt)=cxx$`!P^;5q zYq5WiJ#by@-*mgsUZ68~6#b>vmzV48N=5eilZ#zPuKx-0y-za7R&m_D3g7j5X4h?n z+nSkH$%lyfpThRLhCe#fjX&`og?|^kXT9K#?e7@G_8-Cj8*RY{fcJc|1H3!&`?^^@ z_$|F$55dDfC)Ni1oEorJi~F`Vr#Se#9oYL;_Wfio9N zEuC|6+Tw7n15=uuY@U!T@;zf&!LEJaSiTpo%Yt|G1N&n6e!zCn*cV!!AKzn$l$PX4 zP}jJG65}az(7U2(k}|1@KY8Bd0%lo179m@{a9Iv~w-TG4UcHy$L3`7TZZ{+ARsXG9 z&nLTP6sVpKcWrn6UhF#`>U-g$uQ`jpCcoDfu6pCB)l4Vm!@f5n=T$sM)pR%qoU%yV z!HQ?eGqepm}_8&q^6D;cIJfLZ-f z5nHD}ff*kj)3X1|#q!4OG4lE+ufWGuC{G@3k_V$Z$h*wQDKI)`cgnE^r|ciWF0UVw z)RJ;(%Zj4*<2;z26SWr{1-C5qnA#&eZ_M_S_RL7hpi}tM$Y=m~+BD&vWN`R4v~-J+dm#ui}0x-wWVei6!Kx?soMr(O@`LBe5Hc+C|`b)m$7%DJ0IufwasD}t$BjX`2Gka={ z%)xbRjA5Qhya%(*_QG#{1irg7=u|bc0**6Jgx;0B_;W?hr4J*Jmc;T`f?*_ilXo2fl;4$MxOoZ>U>5TC9&39jsvw2E3O zDa@9}tT@r9MM+C)gfyo{N>kPqDW;|%xw2Zqs@mja5Awv|lnCN08BT}fW9KEzv!Af_ zf?DWJj_s7tm!l=(i6FW1tiMD)?hkKagzWx$y6pQ#5p&Jk$?;IPLm#%9Nve1-)SS|? zWA0g&swYd$G-(M{a}B{A+g{o4qgmHtz2(ULI;HRUPSn{~=RwJS_1*Bj+=Rc{59EUF z{H(I~49Y%a*@y!=!Cz~0mRK4W{)%;z++TiS?XJ=-wAX#eK3sBH8$t$DQVSisS!N^O z5M$JOW=(B$%^|WTd5JA;KXWaPi9PqMXoW%4dR!vMtK>iaUt=ovKlWY+zCSv^iSAcA z-=zbFl@1^Vj1F)poj=(P|LwGBeGYEwS5zr}5rVPqv?82k7Hu1MI(+6A49~^!0 z)lV3)6VTg7N0{|6V3I?9MsAIAa+J8;40b&G<&t$#uJD@VCHM)Qn#3SeP7Z0}LtS40 zHJ`qlnNf3JHZw4yCH(PFiGMCw5?{U`vCjpG-!H@I^HqDG1TOSi{J#Nwx_N59(eJ+W z;siTgJshoz-8UOyL~j+!3T1FO!w4ZZCkkK^p-`v80AwX?6{`>wO0bpLhz zuf8{a#*IJkQ;g474E_!7_i(g3XowNrT&;Vi?Q-0g-`B>_2jL>iknr;EMM+y08IzmgxZO zJ;%5n*-xy0whyc)IsbO-8P2?t^Zadg_VGMAuOB&BeiZ)THT`n^0-^9zCe{)r~ zeKu1Le7l%_>Kd@uOWc|+=E9Rtd!tYMp6-?XulC7X2L|N%iv#jP)`)CsHprndoA_cM z27_rhvAS?@y~K*ZhKqD9r$lod$GXb%@lNJ|s(nLj;q;(GjY zJT7~_c9nX*TnT;jGW?98aP@{t23#~LuLaAcM=#2WAEvVpis5!B<+JJ~1{*NPi=6IR zwWn3@j;hU}hBuS*j;vPo+Nx&HrTcpLF8Cf?n4|k#G9Qdx=U&N0h5NjFDgC&}&nWH$ zYZLOHGo$b~;xlO2gN_cTS!=i7uXX%-*?)ZxSbA!7rsn3`Y!em7v@!ZO8ZIK|Iq%8a z!QE5rnR~>Z?SqayE{;OZesKWUBm1lH{p;eWFCpiz&tRTWE@vP8udp9bv0r4r!hITf zKL_sSVR0;g{fyOxy^}bm*3F(pcMZnVt4QuMYZH9akJroof6J1Mf0rN|;5^&;`E0(^ z5;$k;;G=A#PoM|>lzvHli@xdShw!_Hh`Ef3|7v)w{Q<$OS)Q3CFSP_Jg^1E3qYdR?g=3No07jL~J@QVXF?v znR^b3=NFDr%jXXlUIrZJnG*GExFoF!lg#xOC3oXR$$2}NJrN?lKTmPZ$T`exq$A(2 zkf4XUBp(02a38t73&is9(`GrR^OYJk{KR#e!3-PMiM@N1te{ZQ&3xB2i z(Fe-*S2|*W-WVg_;XHny(gTiG-mAlzs|4RU6WhO6<@T}rM%A1nY<}`S zP9wOJ`*B#um~ApCn!zNtUjFuaeAXOnUhjLj9k%Qk#m(8DMcF3ob zWY-sS<Ycd*D+ zrLPU)=aZO`R0&^Rt@zwkCO+RO6~A9J!y!CQKlcKn~N?3LZ5d<0Vs-zWZ`5#QO04#Dp4aCT^4cXm2-i*4_2 z`!C1-`hDkYz0N*S|2@Y{Rfl5)Tc8ddUvLPU`3PtJ6nwUR)b?Hgd-Q%Cav$s+VDIQc z=l7|3HrRXP==mA!{5f>Ish|A*pzBsM^pLmcrdJK##aL$Z1<=#J7mj%9UC;jzS^1+} zzMnif^t}w(`VR@R^-ktJd=>7aA5<`BfI6FJJ0#(aer8Y&N!GRzc%?_-9v+1UX`CGU z6x=wo@F2|*Z!r;9HN&sdARb@If`hL@4&PfTZ+|OER)0BCp1=EwY<|2{j&ALg6U+rV zv5|QtYYW&jm(eGuCGE9ilK$#8NqJ?BB)_s=5?|TM3?NU5UcFa>AMg>M?;epI|8koA zUaVYxu>?Gm*}I|OA0{Op7pa%O#0rJC|ANFk6T`kOg|AIf)NcV&{xo@RLpG%sKphh5rKk zz}Di>J36$V*y}}S@4V6La~e^Z;O$#+Z?E;-uZHZDJdt$zx8 z&lh|D0=Yi)egpV7D_-;K_^Y#DiN9XU`N!|$)=oW_y}yios&YVCGG9~%SK^dpz$xyx zlG#CDfZyy3sp9wJ0?s|XpLb`-wtq^H?Vp1O>)(oyv&;@5rxNjOH`okF2D77cc8p1` z#~9aR8IcQ&0_q7e0b&?$i3FemOEqR$@iU*Rq*p{ z_(ZI0CQ0A}Ma=n0lAwE&B<87$%y~N{nd?qU*17|d{@OZ8Tl)rUBf5RNB)#kfp8F-> zejhn_*AdzC4S#qrlldIQlDzH;9KXR*yfcjYiHqdW!lnLz+6Tdsx;9dR9_IJluNVIZ z@GtJEk=^Wpt$%+-JU^KrN57UYQIB>?!M15(=8AMiScw6^*Hf%v4?NtR<@NE460WiI3=;^Z*3(C6VV47j&Qj^3Tgxk{9s|B{L? z4KL15YLT&e^m~say*Ui;al4+G3|l1Q4G&3r)f=6^9~^z;@XDibR9~Q9B}3BT$4Y-YLh`o- zOXcn`sRR3_BjMyS!=?FfDD%TENz8Mp*a5W?@Nm2MJ)mcf0(Bz)lrEb-2Jh=%M2P4A zU}ox_S#lQt;qn8`>_PY!_f1LDnK@|-nwRcN)V`*cnq`u zVrBpTK{tLsM}qDv6#sh*kooE2@h?f@_2o?NmoRIjo_JatesrHCQ!ACTeL@Psy?D=* z6nT?J!IrwTnmFqV2IT#`M6EH=PY>@Iy>_|Yc9-4l_4OQTZ|m^G$h$v4zTl21d1}RR zdHVgQW#?y8<~a3)hv3hC1ii;TIJvS?_QL7A;?Sx@xL*G!f6PnoMNpEA{%{-;?#bDQlq#CLud%-`*28w<*G4Y}1T zW(TW3WE!phgmIwu?uF6n`?SgGC!G_OPdG=*AJhg)@74NB?$WyRKWFR6_@816|3K}r z-mhkD(6@zv@sY14%btH^{wG{`$Ekz&|5+aV_I&ruUGu?b_V{8F+(=n2U4NFo>7YkD z;1wUB4xBUZF)1axXQXWZw3Hv5MfcCJFVvbNzOK%^8D6vnxr)v&J`Df3rx`A_YWO#h zxevFY7nl_Yhu(%yUzR6VoRnuiaE^1QZ1;9I{=ZC=(1#TM36k>iCCS@xP71c1m4dCu znbYvL6l{7!@;7t4Wec}E*c0r5*LHKB5Ab>)_Q7fHM@i0>G`Nyuu>peM`VRvCQ0nO} zNsn)&^aorfhZ;#PEtLM+Fl>q>30_6a_+d3eqXW61$Ns63ga4Gn%)CUnfti`7gU?&1 zZryVe-(C?b-k(TY41c`NTSRn?=SSWRuK)7Knoo@ATF(uCymWVOLhSR*3Rp?6(^r#_ z^(k_e`xiLxArBQYBQ94Cz;n5ay|MROx%A3aiSPX_5{z9G_0piEY?zRI&uMU!YQ}{p56*{BXBUP(No2-cIeS6RER7Bcd9+;u*#AeqRwTPVk<45~ z_iQZp9$+89_aFGv%E8t^hsXaO` zb;swW<_P_<^wY<#ohRm_@+(&IsaDs#=k)EWW{SMIpGF^LuN?h%W(skpRQ@dvzQh3L z7rp(d2wA=2G;{v^WDj~KehqP4I2i-(O_WQk5+&y81W9=DisWnz0{e4P2H#c1uH*DV zZe%vodTP=XFUD3L+d+TSPUgUPO8T3-CFzv|?1N*H@_GRJKT=Au|10*Kr355!@A(S#v$6C(MaW=K2(=a$d2I^%s2q91{(r2UnG2o7lG-JZ@8!%l z%H;58^JLE_(#7X0~0aN)`;)F=g8R~ zXS1jC!MzAs319U+iUTuMc7Fq1|AP`@xsB9Ib-1`qV_qGlE_zA|sfn#V3}*)RaMNkC zYZZ8z$f3ixu^zs#4a5sJ!v(U1Swq|1z42W!DxYSQfG67My=ah-)qSpfdm{A~LDb;8 z@jnax{<7yQnUeTA^Jb^NiCwMEe>^dqFe&vs$Gk20&i1gpy{UzN3%h;; zw%ryU+s@j_e%Oi6=K(M7Zb@HvP}0_&fG5$Py2T63hw`J2iaPSszS0+XK}NzZ(VI%o z>y>DkOQZ)fEnXI~=u2Q3Q{hpH2&2xHUezts7rmG#m!7CWF6brV=}w7$ju_|jeXf=A z`WQChy!2h3KbZf@uaD{fUf}(KFX|2)*9~2o{7Cch@yMJ_oygQuIrq~X@kQ7B!&UD4 zGx)6W@AiBnRrY-wzEkR?eec5tdra}14ifLBCYpNKB5L8)Sy#C0eHWzFZ$WCPi%;F8 zaA&qVxTkH?=$T=z)J|qi5MS@5_HmS0N{;swy@U8!kGI3MGwI5;r)@W(2ij!AXCj#& zb5>sXK!EJ!{3oxY_xkBF7yc2CClTjKz{k6a{U6EM_d|yJO3i*hcuL=+%DNcYt>$3ERgkvj4NYOSGS13eJ!5{lVi^RX$F4wt4&;oYSoaDW?Ca?zsERcLc#7Ob<*)5cN?9m??#R z&-CPc?*GYZkA`8AGa;n2w#JpOU;5sAjvyPY`J+#SNXQ`(n-%5Q&-d-y{K#y$s zY>d3{{`2zu`vb)5pHn3D&2G+Cm0WlzSuQ`FBndAk;{PXN-(N+~Uy=r&0BRuvq~$~) z=lG1Y9`yk8?acl4fCtMaSUYP@@cv9#)vC}87)et*tQ8hEfPU(MiP7pIUP!BcVP287y3F2wawjA;+ z)K@hUYwyn#o1w;TnbtpSooV?4QG4%RJ`ict4PKuAeAk7!k%C>!9(YpCipe34L(lU! z$=iJwu7e*{;FrU5ysC@3*nVvMQTpJfm|Zd_rSv-2F>kKb&mvtH;K-u?rHA_W`cv>( zd$PR2e7~E|xb(;(ex6!z8-{4qzqxzjtIjNtZ=H0_axXrt_+-f^org;a?u4k9`(*p) zGH$uqS?>2YO-`GyZl-G{^50M9 zQ^~94(kp|k*XWOT^~W(Q(p4)lLd;aAR%Sw+}ybAY4e)N#*T@&l;@DPg4JN z2ClTT%#=B;ZmF5bqke!q$Yi17PJ;6u{+vP|aw`|9qr9SK3GlPDEe zvUW+o|85xwJOFReVQjl&uGI@?w*JH}UbmC8@4^1@qMsTWac;l#2OY-#V|F)ODVBVA zGKh(4b)5ZH&VIL=6HIO7I5iT})I=KE$dytfVW3uGx=|csDvwG{-6V1J`3B7~+xE($ zsZCd%lKTgs_TIfz;jce!(G5fy_m=J-0b}OG-BToof53c_pV0GuZxiw!U;9b;ZOGTe z;EyN1H7nUW=ke{q{**=91IU4p4^y}gQuonKEqgigyWjx09&<`Hm@6*Z#tWS33-meo zQ-jOTS;>Xi$ba|7YwQttl39A{Tr);`imvtof@8vgV^fviXzI zi;>UNlS}9%*PJeK&!;fgCWW(~EJbjQR_sc`#*3AXv!U=3h0qrh21gNo+=VmrLco6- zv{yz#sgt^N6dpYK8KTcH|LZh<-XYHWZfdAK!QPYj-!AF&-3|T+WIXaXyf=aLme40( zl}zpgy|3ydm=Ww8BVVepSGdo2ApaHqDsSFE-2^$6359(POCMz$8~CVwviH3M@I82^ z_5g#fFLL&&oSnVe+572zWhR;LeaxbHq{}rY>eA|Aa%J#4!g-pqVUD?wMq)Y(@Q7Pn zI=(lIezyqf-^c|IP!m;q3V!p$>;cZa!dmg4|mDDg5h2o2V3Rwpuh}bBArF!Y_xaa_Z7`_h!etf9y4O#?WXsOf^c9a-Jti;ah_@4IA|!AR^l`WA;`;H&iq zckH&x)6~fY(boj--N^FZghlD0AD}Ci8lhx(sNc>*o|W}P8R`}%O4wW!Gt-)lEK_QdNF-)1iG zMm3{E^*dOl_5}US{@8d*-h)}g}MlM@KjBds+%^Hq9?-e6+P#PGgXP_m#@l^ z?>gVVqve(lm z*+YG#lKHdL!!GoIJH62k>SLVrL_4?-{*JjOYGpY8hGwvDyLVzVMW@ZRzXxdjao$1} z^g3JFqE4%|eAZHC4>4qG#(`-1T72QKCofZm9bO0jWgB%L%Dx|nxQ6`|&Tt&HaiR3D zapr4$!9JK?h$wo0QsA`Cf%ht(o`?c!=D3|M*TmjO4Fq*EMr8jak58A7BiB>&G@$q~ zsE<*#GyRJ)SE2au)f`$gdB8U8kzA|a!i`}{(nqNFWBBsZm~fUU(H}?(M}C6*57P-+l;Fz{bwU!WI)J}7#bwNf}R zDyW5~ze%g1$BAAiYdIV=rSy`*k6>#c-qVe*5B82Zb>^v!ag2le3~QD}ZJahpom`tb z^P;ILfwM}n@{z^*n8Z4 zQ6}mmtg1(52;9e6Q>W9I`cl6N@H3A3940_#+jJJceV;=s71$cbSaCywL z7e+-}Xb1bICHq`+v308To6e;M-FVfXp3MJk+r|Okkk(}Wn7z*WkiE)YXf1Wx=5xUG zDsulaJZ_OTWIsFwob@W~JSF#C{jQ-(-_!fYo>Bd-GiAu}O3rdMeNAm(KhC25P4xz8 z>;(;3@2ZzowQ_JUF2cj7>Lpb_fq}ZHj>~EmrJY%5(@SYvyS*ZRD7xLBPtonXBmUm7 zhbnB1r|1(|pGVFw6aPJjv!9EPpUVs+xQRpPBZ`0%C>{=w40`{w>1WQPpRo{r+Y;*I zO5nUGjie?j*4+cy2==Yl?0RfF2R^-XvKcu~J)99aZy5pi88A14IrYy@2mEas%OVa9 z`MEg~7IeOn?W!IctQF^}OV%$x^Rql|)UG5cKn zwmj+f%_nD@Pu#u$-Csz*GQ3n-a2OX5t1qUny&R0IIp1{!)Is6P)g$wp!M>fxRSoPA zHLw%Zyw73NFCfq1Jy~?l!_!6G^CEb=aHsa&q3~Z2r)@+uWWIe~KVX~IzhE0~{g5{G zrxV}(LpPaSZ`9cbr*!r@%cpELwgZ+5ZEz+X9UtPh^-B(^TXg?&`JYI9CmSv^YT6Cd z$lAcu)dQpOzrL=?deuYlPHzNUe2OEDV)eL1VJ+=_gv08 z*f${8Taf8(rSw^5;m;}Tk@v%j+YIc9;W=!hob@>{x4AK2h?|trn-F~q)m~^&|>GzpR7rti9*P>=KowoiMVmjyOu{-8) zaiFXIC}sb(WB>If(hrHOn5V8)abLN5AeEd~_$%yHeLPF?->5V1T0Ez*SB$)7Gy^*# zj{f*qdvRa%+~4;_yXRg;znlv9#Z$U--%ayLUfm?EzRU#i$i?T&lP=!^_F8QIgGz&qMuFG$1Cp3MP=uWF3Hja*n{;Fd`Panp!S2Z3;3CZ zMr3~x{AsC*Lrt3)Nwxk2K3v^c$_);*pr54c{C|}E4;Sw#ZSV9OXV!H&H5<9aYl?{V zln}3n8#bXFt^>F!bE?SKVY^k<%3>3;y`7kBH~0JLZy83v)7PdEyVq2_)`+gRA@iNn z;E!x~>33pPO5bDespsqqtztLn7j1KGnbxVcZ>oB($?iXmc>b+e{jsh!_z0_iB05`@ z?F$Pfc7H>z!!(H9*9Z@S+6T%PXeD+to`L+wk5}i}La&3poqf;)*7*Ofy#W9DA~^s@ zKeizDpoN%#0bgN)ydcXukz-xoS5&!TJa6WvKPCL33|(f)@fD#z%=M}E=wqf`JNaJh zzQAG`i!77bxC%J#$hBru!(IsX<@oEhjk2gGPT!4PH^A)(v72#XwKJUex!!9w9=95H z-r_9h9ov4;VRF40VSu^U^BHbciA^LWSq;xS!&cIi!@?f<8Xg=(|TTx*vcS?4spg&8rCY@X(E%ga3abBKap9>RL30B>UkkDSR{CGmqGwX!u4c+A z&WuT7f>XJg#ZYA1Ggo5zu%YFtSXa#z*wSY6?2{AJ z^b93v%(^2dn4mcA@gKAB3yA~G5c5%U#OKOcDmK90n8e-m3ow=()7xh%>i;XAxZQa3b(&6XEU(GYKX2VNa%sHt6mnn$j=Rs zn;mCeJNItvN8#dAK0R_D+?AYnm^)l?9CEhE`<5kQ_j$JI?yoe4+I8m9of#D5jn`FJ+` zXt~hg_`*l#>#g;_shSegx4x`p{sKKs@$vd@=(MGGp);IGy*Qtlxnf% zl7A~hzt@6&bE_%*`ME*l{RI9z{3Q+(G5dvKe7G@L)FxcMo>STVX6$+c_P`wS z!>reA#`ZAFc*p-H?x*W0tX}>R{(S#kz5z>elvMm z`c$sHZ7N3_n96YsxbElM;|!%XpSd#24~-R;kBnatow4SIt@kc(f73&`o11kR>l*GH ziWt8ADSfrZJQkC`X`Ebnjn;(vz=M|+P>hMRN?^|K!`R+|6LTxI_> z>kilbx~ab~l>KIogWvY6v=ZW#rOuDdrH=UxMpkQ6@zmil2f|Uw`ELOKHt_Fm$L<9G z5rsc8V46Lk`r4HYFe&>F+{xWLRPIjsb_<;|c4PNzmXY2MSjYb??|&Nx|JBbgyV`)pdnmig?eA!|ADokncHc5HwiupM9@ zj9?3lu@@Adz4H5~n#t9pC+1thzS9o&Nw(>puk}U@tuS`?|L1@CpMUaC;u|m&w&-RG zI^Sn3s{7tTW!)89W5bN2ow!~P7!RNiRLp0LYZc?CMpK=8ZNA-NH+7cS=DJ_8On2XI zGyYlE|4D%Pdwz)dmOh=mp-yM6Z~34_ufN;g-5Q_`=nL$_dc8K*I-pIq4mhUUnvnbX znxQkuI^Fe4(|Gsog+CJAM9ZJC|2@CvWwiHmYMF++ z|K2v<|4~!#&~4VC;XldG{_8#zF=zGfcK6No|Cc^^{hs^QdE3p`wiU~+mqg~sqrCIa z=_UQkuX(~-=k?Ch>W=Q_YetgY^gL8WD@FFIpLG4}=X%NO#_!~hxnpV9JHMkqU5oA7 zd#qHKx=(w}srL|ZuJl&dAM@(JcAf9KzP+!X*Qn?DaQwUdM1jurC77;V_j&7FU&6KP z5AvB^*F#dEe*XP@X4jW8cH`Rh0cqb;&wtR#f9mb8U-LJq59k!>=br91b-i-g^&HoC zi2B#hJ>6~U+Vu;sU8_gkD5!h7+g$%I`}qR(=*kYw8>ut-Tnt&-*L<9f9LB0KE$$rcN8qU;O_5y-G9sLf6MjCTk!vtYuDkt z@t$6{?8jU8^DXCx1L1w+#V!GK(+5)GWcdkE*UNvyyIn8;MC$olub2Nt>iOSr?Gi`J zp1<|lB@dT9&w0M-_qhc6vd?>mdfxT-xW(H_b?y2FZgYJLUao8P4Y;pem#z=!x_13~ z_ZRR%_4BS@$Io!rwNLMG|GZKFuIrW8uKTa;6R&I6d?PD)>>d5ZC-malE#1}6c&|{u s^!nTLC*7{T#hY|Auesye+qrJ|t5z<*=0n|dDFxR){q;xZ+;8`P0R_*&xc~qF literal 0 HcmV?d00001 diff --git a/FunGameServer.sln b/FunGameServer.sln index f7c0a2f..d00eaac 100644 --- a/FunGameServer.sln +++ b/FunGameServer.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FunGame.WebAPI", "FunGame.W EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunGame.Core", "..\FunGame.Core\FunGame.Core.csproj", "{33CAC80F-8394-41DB-B5AF-5E91123E6C84}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunGame.SQLQueryExtension", "..\FunGame.Extension\FunGame.SQLQueryExtension\FunGame.SQLQueryExtension.csproj", "{9EEB3474-B9A1-4E5E-BEF0-14F30D81873C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {33CAC80F-8394-41DB-B5AF-5E91123E6C84}.Debug|Any CPU.Build.0 = Debug|Any CPU {33CAC80F-8394-41DB-B5AF-5E91123E6C84}.Release|Any CPU.ActiveCfg = Release|Any CPU {33CAC80F-8394-41DB-B5AF-5E91123E6C84}.Release|Any CPU.Build.0 = Release|Any CPU + {9EEB3474-B9A1-4E5E-BEF0-14F30D81873C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EEB3474-B9A1-4E5E-BEF0-14F30D81873C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EEB3474-B9A1-4E5E-BEF0-14F30D81873C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EEB3474-B9A1-4E5E-BEF0-14F30D81873C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE