# Amethyst - An experimental Minecraft server written in Elixir. # Copyright (C) 2024 KodiCraft # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . defmodule Amethyst.ConnectionState.Play do require Amethyst.ConnectionState.Macros alias Amethyst.ConnectionState.Macros require Logger @moduledoc """ This module contains the packets and logic for the Play state. """ Macros.defpacket_clientbound :disconnect, 0x1D, 767, [reason: :nbt] Macros.defpacket_clientbound :keep_alive, 0x26, 767, [id: :long] Macros.defpacket_clientbound :chunk_data_and_update_light, 0x27, 767, [ chunk_x: :int, chunk_z: :int, heightmaps: :nbt, data: :byte_array, block_entities: {:array, [ packed_xz: :ubyte, # TODO: This would be interesting to have in a clearer format y: :short, type: :varint, data: :nbt ]}, sky_light_mask: :raw, block_light_mask: :raw, empty_sky_light_mask: :raw, empty_block_light_mask: :raw, sky_light_arrays: {:array, [ sky_light_array: :byte_array ]}, block_light_arrays: {:array, [ block_light_array: :byte_array ]} ] Macros.defpacket_clientbound :login, 0x2B, 767, [ entity_id: :int, is_hardcore: :bool, dimensions: {:array, [name: :string]}, max_players: :varint, view_distance: :varint, simulation_distance: :varint, reduced_debug_info: :bool, enable_respawn_screen: :bool, do_limited_crafting: :bool, dimension_type: :varint, dimension_name: :string, hashed_seed: :long, game_mode: :ubyte, previous_game_mode: :byte, is_debug: :bool, is_flat: :bool, death_location: {:optional, {:compound, [ dimension: :string, location: :pos ]}}, portal_cooldown: :varint, enforces_secure_chat: :bool, ] Macros.defpacket_clientbound :player_info_update_add_player, 0x2E, 767, [ actions: {:literal, 0x01}, players: {:array, [ uuid: :uuid, name: :string, properties: {:array, [ name: :string, value: :string, signature: {:optional, :string}, ]} ]} ] Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x2E, 767, [ actions: {:literal, 0x02}, players: {:array, [ uuid: :uuid, data: {:optional, {:compound, [ chat_session_id: :uuid, public_key_expiry_time: :long, encoded_public_key: :byte_array, public_key_signature: :byte_array ]}} ]} ] Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x2E, 767, [ actions: {:literal, 0x04}, players: {:array, [ uuid: :uuid, gamemode: :varint ]} ] Macros.defpacket_clientbound :player_info_update_update_listed, 0x2E, 767, [ actions: {:literal, 0x08}, players: {:array, [ uuid: :uuid, listed: :bool ]} ] Macros.defpacket_clientbound :player_info_update_update_latency, 0x2E, 767, [ actions: {:literal, 0x10}, players: {:array, [ uuid: :uuid, ping: :varint, # Milliseconds ]} ] Macros.defpacket_clientbound :player_info_update_update_display_name, 0x2E, 767, [ actions: {:literal, 0x20}, players: {:array, [ uuid: :uuid, display_name: {:optional, :nbt} ]} ] Macros.defpacket_clientbound :synchronize_player_position, 0x40, 767, [ x: :double, y: :double, z: :double, yaw: :float, pitch: :float, flags: :byte, teleport_id: :varint ] Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [ chunk_x: :varint, chunk_z: :varint ] Macros.defpacket_clientbound :game_event, 0x22, 767, [ event: :ubyte, value: :float ] # We can use functions to wrap over this packet and make it a bit clearer. # Taking the protocol version here makes it less portable but whatever, fuck this packet def ge_no_respawn_block_available(767), do: %{packet_type: :game_event, event: 0, value: 0} def ge_begin_raining(767), do: %{packet_type: :game_event, event: 1, value: 0} def ge_end_raining(767), do: %{packet_type: :game_event, event: 2, value: 0} def ge_change_game_mode(767, gm) when is_integer(gm), do: %{packet_type: :game_event, event: 3, value: gm} def ge_win_game(767, credits?) when is_integer(credits?), do: %{packet_type: :game_event, event: 4, value: credits?} def ge_game_event(767, event) when is_integer(event), do: %{packet_type: :game_event, event: 5, value: event} def ge_arrow_hit_player(767), do: %{packet_type: :game_event, event: 6, value: 0} def ge_rain_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 7, value: value} def ge_thunder_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 8, value: value} def ge_play_pufferfish_sting_sound(767), do: %{packet_type: :game_event, event: 9, value: 0} def ge_play_elder_guardian_mob_appearance(767), do: %{packet_type: :game_event, event: 10, value: 0} def ge_enable_respawn_screen(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 11, value: enabled?} def ge_limited_crafting(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 12, value: enabled?} def ge_start_waiting_for_level_chunks(767), do: %{packet_type: :game_event, event: 13, value: 0} Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint] Macros.defpacket_serverbound :serverbound_plugin_message, 0x12, 767, [channel: :string, data: :raw] Macros.defpacket_serverbound :keep_alive, 0x18, 767, [id: :long] Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [ x: :double, feet_y: :double, z: :double, on_ground: :bool ] Macros.defpacket_serverbound :set_player_position_and_rotation, 0x1B, 767, [ x: :double, feet_y: :double, z: :double, yaw: :float, pitch: :float, on_ground: :bool ] Macros.defpacket_serverbound :set_player_rotation, 0x1C, 767, [ yaw: :float, pitch: :float, on_ground: :bool # I don't understand their obsession with this... ] Macros.defpacket_serverbound :set_player_on_ground, 0x1D, 767, [on_ground: :bool] Macros.defpacket_serverbound :player_command, 0x25, 767, [eid: :varint, action_id: :varint, jump_boost: :varint] def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do Amethyst.Game.accept_teleport(state[:game], id) :ok end def handle(%{packet_type: :set_player_position_and_rotation, x: x, feet_y: y, z: z, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do # I don't know why we would ever trust on_ground here... the server computes that Amethyst.Game.player_position(state[:game], {x, y, z}) Amethyst.Game.player_rotation(state[:game], {yaw, pitch}) :ok end def handle(%{packet_type: :serverbound_plugin_message, channel: channel, data: _}, 767, _state) do Logger.debug("Got plugin message on #{channel}") end def handle(%{packet_type: :set_player_position, x: x, feet_y: y, z: z, on_ground: _ground}, 767, state) do # I don't know why we would ever trust on_ground here... the server computes that Amethyst.Game.player_position(state[:game], {x, y, z}) :ok end def handle(%{packet_type: :set_player_rotation, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do # I don't know why we would ever trust on_ground here... the server computes that Amethyst.Game.player_rotation(state[:game], {yaw, pitch}) :ok end def handle(%{packet_type: :set_player_on_ground, on_ground: _}, 767, _state) do :ok # Again, don't trust the client for something we can compute end def handle(%{packet_type: :player_command, eid: _eid, action_id: aid, jump_boost: _horse_jump}, 767, _state) do # TODO: Actually handle these events case aid do 0 -> # Start sneaking :ok 1 -> # Stop sneaking :ok 2 -> # Leave bed :ok 3 -> # Start sprinting :ok 4 -> # Stop sprinting :ok 5 -> # Start horse jump :ok 6 -> # Stop horse jump :ok 7 -> # Open vehicle inventory :ok 8 -> # Start elytra flying :ok _ -> raise RuntimeError, "Unknown Player Command Action ID" end end def handle(%{packet_type: :keep_alive, id: id}, 767, state) do ka = state |> Map.get(:keepalive) send(ka, {:respond, id}) :ok end # This function should be started on a new task under the connection handler # and is responsible for keepalive logic. def keepalive_loop(player) do Process.link(player) # Is it fine to do this on loop? <> = :rand.bytes(4) send(player, {:send_packet, %{packet_type: :keep_alive, id: id}}) receive do {:respond, ^id} -> :timer.sleep(250) keepalive_loop(player) after 15_000 -> send(player, {:disconnect, "Timed out! Connection overloaded?"}) end end def disconnect(reason) do %{ packet_type: :disconnect, reason: {:compound, %{ "text" => {:string, reason} }} } end end