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