From 53fe25043d0ec62fc80b2001cca71f1bcdfa2e91 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 17 Sep 2024 19:23:39 +0200 Subject: [PATCH 01/16] Implement handling confirm_teleportation --- apps/amethyst/lib/apps/connection_handler.ex | 3 +++ apps/amethyst/lib/states/play.ex | 22 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index ac43cbe..39dc204 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -64,6 +64,9 @@ defmodule Amethyst.ConnectionHandler do Logger.debug("Sending packet #{inspect(packet)}") send_packet(socket, connstate, packet, version) loop(socket, connstate, version, state) + {:tp_done, id} -> + # TODO: Implement a proper teleportation API + Logger.debug("Accepted teleportation #{inspect(id)}") after 0 -> receive do {:packet, id, data} -> diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index 8830de0..c90c0cd 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -49,6 +49,28 @@ defmodule Amethyst.ConnectionState.Play do portal_cooldown: :varint, enforces_secure_chat: :bool, ] + Macros.defpacket_clientbound :synchronize_player_position, 0x40, 747, [ + x: :double, + y: :double, + z: :double, + yaw: :float, + pitch: :double, + flags: :byte, + teleport_id: :varint + ] + + Macros.defpacket_serverbound :confirm_teleportation, 0x00, 747, [teleport_id: :varint] + + def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 747, state) do + if state[:teleport_id] == id do + # The client has accepted our teleportation, we can let the connection process know + send(self(), {:tp_done, id}) + state |> Map.put(:teleport_id, nil) + else + # The client has not yet accepted our teleportation, we aren't done yet + state + end + end def disconnect(reason) do %{ From f79e0728e671ad79c9f1420b1f2a24940124ec53 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 17 Sep 2024 19:50:21 +0200 Subject: [PATCH 02/16] Implement callback for accept_teleport --- apps/amethyst/lib/api/game.ex | 12 ++++++++++++ apps/amethyst/lib/apps/connection_handler.ex | 3 --- apps/amethyst/lib/states/play.ex | 10 ++-------- apps/example_game/lib/example/game.ex | 6 ++++++ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex index 4cca9a2..17c1cc6 100644 --- a/apps/amethyst/lib/api/game.ex +++ b/apps/amethyst/lib/api/game.ex @@ -60,6 +60,18 @@ defmodule Amethyst.API.Game do mod.player_position(self(), {x, y, z}, refs) end @doc """ + `accept_teleport/3` is called when a client accepts a teleportation as sent by the Synchronize Player Position + packet (TODO: Teleport Player API). This lets you know that the client is now where you expect it to be. + + - 'from' is the PID of the player's connection process (see `login/3`). + - 'id' is the teleport ID (TODO: Teleport Player API) + - 'state_refs' are your references (see `instantiate/1`) + """ + @callback accept_teleport(from :: pid(), id :: integer(), state_refs :: map()) :: :ok + def accept_teleport(%{:mod => mod, :refs => refs}, id) do + mod.accept_teleport(self(), id, refs) + end + @doc """ Whether or not this game instance can be joined by a new player. This should include basic logic such as if joining makes sense, for instance if the game is full or if the game has already started. """ diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index 39dc204..ac43cbe 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -64,9 +64,6 @@ defmodule Amethyst.ConnectionHandler do Logger.debug("Sending packet #{inspect(packet)}") send_packet(socket, connstate, packet, version) loop(socket, connstate, version, state) - {:tp_done, id} -> - # TODO: Implement a proper teleportation API - Logger.debug("Accepted teleportation #{inspect(id)}") after 0 -> receive do {:packet, id, data} -> diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index c90c0cd..9ba37a9 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -62,14 +62,8 @@ defmodule Amethyst.ConnectionState.Play do Macros.defpacket_serverbound :confirm_teleportation, 0x00, 747, [teleport_id: :varint] def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 747, state) do - if state[:teleport_id] == id do - # The client has accepted our teleportation, we can let the connection process know - send(self(), {:tp_done, id}) - state |> Map.put(:teleport_id, nil) - else - # The client has not yet accepted our teleportation, we aren't done yet - state - end + Amethyst.API.Game.accept_teleport(state[:game], id) + :ok end def disconnect(reason) do diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 3b66c48..2bceca0 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -21,6 +21,12 @@ defmodule Example.Game do :ok end + @impl true + def accept_teleport(from, id, _state_refs) do + Logger.info("Player at #{inspect(from)} accepted teleport #{inspect(id)}") + :ok + end + @impl true def joinable?(_refs) do true From 764c4bc387949741290713febf39179c8c7d851b Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 17 Sep 2024 20:01:44 +0200 Subject: [PATCH 03/16] Minor doc update --- apps/amethyst/lib/api/game.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex index 17c1cc6..0276fe6 100644 --- a/apps/amethyst/lib/api/game.ex +++ b/apps/amethyst/lib/api/game.ex @@ -16,8 +16,9 @@ defmodule Amethyst.API.Game do @moduledoc """ - This module includes the interface for defining and registering - a game with Amethyst. + This behaviour should be implemented by any Amethyst Game. It additionally + contains functions that the internal connection handler code uses to more + conveniently call a game's callbacks. """ @doc """ From f037f0de02e1844a64ff63a07e69465ff5998ecc Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 19 Sep 2024 15:11:59 +0200 Subject: [PATCH 04/16] No longer rely on loose error checking and other minor fixes --- apps/amethyst/lib/apps/tcp_listener.ex | 2 +- apps/amethyst/lib/states/configuration.ex | 2 ++ apps/amethyst/lib/states/login.ex | 2 +- apps/amethyst/lib/states/play.ex | 17 +++++++++++++---- apps/example_game/lib/example/game.ex | 5 +++++ 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/apps/amethyst/lib/apps/tcp_listener.ex b/apps/amethyst/lib/apps/tcp_listener.ex index 7696ee0..7c10ca2 100644 --- a/apps/amethyst/lib/apps/tcp_listener.ex +++ b/apps/amethyst/lib/apps/tcp_listener.ex @@ -21,7 +21,7 @@ defmodule Amethyst.TCPListener do """ def accept(port) do - {:ok, socket} = :gen_tcp.listen(port, [:binary, packet: 0, active: false, reuseaddr: true]) + {:ok, socket} = :gen_tcp.listen(port, [:binary, packet: 0, active: false, reuseaddr: true, nodelay: true]) Logger.info("Listening on port #{port}") loop_acceptor(socket) end diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex index 32a2dc9..e6f6c39 100644 --- a/apps/amethyst/lib/states/configuration.ex +++ b/apps/amethyst/lib/states/configuration.ex @@ -258,6 +258,8 @@ defmodule Amethyst.ConnectionState.Configuration do %{id: "minecraft:stalagmite", data: generic_damage}, %{id: "minecraft:outside_border", data: generic_damage}, %{id: "minecraft:generic_kill", data: generic_damage}, + %{id: "minecraft:hot_floor", data: generic_damage}, + %{id: "minecraft:in_wall", data: generic_damage}, ] }}) send(self(), {:send_packet, %{packet_type: :finish_configuration}}) diff --git a/apps/amethyst/lib/states/login.ex b/apps/amethyst/lib/states/login.ex index 2e6f752..bd4be9b 100644 --- a/apps/amethyst/lib/states/login.ex +++ b/apps/amethyst/lib/states/login.ex @@ -70,7 +70,7 @@ defmodule Amethyst.ConnectionState.Login do uuid: player_uuid, username: name, properties: [], - strict_error_handling: false + strict_error_handling: true }}) :ok end diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index 9ba37a9..42a043f 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -49,19 +49,28 @@ defmodule Amethyst.ConnectionState.Play do portal_cooldown: :varint, enforces_secure_chat: :bool, ] - Macros.defpacket_clientbound :synchronize_player_position, 0x40, 747, [ + Macros.defpacket_clientbound :synchronize_player_position, 0x40, 767, [ x: :double, y: :double, z: :double, yaw: :float, - pitch: :double, + pitch: :float, flags: :byte, teleport_id: :varint ] - Macros.defpacket_serverbound :confirm_teleportation, 0x00, 747, [teleport_id: :varint] + Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint] - def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 747, state) do + @spec handle( + %{ + :packet_type => :confirm_teleportation, + :teleport_id => any(), + optional(any()) => any() + }, + 767, + nil | maybe_improper_list() | map() + ) :: :ok + def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do Amethyst.API.Game.accept_teleport(state[:game], id) :ok end diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 2bceca0..4754215 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -12,6 +12,11 @@ defmodule Example.Game do def login(from, cfg, refs) do Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}") Logger.info("The refs for this game are #{inspect(refs)}") + #send(from, {:send_packet, %{ + # packet_type: :synchronize_player_position, + # x: 0.0, y: 0.0, z: 0.0, yaw: 0.0, pitch: 0.0, flags: 0x00, + # teleport_id: 0 + #}}) :accept end From 930a508ad91a67a1f11b916ebba33259e9ce5e83 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 19 Sep 2024 15:40:16 +0200 Subject: [PATCH 05/16] "Loading terrain..."! --- apps/amethyst/lib/api/game.ex | 17 ++++++- apps/amethyst/lib/states/configuration.ex | 60 +++++++++++++---------- apps/amethyst/lib/states/play.ex | 45 +++++++++++++---- apps/example_game/lib/example/game.ex | 14 +++--- 4 files changed, 93 insertions(+), 43 deletions(-) diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex index 0276fe6..95ad5fb 100644 --- a/apps/amethyst/lib/api/game.ex +++ b/apps/amethyst/lib/api/game.ex @@ -33,6 +33,9 @@ defmodule Amethyst.API.Game do You may either :accept or :reject the player for whatever reason, avoid rejecting the player in your default game as that will disconnect the player. + If you :accept the player, you must return a spawn position ({x, y, z}) and + rotation ({yaw, pitch}) + Note that if no new players can join for any reason, your game should return false from `joinable?/1`. The PID received in 'from' will be the one that calls all callbacks @@ -44,7 +47,7 @@ defmodule Amethyst.API.Game do - 'player_cfg' is a keyword list containing the configuration passed by the game client - 'state_refs' are your references (see `instantiate/1`) """ - @callback login(from :: pid(), player_cfg :: keyword(), state_refs :: map()) :: :accept | :reject + @callback login(from :: pid(), player_cfg :: keyword(), state_refs :: map()) :: {:accept, {x :: float(), y :: float(), z :: float()}, {yaw :: float(), pitch ::float()}} | :reject def login(%{:mod => mod, :refs => refs}, player_cfg) do mod.login(self(), player_cfg, refs) end @@ -61,6 +64,18 @@ defmodule Amethyst.API.Game do mod.player_position(self(), {x, y, z}, refs) end @doc """ + `player_rotation/3` is called when a player rotates. This function is called with the absolute angles + that the player client expects. TODO: Teleport Player API. + + - 'from' is the PID of the player's connection process (see `login/3`). + - 'yaw' and 'pitch' are the angles the player expects to rotate to. These are in Minecraft's rotation format. + - `state_refs` are your references (see `instantiate/1`) + """ + @callback player_rotation(from :: pid(), {yaw :: float(), pitch :: float()}, state_refs :: map()) :: :ok + def player_rotation(%{:mod => mod, :refs => refs}, {yaw, pitch}) do + mod.player_rotation(self(), {yaw, pitch}, refs) + end + @doc """ `accept_teleport/3` is called when a client accepts a teleportation as sent by the Synchronize Player Position packet (TODO: Teleport Player API). This lets you know that the client is now where you expect it to be. diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex index e6f6c39..6513ac6 100644 --- a/apps/amethyst/lib/states/configuration.ex +++ b/apps/amethyst/lib/states/configuration.ex @@ -271,33 +271,39 @@ defmodule Amethyst.ConnectionState.Configuration do 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"}) - :ok - 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 - }}) - state + login = Amethyst.API.Game.login(game, state) + case login do + :reject -> + send(self(), {:disconnect, "Default game rejected connection"}) + :ok + {:accept, {x, y, z}, {yaw, pitch}} -> + send(self(), {:send_packet, %{ + packet_type: :login, + entity_id: 0, + is_hardcore: false, + dimensions: [%{name: "minecraft:overworld"}], + max_players: 0, + view_distance: 16, + simulation_distance: 16, + reduced_debug_info: false, + enable_respawn_screen: true, + do_limited_crafting: false, + dimension_type: 0, + dimension_name: "minecraft:overworld", + hashed_seed: 0, + game_mode: 3, + previous_game_mode: -1, + is_debug: false, + is_flat: false, + death_location: nil, + portal_cooldown: 0, + enforces_secure_chat: false + }}) + send(self(), {:send_packet, %{ + packet_type: :synchronize_player_position, + x: x, y: y, z: z, yaw: yaw, pitch: pitch, teleport_id: 0, flags: 0x00 + }}) + state end end diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index 42a043f..0dbfd2c 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -60,21 +60,48 @@ defmodule Amethyst.ConnectionState.Play do ] Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint] + Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [ + x: :double, + feet_y: :double, + z: :double, + on_ground: :bool + ] + Macros.defpacket_serverbound :set_player_position_and_rotation, 0x1B, 767, [ + x: :double, + feet_y: :double, + z: :double, + yaw: :float, + pitch: :float, + on_ground: :bool + ] + Macros.defpacket_serverbound :set_player_rotation, 0x1C, 767, [ + yaw: :float, + pitch: :float, + on_ground: :bool # I don't understand their obsession with this... + ] - @spec handle( - %{ - :packet_type => :confirm_teleportation, - :teleport_id => any(), - optional(any()) => any() - }, - 767, - nil | maybe_improper_list() | map() - ) :: :ok def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do Amethyst.API.Game.accept_teleport(state[:game], id) :ok end + def handle(%{packet_type: :set_player_position_and_rotation, x: x, feet_y: y, z: z, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do + # I don't know why we would ever trust on_ground here... the server computes that + Amethyst.API.Game.player_position(state[:game], {x, y, z}) + Amethyst.API.Game.player_rotation(state[:game], {yaw, pitch}) + :ok + end + def handle(%{packet_type: :set_player_position, x: x, feet_y: y, z: z, on_ground: _ground}, 767, state) do + # I don't know why we would ever trust on_ground here... the server computes that + Amethyst.API.Game.player_position(state[:game], {x, y, z}) + :ok + end + def handle(%{packet_type: :set_player_rotation, yaw: yaw, pitch: pitch, on_ground: _ground}, 767, state) do + # I don't know why we would ever trust on_ground here... the server computes that + Amethyst.API.Game.player_rotation(state[:game], {yaw, pitch}) + :ok + end + def disconnect(reason) do %{ packet_type: :disconnect, diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 4754215..7e8caa5 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -12,20 +12,22 @@ defmodule Example.Game do def login(from, cfg, refs) do Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}") Logger.info("The refs for this game are #{inspect(refs)}") - #send(from, {:send_packet, %{ - # packet_type: :synchronize_player_position, - # x: 0.0, y: 0.0, z: 0.0, yaw: 0.0, pitch: 0.0, flags: 0x00, - # teleport_id: 0 - #}}) - :accept + {:accept, {0.0, 0.0, 0.0}, {0.0, 0.0}} end @impl true + @spec player_position(any(), {any(), any(), any()}, any()) :: :ok def player_position(from, {x, y, z}, _refs) do Logger.info("Player at #{inspect(from)} moved to #{x}, #{y}, #{z}") :ok end + @impl true + def player_rotation(from, {yaw, pitch}, _refs) do + Logger.info("Player at #{inspect(from)} rotated to #{yaw}, #{pitch}") + :ok + end + @impl true def accept_teleport(from, id, _state_refs) do Logger.info("Player at #{inspect(from)} accepted teleport #{inspect(id)}") From 034f21ade75a12b0c8a4b030b8405bb5cea93ae3 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Wed, 25 Sep 2024 11:09:26 +0200 Subject: [PATCH 06/16] begin implementing the second worst packet of this protocol --- apps/amethyst/lib/states/macros.ex | 4 ++-- apps/amethyst/lib/states/play.ex | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/amethyst/lib/states/macros.ex b/apps/amethyst/lib/states/macros.ex index 0cd5722..a6def10 100644 --- a/apps/amethyst/lib/states/macros.ex +++ b/apps/amethyst/lib/states/macros.ex @@ -78,7 +78,7 @@ defmodule Amethyst.ConnectionState.Macros do end def write_signature(packet, signature) do - data = Enum.reduce(signature, "", fn {name, type}, acc -> + Enum.reduce(signature, "", fn {name, type}, acc -> #acc <> apply(Write, type, [Map.get(packet, name)]) case type do {:optional, {:compound, signature}} -> @@ -145,7 +145,7 @@ defmodule Amethyst.ConnectionState.Macros do _ -> false end end - def type_matches(value, {:optional, type}) when is_nil(value), do: true + def type_matches(value, {:optional, _type}) when is_nil(value), do: true def type_matches(value, {:optional, type}), do: type_matches(value, type) def type_matches(value, {:array, signature}) when is_list(value), do: Enum.all?(value, fn item -> check_type(item, signature) end) def type_matches(value, {:compound, signature}) when is_map(value), do: check_type(value, signature) diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index 0dbfd2c..b69ead6 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -16,6 +16,7 @@ defmodule Amethyst.ConnectionState.Play do require Amethyst.ConnectionState.Macros + alias Amethyst.Minecraft.Write alias Amethyst.ConnectionState.Macros require Logger @@ -79,6 +80,19 @@ defmodule Amethyst.ConnectionState.Play do pitch: :float, on_ground: :bool # I don't understand their obsession with this... ] + # this packet sucks, thoughtful design is for losers anyway + def serialize(%{packet_type: :game_event, event: :no_respawn_block_available}, 767), do: Write.ubyte(0) + def serialize(%{packet_type: :game_event, event: :begin_raining}, 767), do: Write.ubyte(1) + def serialize(%{packet_type: :game_event, event: :end_raining}, 767), do: Write.ubyte(2) + def serialize(%{packet_type: :game_event, event: :change_gamemode, value: v}, 767), do: Write.ubyte(3) <> Write.float(v) + def serialize(%{packet_type: :game_event, event: :win_game, value: v}, 767), do: Write.ubyte(4) <> Write.float(v) + def serialize(%{packet_type: :game_event, event: :demo_event, value: v}, 767), do: Write.ubyte(5) <> Write.float(v) + def serialize(%{packet_type: :game_event, event: :arrow_hit_player}, 767), do: Write.ubyte(6) + def serialize(%{packet_type: :game_event, event: :rain_level_change, value: v}, 767), do: Write.ubyte(7) <> Write.float(v) + def serialize(%{packet_type: :game_event, event: :thunder_level_change, value: v}, 767), do: Write.ubyte(8) <> Write.float(v) + def serialize(%{packet_type: :game_event, event: :play_pufferfish_sting_sound}, 767), do: Write.ubyte(9) + def serialize(%{packet_type: :game_event, event: :play_elder_guardian_mob_appearance}, 767), do: Write.ubyte(10) + def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do Amethyst.API.Game.accept_teleport(state[:game], id) From f0c2ef80ec1be1cd253cd9513be5d1c94c4e2b0e Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 26 Sep 2024 14:47:05 +0200 Subject: [PATCH 07/16] work on implementing some more packets --- apps/amethyst/lib/states/configuration.ex | 5 ++ apps/amethyst/lib/states/macros.ex | 13 +++-- apps/amethyst/lib/states/play.ex | 59 +++++++++++++++++------ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex index 6513ac6..c4e99a9 100644 --- a/apps/amethyst/lib/states/configuration.ex +++ b/apps/amethyst/lib/states/configuration.ex @@ -303,6 +303,11 @@ defmodule Amethyst.ConnectionState.Configuration do packet_type: :synchronize_player_position, x: x, y: y, z: z, yaw: yaw, pitch: pitch, teleport_id: 0, flags: 0x00 }}) + send(self(), {:send_packet, Amethyst.ConnectionState.Play.ge_start_waiting_for_level_chunks(767)}) + send(self(), {:send_packet, %{packet_type: :set_center_chunk, + chunk_x: div(x, 16), + chunk_y: div(z, 16) + }}) state end end diff --git a/apps/amethyst/lib/states/macros.ex b/apps/amethyst/lib/states/macros.ex index a6def10..86b3970 100644 --- a/apps/amethyst/lib/states/macros.ex +++ b/apps/amethyst/lib/states/macros.ex @@ -16,18 +16,18 @@ defmodule Amethyst.ConnectionState.Macros do require Logger - defmacro defpacket_serverbound(name, id, version, signature) do + defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do quote do - def deserialize(unquote(id), unquote(version), data) do + def deserialize(unquote(id), unquote(version), data) when unquote(where) do {packet, ""} = Amethyst.ConnectionState.Macros.read_signature(data, unquote(signature)) packet |> Map.put(:packet_type, unquote(name)) end end end - defmacro defpacket_clientbound(name, id, version, signature) do + defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do quote do - def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) do + def serialize(%{packet_type: unquote(name)} = packet, unquote(version)) when unquote(where) do if Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature)) else @@ -79,7 +79,6 @@ defmodule Amethyst.ConnectionState.Macros do def write_signature(packet, signature) do 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 @@ -126,8 +125,8 @@ defmodule Amethyst.ConnectionState.Macros do def type_matches(value, :ushort) when is_integer(value) and value in 0..65535, do: true def type_matches(value, :int) when is_integer(value) and value in -2147483648..2147483647, do: true def type_matches(value, :long) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true - def type_matches(value, :float) when is_float(value), do: true - def type_matches(value, :double) when is_float(value), do: true + def type_matches(value, :float) when is_number(value), do: true + def type_matches(value, :double) when is_number(value), do: true def type_matches(value, :varint) when is_integer(value) and value in -2147483648..2147483647, do: true def type_matches(value, :varlong) when is_integer(value) and value in -9223372036854775808..9223372036854775807, do: true def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index b69ead6..c00c81a 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -16,7 +16,6 @@ defmodule Amethyst.ConnectionState.Play do require Amethyst.ConnectionState.Macros - alias Amethyst.Minecraft.Write alias Amethyst.ConnectionState.Macros require Logger @@ -26,6 +25,28 @@ defmodule Amethyst.ConnectionState.Play do """ Macros.defpacket_clientbound :disconnect, 0x1D, 767, [reason: :nbt] + Macros.defpacket_clientbound :chunk_data_and_update_light, 0x27, 767, [ + chunk_x: :int, + chunk_z: :int, + heightmaps: :nbt, + data: :byte_array, + block_entities: {:array, [ + packed_xz: :ubyte, # TODO: This would be interesting to have in a clearer format + y: :short, + type: :varint, + data: :nbt + ]}, + sky_light_mask: :raw, + block_light_mask: :raw, + empty_sky_light_mask: :raw, + empty_block_light_mask: :raw, + sky_light_arrays: {:array, [ + sky_light_array: :byte_array + ]}, + block_light_arrays: {:array, [ + block_light_array: :byte_array + ]} + ] Macros.defpacket_clientbound :login, 0x2B, 767, [ entity_id: :int, is_hardcore: :bool, @@ -59,6 +80,29 @@ defmodule Amethyst.ConnectionState.Play do flags: :byte, teleport_id: :varint ] + Macros.defpacket_clientbound :set_center_chunk, 0x54, 767, [ + chunk_x: :varint, chunk_z: :varint + ] + + Macros.defpacket_clientbound :game_event, 0x22, 767, [ + event: :ubyte, value: :float + ] + # We can use functions to wrap over this packet and make it a bit clearer. + # Taking the protocol version here makes it less portable but whatever, fuck this packet + def ge_no_respawn_block_available(767), do: %{packet_type: :game_event, event: 0, value: 0} + def ge_begin_raining(767), do: %{packet_type: :game_event, event: 1, value: 0} + def ge_end_raining(767), do: %{packet_type: :game_event, event: 2, value: 0} + def ge_change_game_mode(767, gm) when is_integer(gm), do: %{packet_type: :game_event, event: 3, value: gm} + def ge_win_game(767, credits?) when is_integer(credits?), do: %{packet_type: :game_event, event: 4, value: credits?} + def ge_game_event(767, event) when is_integer(event), do: %{packet_type: :game_event, event: 5, value: event} + def ge_arrow_hit_player(767), do: %{packet_type: :game_event, event: 6, value: 0} + def ge_rain_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 7, value: value} + def ge_thunder_level_change(767, value) when is_number(value), do: %{packet_type: :game_event, event: 8, value: value} + def ge_play_pufferfish_sting_sound(767), do: %{packet_type: :game_event, event: 9, value: 0} + def ge_play_elder_guardian_mob_appearance(767), do: %{packet_type: :game_event, event: 10, value: 0} + def ge_enable_respawn_screen(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 11, value: enabled?} + def ge_limited_crafting(767, enabled?) when is_integer(enabled?), do: %{packet_type: :game_event, event: 12, value: enabled?} + def ge_start_waiting_for_level_chunks(767), do: %{packet_type: :game_event, event: 13, value: 0} Macros.defpacket_serverbound :confirm_teleportation, 0x00, 767, [teleport_id: :varint] Macros.defpacket_serverbound :set_player_position, 0x1A, 767, [ @@ -80,19 +124,6 @@ defmodule Amethyst.ConnectionState.Play do pitch: :float, on_ground: :bool # I don't understand their obsession with this... ] - # this packet sucks, thoughtful design is for losers anyway - def serialize(%{packet_type: :game_event, event: :no_respawn_block_available}, 767), do: Write.ubyte(0) - def serialize(%{packet_type: :game_event, event: :begin_raining}, 767), do: Write.ubyte(1) - def serialize(%{packet_type: :game_event, event: :end_raining}, 767), do: Write.ubyte(2) - def serialize(%{packet_type: :game_event, event: :change_gamemode, value: v}, 767), do: Write.ubyte(3) <> Write.float(v) - def serialize(%{packet_type: :game_event, event: :win_game, value: v}, 767), do: Write.ubyte(4) <> Write.float(v) - def serialize(%{packet_type: :game_event, event: :demo_event, value: v}, 767), do: Write.ubyte(5) <> Write.float(v) - def serialize(%{packet_type: :game_event, event: :arrow_hit_player}, 767), do: Write.ubyte(6) - def serialize(%{packet_type: :game_event, event: :rain_level_change, value: v}, 767), do: Write.ubyte(7) <> Write.float(v) - def serialize(%{packet_type: :game_event, event: :thunder_level_change, value: v}, 767), do: Write.ubyte(8) <> Write.float(v) - def serialize(%{packet_type: :game_event, event: :play_pufferfish_sting_sound}, 767), do: Write.ubyte(9) - def serialize(%{packet_type: :game_event, event: :play_elder_guardian_mob_appearance}, 767), do: Write.ubyte(10) - def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do Amethyst.API.Game.accept_teleport(state[:game], id) From c880ea95f3066d54f70b1cdb34ac9d99c4d228a1 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 1 Oct 2024 08:21:30 +0200 Subject: [PATCH 08/16] Implement player info update packet --- apps/amethyst/lib/states/macros.ex | 8 ++++- apps/amethyst/lib/states/play.ex | 52 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/apps/amethyst/lib/states/macros.ex b/apps/amethyst/lib/states/macros.ex index 86b3970..d52f767 100644 --- a/apps/amethyst/lib/states/macros.ex +++ b/apps/amethyst/lib/states/macros.ex @@ -95,6 +95,7 @@ defmodule Amethyst.ConnectionState.Macros do Enum.reduce(Map.get(packet, name), "", fn item, acc -> acc <> write_signature(item, signature) end) + {:literal, {type, value}} -> acc <> apply(Write, type, [value]) t -> acc <> apply(Write, t, [Map.get(packet, name)]) end end) @@ -104,7 +105,12 @@ defmodule Amethyst.ConnectionState.Macros do try do Enum.all?(signature, fn {name, type} -> case Map.get(packet, name, :missing) do - :missing -> throw {:missing, name} + :missing -> + if elem(type, 0) == :literal do + true + else + throw {:missing, name} + end value -> case type_matches(value, type) do true -> true false -> throw {:mismatch, name, value, type} diff --git a/apps/amethyst/lib/states/play.ex b/apps/amethyst/lib/states/play.ex index c00c81a..e91c06d 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -71,6 +71,58 @@ defmodule Amethyst.ConnectionState.Play do portal_cooldown: :varint, enforces_secure_chat: :bool, ] + Macros.defpacket_clientbound :player_info_update_add_player, 0x2E, 767, [ + actions: {:literal, 0x01}, + players: {:array, [ + uuid: :uuid, + name: :string, + properties: {:array, [ + name: :string, + value: :string, + signature: {:optional, :string}, + ]} + ]} + ] + Macros.defpacket_clientbound :player_info_update_initialize_chat, 0x2E, 767, [ + actions: {:literal, 0x02}, + players: {:array, [ + uuid: :uuid, + data: {:optional, {:compound, [ + chat_session_id: :uuid, + public_key_expiry_time: :long, + encoded_public_key: :byte_array, + public_key_signature: :byte_array + ]}} + ]} + ] + Macros.defpacket_clientbound :player_info_update_update_game_mode, 0x2E, 767, [ + actions: {:literal, 0x04}, + players: {:array, [ + uuid: :uuid, + gamemode: :varint + ]} + ] + Macros.defpacket_clientbound :player_info_update_update_listed, 0x2E, 767, [ + actions: {:literal, 0x08}, + players: {:array, [ + uuid: :uuid, + listed: :bool + ]} + ] + Macros.defpacket_clientbound :player_info_update_update_latency, 0x2E, 767, [ + actions: {:literal, 0x10}, + players: {:array, [ + uuid: :uuid, + ping: :varint, # Milliseconds + ]} + ] + Macros.defpacket_clientbound :player_info_update_update_display_name, 0x2E, 767, [ + actions: {:literal, 0x20}, + players: {:array, [ + uuid: :uuid, + display_name: {:optional, :nbt} + ]} + ] Macros.defpacket_clientbound :synchronize_player_position, 0x40, 767, [ x: :double, y: :double, From 453daa817b37ac9b32f64efb41c12350207306fb Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 1 Oct 2024 10:51:08 +0200 Subject: [PATCH 09/16] Implement part of chunk sending --- apps/amethyst/lib/apps/connection_handler.ex | 50 ++++++++++++++++++++ apps/amethyst/lib/states/configuration.ex | 5 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index ac43cbe..d36a090 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -60,6 +60,39 @@ defmodule Amethyst.ConnectionHandler do {:set_version, newversion} -> Logger.debug("Switching to version #{newversion} from #{version}") loop(socket, connstate, newversion, state) + {:set_position, position} -> + Logger.debug("Updating client position to #{inspect(position)}") + prev_position = Map.get(state, :position) + state = Map.put(state, :position, position) + # If there was no prev position, we consider that we + # definitely moved + prev_cp = if prev_position == nil do nil else chunk_pos(elem(prev_position, 0), elem(prev_position, 1)) end + cp = chunk_pos(elem(position, 0), elem(position, 1)) + if prev_cp != cp do + Logger.debug("Client entered new chunk #{inspect(cp)}") + # We changed chunk borders, update center chunk and begin sending new chunks + send(self(), {:send_packet, %{ + packet_type: :set_center_chunk, + chunk_x: elem(cp, 0), + chunk_z: elem(cp, 1) + }}) + # Figure out which new chunks are visible + prev_chunks = + if prev_cp == nil do + MapSet.new([]) + else + MapSet.new(visible_chunks_from(elem(prev_cp, 0), elem(prev_cp, 1), Map.get(state, :view_distance, 16))) + end + chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), Map.get(state, :view_distance, 16))) + new_chunks = MapSet.difference(chunks, prev_chunks) + # Process these chunks, we can process all chunks in parallel + Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), + new_chunks, + fn chunk -> process_chunk(chunk, state) end, + [ordered: false] + ) |> Stream.run() + end + loop(socket, connstate, version, state) {:send_packet, packet} -> Logger.debug("Sending packet #{inspect(packet)}") send_packet(socket, connstate, packet, version) @@ -73,6 +106,23 @@ defmodule Amethyst.ConnectionHandler do end end + defp chunk_pos(x, z) do + {div(floor(x), 16), div(floor(z), 16)} + end + + defp visible_chunks_from(x, z, view_distance) do + {cx, cz} = chunk_pos(x, z) + (cx - view_distance - 3 .. cx + view_distance + 3) |> Enum.flat_map(fn ix -> + (cz - view_distance - 3 .. cz + view_distance + 3) |> Enum.map(fn iz -> + {ix, iz} + end) + end) + end + + defp process_chunk(_chunk, _state) do + # TODO: Actually ask the game for the desired chunk then send the packets. + end + defp handle_packet(id, data, connstate, version, state) do try do packet = connstate.deserialize(id, version, data) diff --git a/apps/amethyst/lib/states/configuration.ex b/apps/amethyst/lib/states/configuration.ex index c4e99a9..7426b65 100644 --- a/apps/amethyst/lib/states/configuration.ex +++ b/apps/amethyst/lib/states/configuration.ex @@ -305,9 +305,10 @@ defmodule Amethyst.ConnectionState.Configuration do }}) send(self(), {:send_packet, Amethyst.ConnectionState.Play.ge_start_waiting_for_level_chunks(767)}) send(self(), {:send_packet, %{packet_type: :set_center_chunk, - chunk_x: div(x, 16), - chunk_y: div(z, 16) + chunk_x: div(floor(x), 16), + chunk_z: div(floor(z), 16) }}) + send(self(), {:set_position, {x, y, z}}) state end end From 5fae9aa1fffd7b0d41870ddf281f4b25e23a366e Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 1 Oct 2024 19:26:24 +0200 Subject: [PATCH 10/16] Some progress towards sending the map --- apps/amethyst/lib/api/game.ex | 9 +++++++++ apps/amethyst/lib/apps/connection_handler.ex | 10 ++++++++-- apps/example_game/lib/example/game.ex | 18 +++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex index 95ad5fb..3dc1453 100644 --- a/apps/amethyst/lib/api/game.ex +++ b/apps/amethyst/lib/api/game.ex @@ -88,6 +88,15 @@ defmodule Amethyst.API.Game do mod.accept_teleport(self(), id, refs) end @doc """ + The terrain of a specific chunk column. This is automatically used to load chunks for a player. + + For now, this data must be formatted as a 3D list, indexed as [y][z][x]. + """ + @callback chunk(from :: pid(), {x :: integer(), z :: integer()}, state_refs :: map()) :: [[[pos_integer()]]] + def chunk(%{:mod => mod, :refs => refs}, pos) do + mod.chunk(self(), pos, refs) + end + @doc """ Whether or not this game instance can be joined by a new player. This should include basic logic such as if joining makes sense, for instance if the game is full or if the game has already started. """ diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index d36a090..e783bf0 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -119,8 +119,14 @@ defmodule Amethyst.ConnectionHandler do end) end - defp process_chunk(_chunk, _state) do - # TODO: Actually ask the game for the desired chunk then send the packets. + defp process_chunk(chunk, state) do + chunk_array = Amethyst.API.Game.chunk(Map.get(state, :game), chunk) + + # TODO: Actually do heightmaps + import Amethyst.NBT.Write + heightmaps = compound(%{}) + + data = Enum.chunk_every(chunk_array, 16) end defp handle_packet(id, data, connstate, version, state) do diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 7e8caa5..1c308cf 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -12,7 +12,7 @@ defmodule Example.Game do def login(from, cfg, refs) do Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}") Logger.info("The refs for this game are #{inspect(refs)}") - {:accept, {0.0, 0.0, 0.0}, {0.0, 0.0}} + {:accept, {0.0, 10.0, 0.0}, {0.0, 0.0}} end @impl true @@ -38,4 +38,20 @@ defmodule Example.Game do def joinable?(_refs) do true end + + @impl true + def chunk(from, {x, z}, _state_refs) do + Logger.info("Player at #{inspect(from)} wants to know chunk #{x}, #{z}") + (0..255) |> Enum.map(fn y -> + if y < 5 do + (0..15) |> Enum.map(fn _z -> + (0..15) |> Enum.map(fn _x -> 1 end) + end) + else + (0..15) |> Enum.map(fn _z -> + (0..15) |> Enum.map(fn _x -> 0 end) + end) + end + end) + end end From 40792b8d94faac84466fb49a8c358012ff8f28ce Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 1 Oct 2024 19:43:29 +0200 Subject: [PATCH 11/16] extremely minor --- apps/amethyst/lib/apps/connection_handler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index e783bf0..6e56076 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -126,7 +126,7 @@ defmodule Amethyst.ConnectionHandler do import Amethyst.NBT.Write heightmaps = compound(%{}) - data = Enum.chunk_every(chunk_array, 16) + data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air end defp handle_packet(id, data, connstate, version, state) do From c3c3f832864a5312b7a732bb46e9397887e89d96 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Tue, 1 Oct 2024 20:01:14 +0200 Subject: [PATCH 12/16] more work on sending map --- apps/amethyst/lib/apps/connection_handler.ex | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index 6e56076..f7509f2 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -120,13 +120,27 @@ defmodule Amethyst.ConnectionHandler do end defp process_chunk(chunk, state) do + import Amethyst.NBT.Write + alias Amethyst.Minecraft.Write + chunk_array = Amethyst.API.Game.chunk(Map.get(state, :game), chunk) # TODO: Actually do heightmaps - import Amethyst.NBT.Write heightmaps = compound(%{}) data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air + |> Enum.reduce(fn chunk_section, acc -> + blocks = chunk_section |> List.flatten() + block_count = blocks + |> Enum.map(&(if &1 == 0, do: 0, else: 1)) + |> Enum.sum() + + # TODO: Proper paletting + acc <> Write.short(block_count) <> Write.ubyte(15) + end) + + # TODO: Use paletting + end defp handle_packet(id, data, connstate, version, state) do From af76cace7665b121013b473d125b010d44806c4e Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Wed, 2 Oct 2024 15:47:21 +0200 Subject: [PATCH 13/16] Finish trying to implement sending chunks --- apps/amethyst/lib/apps/connection_handler.ex | 68 +++++++++++++++++++- mix.lock | 1 + 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index f7509f2..d61b0e1 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -124,8 +124,10 @@ defmodule Amethyst.ConnectionHandler do alias Amethyst.Minecraft.Write chunk_array = Amethyst.API.Game.chunk(Map.get(state, :game), chunk) + {cx, cz} = chunk # TODO: Actually do heightmaps + # TODO: Doing all this processing could be at home somewhere else heightmaps = compound(%{}) data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air @@ -135,12 +137,72 @@ defmodule Amethyst.ConnectionHandler do |> Enum.map(&(if &1 == 0, do: 0, else: 1)) |> Enum.sum() - # TODO: Proper paletting - acc <> Write.short(block_count) <> Write.ubyte(15) + # Put together the palette + unique_blocks = MapSet.new(blocks) + min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil() + + paletted_container_data = case min_bpe do + 0 -> <<0::8>> <> + # SINGLE VALUED + Write.varint(MapSet.to_list(unique_blocks)[0]) <> + Write.varint(0) # No data, empty pallette + min_bpe when min_bpe in 1..8 -> + # INDIRECT + # Minimum bpe accepted by minecraft is 4 + bpe = max(min_bpe, 4) + palette = MapSet.to_list(unique_blocks) |> + Enum.with_index() |> + Map.new(fn {i, v} -> {v, i} end) + paletted_blocks = blocks |> + Enum.map(&(Map.get(palette, &1))) + paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe) + + Write.ubyte(bpe) <> + Write.varint(map_size(palette)) <> + Enum.reduce(0..map_size(palette), "", fn i, acc -> + acc <> Write.varint(Map.get(palette, i)) + end) <> + Write.varint(floor(bit_size(paletted_data) / 64)) <> + paletted_data + _ -> + # DIRECT + data = long_aligned_bit_string_reduce(blocks, 15) + Write.ubyte(15) <> + Write.varint(floor(bit_size(data) / 64)) <> + data + end + + # TODO: Send biome data, if that even makes sense + acc <> Write.short(block_count) <> paletted_container_data <> <<0::8, 0::8>> end) - # TODO: Use paletting + send(self(), {:send_packet, %{ + packet_type: :chunk_data_and_update_light, + chunk_x: cx, chunk_z: cz, + heightmaps: heightmaps, + data: data, + block_entities: [], + # TODO: Light + sky_light_mask: <<0>>, + block_light_mask: <<0>>, + empty_sky_light_mask: <<0>>, + empty_block_light_mask: <<0>>, + sky_light_arrays: [], + block_light_arrays: [] + }}) + end + defp long_aligned_bit_string_reduce(values, bpe) do + values |> Enum.reduce("", fn value, acc -> + next = acc <> <> + # man i hope they dont suddenly change the size of a long + if rem(bit_size(next), 64) + bpe < 64 do + # gotta pad it + next <> <<0::big-size(64 - rem(bit_size(next), 64))>> + else + next + end + end) end defp handle_packet(id, data, connstate, version, state) do diff --git a/mix.lock b/mix.lock index bd7fcac..40e969f 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "elixir_math": {:hex, :elixir_math, "0.1.2", "5655bdf7f34e30906f31cdcf3031b43dd522ce8d2936b60ad4696b2c752bf5c9", [:mix], [], "hexpm", "34f4e4384903097a8ec566784fa8e9aa2b741247d225741f07cc48250c2aa64c"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, From d0a5b55ae88a996284fea934f80150bb02fbeaf4 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Wed, 2 Oct 2024 17:35:28 +0200 Subject: [PATCH 14/16] try fixing extremely weird bug --- apps/amethyst/lib/apps/connection_handler.ex | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index d61b0e1..758f048 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -85,12 +85,16 @@ defmodule Amethyst.ConnectionHandler do end chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), Map.get(state, :view_distance, 16))) new_chunks = MapSet.difference(chunks, prev_chunks) - # Process these chunks, we can process all chunks in parallel - Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), - new_chunks, - fn chunk -> process_chunk(chunk, state) end, - [ordered: false] - ) |> Stream.run() + # We can process all chunks in parallel + # me = self() + # Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), + # new_chunks, + # fn chunk -> process_chunk(me, chunk, state) end, + # [ordered: false] + # ) |> Stream.run() + if new_chunks |> Enum.map(&process_chunk(self(), &1, state)) |> Enum.any?(&(&1 != :ok)) do + Logger.error("something happened while processing chunks") + end end loop(socket, connstate, version, state) {:send_packet, packet} -> @@ -119,7 +123,7 @@ defmodule Amethyst.ConnectionHandler do end) end - defp process_chunk(chunk, state) do + defp process_chunk(to, chunk, state) do import Amethyst.NBT.Write alias Amethyst.Minecraft.Write @@ -131,20 +135,17 @@ defmodule Amethyst.ConnectionHandler do heightmaps = compound(%{}) data = Enum.chunk_every(chunk_array, 16, 16, 0) # 0 -> air - |> Enum.reduce(fn chunk_section, acc -> + |> Enum.reduce("", fn chunk_section, acc -> blocks = chunk_section |> List.flatten() - block_count = blocks - |> Enum.map(&(if &1 == 0, do: 0, else: 1)) - |> Enum.sum() + block_count = blocks |> Enum.filter(&(&1 != 0)) |> length # Put together the palette unique_blocks = MapSet.new(blocks) min_bpe = MapSet.size(unique_blocks) |> :math.log2() |> ceil() - paletted_container_data = case min_bpe do 0 -> <<0::8>> <> # SINGLE VALUED - Write.varint(MapSet.to_list(unique_blocks)[0]) <> + Write.varint(MapSet.to_list(unique_blocks) |> List.first()) <> Write.varint(0) # No data, empty pallette min_bpe when min_bpe in 1..8 -> # INDIRECT @@ -155,6 +156,7 @@ defmodule Amethyst.ConnectionHandler do Map.new(fn {i, v} -> {v, i} end) paletted_blocks = blocks |> Enum.map(&(Map.get(palette, &1))) + Logger.debug("got here") paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe) Write.ubyte(bpe) <> @@ -166,6 +168,7 @@ defmodule Amethyst.ConnectionHandler do paletted_data _ -> # DIRECT + Logger.debug("got here with bpe #{min_bpe}") data = long_aligned_bit_string_reduce(blocks, 15) Write.ubyte(15) <> Write.varint(floor(bit_size(data) / 64)) <> @@ -176,7 +179,8 @@ defmodule Amethyst.ConnectionHandler do acc <> Write.short(block_count) <> paletted_container_data <> <<0::8, 0::8>> end) - send(self(), {:send_packet, %{ + Logger.debug("Asking to send chunk packet...") + send(to, {:send_packet, %{ packet_type: :chunk_data_and_update_light, chunk_x: cx, chunk_z: cz, heightmaps: heightmaps, @@ -190,11 +194,12 @@ defmodule Amethyst.ConnectionHandler do sky_light_arrays: [], block_light_arrays: [] }}) + :ok end defp long_aligned_bit_string_reduce(values, bpe) do values |> Enum.reduce("", fn value, acc -> - next = acc <> <> + next = acc <> <> # man i hope they dont suddenly change the size of a long if rem(bit_size(next), 64) + bpe < 64 do # gotta pad it From 18a80874dfd21351e7f6342e21ffe0ea8310b8a2 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 3 Oct 2024 14:55:40 +0200 Subject: [PATCH 15/16] Fix weird bug (dont use vscode) --- apps/amethyst/lib/apps/connection_handler.ex | 26 ++++++++------------ apps/example_game/lib/example/game.ex | 4 +-- config/config.exs | 2 +- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index 758f048..a48ed5c 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -86,15 +86,12 @@ defmodule Amethyst.ConnectionHandler do chunks = MapSet.new(visible_chunks_from(elem(cp, 0), elem(cp, 1), Map.get(state, :view_distance, 16))) new_chunks = MapSet.difference(chunks, prev_chunks) # We can process all chunks in parallel - # me = self() - # Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), - # new_chunks, - # fn chunk -> process_chunk(me, chunk, state) end, - # [ordered: false] - # ) |> Stream.run() - if new_chunks |> Enum.map(&process_chunk(self(), &1, state)) |> Enum.any?(&(&1 != :ok)) do - Logger.error("something happened while processing chunks") - end + me = self() + Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), + new_chunks, + fn chunk -> process_chunk(me, chunk, state) end, + [ordered: false] + ) |> Stream.run() end loop(socket, connstate, version, state) {:send_packet, packet} -> @@ -156,19 +153,17 @@ defmodule Amethyst.ConnectionHandler do Map.new(fn {i, v} -> {v, i} end) paletted_blocks = blocks |> Enum.map(&(Map.get(palette, &1))) - Logger.debug("got here") paletted_data = long_aligned_bit_string_reduce(paletted_blocks, bpe) Write.ubyte(bpe) <> Write.varint(map_size(palette)) <> - Enum.reduce(0..map_size(palette), "", fn i, acc -> + Enum.reduce(0..(map_size(palette)-1), "", fn i, acc -> acc <> Write.varint(Map.get(palette, i)) end) <> Write.varint(floor(bit_size(paletted_data) / 64)) <> paletted_data _ -> # DIRECT - Logger.debug("got here with bpe #{min_bpe}") data = long_aligned_bit_string_reduce(blocks, 15) Write.ubyte(15) <> Write.varint(floor(bit_size(data) / 64)) <> @@ -179,7 +174,6 @@ defmodule Amethyst.ConnectionHandler do acc <> Write.short(block_count) <> paletted_container_data <> <<0::8, 0::8>> end) - Logger.debug("Asking to send chunk packet...") send(to, {:send_packet, %{ packet_type: :chunk_data_and_update_light, chunk_x: cx, chunk_z: cz, @@ -198,12 +192,12 @@ defmodule Amethyst.ConnectionHandler do end defp long_aligned_bit_string_reduce(values, bpe) do - values |> Enum.reduce("", fn value, acc -> - next = acc <> <> + values |> Enum.reduce(<<>>, fn value, acc -> + next = <> # man i hope they dont suddenly change the size of a long if rem(bit_size(next), 64) + bpe < 64 do # gotta pad it - next <> <<0::big-size(64 - rem(bit_size(next), 64))>> + <> else next end diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 1c308cf..92097b4 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -40,8 +40,8 @@ defmodule Example.Game do end @impl true - def chunk(from, {x, z}, _state_refs) do - Logger.info("Player at #{inspect(from)} wants to know chunk #{x}, #{z}") + def chunk(_from, {x, z}, _state_refs) do + # Logger.info("Player at #{inspect(from)} wants to know chunk #{x}, #{z}") (0..255) |> Enum.map(fn y -> if y < 5 do (0..15) |> Enum.map(fn _z -> diff --git a/config/config.exs b/config/config.exs index 000c5f1..27de4af 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,5 +10,5 @@ import Config config :logger, :console, level: :debug, - format: "$date $time [$level] $metadata$message", + format: "$date $time [$level] $metadata$message\n", metadata: [] From 847644d8cf3c31577b414ceaec41ef67555ea943 Mon Sep 17 00:00:00 2001 From: Kodi Craft Date: Thu, 3 Oct 2024 17:55:43 +0200 Subject: [PATCH 16/16] Performance tweaks --- apps/amethyst/lib/apps/connection_handler.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/amethyst/lib/apps/connection_handler.ex b/apps/amethyst/lib/apps/connection_handler.ex index a48ed5c..f078872 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -87,11 +87,13 @@ defmodule Amethyst.ConnectionHandler do new_chunks = MapSet.difference(chunks, prev_chunks) # We can process all chunks in parallel me = self() - Task.Supervisor.async_stream(state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor), + ts = state |> Map.get(:game) |> Map.get(:refs) |> Map.get(:task_supervisor) + Task.Supervisor.async(ts, fn -> + Task.Supervisor.async_stream(ts, new_chunks, fn chunk -> process_chunk(me, chunk, state) end, [ordered: false] - ) |> Stream.run() + ) |> Stream.run() end) end loop(socket, connstate, version, state) {:send_packet, packet} ->