diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex index 9021214..50e43b1 100644 --- a/apps/amethyst/lib/states/configuration.ex +++ b/apps/amethyst/lib/states/configuration.ex @@ -130,6 +130,14 @@ defmodule Amethyst.ConnectionState.Configuration do 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) @@ -141,6 +149,154 @@ defmodule Amethyst.ConnectionState.Configuration do |> 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(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), + }) + 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}, + ] + }}) + 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) + if Amethyst.Api.Game.login(game, state) == :reject do + send(self(), {:disconnect, "Default game rejected connection"}) + else + 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: 3, + previous_game_mode: -1, + is_debug: false, + is_flat: false, + death_location: nil, + portal_cooldown: 0, + enforces_secure_chat: false + }}) + end + end + def disconnect(reason) do %{packet_type: :disconnect, reason: {:compound, %{ "text" => {:string, reason} diff --git a/apps/amethyst/lib/states/macros.ex b/apps/amethyst/lib/states/macros.ex index 7aed042..5f97301 100644 --- a/apps/amethyst/lib/states/macros.ex +++ b/apps/amethyst/lib/states/macros.ex @@ -39,6 +39,14 @@ defmodule Amethyst.ConnectionState.Macros 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, t} -> {[exists], rest} = Read.start(rest) |> Read.bool() |> Read.stop() if exists do @@ -64,6 +72,11 @@ defmodule Amethyst.ConnectionState.Macros do data = Enum.reduce(signature, "", fn {name, type}, acc -> #acc <> apply(Write, type, [Map.get(packet, name)]) 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) diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex new file mode 100644 index 0000000..8830de0 --- /dev/null +++ b/apps/amethyst/lib/states/play.ex @@ -0,0 +1,61 @@ +# 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.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 :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, + ] + + def disconnect(reason) do + %{ + packet_type: :disconnect, + reason: {:compound, %{ + "text" => {:string, reason} + }} + } + end +end