This commit is contained in:
Kodi Craft 2024-10-03 20:36:26 +02:00
parent 7821b20758
commit f083a99702
Signed by: kodi
GPG Key ID: 69D9EED60B242822
10 changed files with 410 additions and 42 deletions

View File

@ -16,8 +16,9 @@
defmodule Amethyst.API.Game do defmodule Amethyst.API.Game do
@moduledoc """ @moduledoc """
This module includes the interface for defining and registering This behaviour should be implemented by any Amethyst Game. It additionally
a game with Amethyst. contains functions that the internal connection handler code uses to more
conveniently call a game's callbacks.
""" """
@doc """ @doc """
@ -32,6 +33,9 @@ defmodule Amethyst.API.Game do
You may either :accept or :reject the player for whatever reason, avoid 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. 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`. 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 The PID received in 'from' will be the one that calls all callbacks
@ -43,7 +47,7 @@ defmodule Amethyst.API.Game do
- 'player_cfg' is a keyword list containing the configuration passed by the game client - 'player_cfg' is a keyword list containing the configuration passed by the game client
- 'state_refs' are your references (see `instantiate/1`) - '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 def login(%{:mod => mod, :refs => refs}, player_cfg) do
mod.login(self(), player_cfg, refs) mod.login(self(), player_cfg, refs)
end end
@ -60,6 +64,39 @@ defmodule Amethyst.API.Game do
mod.player_position(self(), {x, y, z}, refs) mod.player_position(self(), {x, y, z}, refs)
end end
@doc """ @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.
- '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 """
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 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. if joining makes sense, for instance if the game is full or if the game has already started.
""" """

View File

@ -60,6 +60,42 @@ defmodule Amethyst.ConnectionHandler do
{:set_version, newversion} -> {:set_version, newversion} ->
Logger.debug("Switching to version #{newversion} from #{version}") Logger.debug("Switching to version #{newversion} from #{version}")
loop(socket, connstate, newversion, state) 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)
# We can process all chunks in parallel
me = self()
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() end)
end
loop(socket, connstate, version, state)
{:send_packet, packet} -> {:send_packet, packet} ->
Logger.debug("Sending packet #{inspect(packet)}") Logger.debug("Sending packet #{inspect(packet)}")
send_packet(socket, connstate, packet, version) send_packet(socket, connstate, packet, version)
@ -73,6 +109,103 @@ defmodule Amethyst.ConnectionHandler do
end end
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(to, chunk, state) do
import Amethyst.NBT.Write
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
|> Enum.reduce("", fn chunk_section, acc ->
blocks = chunk_section |> List.flatten()
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) |> List.first()) <>
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)-1), "", 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)
send(to, {: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: []
}})
:ok
end
defp long_aligned_bit_string_reduce(values, bpe) do
values |> Enum.reduce(<<>>, fn value, acc ->
next = <<acc::bitstring, value::size(bpe)-big>>
# 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::bitstring, 0::big-size(64 - rem(bit_size(next), 64))>>
else
next
end
end)
end
defp handle_packet(id, data, connstate, version, state) do defp handle_packet(id, data, connstate, version, state) do
try do try do
packet = connstate.deserialize(id, version, data) packet = connstate.deserialize(id, version, data)

View File

@ -21,7 +21,7 @@ defmodule Amethyst.TCPListener do
""" """
def accept(port) 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}") Logger.info("Listening on port #{port}")
loop_acceptor(socket) loop_acceptor(socket)
end end

View File

@ -258,6 +258,8 @@ defmodule Amethyst.ConnectionState.Configuration do
%{id: "minecraft:stalagmite", data: generic_damage}, %{id: "minecraft:stalagmite", data: generic_damage},
%{id: "minecraft:outside_border", data: generic_damage}, %{id: "minecraft:outside_border", data: generic_damage},
%{id: "minecraft:generic_kill", 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}}) send(self(), {:send_packet, %{packet_type: :finish_configuration}})
@ -269,33 +271,45 @@ defmodule Amethyst.ConnectionState.Configuration do
send(self(), {:set_state, Amethyst.ConnectionState.Play}) send(self(), {:set_state, Amethyst.ConnectionState.Play})
game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create() game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create()
state = state |> Map.put(:game, game) state = state |> Map.put(:game, game)
if Amethyst.API.Game.login(game, state) == :reject do login = Amethyst.API.Game.login(game, state)
send(self(), {:disconnect, "Default game rejected connection"}) case login do
:ok :reject ->
else send(self(), {:disconnect, "Default game rejected connection"})
send(self(), {:send_packet, %{ :ok
packet_type: :login, {:accept, {x, y, z}, {yaw, pitch}} ->
entity_id: 0, send(self(), {:send_packet, %{
is_hardcore: false, packet_type: :login,
dimensions: [%{name: "minecraft:overworld"}], entity_id: 0,
max_players: 0, is_hardcore: false,
view_distance: 16, dimensions: [%{name: "minecraft:overworld"}],
simulation_distance: 16, max_players: 0,
reduced_debug_info: false, view_distance: 16,
enable_respawn_screen: true, simulation_distance: 16,
do_limited_crafting: false, reduced_debug_info: false,
dimension_type: 0, enable_respawn_screen: true,
dimension_name: "minecraft:overworld", do_limited_crafting: false,
hashed_seed: 0, dimension_type: 0,
game_mode: 3, dimension_name: "minecraft:overworld",
previous_game_mode: -1, hashed_seed: 0,
is_debug: false, game_mode: 3,
is_flat: false, previous_game_mode: -1,
death_location: nil, is_debug: false,
portal_cooldown: 0, is_flat: false,
enforces_secure_chat: false death_location: nil,
}}) portal_cooldown: 0,
state 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
}})
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(floor(x), 16),
chunk_z: div(floor(z), 16)
}})
send(self(), {:set_position, {x, y, z}})
state
end end
end end

View File

@ -70,7 +70,7 @@ defmodule Amethyst.ConnectionState.Login do
uuid: player_uuid, uuid: player_uuid,
username: name, username: name,
properties: [], properties: [],
strict_error_handling: false strict_error_handling: true
}}) }})
:ok :ok
end end

View File

@ -16,18 +16,18 @@
defmodule Amethyst.ConnectionState.Macros do defmodule Amethyst.ConnectionState.Macros do
require Logger require Logger
defmacro defpacket_serverbound(name, id, version, signature) do defmacro defpacket_serverbound(name, id, version, signature, where \\ true) do
quote 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, ""} = Amethyst.ConnectionState.Macros.read_signature(data, unquote(signature))
packet |> Map.put(:packet_type, unquote(name)) packet |> Map.put(:packet_type, unquote(name))
end end
end end
end end
defmacro defpacket_clientbound(name, id, version, signature) do defmacro defpacket_clientbound(name, id, version, signature, where \\ true) do
quote 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 if Amethyst.ConnectionState.Macros.check_type(packet, unquote(signature)) do
Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature)) Amethyst.Minecraft.Write.varint(unquote(id)) <> Amethyst.ConnectionState.Macros.write_signature(packet, unquote(signature))
else else
@ -79,7 +79,6 @@ defmodule Amethyst.ConnectionState.Macros do
def write_signature(packet, signature) do def write_signature(packet, signature) do
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 case type do
{:optional, {:compound, signature}} -> {:optional, {:compound, signature}} ->
case Map.get(packet, name) do case Map.get(packet, name) do
@ -96,6 +95,7 @@ defmodule Amethyst.ConnectionState.Macros do
Enum.reduce(Map.get(packet, name), "", fn item, acc -> Enum.reduce(Map.get(packet, name), "", fn item, acc ->
acc <> write_signature(item, signature) acc <> write_signature(item, signature)
end) end)
{:literal, {type, value}} -> acc <> apply(Write, type, [value])
t -> acc <> apply(Write, t, [Map.get(packet, name)]) t -> acc <> apply(Write, t, [Map.get(packet, name)])
end end
end) end)
@ -105,7 +105,12 @@ defmodule Amethyst.ConnectionState.Macros do
try do try do
Enum.all?(signature, fn {name, type} -> Enum.all?(signature, fn {name, type} ->
case Map.get(packet, name, :missing) do 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 value -> case type_matches(value, type) do
true -> true true -> true
false -> throw {:mismatch, name, value, type} false -> throw {:mismatch, name, value, type}
@ -126,8 +131,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, :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, :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, :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, :float) when is_number(value), do: true
def type_matches(value, :double) when is_float(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, :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, :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 def type_matches(value, :uuid) when is_binary(value) and byte_size(value) == 36, do: true

View File

@ -25,6 +25,28 @@ defmodule Amethyst.ConnectionState.Play do
""" """
Macros.defpacket_clientbound :disconnect, 0x1D, 767, [reason: :nbt] 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, [ Macros.defpacket_clientbound :login, 0x2B, 767, [
entity_id: :int, entity_id: :int,
is_hardcore: :bool, is_hardcore: :bool,
@ -49,6 +71,133 @@ defmodule Amethyst.ConnectionState.Play do
portal_cooldown: :varint, portal_cooldown: :varint,
enforces_secure_chat: :bool, 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 :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...
]
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 def disconnect(reason) do
%{ %{

View File

@ -12,17 +12,46 @@ defmodule Example.Game do
def login(from, cfg, refs) do def login(from, cfg, refs) do
Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}") Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}")
Logger.info("The refs for this game are #{inspect(refs)}") Logger.info("The refs for this game are #{inspect(refs)}")
:accept {:accept, {0.0, 10.0, 0.0}, {0.0, 0.0}}
end end
@impl true @impl true
@spec player_position(any(), {any(), any(), any()}, any()) :: :ok
def player_position(from, {x, y, z}, _refs) do def player_position(from, {x, y, z}, _refs) do
Logger.info("Player at #{inspect(from)} moved to #{x}, #{y}, #{z}") Logger.info("Player at #{inspect(from)} moved to #{x}, #{y}, #{z}")
:ok :ok
end 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)}")
:ok
end
@impl true @impl true
def joinable?(_refs) do def joinable?(_refs) do
true true
end 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 end

View File

@ -10,5 +10,5 @@
import Config import Config
config :logger, :console, config :logger, :console,
level: :debug, level: :debug,
format: "$date $time [$level] $metadata$message", format: "$date $time [$level] $metadata$message\n",
metadata: [] metadata: []

View File

@ -2,6 +2,7 @@
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "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"}, "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"}, "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"}, "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"}, "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"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},