From 204c6b0142ccce6782bb0831ebfc9018e19b78b9 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Sun, 21 Jul 2024 01:59:57 +0200 Subject: [PATCH 1/7] Get past configuration stage --- lib/servers/configuration.ex | 29 +++++++++++++++++--- lib/servers/play.ex | 52 ++++++++++++++++++++++++++++++++++++ mix.exs | 2 +- 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 lib/servers/play.ex diff --git a/lib/servers/configuration.ex b/lib/servers/configuration.ex index e789a15..946e203 100644 --- a/lib/servers/configuration.ex +++ b/lib/servers/configuration.ex @@ -97,11 +97,11 @@ defmodule Amethyst.Server.Configuration do # Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs def deserialize(0x07, data) do {[count], rest} = Read.start(data) |> Read.varint |> Read.stop - {packs, _} = Enum.reduce(0..count, {[], rest}, fn _, {acc, rest} -> + {packs, _} = Enum.reduce(1..count, {[], rest}, fn _, {acc, rest} -> {[namespace, id, version], rest} = Read.start(rest) |> Read.string |> Read.string |> Read.string |> Read.stop {[{namespace, id, version} | acc], rest} end) - {:resource_pack_stack, packs} + {:serverbound_known_packs, packs} end def deserialize(type, _) do raise RuntimeError, "Got unknown packet type #{type}!" @@ -235,12 +235,35 @@ defmodule Amethyst.Server.Configuration do ## HANDLING @impl true + # Client Information https://wiki.vg/Protocol#Client_Information + def handle({:client_information, locale, v_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing}, client, state) do + state = state |> Keyword.put(:locale, locale) |> Keyword.put(:view_dist, v_dist) |> Keyword.put(:chat_mode, chat_mode) |> + Keyword.put(:chat_colors, chat_colors) |> Keyword.put(:displayed_skin_parts, displayed_skin_parts) |> Keyword.put(:main_hand, main_hand) |> + Keyword.put(:text_filtering, text_filtering) |> Keyword.put(:allow_listing, allow_listing) + # TODO: Here we should create the game handling task for this player and give it + # this data. + transmit({:clientbound_plugin_message, "minecraft:brand", Write.string("amethyst")}, client) + transmit({:clientbound_known_packs, [{"minecraft", "core", "1.21"}]}, client) + {:ok, state} + end + # Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs + def handle({:serverbound_known_packs, _packs}, client, state) do + # L + ratio + don't care + didn't ask + finish configuration + # TODO: we should send registries i think? does amethyst need to deal with vanilla registries at all? god only knows + transmit({:finish_configuration}, client) + {:ok, state} + end + # Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration + def handle({:acknowledge_finish_configuration}, client, state) do + Amethyst.Server.Play.serve(client, state) + end # Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration) def handle({:serverbound_plugin_message, channel, data}, client, state) do handle_plugin_message(channel, data, client, state) end - def handle(tuple, _) do + def handle(tuple, state) do Logger.error("Unhandled but known packet #{elem(tuple, 0)}") + {:unhandled, state} end defp handle_plugin_message("minecraft:brand", data, _client, state) do {[brand], ""} = Read.start(data) |> Read.string |> Read.stop diff --git a/lib/servers/play.ex b/lib/servers/play.ex new file mode 100644 index 0000000..69c4e1f --- /dev/null +++ b/lib/servers/play.ex @@ -0,0 +1,52 @@ +# 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.Server.Play do + @moduledoc """ + This module contains the logic for the Play stage of the server. + """ + require Logger + use Amethyst.Server + + alias Amethyst.Minecraft.Read + alias Amethyst.Minecraft.Write + + @impl true + def init(state) do + state + end + + ## DESERIALIZATION + @impl true + + def deserialize(type, _) do + raise RuntimeError, "Got unknown packet type #{type}!" + end + + ## SERIALIZATION + @impl true + + def serialize(packet) do + raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" + end + + ## HANDLING + @impl true + def handle(tuple, _, state) do + Logger.error("Unhandled but known packet #{elem(tuple, 0)}") + {:unhandled, state} + end +end diff --git a/mix.exs b/mix.exs index bed6248..d24cd48 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,7 @@ defmodule Amethyst.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger], + extra_applications: [:logger, :public_key], mod: {Amethyst.Application, []} ] end From 8572ac5443f23716c48fb8acdfd115d84cff1605 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Sun, 21 Jul 2024 03:13:30 +0200 Subject: [PATCH 2/7] Add big and awful code --- lib/data.ex | 4 ++++ lib/servers/play.ex | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/data.ex b/lib/data.ex index a927731..7ffe1db 100644 --- a/lib/data.ex +++ b/lib/data.ex @@ -63,6 +63,10 @@ defmodule Amethyst.Minecraft.Write do def string(value) do <> end + + def position({x, y, z}) do + <> + end end defmodule Amethyst.Minecraft.Read do diff --git a/lib/servers/play.ex b/lib/servers/play.ex index 69c4e1f..55daef5 100644 --- a/lib/servers/play.ex +++ b/lib/servers/play.ex @@ -38,7 +38,23 @@ defmodule Amethyst.Server.Play do ## SERIALIZATION @impl true - + # Login https://wiki.vg/Protocol#Login_(play) + def serialize({:login, eid, hardcore, dimensions, + max_players, view_distance, simulation_distance, + reduce_debug, enable_respawn_screen, limited_crafting, + dim_type, dim_name, hashed_seed, gamemode, prev_gm, + is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) do + # TODO: this singlehandedly made me regret not making the write API better, please rework :( + <> <> if(hardcore, do: <<1::big-8>>, else: <<0::big-8>>) <> + Write.varint(length(dimensions)) <> Enum.reduce(dimensions, "", fn dim, acc -> acc <> Write.string(dim) end) <> + Write.varint(max_players) <> Write.varint(view_distance) <> Write.varint(simulation_distance) <> if(reduce_debug, do: <<1::big-8>>, else: <<0::big-8>>) <> + if(enable_respawn_screen, do: <<1::big-8>>, else: <<0::big-8>>) + if(limited_crafting, do: <<1::big-8>>, else: <<0::big-8>>) <> Write.varint(dim_type) <> Write.string(dim_name) <> + hashed_seed <> <> <> <> <> + if(is_debug, do: <<1::big-8>>, else: <<0::big-8>>) <> if(is_flat, do: <<1::big-8>>, else: <<0::big-8>>) <> + if(death_loc == nil, do: <<0::big-8>>, else: <<1::big-8>> <> Write.string(elem(death_loc, 0)) <> Write.position(elem(death_loc, 1))) <> + Write.varint(portal_cooldown) <> if(enforce_chat, do: <<1::big-8>>, else: <<0::big-8>>) + end def serialize(packet) do raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" end @@ -49,4 +65,14 @@ defmodule Amethyst.Server.Play do Logger.error("Unhandled but known packet #{elem(tuple, 0)}") {:unhandled, state} end + + defp gamemode_id(gm) do + case gm do + nil -> -1 + :survival -> 0 + :creative -> 1 + :adventure -> 2 + :spectator -> 3 + end + end end From b10be1407992db6a70f0cecfd84b4b96839ca8f5 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Sun, 21 Jul 2024 03:26:10 +0200 Subject: [PATCH 3/7] Begin implementing login (play) --- lib/servers/configuration.ex | 5 +++++ lib/servers/play.ex | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/servers/configuration.ex b/lib/servers/configuration.ex index 946e203..2224982 100644 --- a/lib/servers/configuration.ex +++ b/lib/servers/configuration.ex @@ -255,6 +255,11 @@ defmodule Amethyst.Server.Configuration do end # Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration def handle({:acknowledge_finish_configuration}, client, state) do + # TODO: All of this stuff should obviously not be hardcoded here + Amethyst.Server.Play.transmit({:login, + 0, false, ["minecraft:overworld"], 0, 16, 16, false, true, true, 0, + "minecraft:overworld", <<0::64>>, :spectator, nil, false, true, nil, 0, false + }, client) Amethyst.Server.Play.serve(client, state) end # Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration) diff --git a/lib/servers/play.ex b/lib/servers/play.ex index 55daef5..ed5f28b 100644 --- a/lib/servers/play.ex +++ b/lib/servers/play.ex @@ -45,6 +45,7 @@ defmodule Amethyst.Server.Play do dim_type, dim_name, hashed_seed, gamemode, prev_gm, is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) do # TODO: this singlehandedly made me regret not making the write API better, please rework :( + Write.varint(0x2B) <> <> <> if(hardcore, do: <<1::big-8>>, else: <<0::big-8>>) <> Write.varint(length(dimensions)) <> Enum.reduce(dimensions, "", fn dim, acc -> acc <> Write.string(dim) end) <> Write.varint(max_players) <> Write.varint(view_distance) <> Write.varint(simulation_distance) <> if(reduce_debug, do: <<1::big-8>>, else: <<0::big-8>>) <> From 8e57427c999458b413d1093f9a1f1f0cf5427806 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 1 Aug 2024 04:48:01 +0200 Subject: [PATCH 4/7] Add exdoc --- mix.exs | 12 ++++++++++-- mix.lock | 6 ++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index d24cd48..0aeb80d 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,14 @@ defmodule Amethyst.MixProject do version: "0.1.0", elixir: "~> 1.17", start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + + name: "Amethyst", + source_url: "https://git.colon-three.com/kodi/amethyst", + docs: [ + main: "readme", + extras: ["README.md", "LICENSE.md"] + ] ] end @@ -23,7 +30,8 @@ defmodule Amethyst.MixProject do defp deps do [ {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:uuid, "~> 1.1"} + {:uuid, "~> 1.1"}, + {:ex_doc, "~> 0.22", only: :dev, runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 15dbe6b..cf7b270 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,13 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, } From 9f9526f5dea73b4b492b30aca41862ff2f8d2a16 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 1 Aug 2024 05:14:05 +0200 Subject: [PATCH 5/7] Flattened writing interface --- lib/data.ex | 63 ++++++++++++++++++++++++++++++++++++------- lib/servers/play.ex | 16 +++++------ lib/servers/status.ex | 2 +- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/lib/data.ex b/lib/data.ex index 7ffe1db..239e33a 100644 --- a/lib/data.ex +++ b/lib/data.ex @@ -18,21 +18,55 @@ defmodule Amethyst.Minecraft.Write do import Bitwise @moduledoc """ - This module contains functions for writing certain Minecraft data types which are more complex - than simple binary data. + 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) do + def uuid(uuid) when is_binary(uuid) do UUID.string_to_binary!(uuid) end - def bool(value) do + def bool(value) when is_boolean(value) do case value do true -> <<0x01::8>> false -> <<0x00::8>> end 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) @@ -73,13 +107,12 @@ defmodule Amethyst.Minecraft.Read do import Bitwise @moduledoc """ - This module contains functions for reading Minecraft data. Unlike Amethyst.Minecraft.Write, this - includes all supported data types in order to make the interface more consistent. + 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. + 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 @@ -87,9 +120,15 @@ defmodule Amethyst.Minecraft.Read do {[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 @@ -139,6 +178,9 @@ defmodule Amethyst.Minecraft.Read do {[data | acc], rest, :reversed} 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))) @@ -155,6 +197,9 @@ defmodule Amethyst.Minecraft.Read 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))) @@ -165,10 +210,10 @@ defmodule Amethyst.Minecraft.Read do {[value | acc], rest, :reversed} end def varlong(_, read, _) when read >= 10 do - raise RuntimeError, "Got a varint which is too big!" + raise RuntimeError, "Got a varlong which is too big!" end def varlong({_, ""}, _, _) do - raise RuntimeError, "Got an incomplete varint!" + raise RuntimeError, "Got an incomplete varlong!" end def string({acc, data, :reversed}) do diff --git a/lib/servers/play.ex b/lib/servers/play.ex index ed5f28b..03c4d20 100644 --- a/lib/servers/play.ex +++ b/lib/servers/play.ex @@ -44,17 +44,17 @@ defmodule Amethyst.Server.Play do reduce_debug, enable_respawn_screen, limited_crafting, dim_type, dim_name, hashed_seed, gamemode, prev_gm, is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) do - # TODO: this singlehandedly made me regret not making the write API better, please rework :( + # TODO: This is a big unreadable slab of serialization, it needs a proper rework at some point Write.varint(0x2B) <> - <> <> if(hardcore, do: <<1::big-8>>, else: <<0::big-8>>) <> + Write.int(eid) <> Write.bool(hardcore) <> Write.varint(length(dimensions)) <> Enum.reduce(dimensions, "", fn dim, acc -> acc <> Write.string(dim) end) <> - Write.varint(max_players) <> Write.varint(view_distance) <> Write.varint(simulation_distance) <> if(reduce_debug, do: <<1::big-8>>, else: <<0::big-8>>) <> - if(enable_respawn_screen, do: <<1::big-8>>, else: <<0::big-8>>) - if(limited_crafting, do: <<1::big-8>>, else: <<0::big-8>>) <> Write.varint(dim_type) <> Write.string(dim_name) <> - hashed_seed <> <> <> <> <> - if(is_debug, do: <<1::big-8>>, else: <<0::big-8>>) <> if(is_flat, do: <<1::big-8>>, else: <<0::big-8>>) <> + Write.varint(max_players) <> Write.varint(view_distance) <> Write.varint(simulation_distance) <> Write.bool(reduce_debug) <> + Write.bool(enable_respawn_screen) <> + Write.bool(limited_crafting) <> Write.varint(dim_type) <> Write.string(dim_name) <> + hashed_seed <> Write.ubyte(gamemode_id(gamemode)) <> Write.byte(gamemode_id(prev_gm)) <> + Write.bool(is_debug) <> Write.bool(is_flat) <> if(death_loc == nil, do: <<0::big-8>>, else: <<1::big-8>> <> Write.string(elem(death_loc, 0)) <> Write.position(elem(death_loc, 1))) <> - Write.varint(portal_cooldown) <> if(enforce_chat, do: <<1::big-8>>, else: <<0::big-8>>) + Write.varint(portal_cooldown) <> Write.bool(enforce_chat) end def serialize(packet) do raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" diff --git a/lib/servers/status.ex b/lib/servers/status.ex index 23a272b..d49150a 100644 --- a/lib/servers/status.ex +++ b/lib/servers/status.ex @@ -51,7 +51,7 @@ defmodule Amethyst.Server.Status do Write.varint(0x00) <> Write.string(data) end def serialize({:ping_response, payload}) do - Write.varint(0x01) <> <> + Write.varint(0x01) <> Write.long(payload) end def serialize(packet) do raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" From 9c6c275b3e25a63d58224c8f0676741a10451ac3 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 1 Aug 2024 06:05:04 +0200 Subject: [PATCH 6/7] Add useful Write functions --- lib/data.ex | 22 ++++++++++++++++++++++ lib/servers/configuration.ex | 24 ++++++++++-------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/data.ex b/lib/data.ex index 239e33a..8a8aedb 100644 --- a/lib/data.ex +++ b/lib/data.ex @@ -101,6 +101,28 @@ defmodule Amethyst.Minecraft.Write do 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 end defmodule Amethyst.Minecraft.Read do diff --git a/lib/servers/configuration.ex b/lib/servers/configuration.ex index 2224982..7457e68 100644 --- a/lib/servers/configuration.ex +++ b/lib/servers/configuration.ex @@ -143,16 +143,12 @@ defmodule Amethyst.Server.Configuration do end # Remove Resource Pack https://wiki.vg/Protocol#Remove_Resource_Pack_(configuration) def serialize({:remove_resource_pack, id}) do - if id == nil do - Write.varint(0x08) <> <<0x00>> - else - Write.varint(0x08) <> <<0x01>> <> Write.string(id) - end + Write.option(id, &Write.string/1) end # Add Resource Pack https://wiki.vg/Protocol#Add_Resource_Pack_(configuration) def serialize({:add_resource_pack, id, url, hash, forced, msg}) do Write.varint(0x09) <> Write.string(id) <> Write.string(url) <> Write.string(hash) <> - if(forced, do: <<0x01>>, else: <<0x00>>) <> if(msg == nil, do: <<0x00>>, else: <<0x01>> <> Write.string(msg)) + Write.bool(forced) <> Write.option(msg, &Write.string/1) end # Store Cookie https://wiki.vg/Protocol#Store_Cookie_(configuration) def serialize({:store_cookie, id, data}) do @@ -164,7 +160,7 @@ defmodule Amethyst.Server.Configuration do end # Feature Flags https://wiki.vg/Protocol#Feature_Flags def serialize({:feature_flags, flags}) do - Write.varint(0x0C) <> Write.varint(length(flags)) <> Enum.reduce(flags, "", fn id, acc -> acc <> Write.string(id) end) + Write.varint(0x0C) <> Write.varint(length(flags)) <> Write.list(flags, &Write.string/1) end # Update Tags https://wiki.vg/Protocol#Update_Tags def serialize({:update_tags, tags}) do @@ -174,19 +170,17 @@ defmodule Amethyst.Server.Configuration do # Clientbound Known Packs https://wiki.vg/Protocol#Clientbound_Known_Packs def serialize({:clientbound_known_packs, packs}) do Write.varint(0x0E) <> Write.varint(length(packs)) <> - Enum.reduce(packs, "", fn {namespace, id, version}, acc -> acc <> Write.string(namespace) <> Write.string(id) <> Write.string(version) end) + Write.list(packs, fn {namespace, id, version} -> Write.string(namespace) <> Write.string(id) <> Write.string(version) end) end # Custom Report Details https://wiki.vg/Protocol#Custom_Report_Details def serialize({:custom_report_details, details}) do Write.varint(0x0F) <> Write.varint(length(details)) <> - Enum.reduce(details, "", fn {id, data}, acc -> acc <> Write.string(id) <> Write.string(data) end) + Write.list(details, fn {id, data} -> Write.string(id) <> Write.string(data) end) end # Server Links https://wiki.vg/Protocol#Server_Links_(configuration) def serialize({:server_links, links}) do Write.varint(0x10) <> Write.varint(length(links)) <> - Enum.reduce(links, "", fn {label, url}, acc -> - acc <> serialize_link_label(label) <> Write.string(url) - end) + Write.list(links, fn {label, url} -> serialize_link_label(label) <> Write.string(url) end) end def serialize(packet) do raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}" @@ -196,8 +190,10 @@ defmodule Amethyst.Server.Configuration do acc <> Write.string(id) <> Write.varint(length(elements)) <> serialize_elements(elements) end defp serialize_elements(elements) do - Enum.reduce(elements, "", fn {id, ids}, acc -> acc <> Write.string(id) <> Write.varint(length(ids)) <> - Enum.reduce(ids, "", fn id, acc -> acc <> Write.varint(id) end) end) + Write.list(elements, fn {id, ids} -> + Write.string(id) <> Write.varint(length(ids)) <> + Write.list(ids, &Write.varint/1) + end) end defp serialize_link_label(:bug_report) do <<0x01>> <> Write.varint(0x00) From b1fa25fb1d977bcaa7d467d3ea4bc86697de6797 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 1 Aug 2024 06:42:43 +0200 Subject: [PATCH 7/7] Fix bug in serializing login (play) but revealed other, more confusing bug --- lib/servers/play.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/servers/play.ex b/lib/servers/play.ex index 03c4d20..ce6157b 100644 --- a/lib/servers/play.ex +++ b/lib/servers/play.ex @@ -43,11 +43,11 @@ defmodule Amethyst.Server.Play do max_players, view_distance, simulation_distance, reduce_debug, enable_respawn_screen, limited_crafting, dim_type, dim_name, hashed_seed, gamemode, prev_gm, - is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) do - # TODO: This is a big unreadable slab of serialization, it needs a proper rework at some point + is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) when byte_size(hashed_seed) == 8 do + # TODO: This is a big unreadable slab of serialization which makes bugs really hard to catch, it needs a proper rework at some point Write.varint(0x2B) <> Write.int(eid) <> Write.bool(hardcore) <> - Write.varint(length(dimensions)) <> Enum.reduce(dimensions, "", fn dim, acc -> acc <> Write.string(dim) end) <> + Write.varint(length(dimensions)) <> Write.list(dimensions, &Write.string/1) <> Write.varint(max_players) <> Write.varint(view_distance) <> Write.varint(simulation_distance) <> Write.bool(reduce_debug) <> Write.bool(enable_respawn_screen) <> Write.bool(limited_crafting) <> Write.varint(dim_type) <> Write.string(dim_name) <>