# 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.Minecraft.Write do import Bitwise @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 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 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 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 @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 end