/// <summary> /// Обрабатывает полученное сообщение по TCP соединению для запросов статистики /// </summary> void OnStatsReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { var str = Encoding.UTF8.GetString(GSUtils.XorBytes(buffer, 0, count - 7, "GameSpy3D"), 0, count); LogTrace("STATS " + str); // Авторизация на сервере. Выдаем случайный ключ сессии (не обязательно) if (str.StartsWith(@"\auth\\gamename\", StringComparison.OrdinalIgnoreCase)) { var sesskey = Interlocked.Increment(ref _sessionCounter).ToString("0000000000"); handler.Send(node, GSUtils.XorBytes($@"\lc\2\sesskey\{sesskey}\proof\0\id\1\final\", "GameSpy3D", 7)); return; } // Игра присылает серверу ID авторизованного профиля. Но мы и так его знаем, поэтому просто фейкаем успех if (str.StartsWith(@"\authp\\pid\", StringComparison.OrdinalIgnoreCase)) { var pid = GetPidFromInput(str, 12); var profileId = long.Parse(pid); handler.Send(node, GSUtils.XorBytes($@"\pauthr\{pid}\lid\1\final\", "GameSpy3D", 7)); return; } // Запрос данных профиля. Надо отправить данные по списку запрошенных ключей. // Всегда запрашивается статистика if (str.StartsWith(@"\getpd\", StringComparison.OrdinalIgnoreCase)) { // \\getpd\\\\pid\\87654321\\ptype\\3\\dindex\\0\\keys\\\u0001points\u0001points2\u0001points3\u0001stars\u0001games\u0001wins\u0001disconn\u0001a_durat\u0001m_streak\u0001f_race\u0001SM_wins\u0001Chaos_wins\u0001Ork_wins\u0001Tau_wins\u0001SoB_wins\u0001DE_wins\u0001Eldar_wins\u0001IG_wins\u0001Necron_wins\u0001lsw\u0001rnkd_vics\u0001con_rnkd_vics\u0001team_vics\u0001mdls1\u0001mdls2\u0001rg\u0001pw\\lid\\1\\final\\ // \getpd\\pid\87654321\ptype\3\dindex\0\keys\pointspoints2points3starsgameswinsdisconna_duratm_streakf_raceSM_winsChaos_winsOrk_winsTau_winsSoB_winsDE_winsEldar_winsIG_winsNecron_winslswrnkd_vicscon_rnkd_vicsteam_vicsmdls1mdls2rgpw\lid\1\final\ var profileId = GetPidFromInput(str, 12); var keysIndex = str.IndexOf("keys") + 5; var keys = str.Substring(keysIndex); var keysList = keys.Split(new string[] { "\u0001", "\\lid\\1\\final\\", "final", "\\", "lid" }, StringSplitOptions.RemoveEmptyEntries); var keysResult = new StringBuilder(); var stats = _emulationAdapter.GetUserStatsInfo(profileId.ParseToLongOrDefault()); for (int i = 0; i < keysList.Length; i++) { var key = keysList[i]; keysResult.Append("\\" + key + "\\"); switch (key) { case "points": keysResult.Append(stats.Score1v1); break; case "points2": keysResult.Append(stats.Score2v2); break; case "points3": keysResult.Append(stats.Score3v3_4v4); break; case "stars": keysResult.Append(stats.StarsCount); break; case "games": keysResult.Append(stats.GamesCount); break; case "wins": keysResult.Append(stats.WinsCount); break; case "disconn": keysResult.Append(stats.Disconnects); break; case "a_durat": keysResult.Append(stats.AverageDuration); break; case "m_streak": keysResult.Append(stats.Best1v1Winstreak); break; case "f_race": keysResult.Append(stats.FavouriteRace); break; // Ключи, которые не используюся игрой, но запрашиваются. Может на что-то и влияет, но я ничего не обнаружил /* case "SM_wins": keysResult.Append("0"); break; * case "Chaos_wins": keysResult.Append("0"); break; * case "Ork_wins": keysResult.Append("0"); break; * case "Tau_wins": keysResult.Append("0"); break; * case "SoB_wins": keysResult.Append("0"); break; * case "DE_wins": keysResult.Append("0"); break; * case "Eldar_wins": keysResult.Append("0"); break; * case "IG_wins": keysResult.Append("0"); break; * case "Necron_wins": keysResult.Append("0"); break; * case "lsw": keysResult.Append("0"); break; * case "rnkd_vics": keysResult.Append("0"); break; * case "con_rnkd_vics": keysResult.Append("0"); break; * case "team_vics": keysResult.Append("0"); break; * case "mdls1": keysResult.Append("0"); break; * case "mdls2": keysResult.Append("0"); break; * case "rg": keysResult.Append("0"); break; * case "pw": keysResult.Append("0"); break;*/ default: keysResult.Append("0"); break; } } handler.Send(node, GSUtils.XorBytes($@"\getpdr\1\lid\1\pid\{profileId}\mod\{stats.ModifiedTimeTick}\length\{keys.Length}\data\{keysResult}\final\", "GameSpy3D", 7)); return; } // Игра присылает обновление данных профиля по списку ключей. // Игнорируем, потому что статистика обновляется другим способов. Просто фейкаем успех if (str.StartsWith(@"\setpd\", StringComparison.OrdinalIgnoreCase)) { var pid = GetPidFromInput(str, 12); var lidIndex = str.IndexOf("\\lid\\", StringComparison.OrdinalIgnoreCase); var lid = str.Substring(lidIndex + 5, 1); var timeInSeconds = (ulong)((DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); // \setpd\\pid\3\ptype\1\dindex\0\kv\1\lid\1\length\413\data\\ckey\5604 - 7796 - 6425 - 0127 - DA96\system\Nr.Proc:8, Type: 586, GenuineIntel, unknown: f = 6,m = 12, Fam: 6, Mdl: 12, St: 3, Fe: 7, OS: 7, Ch: 15\speed\CPUSpeed: 3.5Mhz\os\OS NT 6.2\lang\Language: Русский(Россия), Country: Россия, User Language:Русский(Россия), User Country:Россия\vid\Card: Dx9: Hardware TnL, NVIDIA GeForce GTX 1080, \vidmod\Mode: 1920 x 1080 x 32\mem\2048Mb phys. memory handler.Send(node, GSUtils.XorBytes($@"\setpdr\1\lid\{lid}\pid\{pid}\mod\{timeInSeconds}\final\", "GameSpy3D", 7)); return; } // Игры присылает информацию о завершенной игре (камстока или автоматч) // Отсюда можно обновлять статистику игроков. Может быть прислано несколько раз от разных игроков в конце одного и того же матча, // поэтому необходима защита на сервере от двойного засчитывания статистики if (str.StartsWith(@"\updgame\", StringComparison.OrdinalIgnoreCase)) { var gamedataIndex = str.IndexOf("gamedata"); // Обрезаем конец сообщения var finalIndex = str.IndexOf("final"); var gameDataString = str.Substring(gamedataIndex + 9, finalIndex - gamedataIndex - 10); var valuesList = gameDataString.Split(new string[] { "\u0001", "\\lid\\1\\final\\", "\\" }, StringSplitOptions.None); // Преобразываем все пары ключ-значение в словарь для удобства var dictionary = new Dictionary <string, string>(); for (int i = 0; i < valuesList.Length - 1; i += 2) { if (i == valuesList.Length - 1) { continue; } dictionary[valuesList[i]] = valuesList[i + 1]; } if (!dictionary.TryGetValue("Mod", out string v)) { dictionary.Clear(); for (int i = 1; i < valuesList.Length - 1; i += 2) { if (i == valuesList.Length - 1) { continue; } dictionary[valuesList[i]] = valuesList[i + 1]; } } // Использованный мод и его версия var mod = dictionary["Mod"]; var modVersion = dictionary["ModVer"]; var playersCount = int.Parse(dictionary["Players"]); for (int i = 0; i < playersCount; i++) { // Dont process games with AI if (dictionary["PHuman_" + i] != "1") { LogTrace($"Stats socket: GAME WITH NONHUMAN PLAYER"); return; } } var gameInternalSession = dictionary["SessionID"]; var teamsCount = int.Parse(dictionary["Teams"]); var version = dictionary["Version"]; // Строим уникальные идентификатор сессии игры, чтобы в дальнейшем не зачислить дважды одну и ту же игру var uniqueGameSessionBuilder = new StringBuilder(gameInternalSession); for (int i = 0; i < playersCount; i++) { uniqueGameSessionBuilder.Append('<'); uniqueGameSessionBuilder.Append(dictionary["player_" + i]); uniqueGameSessionBuilder.Append('>'); } var uniqueSession = uniqueGameSessionBuilder.ToString(); // Строим объекты с данным игроков var players = new PlayerData[playersCount]; for (int i = 0; i < players.Length; i++) { var player = new PlayerData(); player.Name = dictionary["player_" + i]; player.Race = dictionary["PRace_" + i]; player.Team = int.Parse(dictionary["PTeam_" + i]); player.FinalState = (PlayerFinalState)Enum.Parse(typeof(PlayerFinalState), dictionary["PFnlState_" + i]); players[i] = player; } // Собираем окончательный объект с данными var gameFinishedMessage = new GameFinishedData { Map = dictionary["Scenario"], SessionId = uniqueSession, Duration = long.Parse(dictionary["Duration"]), ModName = dictionary["Mod"], ModVersion = dictionary["ModVer"], Players = players, IsRateGame = dictionary["Ladder"] == "1" }; _emulationAdapter.SendGameFinishedData(gameFinishedMessage); //DowstatsReplaySender.SendReplay(gameFinishedMessage); return; } // Создание новой игры до регистрации статистики. Никакой полезной информации нет, поэтому игнорируем. // Вся логика произойдет в момент отправки данных об игре. if (str.StartsWith(@"\newgame\", StringComparison.OrdinalIgnoreCase)) { return; } // На случай, если есть еще какие-то команды Debugger.Break(); }
/// <summary> /// Обрабатывает получение нового TPC соединения для получения статистики игроков /// </summary> void OnStatsAccept(TcpPortHandler handler, TcpClientNode node, CancellationToken token) { // Отправляем challenge сервера шифрованный через Xor. Надо отправить, но особо ни на что не влияет // Все сообщения также надо будет прогонять через Xor. handler.Send(GSUtils.XorBytes(@"\lc\1\challenge\KNDVKXFQWP\id\1\final\", "GameSpy3D", 7)); }
void OnServerRetrieveReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { var str = buffer.ToASCII(count); LogTrace("RETRIEVE " + str); var endPoint = node.RemoteEndPoint; if (endPoint == null) { handler.KillClient(node); return; } string[] data = str.Split(new char[] { '\x00' }, StringSplitOptions.RemoveEmptyEntries); string validate = data[4]; string filter = null; bool isAutomatch = false; if (validate.Length > 8) { filter = validate.Substring(8); validate = validate.Substring(0, 8); } else { //Log(Category, "ROOMS REQUEST - "+ data[2]); isAutomatch = data[2].EndsWith("am"); if (!isAutomatch) { SendChatRooms(handler, node, validate); return; } } var lobbies = _emulationAdapter.GetOpenedLobbies(); try { // var currentRating = ServerContext.ChatServer.CurrentRating; /*for (int i = 0; i < lobbies.Length; i++) * { * var server = lobbies[i]; * * //server["score_"] = GetCurrentRating(server.MaxPlayers); * }*/ var fields = data[5].Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); var unencryptedBytes = ParseHelper.PackServerList(endPoint, lobbies, fields, isAutomatch); _lastLoadedLobbies.Clear(); for (int i = 0; i < lobbies.Length; i++) { var server = lobbies[i]; var address = server.HostIP ?? server.LocalIP; var port = ushort.Parse(server.HostPort ?? server.LocalPort); var channelHash = ChatCrypt.PiStagingRoomHash(address, address, port); Log($"HASHFOR {address}:{port} {channelHash}"); server.RoomHash = channelHash; _lastLoadedLobbies[channelHash] = server; } Log("SERVERS VALIDATE VALUE ~" + validate + "~"); var encryptedBytes = GSEncoding.Encode(_gameGSkeyBytes, validate.ToAsciiBytes(), unencryptedBytes, unencryptedBytes.LongLength); Log("SERVERS bytes " + encryptedBytes.Length); int autoGames = 0; int customGames = 0; // TODO вынести в отдельно for (int i = 0; i < lobbies.Length; i++) { var server = lobbies[i]; if (server.Ranked) { autoGames++; } else { customGames++; } } //CoreContext.ClientServer.SendAsServerMessage( // "Received game list: " + customGames + " - custom; " + autoGames + // " - auto; Mod: "+ CoreContext.ThunderHawkModManager.CurrentModName); handler.Send(node, encryptedBytes); } finally { handler.KillClient(node); } }
/// <summary> /// Отправляем список публичных комнат в чате. /// Сейчас фейкает только один чат рум GPG1 с названием "Room 1" /// Игра сама подставит красивое имя из HTTP данных о списке комнат /// </summary> /// <param name="validate">Строка валидации запроса. Должна быть получена от игры</param> void SendChatRooms(TcpPortHandler handler, TcpClientNode node, string validate) { var bytes = new List <byte>(); //var remoteEndPoint = handler.RemoteEndPoint; //bytes.AddRange(remoteEndPoint.Address.GetAddressBytes()); bytes.AddRange(IPAddress.Loopback.GetAddressBytes()); byte[] value2 = BitConverter.GetBytes((ushort)6500); bytes.AddRange(BitConverter.IsLittleEndian ? value2.Reverse() : value2); bytes.Add(5); // fields count bytes.Add(0); // Забивает поля, которые нужны игре. В этом же порядке надо будет пихнуть значения дальше для каждой комнаты bytes.AddRange("hostname".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numwaiting".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("maxwaiting".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numservers".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); bytes.AddRange("numplayersname".ToAsciiBytes()); bytes.Add(0); bytes.Add(0); // Изначально было 10 комнат в игре, но мы сделаем только одну и весь код написан для синхронизации чата в игре с чатом из лаунчера // for (int i = 1; i <= 10; i++) // { // Странный байт в начале инфы о комнате bytes.Add(81); // инфа об IP комнаты, но игре на нее пофиг var b2 = BitConverter.GetBytes((long)1); bytes.Add(b2[3]); bytes.Add(b2[2]); bytes.Add(b2[1]); bytes.Add(b2[0]); // инфа о порте комнаты, но игре на нее пофиг bytes.Add(0); bytes.Add(0); // Скрытое название комнаты. Только такой формат принимает с цифрой в конце bytes.Add(255); bytes.AddRange("Room 1".ToAsciiBytes()); bytes.Add(0); // Количество игроков в комнате bytes.Add(255); bytes.AddRange(_emulationAdapter.ActivePlayersCount.ToString().ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("1000".ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("1".ToAsciiBytes()); bytes.Add(0); bytes.Add(255); bytes.AddRange("20".ToAsciiBytes()); bytes.Add(0); // } // Непонятный набор байт в конце, но без него не сработает bytes.AddRange(new byte[] { 0, 255, 255, 255, 255 }); var array = bytes.ToArray(); // Шифруем алгоритмом спая. Участвует строка валидации и уникальный ключ игры byte[] enc = GSEncoding.Encode(_gameGSkeyBytes, validate.ToAsciiBytes(), array, array.LongLength); handler.Send(node, enc); handler.KillClient(node); }
/// <summary> /// Обработка HTTP запроса от игры на порт 80. /// Обрабатывает новостное сообщение, наличие патча, запрос страницы статистики, настройки автоматча и список имен комнат чата. /// </summary> void OnHttpReceived(TcpPortHandler handler, TcpClientNode node, byte[] buffer, int count) { try { var str = buffer.ToUtf8(count); LogTrace("HTTP CLIENT HASH " + node.GetHashCode()); LogTrace("HTTP " + str); HttpRequest request; using (var ms = new MemoryStream(buffer, 0, count, false, true)) request = HttpHelper.GetRequest(ms); using (var ms = new MemoryStream()) { // Запрос страницы статистики по кнопке из игры if (request.Url.StartsWith("/SS_StatsPage", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.DowstatsRedirect()); goto END; } // Запрос текста новостей if (request.Url.EndsWith("news.txt", StringComparison.OrdinalIgnoreCase)) { LogForUser($"News requested"); // Фикс для рускоязычных if (request.Url.EndsWith("Russiandow_news.txt", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RusNews, Encoding.Unicode)); } else { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.EnNews, Encoding.Unicode)); } goto END; } // Отправка сообщения дня. Вроде нигде не отображается if (request.Url.StartsWith("/motd/motd", StringComparison.OrdinalIgnoreCase)) { HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RusNews, Encoding.Unicode)); goto END; } // Проверка на существование патча. Можно прокидывать свои патчи для игры if (request.Url.StartsWith("/motd/vercheck", StringComparison.OrdinalIgnoreCase)) { LogForUser($"Vercheck requested"); // Пример отправки ссылки на скачивания патча //HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(@"\newver\1\newvername\1.4\dlurl\http://127.0.0.1/NewPatchHere.exe")); // Отправка инфы о том, что патча сейчас нет HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(@"\newver\0", Encoding.UTF8)); goto END; } // Запрос списка комнат с именами для отображения в интерфейсе if (request.Url.EndsWith("LobbyRooms.lua", StringComparison.OrdinalIgnoreCase)) { LogForUser($"LobbyRooms requested"); HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.RoomPairs, Encoding.ASCII)); goto END; } // Запрос дефолных настроек автоматча if (request.Url.EndsWith("AutomatchDefaultsSS.lua", StringComparison.OrdinalIgnoreCase) || request.Url.EndsWith("AutomatchDefaultsDXP2Fixed.lua", StringComparison.OrdinalIgnoreCase)) { LogForUser($"AutomatchDefaults requested"); //HttpHelper.WriteResponse(ms, HttpResponceBuilder.TextFileBytes(CoreContext.MasterServer.AutomatchDefaultsBytes)); HttpHelper.WriteResponse(ms, HttpResponceBuilder.Text(GameSpyHttpDataConstants.AutomatchDefaults, Encoding.ASCII)); goto END; } /*if (request.Url.EndsWith("homepage.php.htm", StringComparison.OrdinalIgnoreCase)) * { * if (StatsResponce == null || (DateTime.Now - _lastStatsUpdate).TotalMinutes > 5) * StatsResponce = BuildTop10StatsResponce(); * * HttpHelper.WriteResponse(ms, StatsResponce); * goto END; * }*/ // Если дошли сюда - отправляет NotFound HttpHelper.WriteResponse(ms, HttpResponceBuilder.NotFound()); END: LogTrace("HTTP WANT TO SEND " + node.GetHashCode() + " " + ms.Length); handler.Send(node, ms.ToArray()); handler.KillClient(node); } } catch (InvalidDataException ex) { //Log(ex); } }