amethyst/lib/data.ex
Kodi Craft 9c6c275b3e
All checks were successful
Build & Test / nix-build (push) Successful in 1m17s
Add useful Write functions
2024-08-01 06:05:04 +02:00

247 lines
7.5 KiB
Elixir

# 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.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
<<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)
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
<<value::64-unsigned>> = <<value::64-signed>>
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
<<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.
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, <<value, rest::binary>>, :reversed}) do
{[value != 0 | acc], rest, :reversed}
end
def byte({acc, <<value::big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def ubyte({acc, <<value::big-unsigned, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def short({acc, <<value::16-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def ushort({acc, <<value::16-big-unsigned, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def int({acc, <<value::32-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def long({acc, <<value::64-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def float({acc, <<value::32-float-big, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def double({acc, <<value::64-float-big, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def uuid({acc, <<uuid::16-binary, rest::binary>>, :reversed}) do
{[UUID.binary_to_string!(uuid) | acc], rest, :reversed}
end
def raw({acc, data, :reversed}, amount) do
<<data::binary-size(amount), rest::binary>> = 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::32-signed>> = <<total::32-unsigned>>
{[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::64-signed>> = <<total::64-unsigned>>
{[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()
<<value::binary-size(length), rest::binary>> = rest
{[value | acc], rest, :reversed}
end
end