Compare commits

..

No commits in common. "main" and "first-attempt" have entirely different histories.

19 changed files with 157 additions and 614 deletions

View File

@ -86,8 +86,6 @@
{Credo.Check.Readability.UnnecessaryAliasExpansion, []}, {Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []}, {Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []}, {Credo.Check.Readability.WithSingleClause, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
# #
## Refactoring Opportunities ## Refactoring Opportunities
@ -108,8 +106,6 @@
{Credo.Check.Refactor.RejectReject, []}, {Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []}, {Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []}, {Credo.Check.Refactor.WithClauses, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.FilterReject, []},
# #
## Warnings ## Warnings
@ -150,6 +146,7 @@
{Credo.Check.Design.DuplicatedCode, []}, {Credo.Check.Design.DuplicatedCode, []},
{Credo.Check.Design.SkipTestWithoutComment, []}, {Credo.Check.Design.SkipTestWithoutComment, []},
{Credo.Check.Readability.AliasAs, []}, {Credo.Check.Readability.AliasAs, []},
{Credo.Check.Readability.BlockPipe, []},
{Credo.Check.Readability.ImplTrue, []}, {Credo.Check.Readability.ImplTrue, []},
{Credo.Check.Readability.MultiAlias, []}, {Credo.Check.Readability.MultiAlias, []},
{Credo.Check.Readability.NestedFunctionCalls, []}, {Credo.Check.Readability.NestedFunctionCalls, []},
@ -157,13 +154,16 @@
{Credo.Check.Readability.OnePipePerLine, []}, {Credo.Check.Readability.OnePipePerLine, []},
{Credo.Check.Readability.SeparateAliasRequire, []}, {Credo.Check.Readability.SeparateAliasRequire, []},
{Credo.Check.Readability.SingleFunctionToBlockPipe, []}, {Credo.Check.Readability.SingleFunctionToBlockPipe, []},
{Credo.Check.Readability.SinglePipe, []},
{Credo.Check.Readability.Specs, []}, {Credo.Check.Readability.Specs, []},
{Credo.Check.Readability.StrictModuleLayout, []}, {Credo.Check.Readability.StrictModuleLayout, []},
{Credo.Check.Readability.WithCustomTaggedTuple, []}, {Credo.Check.Readability.WithCustomTaggedTuple, []},
{Credo.Check.Refactor.ABCSize, []}, {Credo.Check.Refactor.ABCSize, []},
{Credo.Check.Refactor.AppendSingleItem, []}, {Credo.Check.Refactor.AppendSingleItem, []},
{Credo.Check.Refactor.DoubleBooleanNegation, []}, {Credo.Check.Refactor.DoubleBooleanNegation, []},
{Credo.Check.Refactor.FilterReject, []},
{Credo.Check.Refactor.IoPuts, []}, {Credo.Check.Refactor.IoPuts, []},
{Credo.Check.Refactor.MapMap, []},
{Credo.Check.Refactor.ModuleDependencies, []}, {Credo.Check.Refactor.ModuleDependencies, []},
{Credo.Check.Refactor.NegatedIsNil, []}, {Credo.Check.Refactor.NegatedIsNil, []},
{Credo.Check.Refactor.PassAsyncInTestCases, []}, {Credo.Check.Refactor.PassAsyncInTestCases, []},
@ -174,8 +174,9 @@
{Credo.Check.Warning.LeakyEnvironment, []}, {Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.MapGetUnsafePass, []}, {Credo.Check.Warning.MapGetUnsafePass, []},
{Credo.Check.Warning.MixEnv, []}, {Credo.Check.Warning.MixEnv, []},
{Credo.Check.Warning.UnsafeToAtom, []}, {Credo.Check.Warning.UnsafeToAtom, []}
{Credo.Check.Refactor.MapInto, []},
# {Credo.Check.Refactor.MapInto, []},
# #
# Custom checks can be created using `mix credo.gen.check`. # Custom checks can be created using `mix credo.gen.check`.

View File

@ -19,7 +19,6 @@ defmodule Amethyst.ConnectionHandler do
This module is responsible for handling incoming packets and sending outgoing packets. It keeps track of what state the connection is in and which game should This module is responsible for handling incoming packets and sending outgoing packets. It keeps track of what state the connection is in and which game should
receive the packets. receive the packets.
""" """
alias Amethyst.Minecraft.Write
require Logger require Logger
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec() @spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
@ -49,8 +48,11 @@ defmodule Amethyst.ConnectionHandler do
@spec loop(:gen_tcp.socket(), atom(), integer(), map()) :: no_return() @spec loop(:gen_tcp.socket(), atom(), integer(), map()) :: no_return()
defp loop(socket, connstate, version, state) do defp loop(socket, connstate, version, state) do
receive do receive do
:closed ->
Logger.info("Connection #{inspect(socket)} closed.")
Process.exit(self(), :normal)
{:disconnect, reason} -> {:disconnect, reason} ->
disconnect(socket, reason, connstate, version, state) disconnect(socket, reason, connstate, version)
Process.exit(self(), :normal) Process.exit(self(), :normal)
{:set_state, newstate} -> {:set_state, newstate} ->
Logger.debug("Switching to state #{newstate} from #{connstate}") Logger.debug("Switching to state #{newstate} from #{connstate}")
@ -58,16 +60,6 @@ defmodule Amethyst.ConnectionHandler do
{:set_version, newversion} -> {:set_version, newversion} ->
Logger.debug("Switching to version #{newversion} from #{version}") Logger.debug("Switching to version #{newversion} from #{version}")
loop(socket, connstate, newversion, state) loop(socket, connstate, newversion, state)
{:set_encryption, secret} ->
Logger.debug("Enabling encryption with shared secret #{inspect(secret)}")
encryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, true)
decryption_state = :crypto.crypto_init(:aes_128_cfb8, secret, secret, false)
state = state |> Map.put(:encryption_state, encryption_state) |> Map.put(:decryption_state, decryption_state)
loop(socket, connstate, version, state)
{:set_compression, threshold} ->
Logger.debug("Enabling comrpession with threshold #{threshold}")
state = Map.put(state, :compression, threshold)
loop(socket, connstate, version, state)
{:set_position, position} -> {:set_position, position} ->
prev_position = Map.get(state, :position) prev_position = Map.get(state, :position)
state = Map.put(state, :position, position) state = Map.put(state, :position, position)
@ -88,9 +80,9 @@ defmodule Amethyst.ConnectionHandler do
if prev_cp == nil do if prev_cp == nil do
MapSet.new([]) MapSet.new([])
else else
MapSet.new(visible_chunks_from(elem(prev_cp, 0), elem(prev_cp, 1), 16)) # This 16 would be the server view distance!! MapSet.new(visible_chunks_from(elem(prev_cp, 0), elem(prev_cp, 1), Map.get(state, :view_distance, 16)))
end end
chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), 16)) chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), Map.get(state, :view_distance, 16)))
new_chunks = MapSet.difference(chunks, prev_chunks) new_chunks = MapSet.difference(chunks, prev_chunks)
Logger.debug("Sending #{MapSet.size(new_chunks)} chunks...") Logger.debug("Sending #{MapSet.size(new_chunks)} chunks...")
# We can process all chunks in parallel # We can process all chunks in parallel
@ -106,20 +98,10 @@ defmodule Amethyst.ConnectionHandler do
loop(socket, connstate, version, state) loop(socket, connstate, version, state)
{:send_packet, packet} -> {:send_packet, packet} ->
# Logger.debug("Sending packet #{inspect(packet)}") # Logger.debug("Sending packet #{inspect(packet)}")
send_packet(socket, connstate, packet, version, state) send_packet(socket, connstate, packet, version)
loop(socket, connstate, version, state) loop(socket, connstate, version, state)
after 0 -> after 0 ->
# Received stuff from the connection receiver is lower priority
receive do receive do
:closed ->
Logger.info("Connection #{inspect(socket)} closed.")
Process.exit(self(), :normal)
{:get_encryption, from} ->
send(from, Map.get(state, :decryption_state))
loop(socket, connstate, version, state)
{:get_compression, from} ->
send(from, Map.get(state, :compression))
loop(socket, connstate, version, state)
{:packet, id, data} -> {:packet, id, data} ->
state = handle_packet(id, data, connstate, version, state) state = handle_packet(id, data, connstate, version, state)
loop(socket, connstate, version, state) loop(socket, connstate, version, state)
@ -153,94 +135,69 @@ defmodule Amethyst.ConnectionHandler do
heightmaps = compound(%{}) heightmaps = compound(%{})
data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air
|> Enum.reduce("", fn chunk_section, acc -> |> Enum.reduce("", fn chunk_section, acc ->
blocks_and_lights = chunk_section |> List.flatten() blocks = chunk_section |> List.flatten()
block_count = blocks_and_lights |> Enum.filter(fn {bs, _, _} -> bs != 0 end) |> length block_count = blocks |> Enum.filter(&(&1 != 0)) |> length
# Put together the palette # Put together the palette
unique_blocks = MapSet.new(blocks_and_lights |> Enum.map(fn {bs, _, _} -> bs end)) unique_blocks = MapSet.new(blocks)
min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil() min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil()
paletted_container_data = case min_bpe do
0 ->
# SINGLE VALUED
Write.ubyte(0) <>
Write.varint(MapSet.to_list(unique_blocks) |> List.first()) <>
Write.varint(0) # No data, empty pallette
min_bpe when min_bpe in 1..8 ->
# INDIRECT
# Minimum bpe accepted by minecraft is 4
bpe = max(min_bpe, 4)
palette = MapSet.to_list(unique_blocks) |>
Enum.with_index() |>
Map.new(fn {i, v} -> {i, v} end)
paletted_blocks = blocks |>
Enum.map(&(Map.get(palette, &1)))
paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe)
paletted_container_data = case min_bpe do Write.ubyte(bpe) <>
0 -> Write.varint(map_size(palette)) <>
# SINGLE VALUED Enum.reduce(palette, "", fn {_k, v}, acc ->
Write.ubyte(0) <> acc <> Write.varint(v)
Write.varint(MapSet.to_list(unique_blocks) |> List.first()) <> end) <>
Write.varint(0) # No data, empty pallette Write.varint(floor(bit_size(paletted_data) / 64)) <>
paletted_data
_ ->
# DIRECT
data = long_aligned_bit_string_reduce(blocks, 15)
Write.ubyte(15) <>
Write.varint(floor(bit_size(data) / 64)) <>
data
end
min_bpe when min_bpe in 1..8 -> acc <> Write.short(block_count) <> paletted_container_data <>
# INDIRECT <<0::8, 0::8, 0::8>> # TODO: This should be biome data
# Minimum bpe accepted by minecraft is 4 end)
bpe = max(min_bpe, 4)
palette = MapSet.to_list(unique_blocks) |>
Enum.with_index() |>
Map.new(fn {v, i} -> {v, i} end)
paletted_blocks = blocks_and_lights |>
Enum.map(fn {bs, _, _} -> Map.get(palette, bs) end)
paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe,
fn bs -> <<bs::signed-integer-big-size(bpe)>> end)
Write.ubyte(bpe) <>
Write.varint(map_size(palette)) <>
(Enum.sort(palette, fn {_k1, v1}, {_k2, v2} -> v1 < v2 end) |>
Enum.reduce("", fn {k, _v}, acc -> acc <> Write.varint(k) end)) <>
Write.varint(floor(bit_size(paletted_data) / 64)) <>
paletted_data
_ -> send(to, {:send_packet, %{
# DIRECT packet_type: :chunk_data_and_update_light,
data = long_aligned_bit_string_reduce(blocks_and_lights, 15, chunk_x: cx, chunk_z: cz,
fn {bs, _sl, _bl} -> <<bs::signed-integer-big-size(15)>> end) heightmaps: heightmaps,
Write.ubyte(15) <> data: data,
Write.varint(floor(bit_size(data) / 64)) <> block_entities: [],
data # TODO: Light
end sky_light_mask: Write.varint(0),
block_light_mask: Write.varint(0),
acc <> Write.short(block_count) <> paletted_container_data <> empty_sky_light_mask: Write.varint(0),
<<0::8, 0::8, 0::8>> # TODO: This should be biome data empty_block_light_mask: Write.varint(0),
end) sky_light_arrays: [],
block_light_arrays: []
sky_light_sections = Enum.chunk_every(chunk_array, 16, 16, 0) }})
|> Enum.map(fn chunk_section -> :ok
chunk_section |> List.flatten() |> Enum.map(fn {_bs, sl, _bl} -> sl end)
end)
non_empty_sky_light_sections = sky_light_sections |> Enum.filter(fn section -> !Enum.all?(section, &(&1 == 0)) end)
has_sky_light = [false] ++ (sky_light_sections |> Enum.map(fn section -> !Enum.all?(section, &(&1 == 0)) end)) ++ [false]
empty_sky_light = has_sky_light |> Enum.map(&not/1)
sky_light_array = non_empty_sky_light_sections |> Enum.map(&(Enum.reduce(&1, <<>>, fn light, acc ->
<<acc::bitstring, light::size(4)>>
end)))
block_light_sections = Enum.chunk_every(chunk_array, 16, 16, 0)
|> Enum.map(fn chunk_section ->
chunk_section |> List.flatten() |> Enum.map(fn {_bs, _sl, bl} -> bl end)
end)
non_empty_block_light_sections = block_light_sections |> Enum.filter(fn section -> !Enum.all?(section, &(&1 == 0)) end)
has_block_light = [false] ++ (block_light_sections |> Enum.map(fn section -> !Enum.all?(section, &(&1 == 0)) end)) ++ [false]
empty_block_light = has_block_light |> Enum.map(&not/1)
block_light_array = non_empty_block_light_sections |> Enum.map(&(Enum.reduce(&1, <<>>, fn light, acc ->
<<acc::bitstring, light::size(4)>>
end)))
send(to, {:send_packet, %{
packet_type: :chunk_data_and_update_light,
chunk_x: cx, chunk_z: cz,
heightmaps: heightmaps,
data: data,
block_entities: [],
sky_light_mask: has_sky_light,
block_light_mask: has_block_light,
empty_sky_light_mask: empty_sky_light,
empty_block_light_mask: empty_block_light,
sky_light_arrays: sky_light_array |> Enum.map(&(%{sky_light_array: &1})),
block_light_arrays: block_light_array |> Enum.map(&(%{block_light_array: &1}))
}})
:ok
end end
defp long_aligned_bit_string_reduce(values, bpe, func) do defp long_aligned_bit_string_reduce(values, bpe) do
values |> Enum.reduce(<<>>, fn value, acc -> values |> Enum.reduce(<<>>, fn value, acc ->
ret = func.(value) next = <<acc::bitstring, value::big-size(bpe)>>
next = <<acc::bitstring, ret::bitstring-size(bpe)>>
# man i hope they dont suddenly change the size of a long # man i hope they dont suddenly change the size of a long
if rem(bit_size(next), 64) + bpe > 64 do if rem(bit_size(next), 64) + bpe > 64 do
# gotta pad it # gotta pad it
@ -270,39 +227,21 @@ defmodule Amethyst.ConnectionHandler do
end end
rescue rescue
e -> e ->
if Application.get_env(:amethyst, :release, false) do if Mix.env() == :dev do
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}") Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
else else
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}") Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
end end
state state
end end
end end
defp send_packet(socket, connstate, packet, version, state) do defp send_packet(socket, connstate, packet, version) do
try do try do
data = connstate.serialize(packet, version) data = connstate.serialize(packet, version)
container = if Map.get(state, :compression) == nil do length = byte_size(data) |> Amethyst.Minecraft.Write.varint()
# Packet ID is included in data :gen_tcp.send(socket, length <> data)
Write.varint(byte_size(data)) <> data
else
threshold = Map.get(state, :compression, 0)
data_length = byte_size(data)
if data_length >= threshold do
compressed = Write.varint(data_length) <> :zlib.compress(data)
Write.varint(byte_size(compressed)) <> compressed
else
compressed = Write.varint(0) <> data
Write.varint(byte_size(compressed)) <> compressed
end
end
encrypted = if Map.get(state, :encryption_state) == nil do
container
else
Map.get(state, :encryption_state) |> :crypto.crypto_update(container)
end
:gen_tcp.send(socket, encrypted)
rescue rescue
e -> e ->
Logger.error("Error sending packet #{inspect(packet)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}") Logger.error("Error sending packet #{inspect(packet)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
@ -310,12 +249,12 @@ defmodule Amethyst.ConnectionHandler do
end end
end end
defp disconnect(socket, reason, connstate, version, state) do defp disconnect(socket, reason, connstate, version) do
Logger.info("Disconnecting connection #{inspect(socket)}") Logger.info("Disconnecting connection #{inspect(socket)}")
Logger.debug("Disconnecting connection #{inspect(socket)}: #{reason}") Logger.debug("Disconnecting connection #{inspect(socket)}: #{reason}")
case connstate.disconnect(reason) do case connstate.disconnect(reason) do
nil -> nil nil -> nil
packet -> send_packet(socket, connstate, packet, version, state) packet -> send_packet(socket, connstate, packet, version)
end end
:gen_tcp.close(socket) :gen_tcp.close(socket)
end end

View File

@ -35,7 +35,7 @@ defmodule Amethyst.ConnectionReceiver do
{:ok, spawn(fn -> {:ok, spawn(fn ->
Process.set_label("ConnectionReceiver for #{inspect(socket)}") Process.set_label("ConnectionReceiver for #{inspect(socket)}")
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0) {:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
receive(socket, pid, nil, nil) receive(socket, pid)
end)} end)}
end end
@ -44,89 +44,42 @@ defmodule Amethyst.ConnectionReceiver do
{:ok, spawn_link(fn -> {:ok, spawn_link(fn ->
Process.set_label("ConnectionReceiver for #{inspect(socket)}") Process.set_label("ConnectionReceiver for #{inspect(socket)}")
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0) {:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
receive(socket, pid, nil, nil) receive(socket, pid)
end)} end)}
end end
@spec receive(:gen_tcp.socket(), pid(), nil | :crypto.crypto_state(), nil | pos_integer()) :: no_return() @spec receive(:gen_tcp.socket(), pid()) :: no_return()
def receive(socket, sender, estate, cstate) do def receive(socket, sender) do
case get_packet(socket, estate, cstate) do case get_packet(socket) do
:closed -> send(sender, :closed) :closed -> send(sender, :closed)
Process.exit(self(), :normal) Process.exit(self(), :normal)
{:error, error} -> Logger.error("Error reading packet: #{error}") {:error, error} -> Logger.error("Error reading packet: #{error}")
{id, data} -> send(sender, {:packet, id, data}) {id, data} -> send(sender, {:packet, id, data})
end end
receive(socket, sender)
estate = if estate == nil do
# Ask the handler if we have encryption now
send(sender, {:get_encryption, self()})
receive do
nil -> nil
some -> some
end
else estate end
cstate = if cstate == nil do
# Ask the handler if we have encryption now
send(sender, {:get_compression, self()})
receive do
nil -> nil
some -> some
end
else cstate end
receive(socket, sender, estate, cstate)
end end
def get_packet(client, estate, cstate) do def get_packet(client) do
case get_varint(client, "", estate) do case get_varint(client, "") do
:closed -> :closed :closed -> :closed
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
{[length], ""} -> {[length], ""} ->
data = :gen_tcp.recv(client, length) recv = :gen_tcp.recv(client, length)
case data do case recv do
{:ok, data} -> {:ok, full_packet} -> ({[id], data} = Read.start(full_packet) |> Read.varint() |> Read.stop()
# Perform decryption and decompression {id, data})
decrypted = if estate == nil do
data
else
:crypto.crypto_update(estate, data)
end
full_packet = if cstate == nil do
# Using "without compression" container
decrypted
else
# Using "with compression" container
{[dlength], rest} = Read.start(decrypted) |> Read.varint |> Read.stop
if dlength == 0 do
# Uncompressed data
rest
else
# Compressed data
rest |> :zlib.uncompress()
end
end
{[id], data} = Read.start(full_packet) |> Read.varint |> Read.stop
{id, data}
{:error, :closed} -> :closed {:error, :closed} -> :closed
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end
end end
end end
defp get_varint(client, acc, estate) do defp get_varint(client, acc) do
case :gen_tcp.recv(client, 1) do case :gen_tcp.recv(client, 1) do
{:ok, byte} -> {:ok, byte} -> case byte do
byte = case estate do <<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop()
nil -> byte <<1::1, _::7>> -> get_varint(client, acc <> byte)
ds -> :crypto.crypto_update(ds, byte) end
end
case byte do
<<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop()
<<1::1, _::7>> -> get_varint(client, acc <> byte, estate)
end
{:error, :closed} -> :closed {:error, :closed} -> :closed
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end

View File

@ -16,7 +16,6 @@
defmodule Amethyst.Minecraft.Write do defmodule Amethyst.Minecraft.Write do
import Bitwise import Bitwise
require Logger
@moduledoc """ @moduledoc """
This module contains functions for writing Minecraft data. This module contains functions for writing Minecraft data.
@ -140,28 +139,6 @@ defmodule Amethyst.Minecraft.Write do
def nbt(value) do def nbt(value) do
Amethyst.NBT.Write.write_net(value) Amethyst.NBT.Write.write_net(value)
end end
def bitset(list) do
unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end))
aligned = if rem(bit_size(unaligned), 64) == 0 do
unaligned
else
<<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>>
end
varint(div(byte_size(aligned), 8)) <> aligned
end
def fixed_bitset(list, length) do
unaligned = Enum.reduce(list, <<>>, &(if &1 do <<1::1, &2::bitstring>> else <<0::1, &2::bitstring>> end))
if bit_size(unaligned) != length do
raise ArgumentError, "Fixed bitset is not the correct length"
end
aligned = if rem(bit_size(unaligned), 64) == 0 do
unaligned
else
<<0::size(64 - rem(bit_size(unaligned), 64)), unaligned::bitstring>>
end
aligned
end
end end
defmodule Amethyst.Minecraft.Read do defmodule Amethyst.Minecraft.Read do
@ -239,11 +216,6 @@ defmodule Amethyst.Minecraft.Read do
{[data | acc], rest, :reversed} {[data | acc], rest, :reversed}
end end
def byte_array({acc, data, :reversed}) do
{[length], rest} = start(data) |> varint |> stop
raw({acc, rest, :reversed}, length)
end
@doc """ @doc """
Reads a varint. `read` tracks the number of bytes read and `nacc` tracks the number being read. Reads a varint. `read` tracks the number of bytes read and `nacc` tracks the number being read.
""" """
@ -294,10 +266,4 @@ defmodule Amethyst.Minecraft.Read do
def raw({acc, data, :reversed}) do def raw({acc, data, :reversed}) do
{[data | acc], "", :reversed} {[data | acc], "", :reversed}
end end
def fixed_bitset({acc, data, :reversed}, length) do
bytes = ceil(length / 8)
<<value::binary-size(bytes), rest::binary>> = data
value = :binary.bin_to_list(value, 0, 1) |> Enum.reverse() |> Enum.take(length) |> Enum.map(& &1 != 0)
{[value | acc], rest, :reversed}
end
end end

