Merge branch 'main' of git.colon-three.com:kodi/amethyst
All checks were successful
Build & Test / nix-build (push) Successful in 1m12s

This commit is contained in:
Kodi Craft 2024-08-04 07:44:13 +02:00
commit a285e31175
Signed by: kodi
GPG Key ID: 69D9EED60B242822
6 changed files with 218 additions and 30 deletions

View File

@ -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
<<value::8-signed-big>>
end
def ubyte(value) when value in 0..255 do
<<value::8-unsigned-big>>
end
def short(value) when value in -32_768..32_767 do
<<value::16-signed-big>>
end
def ushort(value) when value in 0..65_535 do
<<value::16-unsigned-big>>
end
def int(value) when value in -2_147_483_648..2_147_483_647 do
<<value::32-signed-big>>
end
def long(value) when value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807 do
<<value::64-signed-big>>
end
def float(value) when is_number(value) do
<<value::32-float>>
end
def double(value) when is_number(value) do
<<value::64-float>>
end
def varint(value) when value in -2_147_483_648..2_147_483_647 do
<<value::32-unsigned>> = <<value::32-signed>> # This is a trick to allow the arithmetic shift to act as a logical shift
varnum("", value)
@ -63,19 +97,44 @@ defmodule Amethyst.Minecraft.Write do
def string(value) do
<<varint(byte_size(value))::binary, value::binary>>
end
def position({x, y, z}) do
<<x::signed-big-26, z::signed-big-26, y::signed-big-12>>
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
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
@ -83,9 +142,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
@ -135,6 +200,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)))
@ -151,6 +219,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)))
@ -161,10 +232,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

View File

@ -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}!"
@ -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)
@ -235,12 +231,40 @@ 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
# 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)
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

79
lib/servers/play.ex Normal file
View File

@ -0,0 +1,79 @@
# 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 <https://www.gnu.org/licenses/>.
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
# 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}) 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)) <> 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) <>
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) <> Write.bool(enforce_chat)
end
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
defp gamemode_id(gm) do
case gm do
nil -> -1
:survival -> 0
:creative -> 1
:adventure -> 2
:spectator -> 3
end
end
end

View File

@ -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) <> <<payload::64-big-signed>>
Write.varint(0x01) <> Write.long(payload)
end
def serialize(packet) do
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"

14
mix.exs
View File

@ -7,14 +7,21 @@ 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
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
extra_applications: [:logger, :public_key],
mod: {Amethyst.Application, []}
]
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

View File

@ -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"},
}