# 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 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 # 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