From 3b22db761b745a144bdca6d67e134c2d2aa49172 Mon Sep 17 00:00:00 2001 From: Anthony Mineo Date: Wed, 6 Oct 2021 17:55:39 -0400 Subject: [PATCH] * init --- .formatter.exs | 4 + .gitignore | 29 +++++ README.md | 21 +++ lib/t2_server_query.ex | 16 +++ lib/t2_server_query/packet_parser.ex | 185 +++++++++++++++++++++++++++ lib/t2_server_query/query_result.ex | 65 ++++++++++ lib/t2_server_query/udp_server.ex | 94 ++++++++++++++ mix.exs | 27 ++++ mix.lock | 6 + test/packet_parser_test.exs | 51 ++++++++ test/t2_server_query_test.exs | 93 ++++++++++++++ test/test_helper.exs | 1 + 12 files changed, 592 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/t2_server_query.ex create mode 100644 lib/t2_server_query/packet_parser.ex create mode 100644 lib/t2_server_query/query_result.ex create mode 100644 lib/t2_server_query/udp_server.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/packet_parser_test.exs create mode 100644 test/t2_server_query_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f8f740 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +t2_server_query-*.tar + +# Temporary files, for example, from tests. +/tmp/ + +# Ignore VSCode Extentions Files +.elixir_ls \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5508fa5 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# T2ServerQuery + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `t2_server_query` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:t2_server_query, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/t2_server_query](https://hexdocs.pm/t2_server_query). + diff --git a/lib/t2_server_query.ex b/lib/t2_server_query.ex new file mode 100644 index 0000000..af61349 --- /dev/null +++ b/lib/t2_server_query.ex @@ -0,0 +1,16 @@ +defmodule T2ServerQuery do + @moduledoc """ + Documentation for `T2ServerQuery`. + """ + + require Logger + + # Just a simple debug logging util + def log(thing_to_log) do + Logger.info(inspect thing_to_log) + IO.puts "\n____________________________________________\n" + thing_to_log + end + + +end diff --git a/lib/t2_server_query/packet_parser.ex b/lib/t2_server_query/packet_parser.ex new file mode 100644 index 0000000..e9ae05a --- /dev/null +++ b/lib/t2_server_query/packet_parser.ex @@ -0,0 +1,185 @@ +defmodule T2ServerQuery.PacketParser do + @moduledoc """ + Documentation for `T2ServerQuery.PacketParser`. + """ + + alias T2ServerQuery.QueryResult + + + def init({:error, host}, _) do + results = %QueryResult{} + + {:error, + %{results | + server_status: :offline, + server_name: host, + server_description: "Host unreachable, timed out." + } + } + end + + def init(info_packet, status_packet) when is_binary(info_packet) and is_binary(status_packet) do + + info_results = info_packet + |> decode_clean_packet() + |> handle_info_packet() + + + status_results = status_packet + |> decode_clean_packet() + |> handle_status_packet() + |> parse_player_team_scores() + + + pack_results({:ok, status_results, info_results}) + end + + defp pack_results({:ok, status_results, info_results}) do + results = %QueryResult{} + + {:ok, + %{results | + server_status: :online, + server_name: info_results.server_name, + game_type: status_results.game_type, + mission_type: status_results.mission_type, + map_name: status_results.map_name, + player_count: status_results.player_count, + max_player_count: status_results.max_player_count, + bot_count: status_results.bot_count, + server_description: status_results.server_description, + team_count: status_results.team_count, + teams: status_results.teams, + players: status_results.players + } + } + end + + # Info packet structure + defp handle_info_packet({:ok, info_packet}) do + << + _header :: size(192), + server_name :: bitstring + >> = info_packet + + %{server_name: server_name} + end + + + # Status packet structure + defp handle_status_packet({:ok, status_packet}) do + #IO.inspect status_packet, limit: :infinity + << + _header :: size(48), + + game_type_length :: little-integer, + game_type :: binary-size(game_type_length), + mission_type_length :: little-integer, + mission_type :: binary-size(mission_type_length), + map_name_length :: little-integer, + map_name :: binary-size(map_name_length), + + _skip_a :: size(8), + + player_count :: little-integer, + max_player_count :: little-integer, + bot_count :: little-integer, + + _skip_b :: size(16), + + server_description_length :: little-integer, + server_description :: binary-size(server_description_length), + + _skip_c :: size(16), + + team_count :: binary-size(1), + + rest :: bitstring + >> = status_packet + + + %{ + game_type_length: game_type_length, + game_type: game_type, + mission_type_length: mission_type_length, + mission_type: mission_type, + map_name_length: map_name_length, + map_name: map_name, + player_count: player_count, + max_player_count: max_player_count, + bot_count: bot_count, + server_description_length: server_description_length, + server_description: server_description, + team_count: String.to_integer(team_count), + teams: [], + players: [], + data: rest + } + end + + + + # Take the ..rest of the status packet and parse out the team and player scores + defp parse_player_team_scores(packet) do + ## Break the status query packet into multiple parts + ## raw_game_info contains the map, gametype, mod, and description + ## raw_players_info contains players, assigned team and score + [raw_team_scores | raw_players_info] = String.split(packet.data, "\n#{packet.player_count}", trim: true) + + pack_teams = raw_team_scores + |> String.trim_leading + |> String.split("\n") + |> Enum.map(&parse_team_scores(&1)) + |> Enum.to_list + + + pack_players = raw_players_info + |> clean_player_info() + |> Enum.map(&parse_player_scores(&1)) + |> Enum.to_list + + # We're done parsing the data key so we can remove it from our compiled struct + cleaned_packet = Map.delete(packet, :data) + %{cleaned_packet | teams: pack_teams, players: pack_players } + end + + + # Convert player array into a map + # parse_player_scores(["Inferno", "305"]) + # > %{team: "Inferno", score: 305} + defp parse_team_scores(raw_team_scores) do + Enum.zip([:name, :score], String.split(raw_team_scores, "\t")) + |> Map.new + |> convert_score() + end + + + # Convert player array into a map + # parse_player_scores(["Anthony", "Storm", "100"]) + # > %{player: "ElKapitan ", score: 100, team: "Inferno"} + defp parse_player_scores(player) do + Enum.zip([:player, :team, :score], String.split(player, "\t", trim: true)) + |> Map.new + |> convert_score() + end + + # Clean and spaces that might be in the packet for odd reason + defp decode_clean_packet(packet) do + packet + |> String.replace(" ", "") + |> Base.decode16(case: :mixed) + end + + # Convert string scores into integers + defp convert_score(%{score: score} = data) when is_binary(score) and not is_nil(data), do: %{data | score: String.to_integer(score)} + defp convert_score(%{score: score} = data) when is_integer(score) and not is_nil(data), do: data + defp convert_score(data), do: data + + # Strip all non-printable UTF chars but preserve spaces, tabs and new-lines + defp clean_player_info(raw_players_info) do + Regex.replace(~r/(*UTF)[^\w\ \t\n\/*+-]+/, List.to_string(raw_players_info), "") + |> String.trim_leading + |> String.split("\n") + end + +end diff --git a/lib/t2_server_query/query_result.ex b/lib/t2_server_query/query_result.ex new file mode 100644 index 0000000..6beab28 --- /dev/null +++ b/lib/t2_server_query/query_result.ex @@ -0,0 +1,65 @@ +defmodule T2ServerQuery.QueryResult do + @moduledoc """ + Shape of the server query result struct. + + %T2ServerQuery.QueryResult{ + server_status: :online, + bot_count: 30, + game_type: "Classic", + map_name: "Cold as Ice [b]", + max_player_count: 64, + mission_type: "Capture the Flag", + player_count: 29, + players: [ + %{player: "Rooster128", score: "0", team: "Storm"}, + %{player: "sneakygnome", score: "0", team: "Inferno"}, + %{player: "Waldred ", score: "0", team: "Inferno"}, + %{player: "HDPTetchy ", score: "0", team: "Storm"}, + %{player: "0wnj0o", score: "0", team: "Inferno"}, + %{player: "idjit ", score: "0", team: "Storm"}, + %{player: "JesusChrist ", score: "0", team: "Storm"}, + %{player: "Sofaking--bakeD ", score: "0", team: "Inferno"}, + %{player: "saKe ", score: "0", team: "Inferno"}, + %{player: "ZurkinWood497", score: "0", team: "Storm"}, + %{player: "TerryTC ", score: "0", team: "Inferno"}, + %{player: "WankBullet ", score: "0", team: "Storm"}, + %{player: "CyClones", score: "0", team: "Inferno"}, + %{player: "huntergirl10", score: "0", team: "Storm"}, + %{player: "ChocoTaco", score: "0", team: "Inferno"}, + %{player: "Dirk", score: "0", team: "Storm"}, + %{player: "Krell", score: "0", team: "Storm"}, + %{player: "high5slayer", score: "0", team: "Inferno"}, + %{player: "Red Fraction ", score: "0", team: "Inferno"}, + %{player: "-MaLice--", score: "0", team: "Storm"}, + %{player: "wiltedflower ", score: "0", team: "Inferno"}, + %{player: "Glarm ", score: "0", team: "Storm"}, + %{player: "AlphaSentinel", score: "0", team: "Inferno"}, + %{player: "The-Punisher ", score: "0", team: "Storm"}, + %{player: "2SmOkeD", score: "0", team: "Inferno"}, + %{player: "iPrecision", score: "0", team: "Storm"}, + %{player: "Halo 2 ", score: "0", team: "Storm"}, + %{player: "Sami-FIN ", score: "0", team: "Inferno"}, + %{player: "rileygarbels", score: "0", team: "Storm"} + ], + server_description: "This server is using bots that are adapted to playing Classic. http://tribes2bots.byethost4.com/forum/index.php?topic=57.msg234", + server_name: "Classic Bots Server", + team_count: 2, + teams: [%{name: "Storm", score: "0"}, %{name: "Inferno", score: "0"}] + } + """ + + defstruct [ + server_status: :offline, + server_name: "", + game_type: "", + mission_type: "", + map_name: "", + player_count: 0, + max_player_count: 0, + bot_count: 0, + server_description: "", + team_count: 0, + teams: [], + players: [] + ] +end diff --git a/lib/t2_server_query/udp_server.ex b/lib/t2_server_query/udp_server.ex new file mode 100644 index 0000000..27f98c0 --- /dev/null +++ b/lib/t2_server_query/udp_server.ex @@ -0,0 +1,94 @@ +defmodule T2ServerQuery.UdpServer do + @moduledoc """ + Documentation for `UdpServer`. + """ + require Logger + + alias T2ServerQuery.PacketParser + + @doc """ + Perform a server query. + Results should be in the form of a tuple + - {:ok, %T2ServerQuery.QueryResult{}} + - {:error, %T2ServerQuery.QueryResult{}} + + Querying a Tribes 2 server actually requires sending 2 different packets to the server where the first byte is denoting what we're asking for in response. The first is called the 'info' packet which doesnt contain much more then the server name. The second is called the 'status' packet which contains all the meat and potatoes. + + + ## Examples + + iex> T2ServerQuery.UdpServer.query("35.239.88.241") + {:ok, + %T2ServerQuery.QueryResult{ + bot_count: 0, + game_type: "Classic", + map_name: "Canker", + max_player_count: 64, + mission_type: "LakRabbit", + player_count: 0, + players: [%{}], + server_description: "Celebrating 20 Years of Tribes2! More information in Discord. playt2.com/discord", + server_name: "Discord PUB", + server_status: :online, + team_count: 1, + teams: [%{name: "Storm", score: 0}] + }} + + iex> T2ServerQuery.UdpServer.query("127.0.0.1") + {:error, + %T2ServerQuery.QueryResult{ + bot_count: 0, + game_type: "", + map_name: "", + max_player_count: 0, + mission_type: "", + player_count: 0, + players: [], + server_description: "Host unreachable, timed out.", + server_name: "127.0.0.1:28000", + server_status: :offline, + team_count: 0, + teams: [] + }} + + """ + def query(server_ip, port \\ 28_000, timeout \\ 3_500) do + Logger.info "query: #{server_ip}" + + {:ok, socket} = :gen_udp.open(0, [:binary, {:active, false}]) + + # Convert a string ip from "127.0.0.1" into {127, 0, 0, 1} + {:ok, s_ip} = server_ip + |> to_charlist() + |> :inet.parse_address() + + + qry_info_packet = <<14, 2, 1, 2, 3, 4>> + qry_status_packet = <<18, 2, 1, 2, 3, 4>> + + # Requst info packet + :gen_udp.send(socket, s_ip, port, qry_info_packet) + hex_info_packet = :gen_udp.recv(socket, 0, timeout) + |> handle_udp_response(server_ip, port) + + # Request status packet + :gen_udp.send(socket, s_ip, port, qry_status_packet) + hex_status_packet = :gen_udp.recv(socket, 0, timeout) + |> handle_udp_response(server_ip, port) + + # Combine and parse results + PacketParser.init(hex_info_packet, hex_status_packet) + end + + + defp handle_udp_response({:ok, {_ip, _port, packet}}, _server_ip, _port) do + packet + |> Base.encode16 + end + + defp handle_udp_response({:error, :timeout}, server_ip, port) do + Logger.error "TIMEOUT --> #{server_ip}:#{port}" + {:error, "#{server_ip}:#{port}"} + end + +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..e7b30a8 --- /dev/null +++ b/mix.exs @@ -0,0 +1,27 @@ +defmodule T2ServerQuery.MixProject do + use Mix.Project + + def project do + [ + app: :t2_server_query, + version: "0.1.0", + elixir: "~> 1.12", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:credo, "~> 1.5"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..bf1731d --- /dev/null +++ b/mix.lock @@ -0,0 +1,6 @@ +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, +} diff --git a/test/packet_parser_test.exs b/test/packet_parser_test.exs new file mode 100644 index 0000000..0528f66 --- /dev/null +++ b/test/packet_parser_test.exs @@ -0,0 +1,51 @@ +defmodule PacketParserTest do + use ExUnit.Case, async: true + + alias T2ServerQuery.PacketParser + + test "Parse UDP Packet One (Bot Server)" do + hex_info_packet_one = "10 02 01 02 03 04 04 56 45 52 35 33 00 00 00 33 00 00 00 CA 61 00 00 13 43 6C 61 73 73 69 63 20 42 6F 74 73 20 53 65 72 76 65 72" + hex_status_packet_one = "14 02 01 02 03 04 07 43 6C 61 73 73 69 63 10 43 61 70 74 75 72 65 20 74 68 65 20 46 6C 61 67 0F 43 6F 6C 64 20 61 73 20 49 63 65 20 5B 62 5D 21 1D 40 1E B6 09 7F 54 68 69 73 20 73 65 72 76 65 72 20 69 73 20 75 73 69 6E 67 20 62 6F 74 73 20 74 68 61 74 20 61 72 65 20 61 64 61 70 74 65 64 20 74 6F 20 70 6C 61 79 69 6E 67 20 43 6C 61 73 73 69 63 2E 20 68 74 74 70 3A 2F 2F 74 72 69 62 65 73 32 62 6F 74 73 2E 62 79 65 74 68 6F 73 74 34 2E 63 6F 6D 2F 66 6F 72 75 6D 2F 69 6E 64 65 78 2E 70 68 70 3F 74 6F 70 69 63 3D 35 37 2E 6D 73 67 32 33 34 A7 02 32 0A 53 74 6F 72 6D 09 30 0A 49 6E 66 65 72 6E 6F 09 30 0A 32 39 0A 10 0E 52 6F 6F 73 74 65 72 31 32 38 11 09 53 74 6F 72 6D 09 30 0A 10 0E 73 6E 65 61 6B 79 67 6E 6F 6D 65 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 57 61 6C 64 72 65 64 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 48 44 50 7C 54 65 74 63 68 79 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 30 77 6E 6A 30 6F 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 69 64 6A 69 74 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 4A 65 73 75 73 43 68 72 69 73 74 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 53 6F 66 61 6B 69 6E 67 2D 7C 2D 62 61 6B 65 44 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 73 61 4B 65 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 5A 75 72 6B 69 6E 57 6F 6F 64 34 39 37 11 09 53 74 6F 72 6D 09 30 0A 10 0E 54 65 72 72 79 54 43 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 57 61 6E 6B 42 75 6C 6C 65 74 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 43 79 43 6C 6F 6E 65 73 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 68 75 6E 74 65 72 67 69 72 6C 31 30 11 09 53 74 6F 72 6D 09 30 0A 10 0E 43 68 6F 63 6F 54 61 63 6F 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 44 69 72 6B 11 09 53 74 6F 72 6D 09 30 0A 10 0E 4B 72 65 6C 6C 11 09 53 74 6F 72 6D 09 30 0A 10 0E 68 69 67 68 35 73 6C 61 79 65 72 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 52 65 64 20 46 72 61 63 74 69 6F 6E 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 2D 4D 61 4C 69 63 65 2D 2D 11 09 53 74 6F 72 6D 09 30 0A 10 0E 77 69 6C 74 65 64 66 6C 6F 77 65 72 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 47 6C 61 72 6D 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 41 6C 70 68 61 53 65 6E 74 69 6E 65 6C 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 5E 54 68 65 2D 50 75 6E 69 73 68 65 72 5E 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 32 53 6D 4F 6B 65 44 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 69 50 72 65 63 69 73 69 6F 6E 11 09 53 74 6F 72 6D 09 30 0A 10 0E 48 61 6C 6F 20 32 20 11 09 53 74 6F 72 6D 09 30 0A 10 0E 53 61 6D 69 2D 46 49 4E 20 11 09 49 6E 66 65 72 6E 6F 09 30 0A 10 0E 72 69 6C 65 79 67 61 72 62 65 6C 73 11 09 53 74 6F 72 6D 09 30" + + {:ok, result} = PacketParser.init(hex_info_packet_one, hex_status_packet_one) + |> T2ServerQuery.log + + assert result.server_status == :online + assert result.bot_count == 30 + assert result.game_type == "Classic" + assert result.map_name == "Cold as Ice [b]" + assert result.max_player_count == 64 + assert result.mission_type == "Capture the Flag" + assert result.player_count == 29 + assert length(result.players) == result.player_count + assert List.first(result.players) == %{player: "Rooster128", score: 0, team: "Storm"} + assert result.server_description == "This server is using bots that are adapted to playing Classic. http://tribes2bots.byethost4.com/forum/index.php?topic=57.msg234" + assert result.server_name == "Classic Bots Server" + assert result.team_count == 2 + assert List.first(result.teams) == %{name: "Storm", score: 0} + end + + + test "Parse UDP Packet Two (DiscordPUB Server)" do + hex_info_packet_two = "10 02 01 02 03 04 04 56 45 52 35 33 00 00 00 33 00 00 00 CA 61 00 00 0B 44 69 73 63 6F 72 64 20 50 55 42" + hex_status_packet_two = "14 02 01 02 03 04 07 43 6C 61 73 73 69 63 09 4C 61 6B 52 61 62 62 69 74 08 53 75 6E 44 61 6E 63 65 A1 00 40 00 E5 08 6A 43 65 6C 65 62 72 61 74 69 6E 67 20 32 30 20 59 65 61 72 73 20 6F 66 20 54 72 69 62 65 73 32 21 20 4D 6F 72 65 20 69 6E 66 6F 72 6D 61 74 69 6F 6E 20 69 6E 20 44 69 73 63 6F 72 64 2E 20 3C 61 3A 70 6C 61 79 74 32 2E 63 6F 6D 2F 64 69 73 63 6F 72 64 3E 70 6C 61 79 74 32 2E 63 6F 6D 2F 64 69 73 63 6F 72 64 3C 2F 61 3E 0B 00 31 0A 53 74 6F 72 6D 09 30 0A 30" + + {:ok, result} = PacketParser.init(hex_info_packet_two, hex_status_packet_two) + |> T2ServerQuery.log + + assert result.server_status == :online + assert result.bot_count == 0 + assert result.game_type == "Classic" + assert result.map_name == "SunDance" + assert result.max_player_count == 64 + assert result.mission_type == "LakRabbit" + assert result.player_count == 0 + assert List.first(result.players) == %{} + assert result.server_description == "Celebrating 20 Years of Tribes2! More information in Discord. playt2.com/discord" + assert result.server_name == "Discord PUB" + assert result.team_count == 1 + assert List.first(result.teams) == %{name: "Storm", score: 0} + end + + +end diff --git a/test/t2_server_query_test.exs b/test/t2_server_query_test.exs new file mode 100644 index 0000000..83b0a0f --- /dev/null +++ b/test/t2_server_query_test.exs @@ -0,0 +1,93 @@ +defmodule T2ServerQueryTest do + use ExUnit.Case, async: true + alias T2ServerQuery.UdpServer + + doctest T2ServerQuery + + + test "Gracefully handle timeouts and unreachable servers" do + # We know this query is going to timeout, so lets not wait around :) + timeout = 250 + + port = Enum.random(28_000..28_999) + {:error, result} = T2ServerQuery.UdpServer.query("127.0.0.1", port, timeout) + |> T2ServerQuery.log + + assert result.server_status == :offline + assert result.server_description == "Host unreachable, timed out." + assert result.server_name == "127.0.0.1:#{port}" + end + + + test "Live test a number of Tribes 2 servers" do + tasks = [ + Task.async(T2ServerQuery.UdpServer, :query, ["35.239.88.241"]), + Task.async(T2ServerQuery.UdpServer, :query, ["97.99.172.12", 28_001]), + Task.async(T2ServerQuery.UdpServer, :query, ["67.222.138.13"]) + ] + + server_list = Task.yield_many(tasks) + |> Enum.map(fn {task, result} -> + #|> Enum.with_index(fn {task, result}, index -> + # :ok should be returned for each task and result + # assert {task, {:ok, result}} == Enum.at(server_list, index) + test_server_status(result) + end) + + end + + defp test_server_status({:ok, _}) do + assert true + end + defp test_server_status({:error, _}) do + assert false + end + defp test_server_status(nil) do + assert false + end + +end + + + +#qry_test = T2ServerQuery.UdpServer.query("127.0.0.1") +#IO.inspect qry_test + +#qry_test2 = T2ServerQuery.UdpServer.query("35.239.88.241") +#IO.inspect qry_test2 + + +# tasks = [ +# Task.async(T2ServerQuery.UdpServer, :query, ["127.0.0.1"]), +# Task.async(T2ServerQuery.UdpServer, :query, ["35.239.88.241"]), +# Task.async(T2ServerQuery.UdpServer, :query, ["97.99.172.12", 28001]), +# Task.async(T2ServerQuery.UdpServer, :query, ["67.222.138.13"]), +# Task.async(T2ServerQuery.UdpServer, :query, ["91.55.51.94"]), +# ] + +# IO.inspect Task.yield_many(tasks) + +# task0 = Task.async(T2ServerQuery.UdpServer, :query, ["127.0.0.1"]) +# task1 = Task.async(T2ServerQuery.UdpServer, :query, ["35.239.88.241"]) +# task2 = Task.async(T2ServerQuery.UdpServer, :query, ["97.99.172.12", 28001]) +# task3 = Task.async(T2ServerQuery.UdpServer, :query, ["67.222.138.13"]) +# task4 = Task.async(T2ServerQuery.UdpServer, :query, ["91.55.51.94"]) +# # res4 = Task.await(task4) +# # IO.inspect res4.server_name + +# res0 = Task.await(task0) +# res1 = Task.await(task1) +# res2 = Task.await(task2) +# res3 = Task.await(task3) +# res4 = Task.await(task4) + + +# IO.inspect res1.server_name +# IO.inspect res2.server_name +# IO.inspect res3.server_name +# IO.inspect res4.server_name + +# T2ServerQuery.UdpServer.query("35.239.88.241") +# T2ServerQuery.UdpServer.query("97.99.172.12", 28001) +# T2ServerQuery.UdpServer.query("67.222.138.13") +# T2ServerQuery.UdpServer.query("91.55.51.94") diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()