View File

@ -30,6 +30,10 @@ defmodule Amethyst.Keys do
GenServer.start_link(__MODULE__, bits, name: __MODULE__) GenServer.start_link(__MODULE__, bits, name: __MODULE__)
end end
def get_priv do
GenServer.call(__MODULE__, :get_priv)
end
def get_pub do def get_pub do
GenServer.call(__MODULE__, :get_pub) GenServer.call(__MODULE__, :get_pub)
end end
@ -48,19 +52,22 @@ defmodule Amethyst.Keys do
rsa_public_key = {:RSAPublicKey, modulus, public_exponent} rsa_public_key = {:RSAPublicKey, modulus, public_exponent}
Logger.info("Generated RSA keys") Logger.info("Generated RSA keys")
{:ok, {rsa_public_key, rsa_private_key, bits}} {:ok, {rsa_public_key, rsa_private_key}}
end end
@impl true @impl true
def handle_call(:get_pub, _from, {pubkey, privkey, bits}) do def handle_call(:get_priv, _from, {pubkey, privkey}) do
{:SubjectPublicKeyInfo, pk, :not_encrypted} = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, pubkey) {:reply, :public_key.der_encode(:RSAPrivateKey, privkey), {pubkey, privkey}}
# Logger.debug("#{inspect(pem_encoded, limit: :infinity)}")
{:reply, pk, {pubkey, privkey, bits}}
end end
@impl true @impl true
def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey, bits}) do def handle_call(:get_pub, _from, {pubkey, privkey}) do
{:reply, :public_key.der_encode(:RSAPublicKey, pubkey), {pubkey, privkey}}
end
@impl true
def handle_call({:decrypt, encrypted}, _from, {pubkey, privkey}) do
plaintext = :public_key.decrypt_private(encrypted, privkey) plaintext = :public_key.decrypt_private(encrypted, privkey)
{:reply, plaintext, {pubkey, privkey, bits}} {:reply, plaintext, {pubkey, privkey}}
end end
end end

