From 5ac9fe3bedd2fdc3cf4cdf5b2d109d7e990a5283 Mon Sep 17 00:00:00 2001 From: manfred Date: Thu, 18 Dec 2025 19:57:40 +0800 Subject: [PATCH] feat: Add and modified Gameserver --- logic/Server/ArgumentOptions.cs | 79 +++ logic/Server/CopyInfo.cs | 294 +++++++++ logic/Server/GameServer.cs | 602 ++++++++++++++++++ logic/Server/HttpSender.cs | 81 +++ logic/Server/PlaybackServer.cs | 331 ++++++++++ logic/Server/Program.cs | 76 +++ logic/Server/Properties/launchSettings.json | 8 + logic/Server/RpcServices.cs | 665 ++++++++++++++++++++ logic/Server/Server.csproj | 32 + logic/Server/ServerBase.cs | 13 + logic/Server/ServerLogging.cs | 15 + 11 files changed, 2196 insertions(+) create mode 100644 logic/Server/ArgumentOptions.cs create mode 100644 logic/Server/CopyInfo.cs create mode 100644 logic/Server/GameServer.cs create mode 100644 logic/Server/HttpSender.cs create mode 100644 logic/Server/PlaybackServer.cs create mode 100644 logic/Server/Program.cs create mode 100644 logic/Server/Properties/launchSettings.json create mode 100644 logic/Server/RpcServices.cs create mode 100644 logic/Server/Server.csproj create mode 100644 logic/Server/ServerBase.cs create mode 100644 logic/Server/ServerLogging.cs diff --git a/logic/Server/ArgumentOptions.cs b/logic/Server/ArgumentOptions.cs new file mode 100644 index 00000000..9606adc9 --- /dev/null +++ b/logic/Server/ArgumentOptions.cs @@ -0,0 +1,79 @@ +using CommandLine; +using Preparation.Utility; + +namespace Server +{ + public class DefaultArgumentOptions + { + public const string FileName = "xxxxxxxxx"; + public const string Token = "xxxxxxxxx"; + public const string Url = "xxxxxxxxx"; + public const string MapResource = "xxxxxxxxx"; + } + public class ArgumentOptions + { + [Option("ip", Required = false, HelpText = "Server listening ip")] + public string IP { get; set; } = "0.0.0.0"; + + [Option('p', "port", Required = true, HelpText = "Server listening port")] + public ushort ServerPort { get; set; } = 8888; + + [Option("teamCount", Required = false, HelpText = "The number of teams, 4 by defualt")] + public ushort TeamCount { get; set; } = 4; + + [Option("CharacterNum", Required = false, HelpText = "The max number of Character, 6 by default")] + public ushort CharacterCount { get; set; } = 6; + + [Option('g', "gameTimeInSecond", Required = false, HelpText = "The time of the game in second, 10 minutes by default")] + public uint GameTimeInSecond { get; set; } = GameData.GameDurationInSecond; + + [Option("homeNum", Required = false, HelpText = "The number of Home , 1 by default")] + public ushort HomeCount { get; set; } = 1; + + [Option('f', "fileName", Required = false, HelpText = "The file to store playback file or to read file.")] + public string FileName { get; set; } = "xxxxxxxxx"; + + [Option("notAllowSpectator", Required = false, HelpText = "Whether to allow a spectator to watch the game.")] + public bool NotAllowSpectator { get; set; } = false; + + [Option('b', "playback", Required = false, HelpText = "Whether open the server in a playback mode.")] + public bool Playback { get; set; } = false; + + [Option("playbackSpeed", Required = false, HelpText = "The speed of the playback, between 0.25 and 4.0")] + public double PlaybackSpeed { get; set; } = 1.0; + + [Option("resultOnly", Required = false, HelpText = "In playback mode to get the result directly")] + public bool ResultOnly { get; set; } = false; + + [Option('k', "token", Required = false, HelpText = "Web API Token")] + public string Token { get; set; } = "xxxxxxxxx"; + + [Option('u', "url", Required = false, HelpText = "Web Url")] + public string Url { get; set; } = "xxxxxxxxx"; + + [Option('m', "mapResource", Required = false, HelpText = "Map Resource Path")] + public string MapResource { get; set; } = DefaultArgumentOptions.MapResource; + + [Option("requestOnly", Required = false, HelpText = "Only send web requests")] + public bool RequestOnly { get; set; } = false; + + [Option("finalGame", Required = false, HelpText = "Whether it is the final game")] + public bool FinalGame { get; set; } = false; + + [Option("cheatMode", Required = false, HelpText = "Whether to open the cheat code")] + public bool CheatMode { get; set; } = false; + + [Option("resultFileName", Required = false, HelpText = "Result file name, saved as .json")] + public string ResultFileName { get; set; } = "xxxxxxxxx"; + + [Option("startLockFile", Required = false, HelpText = "Whether to create a file that identifies whether the game has started")] + public string StartLockFile { get; set; } = "114514"; + + [Option("mode", Required = false, HelpText = "Whether to run final competition. 0 本地玩,1 最终比赛,2 天梯")] + public int Mode { get; set; } = 0; + + [Option("loglevel", Required = false, HelpText = "Set the log level: 1=Error, 2=Warning, 3=Info, 4=Debug, 5=Trace")] + public int LogLevel { get; set; } = 5; // 默认Trace级别 + + } +} diff --git a/logic/Server/CopyInfo.cs b/logic/Server/CopyInfo.cs new file mode 100644 index 00000000..4ac5582d --- /dev/null +++ b/logic/Server/CopyInfo.cs @@ -0,0 +1,294 @@ +using GameClass.GameObj; +using GameClass.GameObj.Areas; +using Preparation.Utility; +using Protobuf; +using Utility = Preparation.Utility; + +namespace Server +{ + public static class CopyInfo + { + public static MessageOfObj? Auto(GameObj gameObj, long time) + { + if (gameObj.IsRemoved == true) + return null; + switch (gameObj.Type) + { + case GameObjType.CHARACTER: + return Character((Character)gameObj, time); + case GameObjType.ECONOMY_RESOURCE: + return EconomyResource((E_Resource)gameObj); + case GameObjType.ADDITIONAL_RESOURCE: + return AdditionResource((A_Resource)gameObj); + case GameObjType.CONSTRUCTION: + Construction construction = (Construction)gameObj; + if (construction.ConstructionType == Utility.ConstructionType.BARRACKS) + return Barracks(construction); + else if (construction.ConstructionType == Utility.ConstructionType.SPRING) + return Spring(construction); + else if (construction.ConstructionType == Utility.ConstructionType.FARM) + return Farm(construction); + return null; + case GameObjType.TRAP: + if (gameObj is HOLE hole) + return Traps(hole); + else if (gameObj is Cage cage) + return Traps(cage); + return null; + default: return null; + } + } + + public static MessageOfObj? Auto(MessageOfNews news) + { + MessageOfObj objMsg = new() + { + NewsMessage = news + }; + return objMsg; + } + private static MessageOfObj? Base(Base player, long time) + { + MessageOfObj msg = new() + { + TeamMessage = new() + { + TeamId = player.TeamID, + PlayerId = player.PlayerID, + Score = player.MoneyPool.Score, + Energy = player.MoneyPool.Money, + } + }; + return msg; + } + + public static MessageOfObj? Auto(Base @base, long time) + { + return Base(@base, time); + } + private static MessageOfObj? Character(Character player, long time) + { + MessageOfObj msg = new() + { + CharacterMessage = new() + { + Guid = player.ID, + + TeamId = player.TeamID, + PlayerId = player.PlayerID, + + CharacterType = Transformation.CharacterTypeToProto(player.CharacterType), + + CharacterActiveState = Transformation.CharacterStateToProto(player.CharacterState1), + + IsBlind = player.blind, + BlindTime = player.BlindTime, + IsStunned = player.stunned, + StunnedTime = player.StunnedTime, + IsInvisible = !player.visible, + InvisibleTime = player.InvisibleTime, + IsBurned = player.burned, + BurnedTime = player.BurnedTime, + HarmCut = player.HarmCut, + HarmCutTime = player.HarmCutTime, + + CharacterPassiveState = Transformation.CharacterStateToProto(player.CharacterState2), + + X = player.Position.x, + Y = player.Position.y, + + FacingDirection = player.FacingDirection.Angle(), + Speed = player.MoveSpeed, + ViewRange = player.ViewRange, + + CommonAttack = (int)player.AttackPower, + // 待修改,Character.cs中没有CommonAttackCD + CommonAttackCd = (int)(1 / player.ATKFrequency), + CommonAttackRange = (int)player.AttackSize, + + SkillAttackCd = player.skillCD, + + EconomyDepletion = player.EconomyDepletion, + KillScore = (int)player.GetCost(), + + Hp = (int)player.HP, + + // 待修改,Shield要分两类 + ShieldEquipment = (int)player.Shield, // 护盾装备 + ShoesEquipment = (int)player.Shoes, // 加成值 + ShoesTime = player.ShoesTime, // 包含所有速度加成的时间 + IsPurified = player.Purified, + PurifiedTime = player.PurifiedTime, + IsBerserk = player.IsBerserk, + BerserkTime = player.BerserkTime, + + AttackBuffNum = (int)player.CrazyManNum, + AttackBuffTime = player.CrazyManTime, + SpeedBuffTime = player.QuickStepTime, + VisionBuffTime = player.WideViewTime, + } + }; + return msg; + } + + private static MessageOfObj EconomyResource(E_Resource economyresource) + { + MessageOfObj msg = new() + { + EconomyResourceMessage = new() + { + EconomyResourceState = Transformation.EconomyResourceStateToProto(economyresource.ERstate), + EconomyResourceType = Transformation.EconomyResourceTypeToProto(economyresource.EResourceType), + + X = economyresource.Position.x, + Y = economyresource.Position.y, + + Process = (10000 - (int)economyresource.HP) / 10000, + Id = 0, + } + }; + return msg; + } + + private static MessageOfObj AdditionResource(A_Resource additionResource) + { + MessageOfObj msg = new() + { + AdditionResourceMessage = new() + { + AdditionResourceType = Transformation.AResourceToProto(additionResource.AResourceType), + AdditionResourceState = Transformation.AResourceStateToProto(additionResource.ARstate), + + X = additionResource.Position.x, + Y = additionResource.Position.y, + + Hp = (int)additionResource.HP, + Id = 0, + } + }; + // Debugger.Output(additionResource, additionResource.Place.ToString()+" "+additionResource.Position.ToString()); + return msg; + } + + private static MessageOfObj Barracks(Construction construction) + { + MessageOfObj msg = new() + { + BarracksMessage = new() + { + X = construction.Position.x, + Y = construction.Position.y, + + Hp = (int)construction.HP, + + TeamId = construction.TeamID, + Id = 0, + } + }; + return msg; + } + + private static MessageOfObj Spring(Construction construction) + { + MessageOfObj msg = new() + { + SpringMessage = new() + { + X = construction.Position.x, + Y = construction.Position.y, + + Hp = (int)construction.HP, + + TeamId = construction.TeamID, + Id = 0, + } + }; + return msg; + } + + private static MessageOfObj Farm(Construction construction) + { + MessageOfObj msg = new() + { + FarmMessage = new() + { + X = construction.Position.x, + Y = construction.Position.y, + + Hp = (int)construction.HP, + + TeamId = construction.TeamID, + Id = 0, + } + }; + return msg; + } + + // private static MessageOfObj Traps(GameObj trap) + // { + // MessageOfObj msg = new() + // { + // TrapMessage = new() + // { + // TrapType = trap switch + // { + // HOLE _ => Protobuf.TrapType.Hole, + // Cage _ => Protobuf.TrapType.Cage, + // }, + + // X = trap.Position.x, + // Y = trap.Position.y, + + // TeamId = trap switch + // { + // HOLE t => t.TeamID.Get(), + // Cage c => c.TeamID.Get(), + // }, + // Id = 0, + + // TrapValid = !trap.IsActivated.Get(), + // } + // }; + // return msg; + // } + private static MessageOfObj Traps(HOLE trap) + { + MessageOfObj msg = new() + { + TrapMessage = new() + { + TrapType = Protobuf.TrapType.Hole, + + X = trap.Position.x, + Y = trap.Position.y, + + TeamId = trap.TeamID.Get(), + Id = 0, + + TrapValid = !trap.IsActivated.Get(), + } + }; + return msg; + } + + private static MessageOfObj Traps(Cage trap) + { + MessageOfObj msg = new() + { + TrapMessage = new() + { + TrapType = Protobuf.TrapType.Cage, + + X = trap.Position.x, + Y = trap.Position.y, + + TeamId = trap.TeamID.Get(), + Id = 0, + + TrapValid = !trap.IsActivated.Get(), + } + }; + return msg; + } + } +} diff --git a/logic/Server/GameServer.cs b/logic/Server/GameServer.cs new file mode 100644 index 00000000..4b2e3c27 --- /dev/null +++ b/logic/Server/GameServer.cs @@ -0,0 +1,602 @@ +using GameClass.GameObj; +using GameClass.GameObj.Map; +using GameClass.MapGenerator; +using Gaming; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Playback; +using Preparation.Utility; +using Preparation.Utility.Logging; +using Protobuf; +using System; +using System.Collections.Concurrent; +using System.Threading; +using Timothy.FrameRateTask; + +namespace Server +{ + public class ContestResult + { + public required string status; + public required double[] scores; + } + partial class GameServer : ServerBase + { + private readonly ConcurrentDictionary semaDict0 = new(); //for spectator and team0 player + private readonly ConcurrentDictionary semaDict1 = new(); + private readonly ConcurrentDictionary semaDict2 = new(); + private readonly ConcurrentDictionary semaDict3 = new(); + // private object semaDictLock = new(); + protected readonly ArgumentOptions options; + private readonly HttpSender httpSender; + private readonly object gameLock = new(); + private MessageToClient currentGameInfo = new(); + private readonly MessageOfObj currentMapMsg = new(); + private readonly object newsLock = new(); + private readonly List currentNews = []; + private readonly SemaphoreSlim endGameSem = new(0); + protected readonly Game game; + private readonly uint spectatorMinPlayerID = 2023; //? + public int playerNum; + public int TeamCount => options.TeamCount; + protected long[][] communicationToGameID; // 通信用的ID映射到游戏内的ID,0指向队伍1,1指向队伍2,通信中0为大本营,1-5为船 + private readonly object messageToAllClientsLock = new(); + public static readonly long SendMessageToClientIntervalInMilliseconds = 50; + private readonly MessageWriter? mwr = null; + private readonly object spectatorJoinLock = new(); + + public void StartGame() + { + if (game.GameMap.Timer.IsGaming) return; + foreach (var team in communicationToGameID) + { + foreach (var id in team) + { + if (id == GameObj.invalidID) return;//如果有未初始化的玩家,不开始游戏 + } + } + GameServerLogging.logger.LogInfo("Game starts!"); + CreateStartFile(); + game.StartGame((int)options.GameTimeInSecond * 1000); + Thread.Sleep(1); + new Thread(() => + { + bool flag = true; + new FrameRateTaskExecutor + ( + () => game.GameMap.Timer.IsGaming, + () => + { + if (flag == true) + { + ReportGame(GameState.GameStart); + flag = false; + } + else ReportGame(GameState.GameRunning); + }, + SendMessageToClientIntervalInMilliseconds, + () => + { + ReportGame(GameState.GameEnd); // 最后发一次消息,唤醒发消息的线程,防止发消息的线程由于有概率处在 Wait 状态而卡住 + OnGameEnd(); + return 0; + } + ).Start(); + }) + { IsBackground = true }.Start(); + } + + public void CreateStartFile() + { + if (options.StartLockFile != DefaultArgumentOptions.FileName) + { + using var _ = File.Create(options.StartLockFile); + GameServerLogging.logger.LogInfo("Successfully Created StartLockFile!"); + } + } + + public override void WaitForEnd() + { + endGameSem.Wait(); + mwr?.Dispose(); + } + + private void SaveGameResult(string path) + { + Dictionary result = []; + int[] score = GetScore(); + result.Add("Team 1", score[0]); + result.Add("Team 2", score[1]); + result.Add("Team 3", score[2]); + result.Add("Team 4", score[3]); + JsonSerializer serializer = new(); //? + using StreamWriter sw = new(path); + using JsonTextWriter writer = new(sw); + serializer.Serialize(writer, result); + } + + protected void SendGameResult(int[] scores, bool crashed = false) // 天梯的 Server 给网站发消息记录比赛结果 + { + string? url2 = Environment.GetEnvironmentVariable("FINISH_URL"); + if (url2 == null) + { + GameServerLogging.logger.LogInfo("Null FINISH_URL!"); + return; + } + else + { + httpSender.Url = url2; + httpSender.Token = options.Token; + } + string state = crashed ? "Crashed" : "Finished"; + string[][] player_role = new string[4][]; + player_role[0] = new string[options.CharacterCount]; + player_role[1] = new string[options.CharacterCount]; + player_role[2] = new string[options.CharacterCount]; + player_role[3] = new string[options.CharacterCount]; + foreach (var team in game.TeamList) + { + int count = 0; + List list; + list = team.CharacterPool.Travel((character) => character.CharacterType); + foreach (var type in list) + { + player_role[team.TeamID][count] = type.ToString(); + count++; + } + } + httpSender?.SendHttpRequest(scores, state, player_role).Wait(); + } + + protected double[] PullScore(double[] scores) + { + string? url2 = Environment.GetEnvironmentVariable("SCORE_URL"); + if (url2 != null) + { + httpSender.Url = url2; + httpSender.Token = options.Token; + double[] org = httpSender.GetLadderScore(scores).Result; + if (org.Length == 0) + { + GameServerLogging.logger.LogInfo("Error: No data returned from the web!"); + return new double[0]; + } + else + { + double[] final = LadderCalculate(org, scores); + return final; + } + } + else + { + GameServerLogging.logger.LogInfo("Null SCORE_URL Environment!"); + return new double[0]; + } + } + + protected static double[] LadderCalculate(double[] oriScores, double[] competitionScores) + { + /* + * 天梯得分算法注解 + * + * 天梯得分算法是经过多轮调整得到,得分算法的设计原则参见: + * https://github.com/eesast/THUAI6/discussions/441 + * 以及 + * https://github.com/eesast/THUAI5/discussions/86 + * 中的讨论 + */ + + // 调整顺序,让第一项成为获胜者,便于计算 + bool scoresReverse = false; // 顺序是否需要交换 + if (competitionScores[0] < competitionScores[1]) // 第一项为落败者 + scoresReverse = true; + else if (competitionScores[0] == competitionScores[1])// 平局 + { + if (oriScores[0] == oriScores[1]) + // 完全平局,不改变天梯分数 + { + double[] Score = [0, 0]; + return Score; + } + if (oriScores[0] > oriScores[1]) + // 本次游戏平局,但一方天梯分数高,另一方天梯分数低, + // 需要将两者向中间略微靠拢,因此天梯分数低的定为获胜者 + scoresReverse = true; + } + if (scoresReverse)// 如果需要换,交换两者的顺序 + { + (competitionScores[0], competitionScores[1]) = (competitionScores[1], competitionScores[0]); + (oriScores[0], oriScores[1]) = (oriScores[1], oriScores[0]); + } + + const double normalDeltaThereshold = 100.0; // 天梯分数差参数,天梯分差超过此阈值太多则增长缓慢 + const double correctParam = normalDeltaThereshold * 1.2; // 修正参数 + const double winnerWeight = 4e-10; // 获胜者天梯得分权值 + const double loserWeight = 1.5e-10; // 落败者天梯得分权值 + const double scoreDeltaThereshold = 50000.0; // 比赛得分参数,比赛得分超过此阈值太多则增长缓慢 + + double[] resScore = [0, 0]; + double oriDelta = oriScores[0] - oriScores[1]; // 天梯原分数差 + double competitionDelta = competitionScores[0] - competitionScores[1]; // 本次比赛分数差 + double normalOriDelta = oriDelta / normalDeltaThereshold; // 标准化天梯原分数差 + double correctRate = oriDelta / correctParam; // 修正率,修正方向为缩小分数差 + double correct = 0.5 * (Math.Tanh((competitionDelta - scoreDeltaThereshold) / scoreDeltaThereshold + - correctRate) + + 1.0); // 分数修正 + resScore[0] = Math.Min(300, Math.Round(Math.Pow(competitionScores[0], 2) + * winnerWeight + * (1 - Math.Tanh(normalOriDelta)) + * correct)); // 胜者所加天梯分) + resScore[1] = Math.Max(-120, -Math.Round(Math.Pow(competitionDelta, 2) + * loserWeight + * (1 - Math.Tanh(normalOriDelta)) + * correct)); // 败者所扣天梯分 + if (scoresReverse)// 顺序换回 + (resScore[0], resScore[1]) = (resScore[1], resScore[0]); + return resScore; + } + + private void OnGameEnd() + { + game.ClearAllLists(); + mwr?.Flush(); + if (options.ResultFileName != DefaultArgumentOptions.FileName) + SaveGameResult(options.ResultFileName.EndsWith(".json") + ? options.ResultFileName + : options.ResultFileName + ".json"); + int[] scores = GetScore(); + double[] doubleArray = scores.Select(x => (double)x).ToArray(); + if (options.Mode == 2) + { + bool crash = false; + doubleArray = PullScore(doubleArray); + if (doubleArray.Length == 0) + { + crash = true; + GameServerLogging.logger.LogInfo("Error: No data returned from the web!"); + } + else + scores = doubleArray.Select(x => (int)x).ToArray(); + endGameSem.Release(); + Thread.Sleep(1); + SendGameResult(scores, crash); + } + else if (options.Mode == 1) + { + /* + int[] s = new int[2]; + if (scores[1] > scores[0]) + s = [0, 2]; + else if (scores[1] == scores[0]) + s = [1, 1]; + else + s = [2, 0]; + */ // 得分计算方式待定 + endGameSem.Release(); + Thread.Sleep(1); + SendGameResult(s); + } + } + + public void ReportGame(GameState gameState, bool requiredGaming = true) + { + var gameObjList = game.GetGameObj(); + currentGameInfo = new(); + lock (messageToAllClientsLock) + { + switch (gameState) + { + case GameState.GameRunning: + case GameState.GameEnd: + case GameState.GameStart: + if (gameState == GameState.GameStart || IsSpectatorJoin) + { + currentGameInfo.ObjMessage.Add(currentMapMsg); + IsSpectatorJoin = false; + } + long time = Environment.TickCount64; + foreach (GameObj gameObj in gameObjList.Cast()) + { + MessageOfObj? msg = CopyInfo.Auto(gameObj, time); + if (msg != null) currentGameInfo.ObjMessage.Add(msg); + } + foreach (Base team in game.TeamList) + { + MessageOfObj? msg = CopyInfo.Auto(team, time); + if (msg != null) currentGameInfo.ObjMessage.Add(msg); + } + lock (newsLock) + { + foreach (var news in currentNews) + { + MessageOfObj? msg = CopyInfo.Auto(news); + if (msg != null) currentGameInfo.ObjMessage.Add(msg); + } + currentNews.Clear(); + } + currentGameInfo.GameState = gameState; + currentGameInfo.AllMessage = GetMessageOfAll(game.GameMap.Timer.NowTime()); + mwr?.WriteOne(currentGameInfo); + break; + default: + break; + } + } + lock (spectatorJoinLock) + { + foreach (var kvp in semaDict0) + { + kvp.Value.Item1.Release(); + } + foreach (var kvp in semaDict1) + { + kvp.Value.Item1.Release(); + } + foreach (var kvp in semaDict2) + { + kvp.Value.Item1.Release(); + } + foreach (var kvp in semaDict3) + { + kvp.Value.Item1.Release(); + } + + // 若此时观战者加入,则死锁,所以需要 spectatorJoinLock + + foreach (var kvp in semaDict0) + { + kvp.Value.Item2.Wait(); + } + foreach (var kvp in semaDict1) + { + kvp.Value.Item2.Wait(); + } + foreach (var kvp in semaDict2) + { + kvp.Value.Item2.Wait(); + } + foreach (var kvp in semaDict3) + { + kvp.Value.Item2.Wait(); + } + } + } + + private bool PlayerDeceased(int playerID) + { + return game.GameMap.GameObjDict[GameObjType.CHARACTER].Cast()?.Find( + character => character.PlayerID == playerID && character.CharacterState2 == Preparation.Utility.CharacterState.DECEASED + ) != null; + } + /* + public override int[] GetMoney() + { + int[] money = new int[2]; // 0代表Team 1,1代表Team 2... + foreach (Base team in game.TeamList) + { + money[(int)team.TeamID] = (int)game.GetTeamMoney(team.TeamID); + } + return money; + } + */ + public override int[] GetMaterial() + { + int[] material = new int[4]; // 0代表Team 1,1代表Team 2... + foreach (Base team in game.TeamList) + { + material[(int)team.TeamID] = (int)game.GetTeamMaterial(team.TeamID); //GetTeamMaterial还未定义 + } + return material; + } + public override int[] GetComputingPower() + { + int[] computingpower = new int[4]; // 0代表Team 1,1代表Team 2... + foreach (Base team in game.TeamList) + { + computingpower[(int)team.TeamID] = (int)game.GetTeamComputingPower(team.TeamID); //GetTeamComputingPower还未定义 + } + return computingpower; + } + public override int[] GetScore() + { + int[] score = new int[4]; // 0代表Team 1,1代表Team 2... + foreach (Base team in game.TeamList) + { + score[(int)team.TeamID] = (int)game.GetTeamScore(team.TeamID); + } + return score; + } + + private uint GetBirthPointIdx(long playerID) // 获取出生点位置 + { + return (uint)playerID + 1; // ID从0-8,出生点从1-9 + } + + private bool ValidPlayerID(long playerID) + { + if (playerID == 0 || (1 <= playerID && playerID <= options.CharacterCount)) + return true; + return false; + } + + private MessageOfAll GetMessageOfAll(int time) + { + MessageOfAll msg = new() + { + GameTime = time + }; + int[] score = GetScore(); + msg.Team1Score = score[0]; + msg.MonstersTeamScore = score[1]; + int[] economy = GetMoney(); + msg.BuddhistsTeamEconomy = economy[0]; + msg.MonstersTeamEconomy = economy[1]; + // msg.RedHomeHp = (int)game.TeamList[0].Home.HP; + // msg.BlueHomeHp = (int)game.TeamList[1].Home.HP; + return msg; + } + private MessageOfAll GetMessageOfAll(int time) + { + MessageOfAll msg = new() + { + GameTime = time + }; + int[] scores = GetScore(); + int[] materials = GetMaterial(); + int[] computing = GetComputingPower(); + + for (int i = 0; i < option.TeamCount; i++) // 确保 teams 字段有 TeamCount 个元素,teams[i] 对应 team i+1 + { + var teamInfo = new MessageOfAll.Types.TeamInfo + { + score = (i < scores.Length) ? scores[i] : 0, + material = (i < materials.Length) ? materials[i] : 0, + computing_power = (i < computing.Length) ? computing[i] : 0, + factory_hp = 0 + }; + + msg.Teams.Add(teamInfo); + } + + return msg; + } + + private MessageOfMap MapMsg() + { + MessageOfMap msgOfMap = new() + { + Height = game.GameMap.Height, + Width = game.GameMap.Width + }; + for (int i = 0; i < game.GameMap.Height; i++) + { + msgOfMap.Rows.Add(new MessageOfMap.Types.Row()); + for (int j = 0; j < game.GameMap.Width; j++) + { + msgOfMap.Rows[i].Cols.Add(Transformation.PlaceTypeToProto(game.GameMap.ProtoGameMap[i, j])); + } + } + return msgOfMap; + } + + public GameServer(ArgumentOptions options) + { + this.options = options; + LogLevel logLevel = options.LogLevel switch + { + 1 => LogLevel.Error, + 2 => LogLevel.Warning, + 3 => LogLevel.Information, + 4 => LogLevel.Debug, + 5 => LogLevel.Trace, + _ => LogLevel.Information + }; + AdvancedLoggerFactory.SetLogLevel(logLevel); + if (options.MapResource == DefaultArgumentOptions.MapResource) + game = new(MapInfo.defaultMapStruct, options.TeamCount); + else + { + // txt文本方案 + if (options.MapResource.EndsWith(".txt")) + { + try + { + uint[,] map = new uint[GameData.MapRows, GameData.MapCols]; + string? line; + int i = 0, j = 0; + using StreamReader sr = new(options.MapResource); + #region 读取txt地图 + while (!sr.EndOfStream && i < GameData.MapRows) + { + if ((line = sr.ReadLine()) != null) + { + string[] nums = line.Split(' '); + foreach (string item in nums) + { + if (item.Length > 1)//以兼容原方案 + map[i, j] = (uint)int.Parse(item); + else + //2022-04-22 by LHR 十六进制编码地图方案(防止地图编辑员瞎眼x + map[i, j] = (uint)MapEncoder.Hex2Dec(char.Parse(item)); + j++; + if (j >= GameData.MapCols) + { + j = 0; + break; + } + } + i++; + } + } + #endregion + game = new(new(GameData.MapRows, GameData.MapCols, map), options.TeamCount); + } + catch + { + game = new(MapInfo.defaultMapStruct, options.TeamCount); + } + } + // MapStruct二进制方案 + else if (options.MapResource.EndsWith(".map")) + { + try + { + game = new(MapStruct.FromFile(options.MapResource), options.TeamCount); + } + catch + { + game = new(MapInfo.defaultMapStruct, options.TeamCount); + } + } + else + { + game = new(MapInfo.defaultMapStruct, options.TeamCount); + } + } + currentMapMsg = new() { MapMessage = MapMsg() }; + playerNum = options.CharacterCount + options.HomeCount; + communicationToGameID = new long[TeamCount][]; + for (int i = 0; i < TeamCount; i++) + { + communicationToGameID[i] = new long[options.CharacterCount + options.HomeCount]; + } + //创建server时先设定待加入对象都是invalid + for (int team = 0; team < TeamCount; team++) + { + communicationToGameID[team][0] = GameObj.invalidID; // team + for (int i = 1; i <= options.CharacterCount; i++) + { + communicationToGameID[team][i] = GameObj.invalidID; //character + } + } + + if (options.FileName != DefaultArgumentOptions.FileName) + { + try + { + mwr = new(options.FileName, options.TeamCount, options.CharacterCount); + } + catch + { + GameServerLogging.logger.LogInfo($"Error: Cannot create the playback file: {options.FileName}!"); + } + } + + string? token2 = Environment.GetEnvironmentVariable("TOKEN"); + if (token2 == null) + { + GameServerLogging.logger.LogInfo("Null TOKEN Environment!"); + } + else + options.Token = token2; + if (options.Url != DefaultArgumentOptions.Url && options.Token != DefaultArgumentOptions.Token) + { + httpSender = new(options.Url, options.Token); + } + else + { + httpSender = new(DefaultArgumentOptions.Url, DefaultArgumentOptions.Token); + } + } + } +} diff --git a/logic/Server/HttpSender.cs b/logic/Server/HttpSender.cs new file mode 100644 index 00000000..7bde438d --- /dev/null +++ b/logic/Server/HttpSender.cs @@ -0,0 +1,81 @@ +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.Http.Json; + +namespace Server +{ + class HttpSender(string url, string token) + { + private string url = url; + private string token = token; + + public string Url + { + get { return url; } + set { url = value; } + } + + public string Token + { + get { return token; } + set { token = value; } + } + + // void Test() + // { + // this.SendHttpRequest(new()).Wait(); + // } + public async Task SendHttpRequest(int[] scores, string state, string[][] player_role) + { + try + { + var request = new HttpClient(); + request.DefaultRequestHeaders.Authorization = new("Bearer", token); + using var response = await request.PostAsync(url, JsonContent.Create(new + { + status = state, + scores = new int[] { scores[0], scores[1], scores[2], scores[3] }, + player_roles = player_role + })); + GameServerLogging.logger.LogInfo("Send to web successfully!"); + GameServerLogging.logger.LogInfo($"Web response: {await response.Content.ReadAsStringAsync()}"); + } + catch (Exception e) + { + GameServerLogging.logger.LogInfo("Fail to send msg to web!"); + GameServerLogging.logger.LogInfo(e.ToString()); + } + } + + public async Task GetLadderScore(double[] scores) + { + try + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Authorization = new("Bearer", token); + var response = await httpClient.PostAsync(url, null); + + // 读取响应内容为字符串 + var jsonString = await response.Content.ReadAsStringAsync(); + + // 解析 JSON 字符串 + var result = JsonConvert.DeserializeObject(jsonString); + return result.scores.Select(score => (double)score).ToArray(); + } + catch (Exception e) + { + GameServerLogging.logger.LogInfo("Error when pulling ladder score!"); + GameServerLogging.logger.LogInfo(e.ToString()); + return new double[0]; + } + + } + } + + internal class TeamScore + { + public int TeamID { get; set; } = 0; + public int Score { get; set; } = 0; + } +} \ No newline at end of file diff --git a/logic/Server/PlaybackServer.cs b/logic/Server/PlaybackServer.cs new file mode 100644 index 00000000..752ebe18 --- /dev/null +++ b/logic/Server/PlaybackServer.cs @@ -0,0 +1,331 @@ +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Playback; +using Protobuf; +using System.Collections.Concurrent; +using Timothy.FrameRateTask; + +namespace Server +{ + class PlaybackServer(ArgumentOptions options) : ServerBase + { + protected readonly ArgumentOptions options = options; + private long[] teamScore = []; + private readonly ConcurrentDictionary semaDict = new(); + // private object semaDictLock = new(); + private MessageToClient? currentGameInfo = new(); + private MessageOfObj currentMapMsg = new(); + private const uint spectatorMinPlayerID = 2023; + // private List spectatorList = new List(); + public int TeamCount => options.TeamCount; + private readonly object spectatorJoinLock = new(); + protected object spectatorLock = new(); + protected bool isSpectatorJoin = false; + protected bool IsSpectatorJoin + { + get + { + lock (spectatorLock) + return isSpectatorJoin; + } + + set + { + lock (spectatorLock) + isSpectatorJoin = value; + } + } + private bool IsGaming { get; set; } = true; + private int[] finalScore = []; + public int[] FinalScore + { + get + { + return finalScore; + } + } + public override int[] GetMoney() => []; + public override int[] GetScore() => FinalScore; + + public override async Task AddCharacter(CharacterMsg request, + IServerStreamWriter responseStream, + ServerCallContext context) + { + PlaybackServerLogging.logger.LogInfo($"AddPlayer: {request.CharacterId}"); + if (request.CharacterId >= spectatorMinPlayerID && options.NotAllowSpectator == false) + { + // 观战模式 + lock (spectatorJoinLock) // 具体原因见另一个上锁的地方 + { + if (semaDict.TryAdd(request.CharacterId, (new SemaphoreSlim(0, 1), new SemaphoreSlim(0, 1)))) + { + PlaybackServerLogging.logger.LogInfo("A new spectator comes to watch this game"); + IsSpectatorJoin = true; + } + else + { + PlaybackServerLogging.logger.LogInfo($"Duplicated Spectator ID {request.CharacterId}"); + return; + } + } + do + { + semaDict[request.CharacterId].Item1.Wait(); + try + { + if (currentGameInfo != null) + { + await responseStream.WriteAsync(currentGameInfo); + PlaybackServerLogging.logger.LogInfo("Send!"); + } + } + catch (InvalidOperationException) + { + if (semaDict.TryRemove(request.CharacterId, out var semas)) + { + try + { + semas.Item1.Release(); + semas.Item2.Release(); + } + catch { } + PlaybackServerLogging.logger.LogInfo($"The spectator {request.CharacterId} exited"); + return; + } + } + catch (Exception e) + { + PlaybackServerLogging.logger.LogInfo(e.ToString()); + } + finally + { + try + { + semaDict[request.CharacterId].Item2.Release(); + } + catch { } + } + } while (IsGaming); + return; + } + } + + public void ReportGame(MessageToClient? msg) + { + currentGameInfo = msg; + if (currentGameInfo != null && currentGameInfo.GameState == GameState.GameStart) + { + currentMapMsg = currentGameInfo.ObjMessage[0]; + } + + if (currentGameInfo != null && IsSpectatorJoin) + { + currentGameInfo.ObjMessage.Add(currentMapMsg); + IsSpectatorJoin = false; + } + + foreach (var kvp in semaDict) + { + kvp.Value.Item1.Release(); + } + + foreach (var kvp in semaDict) + { + kvp.Value.Item2.Wait(); + } + } + + public override void WaitForEnd() + { + try + { + if (options.ResultOnly) + { + using (MessageReader mr = new(options.FileName)) + { + PlaybackServerLogging.logger.LogInfo("Parsing playback file..."); + teamScore = new long[mr.teamCount]; + finalScore = new int[mr.teamCount]; + int infoNo = 0; + object cursorLock = new(); + var initialTop = Console.CursorTop; + var initialLeft = Console.CursorLeft; + while (true) + { + MessageToClient? msg = null; + for (int i = 0; i < mr.teamCount; ++i) + { + for (int j = 0; j < mr.playerCount; ++j) + { + msg = mr.ReadOne(); + if (msg == null) + { + PlaybackServerLogging.logger.LogInfo( + "The game doesn't come to an end because of timing up!"); + IsGaming = false; + goto endParse; + } + + lock (cursorLock) + { + var curTop = Console.CursorTop; + var curLeft = Console.CursorLeft; + Console.SetCursorPosition(initialLeft, initialTop); + PlaybackServerLogging.logger.LogInfo( + $"Parsing messages... Current message number: {infoNo}"); + Console.SetCursorPosition(curLeft, curTop); + } + + if (msg != null) + { + //teamScore[i] = msg.TeamScore; + } + } + } + + ++infoNo; + + if (msg == null) + { + PlaybackServerLogging.logger.LogInfo("No game information in this file!"); + goto endParse; + } + if (msg.GameState == GameState.GameEnd) + { + PlaybackServerLogging.logger.LogInfo("Game over normally!"); + finalScore[0] = msg.AllMessage.BuddhistsTeamScore; + finalScore[1] = msg.AllMessage.MonstersTeamScore; + goto endParse; + } + } + endParse: + PlaybackServerLogging.logger.LogInfo($"Successfully parsed {infoNo} informations!"); + } + } + else + { + long timeInterval = GameServer.SendMessageToClientIntervalInMilliseconds; + if (options.PlaybackSpeed != 1.0) + { + options.PlaybackSpeed = Math.Max(0.25, Math.Min(4.0, options.PlaybackSpeed)); + timeInterval = (int)Math.Round(timeInterval / options.PlaybackSpeed); + } + using MessageReader mr = new(options.FileName); + teamScore = new long[mr.teamCount]; + finalScore = new int[mr.teamCount]; + int infoNo = 0; + object cursorLock = new(); + var msgCurTop = Console.CursorTop; + var msgCurLeft = Console.CursorLeft; + var frt = new FrameRateTaskExecutor + ( + loopCondition: () => true, + loopToDo: () => + { + MessageToClient? msg = null; + + msg = mr.ReadOne(); + if (msg == null) + { + PlaybackServerLogging.logger.LogInfo( + "The game doesn't come to an end because of timing up!"); + IsGaming = false; + ReportGame(msg); + return false; + } + ReportGame(msg); + lock (cursorLock) + { + var curTop = Console.CursorTop; + var curLeft = Console.CursorLeft; + Console.SetCursorPosition(msgCurLeft, msgCurTop); + PlaybackServerLogging.logger.LogInfo( + $"Sending messages... Current message number: {infoNo}"); + Console.SetCursorPosition(curLeft, curTop); + } + if (msg != null) + { + foreach (var item in msg.ObjMessage) + { + if (item.TeamMessage != null) + teamScore[item.TeamMessage.TeamId] = item.TeamMessage.Score; + + } + } + + ++infoNo; + if (msg == null) + { + PlaybackServerLogging.logger.LogInfo("No game information in this file!"); + IsGaming = false; + ReportGame(msg); + return false; + } + if (msg.GameState == GameState.GameEnd) + { + PlaybackServerLogging.logger.LogInfo("Game over normally!"); + IsGaming = false; + finalScore[0] = msg.AllMessage.BuddhistsTeamScore; + finalScore[1] = msg.AllMessage.MonstersTeamScore; + ReportGame(msg); + return false; + } + return true; + }, + timeInterval: timeInterval, + finallyReturn: () => 0 + ) + { AllowTimeExceed = true, MaxTolerantTimeExceedCount = 5 }; + PlaybackServerLogging.logger.LogInfo("The server is well prepared!"); + PlaybackServerLogging.logger.LogInfo( + "Please MAKE SURE that you have opened all the clients to watch the game!"); + PlaybackServerLogging.logger.LogInfo( + "If ALL clients have opened, press any key to start"); + Console.ReadKey(); + + new Thread + ( + () => + { + var rateCurTop = Console.CursorTop; + var rateCurLeft = Console.CursorLeft; + lock (cursorLock) + { + rateCurTop = Console.CursorTop; + rateCurLeft = Console.CursorLeft; + PlaybackServerLogging.logger.LogInfo( + $"Send message to clients frame rate: {frt.FrameRate}"); + } + while (!frt.Finished) + { + lock (cursorLock) + { + var curTop = Console.CursorTop; + var curLeft = Console.CursorLeft; + Console.SetCursorPosition(rateCurLeft, rateCurTop); + PlaybackServerLogging.logger.LogInfo( + $"Send message to clients frame rate: {frt.FrameRate}"); + Console.SetCursorPosition(curLeft, curTop); + } + Thread.Sleep(1000); + } + } + ) + { IsBackground = true }.Start(); + + lock (cursorLock) + { + msgCurLeft = Console.CursorLeft; + msgCurTop = Console.CursorTop; + PlaybackServerLogging.logger.LogInfo("Sending messages..."); + } + frt.Start(); + } + } + finally + { + teamScore ??= []; + } + } + } +} \ No newline at end of file diff --git a/logic/Server/Program.cs b/logic/Server/Program.cs new file mode 100644 index 00000000..fb923284 --- /dev/null +++ b/logic/Server/Program.cs @@ -0,0 +1,76 @@ +using CommandLine; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Preparation.Utility; +using Preparation.Utility.Logging; +using Protobuf; + +namespace Server +{ + public class Program + { + /// + /// 通过网站 http://www.network-science.de/ascii/ 使用字体"standard"生成 + /// + private const string WelcomeMessage = """ + + """; + + private static (ServerBase, AdvancedLoggerFactory.Logger) CreateServer(ArgumentOptions options) + => options.Playback + ? (new PlaybackServer(options), PlaybackServerLogging.logger) + : (new GameServer(options), GameServerLogging.logger); + + public static int Main(string[] args) + { + var loggerMain = AdvancedLoggerFactory.CreateLogger("Main"); + var argsStr = string.Join(' ', args); + loggerMain.LogRaw(argsStr); + + ArgumentOptions? options = null; + _ = Parser.Default.ParseArguments(args).WithParsed(o => { options = o; }); + if (options == null) + { + loggerMain.LogError("Argument parsing failed!"); + return 1; + } + + if (options.StartLockFile == "114514") + { + loggerMain.LogRaw(WelcomeMessage); + } + loggerMain.LogInfo($"Server begins to run: {options.ServerPort}"); + + try + { + var (server, logger) = CreateServer(options); + Grpc.Core.Server rpcServer = new([new ChannelOption(ChannelOptions.SoReuseport, 0)]) + { + Services = { AvailableService.BindService(server) }, + Ports = { new ServerPort(options.IP, options.ServerPort, ServerCredentials.Insecure) } + }; + rpcServer.Start(); + + logger.LogInfo("Server begins to listen!"); + server.WaitForEnd(); + logger.LogInfo("Server end!"); + rpcServer.ShutdownAsync().Wait(); + + logger.LogInfo("=================== Final Score ===================="); + + logger.LogInfo($"Team0: {server.GetScore()[0]}"); // + logger.LogInfo($"Team1: {server.GetScore()[1]}"); // + logger.LogInfo($"Team2: {server.GetScore()[2]}"); // + logger.LogInfo($"Team3: {server.GetScore()[3]}"); // + } + catch (Exception ex) + { + loggerMain = AdvancedLoggerFactory.CreateLogger("Main"); + loggerMain.LogError(ex.ToString()); + if (ex.StackTrace is not null) + loggerMain.LogError(ex.StackTrace); + } + return 0; + } + } +} \ No newline at end of file diff --git a/logic/Server/Properties/launchSettings.json b/logic/Server/Properties/launchSettings.json new file mode 100644 index 00000000..1be99b0a --- /dev/null +++ b/logic/Server/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Server": { + "commandName": "Project", + "commandLineArgs": "--port 8888" + } + } +} \ No newline at end of file diff --git a/logic/Server/RpcServices.cs b/logic/Server/RpcServices.cs new file mode 100644 index 00000000..1548f040 --- /dev/null +++ b/logic/Server/RpcServices.cs @@ -0,0 +1,665 @@ +using GameClass.GameObj; +using Gaming; +using Grpc.Core; +using Microsoft.Extensions.Logging; +using Preparation.Utility; +using Protobuf; +using Utility = Preparation.Utility; + +namespace Server +{ + partial class GameServer : ServerBase + { + private int playerCountNow = 0; + protected object spectatorLock = new(); + protected bool isSpectatorJoin = false; + protected bool IsSpectatorJoin + { + get + { + lock (spectatorLock) return isSpectatorJoin; + } + + set + { + lock (spectatorLock) + isSpectatorJoin = value; + } + } + public override Task TryConnection(IDMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY TryConnection: Player {request.CharacterId} from Team {request.TeamId}"); + var onConnection = new BoolRes(); + lock (gameLock) + { + if (0 <= request.CharacterId && request.CharacterId < playerNum) + { + onConnection.ActSuccess = true; + GameServerLogging.logger.LogInfo($"TryConnection: {onConnection.ActSuccess}"); + return Task.FromResult(onConnection); + } + } + onConnection.ActSuccess = false; + GameServerLogging.logger.LogDebug("END TryConnection"); + return Task.FromResult(onConnection); + } + + #region 游戏开局调用一次的服务 + + protected readonly object addPlayerLock = new(); + public override async Task AddCharacter(CharacterMsg request, IServerStreamWriter responseStream, ServerCallContext context) + { +#if !DEBUG + GameServerLogging.logger.LogInfo($"AddPlayer: Player {request.CharacterId} from Team {request.TeamId}"); +#endif + if (request.CharacterId >= spectatorMinPlayerID && options.NotAllowSpectator == false) + { + GameServerLogging.logger.LogDebug($"TRY Add Spectator: Player {request.CharacterId}"); + // 观战模式 + lock (spectatorJoinLock) // 具体原因见另一个上锁的地方 + { + if (semaDict0.TryAdd(request.CharacterId, (new SemaphoreSlim(0, 1), new SemaphoreSlim(0, 1)))) + { + GameServerLogging.logger.LogInfo("A new spectator comes to watch this game"); + IsSpectatorJoin = true; + } + else + { + GameServerLogging.logger.LogInfo($"Duplicated Spectator ID {request.CharacterId}"); + return; + } + } + do + { + semaDict0[request.CharacterId].Item1.Wait(); + try + { + if (currentGameInfo != null) + { + var info = currentGameInfo.Clone(); + for (int i = info.ObjMessage.Count - 1; i >= 0; i--) + { + if (info.ObjMessage[i].NewsMessage != null) + { + info.ObjMessage.RemoveAt(i); + } + } + await responseStream.WriteAsync(info); + // GameServerLogging.logger.LogInfo("Send!", false); + } + } + catch (InvalidOperationException) + { + if (semaDict0.TryRemove(request.CharacterId, out var semas)) + { + try + { + semas.Item1.Release(); + semas.Item2.Release(); + } + catch + { + } + GameServerLogging.logger.LogInfo($"The spectator {request.CharacterId} exited"); + return; + } + } + catch (Exception ex) + { + GameServerLogging.logger.LogInfo(ex.ToString()); + } + finally + { + try + { + semaDict0[request.CharacterId].Item2.Release(); + } + catch + { + } + } + } while (game.GameMap.Timer.IsGaming); + GameServerLogging.logger.LogDebug("END Add Spectator"); + return; + } + GameServerLogging.logger.LogDebug( + $"TRY Add Player: Player {request.CharacterId} from Team {request.TeamId}"); + if (game.GameMap.Timer.IsGaming) + return; + if (!ValidPlayerID(request.CharacterId)) //玩家id是否正确 + return; + if (request.TeamId >= TeamCount) //队伍只能是0,1 + return; + if (communicationToGameID[request.TeamId][request.CharacterId] != GameObj.invalidID) //是否已经添加了该玩家 + return; + GameServerLogging.logger.LogDebug("AddPlayer: Check Correct"); + lock (addPlayerLock) + { + GameServerLogging.logger.LogDebug("ch id :" + request.CharacterId + " te id:" + request.TeamId + " type :" + request.CharacterType + " side: " + request.SideFlag); + Game.PlayerInitInfo playerInitInfo = new(request.TeamId, request.CharacterId, Transformation.CharacterTypeFromProto(request.CharacterType), request.SideFlag); + long newPlayerID = game.AddCharacter(playerInitInfo); + if (newPlayerID == GameObj.invalidID) + { + GameServerLogging.logger.LogError("FAIL AddPlayer"); + return; + } + communicationToGameID[request.TeamId][request.CharacterId] = newPlayerID; + var temp = (new SemaphoreSlim(0, 1), new SemaphoreSlim(0, 1)); + bool start = false; + GameServerLogging.logger.LogInfo($"Player {request.CharacterId} from Team {request.TeamId} joins"); + lock (spectatorJoinLock) // 为了保证绝对安全,还是加上这个锁吧 + { + if (request.TeamId == 0) + { + if (semaDict0.TryAdd(request.CharacterId, temp)) + { + start = Interlocked.Increment(ref playerCountNow) == (playerNum * TeamCount); + GameServerLogging.logger.LogDebug($"PlayerCountNow: {playerCountNow}"); + GameServerLogging.logger.LogDebug($"PlayerTotalNum: {playerNum * TeamCount}"); + } + } + else if (request.TeamId == 1) + { + if (semaDict1.TryAdd(request.CharacterId, temp)) + { + start = Interlocked.Increment(ref playerCountNow) == (playerNum * TeamCount); + GameServerLogging.logger.LogDebug($"PlayerCountNow: {playerCountNow}"); + GameServerLogging.logger.LogDebug($"PlayerNum: {playerNum * TeamCount}"); + } + } + } + if (start) + { + StartGame(); + } + } + bool exitFlag = false; + bool firstTime = true; + do + { + if (request.TeamId == 0) + semaDict0[request.CharacterId].Item1.Wait(); + else if (request.TeamId == 1) + semaDict1[request.CharacterId].Item1.Wait(); + Character? character = game.GameMap.FindCharacterInPlayerID(request.TeamId, request.CharacterId); + // if(character!=null) + // { + // GameServerLogging.logger.LogInfo($"Character {request.PlayerId} exist! IsRemoved {character.IsRemoved}"); + // } + // else{ + // GameServerLogging.logger.LogInfo($"Character {request.PlayerId} null"); + // } + if (!firstTime && request.CharacterId > 0 && (character == null || character.IsRemoved == true)) + { + // GameServerLogging.logger.LogInfo($"Cannot find character {request.PlayerId} from Team {request.TeamId}!"); + } + else + { + if (firstTime) + firstTime = false; + try + { + if (currentGameInfo != null && !exitFlag) + { + await responseStream.WriteAsync(currentGameInfo); + // GameServerLogging.logger.LogInfo( + // $"Send to Player{request.CharacterId} from Team {request.TeamId}!", + // false); + } + } + catch + { + if (!exitFlag) + { + GameServerLogging.logger.LogInfo($"The client {request.CharacterId} exited"); + exitFlag = true; + } + } + } + (request.TeamId == 0 ? semaDict0 : semaDict1)[request.CharacterId].Item2.Release(); + } while (game.GameMap.Timer.IsGaming); + } + + public override Task GetMap(NullRequest request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug($"GetMap: IP {context.Peer}"); + return Task.FromResult(MapMsg()); + } + + #endregion + + #region 游戏过程中玩家执行操作的服务 + + #region 普通角色操作 + + /*public override Task Activate(ActivateMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug($"TRY Activate: Player {request.PlayerId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.PlayerId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.ActivateCharacter(request.TeamId, Transformation.CharacterTyprFromProto(request.CharacterType)); + if (!game.GameMap.Timer.IsGaming) boolRes.ActSuccess = false; + GameServerLogging.logger.LogDebug($"END Activate: {boolRes.ActSuccess}"); + return Task.FromResult(boolRes); + }*/ + + public override Task Move(MoveMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Move: Player {request.CharacterId} from Team {request.TeamId}, " + + $"TimeInMilliseconds: {request.TimeInMilliseconds}"); + MoveRes moveRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + moveRes.ActSuccess = false; + return Task.FromResult(moveRes); + } + if (double.IsNaN(request.Angle)) + { + moveRes.ActSuccess = false; + return Task.FromResult(moveRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + moveRes.ActSuccess = game.MoveCharacter( + request.TeamId, request.CharacterId, + (int)request.TimeInMilliseconds, request.Angle); + if (!game.GameMap.Timer.IsGaming) + moveRes.ActSuccess = false; + GameServerLogging.logger.LogDebug($"END Move: {moveRes.ActSuccess}"); + return Task.FromResult(moveRes); + } + + public override Task Recover(RecoverMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Recover: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Recover(request.TeamId, request.CharacterId, request.RecoveredHp); + GameServerLogging.logger.LogDebug("END Recover"); + return Task.FromResult(boolRes); + } + + public override Task Produce(IDMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Produce: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Produce(request.TeamId, request.CharacterId); + GameServerLogging.logger.LogDebug("END Produce"); + return Task.FromResult(boolRes); + } + + + public override Task Rebuild(ConstructMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Rebuild: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Construct( + request.TeamId, request.CharacterId, + Transformation.ConstructionFromProto(request.ConstructionType)); + GameServerLogging.logger.LogDebug("END Rebuild"); + return Task.FromResult(boolRes); + } + + public override Task Construct(ConstructMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Construct: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Construct( + request.TeamId, request.CharacterId, + Transformation.ConstructionFromProto(request.ConstructionType)); + GameServerLogging.logger.LogDebug("END Construct"); + return Task.FromResult(boolRes); + } + + public override Task ConstructTrap(ConstructTrapMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY ConstructTrap: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Construct( + request.TeamId, request.CharacterId, + Transformation.TrapTypeFromProto(request.TrapType)); + GameServerLogging.logger.LogDebug("END ConstructTrap"); + return Task.FromResult(boolRes); + } + + public override Task Equip(EquipMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Construct: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Equip( + request.TeamId, request.CharacterId, + Transformation.EquipmentTypeFromProto(request.EquipmentType)); + GameServerLogging.logger.LogDebug("END Equip"); + return Task.FromResult(boolRes); + } + + public override Task Attack(AttackMsg request, ServerCallContext context) + { + + GameServerLogging.logger.LogDebug( + $"TRY Attack: Player {request.CharacterId} from Team {request.TeamId} attacking Player {request.AttackedCharacterId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + if (request.AttackedCharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // if (request.AttackRange <= 0) + // { + // boolRes.ActSuccess = false; + // return Task.FromResult(boolRes); + // } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Attack( + request.TeamId, request.CharacterId, + request.AttackedTeam, request.AttackedCharacterId); + GameServerLogging.logger.LogDebug("END Attack"); + return Task.FromResult(boolRes); + } + + public override Task Cast(CastMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Cast: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + /*if (request.SkillId <= 0) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + }*/ + // if (request.AttackRange <= 0) + // { + // boolRes.ActSuccess = false; + // return Task.FromResult(boolRes); + // } + boolRes.ActSuccess = game.CastSkill( + request.TeamId, request.CharacterId, request.Angle); + GameServerLogging.logger.LogDebug("END Cast"); + return Task.FromResult(boolRes); + } + + public override Task AttackConstruction(AttackConstructionMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY AttackConstruction: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.AttackConstruction( + request.TeamId, request.CharacterId + ); + GameServerLogging.logger.LogDebug("END AttackConstruction"); + return Task.FromResult(boolRes); + } + + public override Task AttackAdditionResource(AttackAdditionResourceMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY AttackAdditionResource: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.AttackResource( + request.TeamId, request.CharacterId + ); + GameServerLogging.logger.LogDebug("END AttackAdditionResource"); + return Task.FromResult(boolRes); + } + + public override Task Send(SendMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY Send: From Player {request.CharacterId} To Player {request.ToCharacterId} from Team {request.TeamId}"); + var boolRes = new BoolRes(); + if (request.CharacterId >= spectatorMinPlayerID || PlayerDeceased((int)request.CharacterId)) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + if (!ValidPlayerID(request.CharacterId) + || !ValidPlayerID(request.ToCharacterId) + || request.CharacterId == request.ToCharacterId) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + GameServerLogging.logger.LogDebug($"Send: As {request.MessageCase}"); + switch (request.MessageCase) + { + case SendMsg.MessageOneofCase.TextMessage: + { + if (request.TextMessage.Length > 256) + { + GameServerLogging.logger.LogDebug("Send: Text message string is too long!"); + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + MessageOfNews news = new() + { + TextMessage = request.TextMessage, + FromId = request.CharacterId, + ToId = request.ToCharacterId, + TeamId = request.TeamId + }; + lock (newsLock) + { + currentNews.Add(news); + } + GameServerLogging.logger.LogDebug("Send: Text: " + news.TextMessage); + boolRes.ActSuccess = true; + GameServerLogging.logger.LogDebug($"END Send"); + return Task.FromResult(boolRes); + } + case SendMsg.MessageOneofCase.BinaryMessage: + { + if (request.BinaryMessage.Length > 256) + { + GameServerLogging.logger.LogDebug("Send: Binary message string is too long!"); + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + MessageOfNews news = new() + { + BinaryMessage = request.BinaryMessage, + FromId = request.CharacterId, + ToId = request.ToCharacterId, + TeamId = request.TeamId + }; + lock (newsLock) + { + currentNews.Add(news); + } + GameServerLogging.logger.LogDebug($"BinaryMessageLength: {news.BinaryMessage.Length}"); + boolRes.ActSuccess = true; + GameServerLogging.logger.LogDebug($"END Send"); + return Task.FromResult(boolRes); + } + default: + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + } + } + + #endregion + + #region 核心角色操作 + + public override Task CreatCharacter(CreatCharacterMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY CreatCharacter: CharacterType {request.CharacterType} from Team {request.TeamId}"); + var activateCost = Transformation.CharacterTypeFromProto(request.CharacterType) switch + { + Utility.CharacterType.TangSeng => GameData.TangSengcost, + Utility.CharacterType.SunWukong => GameData.SunWukongcost, + Utility.CharacterType.ZhuBajie => GameData.ZhuBajiecost, + Utility.CharacterType.ShaWujing => GameData.ShaWujingcost, + Utility.CharacterType.BaiLongma => GameData.BaiLongmacost, + Utility.CharacterType.Monkid => GameData.Monkidcost, + + Utility.CharacterType.JiuLing => GameData.JiuLingcost, + Utility.CharacterType.HongHaier => GameData.HongHaiercost, + Utility.CharacterType.NiuMowang => GameData.NiuMowangcost, + Utility.CharacterType.TieShan => GameData.TieShancost, + Utility.CharacterType.ZhiZhujing => GameData.ZhiZhujingcost, + Utility.CharacterType.Pawn => GameData.Pawncost, + + _ => int.MaxValue + }; + var teamMoneyPool = game.TeamList[(int)request.TeamId].MoneyPool; + if (activateCost > teamMoneyPool.Money) + { + return Task.FromResult(new BoolRes { ActSuccess = false }); + } + /*if (game.TeamList[(int)request.TeamId].Hero.HP <= 0) + { + return Task.FromResult(new BoolRes { ActSuccess = false }); + }*/ + BoolRes boolRes = new() + { + ActSuccess = + game.ActivateCharacter( + request.TeamId, + Transformation.CharacterTypeFromProto(request.CharacterType), + request.BirthpointIndex) + != GameObj.invalidID + }; + if (boolRes.ActSuccess) teamMoneyPool.SubMoney(activateCost); + GameServerLogging.logger.LogDebug("END CreatCharacter"); + return Task.FromResult(boolRes); + } + + public override Task CreatCharacterRID(CreatCharacterMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY CreatCharacter: CharacterType {request.CharacterType} from Team {request.TeamId}"); + var activateCost = Transformation.CharacterTypeFromProto(request.CharacterType) switch + { + Utility.CharacterType.TangSeng => GameData.TangSengcost, + Utility.CharacterType.SunWukong => GameData.SunWukongcost, + Utility.CharacterType.ZhuBajie => GameData.ZhuBajiecost, + Utility.CharacterType.ShaWujing => GameData.ShaWujingcost, + Utility.CharacterType.BaiLongma => GameData.BaiLongmacost, + Utility.CharacterType.Monkid => GameData.Monkidcost, + + Utility.CharacterType.JiuLing => GameData.JiuLingcost, + Utility.CharacterType.HongHaier => GameData.HongHaiercost, + Utility.CharacterType.NiuMowang => GameData.NiuMowangcost, + Utility.CharacterType.TieShan => GameData.TieShancost, + Utility.CharacterType.ZhiZhujing => GameData.ZhiZhujingcost, + Utility.CharacterType.Pawn => GameData.Pawncost, + + _ => int.MaxValue + }; + var teamMoneyPool = game.TeamList[(int)request.TeamId].MoneyPool; + if (activateCost > teamMoneyPool.Money) + { + return Task.FromResult(new CreatCharacterRes { ActSuccess = false }); + } + /*if (game.TeamList[(int)request.TeamId].Hero.HP <= 0) + { + return Task.FromResult(new CreatCharacterRes { ActSuccess = false }); + }*/ + var playerId = game.ActivateCharacter( + request.TeamId, + Transformation.CharacterTypeFromProto(request.CharacterType), + request.BirthpointIndex); + + CreatCharacterRes creatCharacterRes = new() + { + ActSuccess = playerId != GameObj.invalidID, + PlayerId = playerId + }; + if (creatCharacterRes.ActSuccess) teamMoneyPool.SubMoney(activateCost); + GameServerLogging.logger.LogDebug("END CreatCharacterRID"); + return Task.FromResult(creatCharacterRes); + } + + public override Task EndAllAction(IDMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug( + $"TRY EndAllAction: Player {request.CharacterId} from Team {request.TeamId}"); + BoolRes boolRes = new(); + if (request.CharacterId >= spectatorMinPlayerID) + { + boolRes.ActSuccess = false; + return Task.FromResult(boolRes); + } + // var gameID = communicationToGameID[request.TeamId][request.PlayerId]; + boolRes.ActSuccess = game.Stop(request.TeamId, request.CharacterId); + GameServerLogging.logger.LogDebug("END EndAllAction"); + return Task.FromResult(boolRes); + } + + #endregion + + #endregion + } +} diff --git a/logic/Server/Server.csproj b/logic/Server/Server.csproj new file mode 100644 index 00000000..1432b241 --- /dev/null +++ b/logic/Server/Server.csproj @@ -0,0 +1,32 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/logic/Server/ServerBase.cs b/logic/Server/ServerBase.cs new file mode 100644 index 00000000..95b7f60d --- /dev/null +++ b/logic/Server/ServerBase.cs @@ -0,0 +1,13 @@ +using Protobuf; + +namespace Server +{ + abstract class ServerBase : AvailableService.AvailableServiceBase + { + public abstract void WaitForEnd(); + //public abstract int[] GetMoney(); + public abstract int[] GetScore(); + public abstract int[] GetMaterial(); + public abstract int[] GetComputingPower(); + } +} \ No newline at end of file diff --git a/logic/Server/ServerLogging.cs b/logic/Server/ServerLogging.cs new file mode 100644 index 00000000..87179d78 --- /dev/null +++ b/logic/Server/ServerLogging.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Logging; +using Preparation.Utility.Logging; + +namespace Server +{ + public static class GameServerLogging + { + public static readonly AdvancedLoggerFactory.Logger logger = AdvancedLoggerFactory.CreateLogger("GameServer"); + } + + public static class PlaybackServerLogging + { + public static readonly AdvancedLoggerFactory.Logger logger = AdvancedLoggerFactory.CreateLogger("PlaybackServer"); + } +} \ No newline at end of file