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