View File

@ -88,22 +88,11 @@ defmodule Amethyst.Game do
mod.accept_teleport(self(), id, refs) mod.accept_teleport(self(), id, refs)
end end
@doc """ @doc """
`chat/3` is called when a player sends a chat message. Note that it is not how slash commands should be handled.
- 'from' is the PID of the player's connection process (see `login/3`).
- 'message' is the chat message sent by the player.
- `state_refs` are your references (see `instantiate/1`)
"""
@callback chat(from :: pid(), message :: binary(), state_refs :: map()) :: :ok
def chat(%{:mod => mod, :refs => refs}, message) do
mod.chat(self(), message, refs)
end
@doc """
The terrain of a specific chunk column. This is automatically used to load chunks for a player. The terrain of a specific chunk column. This is automatically used to load chunks for a player.
For now, this data must be formatted as a 3D list, indexed as [y][z][x]. For now, this data must be formatted as a 3D list, indexed as [y][z][x].
""" """
@callback chunk(from :: pid(), {x :: integer(), z :: integer()}, state_refs :: map()) :: [[[{block_state :: pos_integer(), sky_light :: 0..15, block_light :: 0..15}]]] @callback chunk(from :: pid(), {x :: integer(), z :: integer()}, state_refs :: map()) :: [[[pos_integer()]]]
def chunk(%{:mod => mod, :refs => refs}, pos) do def chunk(%{:mod => mod, :refs => refs}, pos) do
mod.chunk(self(), pos, refs) mod.chunk(self(), pos, refs)
end end

