mirror of
https://github.com/amineo/t2_server_query_elixir.git
synced 2026-01-19 18:14:44 +00:00
* init
This commit is contained in:
commit
3b22db761b
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -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).
|
||||
|
||||
16
lib/t2_server_query.ex
Normal file
16
lib/t2_server_query.ex
Normal file
|
|
@ -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
|
||||
185
lib/t2_server_query/packet_parser.ex
Normal file
185
lib/t2_server_query/packet_parser.ex
Normal file
|
|
@ -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
|
||||
65
lib/t2_server_query/query_result.ex
Normal file
65
lib/t2_server_query/query_result.ex
Normal file
|
|
@ -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
|
||||
94
lib/t2_server_query/udp_server.ex
Normal file
94
lib/t2_server_query/udp_server.ex
Normal file
|
|
@ -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. <a:playt2.com/discord>playt2.com/discord</a>",
|
||||
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
|
||||
27
mix.exs
Normal file
27
mix.exs
Normal file
|
|
@ -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
|
||||
6
mix.lock
Normal file
6
mix.lock
Normal file
|
|
@ -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"},
|
||||
}
|
||||
51
test/packet_parser_test.exs
Normal file
51
test/packet_parser_test.exs
Normal file
|
|
@ -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. <a:playt2.com/discord>playt2.com/discord</a>"
|
||||
assert result.server_name == "Discord PUB"
|
||||
assert result.team_count == 1
|
||||
assert List.first(result.teams) == %{name: "Storm", score: 0}
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
93
test/t2_server_query_test.exs
Normal file
93
test/t2_server_query_test.exs
Normal file
|
|
@ -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")
|
||||
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
|
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
||||
Loading…
Reference in a new issue