Begin complete rewrite of communication system
All checks were successful
Build & Test / nix-build (push) Successful in 1m31s
All checks were successful
Build & Test / nix-build (push) Successful in 1m31s
This commit is contained in:
parent
9842195b8e
commit
5413708b29
@ -24,7 +24,7 @@ defmodule Amethyst.Application do
|
|||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = [
|
||||||
{Task.Supervisor, name: Amethyst.ConnectionSupervisor},
|
{DynamicSupervisor, name: Amethyst.ConnectionSupervisor},
|
||||||
{Amethyst.Keys, 1024},
|
{Amethyst.Keys, 1024},
|
||||||
{Amethyst.GameCoordinator, %Amethyst.GameCoordinator.State{games: %{}, gid: 0}},
|
{Amethyst.GameCoordinator, %Amethyst.GameCoordinator.State{games: %{}, gid: 0}},
|
||||||
{PartitionSupervisor,
|
{PartitionSupervisor,
|
||||||
|
110
apps/amethyst/lib/apps/connection_handler.ex
Normal file
110
apps/amethyst/lib/apps/connection_handler.ex
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# 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.ConnectionHandler do
|
||||||
|
@moduledoc """
|
||||||
|
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 ElixirSense.Log
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
|
||||||
|
def child_spec(socket) do
|
||||||
|
%{
|
||||||
|
id: __MODULE__,
|
||||||
|
start: {__MODULE__, :start, [socket, Amethyst.ConnectionState.Handshake, 0]}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec start(:gen_tcp.socket(), atom(), pos_integer()) :: no_return()
|
||||||
|
def start(socket, connstate, version) do
|
||||||
|
{:ok, spawn(fn ->
|
||||||
|
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
||||||
|
loop(socket, connstate, version)
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec start_link(:gen_tcp.socket(), atom(), pos_integer()) :: no_return()
|
||||||
|
def start_link(socket, connstate, version) do
|
||||||
|
{:ok, spawn_link(fn ->
|
||||||
|
Process.set_label("ConnectionHandler for #{inspect(socket)}")
|
||||||
|
loop(socket, connstate, version)
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec loop(:gen_tcp.socket(), atom(), pos_integer()) :: no_return()
|
||||||
|
defp loop(socket, connstate, version) do
|
||||||
|
receive do
|
||||||
|
:closed ->
|
||||||
|
Logger.info("Connection #{inspect(socket)} closed.")
|
||||||
|
Process.exit(self(), :normal)
|
||||||
|
{:disconnect, reason} ->
|
||||||
|
disconnect(socket, reason, connstate, version)
|
||||||
|
Process.exit(self(), :normal)
|
||||||
|
{:set_state, newstate} ->
|
||||||
|
Logger.debug("Switching to state #{newstate} from #{connstate}")
|
||||||
|
loop(socket, newstate, version)
|
||||||
|
{:set_version, newversion} ->
|
||||||
|
Logger.debug("Switching to version #{newversion} from #{version}")
|
||||||
|
loop(socket, connstate, newversion)
|
||||||
|
{:send_packet, packet} ->
|
||||||
|
Logger.debug("Sending packet #{inspect(packet)}")
|
||||||
|
send_packet(socket, connstate, packet, version)
|
||||||
|
loop(socket, connstate, version)
|
||||||
|
after 0 ->
|
||||||
|
receive do
|
||||||
|
{:packet, id, data} ->
|
||||||
|
handle_packet(id, data, connstate, version)
|
||||||
|
loop(socket, connstate, version)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_packet(id, data, connstate, version) do
|
||||||
|
try do
|
||||||
|
packet = connstate.deserialize(id, version, data)
|
||||||
|
case connstate.handle(packet, version) do
|
||||||
|
:ok -> :ok
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("Error handling packet with ID #{id} in state #{connstate}: #{reason}")
|
||||||
|
send(self(), {:disconnect, "Error handling packet #{id}: #{reason}"})
|
||||||
|
_ ->
|
||||||
|
Logger.warn("Unknown return value from handle_packet for packet #{id} in state #{connstate}")
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
Logger.error("Error handling packet with ID #{id} in state #{connstate}: #{Exception.format(:error, e, __STACKTRACE__)}")
|
||||||
|
send(self(), {:disconnect, "Error handling packet #{id}: #{Exception.format(:error, e, __STACKTRACE__)}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_packet(socket, connstate, packet, version) do
|
||||||
|
data = connstate.serialize(packet, version)
|
||||||
|
length = byte_size(data) |> Amethyst.Minecraft.Write.varint()
|
||||||
|
:gen_tcp.send(socket, length <> data)
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
87
apps/amethyst/lib/apps/connection_receiver.ex
Normal file
87
apps/amethyst/lib/apps/connection_receiver.ex
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# 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.ConnectionReceiver do
|
||||||
|
@moduledoc """
|
||||||
|
This module waits for data incoming through a TCP connection, reads entire packets at a time and sends them to the handler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
alias Amethyst.Minecraft.Read
|
||||||
|
|
||||||
|
@spec child_spec(:gen_tcp.socket()) :: Supervisor.child_spec()
|
||||||
|
def child_spec(socket) do
|
||||||
|
%{
|
||||||
|
id: __MODULE__,
|
||||||
|
start: {__MODULE__, :start, [socket]}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec start(:gen_tcp.socket()) :: no_return()
|
||||||
|
def start(socket) 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)
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec start_link(:gen_tcp.socket()) :: no_return()
|
||||||
|
def start_link(socket) 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)
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@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
|
||||||
|
receive(socket, sender)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_packet(client) do
|
||||||
|
case get_varint(client, "") do
|
||||||
|
:closed -> :closed
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
{[length], ""} ->
|
||||||
|
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) do
|
||||||
|
case :gen_tcp.recv(client, 1) do
|
||||||
|
{: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
|
||||||
|
end
|
||||||
|
end
|
@ -29,7 +29,7 @@ defmodule Amethyst.TCPListener do
|
|||||||
@spec loop_acceptor(socket :: :gen_tcp.socket()) :: no_return()
|
@spec loop_acceptor(socket :: :gen_tcp.socket()) :: no_return()
|
||||||
defp loop_acceptor(socket) do
|
defp loop_acceptor(socket) do
|
||||||
{:ok, client} = :gen_tcp.accept(socket)
|
{:ok, client} = :gen_tcp.accept(socket)
|
||||||
{:ok, pid} = Task.Supervisor.start_child(Amethyst.ConnectionSupervisor, fn -> Amethyst.Server.Handshake.serve(client) end)
|
{:ok, pid} = DynamicSupervisor.start_child(Amethyst.ConnectionSupervisor, {Amethyst.ConnectionReceiver, client})
|
||||||
:ok = :gen_tcp.controlling_process(client, pid)
|
:ok = :gen_tcp.controlling_process(client, pid)
|
||||||
Logger.info("Received connection from #{inspect(client)}, assigned to PID #{inspect(pid)}")
|
Logger.info("Received connection from #{inspect(client)}, assigned to PID #{inspect(pid)}")
|
||||||
loop_acceptor(socket)
|
loop_acceptor(socket)
|
||||||
|
@ -35,6 +35,10 @@ defmodule Amethyst.Minecraft.Write do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def raw(value) do
|
||||||
|
value
|
||||||
|
end
|
||||||
|
|
||||||
def byte(value) when value in -128..127 do
|
def byte(value) when value in -128..127 do
|
||||||
<<value::8-signed-big>>
|
<<value::8-signed-big>>
|
||||||
end
|
end
|
||||||
@ -98,6 +102,14 @@ defmodule Amethyst.Minecraft.Write do
|
|||||||
<<varint(byte_size(value))::binary, value::binary>>
|
<<varint(byte_size(value))::binary, value::binary>>
|
||||||
end
|
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
|
def position({x, y, z}) do
|
||||||
<<x::signed-big-26, z::signed-big-26, y::signed-big-12>>
|
<<x::signed-big-26, z::signed-big-26, y::signed-big-12>>
|
||||||
end
|
end
|
||||||
@ -243,4 +255,8 @@ defmodule Amethyst.Minecraft.Read do
|
|||||||
<<value::binary-size(length), rest::binary>> = rest
|
<<value::binary-size(length), rest::binary>> = rest
|
||||||
{[value | acc], rest, :reversed}
|
{[value | acc], rest, :reversed}
|
||||||
end
|
end
|
||||||
|
def json({acc, data, :reversed}) do
|
||||||
|
{[value], rest, :reversed} = string({[], data, :reversed})
|
||||||
|
{[Jason.decode!(value) | acc], rest, :reversed}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,361 +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.Server.Configuration do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains the logic for the Configuration stage of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
use Amethyst.Server
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
alias Amethyst.Minecraft.Write
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(state) do
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
## DESERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Client Information https://wiki.vg/Protocol#Client_Information
|
|
||||||
def deserialize(0x00, data) do
|
|
||||||
{[locale, view_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing], ""} =
|
|
||||||
Read.start(data) |> Read.string |> Read.byte |> Read.varint |> Read.bool |> Read.ubyte |> Read.varint |> Read.bool |> Read.bool |> Read.stop
|
|
||||||
chat_mode = case chat_mode do
|
|
||||||
0 -> :enabled
|
|
||||||
1 -> :commands_only
|
|
||||||
2 -> :hidden
|
|
||||||
_ -> raise RuntimeError, "Unknown chat mode #{chat_mode}"
|
|
||||||
end
|
|
||||||
main_hand = case main_hand do
|
|
||||||
0 -> :left
|
|
||||||
1 -> :right
|
|
||||||
_ -> raise RuntimeError, "Unknown main hand #{main_hand}"
|
|
||||||
end
|
|
||||||
{:client_information, locale, view_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing}
|
|
||||||
end
|
|
||||||
# Cookie Response https://wiki.vg/Protocol#Cookie_Response_(configuration)
|
|
||||||
def deserialize(0x01, data) do
|
|
||||||
{[key, exists], rest} = Read.start(data) |> Read.string |> Read.bool |> Read.stop
|
|
||||||
if exists do
|
|
||||||
{[length], rest} = Read.start(rest) |> Read.varint |> Read.stop
|
|
||||||
{[data], _} = Read.start(rest) |> Read.raw(length) |> Read.stop
|
|
||||||
{:cookie_response, key, data}
|
|
||||||
else
|
|
||||||
{:cookie_response, key, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration)
|
|
||||||
def deserialize(0x02, data) do
|
|
||||||
{[channel], rest} = Read.start(data) |> Read.string |> Read.stop
|
|
||||||
{:serverbound_plugin_message, channel, rest}
|
|
||||||
end
|
|
||||||
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
|
||||||
def deserialize(0x03, "") do
|
|
||||||
{:acknowledge_finish_configuration}
|
|
||||||
end
|
|
||||||
# Serverbound Keep Alive https://wiki.vg/Protocol#Serverbound_Keep_Alive_(configuration)
|
|
||||||
def deserialize(0x04, data) do
|
|
||||||
{[id], ""} = Read.start(data) |> Read.long |> Read.stop
|
|
||||||
{:serverbound_keep_alive, id}
|
|
||||||
end
|
|
||||||
# Pong https://wiki.vg/Protocol#Pong_(configuration)
|
|
||||||
def deserialize(0x05, data) do
|
|
||||||
{[id], ""} = Read.start(data) |> Read.int |> Read.stop
|
|
||||||
{:pong, id}
|
|
||||||
end
|
|
||||||
# Resource Pack Response https://wiki.vg/Protocol#Resource_Pack_Response_(configuration)
|
|
||||||
def deserialize(0x06, data) do
|
|
||||||
{[uuid, result], ""} = Read.start(data) |> Read.uuid |> Read.varint |> Read.stop
|
|
||||||
result = case result do
|
|
||||||
0 -> :successfully_downloaded
|
|
||||||
1 -> :declined
|
|
||||||
2 -> :failed_to_download
|
|
||||||
3 -> :accepted
|
|
||||||
4 -> :downloaded
|
|
||||||
5 -> :invalid_url
|
|
||||||
6 -> :failed_to_reload
|
|
||||||
7 -> :discarded
|
|
||||||
_ -> raise RuntimeError, "Unknown resource pack response #{result}"
|
|
||||||
end
|
|
||||||
{:resource_pack_response, uuid, result}
|
|
||||||
end
|
|
||||||
# Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs
|
|
||||||
def deserialize(0x07, data) do
|
|
||||||
{[count], rest} = Read.start(data) |> Read.varint |> Read.stop
|
|
||||||
{packs, _} = Enum.reduce(1..count, {[], rest}, fn _, {acc, rest} ->
|
|
||||||
{[namespace, id, version], rest} = Read.start(rest) |> Read.string |> Read.string |> Read.string |> Read.stop
|
|
||||||
{[{namespace, id, version} | acc], rest}
|
|
||||||
end)
|
|
||||||
{:serverbound_known_packs, packs}
|
|
||||||
end
|
|
||||||
def deserialize(type, _) do
|
|
||||||
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## SERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Cookie Request https://wiki.vg/Protocol#Cookie_Request_(configuration)
|
|
||||||
def serialize({:cookie_request, id}) do
|
|
||||||
Write.varint(0x00) <> Write.string(id)
|
|
||||||
end
|
|
||||||
# Clientbound Plugin Message https://wiki.vg/Protocol#Clientbound_Plugin_Message_(configuration)
|
|
||||||
def serialize({:clientbound_plugin_message, channel, data}) do
|
|
||||||
Write.varint(0x01) <> Write.string(channel) <> data
|
|
||||||
end
|
|
||||||
# Disconnect https://wiki.vg/Protocol#Disconnect_(configuration)
|
|
||||||
def serialize({:disconnect, reason}) do
|
|
||||||
Write.varint(0x02) <> Write.string(reason)
|
|
||||||
end
|
|
||||||
# Finish Configuration https://wiki.vg/Protocol#Finish_Configuration
|
|
||||||
def serialize({:finish_configuration}) do
|
|
||||||
Write.varint(0x03)
|
|
||||||
end
|
|
||||||
# Clientbound Keep Alive https://wiki.vg/Protocol#Clientbound_Keep_Alive_(configuration)
|
|
||||||
def serialize({:clientbound_keep_alive, id}) do
|
|
||||||
Write.varint(0x04) <> <<id::64-big-signed>>
|
|
||||||
end
|
|
||||||
# Ping https://wiki.vg/Protocol#Ping_(configuration)
|
|
||||||
def serialize({:ping, id}) do
|
|
||||||
Write.varint(0x05) <> <<id::32-big-signed>>
|
|
||||||
end
|
|
||||||
# Reset Chat https://wiki.vg/Protocol#Reset_Chat
|
|
||||||
def serialize({:reset_chat}) do
|
|
||||||
Write.varint(0x06)
|
|
||||||
end
|
|
||||||
# Registry Data https://wiki.vg/Protocol#Registry_Data
|
|
||||||
def serialize({:registry_data, id, entries}) do
|
|
||||||
Write.varint(0x07) <> Write.string(id) <> Write.varint(length(entries)) <> Enum.map_join(entries, "", fn {name, nbt} ->
|
|
||||||
Write.string(name) <>
|
|
||||||
if nbt == nil do
|
|
||||||
Write.bool(false)
|
|
||||||
else
|
|
||||||
Write.bool(true) <> Amethyst.NBT.Write.write_net(nbt)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
# Remove Resource Pack https://wiki.vg/Protocol#Remove_Resource_Pack_(configuration)
|
|
||||||
def serialize({:remove_resource_pack, id}) do
|
|
||||||
Write.option(id, &Write.string/1)
|
|
||||||
end
|
|
||||||
# Add Resource Pack https://wiki.vg/Protocol#Add_Resource_Pack_(configuration)
|
|
||||||
def serialize({:add_resource_pack, id, url, hash, forced, msg}) do
|
|
||||||
Write.varint(0x09) <> Write.string(id) <> Write.string(url) <> Write.string(hash) <>
|
|
||||||
Write.bool(forced) <> Write.option(msg, &Write.string/1)
|
|
||||||
end
|
|
||||||
# Store Cookie https://wiki.vg/Protocol#Store_Cookie_(configuration)
|
|
||||||
def serialize({:store_cookie, id, data}) do
|
|
||||||
Write.varint(0x0A) <> Write.string(id) <> Write.string(data)
|
|
||||||
end
|
|
||||||
# Transfer https://wiki.vg/Protocol#Transfer_(configuration)
|
|
||||||
def serialize({:transfer, addr, port}) do
|
|
||||||
Write.varint(0x0B) <> Write.string(addr) <> Write.varint(port)
|
|
||||||
end
|
|
||||||
# Feature Flags https://wiki.vg/Protocol#Feature_Flags
|
|
||||||
def serialize({:feature_flags, flags}) do
|
|
||||||
Write.varint(0x0C) <> Write.varint(length(flags)) <> Write.list(flags, &Write.string/1)
|
|
||||||
end
|
|
||||||
# Update Tags https://wiki.vg/Protocol#Update_Tags
|
|
||||||
def serialize({:update_tags, tags}) do
|
|
||||||
Write.varint(0x0D) <> Write.varint(length(tags)) <>
|
|
||||||
Enum.reduce(tags, "", &serialize_tag/2)
|
|
||||||
end
|
|
||||||
# Clientbound Known Packs https://wiki.vg/Protocol#Clientbound_Known_Packs
|
|
||||||
def serialize({:clientbound_known_packs, packs}) do
|
|
||||||
Write.varint(0x0E) <> Write.varint(length(packs)) <>
|
|
||||||
Write.list(packs, fn {namespace, id, version} -> Write.string(namespace) <> Write.string(id) <> Write.string(version) end)
|
|
||||||
end
|
|
||||||
# Custom Report Details https://wiki.vg/Protocol#Custom_Report_Details
|
|
||||||
def serialize({:custom_report_details, details}) do
|
|
||||||
Write.varint(0x0F) <> Write.varint(length(details)) <>
|
|
||||||
Write.list(details, fn {id, data} -> Write.string(id) <> Write.string(data) end)
|
|
||||||
end
|
|
||||||
# Server Links https://wiki.vg/Protocol#Server_Links_(configuration)
|
|
||||||
def serialize({:server_links, links}) do
|
|
||||||
Write.varint(0x10) <> Write.varint(length(links)) <>
|
|
||||||
Write.list(links, fn {label, url} -> serialize_link_label(label) <> Write.string(url) end)
|
|
||||||
end
|
|
||||||
def serialize(packet) do
|
|
||||||
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
defp serialize_tag({id, elements}, acc) do
|
|
||||||
acc <> Write.string(id) <> Write.varint(length(elements)) <> serialize_elements(elements)
|
|
||||||
end
|
|
||||||
defp serialize_elements(elements) do
|
|
||||||
Write.list(elements, fn {id, ids} ->
|
|
||||||
Write.string(id) <> Write.varint(length(ids)) <>
|
|
||||||
Write.list(ids, &Write.varint/1)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:bug_report) do
|
|
||||||
<<0x01>> <> Write.varint(0x00)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:community_guidelines) do
|
|
||||||
<<0x01>> <> Write.varint(0x01)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:support) do
|
|
||||||
<<0x01>> <> Write.varint(0x02)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:status) do
|
|
||||||
<<0x01>> <> Write.varint(0x03)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:feedback) do
|
|
||||||
<<0x01>> <> Write.varint(0x04)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:community) do
|
|
||||||
<<0x01>> <> Write.varint(0x05)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:website) do
|
|
||||||
<<0x01>> <> Write.varint(0x06)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:forums) do
|
|
||||||
<<0x01>> <> Write.varint(0x07)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:news) do
|
|
||||||
<<0x01>> <> Write.varint(0x08)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(:announcements) do
|
|
||||||
<<0x01>> <> Write.varint(0x09)
|
|
||||||
end
|
|
||||||
defp serialize_link_label(other) do
|
|
||||||
<<0x00>> <> Write.string(other)
|
|
||||||
end
|
|
||||||
|
|
||||||
## HANDLING
|
|
||||||
@impl true
|
|
||||||
# Client Information https://wiki.vg/Protocol#Client_Information
|
|
||||||
def handle({:client_information, locale, v_dist, chat_mode, chat_colors, displayed_skin_parts, main_hand, text_filtering, allow_listing}, client, state) do
|
|
||||||
state = state |> Keyword.put(:locale, locale) |> Keyword.put(:view_dist, v_dist) |> Keyword.put(:chat_mode, chat_mode) |>
|
|
||||||
Keyword.put(:chat_colors, chat_colors) |> Keyword.put(:displayed_skin_parts, displayed_skin_parts) |> Keyword.put(:main_hand, main_hand) |>
|
|
||||||
Keyword.put(:text_filtering, text_filtering) |> Keyword.put(:allow_listing, allow_listing)
|
|
||||||
# TODO: Here we should create the game handling task for this player and give it
|
|
||||||
# this data.
|
|
||||||
transmit({:clientbound_plugin_message, "minecraft:brand", Write.string("amethyst")}, client)
|
|
||||||
transmit({:clientbound_known_packs, [{"minecraft", "core", "1.21"}, {"minecraft", "dimension_type", "1.21"}]}, client)
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
# Serverbound Known Packs https://wiki.vg/Protocol#Serverbound_Known_Packs
|
|
||||||
def handle({:serverbound_known_packs, _packs}, client, state) do
|
|
||||||
# L + ratio + don't care + didn't ask + finish configuration
|
|
||||||
import Amethyst.NBT.Write
|
|
||||||
# TODO: This shouldn't be hard-coded but we obviously don't know what we will need until we have game handling
|
|
||||||
# This can at least be followed as a "minimum" of what we need to send for the client not to complain
|
|
||||||
transmit({:registry_data, "minecraft:dimension_type", [{"amethyst:basic", 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)
|
|
||||||
})}]}, client)
|
|
||||||
transmit({:registry_data, "minecraft:painting_variant", [{"minecraft:kebab", compound(%{
|
|
||||||
"asset_id" => string("minecraft:kebab"),
|
|
||||||
"height" => int(1),
|
|
||||||
"width" => int(1),
|
|
||||||
})}]}, client)
|
|
||||||
transmit({:registry_data, "minecraft:wolf_variant", [{"minecraft:wolf_ashen", 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"),
|
|
||||||
})}]}, client)
|
|
||||||
# https://gist.github.com/WinX64/ab8c7a8df797c273b32d3a3b66522906 minecraft:plains
|
|
||||||
basic_biome = compound(%{
|
|
||||||
"effects" => compound(%{
|
|
||||||
"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),
|
|
||||||
"sound" => string("minecraft:ambient.cave"),
|
|
||||||
"block_search_extent" => int(8)
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
"has_precipitation" => byte(1),
|
|
||||||
"temperature" => float(0.8),
|
|
||||||
"downfall" => float(0.4),
|
|
||||||
})
|
|
||||||
transmit({:registry_data, "minecraft:worldgen/biome", [
|
|
||||||
{"amethyst:basic", basic_biome}, {"minecraft:plains", basic_biome}
|
|
||||||
]}, client)
|
|
||||||
# holy fucking shit
|
|
||||||
generic_damage = compound(%{
|
|
||||||
"scaling" => string("when_caused_by_living_non_player"),
|
|
||||||
"exhaustion" => float(0.0),
|
|
||||||
"message_id" => string("generic")
|
|
||||||
})
|
|
||||||
transmit({:registry_data, "minecraft:damage_type", [
|
|
||||||
{"minecraft:in_fire", generic_damage}, {"minecraft:campfire", generic_damage}, {"minecraft:lightning_bolt", generic_damage},
|
|
||||||
{"minecraft:on_fire", generic_damage}, {"minecraft:lava", generic_damage}, {"minecraft:hot_floor", generic_damage},
|
|
||||||
{"minecraft:in_wall", generic_damage}, {"minecraft:cramming", generic_damage}, {"minecraft:drown", generic_damage},
|
|
||||||
{"minecraft:starve", generic_damage}, {"minecraft:cactus", generic_damage}, {"minecraft:fall", generic_damage},
|
|
||||||
{"minecraft:fly_into_wall", generic_damage}, {"minecraft:out_of_world", generic_damage}, {"minecraft:generic", generic_damage},
|
|
||||||
{"minecraft:magic", generic_damage}, {"minecraft:wither", generic_damage}, {"minecraft:dragon_breath", generic_damage},
|
|
||||||
{"minecraft:dry_out", generic_damage}, {"minecraft:sweet_berry_bush", generic_damage}, {"minecraft:freeze", generic_damage},
|
|
||||||
{"minecraft:stalagmite", generic_damage}, {"minecraft:outside_border", generic_damage}, {"minecraft:generic_kill", generic_damage},
|
|
||||||
]}, client)
|
|
||||||
transmit({:finish_configuration}, client)
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
|
||||||
def handle({:acknowledge_finish_configuration}, client, state) do
|
|
||||||
game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create()
|
|
||||||
state = Keyword.put(state, :game, game)
|
|
||||||
if Amethyst.API.Game.login(game, state) == :reject do
|
|
||||||
raise RuntimeError, "Default game rejected login!"
|
|
||||||
else
|
|
||||||
# TODO: All of this stuff should obviously not be hardcoded here
|
|
||||||
Amethyst.Server.Play.transmit({:login,
|
|
||||||
0, false, ["minecraft:overworld"], 0, 16, 16, false, true, true, 0,
|
|
||||||
"minecraft:overworld", <<0::64>>, :spectator, nil, false, true, nil, 0, false
|
|
||||||
}, client)
|
|
||||||
Amethyst.Server.Play.serve(client, state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration)
|
|
||||||
def handle({:serverbound_plugin_message, channel, data}, client, state) do
|
|
||||||
handle_plugin_message(channel, data, client, state)
|
|
||||||
end
|
|
||||||
def handle(tuple, state) do
|
|
||||||
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
|
|
||||||
{:unhandled, state}
|
|
||||||
end
|
|
||||||
defp handle_plugin_message("minecraft:brand", data, _client, state) do
|
|
||||||
{[brand], ""} = Read.start(data) |> Read.string |> Read.stop
|
|
||||||
Logger.info("Client using brand: #{brand}")
|
|
||||||
{:ok, Keyword.put(state, :brand, brand)}
|
|
||||||
end
|
|
||||||
defp handle_plugin_message("amethyst:hello", _data, client, state) do
|
|
||||||
Logger.info("Client is Amethyst aware! Hello!")
|
|
||||||
transmit({:clientbound_plugin_message, "amethyst:hello", ""}, client)
|
|
||||||
{:ok, Keyword.put(state, :knows_amethyst, true)}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,87 +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.Server.Generic do
|
|
||||||
@moduledoc """
|
|
||||||
This module includes generic logic which may be used by all stages of the server, including,
|
|
||||||
for instance, listening for packets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
|
|
||||||
def get_packet(client) do
|
|
||||||
{[length], ""} = get_varint(client, "")
|
|
||||||
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} -> raise RuntimeError, "TODO: Handle disconnections reasonably"
|
|
||||||
{:error, error} -> raise RuntimeError, "An error has occured while waiting on a packet: #{error}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get_varint(client, acc) do
|
|
||||||
{:ok, byte} = :gen_tcp.recv(client, 1)
|
|
||||||
case byte do
|
|
||||||
<<0::1, _::7>> -> Read.start(acc <> byte) |> Read.varint() |> Read.stop()
|
|
||||||
<<1::1, _::7>> -> get_varint(client, acc <> byte)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule Amethyst.Server do
|
|
||||||
@moduledoc """
|
|
||||||
This module includes shared boilerplate code for all stages of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@callback init(any()) :: any()
|
|
||||||
@callback deserialize(integer(), binary()) :: any()
|
|
||||||
@callback serialize(any()) :: binary()
|
|
||||||
@callback handle(any(), :gen_tcp.socket(), any()) :: {:ok, any()} | {:unhandled, any()}
|
|
||||||
|
|
||||||
defmacro __using__(_opts) do
|
|
||||||
quote do
|
|
||||||
@behaviour Amethyst.Server
|
|
||||||
|
|
||||||
@spec serve(:gen_tcp.socket(), any()) :: no_return()
|
|
||||||
def serve(client, state \\ []) do
|
|
||||||
Logger.debug("#{__MODULE__} serving client #{inspect(client)}")
|
|
||||||
serve_loop(client, init(state))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp serve_loop(client, state) do
|
|
||||||
{id, data} = Amethyst.Server.Generic.get_packet(client)
|
|
||||||
Logger.debug("State: #{inspect(state)}")
|
|
||||||
packet = deserialize(id, data)
|
|
||||||
Logger.debug("Got packet #{inspect(packet)}")
|
|
||||||
{result, state} = handle(packet, client, state)
|
|
||||||
if result != :ok do
|
|
||||||
Logger.warning("Handler returned result #{result}")
|
|
||||||
end
|
|
||||||
serve_loop(client, state)
|
|
||||||
end
|
|
||||||
|
|
||||||
def transmit(packet, client) do
|
|
||||||
Logger.debug("Transmitting #{inspect(packet)}")
|
|
||||||
data = serialize(packet)
|
|
||||||
length = byte_size(data) |> Amethyst.Minecraft.Write.varint()
|
|
||||||
Logger.debug("Sending #{inspect(length <> data)}")
|
|
||||||
:gen_tcp.send(client, length <> data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,72 +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.Server.Handshake do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains the logic for the Handshake stage of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
use Amethyst.Server
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(state) do
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
## DESERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Handshake https://wiki.vg/Protocol#Handshake
|
|
||||||
@spec deserialize(0, binary()) ::
|
|
||||||
{:handshake, any(), any(), any(), :login | :status | :transfer}
|
|
||||||
def deserialize(0x00, <<data::binary>>) do
|
|
||||||
{[ver, addr, port, next], ""} = Read.start(data) |> Read.varint() |> Read.string() |> Read.ushort() |> Read.varint() |> Read.stop()
|
|
||||||
next = case next do
|
|
||||||
1 -> :status
|
|
||||||
2 -> :login
|
|
||||||
3 -> :transfer
|
|
||||||
_ -> raise RuntimeError, "Client requested moving to an unknown state!"
|
|
||||||
end
|
|
||||||
{:handshake, ver, addr, port, next}
|
|
||||||
end
|
|
||||||
def deserialize(type, _) do
|
|
||||||
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## SERIALIZATION
|
|
||||||
@impl true
|
|
||||||
def serialize(_) do
|
|
||||||
raise RuntimeError, "No packets can be transmitted while still in the handshake stage!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## HANDLING
|
|
||||||
@impl true
|
|
||||||
# Handshake https://wiki.vg/Protocol#Handshake
|
|
||||||
@spec handle(any(), any(), any()) :: no_return()
|
|
||||||
def handle({:handshake, 767, addr, port, next}, client, state) do
|
|
||||||
Logger.info("Got handshake, version 767 on #{addr}:#{port}. Wants to move to #{next}")
|
|
||||||
case next do
|
|
||||||
:status -> Amethyst.Server.Status.serve(client, state)
|
|
||||||
:login -> Amethyst.Server.Login.serve(client, state)
|
|
||||||
_ -> raise RuntimeError, "Unhandled move to next mode #{next}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def handle(tuple, _, state) do
|
|
||||||
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
|
|
||||||
{:unhandled, state}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,136 +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.Server.Login do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains the logic for the Login stage of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
use Amethyst.Server
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
alias Amethyst.Minecraft.Write
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(state) do
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
## DESERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Login Start https://wiki.vg/Protocol#Login_Start
|
|
||||||
def deserialize(0x00, data) do
|
|
||||||
{[name, uuid], ""} = Read.start(data) |> Read.string() |> Read.uuid() |> Read.stop()
|
|
||||||
{:login_start, name, uuid}
|
|
||||||
end
|
|
||||||
# Encryption Response https://wiki.vg/Protocol#Encryption_Response
|
|
||||||
def deserialize(0x01, data) do
|
|
||||||
{[secret_length], rest} = Read.start(data) |> Read.varint() |> Read.stop()
|
|
||||||
{[secret, verify_token_length], rest} = Read.start(rest) |> Read.raw(secret_length) |> Read.varint() |> Read.stop()
|
|
||||||
{[verify_token], ""} = Read.start(rest) |> Read.raw(verify_token_length) |> Read.stop()
|
|
||||||
{:encryption_response, secret, verify_token}
|
|
||||||
end
|
|
||||||
# Login Plugin Response https://wiki.vg/Protocol#Login_Plugin_Response
|
|
||||||
def deserialize(0x02, data) do
|
|
||||||
{[message_id, success], rest} = Read.start(data) |> Read.varint() |> Read.bool() |> Read.stop()
|
|
||||||
if success do
|
|
||||||
{:login_plugin_response, message_id, rest}
|
|
||||||
else
|
|
||||||
{:login_plugin_response, message_id, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Login Acknowledged https://wiki.vg/Protocol#Login_Acknowledged
|
|
||||||
def deserialize(0x03, "") do
|
|
||||||
{:login_acknowledged}
|
|
||||||
end
|
|
||||||
# Cookie Response https://wiki.vg/Protocol#Cookie_Response_(login)
|
|
||||||
def deserialize(0x04, data) do
|
|
||||||
{[key, exists], rest} = Read.start(data) |> Read.string() |> Read.bool() |> Read.stop()
|
|
||||||
if exists do
|
|
||||||
{[length], rest} = Read.start(rest) |> Read.varint() |> Read.stop()
|
|
||||||
{[data], _} = Read.start(rest) |> Read.raw(length) |> Read.stop()
|
|
||||||
{:cookie_response, key, data}
|
|
||||||
else
|
|
||||||
{:cookie_response, key, nil}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def deserialize(type, _) do
|
|
||||||
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## SERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Disconnect (login) https://wiki.vg/Protocol#Disconnect_(login)
|
|
||||||
def serialize({:disconnect, reason}) do
|
|
||||||
Write.varint(0x00) <> Write.string(reason)
|
|
||||||
end
|
|
||||||
# Encryption Request https://wiki.vg/Protocol#Encryption_Request
|
|
||||||
def serialize({:encryption_request, server_id, pubkey, verify_token, auth}) do
|
|
||||||
Write.varint(0x01) <>
|
|
||||||
Write.string(server_id) <>
|
|
||||||
Write.varint(byte_size(pubkey)) <> pubkey <>
|
|
||||||
Write.varint(byte_size(verify_token)) <> verify_token <>
|
|
||||||
Write.bool(auth)
|
|
||||||
end
|
|
||||||
# Login Success https://wiki.vg/Protocol#Login_Success
|
|
||||||
def serialize({:login_success, uuid, username, props, strict}) do
|
|
||||||
Write.varint(0x02) <> Write.uuid(uuid) <> Write.string(username) <> Write.varint(length(props)) <>
|
|
||||||
Enum.reduce(props, "", fn {name, value, signature}, acc -> acc <> Write.string(name) <> Write.string(value) <> case signature do
|
|
||||||
nil -> <<0x00>>
|
|
||||||
signature -> <<0x01>> <> Write.string(signature)
|
|
||||||
end end) <> Write.bool(strict)
|
|
||||||
end
|
|
||||||
# Set Compression https://wiki.vg/Protocol#Set_Compression
|
|
||||||
def serialize({:set_compression, threshold}) do
|
|
||||||
Write.varint(0x03) <> Write.varint(threshold)
|
|
||||||
end
|
|
||||||
# Login Plugin Request https://wiki.vg/Protocol#Login_Plugin_Request
|
|
||||||
def serialize({:login_plugin_request, id, channel, data}) do
|
|
||||||
Write.varint(0x04) <> Write.varint(id) <> Write.string(channel) <> data
|
|
||||||
end
|
|
||||||
# Cookie Request (login) https://wiki.vg/Protocol#Cookie_Request_(login)
|
|
||||||
def serialize({:cookie_request_login, id}) do
|
|
||||||
Write.varint(0x05) <> Write.string(id)
|
|
||||||
end
|
|
||||||
def serialize(packet) do
|
|
||||||
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
## HANDLING
|
|
||||||
@impl true
|
|
||||||
# Login Start https://wiki.vg/Protocol#Login_Start
|
|
||||||
def handle({:login_start, name, uuid}, client, state) do
|
|
||||||
Logger.info("Logging in #{name} (#{uuid})")
|
|
||||||
if Application.fetch_env!(:amethyst, :encryption) do
|
|
||||||
raise RuntimeError, "Encryption is currently unsupported." # TODO: Implement encryption
|
|
||||||
# verify_token = :crypto.strong_rand_bytes(4)
|
|
||||||
# pubkey = Amethyst.Keys.get_pub()
|
|
||||||
# auth = Application.fetch_env!(:amethyst, :auth)
|
|
||||||
# transmit({:encryption_request, "amethyst", pubkey, verify_token, auth}, client) # This is broken for some reason? java.lang.IllegalStateException: Protocol Error
|
|
||||||
else
|
|
||||||
transmit({:login_success, uuid, name, [], false}, client)
|
|
||||||
end
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
def handle({:login_acknowledged}, client, state) do
|
|
||||||
Amethyst.Server.Configuration.serve(client, state)
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
def handle(tuple, _, state) do
|
|
||||||
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
|
|
||||||
{:unhandled, state}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,127 +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.Server.Play do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains the logic for the Play stage of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
use Amethyst.Server
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
alias Amethyst.Minecraft.Write
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(state) do
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
## DESERIALIZATION
|
|
||||||
@impl true
|
|
||||||
def deserialize(0x12, data) do
|
|
||||||
{[channel], data} = Read.start(data) |> Read.string |> Read.stop
|
|
||||||
{:serverbound_plugin_message, channel, data}
|
|
||||||
end
|
|
||||||
@impl true
|
|
||||||
def deserialize(0x1A, data) do
|
|
||||||
{[x, feet_y, z, on_ground], ""} = Read.start(data) |> Read.double |> Read.double |> Read.double |> Read.bool |> Read.stop
|
|
||||||
{:set_player_position, x, feet_y, z, on_ground}
|
|
||||||
end
|
|
||||||
@impl true
|
|
||||||
def deserialize(0x1B, data) do
|
|
||||||
{[x, feet_y, z, yaw, pitch, on_ground], ""} = Read.start(data) |> Read.double |> Read.double |> Read.double |> Read.float |> Read.float |> Read.bool |> Read.stop
|
|
||||||
{:set_position_position_and_rotation, x, feet_y, z, yaw, pitch, on_ground}
|
|
||||||
end
|
|
||||||
@impl true
|
|
||||||
def deserialize(0x1C, data) do
|
|
||||||
{[yaw, pitch, on_ground], ""} = Read.start(data) |> Read.float |> Read.float |> Read.bool |> Read.stop()
|
|
||||||
{:set_player_rotation, yaw, pitch, on_ground}
|
|
||||||
end
|
|
||||||
@impl true
|
|
||||||
def deserialize(0x1D, data) do
|
|
||||||
{[on_ground], ""} = Read.start(data) |> Read.bool |> Read.stop()
|
|
||||||
{:set_player_on_ground, on_ground}
|
|
||||||
end
|
|
||||||
def deserialize(type, _) do
|
|
||||||
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## SERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Login https://wiki.vg/Protocol#Login_(play)
|
|
||||||
def serialize({:login, eid, hardcore, dimensions,
|
|
||||||
max_players, view_distance, simulation_distance,
|
|
||||||
reduce_debug, enable_respawn_screen, limited_crafting,
|
|
||||||
dim_type, dim_name, hashed_seed, gamemode, prev_gm,
|
|
||||||
is_debug, is_flat, death_loc, portal_cooldown, enforce_chat}) when byte_size(hashed_seed) == 8 do
|
|
||||||
# TODO: This is a big unreadable slab of serialization which makes bugs really hard to catch, it needs a proper rework at some point
|
|
||||||
Write.varint(0x2B) <>
|
|
||||||
Write.int(eid) <>
|
|
||||||
Write.bool(hardcore) <>
|
|
||||||
Write.varint(length(dimensions)) <>
|
|
||||||
Write.list(dimensions, &Write.string/1) <>
|
|
||||||
Write.varint(max_players) <>
|
|
||||||
Write.varint(view_distance) <>
|
|
||||||
Write.varint(simulation_distance) <>
|
|
||||||
Write.bool(reduce_debug) <>
|
|
||||||
Write.bool(enable_respawn_screen) <>
|
|
||||||
Write.bool(limited_crafting) <>
|
|
||||||
Write.varint(dim_type) <>
|
|
||||||
Write.string(dim_name) <>
|
|
||||||
hashed_seed <>
|
|
||||||
Write.ubyte(gamemode_id(gamemode)) <>
|
|
||||||
Write.byte(gamemode_id(prev_gm)) <>
|
|
||||||
Write.bool(is_debug) <>
|
|
||||||
Write.bool(is_flat) <>
|
|
||||||
if(death_loc == nil, do: Write.bool(false), else: Write.bool(true) <> Write.string(elem(death_loc, 0)) <> Write.position(elem(death_loc, 1))) <>
|
|
||||||
Write.varint(portal_cooldown) <>
|
|
||||||
Write.bool(enforce_chat)
|
|
||||||
end
|
|
||||||
def serialize(packet) do
|
|
||||||
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
## HANDLING
|
|
||||||
@impl true
|
|
||||||
def handle({:serverbound_plugin_message, channel, data}, _client, state) do
|
|
||||||
Logger.debug("Got plugin message #{channel} with data #{inspect(data)}")
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
def handle({:set_player_position, x, y, z, _on_ground}, _client, state) do
|
|
||||||
# We do not accept movement packets until we get a :confirm_teleportation
|
|
||||||
if Keyword.fetch(state, :awaiting_tp_confirm) != :error do
|
|
||||||
Logger.warning("Got :set_player_position packet while waiting for :confirm_teleportation!")
|
|
||||||
{:ok, state}
|
|
||||||
else
|
|
||||||
game = Keyword.fetch!(state, :game)
|
|
||||||
{Amethyst.API.Game.player_position(game, {x, y, z}), state}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
def handle(tuple, _, state) do # TODO: These error cases should be somehow moved into some shared area? Maybe even moved out of the modules themselves
|
|
||||||
Logger.error("Unhandled but known packet #{inspect(tuple)}")
|
|
||||||
{:unhandled, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp gamemode_id(gm) do
|
|
||||||
case gm do
|
|
||||||
nil -> -1
|
|
||||||
:survival -> 0
|
|
||||||
:creative -> 1
|
|
||||||
:adventure -> 2
|
|
||||||
:spectator -> 3
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,85 +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.Server.Status do
|
|
||||||
@moduledoc """
|
|
||||||
This module contains the logic for the Status stage of the server.
|
|
||||||
"""
|
|
||||||
require Logger
|
|
||||||
use Amethyst.Server
|
|
||||||
|
|
||||||
alias Amethyst.Minecraft.Read
|
|
||||||
alias Amethyst.Minecraft.Write
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init(state) do
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
## DESERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Status Request https://wiki.vg/Protocol#Status_Request
|
|
||||||
def deserialize(0x00, _) do
|
|
||||||
{:status_request}
|
|
||||||
end
|
|
||||||
# Ping Request https://wiki.vg/Protocol#Ping_Request
|
|
||||||
def deserialize(0x01, <<data::binary>>) do
|
|
||||||
{[payload], ""} = Read.start(data) |> Read.long() |> Read.stop()
|
|
||||||
{:ping_request, payload}
|
|
||||||
end
|
|
||||||
def deserialize(type, _) do
|
|
||||||
raise RuntimeError, "Got unknown packet type #{type}!"
|
|
||||||
end
|
|
||||||
|
|
||||||
## SERIALIZATION
|
|
||||||
@impl true
|
|
||||||
# Status Response https://wiki.vg/Protocol#Status_Response
|
|
||||||
def serialize({:status_response, data}) do
|
|
||||||
Write.varint(0x00) <> Write.string(data)
|
|
||||||
end
|
|
||||||
def serialize({:ping_response, payload}) do
|
|
||||||
Write.varint(0x01) <> Write.long(payload)
|
|
||||||
end
|
|
||||||
def serialize(packet) do
|
|
||||||
raise ArgumentError, "Tried serializing unknown packet #{inspect(packet)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
## HANDLING
|
|
||||||
@impl true
|
|
||||||
# Status Request https://wiki.vg/Protocol#Status_Request
|
|
||||||
def handle({:status_request}, client, state) do
|
|
||||||
# We want to make this more dynamic in the future, but this works for now
|
|
||||||
packet = {:status_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
|
|
||||||
})}
|
|
||||||
transmit(packet, client)
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
def handle({:ping_request, payload}, client, state) do
|
|
||||||
packet = {:ping_response, payload}
|
|
||||||
transmit(packet, client)
|
|
||||||
{:ok, state}
|
|
||||||
end
|
|
||||||
def handle(tuple, _, state) do
|
|
||||||
Logger.error("Unhandled but known packet #{elem(tuple, 0)}")
|
|
||||||
{:unhandled, state}
|
|
||||||
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) 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
|
69
apps/amethyst/lib/states/login.ex
Normal file
69
apps/amethyst/lib/states/login.ex
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# 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 disconnect(reason) do
|
||||||
|
%{packet_type: :disconnect, reason: %{
|
||||||
|
"text" => "You have been disconnected:\n#{reason}",
|
||||||
|
"color" => "red"
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
81
apps/amethyst/lib/states/macros.ex
Normal file
81
apps/amethyst/lib/states/macros.ex
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# 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.Macros do
|
||||||
|
defmacro defpacket_serverbound(name, id, version, signature) do
|
||||||
|
quote do
|
||||||
|
def deserialize(unquote(id), unquote(version), data) 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) do
|
||||||
|
quote do
|
||||||
|
def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) do
|
||||||
|
Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature))
|
||||||
|
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, 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
|
||||||
|
{:array, signature} ->
|
||||||
|
{[count], rest} = Read.start(rest) |> Read.varint() |> Read.stop()
|
||||||
|
{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}
|
||||||
|
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
|
||||||
|
data = Enum.reduce(signature, "", fn {name, type}, acc ->
|
||||||
|
#acc <> apply(Write, type, [Map.get(packet, name)])
|
||||||
|
case type do
|
||||||
|
{: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)
|
||||||
|
t -> acc <> apply(Write, t, [Map.get(packet, name)])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
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) 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) 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
|
@ -38,7 +38,8 @@ defmodule Amethyst.MixProject do
|
|||||||
[
|
[
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:uuid, "~> 1.1"},
|
{:uuid, "~> 1.1"},
|
||||||
{:ex_doc, "~> 0.22", only: :dev, runtime: false}
|
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
|
||||||
|
{:jason, "~> 1.4"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user