View File

@ -1,26 +0,0 @@
# 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.Sha1 do
@moduledoc """
This module implements the awkward non-standard hex digest hash that Minecraft uses
"""
def hash(input) do
# this sucks but so does the fact that mojang makes me do this so it fits
<<number::integer-signed-size(20*8)>> = :crypto.hash(:sha, input)
inspect(number, base: :hex) |> String.replace("0x", "") |> String.downcase()
end
end

View File

@ -203,10 +203,10 @@ defmodule Amethyst.ConnectionState.Configuration do
# https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains # https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains
basic_biome = compound(%{ basic_biome = compound(%{
"effects" => compound(%{ "effects" => compound(%{
"sky_color" => int(7_907_327), "sky_color" => int(7907327),
"water_fog_color" => int(329_011), "water_fog_color" => int(329011),
"fog_color" => int(12_638_463), "fog_color" => int(12638463),
"water_color" => int(4_159_204), "water_color" => int(4159204),
"mood_sound" => compound(%{ "mood_sound" => compound(%{
"tick_delay" => int(6000), "tick_delay" => int(6000),
"offset" => float(2.0), "offset" => float(2.0),
@ -310,16 +310,6 @@ defmodule Amethyst.ConnectionState.Configuration do
chunk_z: div(floor(z), 16) chunk_z: div(floor(z), 16)
}}) }})
send(self(), {:set_position, {x, y, z}}) send(self(), {:set_position, {x, y, z}})
send(self(), {:send_packet, %{packet_type: :player_info_update_add_player,
players: [
%{
uuid: Map.get(state, :uuid),
name: Map.get(state, :name),
properties: Map.get(state, :properties) |>
Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end)
}
]
}})
# Begin keepalive loop # Begin keepalive loop
# TODO: Put it under some supervisor # TODO: Put it under some supervisor
me = self() me = self()

View File

@ -60,20 +60,10 @@ defmodule Amethyst.ConnectionState.Login do
Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, [] Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, []
Macros.defpacket_serverbound :cookie_response, 0x04, 767, [identifier: :string, payload: {:optional, :byte_array}] Macros.defpacket_serverbound :cookie_response, 0x04, 767, [identifier: :string, payload: {:optional, :byte_array}]
def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, state) do def handle(%{packet_type: :login_start, name: name, player_uuid: player_uuid}, 767, _state) do
Logger.debug("Received login start for #{name} with UUID #{player_uuid}") Logger.debug("Received login start for #{name} with UUID #{player_uuid}")
if Application.get_env(:amethyst, :encryption, true) do if Application.fetch_env!(:amethyst, :encryption) do
verify_token = :crypto.strong_rand_bytes(4) raise RuntimeError, "Encryption is not currently supported"
public_key = Amethyst.Keys.get_pub()
Logger.debug("Public key: #{inspect(public_key, limit: :infinity)}")
send(self(), {:send_packet, %{
packet_type: :encryption_request,
server_id: "",
public_key: public_key,
verify_token: verify_token,
should_authenticate: Application.get_env(:amethyst, :auth, false)
}})
state |> Map.put(:verify_token, verify_token) |> Map.put(:name, name) |> Map.put(:uuid, player_uuid)
else else
send(self(), {:send_packet, %{ send(self(), {:send_packet, %{
packet_type: :login_success, packet_type: :login_success,
@ -85,57 +75,7 @@ defmodule Amethyst.ConnectionState.Login do
:ok :ok
end end
end end
def handle(%{packet_type: :encryption_response, shared_secret: secret, verify_token: verify_token}, 767, state) do
secret = Amethyst.Keys.decrypt(secret)
verify_token = Amethyst.Keys.decrypt(verify_token)
if verify_token == Map.get(state, :verify_token, :never) do
send(self(), {:set_encryption, secret})
if Application.get_env(:amethyst, :compression, nil) != nil do
threshold = Application.get_env(:amethyst, :compression, 0)
send(self(), {:send_packet, %{packet_type: :set_compression, threshold: threshold}})
send(self(), {:set_compression, threshold})
end
if Application.get_env(:amethyst, :auth, false) == false do
# Don't check authentication
send(self(), {:send_packet, %{
packet_type: :login_success,
uuid: Map.get(state, :uuid),
username: Map.get(state, :name),
properties: [],
strict_error_handling: true
}})
Map.put(state, :authenticated, false)
else
# Check authentication
pubkey = Amethyst.Keys.get_pub()
hash = Amethyst.Minecraft.Sha1.hash(secret <> pubkey)
url = Application.get_env(:amethyst, :session_server, "https://sessionserver.mojang.com") <> "/session/minecraft/hasJoined?username=" <>
Map.get(state, :name) <> "&serverId=" <> hash # I don't think we need to verify the IP in the use case of Amethyst...
response = Req.get!(url,
headers: [
{"user-agent", "Amethyst/1.0"}
]).body
<<c1::binary-size(8), c2::binary-size(4), c3::binary-size(4), c4::binary-size(4), c5::binary>> = response["id"]
uuid = [c1, c2, c3, c4, c5] |> Enum.join("-")
send(self(), {:send_packet, %{
packet_type: :login_success,
uuid: uuid,
username: response["name"],
properties: response["properties"] |>
Enum.map(fn prop -> %{name: prop["name"], value: prop["value"], signature: Map.get(prop, "signature")} end),
strict_error_handling: true
}})
Map.put(state, :authenticated, true) |> Map.put(:uuid, uuid) |> Map.put(:name, response["name"]) |> Map.put(:properties, response["properties"])
end
else
raise RuntimeError, "Invalid verify token. Broken encryption?"
end
end
def handle(%{packet_type: :login_acknowledged}, 767, _state) do def handle(%{packet_type: :login_acknowledged}, 767, _state) do
Logger.debug("Received login acknowledged") Logger.debug("Received login acknowledged")
send(self(), {:set_state, Amethyst.ConnectionState.Configuration}) send(self(), {:set_state, Amethyst.ConnectionState.Configuration})

View File

@ -14,12 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# TODO!!!: REDO THIS WHOLE THING AGAIN IT'S A MESS
defmodule Amethyst.ConnectionState.Macros do defmodule Amethyst.ConnectionState.Macros do
@moduledoc """
Useful macros for defining packets.
"""
require Logger require Logger
defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do
quote do quote do
@ -33,8 +28,7 @@ defmodule Amethyst.ConnectionState.Macros do
defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do
quote do quote do
def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) when unquote(where) do def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) when unquote(where) do
# Don't check types if we are in release mode if Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
if Application.get_env(:amethyst, :release, false) || Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature)) Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature))
else else
raise "Invalid packet type for #{unquote(name)}! Got #{inspect(packet)}" raise "Invalid packet type for #{unquote(name)}! Got #{inspect(packet)}"
@ -58,13 +52,6 @@ defmodule Amethyst.ConnectionState.Macros do
else else
{[nil | acc], rest, :reversed} {[nil | acc], rest, :reversed}
end end
{:optional, {:raw, length}} ->
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
if exists do
Read.raw({acc, rest, :reversed}, length)
else
{[nil | acc], rest, :reversed}
end
{:optional, t} -> {:optional, t} ->
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop() {[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
if exists do if exists do
@ -72,10 +59,6 @@ defmodule Amethyst.ConnectionState.Macros do
else else
{[nil | acc], rest, :reversed} {[nil | acc], rest, :reversed}
end end
{:fixed_bitset, length} ->
Read.fixed_bitset({acc, rest, :reversed}, length)
{:raw, length} ->
Read.raw({acc, rest, :reversed}, length)
{:array, signature} -> {:array, signature} ->
{[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop() {[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop()
if count == 0 do if count == 0 do
@ -112,7 +95,7 @@ defmodule Amethyst.ConnectionState.Macros do
Enum.reduce(Map.get(packet, name), "", fn item, acc -> Enum.reduce(Map.get(packet, name), "", fn item, acc ->
acc <> write_signature(item, signature) acc <> write_signature(item, signature)
end) end)
{:literal, type, value} -> acc <> apply(Write, type, [value]) {:literal, {type, value}} -> acc <> apply(Write, type, [value])
t -> acc <> apply(Write, t, [Map.get(packet, name)]) t -> acc <> apply(Write, t, [Map.get(packet, name)])
end end
end) end)
@ -123,7 +106,7 @@ defmodule Amethyst.ConnectionState.Macros do
Enum.all?(signature, fn {name, type} -> Enum.all?(signature, fn {name, type} ->
case Map.get(packet, name, :missing) do case Map.get(packet, name, :missing) do
:missing -> :missing ->
if is_tuple(type) && elem(type, 0) == :literal do if elem(type, 0) == :literal do
true true
else else
throw {:missing, name} throw {:missing, name}
@ -144,22 +127,22 @@ defmodule Amethyst.ConnectionState.Macros do
def type_matches(value, :bool) when is_boolean(value), do: true def type_matches(value, :bool) when is_boolean(value), do: true
def type_matches(value, :byte) when is_integer(value) and value in -128..127, do: true def type_matches(value, :byte) when is_integer(value) and value in -128..127, do: true
def type_matches(value, :ubyte) when is_integer(value) and value in 0..255, do: true def type_matches(value, :ubyte) when is_integer(value) and value in 0..255, do: true
def type_matches(value, :short) when is_integer(value) and value in -32_768..32_767, do: true def type_matches(value, :short) when is_integer(value) and value in -32768..32767, do: true
def type_matches(value, :ushort) when is_integer(value) and value in 0..65_535, do: true def type_matches(value, :ushort) when is_integer(value) and value in 0..65535, do: true
def type_matches(value, :int) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true def type_matches(value, :int) when is_integer(value) and value in -2147483648..2147483647, do: true
def type_matches(value, :long) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true def type_matches(value, :long) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true
def type_matches(value, :float) when is_number(value), do: true def type_matches(value, :float) when is_number(value), do: true
def type_matches(value, :double) when is_number(value), do: true def type_matches(value, :double) when is_number(value), do: true
def type_matches(value, :varint) when is_integer(value) and value in -2_147_483_648..2_147_483_647, do: true def type_matches(value, :varint) when is_integer(value) and value in -2147483648..2147483647, do: true
def type_matches(value, :varlong) when is_integer(value) and value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807, do: true def type_matches(value, :varlong) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true
def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true
def type_matches(value, :string) when is_binary(value), do: true def type_matches(value, :string) when is_binary(value), do: true
def type_matches(value, :raw) when is_binary(value), do: true def type_matches(value, :raw) when is_binary(value), do: true
def type_matches(value, :byte_array) when is_binary(value), do: true def type_matches(value, :byte_array) when is_binary(value), do: true
def type_matches({x, y, z}, :position) when def type_matches({x, y, z}, :position) when
is_integer(x) and x in -33_554_432..33_554_431 and is_integer(x) and x in -33554432..33554431 and
is_integer(y) and y in -2048..2047 and is_integer(y) and y in -2048..2047 and
is_integer(z) and z in -33_554_432..33_554_431, do: true is_integer(z) and z in -33554432..33554431, do: true
def type_matches(value, :nbt), do: Amethyst.NBT.Write.check_type(value) def type_matches(value, :nbt), do: Amethyst.NBT.Write.check_type(value)
def type_matches(value, :json) do def type_matches(value, :json) do
case Jason.encode(value) do case Jason.encode(value) do
@ -170,7 +153,6 @@ defmodule Amethyst.ConnectionState.Macros do
def type_matches(value, {:optional, _type}) when is_nil(value), do: true def type_matches(value, {:optional, _type}) when is_nil(value), do: true
def type_matches(value, {:optional, type}), do: type_matches(value, type) def type_matches(value, {:optional, type}), do: type_matches(value, type)
def type_matches(value, {:array, signature}) when is_list(value), do: Enum.all?(value, fn item -> check_type(item, signature) end) def type_matches(value, {:array, signature}) when is_list(value), do: Enum.all?(value, fn item -> check_type(item, signature) end)
def type_matches(value, :bitset) when is_list(value), do: Enum.all?(value, fn item -> is_boolean(item) end)
def type_matches(value, {:compound, signature}) when is_map(value), do: check_type(value, signature) def type_matches(value, {:compound, signature}) when is_map(value), do: check_type(value, signature)
def type_matches(_, _) do def type_matches(_, _) do
false false

View File

@ -36,10 +36,10 @@ defmodule Amethyst.ConnectionState.Play do
type: :varint, type: :varint,
data: :nbt data: :nbt
]}, ]},
sky_light_mask: :bitset, sky_light_mask: :raw,
block_light_mask: :bitset, block_light_mask: :raw,
empty_sky_light_mask: :bitset, empty_sky_light_mask: :raw,
empty_block_light_mask: :bitset, empty_block_light_mask: :raw,
sky_light_arrays: {:array, [ sky_light_arrays: {:array, [
sky_light_array: :byte_array sky_light_array: :byte_array
]}, ]},
@ -71,8 +71,8 @@ defmodule Amethyst.ConnectionState.Play do
portal_cooldown: :varint, portal_cooldown: :varint,
enforces_secure_chat: :bool, enforces_secure_chat: :bool,
] ]
Macros.defpacket_clientbound :player_info_update_add_player, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_add_player, 0x2E, 767, [
actions: {:literal, :byte, 0x01}, actions: {:literal, 0x01},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
name: :string, name: :string,
@ -83,8 +83,8 @@ defmodule Amethyst.ConnectionState.Play do
]} ]}
]} ]}
] ]
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x2E, 767, [
actions: {:literal, :byte, 0x02}, actions: {:literal, 0x02},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
data: {:optional, {:compound, [ data: {:optional, {:compound, [
@ -95,29 +95,29 @@ defmodule Amethyst.ConnectionState.Play do
]}} ]}}
]} ]}
] ]
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x2E, 767, [
actions: {:literal, :byte, 0x04}, actions: {:literal, 0x04},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
gamemode: :varint gamemode: :varint
]} ]}
] ]
Macros.defpacket_clientbound :player_info_update_update_listed, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_update_listed, 0x2E, 767, [
actions: {:literal, :byte, 0x08}, actions: {:literal, 0x08},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
listed: :bool listed: :bool
]} ]}
] ]
Macros.defpacket_clientbound :player_info_update_update_latency, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_update_latency, 0x2E, 767, [
actions: {:literal, :byte, 0x10}, actions: {:literal, 0x10},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
ping: :varint, # Milliseconds ping: :varint, # Milliseconds
]} ]}
] ]
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x3E, 767, [ Macros.defpacket_clientbound :player_info_update_update_display_name, 0x2E, 767, [
actions: {:literal, :byte, 0x20}, actions: {:literal, 0x20},
players: {:array, [ players: {:array, [
uuid: :uuid, uuid: :uuid,
display_name: {:optional, :nbt} display_name: {:optional, :nbt}
@ -135,10 +135,6 @@ defmodule Amethyst.ConnectionState.Play do
Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [ Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [
chunk_x: :varint, chunk_z: :varint chunk_x: :varint, chunk_z: :varint
] ]
Macros.defpacket_clientbound :system_chat_message, 0x6C, 767, [
content: :nbt,
overlay: :bool
]
Macros.defpacket_clientbound :game_event, 0x22, 767, [ Macros.defpacket_clientbound :game_event, 0x22, 767, [
event: :ubyte, value: :float event: :ubyte, value: :float
@ -161,14 +157,6 @@ defmodule Amethyst.ConnectionState.Play do
def ge_start_waiting_for_level_chunks(767), do: %{packet_type: :game_event, event: 13, value: 0} def ge_start_waiting_for_level_chunks(767), do: %{packet_type: :game_event, event: 13, value: 0}
Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint] Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint]
Macros.defpacket_serverbound :chat_message, 0x06, 767, [
message: :string,
timestamp: :long,
salt: :long,
signature: {:optional, {:raw, 256}},
message_count: :varint,
acknowledged: {:fixed_bitset, 20}
]
Macros.defpacket_serverbound :serverbound_plugin_message, 0x12, 767, [channel: :string, data: :raw] Macros.defpacket_serverbound :serverbound_plugin_message, 0x12, 767, [channel: :string, data: :raw]
Macros.defpacket_serverbound :keep_alive, 0x18, 767, [id: :long] Macros.defpacket_serverbound :keep_alive, 0x18, 767, [id: :long]
Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [ Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [
@ -245,12 +233,6 @@ defmodule Amethyst.ConnectionState.Play do
end end
end end
def handle(%{packet_type: :chat_message, message: msg, timestamp: _, salt: _, signature: _, message_count: _, acknowledged: _}, 767, state) do
# We will never support message signing
state |> Map.get(:game) |> Amethyst.Game.chat(msg)
:ok
end
def handle(%{packet_type: :keep_alive, id: id}, 767, state) do def handle(%{packet_type: :keep_alive, id: id}, 767, state) do
ka = state |> Map.get(:keepalive) ka = state |> Map.get(:keepalive)
send(ka, {:respond, id}) send(ka, {:respond, id})

View File

@ -39,8 +39,7 @@ defmodule Amethyst.MixProject do
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:uuid, "~> 1.1"}, {:uuid, "~> 1.1"},
{:ex_doc, "~> 0.22", only: :dev, runtime: false}, {:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:jason, "~> 1.4"}, {:jason, "~> 1.4"}
{:req, "~> 0.5.6", runtime: true}
] ]
end end
end end

View File

@ -1,10 +0,0 @@
defmodule Sha1Test do
alias Amethyst.Minecraft.Sha1
use ExUnit.Case, async: true
test "Mojang sha1 hash digest" do
assert Sha1.hash("Notch") == "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"
assert Sha1.hash("jeb_") == "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"
assert Sha1.hash("simon") == "88e16a1019277b15d58faf0541e11910eb756f6"
end
end

View File

@ -2,12 +2,6 @@ defmodule Example.Game do
require Logger require Logger
@behaviour Amethyst.Game @behaviour Amethyst.Game
@moduledoc """
Example game used for testing Amethyst.
"""
require Amethyst.NBT.Write
alias Amethyst.NBT
@impl true @impl true
def instantiate(supervisor) do def instantiate(supervisor) do
Logger.info("The supervisor for this game is at #{inspect(supervisor)}") Logger.info("The supervisor for this game is at #{inspect(supervisor)}")
@ -41,36 +35,21 @@ alias Amethyst.NBT
:ok :ok
end end
@impl true
def chat(from, message, _state_refs) do
Logger.info("Player at #{inspect(from)} said: #{inspect(message)}")
send(from, {:send_packet, %{
packet_type: :system_chat_message,
content: NBT.Write.compound(%{"text" => NBT.Write.string("You said: #{message}")}),
overlay: false
}})
:ok
end
@impl true @impl true
def joinable?(_refs) do def joinable?(_refs) do
true true
end end
@impl true @impl true
def chunk(_from, {cx, cz}, _state_refs) do def chunk(_from, {_cx, _cz}, _state_refs) do
# Logger.info("Player at #{inspect(from)} wants to know chunk #{cx}, #{cz}") # Logger.info("Player at #{inspect(from)} wants to know chunk #{cx}, #{cz}")
(0..255) |> Enum.map(fn y -> (0..255) |> Enum.map(fn y ->
(0..15) |> Enum.map(fn z -> (0..15) |> Enum.map(fn z ->
(0..15) |> Enum.map(fn x -> (0..15) |> Enum.map(fn x ->
gx = cx * 16 + x if y <= x + z do
gz = cz * 16 + z 3
gy = y
if rem(gx, 4) == 0 && rem(gy, 4) == 0 && rem(gz, 4) == 0 do
{abs(rem(div(gx + gy + gz, 4), 300)), 7, 7}
else else
{0, 7, 7} 0
end end
end) end)
end) end)

View File

@ -2,9 +2,6 @@ import Config
config :amethyst, config :amethyst,
port: 25599, # Bogus port for testing, avoids unexpected conflicts port: 25599, # Bogus port for testing, avoids unexpected conflicts
encryption: true, # Whether or not to request encryption from clients. encryption: false, # Whether or not to request encryption from clients.
auth: true, # Whether or not users should be authenticated with Mojang. auth: false, # Whether or not users should be authenticated with Mojang.
session_server: "https://sessionserver.mojang.com", # Base URL to the Mojang session server default_game: Example.Game # Which game new players should be sent to
compression: 256, # Packets larger than this amount are sent compressed. Set to nil to disable compression.
default_game: Example.Game, # Which game new players should be sent to
release: config_env() == :prod # If this is set to false, Amethyst will perform additional checks at runtime and will handle errors more loosely

View File

@ -43,20 +43,14 @@
}) })
); );
package = env: release: pkgs: (pkgs.beamPackages.mixRelease { #secrets = import ./secrets.nix;
package = env: pkgs: (pkgs.beamPackages.mixRelease {
pname = "amethyst"; pname = "amethyst";
version = "0.1.0"; version = "0.1.0";
src = ./.; src = ./.;
elixir = pkgs.elixir; elixir = pkgs.elixir;
mixEnv = env; mixEnv = env;
mixReleaseName = release;
# This should just be part of mixRelease, I don't know why we need to do it here
installPhase = ''
runHook preInstall
mix release ${release} --no-deps-check --path "$out"
runHook postInstall
'';
meta.mainProgram = "${release}";
erlangDeterministicBuilds = false; # Technically less pure but allows doctests erlangDeterministicBuilds = false; # Technically less pure but allows doctests
removeCookie = false; # Insecure; Access to the file system can allow the cookie to be read and provides remote control of the Erlang VM removeCookie = false; # Insecure; Access to the file system can allow the cookie to be read and provides remote control of the Erlang VM
mixNixDeps = import ./mix.nix {inherit lib; beamPackages = pkgs.beamPackages;}; mixNixDeps = import ./mix.nix {inherit lib; beamPackages = pkgs.beamPackages;};
@ -70,9 +64,8 @@
in { in {
packages = eachSystem (pkgs: packages = eachSystem (pkgs:
{ {
default = package "prod" "amethyst" pkgs; default = package "prod" pkgs;
dev = package "dev" "amethyst" pkgs; dev = package "dev" pkgs;
example = package "prop" "example" pkgs;
} }
); );

View File

@ -8,7 +8,7 @@ defmodule AmethystUmbrella.MixProject do
elixir: "~> 1.17", elixir: "~> 1.17",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps(), deps: deps(),
# default_release: :amethyst, default_release: :amethyst,
releases: [ releases: [
amethyst: [ amethyst: [

View File

@ -5,19 +5,11 @@
"elixir_math": {:hex, :elixir_math, "0.1.2", "5655bdf7f34e30906f31cdcf3031b43dd522ce8d2936b60ad4696b2c752bf5c9", [:mix], [], "hexpm", "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c"}, "elixir_math": {:hex, :elixir_math, "0.1.2", "5655bdf7f34e30906f31cdcf3031b43dd522ce8d2936b60ad4696b2c752bf5c9", [:mix], [], "hexpm", "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c"},
"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"}, "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.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"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": {: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_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"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"pre_commit": {:hex, :pre_commit, "0.3.4", "e2850f80be8090d50ad8019ef2426039307ff5dfbe70c736ad0d4d401facf304", [:mix], [], "hexpm", "16f684ba4f1fed1cba6b19e082b0f8d696e6f1c679285fedf442296617ba5f4e"}, "pre_commit": {:hex, :pre_commit, "0.3.4", "e2850f80be8090d50ad8019ef2426039307ff5dfbe70c736ad0d4d401facf304", [:mix], [], "hexpm", "16f684ba4f1fed1cba6b19e082b0f8d696e6f1c679285fedf442296617ba5f4e"},
"req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
} }

130
mix.nix
View File

@ -47,19 +47,6 @@ let
beamDeps = []; beamDeps = [];
}; };
elixir_math = buildMix rec {
name = "elixir_math";
version = "0.1.2";
src = fetchHex {
pkg = "elixir_math";
version = "${version}";
sha256 = "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c";
};
beamDeps = [];
};
ex_doc = buildMix rec { ex_doc = buildMix rec {
name = "ex_doc"; name = "ex_doc";
version = "0.34.2"; version = "0.34.2";
@ -86,32 +73,6 @@ let
beamDeps = []; beamDeps = [];
}; };
finch = buildMix rec {
name = "finch";
version = "0.19.0";
src = fetchHex {
pkg = "finch";
version = "${version}";
sha256 = "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6";
};
beamDeps = [ mime mint nimble_options nimble_pool telemetry ];
};
hpax = buildMix rec {
name = "hpax";
version = "1.0.0";
src = fetchHex {
pkg = "hpax";
version = "${version}";
sha256 = "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601";
};
beamDeps = [];
};
jason = buildMix rec { jason = buildMix rec {
name = "jason"; name = "jason";
version = "1.4.4"; version = "1.4.4";
@ -164,45 +125,6 @@ let
beamDeps = [ makeup ]; beamDeps = [ makeup ];
}; };
mime = buildMix rec {
name = "mime";
version = "2.0.6";
src = fetchHex {
pkg = "mime";
version = "${version}";
sha256 = "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e";
};
beamDeps = [];
};
mint = buildMix rec {
name = "mint";
version = "1.6.2";
src = fetchHex {
pkg = "mint";
version = "${version}";
sha256 = "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9";
};
beamDeps = [ hpax ];
};
nimble_options = buildMix rec {
name = "nimble_options";
version = "1.1.1";
src = fetchHex {
pkg = "nimble_options";
version = "${version}";
sha256 = "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44";
};
beamDeps = [];
};
nimble_parsec = buildMix rec { nimble_parsec = buildMix rec {
name = "nimble_parsec"; name = "nimble_parsec";
version = "1.4.0"; version = "1.4.0";
@ -216,58 +138,6 @@ let
beamDeps = []; beamDeps = [];
}; };
nimble_pool = buildMix rec {
name = "nimble_pool";
version = "1.1.0";
src = fetchHex {
pkg = "nimble_pool";
version = "${version}";
sha256 = "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a";
};
beamDeps = [];
};
pre_commit = buildMix rec {
name = "pre_commit";
version = "0.3.4";
src = fetchHex {
pkg = "pre_commit";
version = "${version}";
sha256 = "16f684ba4f1fed1cba6b19e082b0f8d696e6f1c679285fedf442296617ba5f4e";
};
beamDeps = [];
};
req = buildMix rec {
name = "req";
version = "0.5.6";
src = fetchHex {
pkg = "req";
version = "${version}";
sha256 = "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185";
};
beamDeps = [ finch jason mime ];
};
telemetry = buildRebar3 rec {
name = "telemetry";
version = "1.3.0";
src = fetchHex {
pkg = "telemetry";
version = "${version}";
sha256 = "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6";
};
beamDeps = [];
};
uuid = buildMix rec { uuid = buildMix rec {
name = "uuid"; name = "uuid";
version = "1.1.8"; version = "1.1.8";