Compare commits
No commits in common. "main" and "first-attempt" have entirely different histories.
main
...
first-atte
@ -86,8 +86,6 @@
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||
{Credo.Check.Readability.VariableNames, []},
|
||||
{Credo.Check.Readability.WithSingleClause, []},
|
||||
{Credo.Check.Readability.BlockPipe, []},
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
|
||||
#
|
||||
## Refactoring Opportunities
|
||||
@ -108,8 +106,6 @@
|
||||
{Credo.Check.Refactor.RejectReject, []},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.MapMap, []},
|
||||
{Credo.Check.Refactor.FilterReject, []},
|
||||
|
||||
#
|
||||
## Warnings
|
||||
@ -150,6 +146,7 @@
|
||||
{Credo.Check.Design.DuplicatedCode, []},
|
||||
{Credo.Check.Design.SkipTestWithoutComment, []},
|
||||
{Credo.Check.Readability.AliasAs, []},
|
||||
{Credo.Check.Readability.BlockPipe, []},
|
||||
{Credo.Check.Readability.ImplTrue, []},
|
||||
{Credo.Check.Readability.MultiAlias, []},
|
||||
{Credo.Check.Readability.NestedFunctionCalls, []},
|
||||
@ -157,13 +154,16 @@
|
||||
{Credo.Check.Readability.OnePipePerLine, []},
|
||||
{Credo.Check.Readability.SeparateAliasRequire, []},
|
||||
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
{Credo.Check.Readability.Specs, []},
|
||||
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||
{Credo.Check.Readability.WithCustomTaggedTuple, []},
|
||||
{Credo.Check.Refactor.ABCSize, []},
|
||||
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||
{Credo.Check.Refactor.DoubleBooleanNegation, []},
|
||||
{Credo.Check.Refactor.FilterReject, []},
|
||||
{Credo.Check.Refactor.IoPuts, []},
|
||||
{Credo.Check.Refactor.MapMap, []},
|
||||
{Credo.Check.Refactor.ModuleDependencies, []},
|
||||
{Credo.Check.Refactor.NegatedIsNil, []},
|
||||
{Credo.Check.Refactor.PassAsyncInTestCases, []},
|
||||
@ -174,8 +174,9 @@
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||
{Credo.Check.Warning.MixEnv, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []},
|
||||
{Credo.Check.Refactor.MapInto, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||
|
||||
# {Credo.Check.Refactor.MapInto, []},
|
||||
|
||||
#
|
||||
# Custom checks can be created using `mix credo.gen.check`.
|
||||
|
@ -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
|
||||
receive the packets.
|
||||
"""
|
||||
alias Amethyst.Minecraft.Write
|
||||
require Logger
|
||||
|
||||
@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()
|
||||
defp loop(socket, connstate, version, state) do
|
||||
receive do
|
||||
:closed ->
|
||||
Logger.info("Connection #{inspect(socket)} closed.")
|
||||
Process.exit(self(), :normal)
|
||||
{:disconnect, reason} ->
|
||||
disconnect(socket, reason, connstate, version, state)
|
||||
disconnect(socket, reason, connstate, version)
|
||||
Process.exit(self(), :normal)
|
||||
{:set_state, newstate} ->
|
||||
Logger.debug("Switching to state #{newstate} from #{connstate}")
|
||||
@ -58,16 +60,6 @@ defmodule Amethyst.ConnectionHandler do
|
||||
{:set_version, newversion} ->
|
||||
Logger.debug("Switching to version #{newversion} from #{version}")
|
||||
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} ->
|
||||
prev_position = Map.get(state, :position)
|
||||
state = Map.put(state, :position, position)
|
||||
@ -88,9 +80,9 @@ defmodule Amethyst.ConnectionHandler do
|
||||
if prev_cp == nil do
|
||||
MapSet.new([])
|
||||
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
|
||||
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)
|
||||
Logger.debug("Sending #{MapSet.size(new_chunks)} chunks...")
|
||||
# We can process all chunks in parallel
|
||||
@ -106,20 +98,10 @@ defmodule Amethyst.ConnectionHandler do
|
||||
loop(socket, connstate, version, state)
|
||||
{:send_packet, 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)
|
||||
after 0 ->
|
||||
# Received stuff from the connection receiver is lower priority
|
||||
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} ->
|
||||
state = handle_packet(id, data, connstate, version, state)
|
||||
loop(socket, connstate, version, state)
|
||||
@ -153,94 +135,69 @@ defmodule Amethyst.ConnectionHandler do
|
||||
heightmaps = compound(%{})
|
||||
|
||||
data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air
|
||||
|> Enum.reduce("", fn chunk_section, acc ->
|
||||
blocks_and_lights = chunk_section |> List.flatten()
|
||||
block_count = blocks_and_lights |> Enum.filter(fn {bs, _, _} -> bs != 0 end) |> length
|
||||
|> Enum.reduce("", fn chunk_section, acc ->
|
||||
blocks = chunk_section |> List.flatten()
|
||||
block_count = blocks |> Enum.filter(&(&1 != 0)) |> length
|
||||
|
||||
# Put together the palette
|
||||
unique_blocks = MapSet.new(blocks_and_lights |> Enum.map(fn {bs, _, _} -> bs end))
|
||||
min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil()
|
||||
# Put together the palette
|
||||
unique_blocks = MapSet.new(blocks)
|
||||
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
|
||||
0 ->
|
||||
# SINGLE VALUED
|
||||
Write.ubyte(0) <>
|
||||
Write.varint(MapSet.to_list(unique_blocks) |> List.first()) <>
|
||||
Write.varint(0) # No data, empty pallette
|
||||
Write.ubyte(bpe) <>
|
||||
Write.varint(map_size(palette)) <>
|
||||
Enum.reduce(palette, "", fn {_k, v}, acc ->
|
||||
acc <> Write.varint(v)
|
||||
end) <>
|
||||
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 ->
|
||||
# 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 {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
|
||||
acc <> Write.short(block_count) <> paletted_container_data <>
|
||||
<<0::8, 0::8, 0::8>> # TODO: This should be biome data
|
||||
end)
|
||||
|
||||
_ ->
|
||||
# DIRECT
|
||||
data = long_aligned_bit_string_reduce(blocks_and_lights, 15,
|
||||
fn {bs, _sl, _bl} -> <<bs::signed-integer-big-size(15)>> end)
|
||||
Write.ubyte(15) <>
|
||||
Write.varint(floor(bit_size(data) / 64)) <>
|
||||
data
|
||||
end
|
||||
|
||||
acc <> Write.short(block_count) <> paletted_container_data <>
|
||||
<<0::8, 0::8, 0::8>> # TODO: This should be biome data
|
||||
end)
|
||||
|
||||
sky_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} -> 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(¬/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(¬/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
|
||||
send(to, {:send_packet, %{
|
||||
packet_type: :chunk_data_and_update_light,
|
||||
chunk_x: cx, chunk_z: cz,
|
||||
heightmaps: heightmaps,
|
||||
data: data,
|
||||
block_entities: [],
|
||||
# TODO: Light
|
||||
sky_light_mask: Write.varint(0),
|
||||
block_light_mask: Write.varint(0),
|
||||
empty_sky_light_mask: Write.varint(0),
|
||||
empty_block_light_mask: Write.varint(0),
|
||||
sky_light_arrays: [],
|
||||
block_light_arrays: []
|
||||
}})
|
||||
:ok
|
||||
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 ->
|
||||
ret = func.(value)
|
||||
next = <<acc::bitstring, ret::bitstring-size(bpe)>>
|
||||
next = <<acc::bitstring, value::big-size(bpe)>>
|
||||
# man i hope they dont suddenly change the size of a long
|
||||
if rem(bit_size(next), 64) + bpe > 64 do
|
||||
# gotta pad it
|
||||
@ -270,39 +227,21 @@ defmodule Amethyst.ConnectionHandler do
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
if Application.get_env(:amethyst, :release, false) do
|
||||
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
|
||||
if Mix.env() == :dev do
|
||||
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
||||
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__)}")
|
||||
end
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp send_packet(socket, connstate, packet, version, state) do
|
||||
defp send_packet(socket, connstate, packet, version) do
|
||||
try do
|
||||
data = connstate.serialize(packet, version)
|
||||
container = if Map.get(state, :compression) == nil do
|
||||
# Packet ID is included in 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)
|
||||
length = byte_size(data) |> Amethyst.Minecraft.Write.varint()
|
||||
:gen_tcp.send(socket, length <> data)
|
||||
rescue
|
||||
e ->
|
||||
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
|
||||
|
||||
defp disconnect(socket, reason, connstate, version, state) do
|
||||
defp disconnect(socket, reason, connstate, version) do
|
||||
Logger.info("Disconnecting connection #{inspect(socket)}")
|
||||
Logger.debug("Disconnecting connection #{inspect(socket)}: #{reason}")
|
||||
case connstate.disconnect(reason) do
|
||||
nil -> nil
|
||||
packet -> send_packet(socket, connstate, packet, version, state)
|
||||
packet -> send_packet(socket, connstate, packet, version)
|
||||
end
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
|
@ -35,7 +35,7 @@ defmodule Amethyst.ConnectionReceiver do
|
||||
{:ok, spawn(fn ->
|
||||
Process.set_label("ConnectionReceiver for #{inspect(socket)}")
|
||||
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
|
||||
receive(socket, pid, nil, nil)
|
||||
receive(socket, pid)
|
||||
end)}
|
||||
end
|
||||
|
||||
@ -44,89 +44,42 @@ defmodule Amethyst.ConnectionReceiver do
|
||||
{:ok, spawn_link(fn ->
|
||||
Process.set_label("ConnectionReceiver for #{inspect(socket)}")
|
||||
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
|
||||
receive(socket, pid, nil, nil)
|
||||
receive(socket, pid)
|
||||
end)}
|
||||
end
|
||||
|
||||
@spec receive(:gen_tcp.socket(), pid(), nil | :crypto.crypto_state(), nil | pos_integer()) :: no_return()
|
||||
def receive(socket, sender, estate, cstate) do
|
||||
case get_packet(socket, estate, cstate) do
|
||||
@spec receive(:gen_tcp.socket(), pid()) :: no_return()
|
||||
def receive(socket, sender) do
|
||||
case get_packet(socket) do
|
||||
:closed -> send(sender, :closed)
|
||||
Process.exit(self(), :normal)
|
||||
{:error, error} -> Logger.error("Error reading packet: #{error}")
|
||||
{id, data} -> send(sender, {:packet, id, data})
|
||||
end
|
||||
|
||||
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)
|
||||
receive(socket, sender)
|
||||
end
|
||||
|
||||
def get_packet(client, estate, cstate) do
|
||||
case get_varint(client, "", estate) do
|
||||
def get_packet(client) do
|
||||
case get_varint(client, "") do
|
||||
:closed -> :closed
|
||||
{:error, error} -> {:error, error}
|
||||
{[length], ""} ->
|
||||
data = :gen_tcp.recv(client, length)
|
||||
case data do
|
||||
{:ok, data} ->
|
||||
# Perform decryption and decompression
|
||||
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}
|
||||
recv = :gen_tcp.recv(client, length)
|
||||
case recv do
|
||||
{:ok, full_packet} -> ({[id], data} = Read.start(full_packet) |> Read.varint() |> Read.stop()
|
||||
{id, data})
|
||||
{:error, :closed} -> :closed
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_varint(client, acc, estate) do
|
||||
defp get_varint(client, acc) do
|
||||
case :gen_tcp.recv(client, 1) do
|
||||
{:ok, byte} ->
|
||||
byte = case estate do
|
||||
nil -> byte
|
||||
ds -> :crypto.crypto_update(ds, byte)
|
||||
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
|
||||
{:ok, byte} -> case byte do
|
||||
<<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop()
|
||||
<<1::1, _::7>> -> get_varint(client, acc <> byte)
|
||||
end
|
||||
{:error, :closed} -> :closed
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
defmodule Amethyst.Minecraft.Write do
|
||||
import Bitwise
|
||||
require Logger
|
||||
|
||||
@moduledoc """
|
||||
This module contains functions for writing Minecraft data.
|
||||
@ -140,28 +139,6 @@ defmodule Amethyst.Minecraft.Write do
|
||||
def nbt(value) do
|
||||
Amethyst.NBT.Write.write_net(value)
|
||||
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
|
||||
|
||||
defmodule Amethyst.Minecraft.Read do
|
||||
@ -239,11 +216,6 @@ defmodule Amethyst.Minecraft.Read do
|
||||
{[data | acc], rest, :reversed}
|
||||
end
|
||||
|
||||
def byte_array({acc, data, :reversed}) do
|
||||
{[length], rest} = start(data) |> varint |> stop
|
||||
raw({acc, rest, :reversed}, length)
|
||||
end
|
||||
|
||||
@doc """
|
||||
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
|
||||
{[data | acc], "", :reversed}
|
||||
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
|
||||
|
@ -30,6 +30,10 @@ defmodule Amethyst.Keys do
|
||||
GenServer.start_link(__MODULE__, bits, name: __MODULE__)
|
||||
end
|
||||
|
||||
def get_priv do
|
||||
GenServer.call(__MODULE__, :get_priv)
|
||||
end
|
||||
|
||||
def get_pub do
|
||||
GenServer.call(__MODULE__, :get_pub)
|
||||
end
|
||||
@ -48,19 +52,22 @@ defmodule Amethyst.Keys do
|
||||
rsa_public_key = {:RSAPublicKey, modulus, public_exponent}
|
||||
|
||||
Logger.info("Generated RSA keys")
|
||||
{:ok, {rsa_public_key, rsa_private_key, bits}}
|
||||
{:ok, {rsa_public_key, rsa_private_key}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get_pub, _from, {pubkey, privkey, bits}) do
|
||||
{:SubjectPublicKeyInfo, pk, :not_encrypted} = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, pubkey)
|
||||
# Logger.debug("#{inspect(pem_encoded, limit: :infinity)}")
|
||||
{:reply, pk, {pubkey, privkey, bits}}
|
||||
def handle_call(:get_priv, _from, {pubkey, privkey}) do
|
||||
{:reply, :public_key.der_encode(:RSAPrivateKey, privkey), {pubkey, privkey}}
|
||||
end
|
||||
|
||||
@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)
|
||||
{:reply, plaintext, {pubkey, privkey, bits}}
|
||||
{:reply, plaintext, {pubkey, privkey}}
|
||||
end
|
||||
end
|
||||
|
@ -88,22 +88,11 @@ defmodule Amethyst.Game do
|
||||
mod.accept_teleport(self(), id, refs)
|
||||
end
|
||||
@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.
|
||||
|
||||
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
|
||||
mod.chunk(self(), pos, refs)
|
||||
end
|
||||
|
@ -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
|
@ -203,10 +203,10 @@ defmodule Amethyst.ConnectionState.Configuration do
|
||||
# https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains
|
||||
basic_biome = compound(%{
|
||||
"effects" => compound(%{
|
||||
"sky_color" => int(7_907_327),
|
||||
"water_fog_color" => int(329_011),
|
||||
"fog_color" => int(12_638_463),
|
||||
"water_color" => int(4_159_204),
|
||||
"sky_color" => int(7907327),
|
||||
"water_fog_color" => int(329011),
|
||||
"fog_color" => int(12638463),
|
||||
"water_color" => int(4159204),
|
||||
"mood_sound" => compound(%{
|
||||
"tick_delay" => int(6000),
|
||||
"offset" => float(2.0),
|
||||
@ -310,16 +310,6 @@ defmodule Amethyst.ConnectionState.Configuration do
|
||||
chunk_z: div(floor(z), 16)
|
||||
}})
|
||||
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
|
||||
# TODO: Put it under some supervisor
|
||||
me = self()
|
||||
|
@ -60,20 +60,10 @@ defmodule Amethyst.ConnectionState.Login do
|
||||
Macros.defpacket_serverbound :login_acknowledged, 0x03, 767, []
|
||||
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}")
|
||||
if Application.get_env(:amethyst, :encryption, true) do
|
||||
verify_token = :crypto.strong_rand_bytes(4)
|
||||
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)
|
||||
if Application.fetch_env!(:amethyst, :encryption) do
|
||||
raise RuntimeError, "Encryption is not currently supported"
|
||||
else
|
||||
send(self(), {:send_packet, %{
|
||||
packet_type: :login_success,
|
||||
@ -85,57 +75,7 @@ defmodule Amethyst.ConnectionState.Login do
|
||||
:ok
|
||||
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
|
||||
Logger.debug("Received login acknowledged")
|
||||
send(self(), {:set_state, Amethyst.ConnectionState.Configuration})
|
||||
|
@ -14,12 +14,7 @@
|
||||
# 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/>.
|
||||
|
||||
# TODO!!!: REDO THIS WHOLE THING AGAIN IT'S A MESS
|
||||
|
||||
defmodule Amethyst.ConnectionState.Macros do
|
||||
@moduledoc """
|
||||
Useful macros for defining packets.
|
||||
"""
|
||||
require Logger
|
||||
defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do
|
||||
quote do
|
||||
@ -33,8 +28,7 @@ defmodule Amethyst.ConnectionState.Macros do
|
||||
defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do
|
||||
quote 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 Application.get_env(:amethyst, :release, false) || Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
|
||||
if Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
|
||||
Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature))
|
||||
else
|
||||
raise "Invalid packet type for #{unquote(name)}! Got #{inspect(packet)}"
|
||||
@ -58,13 +52,6 @@ defmodule Amethyst.ConnectionState.Macros do
|
||||
else
|
||||
{[nil | acc], rest, :reversed}
|
||||
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} ->
|
||||
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
|
||||
if exists do
|
||||
@ -72,10 +59,6 @@ defmodule Amethyst.ConnectionState.Macros do
|
||||
else
|
||||
{[nil | acc], rest, :reversed}
|
||||
end
|
||||
{:fixed_bitset, length} ->
|
||||
Read.fixed_bitset({acc, rest, :reversed}, length)
|
||||
{:raw, length} ->
|
||||
Read.raw({acc, rest, :reversed}, length)
|
||||
{:array, signature} ->
|
||||
{[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop()
|
||||
if count == 0 do
|
||||
@ -112,7 +95,7 @@ defmodule Amethyst.ConnectionState.Macros do
|
||||
Enum.reduce(Map.get(packet, name), "", fn item, acc ->
|
||||
acc <> write_signature(item, signature)
|
||||
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)])
|
||||
end
|
||||
end)
|
||||
@ -123,7 +106,7 @@ defmodule Amethyst.ConnectionState.Macros do
|
||||
Enum.all?(signature, fn {name, type} ->
|
||||
case Map.get(packet, name, :missing) do
|
||||
:missing ->
|
||||
if is_tuple(type) && elem(type, 0) == :literal do
|
||||
if elem(type, 0) == :literal do
|
||||
true
|
||||
else
|
||||
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, :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, :short) when is_integer(value) and value in -32_768..32_767, do: true
|
||||
def type_matches(value, :ushort) when is_integer(value) and value in 0..65_535, 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, :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, :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..65535, 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 -9223372036854775808..9223372036854775807, 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, :varint) when is_integer(value) and value in -2_147_483_648..2_147_483_647, 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, :varint) when is_integer(value) and value in -2147483648..2147483647, 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, :string) 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({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(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, :json) 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}), 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, :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(_, _) do
|
||||
false
|
||||
|
@ -36,10 +36,10 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
type: :varint,
|
||||
data: :nbt
|
||||
]},
|
||||
sky_light_mask: :bitset,
|
||||
block_light_mask: :bitset,
|
||||
empty_sky_light_mask: :bitset,
|
||||
empty_block_light_mask: :bitset,
|
||||
sky_light_mask: :raw,
|
||||
block_light_mask: :raw,
|
||||
empty_sky_light_mask: :raw,
|
||||
empty_block_light_mask: :raw,
|
||||
sky_light_arrays: {:array, [
|
||||
sky_light_array: :byte_array
|
||||
]},
|
||||
@ -71,8 +71,8 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
portal_cooldown: :varint,
|
||||
enforces_secure_chat: :bool,
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_add_player, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x01},
|
||||
Macros.defpacket_clientbound :player_info_update_add_player, 0x2E, 767, [
|
||||
actions: {:literal, 0x01},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
name: :string,
|
||||
@ -83,8 +83,8 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
]}
|
||||
]}
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x02},
|
||||
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x2E, 767, [
|
||||
actions: {:literal, 0x02},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
data: {:optional, {:compound, [
|
||||
@ -95,29 +95,29 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
]}}
|
||||
]}
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x04},
|
||||
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x2E, 767, [
|
||||
actions: {:literal, 0x04},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
gamemode: :varint
|
||||
]}
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_update_listed, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x08},
|
||||
Macros.defpacket_clientbound :player_info_update_update_listed, 0x2E, 767, [
|
||||
actions: {:literal, 0x08},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
listed: :bool
|
||||
]}
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_update_latency, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x10},
|
||||
Macros.defpacket_clientbound :player_info_update_update_latency, 0x2E, 767, [
|
||||
actions: {:literal, 0x10},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
ping: :varint, # Milliseconds
|
||||
]}
|
||||
]
|
||||
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x3E, 767, [
|
||||
actions: {:literal, :byte, 0x20},
|
||||
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x2E, 767, [
|
||||
actions: {:literal, 0x20},
|
||||
players: {:array, [
|
||||
uuid: :uuid,
|
||||
display_name: {:optional, :nbt}
|
||||
@ -135,10 +135,6 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [
|
||||
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, [
|
||||
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}
|
||||
|
||||
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 :keep_alive, 0x18, 767, [id: :long]
|
||||
Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [
|
||||
@ -245,12 +233,6 @@ defmodule Amethyst.ConnectionState.Play do
|
||||
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
|
||||
ka = state |> Map.get(:keepalive)
|
||||
send(ka, {:respond, id})
|
||||
|
@ -39,8 +39,7 @@ defmodule Amethyst.MixProject do
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:uuid, "~> 1.1"},
|
||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||
{:jason, "~> 1.4"},
|
||||
{:req, "~> 0.5.6", runtime: true}
|
||||
{:jason, "~> 1.4"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
@ -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
|
@ -2,12 +2,6 @@ defmodule Example.Game do
|
||||
require Logger
|
||||
@behaviour Amethyst.Game
|
||||
|
||||
@moduledoc """
|
||||
Example game used for testing Amethyst.
|
||||
"""
|
||||
require Amethyst.NBT.Write
|
||||
alias Amethyst.NBT
|
||||
|
||||
@impl true
|
||||
def instantiate(supervisor) do
|
||||
Logger.info("The supervisor for this game is at #{inspect(supervisor)}")
|
||||
@ -41,36 +35,21 @@ alias Amethyst.NBT
|
||||
:ok
|
||||
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
|
||||
def joinable?(_refs) do
|
||||
true
|
||||
end
|
||||
|
||||
|
||||
@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}")
|
||||
(0..255) |> Enum.map(fn y ->
|
||||
(0..15) |> Enum.map(fn z ->
|
||||
(0..15) |> Enum.map(fn x ->
|
||||
gx = cx * 16 + x
|
||||
gz = cz * 16 + z
|
||||
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}
|
||||
if y <= x + z do
|
||||
3
|
||||
else
|
||||
{0, 7, 7}
|
||||
0
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
@ -2,9 +2,6 @@ import Config
|
||||
|
||||
config :amethyst,
|
||||
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
||||
encryption: true, # Whether or not to request encryption from clients.
|
||||
auth: true, # Whether or not users should be authenticated with Mojang.
|
||||
session_server: "https://sessionserver.mojang.com", # Base URL to the Mojang session server
|
||||
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
|
||||
encryption: false, # Whether or not to request encryption from clients.
|
||||
auth: false, # Whether or not users should be authenticated with Mojang.
|
||||
default_game: Example.Game # Which game new players should be sent to
|
||||
|
17
flake.nix
17
flake.nix
@ -43,20 +43,14 @@
|
||||
})
|
||||
);
|
||||
|
||||
package = env: release: pkgs: (pkgs.beamPackages.mixRelease {
|
||||
#secrets = import ./secrets.nix;
|
||||
|
||||
package = env: pkgs: (pkgs.beamPackages.mixRelease {
|
||||
pname = "amethyst";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
elixir = pkgs.elixir;
|
||||
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
|
||||
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;};
|
||||
@ -70,9 +64,8 @@
|
||||
in {
|
||||
packages = eachSystem (pkgs:
|
||||
{
|
||||
default = package "prod" "amethyst" pkgs;
|
||||
dev = package "dev" "amethyst" pkgs;
|
||||
example = package "prop" "example" pkgs;
|
||||
default = package "prod" pkgs;
|
||||
dev = package "dev" pkgs;
|
||||
}
|
||||
);
|
||||
|
||||
|
2
mix.exs
2
mix.exs
@ -8,7 +8,7 @@ defmodule AmethystUmbrella.MixProject do
|
||||
elixir: "~> 1.17",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
# default_release: :amethyst,
|
||||
default_release: :amethyst,
|
||||
|
||||
releases: [
|
||||
amethyst: [
|
||||
|
8
mix.lock
8
mix.lock
@ -5,19 +5,11 @@
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"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"},
|
||||
}
|
||||
|
130
mix.nix
130
mix.nix
@ -47,19 +47,6 @@ let
|
||||
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 {
|
||||
name = "ex_doc";
|
||||
version = "0.34.2";
|
||||
@ -86,32 +73,6 @@ let
|
||||
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 {
|
||||
name = "jason";
|
||||
version = "1.4.4";
|
||||
@ -164,45 +125,6 @@ let
|
||||
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 {
|
||||
name = "nimble_parsec";
|
||||
version = "1.4.0";
|
||||
@ -216,58 +138,6 @@ let
|
||||
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 {
|
||||
name = "uuid";
|
||||
version = "1.1.8";
|
||||
|
Loading…
Reference in New Issue
Block a user