amethyst/lib/data.ex

156 lines
5.1 KiB
Elixir
Raw Normal View History

2024-07-07 11:47:48 +02:00
# 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 certain Minecraft data types which are more complex
than simple binary data.
"""
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
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.
These functions allow you to chain them into eachother, at the end they will produce a list of all the
values they have read.
2024-07-07 13:35:21 +02:00
You may use the helper function Amethyst.Minecraft.Read.start/1 to start the chain with a binary buffer.
2024-07-07 19:05:32 +02:00
The return value of the chain is a tuple containing the list of values and the remaining binary buffer.
iex> alias Amethyst.Minecraft.Read
2024-07-07 19:05:32 +02:00
iex> {[_, _, _], ""} = Read.start(<<1, 999::16, 64>>) |> Read.bool() |> Read.short() |> Read.byte() |> Read.stop()
{[true, 999, 64], ""}
"""
def start(binary) do
2024-07-07 19:05:32 +02:00
{[], binary, :reversed}
end
def stop({acc, rest, :reversed}) do
{Enum.reverse(acc), rest}
end
2024-07-07 19:05:32 +02:00
def bool({acc, <<value, rest::binary>>, :reversed}) do
{[value != 0 | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def byte({acc, <<value::big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def ubyte({acc, <<value::big-unsigned, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def short({acc, <<value::16-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def ushort({acc, <<value::16-big-unsigned, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def int({acc, <<value::32-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def long({acc, <<value::64-big-signed, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def float({acc, <<value::32-float-big, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
2024-07-07 19:05:32 +02:00
def double({acc, <<value::64-float-big, rest::binary>>, :reversed}) do
{[value | acc], rest, :reversed}
end
def varint(tuple, read \\ 0, nacc \\ 0)
2024-07-07 19:05:32 +02:00
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
2024-07-07 19:05:32 +02:00
def varint({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do
total = nacc + (value <<< (7 * read))
<<value::32-signed>> = <<total::32-unsigned>>
2024-07-07 19:05:32 +02:00
{[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
def varlong(tuple, read \\ 0, nacc \\ 0)
2024-07-07 19:05:32 +02:00
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
2024-07-07 19:05:32 +02:00
def varlong({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do
total = nacc + (value <<< (7 * read))
<<value::64-signed>> = <<total::64-unsigned>>
2024-07-07 19:05:32 +02:00
{[value | acc], rest, :reversed}
end
def varlong(_, read, _) when read >= 10 do
raise RuntimeError, "Got a varint which is too big!"
end
def varlong({_, ""}, _, _) do
raise RuntimeError, "Got an incomplete varint!"
end
2024-07-07 19:05:32 +02:00
def string({acc, data, :reversed}) do
{[length], rest, :reversed} = start(data) |> varint()
<<value::binary-size(length), rest::binary>> = rest
2024-07-07 19:05:32 +02:00
{[value | acc], rest, :reversed}
end
end