diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex index 4cca9a2..3dc1453 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 """ @@ -32,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 @@ -43,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 @@ -60,6 +64,39 @@ 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. + + - '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 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 ac43cbe..f078872 100644 --- a/apps/amethyst/lib/apps/connection_handler.ex +++ b/apps/amethyst/lib/apps/connection_handler.ex @@ -60,6 +60,42 @@ 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) + # 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} -> Logger.debug("Sending packet #{inspect(packet)}") send_packet(socket, connstate, packet, version) @@ -73,6 +109,103 @@ 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(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 = <> + # man i hope they dont suddenly change the size of a long + if rem(bit_size(next), 64) + bpe < 64 do + # gotta pad it + <> + else + next + end + end) + end + defp handle_packet(id, data, connstate, version, state) do try do packet = connstate.deserialize(id, version, data) 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..7426b65 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}}) @@ -269,33 +271,45 @@ 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 + }}) + 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 diff --git a/apps/amethyst/lib/states/login.ex b/apps/amethyst/lib/states/login.ex index ae9fa97..e50030e 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/macros.ex b/apps/amethyst/lib/states/macros.ex index a6def10..d52f767 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 @@ -96,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) @@ -105,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} @@ -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, :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 8830de0..e91c06d 100644 --- a/apps/amethyst/lib/states/play.ex +++ b/apps/amethyst/lib/states/play.ex @@ -25,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, @@ -49,6 +71,133 @@ 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, + 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 %{ diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex index 3b66c48..92097b4 100644 --- a/apps/example_game/lib/example/game.ex +++ b/apps/example_game/lib/example/game.ex @@ -12,17 +12,46 @@ 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 + {:accept, {0.0, 10.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)}") + :ok + end + @impl true 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 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: [] 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"},