# 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 . 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) <> <> end # Ping https://wiki.vg/Protocol#Ping_(configuration) def serialize({:ping, id}) do Write.varint(0x05) <> <> 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 raise ArgumentError, "Packet 'Registry Data' is not yet implemented" 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"}]}, 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 # TODO: we should send registries i think? does amethyst need to deal with vanilla registries at all? god only knows 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 # 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 # 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