Compare commits
No commits in common. "handler_refactor" and "main" have entirely different histories.
handler_re
...
main
@ -87,6 +87,7 @@
|
|||||||
{Credo.Check.Readability.VariableNames, []},
|
{Credo.Check.Readability.VariableNames, []},
|
||||||
{Credo.Check.Readability.WithSingleClause, []},
|
{Credo.Check.Readability.WithSingleClause, []},
|
||||||
{Credo.Check.Readability.BlockPipe, []},
|
{Credo.Check.Readability.BlockPipe, []},
|
||||||
|
{Credo.Check.Readability.SinglePipe, []},
|
||||||
|
|
||||||
#
|
#
|
||||||
## Refactoring Opportunities
|
## Refactoring Opportunities
|
||||||
@ -156,7 +157,6 @@
|
|||||||
{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, []},
|
||||||
|
@ -22,198 +22,108 @@ defmodule Amethyst.ConnectionHandler do
|
|||||||
alias Amethyst.Minecraft.Write
|
alias Amethyst.Minecraft.Write
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@enforce_keys [:handler, :socket]
|
|
||||||
defstruct [
|
|
||||||
:handler,
|
|
||||||
:socket,
|
|
||||||
version: 0,
|
|
||||||
connection_state: Amethyst.ConnectionState.Handshake,
|
|
||||||
game: nil,
|
|
||||||
encryption_state: nil,
|
|
||||||
decryption_state: nil,
|
|
||||||
compression_threshold: nil,
|
|
||||||
authenticated: false,
|
|
||||||
position: {0, 0, 0},
|
|
||||||
player_state: %{}
|
|
||||||
]
|
|
||||||
@type t :: %__MODULE__{
|
|
||||||
handler: pid(),
|
|
||||||
socket: :gen_tcp.socket(),
|
|
||||||
version: 0 | 767,
|
|
||||||
connection_state: Amethyst.ConnectionState.t(),
|
|
||||||
game: nil | Amethyst.GameCoordinator.Game,
|
|
||||||
encryption_state: nil | :crypto.crypto_state(),
|
|
||||||
decryption_state: nil | :crypto.crypto_state(),
|
|
||||||
compression_threshold: nil | non_neg_integer(),
|
|
||||||
authenticated: boolean(),
|
|
||||||
position: {x :: number(), y :: number(), z :: number()},
|
|
||||||
player_state: map()
|
|
||||||
}
|
|
||||||
|
|
||||||
@type operation :: (__MODULE__.t() -> __MODULE__.t())
|
|
||||||
defmodule Operations do
|
|
||||||
@moduledoc """
|
|
||||||
This module includes generators for common operations
|
|
||||||
which the `ConnectionHandler` should perform.
|
|
||||||
"""
|
|
||||||
alias Amethyst.ConnectionHandler
|
|
||||||
|
|
||||||
@spec put(key :: atom(), value :: any()) :: ConnectionHandler.operation()
|
|
||||||
def put(key, value) do
|
|
||||||
fn state ->
|
|
||||||
Map.put(state, key, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec put_ps(key :: any(), value :: any()) :: ConnectionHandler.operation()
|
|
||||||
def put_ps(key, value) do
|
|
||||||
fn state ->
|
|
||||||
Map.put(state, :player_state, Map.put(state.player_state, key, value))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec send_packet(%{packet_type: atom()}) :: ConnectionHandler.operation()
|
|
||||||
def send_packet(packet) do
|
|
||||||
fn state ->
|
|
||||||
try do
|
|
||||||
data = Amethyst.ConnectionState.serialize(packet, state)
|
|
||||||
|
|
||||||
container = if state.compression_threshold == nil do
|
|
||||||
# Packet ID is included in data
|
|
||||||
Write.varint(byte_size(data)) <> data
|
|
||||||
else
|
|
||||||
threshold = state.compression_threshold
|
|
||||||
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 state.encryption_state == nil do
|
|
||||||
container
|
|
||||||
else
|
|
||||||
state.encryption_state |> :crypto.crypto_update(container)
|
|
||||||
end
|
|
||||||
:gen_tcp.send(state.socket, encrypted)
|
|
||||||
rescue
|
|
||||||
e ->
|
|
||||||
Logger.error("Error sending packet #{inspect(packet)}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec disconnect(reason :: String.t()) :: ConnectionHandler.operation()
|
|
||||||
def disconnect(reason) do
|
|
||||||
fn state ->
|
|
||||||
func = Amethyst.ConnectionState.disconnect(state, reason) |> send_packet()
|
|
||||||
func.(state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
|
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
|
||||||
def child_spec(socket) do
|
def child_spec(socket) do
|
||||||
%{
|
%{
|
||||||
id: __MODULE__,
|
id: __MODULE__,
|
||||||
start: {__MODULE__, :start, [socket]}
|
start: {__MODULE__, :start, [socket, Amethyst.ConnectionState.Handshake, 0]}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec start(:gen_tcp.socket()) :: no_return()
|
@spec start(:gen_tcp.socket(), atom(), integer()) :: no_return()
|
||||||
def start(socket) do
|
def start(socket, connstate, version) do
|
||||||
{:ok, spawn(fn ->
|
{:ok, spawn(fn ->
|
||||||
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
||||||
loop(%__MODULE__{socket: socket, handler: self()})
|
loop(socket, connstate, version, %{})
|
||||||
end)}
|
end)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec start_link(:gen_tcp.socket()) :: no_return()
|
@spec start_link(:gen_tcp.socket(), atom(), integer()) :: no_return()
|
||||||
def start_link(socket) do
|
def start_link(socket, connstate, version) do
|
||||||
{:ok, spawn_link(fn ->
|
{:ok, spawn_link(fn ->
|
||||||
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
||||||
loop(%__MODULE__{socket: socket, handler: self()})
|
loop(socket, connstate, version, %{})
|
||||||
end)}
|
end)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@spec loop(:gen_tcp.socket(), atom(), integer(), map()) :: no_return()
|
||||||
Informs the `handler` that a packet was received and that it should handle it.
|
defp loop(socket, connstate, version, state) do
|
||||||
"""
|
|
||||||
@spec packet(handler :: pid(), id :: pos_integer(), data :: binary()) :: :ok
|
|
||||||
def packet(handler, id, data) do
|
|
||||||
send(handler, {:packet, id, data})
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Informs the `handler` that its client's socket closed the connection and that it
|
|
||||||
should proceed with cleaning it up.
|
|
||||||
"""
|
|
||||||
@spec close(handler :: pid()) :: :ok
|
|
||||||
def close(handler) do
|
|
||||||
send(handler, :closed)
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Runs `ops` on the handler's state. All operations in `ops` will be ran sequentially
|
|
||||||
and will not be interrupted by another operation.
|
|
||||||
"""
|
|
||||||
@spec run(handler :: pid(), ops :: [operation()]) :: :ok
|
|
||||||
def run(handler, ops) do
|
|
||||||
send(handler, {:run, ops})
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Runs a function on the handler's state and returns the result. This cannot update the state,
|
|
||||||
this may only be used to get data.
|
|
||||||
"""
|
|
||||||
@spec get(handler :: pid(), f :: (__MODULE__.t() -> item)) :: item when item: any()
|
|
||||||
def get(handler, f) do
|
|
||||||
nonce = :rand.bytes(4)
|
|
||||||
send(handler, {:get, self(), f, nonce})
|
|
||||||
receive do
|
receive do
|
||||||
{:respond, ret, ^nonce} -> ret
|
{:disconnect, reason} ->
|
||||||
|
disconnect(socket, reason, connstate, version, state)
|
||||||
|
Process.exit(self(), :normal)
|
||||||
|
{:set_state, newstate} ->
|
||||||
|
Logger.debug("Switching to state #{newstate} from #{connstate}")
|
||||||
|
loop(socket, newstate, version, state)
|
||||||
|
{: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)
|
||||||
|
# If there was no prev position, we consider that we
|
||||||
|
# definitely moved
|
||||||
|
prev_cp = if prev_position == nil do nil else chunk_pos(elem(prev_position, 0), elem(prev_position, 2)) end
|
||||||
|
cp = chunk_pos(elem(position, 0), elem(position, 2))
|
||||||
|
if prev_cp != cp do
|
||||||
|
Logger.debug("Client entered new chunk #{inspect(cp)}")
|
||||||
|
# We changed chunk borders, update center chunk and begin sending new chunks
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :set_center_chunk,
|
||||||
|
chunk_x: elem(cp, 0),
|
||||||
|
chunk_z: elem(cp, 1)
|
||||||
|
}})
|
||||||
|
# Figure out which new chunks are visible
|
||||||
|
prev_chunks =
|
||||||
|
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!!
|
||||||
end
|
end
|
||||||
|
chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), 16))
|
||||||
|
new_chunks = MapSet.difference(chunks, prev_chunks)
|
||||||
|
Logger.debug("Sending #{MapSet.size(new_chunks)} chunks...")
|
||||||
|
# We can process all chunks in parallel
|
||||||
|
me = self()
|
||||||
|
ts = state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor)
|
||||||
|
Task.Supervisor.async(ts, fn ->
|
||||||
|
Task.Supervisor.async_stream(ts,
|
||||||
|
new_chunks,
|
||||||
|
fn chunk -> process_chunk(me, chunk, state) end,
|
||||||
|
[ordered: false]
|
||||||
|
) |> Stream.run() end)
|
||||||
end
|
end
|
||||||
|
loop(socket, connstate, version, state)
|
||||||
@spec run_ops([operation()], __MODULE__.t()) :: {state :: __MODULE__.t(), successes :: [boolean()]}
|
{:send_packet, packet} ->
|
||||||
defp run_ops(ops, state) do
|
# Logger.debug("Sending packet #{inspect(packet)}")
|
||||||
Enum.reduce(Enum.with_index(ops), {state, []}, fn {op, i}, {state, sacc} ->
|
send_packet(socket, connstate, packet, version, state)
|
||||||
try do
|
loop(socket, connstate, version, state)
|
||||||
{op.(state), [true | sacc]}
|
after 0 ->
|
||||||
rescue
|
# Received stuff from the connection receiver is lower priority
|
||||||
e ->
|
|
||||||
Logger.error("Operation error in op #{i}, continuing regardless: #{Exception.format(:error, e, __STACKTRACE__)}")
|
|
||||||
{state, [false | sacc]}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
@spec loop(__MODULE__.t()) :: no_return()
|
|
||||||
defp loop(state) do
|
|
||||||
receive do
|
receive do
|
||||||
:closed ->
|
:closed ->
|
||||||
# Socket is closed, handle our side of the disconnect
|
Logger.info("Connection #{inspect(socket)} closed.")
|
||||||
Process.exit(self(), :normal)
|
Process.exit(self(), :normal)
|
||||||
{:packet, id, data} when is_integer(id) and id >= 0 and is_binary(data) ->
|
{:get_encryption, from} ->
|
||||||
{state, successes} = run_ops(handle_packet(id, data, state), state)
|
send(from, Map.get(state, :decryption_state))
|
||||||
if !Enum.all?(successes) do
|
loop(socket, connstate, version, state)
|
||||||
Logger.error("Failed to run operations of packet #{inspect(id, base: :hex)}")
|
{: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)
|
||||||
end
|
end
|
||||||
loop(state)
|
|
||||||
{:run, ops} ->
|
|
||||||
{state, successes} = run_ops(ops, state)
|
|
||||||
if !Enum.all?(successes) do
|
|
||||||
Logger.error("Failed to run requested operations.")
|
|
||||||
end
|
|
||||||
loop(state)
|
|
||||||
{:get, from, op, nonce} ->
|
|
||||||
send(from, {:respond, op.(state), nonce})
|
|
||||||
loop(state)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -221,6 +131,7 @@ defmodule Amethyst.ConnectionHandler do
|
|||||||
{floor(round(x) / 16.0), floor(round(z) / 16.0)}
|
{floor(round(x) / 16.0), floor(round(z) / 16.0)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# x, z here is chunk position
|
||||||
defp visible_chunks_from(x, z, view_distance) do
|
defp visible_chunks_from(x, z, view_distance) do
|
||||||
(x - view_distance - 3 .. x + view_distance + 3) |> Enum.flat_map(fn ix ->
|
(x - view_distance - 3 .. x + view_distance + 3) |> Enum.flat_map(fn ix ->
|
||||||
(z - view_distance - 3 .. z + view_distance + 3) |> Enum.map(fn iz ->
|
(z - view_distance - 3 .. z + view_distance + 3) |> Enum.map(fn iz ->
|
||||||
@ -340,21 +251,72 @@ defmodule Amethyst.ConnectionHandler do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec handle_packet(non_neg_integer(), binary(), __MODULE__.t()) :: [operation()]
|
defp handle_packet(id, data, connstate, version, state) do
|
||||||
defp handle_packet(id, data, state) when is_struct(state, __MODULE__) do
|
|
||||||
try do
|
try do
|
||||||
packet = Amethyst.ConnectionState.deserialize(state, id, data)
|
packet = connstate.deserialize(id, version, data)
|
||||||
Amethyst.ConnectionState.handle(state, packet)
|
case connstate.handle(packet, version, state) do
|
||||||
|
:ok -> state
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{reason}")
|
||||||
|
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{reason}"})
|
||||||
|
state
|
||||||
|
newstate ->
|
||||||
|
if is_map(newstate) do
|
||||||
|
newstate
|
||||||
|
else
|
||||||
|
Logger.warning("State change to #{inspect(newstate)} is not a map! Did you forget to return :ok?")
|
||||||
|
state
|
||||||
|
end
|
||||||
|
end
|
||||||
rescue
|
rescue
|
||||||
e ->
|
e ->
|
||||||
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
|
||||||
if Application.get_env(:amethyst, :release, false) do
|
if Application.get_env(:amethyst, :release, false) do
|
||||||
[Operations.disconnect(
|
send(self(), {:disconnect, "§cError handling packet #{inspect(id, base: :hex)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
|
||||||
"§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__)}")
|
||||||
)]
|
|
||||||
else
|
else
|
||||||
[]
|
Logger.error("Error handling packet with ID #{inspect(id, base: :hex)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
||||||
|
end
|
||||||
|
state
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp send_packet(socket, connstate, packet, version, state) 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)
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.error("Error sending packet #{inspect(packet)} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
||||||
|
send(self(), {:disconnect, "§cError sending packet #{inspect(packet)}:\n#{Exception.format(:error, e, __STACKTRACE__)}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp disconnect(socket, reason, connstate, version, state) 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)
|
||||||
|
end
|
||||||
|
:gen_tcp.close(socket)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -34,7 +34,7 @@ defmodule Amethyst.ConnectionReceiver do
|
|||||||
def start(socket) do
|
def start(socket) 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)
|
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
|
||||||
receive(socket, pid, nil, nil)
|
receive(socket, pid, nil, nil)
|
||||||
end)}
|
end)}
|
||||||
end
|
end
|
||||||
@ -43,33 +43,39 @@ defmodule Amethyst.ConnectionReceiver do
|
|||||||
def start_link(socket) do
|
def start_link(socket) 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)
|
{:ok, pid} = Amethyst.ConnectionHandler.start_link(socket, Amethyst.ConnectionState.Handshake, 0)
|
||||||
receive(socket, pid, nil, nil)
|
receive(socket, pid, nil, nil)
|
||||||
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(), nil | :crypto.crypto_state(), nil | pos_integer()) :: no_return()
|
||||||
def receive(socket, handler, estate, cstate) do
|
def receive(socket, sender, estate, cstate) do
|
||||||
case get_packet(socket, estate, cstate) do
|
case get_packet(socket, estate, cstate) do
|
||||||
:closed -> Amethyst.ConnectionHandler.close(handler)
|
: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} -> Amethyst.ConnectionHandler.packet(handler, id, data)
|
{id, data} -> send(sender, {:packet, id, data})
|
||||||
end
|
end
|
||||||
|
|
||||||
estate = if estate == nil do
|
estate = if estate == nil do
|
||||||
Amethyst.ConnectionHandler.get(handler, fn s -> s.encryption_state end)
|
# Ask the handler if we have encryption now
|
||||||
else
|
send(sender, {:get_encryption, self()})
|
||||||
estate
|
receive do
|
||||||
|
nil -> nil
|
||||||
|
some -> some
|
||||||
end
|
end
|
||||||
|
else estate end
|
||||||
|
|
||||||
cstate = if cstate == nil do
|
cstate = if cstate == nil do
|
||||||
Amethyst.ConnectionHandler.get(handler, fn s -> s.compression_threshold end)
|
# Ask the handler if we have encryption now
|
||||||
else
|
send(sender, {:get_compression, self()})
|
||||||
cstate
|
receive do
|
||||||
|
nil -> nil
|
||||||
|
some -> some
|
||||||
end
|
end
|
||||||
|
else cstate end
|
||||||
|
|
||||||
receive(socket, handler, estate, cstate)
|
receive(socket, sender, estate, cstate)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_packet(client, estate, cstate) do
|
def get_packet(client, estate, cstate) do
|
||||||
|
@ -14,44 +14,290 @@
|
|||||||
# 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/>.
|
||||||
|
|
||||||
defmodule Amethyst.Data do
|
defmodule Amethyst.Minecraft.Write do
|
||||||
@type _boolean :: bool()
|
import Bitwise
|
||||||
def is?({:_boolean}, x), do: is_boolean(x)
|
require Logger
|
||||||
@type _byte :: -128..127
|
|
||||||
def is?({:_byte}, x), do: is_integer(x) and x in -128..127
|
@moduledoc """
|
||||||
@type _ubyte :: 0..255
|
This module contains functions for writing Minecraft data.
|
||||||
def is?({:_ubyte}, x), do: is_integer(x) and x in 0..255
|
|
||||||
@type _short :: -32768..32767
|
Each function in this module takes in an input of the proper type and returns a binary
|
||||||
def is?({:_short}, x), do: is_integer(x) and x in -32768..32767
|
of the encoded data.
|
||||||
@type _ushort :: 0..65535
|
"""
|
||||||
def is?({:_ushort}, x), do: is_integer(x) and x in 0..65535
|
|
||||||
@type _int :: -2147483648..2147483647
|
def uuid(uuid) when is_binary(uuid) do
|
||||||
def is?({:_int}, x), do: is_integer(x) and x in -2147483648..2147483647
|
UUID.string_to_binary!(uuid)
|
||||||
@type _uint :: 0..4294967295
|
end
|
||||||
def is?({:_uint}, x), do: is_integer(x) and x in 0..4294967295
|
|
||||||
@type _long :: -9223372036854775808..9223372036854775807
|
def bool(value) when is_boolean(value) do
|
||||||
def is?({:_long}, x), do: is_integer(x) and x in -9223372036854775808..9223372036854775807
|
case value do
|
||||||
@type _float :: float()
|
true -> <<0x01::8>>
|
||||||
def is?({:_float}, x), do: is_float(x)
|
false -> <<0x00::8>>
|
||||||
@type _double :: float()
|
end
|
||||||
def is?({:_double}, x), do: is_float(x)
|
end
|
||||||
@type _string :: String.t()
|
|
||||||
def is?({:_string}, x), do: is_binary(x)
|
def raw(value) do
|
||||||
@type _nbt :: map() # TODO: Type-ify NBT
|
value
|
||||||
def is?({:_nbt}, x), do: is_map(x)
|
end
|
||||||
@type _varint :: _int()
|
|
||||||
def is?({:_varint}, x), do: is?({:_int}, x)
|
def byte(value) when value in -128..127 do
|
||||||
@type _varlong :: _long()
|
<<value::8-signed-big>>
|
||||||
def is?({:_varlong}, x), do: is?({:_long}, x)
|
end
|
||||||
@type _position :: {-33554432..33554431, -2048..2047, -33554432..33554431}
|
|
||||||
def is?({:_position}, {x, y, z}), do: is?({:_int}, x) and is?({:_int}, y) and is?({:_int}, z)
|
def ubyte(value) when value in 0..255 do
|
||||||
@type _uuid :: UUID.uuid4()
|
<<value::8-unsigned-big>>
|
||||||
def is?({:_uuid}, x), do: is_binary(x) and byte_size(x) == 16 # Not the best check but it's good enoug
|
end
|
||||||
@type _bitset :: [boolean()]
|
|
||||||
def is?({:_bitset}, x), do: is_list(x) and Enum.all?(x, &is_boolean/1)
|
def short(value) when value in -32_768..32_767 do
|
||||||
def is?({:_bitset, n}, x), do: is_list(x) and Enum.all?(x, &is_boolean/1) and length(x) == n
|
<<value::16-signed-big>>
|
||||||
@type _optional(n) :: n | nil
|
end
|
||||||
def is?({:_optional, n}, x), do: is?({n}, x) or is_nil(x)
|
|
||||||
@type _array(n) :: [n]
|
def ushort(value) when value in 0..65_535 do
|
||||||
def is?({:_array, n}, x), do: is_list(x) and Enum.all?(x, &is?({n}, &1))
|
<<value::16-unsigned-big>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def int(value) when value in -2_147_483_648..2_147_483_647 do
|
||||||
|
<<value::32-signed-big>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def long(value) when value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807 do
|
||||||
|
<<value::64-signed-big>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def float(value) when is_number(value) do
|
||||||
|
<<value::32-float>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def double(value) when is_number(value) do
|
||||||
|
<<value::64-float>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def varint(value) when value in -2_147_483_648..2_147_483_647 do
|
||||||
|
<<value::32-unsigned>> = <<value::32-signed>> # This is a trick to allow the arithmetic shift to act as a logical shift
|
||||||
|
varnum("", value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def varint(_) do
|
||||||
|
raise ArgumentError, "Value is out of range for a varint"
|
||||||
|
end
|
||||||
|
|
||||||
|
def varlong(value) when value in -9_223_372_036_854_775_808..9_223_372_036_854_775_807
|
||||||
|
do
|
||||||
|
<<value::64-unsigned>> = <<value::64-signed>>
|
||||||
|
varnum("", value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def varlong(_) do
|
||||||
|
raise ArgumentError, "Value is out of range for a varlong"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp varnum(acc, value) when value in 0..127 do
|
||||||
|
acc <> <<0::1, value::7-big>>
|
||||||
|
end
|
||||||
|
|
||||||
|
defp varnum(acc, value) do
|
||||||
|
acc <> <<1::1, value::7-big>> |> varnum(value >>> 7)
|
||||||
|
end
|
||||||
|
|
||||||
|
def string(value) do
|
||||||
|
<<varint(byte_size(value))::binary, value::binary>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def json(value) do
|
||||||
|
string(Jason.encode!(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
def byte_array(value) do
|
||||||
|
<<varint(byte_size(value))::binary, value::binary>>
|
||||||
|
end
|
||||||
|
|
||||||
|
def position({x, y, z}) do
|
||||||
|
<<x::signed-big-26, z::signed-big-26, y::signed-big-12>>
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Writes a list of elements with the given `callback` function. This does not
|
||||||
|
prefix the list with a length, remember to do that yourself if needed.
|
||||||
|
|
||||||
|
iex> Amethyst.Minecraft.Write.list([1, 2, 3, 4], &Amethyst.Minecraft.Write.byte/1)
|
||||||
|
<<1, 2, 3, 4>>
|
||||||
|
"""
|
||||||
|
def list(list, callback) do
|
||||||
|
Enum.reduce(list, "", &(&2 <> callback.(&1)))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Shorthand function for writing a value which may not be present. If `value` is `nil`,
|
||||||
|
writes `false`, otherwise writes `true` followed by the value using `callback`.
|
||||||
|
"""
|
||||||
|
def option(value, callback) do
|
||||||
|
case value do
|
||||||
|
nil -> bool(false)
|
||||||
|
v -> bool(true) <> callback.(v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
import Bitwise
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains functions for reading Minecraft data.
|
||||||
|
|
||||||
|
These functions allow you to chain them into eachother, at the end they will produce a list of all the
|
||||||
|
values they have read.
|
||||||
|
|
||||||
|
You may use the helper function `Amethyst.Minecraft.Read.start/1` to start the chain with a binary buffer.
|
||||||
|
The return value of the chain is a tuple containing the list of values and the remaining binary buffer.
|
||||||
|
|
||||||
|
iex> alias Amethyst.Minecraft.Read
|
||||||
|
iex> {[_, _, _], ""} = Read.start(<<1, 999::16, 64>>) |> Read.bool() |> Read.short() |> Read.byte() |> Read.stop()
|
||||||
|
{[true, 999, 64], ""}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
This function structures an input binary to be used by the functions in `Amethyst.Minecraft.Read`.
|
||||||
|
"""
|
||||||
|
def start(binary) do
|
||||||
|
{[], binary, :reversed}
|
||||||
|
end
|
||||||
|
@doc """
|
||||||
|
This function structures the result of the functions in `Amethyst.Minecraft.Read` to be used in the same order they were read.
|
||||||
|
"""
|
||||||
|
def stop({acc, rest, :reversed}) do
|
||||||
|
{Enum.reverse(acc), rest}
|
||||||
|
end
|
||||||
|
|
||||||
|
def bool({acc, <<value, rest::binary>>, :reversed}) do
|
||||||
|
{[value != 0 | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def byte({acc, <<value::big-signed, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ubyte({acc, <<value::big-unsigned, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def short({acc, <<value::16-big-signed, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def ushort({acc, <<value::16-big-unsigned, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def int({acc, <<value::32-big-signed, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def long({acc, <<value::64-big-signed, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def float({acc, <<value::32-float-big, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def double({acc, <<value::64-float-big, rest::binary>>, :reversed}) do
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def uuid({acc, <<uuid::16-binary, rest::binary>>, :reversed}) do
|
||||||
|
{[UUID.binary_to_string!(uuid) | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def raw({acc, data, :reversed}, amount) do
|
||||||
|
<<data::binary-size(amount), rest::binary>> = data
|
||||||
|
{[data | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
def varint(tuple, read \\ 0, nacc \\ 0)
|
||||||
|
def varint({acc, <<1::1, value::7, rest::binary>>, :reversed}, read, nacc) when read < 5 do
|
||||||
|
varint({acc, rest, :reversed}, read + 1, nacc + (value <<< (7 * read)))
|
||||||
|
end
|
||||||
|
def varint({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do
|
||||||
|
total = nacc + (value <<< (7 * read))
|
||||||
|
<<value::32-signed>> = <<total::32-unsigned>>
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
def varint(_, read, _) when read >= 5 do
|
||||||
|
raise RuntimeError, "Got a varint which is too big!"
|
||||||
|
end
|
||||||
|
def varint({_, ""}, _, _) do
|
||||||
|
raise RuntimeError, "Got an incomplete varint!"
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reads a varlong. `read` tracks the number of bytes read and `nacc` tracks the number being read.
|
||||||
|
"""
|
||||||
|
def varlong(tuple, read \\ 0, nacc \\ 0)
|
||||||
|
def varlong({acc, <<1::1, value::7, rest::binary>>, :reversed}, read, nacc) when read < 10 do
|
||||||
|
varlong({acc, rest, :reversed}, read + 1, nacc + (value <<< (7 * read)))
|
||||||
|
end
|
||||||
|
def varlong({acc, <<0::1, value::7, rest::binary>>, :reversed}, read, nacc) do
|
||||||
|
total = nacc + (value <<< (7 * read))
|
||||||
|
<<value::64-signed>> = <<total::64-unsigned>>
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
def varlong(_, read, _) when read >= 10 do
|
||||||
|
raise RuntimeError, "Got a varlong which is too big!"
|
||||||
|
end
|
||||||
|
def varlong({_, ""}, _, _) do
|
||||||
|
raise RuntimeError, "Got an incomplete varlong!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def string({acc, data, :reversed}) do
|
||||||
|
{[length], rest, :reversed} = start(data) |> varint()
|
||||||
|
<<value::binary-size(length), rest::binary>> = rest
|
||||||
|
{[value | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
def json({acc, data, :reversed}) do
|
||||||
|
{[value], rest, :reversed} = string({[], data, :reversed})
|
||||||
|
{[Jason.decode!(value) | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
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
|
end
|
||||||
|
336
apps/amethyst/lib/states/configuration.ex
Normal file
336
apps/amethyst/lib/states/configuration.ex
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
# 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.ConnectionState.Configuration do
|
||||||
|
require Amethyst.ConnectionState.Macros
|
||||||
|
alias Amethyst.ConnectionState.Macros
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the packets and logic for the Configuration state.
|
||||||
|
"""
|
||||||
|
Macros.defpacket_clientbound :cookie_request, 0x00, 767, [identifier: :string]
|
||||||
|
Macros.defpacket_clientbound :clientbound_plugin_message, 0x01, 767, [channel: :string, data: :raw]
|
||||||
|
Macros.defpacket_clientbound :disconnect, 0x02, 767, [reason: :nbt]
|
||||||
|
Macros.defpacket_clientbound :finish_configuration, 0x03, 767, []
|
||||||
|
Macros.defpacket_clientbound :clientbound_keep_alive, 0x04, 767, [id: :long]
|
||||||
|
Macros.defpacket_clientbound :ping, 0x05, 767, [id: :int]
|
||||||
|
Macros.defpacket_clientbound :reset_chat, 0x06, 767, []
|
||||||
|
Macros.defpacket_clientbound :registry_data, 0x07, 767, [
|
||||||
|
id: :string,
|
||||||
|
entries: {:array, [
|
||||||
|
id: :string,
|
||||||
|
data: {:optional, :nbt}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :remove_resource_pack, 0x08, 767, [
|
||||||
|
uuid: {:optional, :uuid}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :add_resource_pack, 0x09, 767, [
|
||||||
|
uuid: :uuid,
|
||||||
|
url: :string,
|
||||||
|
hash: :string,
|
||||||
|
forced: :bool,
|
||||||
|
prompt_message: {:optional, :string}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :store_cookie, 0x0A, 767, [identifier: :string, payload: :byte_array]
|
||||||
|
Macros.defpacket_clientbound :transfer, 0x0B, 767, [host: :string, port: :varint]
|
||||||
|
Macros.defpacket_clientbound :feature_flags, 0x0C, 767, [flags: {:array, [flag: :string]}]
|
||||||
|
Macros.defpacket_clientbound :update_tags, 0x0D, 767, [
|
||||||
|
tags: {:array, [
|
||||||
|
registry: :string,
|
||||||
|
tags: {:array, [
|
||||||
|
name: :string,
|
||||||
|
entries: {:array, [id: :varint]}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :clientbound_known_packs, 0x0E, 767, [
|
||||||
|
packs: {:array, [
|
||||||
|
namespace: :string,
|
||||||
|
id: :string,
|
||||||
|
version: :string
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :custom_report_details, 0x0F, 767, [
|
||||||
|
details: {:array, [
|
||||||
|
title: :string,
|
||||||
|
desctioption: :string
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :server_links, 0x10, 767, [
|
||||||
|
links: {:array, [
|
||||||
|
is_builtin: :bool,
|
||||||
|
label: :string,
|
||||||
|
url: :string
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
|
||||||
|
Macros.defpacket_serverbound :client_information, 0x00, 767, [
|
||||||
|
locale: :string,
|
||||||
|
view_distance: :byte,
|
||||||
|
chat_mode: :varint,
|
||||||
|
chat_colors: :bool,
|
||||||
|
displayed_skin_parts: :byte,
|
||||||
|
main_hand: :varint,
|
||||||
|
text_filtering: :bool,
|
||||||
|
allow_server_listings: :bool
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :cookie_response, 0x01, 767, [
|
||||||
|
key: :string,
|
||||||
|
payload: {:optional, :byte_array}
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :serverbound_plugin_message, 0x02, 767, [channel: :string, data: :raw]
|
||||||
|
Macros.defpacket_serverbound :acknowledge_finish_configuration, 0x03, 767, []
|
||||||
|
Macros.defpacket_serverbound :serverbound_keep_alive, 0x04, 767, [id: :long]
|
||||||
|
Macros.defpacket_serverbound :pong, 0x05, 767, [id: :int]
|
||||||
|
Macros.defpacket_serverbound :resource_pack_response, 0x06, 767, [uuid: :uuid, result: :varint]
|
||||||
|
Macros.defpacket_serverbound :serverbound_known_packs, 0x07, 767, [
|
||||||
|
packs: {:array, [
|
||||||
|
namespace: :string,
|
||||||
|
id: :string,
|
||||||
|
version: :string
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(%{packet_type: :serverbound_plugin_message, channel: "minecraft:brand", data: data}, 767, state) do
|
||||||
|
{[string], ""} = Amethyst.Minecraft.Read.start(data) |> Amethyst.Minecraft.Read.string() |> Amethyst.Minecraft.Read.stop()
|
||||||
|
Logger.debug("Received brand: #{string}")
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :clientbound_plugin_message,
|
||||||
|
channel: "minecraft:brand",
|
||||||
|
data: Amethyst.Minecraft.Write.string("Amethyst")
|
||||||
|
}})
|
||||||
|
state |> Map.put(:brand, string)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(%{
|
||||||
|
packet_type: :client_information,
|
||||||
|
locale: locale,
|
||||||
|
view_distance: view_distance,
|
||||||
|
chat_mode: chat_mode,
|
||||||
|
chat_colors: chat_colors,
|
||||||
|
displayed_skin_parts: displayed_skin_parts,
|
||||||
|
main_hand: main_hand,
|
||||||
|
text_filtering: text_filtering,
|
||||||
|
allow_server_listings: allow_server_listings
|
||||||
|
}, 767, state) do
|
||||||
|
Logger.debug("Received client information")
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :feature_flags,
|
||||||
|
flags: []
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :clientbound_known_packs,
|
||||||
|
packs: [%{namespace: "minecraft", id: "base", version: "1.21"}]
|
||||||
|
}})
|
||||||
|
state
|
||||||
|
|> Map.put(:locale, locale)
|
||||||
|
|> Map.put(:view_distance, view_distance)
|
||||||
|
|> Map.put(:chat_mode, chat_mode)
|
||||||
|
|> Map.put(:chat_colors, chat_colors)
|
||||||
|
|> Map.put(:displayed_skin_parts, displayed_skin_parts)
|
||||||
|
|> Map.put(:main_hand, main_hand)
|
||||||
|
|> Map.put(:text_filtering, text_filtering)
|
||||||
|
|> Map.put(:allow_server_listings, allow_server_listings)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(%{packet_type: :serverbound_known_packs, packs: _packs}, 767, _state) do
|
||||||
|
Logger.debug("Received known packs")
|
||||||
|
import Amethyst.NBT.Write
|
||||||
|
# TODO: Of course, the registries shouldn't be hard-coded
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :registry_data,
|
||||||
|
id: "minecraft:dimension_type",
|
||||||
|
entries: [
|
||||||
|
%{id: "amethyst:basic", data: compound(%{
|
||||||
|
"has_skylight" => byte(1),
|
||||||
|
"has_ceiling" => byte(0),
|
||||||
|
"ultrawarm" => byte(0),
|
||||||
|
"natural" => byte(1),
|
||||||
|
"coordinate_scale" => float(1.0),
|
||||||
|
"bed_works" => byte(1),
|
||||||
|
"respawn_anchor_works" => byte(1),
|
||||||
|
"min_y" => int(0),
|
||||||
|
"height" => int(256),
|
||||||
|
"logical_height" => int(256),
|
||||||
|
"infiniburn" => string("#"),
|
||||||
|
"effects" => string("minecraft:overworld"),
|
||||||
|
"ambient_light" => float(0.0),
|
||||||
|
"piglin_safe" => byte(0),
|
||||||
|
"has_raids" => byte(1),
|
||||||
|
"monster_spawn_light_level" => int(0),
|
||||||
|
"monster_spawn_block_light_limit" => int(0)
|
||||||
|
})}
|
||||||
|
]
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :registry_data,
|
||||||
|
id: "minecraft:painting_variant", entries: [
|
||||||
|
%{id: "minecraft:kebab", data: compound(%{
|
||||||
|
"asset_id" => string("minecraft:kebab"),
|
||||||
|
"height" => int(1),
|
||||||
|
"width" => int(1),
|
||||||
|
})}
|
||||||
|
]
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :registry_data,
|
||||||
|
id: "minecraft:wolf_variant",
|
||||||
|
entries: [
|
||||||
|
%{id: "minecraft:wolf_ashen", data: compound(%{
|
||||||
|
"wild_texture" => string("minecraft:entity/wolf/wolf_ashen"),
|
||||||
|
"tame_texture" => string("minecraft:entity/wolf/wolf_ashen_tame"),
|
||||||
|
"angry_texture" => string("minecraft:entity/wolf/wolf_ashen_angry"),
|
||||||
|
"biomes" => string("amethyst:basic"),
|
||||||
|
})}
|
||||||
|
]
|
||||||
|
}})
|
||||||
|
# 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),
|
||||||
|
"mood_sound" => compound(%{
|
||||||
|
"tick_delay" => int(6000),
|
||||||
|
"offset" => float(2.0),
|
||||||
|
"sound" => string("minecraft:ambient.cave"),
|
||||||
|
"block_search_extent" => int(8)
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
"has_precipitation" => byte(1),
|
||||||
|
"temperature" => float(0.8),
|
||||||
|
"downfall" => float(0.4),
|
||||||
|
})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :registry_data,
|
||||||
|
id: "minecraft:worldgen/biome",
|
||||||
|
entries: [
|
||||||
|
%{id: "amethyst:basic", data: basic_biome},
|
||||||
|
%{id: "minecraft:plains", data: basic_biome}
|
||||||
|
]
|
||||||
|
}})
|
||||||
|
# this game sucks
|
||||||
|
generic_damage = compound(%{
|
||||||
|
"scaling" => string("when_caused_by_living_non_player"),
|
||||||
|
"exhaustion" => float(0.0),
|
||||||
|
"message_id" => string("generic")
|
||||||
|
})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :registry_data,
|
||||||
|
id: "minecraft:damage_type",
|
||||||
|
entries: [
|
||||||
|
%{id: "minecraft:in_fire", data: generic_damage},
|
||||||
|
%{id: "minecraft:campfire", data: generic_damage},
|
||||||
|
%{id: "minecraft:lightning_bolt", data: generic_damage},
|
||||||
|
%{id: "minecraft:on_fire", data: generic_damage},
|
||||||
|
%{id: "minecraft:lava", data: generic_damage},
|
||||||
|
%{id: "minecraft:cramming", data: generic_damage},
|
||||||
|
%{id: "minecraft:drown", data: generic_damage},
|
||||||
|
%{id: "minecraft:starve", data: generic_damage},
|
||||||
|
%{id: "minecraft:cactus", data: generic_damage},
|
||||||
|
%{id: "minecraft:fall", data: generic_damage},
|
||||||
|
%{id: "minecraft:fly_into_wall", data: generic_damage},
|
||||||
|
%{id: "minecraft:out_of_world", data: generic_damage},
|
||||||
|
%{id: "minecraft:generic", data: generic_damage},
|
||||||
|
%{id: "minecraft:magic", data: generic_damage},
|
||||||
|
%{id: "minecraft:wither", data: generic_damage},
|
||||||
|
%{id: "minecraft:dragon_breath", data: generic_damage},
|
||||||
|
%{id: "minecraft:dry_out", data: generic_damage},
|
||||||
|
%{id: "minecraft:sweet_berry_bush", data: generic_damage},
|
||||||
|
%{id: "minecraft:freeze", data: generic_damage},
|
||||||
|
%{id: "minecraft:stalagmite", data: generic_damage},
|
||||||
|
%{id: "minecraft:outside_border", data: generic_damage},
|
||||||
|
%{id: "minecraft:generic_kill", data: generic_damage},
|
||||||
|
%{id: "minecraft:hot_floor", data: generic_damage},
|
||||||
|
%{id: "minecraft:in_wall", data: generic_damage},
|
||||||
|
]
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, %{packet_type: :finish_configuration}})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(%{packet_type: :acknowledge_finish_configuration}, 767, state) do
|
||||||
|
Logger.debug("Received acknowledge finish configuration")
|
||||||
|
send(self(), {:set_state, Amethyst.ConnectionState.Play})
|
||||||
|
|
||||||
|
game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create()
|
||||||
|
state = state |> Map.put(:game, game)
|
||||||
|
login = Amethyst.Game.login(game, state)
|
||||||
|
case login do
|
||||||
|
:reject ->
|
||||||
|
send(self(), {:disconnect, "Default game rejected connection"})
|
||||||
|
:ok
|
||||||
|
{:accept, {x, y, z}, {yaw, pitch}} ->
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :login,
|
||||||
|
entity_id: 0,
|
||||||
|
is_hardcore: false,
|
||||||
|
dimensions: [%{name: "minecraft:overworld"}],
|
||||||
|
max_players: 0,
|
||||||
|
view_distance: 16,
|
||||||
|
simulation_distance: 16,
|
||||||
|
reduced_debug_info: false,
|
||||||
|
enable_respawn_screen: true,
|
||||||
|
do_limited_crafting: false,
|
||||||
|
dimension_type: 0,
|
||||||
|
dimension_name: "minecraft:overworld",
|
||||||
|
hashed_seed: 0,
|
||||||
|
game_mode: 1,
|
||||||
|
previous_game_mode: -1,
|
||||||
|
is_debug: false,
|
||||||
|
is_flat: false,
|
||||||
|
death_location: nil,
|
||||||
|
portal_cooldown: 0,
|
||||||
|
enforces_secure_chat: false
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :synchronize_player_position,
|
||||||
|
x: x, y: y, z: z, yaw: yaw, pitch: pitch, teleport_id: 0, flags: 0x00
|
||||||
|
}})
|
||||||
|
send(self(), {:send_packet, Amethyst.ConnectionState.Play.ge_start_waiting_for_level_chunks(767)})
|
||||||
|
send(self(), {:send_packet, %{packet_type: :set_center_chunk,
|
||||||
|
chunk_x: div(floor(x), 16),
|
||||||
|
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()
|
||||||
|
pid = spawn(fn -> Amethyst.ConnectionState.Play.keepalive_loop(me) end)
|
||||||
|
state |> Map.put(:keepalive, pid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(reason) do
|
||||||
|
%{packet_type: :disconnect, reason: {:compound, %{
|
||||||
|
"text" => {:string, reason}
|
||||||
|
}}}
|
||||||
|
end
|
||||||
|
end
|
56
apps/amethyst/lib/states/handhsake.ex
Normal file
56
apps/amethyst/lib/states/handhsake.ex
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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.ConnectionState.Handshake do
|
||||||
|
require Amethyst.ConnectionState.Macros
|
||||||
|
alias Amethyst.ConnectionState.Macros
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the packets and logic for the Handshake state.
|
||||||
|
"""
|
||||||
|
# Notice that in the Handshake state, our version is always 0.
|
||||||
|
Macros.defpacket_serverbound :handshake, 0x00, 0, [
|
||||||
|
version: :varint,
|
||||||
|
address: :string,
|
||||||
|
port: :ushort,
|
||||||
|
next: :varint
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(%{packet_type: :handshake, version: 767, address: address, port: port, next: next}, 0, _state) do
|
||||||
|
Logger.debug("Received handshake for #{address}:#{port} with version 767")
|
||||||
|
case next do
|
||||||
|
1 ->
|
||||||
|
send(self(), {:set_state, Amethyst.ConnectionState.Status})
|
||||||
|
send(self(), {:set_version, 767})
|
||||||
|
:ok
|
||||||
|
2 ->
|
||||||
|
send(self(), {:set_state, Amethyst.ConnectionState.Login})
|
||||||
|
send(self(), {:set_version, 767})
|
||||||
|
:ok
|
||||||
|
3 ->
|
||||||
|
send(self(), {:set_state, Amethyst.ConnectionState.Transfer})
|
||||||
|
send(self(), {:set_version, 767})
|
||||||
|
:ok
|
||||||
|
_ -> {:error, "Invalid next state"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(_reason) do
|
||||||
|
# When disconnecting from the handshake state, we can't send any sort of reason.
|
||||||
|
end
|
||||||
|
end
|
152
apps/amethyst/lib/states/login.ex
Normal file
152
apps/amethyst/lib/states/login.ex
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# 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.ConnectionState.Login do
|
||||||
|
require Amethyst.ConnectionState.Macros
|
||||||
|
alias Amethyst.ConnectionState.Macros
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the packets and logic for the Login state.
|
||||||
|
"""
|
||||||
|
Macros.defpacket_clientbound :disconnect, 0x00, 767, [reason: :json]
|
||||||
|
Macros.defpacket_clientbound :encryption_request, 0x01, 767, [
|
||||||
|
server_id: :string,
|
||||||
|
public_key: :byte_array,
|
||||||
|
verify_token: :byte_array,
|
||||||
|
should_authenticate: :bool
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :login_success, 0x02, 767, [
|
||||||
|
uuid: :uuid,
|
||||||
|
username: :string,
|
||||||
|
properties: {:array, [
|
||||||
|
name: :string,
|
||||||
|
value: :string,
|
||||||
|
signature: {:optional, :string}
|
||||||
|
]},
|
||||||
|
strict_error_handling: :bool
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :set_compression, 0x03, 767, [threshold: :varint]
|
||||||
|
Macros.defpacket_clientbound :login_plugin_request, 0x04, 767, [
|
||||||
|
message_id: :varint,
|
||||||
|
channel: :string,
|
||||||
|
data: :raw
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :cookie_request, 0x05, 767, [identifier: :string]
|
||||||
|
|
||||||
|
Macros.defpacket_serverbound :login_start, 0x00, 767, [name: :string, player_uuid: :uuid]
|
||||||
|
Macros.defpacket_serverbound :encryption_response, 0x01, 767, [
|
||||||
|
shared_secret: :byte_array,
|
||||||
|
verify_token: :byte_array
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :login_plugin_response, 0x02, 767, [
|
||||||
|
message_id: :varint,
|
||||||
|
data: {:optional, :raw}
|
||||||
|
]
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
else
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :login_success,
|
||||||
|
uuid: player_uuid,
|
||||||
|
username: name,
|
||||||
|
properties: [],
|
||||||
|
strict_error_handling: true
|
||||||
|
}})
|
||||||
|
: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})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(reason) do
|
||||||
|
%{packet_type: :disconnect, reason:
|
||||||
|
%{
|
||||||
|
"text" => reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
178
apps/amethyst/lib/states/macros.ex
Normal file
178
apps/amethyst/lib/states/macros.ex
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
# 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
|
||||||
|
def deserialize(unquote(id), unquote(version), data) when unquote(where) do
|
||||||
|
{packet, ""} = Amethyst.ConnectionState.Macros.read_signature(data, unquote(signature))
|
||||||
|
packet |> Map.put(:packet_type, unquote(name))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
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)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alias Amethyst.Minecraft.Read
|
||||||
|
alias Amethyst.Minecraft.Write
|
||||||
|
|
||||||
|
def read_signature(data, signature) do
|
||||||
|
names = Enum.map(signature, fn {name, _type} -> name end)
|
||||||
|
{got, rest} = Enum.reduce(signature, Read.start(data), fn {_name, type}, {acc, rest, :reversed} ->
|
||||||
|
case type do
|
||||||
|
{:optional, {:compound, signature}} ->
|
||||||
|
{[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop()
|
||||||
|
if exists do
|
||||||
|
{item, rest} = read_signature(rest, signature)
|
||||||
|
{[item | acc], rest, :reversed}
|
||||||
|
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
|
||||||
|
apply(Read, t, [{acc, rest, :reversed}])
|
||||||
|
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
|
||||||
|
{[[] | acc], rest, :reversed}
|
||||||
|
else
|
||||||
|
{items, rest} = Enum.reduce(1..count, {[], rest}, fn _, {acc, rest} ->
|
||||||
|
{item, rest} = read_signature(rest, signature)
|
||||||
|
{[item | acc], rest}
|
||||||
|
end)
|
||||||
|
{[Enum.reverse(items) | acc], rest, :reversed}
|
||||||
|
end
|
||||||
|
t -> apply(Read, t, [{acc, rest, :reversed}])
|
||||||
|
end
|
||||||
|
end) |> Read.stop()
|
||||||
|
|
||||||
|
{Enum.zip(names, got) |> Map.new(), rest}
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_signature(packet, signature) do
|
||||||
|
Enum.reduce(signature, "", fn {name, type}, acc ->
|
||||||
|
case type do
|
||||||
|
{:optional, {:compound, signature}} ->
|
||||||
|
case Map.get(packet, name) do
|
||||||
|
nil -> acc <> Write.bool(false)
|
||||||
|
_ -> acc <> Write.bool(true) <> write_signature(Map.get(packet, name), signature)
|
||||||
|
end
|
||||||
|
{:optional, t} ->
|
||||||
|
case Map.get(packet, name) do
|
||||||
|
nil -> acc <> Write.bool(false)
|
||||||
|
_ -> acc <> Write.bool(true) <> apply(Write, t, [Map.get(packet, name)])
|
||||||
|
end
|
||||||
|
{:array, signature} ->
|
||||||
|
acc <> Write.varint(Enum.count(Map.get(packet, name))) <>
|
||||||
|
Enum.reduce(Map.get(packet, name), "", fn item, acc ->
|
||||||
|
acc <> write_signature(item, signature)
|
||||||
|
end)
|
||||||
|
{:literal, type, value} -> acc <> apply(Write, type, [value])
|
||||||
|
t -> acc <> apply(Write, t, [Map.get(packet, name)])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_type(packet, signature) do
|
||||||
|
try do
|
||||||
|
Enum.all?(signature, fn {name, type} ->
|
||||||
|
case Map.get(packet, name, :missing) do
|
||||||
|
:missing ->
|
||||||
|
if is_tuple(type) && elem(type, 0) == :literal do
|
||||||
|
true
|
||||||
|
else
|
||||||
|
throw {:missing, name}
|
||||||
|
end
|
||||||
|
value -> case type_matches(value, type) do
|
||||||
|
true -> true
|
||||||
|
false -> throw {:mismatch, name, value, type}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
catch
|
||||||
|
reason ->
|
||||||
|
Logger.debug("Found invalid packet type: #{inspect(reason)}")
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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, :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, :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(y) and y in -2048..2047 and
|
||||||
|
is_integer(z) and z in -33_554_432..33_554_431, 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
|
||||||
|
{:ok, _} -> true
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
283
apps/amethyst/lib/states/play.ex
Normal file
283
apps/amethyst/lib/states/play.ex
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
# 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.ConnectionState.Play do
|
||||||
|
require Amethyst.ConnectionState.Macros
|
||||||
|
alias Amethyst.ConnectionState.Macros
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the packets and logic for the Play state.
|
||||||
|
"""
|
||||||
|
Macros.defpacket_clientbound :disconnect, 0x1D, 767, [reason: :nbt]
|
||||||
|
Macros.defpacket_clientbound :keep_alive, 0x26, 767, [id: :long]
|
||||||
|
Macros.defpacket_clientbound :chunk_data_and_update_light, 0x27, 767, [
|
||||||
|
chunk_x: :int,
|
||||||
|
chunk_z: :int,
|
||||||
|
heightmaps: :nbt,
|
||||||
|
data: :byte_array,
|
||||||
|
block_entities: {:array, [
|
||||||
|
packed_xz: :ubyte, # TODO: This would be interesting to have in a clearer format
|
||||||
|
y: :short,
|
||||||
|
type: :varint,
|
||||||
|
data: :nbt
|
||||||
|
]},
|
||||||
|
sky_light_mask: :bitset,
|
||||||
|
block_light_mask: :bitset,
|
||||||
|
empty_sky_light_mask: :bitset,
|
||||||
|
empty_block_light_mask: :bitset,
|
||||||
|
sky_light_arrays: {:array, [
|
||||||
|
sky_light_array: :byte_array
|
||||||
|
]},
|
||||||
|
block_light_arrays: {:array, [
|
||||||
|
block_light_array: :byte_array
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :login, 0x2B, 767, [
|
||||||
|
entity_id: :int,
|
||||||
|
is_hardcore: :bool,
|
||||||
|
dimensions: {:array, [name: :string]},
|
||||||
|
max_players: :varint,
|
||||||
|
view_distance: :varint,
|
||||||
|
simulation_distance: :varint,
|
||||||
|
reduced_debug_info: :bool,
|
||||||
|
enable_respawn_screen: :bool,
|
||||||
|
do_limited_crafting: :bool,
|
||||||
|
dimension_type: :varint,
|
||||||
|
dimension_name: :string,
|
||||||
|
hashed_seed: :long,
|
||||||
|
game_mode: :ubyte,
|
||||||
|
previous_game_mode: :byte,
|
||||||
|
is_debug: :bool,
|
||||||
|
is_flat: :bool,
|
||||||
|
death_location: {:optional, {:compound, [
|
||||||
|
dimension: :string,
|
||||||
|
location: :pos
|
||||||
|
]}},
|
||||||
|
portal_cooldown: :varint,
|
||||||
|
enforces_secure_chat: :bool,
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_add_player, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x01},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
name: :string,
|
||||||
|
properties: {:array, [
|
||||||
|
name: :string,
|
||||||
|
value: :string,
|
||||||
|
signature: {:optional, :string},
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x02},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
data: {:optional, {:compound, [
|
||||||
|
chat_session_id: :uuid,
|
||||||
|
public_key_expiry_time: :long,
|
||||||
|
encoded_public_key: :byte_array,
|
||||||
|
public_key_signature: :byte_array
|
||||||
|
]}}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x04},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
gamemode: :varint
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_update_listed, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x08},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
listed: :bool
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_update_latency, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x10},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
ping: :varint, # Milliseconds
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :player_info_update_update_display_name, 0x3E, 767, [
|
||||||
|
actions: {:literal, :byte, 0x20},
|
||||||
|
players: {:array, [
|
||||||
|
uuid: :uuid,
|
||||||
|
display_name: {:optional, :nbt}
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
Macros.defpacket_clientbound :synchronize_player_position, 0x40, 767, [
|
||||||
|
x: :double,
|
||||||
|
y: :double,
|
||||||
|
z: :double,
|
||||||
|
yaw: :float,
|
||||||
|
pitch: :float,
|
||||||
|
flags: :byte,
|
||||||
|
teleport_id: :varint
|
||||||
|
]
|
||||||
|
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
|
||||||
|
]
|
||||||
|
# We can use functions to wrap over this packet and make it a bit clearer.
|
||||||
|
# Taking the protocol version here makes it less portable but whatever, fuck this packet
|
||||||
|
def ge_no_respawn_block_available(767), do: %{packet_type: :game_event, event: 0, value: 0}
|
||||||
|
def ge_begin_raining(767), do: %{packet_type: :game_event, event: 1, value: 0}
|
||||||
|
def ge_end_raining(767), do: %{packet_type: :game_event, event: 2, value: 0}
|
||||||
|
def ge_change_game_mode(767, gm) when is_integer(gm), do: %{packet_type: :game_event, event: 3, value: gm}
|
||||||
|
def ge_win_game(767, credits?) when is_integer(credits?), do: %{packet_type: :game_event, event: 4, value: credits?}
|
||||||
|
def ge_game_event(767, event) when is_integer(event), do: %{packet_type: :game_event, event: 5, value: event}
|
||||||
|
def ge_arrow_hit_player(767), do: %{packet_type: :game_event, event: 6, value: 0}
|
||||||
|
def ge_rain_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 7, value: value}
|
||||||
|
def ge_thunder_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 8, value: value}
|
||||||
|
def ge_play_pufferfish_sting_sound(767), do: %{packet_type: :game_event, event: 9, value: 0}
|
||||||
|
def ge_play_elder_guardian_mob_appearance(767), do: %{packet_type: :game_event, event: 10, value: 0}
|
||||||
|
def ge_enable_respawn_screen(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 11, value: enabled?}
|
||||||
|
def ge_limited_crafting(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 12, value: enabled?}
|
||||||
|
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, [
|
||||||
|
x: :double,
|
||||||
|
feet_y: :double,
|
||||||
|
z: :double,
|
||||||
|
on_ground: :bool
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :set_player_position_and_rotation, 0x1B, 767, [
|
||||||
|
x: :double,
|
||||||
|
feet_y: :double,
|
||||||
|
z: :double,
|
||||||
|
yaw: :float,
|
||||||
|
pitch: :float,
|
||||||
|
on_ground: :bool
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :set_player_rotation, 0x1C, 767, [
|
||||||
|
yaw: :float,
|
||||||
|
pitch: :float,
|
||||||
|
on_ground: :bool # I don't understand their obsession with this...
|
||||||
|
]
|
||||||
|
Macros.defpacket_serverbound :set_player_on_ground, 0x1D, 767, [on_ground: :bool]
|
||||||
|
Macros.defpacket_serverbound :player_command, 0x25, 767, [eid: :varint, action_id: :varint, jump_boost: :varint]
|
||||||
|
|
||||||
|
def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do
|
||||||
|
Amethyst.Game.accept_teleport(state[:game], id)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(%{packet_type: :set_player_position_and_rotation, x: x, feet_y: y, z: z, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do
|
||||||
|
# I don't know why we would ever trust on_ground here... the server computes that
|
||||||
|
Amethyst.Game.player_position(state[:game], {x, y, z})
|
||||||
|
Amethyst.Game.player_rotation(state[:game], {yaw, pitch})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
def handle(%{packet_type: :serverbound_plugin_message, channel: channel, data: _}, 767, _state) do
|
||||||
|
Logger.debug("Got plugin message on #{channel}")
|
||||||
|
end
|
||||||
|
def handle(%{packet_type: :set_player_position, x: x, feet_y: y, z: z, on_ground: _ground}, 767, state) do
|
||||||
|
# I don't know why we would ever trust on_ground here... the server computes that
|
||||||
|
Amethyst.Game.player_position(state[:game], {x, y, z})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
def handle(%{packet_type: :set_player_rotation, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do
|
||||||
|
# I don't know why we would ever trust on_ground here... the server computes that
|
||||||
|
Amethyst.Game.player_rotation(state[:game], {yaw, pitch})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
def handle(%{packet_type: :set_player_on_ground, on_ground: _}, 767, _state) do
|
||||||
|
:ok # Again, don't trust the client for something we can compute
|
||||||
|
end
|
||||||
|
def handle(%{packet_type: :player_command, eid: _eid, action_id: aid, jump_boost: _horse_jump}, 767, _state) do
|
||||||
|
# TODO: Actually handle these events
|
||||||
|
case aid do
|
||||||
|
0 -> # Start sneaking
|
||||||
|
:ok
|
||||||
|
1 -> # Stop sneaking
|
||||||
|
:ok
|
||||||
|
2 -> # Leave bed
|
||||||
|
:ok
|
||||||
|
3 -> # Start sprinting
|
||||||
|
:ok
|
||||||
|
4 -> # Stop sprinting
|
||||||
|
:ok
|
||||||
|
5 -> # Start horse jump
|
||||||
|
:ok
|
||||||
|
6 -> # Stop horse jump
|
||||||
|
:ok
|
||||||
|
7 -> # Open vehicle inventory
|
||||||
|
:ok
|
||||||
|
8 -> # Start elytra flying
|
||||||
|
:ok
|
||||||
|
_ -> raise RuntimeError, "Unknown Player Command Action ID"
|
||||||
|
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})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
# This function should be started on a new task under the connection handler
|
||||||
|
# and is responsible for keepalive logic.
|
||||||
|
def keepalive_loop(player) do
|
||||||
|
Process.link(player) # Is it fine to do this on loop?
|
||||||
|
<<id::32>> = :rand.bytes(4)
|
||||||
|
send(player, {:send_packet, %{packet_type: :keep_alive, id: id}})
|
||||||
|
receive do
|
||||||
|
{:respond, ^id} ->
|
||||||
|
:timer.sleep(250)
|
||||||
|
keepalive_loop(player)
|
||||||
|
after
|
||||||
|
15_000 ->
|
||||||
|
send(player, {:disconnect, "Timed out! Connection overloaded?"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(reason) do
|
||||||
|
%{
|
||||||
|
packet_type: :disconnect,
|
||||||
|
reason: {:compound, %{
|
||||||
|
"text" => {:string, reason}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
60
apps/amethyst/lib/states/status.ex
Normal file
60
apps/amethyst/lib/states/status.ex
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# 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.ConnectionState.Status do
|
||||||
|
require Amethyst.ConnectionState.Macros
|
||||||
|
alias Amethyst.ConnectionState.Macros
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module contains the packets and logic for the Status state.
|
||||||
|
"""
|
||||||
|
Macros.defpacket_clientbound :status_response, 0x00, 767, [json_response: :string]
|
||||||
|
Macros.defpacket_clientbound :pong_response, 0x01, 767, [payload: :long]
|
||||||
|
|
||||||
|
Macros.defpacket_serverbound :status_request, 0x00, 767, []
|
||||||
|
Macros.defpacket_serverbound :ping_request, 0x01, 767, [payload: :long]
|
||||||
|
|
||||||
|
def handle(%{packet_type: :status_request}, 767, _state) do
|
||||||
|
Logger.debug("Received status request")
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :status_response,
|
||||||
|
json_response: ~s({
|
||||||
|
"version": {"name": "1.21", "protocol": 767},
|
||||||
|
"players": {"max": -1, "online": 69, "sample": [{"name": "§dAmethyst§r", "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"}]},
|
||||||
|
"description": {"text":"Amethyst is an experimental server written in Elixir"},
|
||||||
|
"enforcesSecureChat": false,
|
||||||
|
"previewsChat": false,
|
||||||
|
"preventsChatReports": true
|
||||||
|
})
|
||||||
|
}})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle(%{packet_type: :ping_request, payload: payload}, 767, _state) do
|
||||||
|
Logger.debug("Received ping request")
|
||||||
|
send(self(), {:send_packet, %{
|
||||||
|
packet_type: :pong_response,
|
||||||
|
payload: payload
|
||||||
|
}})
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(_reason) do
|
||||||
|
# When disconnecting from the status state, we can't send any sort of reason.
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user