2021-10-06 21:55:39 +00:00
defmodule T2ServerQuery.PacketParser do
@moduledoc """
2021-10-07 20:06:22 +00:00
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 .
2021-10-06 21:55:39 +00:00
"""
2022-04-13 21:37:18 +00:00
2021-10-06 21:55:39 +00:00
alias T2ServerQuery.QueryResult
2021-10-07 20:06:22 +00:00
@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 `
2021-10-06 21:55:39 +00:00
2021-10-07 20:06:22 +00:00
"""
2022-04-13 21:37:18 +00:00
@spec init ( { :error , String . t ( ) } , any ( ) ) :: { :error , map ( ) }
2021-10-06 21:55:39 +00:00
def init ( { :error , host } , _ ) do
results = % QueryResult { }
{ :error ,
%{ results |
server_status : :offline ,
server_name : host ,
server_description : " Host unreachable, timed out. "
}
}
end
2022-04-13 21:48:55 +00:00
@spec init ( binary ( ) , binary ( ) ) :: { :ok , QueryResult . t ( ) }
2021-10-06 21:55:39 +00:00
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
2022-04-13 21:48:55 +00:00
@spec pack_results ( { :ok , map ( ) , map ( ) } ) :: { :ok , QueryResult . t ( ) }
2021-10-06 21:55:39 +00:00
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