merge
This commit is contained in:
parent
7821b20758
commit
f083a99702
@ -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.
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
%{
|
%{
|
||||||
|
@ -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
|
||||||
|
@ -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: []
|
||||||
|
1
mix.lock
1
mix.lock
@ -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"},
|
||||||
|
Loading…
Reference in New Issue
Block a user