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
@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.
"""

View File

@ -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 = <<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
try do
packet = connstate.deserialize(id, version, data)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
%{

View File

@ -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

View File

@ -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: []

View File

@ -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"},