diff --git a/apps/amethyst/lib/data.ex b/apps/amethyst/lib/data.ex index 4410e14..4868b73 100644 --- a/apps/amethyst/lib/data.ex +++ b/apps/amethyst/lib/data.ex @@ -14,290 +14,44 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -defmodule Amethyst.Minecraft.Write do - import Bitwise - require Logger - - @moduledoc """ - This module contains functions for writing Minecraft data. - - Each function in this module takes in an input of the proper type and returns a binary - of the encoded data. - """ - - def uuid(uuid) when is_binary(uuid) do - UUID.string_to_binary!(uuid) - end - - def bool(value) when is_boolean(value) do - case value do - true -> <<0x01::8>> - false -> <<0x00::8>> - end - end - - def raw(value) do - value - end - - def byte(value) when value in -128..127 do - <> - end - - def ubyte(value) when value in 0..255 do - <> - end - - def short(value) when value in -32_768..32_767 do - <> - end - - def ushort(value) when value in 0..65_535 do - <> - end - - def int(value) when value in -2_147_483_648..2_147_483_647 do - <> - end - - def long(value) when value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807 do - <> - end - - def float(value) when is_number(value) do - <> - end - - def double(value) when is_number(value) do - <> - end - - def varint(value) when value in -2_147_483_648..2_147_483_647 do - <> = <> # This is a trick to allow the arithmetic shift to act as a logical shift - varnum("", value) - end - - def varint(_) do - raise ArgumentError, "Value is out of range for a varint" - end - - def varlong(value) when value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807 - do - <> = <> - varnum("", value) - end - - def varlong(_) do - raise ArgumentError, "Value is out of range for a varlong" - end - - defp varnum(acc, value) when value in 0..127 do - acc <> <<0::1, value::7-big>> - end - - defp varnum(acc, value) do - acc <> <<1::1, value::7-big>> |> varnum(value >>> 7) - end - - def string(value) do - <> - end - - def json(value) do - string(Jason.encode!(value)) - end - - def byte_array(value) do - <> - end - - def position({x, y, z}) do - <> - end - - @doc """ - Writes a list of elements with the given `callback` function. This does not - prefix the list with a length, remember to do that yourself if needed. - - iex> Amethyst.Minecraft.Write.list([1, 2, 3, 4], &Amethyst.Minecraft.Write.byte/1) - <<1, 2, 3, 4>> - """ - def list(list, callback) do - Enum.reduce(list, "", &(&2 <> callback.(&1))) - end - - @doc """ - Shorthand function for writing a value which may not be present. If `value` is `nil`, - writes `false`, otherwise writes `true` followed by the value using `callback`. - """ - def option(value, callback) do - case value do - nil -> bool(false) - v -> bool(true) <> callback.(v) - end - end - - def nbt(value) do - Amethyst.NBT.Write.write_net(value) - end - - def bitset(list) do - unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end)) - aligned = if rem(bit_size(unaligned), 64) == 0 do - unaligned - else - <<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>> - end - varint(div(byte_size(aligned), 8)) <> aligned - end - def fixed_bitset(list, length) do - unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end)) - if bit_size(unaligned) != length do - raise ArgumentError, "Fixed bitset is not the correct length" - end - aligned = if rem(bit_size(unaligned), 64) == 0 do - unaligned - else - <<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>> - end - aligned - end -end - -defmodule Amethyst.Minecraft.Read do - import Bitwise - - @moduledoc """ - This module contains functions for reading Minecraft data. - - These functions allow you to chain them into eachother, at the end they will produce a list of all the - values they have read. - - You may use the helper function `Amethyst.Minecraft.Read.start/1` to start the chain with a binary buffer. - The return value of the chain is a tuple containing the list of values and the remaining binary buffer. - - iex> alias Amethyst.Minecraft.Read - iex> {[_, _, _], ""} = Read.start(<<1, 999::16, 64>>) |> Read.bool() |> Read.short() |> Read.byte() |> Read.stop() - {[true, 999, 64], ""} - """ - - @doc """ - This function structures an input binary to be used by the functions in `Amethyst.Minecraft.Read`. - """ - def start(binary) do - {[], binary, :reversed} - end - @doc """ - This function structures the result of the functions in `Amethyst.Minecraft.Read` to be used in the same order they were read. - """ - def stop({acc, rest, :reversed}) do - {Enum.reverse(acc), rest} - end - - def bool({acc, <>, :reversed}) do - {[value != 0 | acc], rest, :reversed} - end - - def byte({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def ubyte({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def short({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def ushort({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def int({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def long({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def float({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def double({acc, <>, :reversed}) do - {[value | acc], rest, :reversed} - end - - def uuid({acc, <>, :reversed}) do - {[UUID.binary_to_string!(uuid) | acc], rest, :reversed} - end - - def raw({acc, data, :reversed}, amount) do - <> = data - {[data | acc], rest, :reversed} - end - - def byte_array({acc, data, :reversed}) do - {[length], rest} = start(data) |> varint |> stop - raw({acc, rest, :reversed}, length) - end - - @doc """ - Reads a varint. `read` tracks the number of bytes read and `nacc` tracks the number being read. - """ - def varint(tuple, read \\ 0, nacc \\ 0) - def varint({acc, <<1::1, value::7, rest::binary>>, :reversed}, read, nacc) when read < 5 do - varint({acc, rest, :reversed}, read + 1, nacc + (value <<< (7 * read))) - end - def varint({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do - total = nacc + (value <<< (7 * read)) - <> = <> - {[value | acc], rest, :reversed} - end - def varint(_, read, _) when read >= 5 do - raise RuntimeError, "Got a varint which is too big!" - end - def varint({_, ""}, _, _) do - raise RuntimeError, "Got an incomplete varint!" - end - - @doc """ - Reads a varlong. `read` tracks the number of bytes read and `nacc` tracks the number being read. - """ - def varlong(tuple, read \\ 0, nacc \\ 0) - def varlong({acc, <<1::1, value::7, rest::binary>>, :reversed}, read, nacc) when read < 10 do - varlong({acc, rest, :reversed}, read + 1, nacc + (value <<< (7 * read))) - end - def varlong({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do - total = nacc + (value <<< (7 * read)) - <> = <> - {[value | acc], rest, :reversed} - end - def varlong(_, read, _) when read >= 10 do - raise RuntimeError, "Got a varlong which is too big!" - end - def varlong({_, ""}, _, _) do - raise RuntimeError, "Got an incomplete varlong!" - end - - def string({acc, data, :reversed}) do - {[length], rest, :reversed} = start(data) |> varint() - <> = rest - {[value | acc], rest, :reversed} - end - def json({acc, data, :reversed}) do - {[value], rest, :reversed} = string({[], data, :reversed}) - {[Jason.decode!(value) | acc], rest, :reversed} - end - def raw({acc, data, :reversed}) do - {[data | acc], "", :reversed} - end - def fixed_bitset({acc, data, :reversed}, length) do - bytes = ceil(length / 8) - <> = data - value = :binary.bin_to_list(value, 0, 1) |> Enum.reverse() |> Enum.take(length) |> Enum.map(& &1 != 0) - {[value | acc], rest, :reversed} - end +defmodule Amethyst.Data do + @type _boolean :: bool() + def is?({:_boolean}, x), do: is_boolean(x) + @type _byte :: -128..127 + def is?({:_byte}, x), do: is_integer(x) and x in -128..127 + @type _ubyte :: 0..255 + def is?({:_ubyte}, x), do: is_integer(x) and x in 0..255 + @type _short :: -32768..32767 + def is?({:_short}, x), do: is_integer(x) and x in -32768..32767 + @type _ushort :: 0..65535 + def is?({:_ushort}, x), do: is_integer(x) and x in 0..65535 + @type _int :: -2147483648..2147483647 + def is?({:_int}, x), do: is_integer(x) and x in -2147483648..2147483647 + @type _uint :: 0..4294967295 + def is?({:_uint}, x), do: is_integer(x) and x in 0..4294967295 + @type _long :: -9223372036854775808..9223372036854775807 + def is?({:_long}, x), do: is_integer(x) and x in -9223372036854775808..9223372036854775807 + @type _float :: float() + def is?({:_float}, x), do: is_float(x) + @type _double :: float() + def is?({:_double}, x), do: is_float(x) + @type _string :: String.t() + def is?({:_string}, x), do: is_binary(x) + @type _nbt :: map() # TODO: Type-ify NBT + def is?({:_nbt}, x), do: is_map(x) + @type _varint :: _int() + def is?({:_varint}, x), do: is?({:_int}, x) + @type _varlong :: _long() + def is?({:_varlong}, x), do: is?({:_long}, x) + @type _position :: {-33554432..33554431, -2048..2047, -33554432..33554431} + def is?({:_position}, {x, y, z}), do: is?({:_int}, x) and is?({:_int}, y) and is?({:_int}, z) + @type _uuid :: UUID.uuid4() + def is?({:_uuid}, x), do: is_binary(x) and byte_size(x) == 16 # Not the best check but it's good enoug + @type _bitset :: [boolean()] + def is?({:_bitset}, x), do: is_list(x) and Enum.all?(x, &is_boolean/1) + def is?({:_bitset, n}, x), do: is_list(x) and Enum.all?(x, &is_boolean/1) and length(x) == n + @type _optional(n) :: n | nil + def is?({:_optional, n}, x), do: is?({n}, x) or is_nil(x) + @type _array(n) :: [n] + def is?({:_array, n}, x), do: is_list(x) and Enum.all?(x, &is?({n}, &1)) end diff --git a/apps/amethyst/lib/states/behaviour.ex b/apps/amethyst/lib/states/behaviour.ex deleted file mode 100644 index 0b548e9..0000000 --- a/apps/amethyst/lib/states/behaviour.ex +++ /dev/null @@ -1,48 +0,0 @@ -# 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 do - @moduledoc """ - This behaviour defines the behaviour that all connection states must implement. - """ - - alias Amethyst.ConnectionHandler - @type t :: module() - - @callback serialize(packet :: %{packet_type: atom()}, state :: ConnectionHandler.t()) :: binary() - @spec serialize(state :: ConnectionHandler.t(), packet :: %{packet_type: atom()}) :: binary() - def serialize(state, packet) do - state.connection_state.serialize(packet, state) - end - - @callback deserialize(id :: non_neg_integer(), data :: binary(), state :: ConnectionHandler.t()) :: %{packet_type: atom()} - @spec deserialize(state :: ConnectionHandler.t(), id :: pos_integer(), data :: binary()) :: %{packet_type: atom()} - def deserialize(state, id, data) do - state.connection_state.serialize(id, data, state) - end - - @callback handle(packet :: %{packet_type: atom()}, state :: ConnectionHandler.t()) :: [ConnectionHandler.operation()] - @spec handle(state :: ConnectionHandler.t(), packet :: %{packet_type: atom()}) :: [ConnectionHandler.operation()] - def handle(state, packet) do - state.connection_state.handle(packet) - end - - @callback disconnect(reason :: String.t(), state :: ConnectionHandler.t()) :: %{packet_type: atom()} - @spec disconnect(state :: ConnectionHandler.t(), reason :: String.t()) :: %{packet_type: atom()} - def disconnect(state, reason) do - state.connection_state.disconnect(reason) - end -end diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex deleted file mode 100644 index 6138308..0000000 --- a/apps/amethyst/lib/states/configuration.ex +++ /dev/null @@ -1,337 +0,0 @@ -# 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.Configuration do - @behaviour Amethyst.ConnectionState - require Amethyst.ConnectionState.Macros - alias Amethyst.ConnectionState.Macros - - require Logger - - @moduledoc """ - This module contains the packets and logic for the Configuration state. - """ - Macros.defpacket_clientbound :cookie_request, 0x00, 767, [identifier: :string] - Macros.defpacket_clientbound :clientbound_plugin_message, 0x01, 767, [channel: :string, data: :raw] - Macros.defpacket_clientbound :disconnect, 0x02, 767, [reason: :nbt] - Macros.defpacket_clientbound :finish_configuration, 0x03, 767, [] - Macros.defpacket_clientbound :clientbound_keep_alive, 0x04, 767, [id: :long] - Macros.defpacket_clientbound :ping, 0x05, 767, [id: :int] - Macros.defpacket_clientbound :reset_chat, 0x06, 767, [] - Macros.defpacket_clientbound :registry_data, 0x07, 767, [ - id: :string, - entries: {:array, [ - id: :string, - data: {:optional, :nbt} - ]} - ] - Macros.defpacket_clientbound :remove_resource_pack, 0x08, 767, [ - uuid: {:optional, :uuid} - ] - Macros.defpacket_clientbound :add_resource_pack, 0x09, 767, [ - uuid: :uuid, - url: :string, - hash: :string, - forced: :bool, - prompt_message: {:optional, :string} - ] - Macros.defpacket_clientbound :store_cookie, 0x0A, 767, [identifier: :string, payload: :byte_array] - Macros.defpacket_clientbound :transfer, 0x0B, 767, [host: :string, port: :varint] - Macros.defpacket_clientbound :feature_flags, 0x0C, 767, [flags: {:array, [flag: :string]}] - Macros.defpacket_clientbound :update_tags, 0x0D, 767, [ - tags: {:array, [ - registry: :string, - tags: {:array, [ - name: :string, - entries: {:array, [id: :varint]} - ]} - ]} - ] - Macros.defpacket_clientbound :clientbound_known_packs, 0x0E, 767, [ - packs: {:array, [ - namespace: :string, - id: :string, - version: :string - ]} - ] - Macros.defpacket_clientbound :custom_report_details, 0x0F, 767, [ - details: {:array, [ - title: :string, - desctioption: :string - ]} - ] - Macros.defpacket_clientbound :server_links, 0x10, 767, [ - links: {:array, [ - is_builtin: :bool, - label: :string, - url: :string - ]} - ] - - Macros.defpacket_serverbound :client_information, 0x00, 767, [ - locale: :string, - view_distance: :byte, - chat_mode: :varint, - chat_colors: :bool, - displayed_skin_parts: :byte, - main_hand: :varint, - text_filtering: :bool, - allow_server_listings: :bool - ] - Macros.defpacket_serverbound :cookie_response, 0x01, 767, [ - key: :string, - payload: {:optional, :byte_array} - ] - Macros.defpacket_serverbound :serverbound_plugin_message, 0x02, 767, [channel: :string, data: :raw] - Macros.defpacket_serverbound :acknowledge_finish_configuration, 0x03, 767, [] - Macros.defpacket_serverbound :serverbound_keep_alive, 0x04, 767, [id: :long] - Macros.defpacket_serverbound :pong, 0x05, 767, [id: :int] - Macros.defpacket_serverbound :resource_pack_response, 0x06, 767, [uuid: :uuid, result: :varint] - Macros.defpacket_serverbound :serverbound_known_packs, 0x07, 767, [ - packs: {:array, [ - namespace: :string, - id: :string, - version: :string - ]} - ] - - def handle(%{packet_type: :serverbound_plugin_message, channel: "minecraft:brand", data: data}, 767, state) do - {[string], ""} = Amethyst.Minecraft.Read.start(data) |> Amethyst.Minecraft.Read.string() |> Amethyst.Minecraft.Read.stop() - Logger.debug("Received brand: #{string}") - send(self(), {:send_packet, %{ - packet_type: :clientbound_plugin_message, - channel: "minecraft:brand", - data: Amethyst.Minecraft.Write.string("Amethyst") - }}) - state |> Map.put(:brand, string) - end - - def handle(%{ - packet_type: :client_information, - locale: locale, - view_distance: view_distance, - chat_mode: chat_mode, - chat_colors: chat_colors, - displayed_skin_parts: displayed_skin_parts, - main_hand: main_hand, - text_filtering: text_filtering, - allow_server_listings: allow_server_listings - }, 767, state) do - Logger.debug("Received client information") - send(self(), {:send_packet, %{ - packet_type: :feature_flags, - flags: [] - }}) - send(self(), {:send_packet, %{ - packet_type: :clientbound_known_packs, - packs: [%{namespace: "minecraft", id: "base", version: "1.21"}] - }}) - state - |> Map.put(:locale, locale) - |> Map.put(:view_distance, view_distance) - |> Map.put(:chat_mode, chat_mode) - |> Map.put(:chat_colors, chat_colors) - |> Map.put(:displayed_skin_parts, displayed_skin_parts) - |> Map.put(:main_hand, main_hand) - |> Map.put(:text_filtering, text_filtering) - |> Map.put(:allow_server_listings, allow_server_listings) - end - - def handle(%{packet_type: :serverbound_known_packs, packs: _packs}, 767, _state) do - Logger.debug("Received known packs") - import Amethyst.NBT.Write - # TODO: Of course, the registries shouldn't be hard-coded - send(self(), {:send_packet, %{ - packet_type: :registry_data, - id: "minecraft:dimension_type", - entries: [ - %{id: "amethyst:basic", data: compound(%{ - "has_skylight" => byte(1), - "has_ceiling" => byte(0), - "ultrawarm" => byte(0), - "natural" => byte(1), - "coordinate_scale" => float(1.0), - "bed_works" => byte(1), - "respawn_anchor_works" => byte(1), - "min_y" => int(0), - "height" => int(256), - "logical_height" => int(256), - "infiniburn" => string("#"), - "effects" => string("minecraft:overworld"), - "ambient_light" => float(0.0), - "piglin_safe" => byte(0), - "has_raids" => byte(1), - "monster_spawn_light_level" => int(0), - "monster_spawn_block_light_limit" => int(0) - })} - ] - }}) - send(self(), {:send_packet, %{ - packet_type: :registry_data, - id: "minecraft:painting_variant", entries: [ - %{id: "minecraft:kebab", data: compound(%{ - "asset_id" => string("minecraft:kebab"), - "height" => int(1), - "width" => int(1), - })} - ] - }}) - send(self(), {:send_packet, %{ - packet_type: :registry_data, - id: "minecraft:wolf_variant", - entries: [ - %{id: "minecraft:wolf_ashen", data: compound(%{ - "wild_texture" => string("minecraft:entity/wolf/wolf_ashen"), - "tame_texture" => string("minecraft:entity/wolf/wolf_ashen_tame"), - "angry_texture" => string("minecraft:entity/wolf/wolf_ashen_angry"), - "biomes" => string("amethyst:basic"), - })} - ] - }}) - # https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains - basic_biome = compound(%{ - "effects" => compound(%{ - "sky_color" => int(7_907_327), - "water_fog_color" => int(329_011), - "fog_color" => int(12_638_463), - "water_color" => int(4_159_204), - "mood_sound" => compound(%{ - "tick_delay" => int(6000), - "offset" => float(2.0), - "sound" => string("minecraft:ambient.cave"), - "block_search_extent" => int(8) - }), - }), - "has_precipitation" => byte(1), - "temperature" => float(0.8), - "downfall" => float(0.4), - }) - send(self(), {:send_packet, %{ - packet_type: :registry_data, - id: "minecraft:worldgen/biome", - entries: [ - %{id: "amethyst:basic", data: basic_biome}, - %{id: "minecraft:plains", data: basic_biome} - ] - }}) - # this game sucks - generic_damage = compound(%{ - "scaling" => string("when_caused_by_living_non_player"), - "exhaustion" => float(0.0), - "message_id" => string("generic") - }) - send(self(), {:send_packet, %{ - packet_type: :registry_data, - id: "minecraft:damage_type", - entries: [ - %{id: "minecraft:in_fire", data: generic_damage}, - %{id: "minecraft:campfire", data: generic_damage}, - %{id: "minecraft:lightning_bolt", data: generic_damage}, - %{id: "minecraft:on_fire", data: generic_damage}, - %{id: "minecraft:lava", data: generic_damage}, - %{id: "minecraft:cramming", data: generic_damage}, - %{id: "minecraft:drown", data: generic_damage}, - %{id: "minecraft:starve", data: generic_damage}, - %{id: "minecraft:cactus", data: generic_damage}, - %{id: "minecraft:fall", data: generic_damage}, - %{id: "minecraft:fly_into_wall", data: generic_damage}, - %{id: "minecraft:out_of_world", data: generic_damage}, - %{id: "minecraft:generic", data: generic_damage}, - %{id: "minecraft:magic", data: generic_damage}, - %{id: "minecraft:wither", data: generic_damage}, - %{id: "minecraft:dragon_breath", data: generic_damage}, - %{id: "minecraft:dry_out", data: generic_damage}, - %{id: "minecraft:sweet_berry_bush", data: generic_damage}, - %{id: "minecraft:freeze", data: generic_damage}, - %{id: "minecraft:stalagmite", data: generic_damage}, - %{id: "minecraft:outside_border", data: generic_damage}, - %{id: "minecraft:generic_kill", data: generic_damage}, - %{id: "minecraft:hot_floor", data: generic_damage}, - %{id: "minecraft:in_wall", data: generic_damage}, - ] - }}) - send(self(), {:send_packet, %{packet_type: :finish_configuration}}) - :ok - end - - def handle(%{packet_type: :acknowledge_finish_configuration}, 767, state) do - Logger.debug("Received acknowledge finish configuration") - send(self(), {:set_state, Amethyst.ConnectionState.Play}) - - game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create() - state = state |> Map.put(:game, game) - login = Amethyst.Game.login(game, state) - case login do - :reject -> - send(self(), {:disconnect, "Default game rejected connection"}) - :ok - {:accept, {x, y, z}, {yaw, pitch}} -> - send(self(), {:send_packet, %{ - packet_type: :login, - entity_id: 0, - is_hardcore: false, - dimensions: [%{name: "minecraft:overworld"}], - max_players: 0, - view_distance: 16, - simulation_distance: 16, - reduced_debug_info: false, - enable_respawn_screen: true, - do_limited_crafting: false, - dimension_type: 0, - dimension_name: "minecraft:overworld", - hashed_seed: 0, - game_mode: 1, - previous_game_mode: -1, - is_debug: false, - is_flat: false, - death_location: nil, - portal_cooldown: 0, - enforces_secure_chat: false - }}) - send(self(), {:send_packet, %{ - packet_type: :synchronize_player_position, - x: x, y: y, z: z, yaw: yaw, pitch: pitch, teleport_id: 0, flags: 0x00 - }}) - send(self(), {:send_packet, Amethyst.ConnectionState.Play.ge_start_waiting_for_level_chunks(767)}) - send(self(), {:send_packet, %{packet_type: :set_center_chunk, - chunk_x: div(floor(x), 16), - chunk_z: div(floor(z), 16) - }}) - send(self(), {:set_position, {x, y, z}}) - send(self(), {:send_packet, %{packet_type: :player_info_update_add_player, - players: [ - %{ - uuid: Map.get(state, :uuid), - name: Map.get(state, :name), - properties: Map.get(state, :properties) |> - Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end) - } - ] - }}) - # Begin keepalive loop - # TODO: Put it under some supervisor - me = self() - pid = spawn(fn -> Amethyst.ConnectionState.Play.keepalive_loop(me) end) - state |> Map.put(:keepalive, pid) - end - end - - def disconnect(reason) do - %{packet_type: :disconnect, reason: {:compound, %{ - "text" => {:string, reason} - }}} - end -end diff --git a/apps/amethyst/lib/states/handhsake.ex b/apps/amethyst/lib/states/handhsake.ex deleted file mode 100644 index 6e9c175..0000000 --- a/apps/amethyst/lib/states/handhsake.ex +++ /dev/null @@ -1,57 +0,0 @@ -# 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.Handshake do - @behaviour Amethyst.ConnectionState - require Amethyst.ConnectionState.Macros - alias Amethyst.ConnectionState.Macros - - require Logger - - @moduledoc """ - This module contains the packets and logic for the Handshake state. - """ - # Notice that in the Handshake state, our version is always 0. - Macros.defpacket_serverbound :handshake, 0x00, 0, [ - version: :varint, - address: :string, - port: :ushort, - next: :varint - ] - - def handle(%{packet_type: :handshake, version: 767, address: address, port: port, next: next}, 0, _state) do - Logger.debug("Received handshake for #{address}:#{port} with version 767") - case next do - 1 -> - send(self(), {:set_state, Amethyst.ConnectionState.Status}) - send(self(), {:set_version, 767}) - :ok - 2 -> - send(self(), {:set_state, Amethyst.ConnectionState.Login}) - send(self(), {:set_version, 767}) - :ok - 3 -> - send(self(), {:set_state, Amethyst.ConnectionState.Transfer}) - send(self(), {:set_version, 767}) - :ok - _ -> {:error, "Invalid next state"} - end - end - - def disconnect(_reason) do - # When disconnecting from the handshake state, we can't send any sort of reason. - end -end diff --git a/apps/amethyst/lib/states/login.ex b/apps/amethyst/lib/states/login.ex deleted file mode 100644 index 4c306a8..0000000 --- a/apps/amethyst/lib/states/login.ex +++ /dev/null @@ -1,153 +0,0 @@ -# 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.Login do - @behaviour Amethyst.ConnectionState - require Amethyst.ConnectionState.Macros - alias Amethyst.ConnectionState.Macros - - require Logger - - @moduledoc """ - This module contains the packets and logic for the Login state. - """ - Macros.defpacket_clientbound :disconnect, 0x00, 767, [reason: :json] - Macros.defpacket_clientbound :encryption_request, 0x01, 767, [ - server_id: :string, - public_key: :byte_array, - verify_token: :byte_array, - should_authenticate: :bool - ] - Macros.defpacket_clientbound :login_success, 0x02, 767, [ - uuid: :uuid, - username: :string, - properties: {:array, [ - name: :string, - value: :string, - signature: {:optional, :string} - ]}, - strict_error_handling: :bool - ] - Macros.defpacket_clientbound :set_compression, 0x03, 767, [threshold: :varint] - Macros.defpacket_clientbound :login_plugin_request, 0x04, 767, [ - message_id: :varint, - channel: :string, - data: :raw - ] - Macros.defpacket_clientbound :cookie_request, 0x05, 767, [identifier: :string] - - Macros.defpacket_serverbound :login_start, 0x00, 767, [name: :string, player_uuid: :uuid] - Macros.defpacket_serverbound :encryption_response, 0x01, 767, [ - shared_secret: :byte_array, - verify_token: :byte_array - ] - Macros.defpacket_serverbound :login_plugin_response, 0x02, 767, [ - message_id: :varint, - data: {:optional, :raw} - ] - Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, [] - Macros.defpacket_serverbound :cookie_response, 0x04, 767, [identifier: :string, payload: {:optional, :byte_array}] - - def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, state) do - Logger.debug("Received login start for #{name} with UUID #{player_uuid}") - if Application.get_env(:amethyst, :encryption, true) do - verify_token = :crypto.strong_rand_bytes(4) - public_key = Amethyst.Keys.get_pub() - Logger.debug("Public key: #{inspect(public_key, limit: :infinity)}") - send(self(), {:send_packet, %{ - packet_type: :encryption_request, - server_id: "", - public_key: public_key, - verify_token: verify_token, - should_authenticate: Application.get_env(:amethyst, :auth, false) - }}) - state |> Map.put(:verify_token, verify_token) |> Map.put(:name, name) |> Map.put(:uuid, player_uuid) - else - send(self(), {:send_packet, %{ - packet_type: :login_success, - uuid: player_uuid, - username: name, - properties: [], - strict_error_handling: true - }}) - :ok - end - end - def handle(%{packet_type: :encryption_response, shared_secret: secret, verify_token: verify_token}, 767, state) do - secret = Amethyst.Keys.decrypt(secret) - verify_token = Amethyst.Keys.decrypt(verify_token) - if verify_token == Map.get(state, :verify_token, :never) do - send(self(), {:set_encryption, secret}) - - if Application.get_env(:amethyst, :compression, nil) != nil do - threshold = Application.get_env(:amethyst, :compression, 0) - send(self(), {:send_packet, %{packet_type: :set_compression, threshold: threshold}}) - send(self(), {:set_compression, threshold}) - end - - if Application.get_env(:amethyst, :auth, false) == false do - # Don't check authentication - send(self(), {:send_packet, %{ - packet_type: :login_success, - uuid: Map.get(state, :uuid), - username: Map.get(state, :name), - properties: [], - strict_error_handling: true - }}) - Map.put(state, :authenticated, false) - else - # Check authentication - pubkey = Amethyst.Keys.get_pub() - hash = Amethyst.Minecraft.Sha1.hash(secret <> pubkey) - url = Application.get_env(:amethyst, :session_server, "https://sessionserver.mojang.com") <> "/session/minecraft/hasJoined?username=" <> - Map.get(state, :name) <> "&serverId=" <> hash # I don't think we need to verify the IP in the use case of Amethyst... - - response = Req.get!(url, - headers: [ - {"user-agent", "Amethyst/1.0"} - ]).body - - <> = response["id"] - uuid = [c1, c2, c3, c4, c5] |> Enum.join("-") - - send(self(), {:send_packet, %{ - packet_type: :login_success, - uuid: uuid, - username: response["name"], - properties: response["properties"] |> - Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end), - strict_error_handling: true - }}) - Map.put(state, :authenticated, true) |> Map.put(:uuid, uuid) |> Map.put(:name, response["name"]) |> Map.put(:properties, response["properties"]) - end - else - raise RuntimeError, "Invalid verify token. Broken encryption?" - end - end - def handle(%{packet_type: :login_acknowledged}, 767, _state) do - Logger.debug("Received login acknowledged") - send(self(), {:set_state, Amethyst.ConnectionState.Configuration}) - :ok - end - - def disconnect(reason) do - %{packet_type: :disconnect, reason: - %{ - "text" => reason - } - } - end -end diff --git a/apps/amethyst/lib/states/macros.ex b/apps/amethyst/lib/states/macros.ex deleted file mode 100644 index 88554ac..0000000 --- a/apps/amethyst/lib/states/macros.ex +++ /dev/null @@ -1,180 +0,0 @@ -# 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 . - -# TODO!!!: REDO THIS WHOLE THING AGAIN IT'S A MESS - -defmodule Amethyst.ConnectionState.Macros do - @moduledoc """ - Useful macros for defining packets. - """ - require Logger - defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do - quote do - @impl true - def deserialize(unquote(id), unquote(version), data) when unquote(where) do - {packet, ""} = Amethyst.ConnectionState.Macros.read_signature(data, unquote(signature)) - packet |> Map.put(:packet_type, unquote(name)) - end - end - end - - defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do - quote do - @impl true - def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) when unquote(where) do - # Don't check types if we are in release mode - if Application.get_env(:amethyst, :release, false) || Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do - Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature)) - else - raise "Invalid packet type for #{unquote(name)}! Got #{inspect(packet)}" - end - end - end - end - - alias Amethyst.Minecraft.Read - alias Amethyst.Minecraft.Write - - def read_signature(data, signature) do - names = Enum.map(signature, fn {name, _type} -> name end) - {got, rest} = Enum.reduce(signature, Read.start(data), fn {_name, type}, {acc, rest, :reversed} -> - case type do - {:optional, {:compound, signature}} -> - {[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop() - if exists do - {item, rest} = read_signature(rest, signature) - {[item | acc], rest, :reversed} - else - {[nil | acc], rest, :reversed} - end - {:optional, {:raw, length}} -> - {[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop() - if exists do - Read.raw({acc, rest, :reversed}, length) - else - {[nil | acc], rest, :reversed} - end - {:optional, t} -> - {[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop() - if exists do - apply(Read, t, [{acc, rest, :reversed}]) - else - {[nil | acc], rest, :reversed} - end - {:fixed_bitset, length} -> - Read.fixed_bitset({acc, rest, :reversed}, length) - {:raw, length} -> - Read.raw({acc, rest, :reversed}, length) - {:array, signature} -> - {[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop() - if count == 0 do - {[[] | acc], rest, :reversed} - else - {items, rest} = Enum.reduce(1..count, {[], rest}, fn _, {acc, rest} -> - {item, rest} = read_signature(rest, signature) - {[item | acc], rest} - end) - {[Enum.reverse(items) | acc], rest, :reversed} - end - t -> apply(Read, t, [{acc, rest, :reversed}]) - end - end) |> Read.stop() - - {Enum.zip(names, got) |> Map.new(), rest} - end - - def write_signature(packet, signature) do - Enum.reduce(signature, "", fn {name, type}, acc -> - case type do - {:optional, {:compound, signature}} -> - case Map.get(packet, name) do - nil -> acc <> Write.bool(false) - _ -> acc <> Write.bool(true) <> write_signature(Map.get(packet, name), signature) - end - {:optional, t} -> - case Map.get(packet, name) do - nil -> acc <> Write.bool(false) - _ -> acc <> Write.bool(true) <> apply(Write, t, [Map.get(packet, name)]) - end - {:array, signature} -> - acc <> Write.varint(Enum.count(Map.get(packet, name))) <> - Enum.reduce(Map.get(packet, name), "", fn item, acc -> - acc <> write_signature(item, signature) - end) - {:literal, type, value} -> acc <> apply(Write, type, [value]) - t -> acc <> apply(Write, t, [Map.get(packet, name)]) - end - end) - end - - def check_type(packet, signature) do - try do - Enum.all?(signature, fn {name, type} -> - case Map.get(packet, name, :missing) do - :missing -> - if is_tuple(type) && elem(type, 0) == :literal do - true - else - throw {:missing, name} - end - value -> case type_matches(value, type) do - true -> true - false -> throw {:mismatch, name, value, type} - end - end - end) - catch - reason -> - Logger.debug("Found invalid packet type: #{inspect(reason)}") - false - end - end - - def type_matches(value, :bool) when is_boolean(value), do: true - def type_matches(value, :byte) when is_integer(value) and value in -128..127, do: true - def type_matches(value, :ubyte) when is_integer(value) and value in 0..255, do: true - def type_matches(value, :short) when is_integer(value) and value in -32_768..32_767, do: true - def type_matches(value, :ushort) when is_integer(value) and value in 0..65_535, do: true - def type_matches(value, :int) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true - def type_matches(value, :long) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true - def type_matches(value, :float) when is_number(value), do: true - def type_matches(value, :double) when is_number(value), do: true - def type_matches(value, :varint) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true - def type_matches(value, :varlong) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true - def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true - def type_matches(value, :string) when is_binary(value), do: true - def type_matches(value, :raw) when is_binary(value), do: true - def type_matches(value, :byte_array) when is_binary(value), do: true - def type_matches({x, y, z}, :position) when - is_integer(x) and x in -33_554_432..33_554_431 and - is_integer(y) and y in -2048..2047 and - is_integer(z) and z in -33_554_432..33_554_431, do: true - def type_matches(value, :nbt), do: Amethyst.NBT.Write.check_type(value) - def type_matches(value, :json) do - case Jason.encode(value) do - {:ok, _} -> true - _ -> false - end - end - def type_matches(value, {:optional, _type}) when is_nil(value), do: true - def type_matches(value, {:optional, type}), do: type_matches(value, type) - def type_matches(value, {:array, signature}) when is_list(value), do: Enum.all?(value, fn item -> check_type(item, signature) end) - def type_matches(value, :bitset) when is_list(value), do: Enum.all?(value, fn item -> is_boolean(item) end) - def type_matches(value, {:compound, signature}) when is_map(value), do: check_type(value, signature) - def type_matches(_, _) do - false - end -end diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex deleted file mode 100644 index e11bbde..0000000 --- a/apps/amethyst/lib/states/play.ex +++ /dev/null @@ -1,284 +0,0 @@ -# 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 - @behaviour Amethyst.ConnectionState - 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: :bitset, - block_light_mask: :bitset, - empty_sky_light_mask: :bitset, - empty_block_light_mask: :bitset, - 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, 0x3E, 767, [ - actions: {:literal, :byte, 0x01}, - players: {:array, [ - uuid: :uuid, - name: :string, - properties: {:array, [ - name: :string, - value: :string, - signature: {:optional, :string}, - ]} - ]} - ] - Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x3E, 767, [ - actions: {:literal, :byte, 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, 0x3E, 767, [ - actions: {:literal, :byte, 0x04}, - players: {:array, [ - uuid: :uuid, - gamemode: :varint - ]} - ] - Macros.defpacket_clientbound :player_info_update_update_listed, 0x3E, 767, [ - actions: {:literal, :byte, 0x08}, - players: {:array, [ - uuid: :uuid, - listed: :bool - ]} - ] - Macros.defpacket_clientbound :player_info_update_update_latency, 0x3E, 767, [ - actions: {:literal, :byte, 0x10}, - players: {:array, [ - uuid: :uuid, - ping: :varint, # Milliseconds - ]} - ] - Macros.defpacket_clientbound :player_info_update_update_display_name, 0x3E, 767, [ - actions: {:literal, :byte, 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 :system_chat_message, 0x6C, 767, [ - content: :nbt, - overlay: :bool - ] - - 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 :chat_message, 0x06, 767, [ - message: :string, - timestamp: :long, - salt: :long, - signature: {:optional, {:raw, 256}}, - message_count: :varint, - acknowledged: {:fixed_bitset, 20} - ] - 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: :chat_message, message: msg, timestamp: _, salt: _, signature: _, message_count: _, acknowledged: _}, 767, state) do - # We will never support message signing - state |> Map.get(:game) |> Amethyst.Game.chat(msg) - :ok - 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 diff --git a/apps/amethyst/lib/states/status.ex b/apps/amethyst/lib/states/status.ex deleted file mode 100644 index 0f549a4..0000000 --- a/apps/amethyst/lib/states/status.ex +++ /dev/null @@ -1,61 +0,0 @@ -# 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.Status do - @behaviour Amethyst.ConnectionState - require Amethyst.ConnectionState.Macros - alias Amethyst.ConnectionState.Macros - - require Logger - - @moduledoc """ - This module contains the packets and logic for the Status state. - """ - Macros.defpacket_clientbound :status_response, 0x00, 767, [json_response: :string] - Macros.defpacket_clientbound :pong_response, 0x01, 767, [payload: :long] - - Macros.defpacket_serverbound :status_request, 0x00, 767, [] - Macros.defpacket_serverbound :ping_request, 0x01, 767, [payload: :long] - - def handle(%{packet_type: :status_request}, 767, _state) do - Logger.debug("Received status request") - send(self(), {:send_packet, %{ - packet_type: :status_response, - json_response: ~s({ -"version": {"name": "1.21", "protocol": 767}, -"players": {"max": -1, "online": 69, "sample": [{"name": "§dAmethyst§r", "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"}]}, -"description": {"text":"Amethyst is an experimental server written in Elixir"}, -"enforcesSecureChat": false, -"previewsChat": false, -"preventsChatReports": true -}) - }}) - :ok - end - - def handle(%{packet_type: :ping_request, payload: payload}, 767, _state) do - Logger.debug("Received ping request") - send(self(), {:send_packet, %{ - packet_type: :pong_response, - payload: payload - }}) - :ok - end - - def disconnect(_reason) do - # When disconnecting from the status state, we can't send any sort of reason. - end -end