266 lines
9.5 KiB
Elixir
266 lines
9.5 KiB
Elixir
# 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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?
|
|
<<id::32>> = :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
|