# 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 :keep_alive, 0x26, 767, [id: :long]
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,
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,
]
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,
z: :double,
yaw: :float,
pitch: :float,
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 :serverbound_plugin_message, 0x12, 767, [channel: :string, data: :raw]
Macros.defpacket_serverbound :keep_alive, 0x18, 767, [id: :long]
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...
]
Macros.defpacket_serverbound :set_player_on_ground, 0x1D, 767, [on_ground: :bool]
Macros.defpacket_serverbound :player_command, 0x25, 767, [eid: :varint, action_id: :varint, jump_boost: :varint]
def handle(%{packet_type: :confirm_teleportation, teleport_id: id}, 767, state) do
Amethyst.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.Game.player_position(state[:game], {x, y, z})
Amethyst.Game.player_rotation(state[:game], {yaw, pitch})
:ok
end
def handle(%{packet_type: :serverbound_plugin_message, channel: channel, data: _}, 767, _state) do
Logger.debug("Got plugin message on #{channel}")
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.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.Game.player_rotation(state[:game], {yaw, pitch})
:ok
end
def handle(%{packet_type: :set_player_on_ground, on_ground: _}, 767, _state) do
:ok # Again, don't trust the client for something we can compute
end
def handle(%{packet_type: :player_command, eid: _eid, action_id: aid, jump_boost: _horse_jump}, 767, _state) do
# TODO: Actually handle these events
case aid do
0 -> # Start sneaking
:ok
1 -> # Stop sneaking
:ok
2 -> # Leave bed
:ok
3 -> # Start sprinting
:ok
4 -> # Stop sprinting
:ok
5 -> # Start horse jump
:ok
6 -> # Stop horse jump
:ok
7 -> # Open vehicle inventory
:ok
8 -> # Start elytra flying
:ok
_ -> raise RuntimeError, "Unknown Player Command Action ID"
end
end
def handle(%{packet_type: :keep_alive, id: id}, 767, state) do
ka = state |> Map.get(:keepalive)
send(ka, {:respond, id})
:ok
end
# This function should be started on a new task under the connection handler
# and is responsible for keepalive logic.
def keepalive_loop(player) do
Process.link(player) # Is it fine to do this on loop?
<> = :rand.bytes(4)
send(player, {:send_packet, %{packet_type: :keep_alive, id: id}})
receive do
{:respond, ^id} ->
:timer.sleep(250)
keepalive_loop(player)
after
15_000 ->
send(player, {:disconnect, "Timed out! Connection overloaded?"})
end
end
def disconnect(reason) do
%{
packet_type: :disconnect,
reason: {:compound, %{
"text" => {:string, reason}
}}
}
end
end