mirror of
https://github.com/amineo/t2_server_query_elixir.git
synced 2026-04-23 05:05:21 +00:00
* Init ExDoc
This commit is contained in:
parent
3b22db761b
commit
6ab3e06e78
8 changed files with 277 additions and 172 deletions
|
|
@ -1,11 +1,58 @@
|
|||
defmodule T2ServerQuery.PacketParser do
|
||||
@moduledoc """
|
||||
Documentation for `T2ServerQuery.PacketParser`.
|
||||
This module does the heavy lifting with parsing a Tribes 2 query response packet.
|
||||
|
||||
## UDP Packet Anatomy
|
||||
|
||||
### Info Packet
|
||||
<<
|
||||
_header :: size(192),
|
||||
server_name :: bitstring
|
||||
>>
|
||||
|
||||
### Status Packet
|
||||
<<
|
||||
_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
|
||||
>>
|
||||
|
||||
Notice the `_skip_(a|b|c)` mappings. I havn't quite figured out what they refer to yet but they don't seem that important. They likely relate to a few server flags like `tournament_mode`, `cpu_speed`, `is_linux`.
|
||||
|
||||
Refer to `T2ServerQuery.QueryResult` for what a typical struct would look like.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
alias T2ServerQuery.QueryResult
|
||||
|
||||
@doc """
|
||||
This function expects both an `info` and `status` packet to be passed in that is in a `Base.encode16` format.
|
||||
Normally you wouldn't need to run this function manually since it's called in a pipeline from the main `T2ServerQuery.query`
|
||||
|
||||
"""
|
||||
def init({:error, host}, _) do
|
||||
results = %QueryResult{}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
defmodule T2ServerQuery.QueryResult do
|
||||
@moduledoc """
|
||||
Shape of the server query result struct.
|
||||
## Struct Shape
|
||||
|
||||
%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"}]
|
||||
}
|
||||
%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 [
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue