├── .formatter.exs ├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── config └── config.exs ├── coveralls.json ├── docs └── screenshot.png ├── lib ├── minecraft │ ├── application.ex │ ├── chunk.ex │ ├── connection.ex │ ├── crypto.ex │ ├── crypto │ │ ├── aes.ex │ │ └── sha.ex │ ├── nif.ex │ ├── packet.ex │ ├── packet │ │ ├── client │ │ │ ├── handshake.ex │ │ │ ├── login │ │ │ │ ├── encryption_response.ex │ │ │ │ └── login_start.ex │ │ │ ├── play │ │ │ │ ├── client_settings.ex │ │ │ │ ├── client_status.ex │ │ │ │ ├── keep_alive.ex │ │ │ │ ├── player_look.ex │ │ │ │ ├── player_position.ex │ │ │ │ ├── player_position_and_look.ex │ │ │ │ ├── plugin_message.ex │ │ │ │ └── teleport_confirm.ex │ │ │ └── status │ │ │ │ ├── ping.ex │ │ │ │ └── request.ex │ │ └── server │ │ │ ├── login │ │ │ ├── encryption_request.ex │ │ │ └── login_success.ex │ │ │ ├── play │ │ │ ├── chunk_data.ex │ │ │ ├── join_game.ex │ │ │ ├── keep_alive.ex │ │ │ ├── player_abilities.ex │ │ │ ├── player_position_and_look.ex │ │ │ └── spawn_position.ex │ │ │ └── status │ │ │ ├── pong.ex │ │ │ └── response.ex │ ├── protocol.ex │ ├── protocol │ │ └── handler.ex │ ├── server.ex │ ├── state_machine.ex │ ├── users.ex │ └── world.ex └── mix │ └── tasks │ └── compile.nifs.ex ├── mix.exs ├── mix.lock ├── priv └── .gitkeep ├── src ├── biome.c ├── biome.h ├── chunk.c ├── chunk.h ├── nifs.c ├── perlin.c └── perlin.h └── test ├── minecraft ├── crypto │ └── sha_test.exs ├── crypto_test.exs ├── integration │ └── integration_test.exs ├── nif_test.exs ├── packet_test.exs └── world_test.exs ├── support └── test_client.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: ex_doc 11 | versions: 12 | - 0.23.0 13 | - 0.24.0 14 | - 0.24.1 15 | - dependency-name: excoveralls 16 | versions: 17 | - 0.13.4 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | minecraft-*.tar 24 | 25 | # Elixir Language Server 26 | .elixir_ls 27 | 28 | # Visual Studio Code config 29 | .vscode 30 | 31 | # Compiled NIFs 32 | *.so 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.6.6 4 | otp_release: 5 | - 20.3.8 6 | sudo: false 7 | env: 8 | - MIX_ENV=test 9 | script: mix coveralls.travis 10 | after_script: 11 | - mix deps.get --only docs 12 | - MIX_ENV=docs mix inch.report 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Oliver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ERL_INCLUDE_PATH=$(shell erl -eval 'io:format("~s~n", [lists:concat([code:root_dir(), "/erts-", erlang:system_info(version), "/include"])])' -s init stop -noshell) 2 | 3 | all: priv/nifs.so 4 | 5 | priv/nifs.so: src/nifs.c src/perlin.c src/chunk.c src/biome.c 6 | cc -Wall -Wextra -Wpedantic -O3 -fPIC -shared -std=c99 -I$(ERL_INCLUDE_PATH) -o priv/nifs.so src/nifs.c src/perlin.c src/chunk.c src/biome.c 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft 2 | 3 | [![Build Status](https://travis-ci.com/thecodeboss/minecraft.svg?branch=master)](https://travis-ci.com/thecodeboss/minecraft) 4 | [![Inline docs](http://inch-ci.org/github/thecodeboss/minecraft.svg)](http://inch-ci.org/github/thecodeboss/minecraft) 5 | [![Coverage Status](https://coveralls.io/repos/github/thecodeboss/minecraft/badge.svg?branch=master)](https://coveralls.io/github/thecodeboss/minecraft?branch=master) 6 | [![Hex.pm version](https://img.shields.io/hexpm/v/minecraft.svg?style=flat-square)](https://hex.pm/packages/minecraft) 7 | [![Hex.pm downloads](https://img.shields.io/hexpm/dt/minecraft.svg?style=flat-square)](https://hex.pm/packages/minecraft) 8 | [![License](https://img.shields.io/hexpm/l/minecraft.svg?style=flat-square)](https://hex.pm/packages/minecraft) 9 | 10 | A Minecraft server implementation in Elixir. Until this reaches version 1.0, please do not consider it ready for running real Minecraft servers (unless you're adventurous). 11 | 12 | You can view [the documentation on Hex](https://hexdocs.pm/minecraft/). 13 | 14 | ![Screenshot](./docs/screenshot.png) 15 | 16 | ## Minecraft Protocol 17 | 18 | The Minecraft Protocol is documented on [wiki.vg](http://wiki.vg/Protocol). The current goal is to support version (1.12.2, protocol 340). 19 | 20 | ## To-do 21 | 22 | The following list of to-do items should be enough to be able to play on the server, at least to the most basic extent. 23 | 24 | ### General 25 | 26 | - [x] World Generation 27 | - [ ] World in-memory storage 28 | - [ ] World persistence on disk 29 | - [ ] Core server logic (this is a catch-all) 30 | 31 | ### Handshake Packets 32 | 33 | - [x] Client: Handshake 34 | 35 | ### Status Packets 36 | 37 | - [x] Client: Request 38 | - [x] Server: Response 39 | - [x] Client: Ping 40 | - [x] Server: Pong 41 | 42 | ### Login Packets 43 | 44 | - [x] Client: Login Start 45 | - [x] Server: Encryption Request 46 | - [x] Client: Encryption Response 47 | - [ ] _(optional)_ Server: Set Compression 48 | - [x] Server: Login Success 49 | - [ ] Server: Disconnect 50 | 51 | ### Play Packets 52 | 53 | - [x] Server: Join Game 54 | - [x] Server: Spawn Position 55 | - [x] Server: Player Abilities 56 | - [x] Client: Plugin Message 57 | - [x] Client: Client Settings 58 | - [x] Server: Player Position and Look 59 | - [x] Client: Teleport Confirm 60 | - [x] Client: Player Position and Look 61 | - [x] Client: Client Status 62 | - [ ] Server: Window Items 63 | - [x] Server: Chunk Data 64 | - [ ] Client: Player 65 | - [x] Client: Player Position 66 | - [x] Client: Player Look 67 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :minecraft, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:minecraft, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/minecraft/packet/client", 4 | "lib/minecraft/packet/server", 5 | "lib/minecraft/world/nif.ex", 6 | "lib/mix/tasks/compile.nifs.ex", 7 | "test/support" 8 | ] 9 | } -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeboss/minecraft/325c6f5cfd9fc186b9677710276c75aad70b8156/docs/screenshot.png -------------------------------------------------------------------------------- /lib/minecraft/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | @impl true 6 | def start(_type, _args) do 7 | children = [ 8 | Minecraft.Crypto, 9 | Minecraft.World, 10 | Minecraft.Users, 11 | Minecraft.Server 12 | ] 13 | 14 | opts = [strategy: :one_for_one, name: Minecraft.Supervisor] 15 | Supervisor.start_link(children, opts) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/minecraft/chunk.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Chunk do 2 | defstruct [:resource] 3 | 4 | @type t :: %__MODULE__{resource: binary} 5 | 6 | defimpl Inspect do 7 | @impl true 8 | def inspect(term, _opts) do 9 | {:ok, {x, z}} = Minecraft.NIF.get_chunk_coordinates(term.resource) 10 | "#Chunk" 11 | end 12 | end 13 | 14 | def serialize(%__MODULE__{resource: resource} = _chunk) do 15 | {:ok, data} = Minecraft.NIF.serialize_chunk(resource) 16 | data 17 | end 18 | 19 | def num_sections(%__MODULE__{resource: resource} = _chunk) do 20 | {:ok, num_sections} = Minecraft.NIF.num_chunk_sections(resource) 21 | num_sections 22 | end 23 | 24 | def get_biome_data(%__MODULE__{resource: resource} = _chunk) do 25 | {:ok, biome_data} = Minecraft.NIF.chunk_biome_data(resource) 26 | biome_data 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/minecraft/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Connection do 2 | @moduledoc """ 3 | Maintains the state of a client's connection, and provides utilities for sending and receiving 4 | data. It is designed to be chained in a fashion similar to [`Plug`](https://hexdocs.pm/plug/). 5 | """ 6 | alias Minecraft.Crypto 7 | alias Minecraft.Packet 8 | require Logger 9 | 10 | @has_joined_url "https://sessionserver.mojang.com/session/minecraft/hasJoined?" 11 | 12 | @typedoc """ 13 | The possible states a client/server can be in. 14 | """ 15 | @type state :: :handshake | :status | :login | :play 16 | 17 | @typedoc """ 18 | Allowed ranch transport types. 19 | """ 20 | @type transport :: :ranch_tcp 21 | 22 | @type t :: %__MODULE__{ 23 | protocol_handler: pid, 24 | assigns: %{atom => any} | nil, 25 | settings: %{atom => any} | nil, 26 | current_state: state, 27 | socket: port | nil, 28 | transport: transport | nil, 29 | client_ip: String.t(), 30 | data: binary | nil, 31 | error: any, 32 | protocol_version: integer | nil, 33 | secret: binary | nil, 34 | join: boolean, 35 | state_machine: pid | nil, 36 | encryptor: Crypto.AES.t() | nil, 37 | decryptor: Crypto.AES.t() | nil 38 | } 39 | 40 | defstruct protocol_handler: nil, 41 | assigns: nil, 42 | settings: nil, 43 | current_state: nil, 44 | encryptor: nil, 45 | decryptor: nil, 46 | socket: nil, 47 | transport: nil, 48 | client_ip: nil, 49 | data: nil, 50 | error: nil, 51 | protocol_version: nil, 52 | secret: nil, 53 | join: false, 54 | state_machine: nil 55 | 56 | @doc """ 57 | Assigns a value to a key in the connection. 58 | ## Examples 59 | iex> conn.assigns[:hello] 60 | nil 61 | iex> conn = assign(conn, :hello, :world) 62 | iex> conn.assigns[:hello] 63 | :world 64 | """ 65 | @spec assign(t, atom, term) :: t 66 | def assign(%__MODULE__{assigns: assigns} = conn, key, value) when is_atom(key) do 67 | %__MODULE__{conn | assigns: Map.put(assigns, key, value)} 68 | end 69 | 70 | @doc """ 71 | Closes the `Connection`. 72 | """ 73 | @spec close(t) :: t 74 | def close(conn) do 75 | :ok = conn.transport.close(conn.socket) 76 | 77 | %__MODULE__{conn | socket: nil, transport: nil, state_machine: nil} 78 | end 79 | 80 | @doc """ 81 | Continues receiving messages from the client. 82 | 83 | To prevent a client from flooding our process mailbox, we only receive one message at a time, 84 | and explicitly `continue` to receive messages once we finish processing the ones we have. 85 | """ 86 | @spec continue(t) :: t 87 | def continue(conn) do 88 | :ok = conn.transport.setopts(conn.socket, active: :once) 89 | conn 90 | end 91 | 92 | @doc """ 93 | Starts encrypting messages sent/received over this `Connection`. 94 | """ 95 | @spec encrypt(t, binary) :: t 96 | def encrypt(conn, secret) do 97 | encryptor = %Crypto.AES{key: secret, ivec: secret} 98 | decryptor = %Crypto.AES{key: secret, ivec: secret} 99 | %__MODULE__{conn | encryptor: encryptor, decryptor: decryptor, secret: secret} 100 | end 101 | 102 | @doc """ 103 | Called when `Connection` is ready for the user to join the server. 104 | """ 105 | @spec join(t) :: t 106 | def join(conn) do 107 | %__MODULE__{conn | join: true} 108 | end 109 | 110 | @doc """ 111 | Initializes a `Connection`. 112 | """ 113 | @spec init(pid(), port(), transport()) :: t 114 | def init(protocol_handler, socket, transport) do 115 | {:ok, {client_ip, _port}} = :inet.peername(socket) 116 | client_ip = IO.iodata_to_binary(:inet.ntoa(client_ip)) 117 | :ok = transport.setopts(socket, active: :once) 118 | 119 | Logger.info(fn -> "Client #{client_ip} connected." end) 120 | 121 | %__MODULE__{ 122 | protocol_handler: protocol_handler, 123 | assigns: %{}, 124 | settings: %{}, 125 | current_state: :handshake, 126 | socket: socket, 127 | transport: transport, 128 | client_ip: client_ip, 129 | data: "" 130 | } 131 | end 132 | 133 | @doc """ 134 | Communicates with Mojang servers to verify user login. 135 | """ 136 | @spec verify_login(t) :: t | {:error, :failed_login_verification} 137 | def verify_login(%__MODULE__{} = conn) do 138 | public_key = Crypto.get_public_key() 139 | username = conn.assigns[:username] 140 | hash = Crypto.SHA.sha(conn.secret <> public_key) 141 | 142 | query_params = URI.encode_query(%{username: username, serverId: hash}) 143 | url = @has_joined_url <> query_params 144 | 145 | with %{body: body, status_code: 200} <- HTTPoison.get!(url), 146 | %{"id" => uuid, "name" => ^username} <- Poison.decode!(body) do 147 | assign(conn, :uuid, normalize_uuid(uuid)) 148 | else 149 | _ -> 150 | {:error, :failed_login_verification} 151 | end 152 | end 153 | 154 | @doc """ 155 | Stores data received from the client in this `Connection`. 156 | """ 157 | @spec put_data(t, binary) :: t 158 | def put_data(conn, data) do 159 | {data, conn} = maybe_decrypt(data, conn) 160 | %__MODULE__{conn | data: conn.data <> data} 161 | end 162 | 163 | @doc """ 164 | Puts the `Connection` into the given `error` state. 165 | """ 166 | @spec put_error(t, any) :: t 167 | def put_error(conn, error) do 168 | %__MODULE__{conn | error: error} 169 | end 170 | 171 | @doc """ 172 | Sets the protocol for the `Connection`. 173 | """ 174 | @spec put_protocol(t, integer) :: t 175 | def put_protocol(conn, protocol_version) do 176 | %__MODULE__{conn | protocol_version: protocol_version} 177 | end 178 | 179 | @doc """ 180 | Replaces the `Connection`'s underlying socket. 181 | """ 182 | @spec put_socket(t, port()) :: t 183 | def put_socket(conn, socket) do 184 | %__MODULE__{conn | socket: socket} 185 | end 186 | 187 | @doc """ 188 | Updates the `Connection` state. 189 | """ 190 | @spec put_state(t, state) :: t 191 | def put_state(conn, state) do 192 | %__MODULE__{conn | current_state: state} 193 | end 194 | 195 | @doc """ 196 | Sets a setting for this `Connection`. 197 | """ 198 | @spec put_setting(t, key :: atom, value :: any) :: t 199 | def put_setting(%__MODULE__{settings: settings} = conn, key, value) do 200 | %__MODULE__{conn | settings: Map.put(settings, key, value)} 201 | end 202 | 203 | @doc """ 204 | Pops a packet from the `Connection`. 205 | """ 206 | @spec read_packet(t) :: {:ok, struct, t} | {:error, t} 207 | def read_packet(conn) do 208 | case Packet.deserialize(conn.data, conn.current_state) do 209 | {packet, rest} when is_binary(rest) -> 210 | Logger.debug(fn -> "RECV: #{inspect(packet)}" end) 211 | {:ok, packet, %__MODULE__{conn | data: rest}} 212 | 213 | {{:error, :invalid_packet}, rest} -> 214 | Logger.error(fn -> 215 | "Received an invalid packet from client, closing connection. #{inspect(conn.data)}" 216 | end) 217 | 218 | {:ok, nil, %__MODULE__{conn | data: rest}} 219 | end 220 | end 221 | 222 | @doc """ 223 | Sends a packet to the client. 224 | """ 225 | @spec send_packet(t, struct) :: t | {:error, :closed} 226 | def send_packet(conn, packet) do 227 | Logger.debug(fn -> "SEND: #{inspect(packet)}" end) 228 | 229 | {:ok, raw} = Packet.serialize(packet) 230 | {raw, conn} = maybe_encrypt(raw, conn) 231 | 232 | :ok = conn.transport.send(conn.socket, raw) 233 | conn 234 | end 235 | 236 | defp maybe_decrypt(request, %__MODULE__{decryptor: nil} = conn) do 237 | {request, conn} 238 | end 239 | 240 | defp maybe_decrypt(request, conn) do 241 | {decrypted, decryptor} = Crypto.AES.decrypt(request, conn.decryptor) 242 | {decrypted, %__MODULE__{conn | decryptor: decryptor}} 243 | end 244 | 245 | defp maybe_encrypt(response, %__MODULE__{encryptor: nil} = conn) do 246 | {response, conn} 247 | end 248 | 249 | defp maybe_encrypt(response, conn) do 250 | {encrypted, encryptor} = Crypto.AES.encrypt(response, conn.encryptor) 251 | {encrypted, %__MODULE__{conn | encryptor: encryptor}} 252 | end 253 | 254 | defp normalize_uuid(<>) do 255 | "#{a}-#{b}-#{c}-#{d}-#{e}" 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /lib/minecraft/crypto.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Crypto do 2 | @moduledoc """ 3 | Module for managing cryptographic keys. 4 | """ 5 | use GenServer 6 | require Logger 7 | 8 | @doc """ 9 | Starts the Crypto server, which generates keys during initialization. 10 | """ 11 | @spec start_link(opts :: Keyword.t()) :: GenServer.on_start() 12 | def start_link(opts \\ []) do 13 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 14 | end 15 | 16 | @doc """ 17 | Gets the public key. 18 | """ 19 | @spec get_public_key() :: binary 20 | def get_public_key() do 21 | GenServer.call(__MODULE__, :get_public_key) 22 | end 23 | 24 | @doc """ 25 | Encrypts a message. 26 | """ 27 | @spec encrypt(message :: binary) :: binary 28 | def encrypt(message) do 29 | GenServer.call(__MODULE__, {:encrypt, message}) 30 | end 31 | 32 | @doc """ 33 | Decrypts a message. 34 | """ 35 | @spec decrypt(message :: binary) :: binary 36 | def decrypt(message) do 37 | GenServer.call(__MODULE__, {:decrypt, message}) 38 | end 39 | 40 | @impl true 41 | def init(_opts) do 42 | {priv_key_file, pub_key_file} = gen_keys() 43 | {priv_key, pub_key, pub_key_der} = load_keys(priv_key_file, pub_key_file) 44 | 45 | state = %{ 46 | priv_key_file: priv_key_file, 47 | pub_key_file: pub_key_file, 48 | priv_key: priv_key, 49 | pub_key: pub_key, 50 | pub_key_der: pub_key_der 51 | } 52 | 53 | {:ok, state} 54 | end 55 | 56 | @impl true 57 | def terminate(reason, %{priv_key_file: priv_key_file}) do 58 | temp_dir = Path.dirname(priv_key_file) 59 | {:ok, _} = File.rm_rf(temp_dir) 60 | {:stop, reason, %{}} 61 | end 62 | 63 | @impl true 64 | def handle_call(:get_keys_dir, _from, %{priv_key_file: priv_key_file} = state) do 65 | {:reply, Path.dirname(priv_key_file), state} 66 | end 67 | 68 | def handle_call(:get_public_key, _from, %{pub_key_der: pub_key_der} = state) do 69 | {:reply, pub_key_der, state} 70 | end 71 | 72 | def handle_call({:decrypt, message}, _from, %{priv_key: priv_key} = state) do 73 | message = :public_key.decrypt_private(message, priv_key) 74 | {:reply, message, state} 75 | end 76 | 77 | def handle_call({:encrypt, message}, _from, %{pub_key: pub_key} = state) do 78 | message = :public_key.encrypt_public(message, pub_key) 79 | {:reply, message, state} 80 | end 81 | 82 | defp gen_keys() do 83 | {temp_dir, 0} = System.cmd("mktemp", ["-d"]) 84 | temp_dir = String.trim(temp_dir) 85 | priv_key_file = Path.join(temp_dir, "mc_private_key.pem") 86 | pub_key_file = Path.join(temp_dir, "mc_public_key.pem") 87 | 88 | {_, 0} = System.cmd("openssl", ~w(genrsa -out #{priv_key_file} 1024), stderr_to_stdout: true) 89 | 90 | {_, 0} = 91 | System.cmd( 92 | "openssl", 93 | ~w(rsa -in #{priv_key_file} -out #{pub_key_file} -outform PEM -pubout), 94 | stderr_to_stdout: true 95 | ) 96 | 97 | Logger.debug(fn -> "Generated RSA keypair in #{temp_dir}" end) 98 | {priv_key_file, pub_key_file} 99 | end 100 | 101 | defp load_keys(priv_key_file, pub_key_file) do 102 | priv_key_pem = File.read!(priv_key_file) 103 | pub_key_pem = File.read!(pub_key_file) 104 | 105 | [priv_entry] = :public_key.pem_decode(priv_key_pem) 106 | priv_key = :public_key.pem_entry_decode(priv_entry) 107 | 108 | [pub_entry] = :public_key.pem_decode(pub_key_pem) 109 | pub_key = :public_key.pem_entry_decode(pub_entry) 110 | {:SubjectPublicKeyInfo, pub_key_der, _} = pub_entry 111 | 112 | {priv_key, pub_key, pub_key_der} 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/minecraft/crypto/aes.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Crypto.AES do 2 | @moduledoc """ 3 | Helper module for encrypting/decrypting using AES/CFB8. 4 | """ 5 | 6 | @type t :: %__MODULE__{key: binary, ivec: binary} 7 | 8 | defstruct key: nil, ivec: nil 9 | 10 | @doc """ 11 | Decrypts a `message`, given the current AES `state`. 12 | """ 13 | @spec decrypt(binary, t) :: {decrypted_message :: binary, new_state :: t} 14 | def decrypt(message, state) do 15 | decrypt(message, state, []) 16 | end 17 | 18 | defp decrypt(<>, %__MODULE__{} = state, decrypted) do 19 | plain_text = :crypto.block_decrypt(:aes_cfb8, state.key, state.ivec, head) 20 | ivec = next_ivec(state.ivec, head) 21 | decrypt(rest, %__MODULE__{state | ivec: ivec}, [decrypted | plain_text]) 22 | end 23 | 24 | defp decrypt("", state, decrypted) do 25 | {IO.iodata_to_binary(decrypted), state} 26 | end 27 | 28 | @doc """ 29 | Encrypts a `message`, given the current AES `state`. 30 | """ 31 | @spec encrypt(binary, t) :: {encrypted_message :: binary, new_state :: t} 32 | def encrypt(message, state) do 33 | encrypt(message, state, []) 34 | end 35 | 36 | defp encrypt(<>, %__MODULE__{} = state, encrypted) do 37 | cipher_text = :crypto.block_encrypt(:aes_cfb8, state.key, state.ivec, head) 38 | ivec = next_ivec(state.ivec, cipher_text) 39 | encrypt(rest, %__MODULE__{state | ivec: ivec}, [encrypted | cipher_text]) 40 | end 41 | 42 | defp encrypt("", state, encrypted) do 43 | {IO.iodata_to_binary(encrypted), state} 44 | end 45 | 46 | defp next_ivec(ivec, data) when byte_size(data) == 1 and byte_size(ivec) == 16 do 47 | <<_::1-binary, rest::15-binary>> = ivec 48 | <> 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/minecraft/crypto/sha.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Crypto.SHA do 2 | @moduledoc """ 3 | Minecraft uses a [non-standard SHA encoding](http://wiki.vg/Protocol_Encryption#Authentication), and 4 | this module implements it. 5 | """ 6 | 7 | @doc """ 8 | Generates the Minecraft SHA of a binary. 9 | """ 10 | @spec sha(binary) :: String.t() 11 | def sha(message) do 12 | case :crypto.hash(:sha, message) do 13 | <> when hash < 0 -> 14 | "-" <> String.downcase(Integer.to_string(-hash, 16)) 15 | 16 | <> -> 17 | String.downcase(Integer.to_string(hash, 16)) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/minecraft/nif.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.NIF do 2 | @moduledoc """ 3 | NIFs for dealing with chunks. 4 | """ 5 | @on_load :load_nifs 6 | 7 | @doc false 8 | @spec load_nifs() :: :ok | {:error, any} 9 | def load_nifs() do 10 | :ok = :erlang.load_nif('./priv/nifs', 0) 11 | end 12 | 13 | @doc """ 14 | Sets the random seed used for world generation. 15 | """ 16 | @spec set_random_seed(integer) :: :ok 17 | def set_random_seed(_seed) do 18 | # Don't raise here, or Dialyzer complains 19 | :erlang.nif_error("NIF set_random_seed/1 not implemented") 20 | end 21 | 22 | @doc """ 23 | Generates a chunk given x and y coordinates. 24 | 25 | Note that these must be chunk coordinates, as they get multiplied by 16 26 | in the NIF. 27 | """ 28 | @spec generate_chunk(float, float) :: {:ok, any} | {:error, any} 29 | def generate_chunk(_chunk_x, _chunk_z) do 30 | # Don't raise here, or Dialyzer complains 31 | :erlang.nif_error("NIF generate_chunk/2 not implemented") 32 | end 33 | 34 | @doc """ 35 | Serializes a Chunk. 36 | """ 37 | @spec serialize_chunk(any) :: {:ok, any} | {:error, any} 38 | def serialize_chunk(_chunk) do 39 | # Don't raise here, or Dialyzer complains 40 | :erlang.nif_error("NIF serialize_chunk/1 not implemented") 41 | end 42 | 43 | @doc """ 44 | Gets coordinates of a chunk. 45 | """ 46 | @spec get_chunk_coordinates(any) :: {:ok, {integer, integer}} | :error 47 | def get_chunk_coordinates(_chunk) do 48 | # Don't raise here, or Dialyzer complains 49 | :erlang.nif_error("NIF get_chunk_coordinates/1 not implemented") 50 | end 51 | 52 | @doc """ 53 | Gets the number of chunk sections in a chunk. 54 | """ 55 | @spec num_chunk_sections(any) :: {:ok, integer} | :error 56 | def num_chunk_sections(_chunk) do 57 | # Don't raise here, or Dialyzer complains 58 | :erlang.nif_error("NIF num_chunk_sections/1 not implemented") 59 | end 60 | 61 | @doc """ 62 | Gets the biome data for a chunk. 63 | """ 64 | @spec chunk_biome_data(any) :: {:ok, binary} | :error 65 | def chunk_biome_data(_chunk) do 66 | # Don't raise here, or Dialyzer complains 67 | :erlang.nif_error("NIF chunk_biome_data/1 not implemented") 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/minecraft/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet do 2 | @moduledoc """ 3 | Base serialization and deserialization routines for packets. 4 | """ 5 | use Bitwise 6 | alias Minecraft.Packet.Client 7 | alias Minecraft.Packet.Server 8 | 9 | @type position :: {x :: -33_554_432..33_554_431, y :: -2048..2047, z :: -33_554_432..33_554_431} 10 | @type varint :: -2_147_483_648..2_147_483_647 11 | @type varlong :: -9_223_372_036_854_775_808..9_223_372_036_854_775_807 12 | 13 | @type packet_types :: 14 | Client.Handshake.t() 15 | | Client.Handshake.t() 16 | | Client.Status.Request.t() 17 | | Client.Status.Ping.t() 18 | | Server.Status.Response.t() 19 | | Server.Status.Pong.t() 20 | | Client.Login.LoginStart.t() 21 | | Client.Login.EncryptionResponse.t() 22 | | Server.Login.EncryptionRequest.t() 23 | | Server.Login.LoginSuccess.t() 24 | | Client.Play.TeleportConfirm.t() 25 | | Client.Play.ClientStatus.t() 26 | | Client.Play.ClientSettings.t() 27 | | Client.Play.PluginMessage.t() 28 | | Client.Play.KeepAlive.t() 29 | | Client.Play.PlayerPosition.t() 30 | | Client.Play.PlayerPositionAndLook.t() 31 | | Client.Play.PlayerLook.t() 32 | | Server.Play.JoinGame.t() 33 | | Server.Play.SpawnPosition.t() 34 | | Server.Play.PlayerAbilities.t() 35 | | Server.Play.PlayerPositionAndLook.t() 36 | | Server.Play.KeepAlive.t() 37 | 38 | @doc """ 39 | Given a raw binary packet, deserializes it into a `Packet` struct. 40 | """ 41 | @spec deserialize(binary, state :: atom, type :: :client | :server) :: 42 | {packet :: term, rest :: binary} | {:error, :invalid_packet} 43 | def deserialize(data, state, type \\ :client) do 44 | {packet_size, data} = decode_varint(data) 45 | <> = data 46 | {packet_id, data} = decode_varint(data) 47 | 48 | case do_deserialize({state, packet_id, type}, data) do 49 | {packet, ""} -> 50 | {packet, rest} 51 | 52 | error -> 53 | {error, rest} 54 | end 55 | end 56 | 57 | defp do_deserialize({state, packet_id, type}, data) do 58 | case {state, packet_id, type} do 59 | # Client Handshake Packets 60 | {:handshake, 0, :client} -> 61 | Client.Handshake.deserialize(data) 62 | 63 | # Client Status Packets 64 | {:status, 0, :client} -> 65 | Client.Status.Request.deserialize(data) 66 | 67 | {:status, 1, :client} -> 68 | Client.Status.Ping.deserialize(data) 69 | 70 | # Server Status Packets 71 | {:status, 0, :server} -> 72 | Server.Status.Response.deserialize(data) 73 | 74 | {:status, 1, :server} -> 75 | Server.Status.Pong.deserialize(data) 76 | 77 | # Client Login Packets 78 | {:login, 0, :client} -> 79 | Client.Login.LoginStart.deserialize(data) 80 | 81 | {:login, 1, :client} -> 82 | Client.Login.EncryptionResponse.deserialize(data) 83 | 84 | # Server Login Packets 85 | # TODO {:login, 0, :server} -> 86 | # Server.Login.Disconnect.deserialize(data) 87 | 88 | {:login, 1, :server} -> 89 | Server.Login.EncryptionRequest.deserialize(data) 90 | 91 | {:login, 2, :server} -> 92 | Server.Login.LoginSuccess.deserialize(data) 93 | 94 | # Client Play Packets 95 | {:play, 0, :client} -> 96 | Client.Play.TeleportConfirm.deserialize(data) 97 | 98 | {:play, 3, :client} -> 99 | Client.Play.ClientStatus.deserialize(data) 100 | 101 | {:play, 4, :client} -> 102 | Client.Play.ClientSettings.deserialize(data) 103 | 104 | {:play, 9, :client} -> 105 | Client.Play.PluginMessage.deserialize(data) 106 | 107 | {:play, 0x0B, :client} -> 108 | Client.Play.KeepAlive.deserialize(data) 109 | 110 | {:play, 0x0D, :client} -> 111 | Client.Play.PlayerPosition.deserialize(data) 112 | 113 | {:play, 0x0E, :client} -> 114 | Client.Play.PlayerPositionAndLook.deserialize(data) 115 | 116 | {:play, 0x0F, :client} -> 117 | Client.Play.PlayerLook.deserialize(data) 118 | 119 | # Server Play Packets 120 | {:play, 0x1F, :server} -> 121 | Server.Play.KeepAlive.deserialize(data) 122 | 123 | {:play, 0x23, :server} -> 124 | Server.Play.JoinGame.deserialize(data) 125 | 126 | {:play, 0x46, :server} -> 127 | Server.Play.SpawnPosition.deserialize(data) 128 | 129 | {:play, 0x2C, :server} -> 130 | Server.Play.PlayerAbilities.deserialize(data) 131 | 132 | {:play, 0x2F, :server} -> 133 | Server.Play.PlayerPositionAndLook.deserialize(data) 134 | 135 | _ -> 136 | {:error, :invalid_packet} 137 | end 138 | end 139 | 140 | @doc """ 141 | Serializes a packet into binary data. 142 | """ 143 | @spec serialize(packet :: struct) :: {:ok, binary} | {:error, term} 144 | def serialize(%struct{} = request) do 145 | {packet_id, packet_binary} = struct.serialize(request) 146 | serialize(packet_id, packet_binary) 147 | end 148 | 149 | @doc """ 150 | Serializes a packet binary into the standard packet format: 151 | 152 | | Field | Type | Description | 153 | | --------- | ------ | ----------------------------------------------- | 154 | | Length | VarInt | Length of packet data + length of the packet ID | 155 | | Packet ID | VarInt | | 156 | | Data | Binary | The serialized packet data | 157 | """ 158 | @spec serialize(packet_id :: integer, binary) :: {:ok, binary} | {:error, term} 159 | def serialize(packet_id, packet_binary) do 160 | packet_id = encode_varint(packet_id) 161 | packet_size = encode_varint(byte_size(packet_binary) + byte_size(packet_id)) 162 | response = <> 163 | {:ok, response} 164 | end 165 | 166 | @doc """ 167 | Decodes a boolean. 168 | """ 169 | @spec decode_bool(binary) :: {decoded :: boolean, rest :: binary} 170 | def decode_bool(<<0, rest::binary>>), do: {false, rest} 171 | def decode_bool(<<1, rest::binary>>), do: {true, rest} 172 | 173 | @doc """ 174 | Decodes a position. 175 | """ 176 | @spec decode_position(binary) :: {position, rest :: binary} 177 | def decode_position(<>) do 178 | {{x, y, z}, rest} 179 | end 180 | 181 | @doc """ 182 | Decodes a variable-size integer. 183 | """ 184 | @spec decode_varint(binary) :: 185 | {decoded :: varint, rest :: binary} | {:error, :too_long | :too_short} 186 | def decode_varint(data) do 187 | decode_varint(data, 0, 0) 188 | end 189 | 190 | defp decode_varint(<<1::1, value::7, rest::binary>>, num_read, acc) when num_read < 5 do 191 | decode_varint(rest, num_read + 1, acc + (value <<< (7 * num_read))) 192 | end 193 | 194 | defp decode_varint(<<0::1, value::7, rest::binary>>, num_read, acc) do 195 | result = acc + (value <<< (7 * num_read)) 196 | <> = <> 197 | {result, rest} 198 | end 199 | 200 | defp decode_varint(_, num_read, _) when num_read >= 5, do: {:error, :too_long} 201 | defp decode_varint("", _, _), do: {:error, :too_short} 202 | 203 | @doc """ 204 | Decodes a variable-size long. 205 | """ 206 | @spec decode_varlong(binary) :: 207 | {decoded :: varlong, rest :: binary} | {:error, :too_long | :too_short} 208 | def decode_varlong(data) do 209 | decode_varlong(data, 0, 0) 210 | end 211 | 212 | defp decode_varlong(<<1::1, value::7, rest::binary>>, num_read, acc) when num_read < 10 do 213 | decode_varlong(rest, num_read + 1, acc + (value <<< (7 * num_read))) 214 | end 215 | 216 | defp decode_varlong(<<0::1, value::7, rest::binary>>, num_read, acc) do 217 | result = acc + (value <<< (7 * num_read)) 218 | <> = <> 219 | {result, rest} 220 | end 221 | 222 | defp decode_varlong(_, num_read, _) when num_read >= 10, do: {:error, :too_long} 223 | defp decode_varlong("", _, _), do: {:error, :too_short} 224 | 225 | @doc """ 226 | Decodes a string. 227 | """ 228 | @spec decode_string(binary) :: {decoded :: binary, rest :: binary} 229 | def decode_string(data) do 230 | {strlen, data} = decode_varint(data) 231 | <> = data 232 | {string, rest} 233 | end 234 | 235 | @doc """ 236 | Encodes a boolean. 237 | """ 238 | @spec encode_bool(boolean) :: binary 239 | def encode_bool(false = _boolean), do: <<0>> 240 | def encode_bool(true), do: <<1>> 241 | 242 | @doc """ 243 | Encodes a position. 244 | """ 245 | @spec encode_position(position) :: binary 246 | def encode_position({x, y, z}) do 247 | <> 248 | end 249 | 250 | @doc """ 251 | Encodes a variable-size integer. 252 | """ 253 | @spec encode_varint(varint) :: binary | {:error, :too_large} 254 | def encode_varint(value) when value in -2_147_483_648..2_147_483_647 do 255 | <> = <> 256 | encode_varint(value, 0, "") 257 | end 258 | 259 | def encode_varint(_) do 260 | {:error, :too_large} 261 | end 262 | 263 | defp encode_varint(value, _, acc) when value <= 127 do 264 | <> 265 | end 266 | 267 | defp encode_varint(value, num_write, acc) when value > 127 and num_write < 5 do 268 | encode_varint(value >>> 7, num_write + 1, <>) 269 | end 270 | 271 | @doc """ 272 | Encodes a string. 273 | """ 274 | @spec encode_string(binary) :: binary 275 | def encode_string(string) do 276 | strlen = encode_varint(byte_size(string)) 277 | <> 278 | end 279 | end 280 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/handshake.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Handshake do 2 | @moduledoc false 3 | import Minecraft.Packet, 4 | only: [decode_varint: 1, decode_string: 1, encode_varint: 1, encode_string: 1] 5 | 6 | @type t :: %__MODULE__{ 7 | packet_id: 0, 8 | protocol_version: integer, 9 | server_addr: String.t(), 10 | server_port: 1..65535, 11 | next_state: :status | :login 12 | } 13 | 14 | defstruct packet_id: 0, 15 | protocol_version: 340, 16 | server_addr: nil, 17 | server_port: nil, 18 | next_state: nil 19 | 20 | @spec serialize(t) :: {packet_id :: 0, binary} 21 | def serialize(%__MODULE__{} = packet) do 22 | protocol_version = encode_varint(packet.protocol_version) 23 | server_addr = encode_string(packet.server_addr) 24 | 25 | next_state = 26 | case packet.next_state do 27 | :status -> 1 28 | :login -> 2 29 | end 30 | 31 | {0, 32 | <>} 34 | end 35 | 36 | @spec deserialize(binary) :: {t, rest :: binary} 37 | def deserialize(data) do 38 | {protocol_version, rest} = decode_varint(data) 39 | {server_addr, rest} = decode_string(rest) 40 | <> = rest 41 | 42 | next_state = 43 | case next_state do 44 | 1 -> :status 45 | 2 -> :login 46 | end 47 | 48 | packet = %__MODULE__{ 49 | protocol_version: protocol_version, 50 | server_addr: server_addr, 51 | server_port: server_port, 52 | next_state: next_state 53 | } 54 | 55 | {packet, rest} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/login/encryption_response.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Login.EncryptionResponse do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_varint: 1, encode_varint: 1] 4 | 5 | @type t :: %__MODULE__{packet_id: 1, shared_secret: binary, verify_token: binary} 6 | defstruct packet_id: 1, 7 | shared_secret: nil, 8 | verify_token: nil 9 | 10 | @spec serialize(t) :: {packet_id :: 1, binary} 11 | def serialize(%__MODULE__{shared_secret: shared_secret, verify_token: verify_token}) do 12 | shared_secret_len = encode_varint(byte_size(shared_secret)) 13 | verify_token_len = encode_varint(byte_size(verify_token)) 14 | 15 | {1, 16 | <>} 18 | end 19 | 20 | @spec deserialize(binary) :: {t, rest :: binary} 21 | def deserialize(data) do 22 | {shared_secret_len, rest} = decode_varint(data) 23 | <> = rest 24 | {verify_token_len, rest} = decode_varint(rest) 25 | <> = rest 26 | {%__MODULE__{shared_secret: shared_secret, verify_token: verify_token}, rest} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/login/login_start.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Login.LoginStart do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_string: 1, encode_string: 1] 4 | @type t :: %__MODULE__{packet_id: 0, username: String.t()} 5 | defstruct packet_id: 0, 6 | username: nil 7 | 8 | @spec serialize(t) :: {packet_id :: 0, binary} 9 | def serialize(%__MODULE__{username: username}) do 10 | {0, encode_string(username)} 11 | end 12 | 13 | @spec deserialize(binary) :: {t, rest :: binary} 14 | def deserialize(data) do 15 | {username, rest} = decode_string(data) 16 | {%__MODULE__{username: username}, rest} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/client_settings.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.ClientSettings do 2 | @moduledoc false 3 | import Minecraft.Packet, 4 | only: [ 5 | decode_bool: 1, 6 | decode_string: 1, 7 | decode_varint: 1, 8 | encode_bool: 1, 9 | encode_string: 1, 10 | encode_varint: 1 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | packet_id: 4, 15 | locale: String.t(), 16 | view_distance: integer, 17 | chat_mode: integer, 18 | chat_colors: boolean, 19 | displayed_skin_parts: integer, 20 | main_hand: :left | :right 21 | } 22 | defstruct packet_id: 4, 23 | locale: "en_us", 24 | view_distance: 8, 25 | chat_mode: 0, 26 | chat_colors: true, 27 | displayed_skin_parts: 0, 28 | main_hand: :right 29 | 30 | @spec serialize(t) :: {packet_id :: 4, binary} 31 | def serialize(%__MODULE__{} = packet) do 32 | main_hand = 33 | case packet.main_hand do 34 | :left -> 0 35 | :right -> 1 36 | end 37 | 38 | {4, 39 | <>} 42 | end 43 | 44 | @spec deserialize(binary) :: {t, rest :: binary} 45 | def deserialize(data) do 46 | {locale, rest} = decode_string(data) 47 | <> = rest 48 | {chat_mode, rest} = decode_varint(rest) 49 | {chat_colors, rest} = decode_bool(rest) 50 | <> = rest 51 | {main_hand, rest} = decode_varint(rest) 52 | main_hand = if main_hand == 0, do: :left, else: :right 53 | 54 | {%__MODULE__{ 55 | locale: locale, 56 | view_distance: view_distance, 57 | chat_mode: chat_mode, 58 | chat_colors: chat_colors, 59 | displayed_skin_parts: displayed_skin_parts, 60 | main_hand: main_hand 61 | }, rest} 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/client_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.ClientStatus do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [encode_varint: 1, decode_varint: 1] 4 | @type t :: %__MODULE__{packet_id: 3, action: :perform_respawn | :request_stats} 5 | defstruct packet_id: 3, 6 | action: :perform_respawn 7 | 8 | @spec serialize(t) :: {packet_id :: 3, binary} 9 | def serialize(%__MODULE__{action: action} = _packet) do 10 | action = if action == :perform_respawn, do: 0, else: 1 11 | {3, <>} 12 | end 13 | 14 | @spec deserialize(binary) :: {t, rest :: binary} 15 | def deserialize(data) do 16 | {action, rest} = decode_varint(data) 17 | 18 | action = if action == 0, do: :perform_respawn, else: :request_stats 19 | 20 | {%__MODULE__{action: action}, rest} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/keep_alive.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.KeepAlive do 2 | @moduledoc false 3 | 4 | defstruct packet_id: 0x0B, 5 | keep_alive_id: nil 6 | 7 | @type t :: %__MODULE__{packet_id: 0x0B, keep_alive_id: integer} 8 | 9 | @spec serialize(t) :: {packet_id :: 0x0B, binary} 10 | def serialize(%__MODULE__{keep_alive_id: keep_alive_id}) do 11 | {0x0B, <>} 12 | end 13 | 14 | @spec deserialize(binary) :: {t, rest :: binary} 15 | def deserialize(data) do 16 | <> = data 17 | {%__MODULE__{keep_alive_id: keep_alive_id}, rest} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/player_look.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.PlayerLook do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_bool: 1, encode_bool: 1] 4 | 5 | defstruct packet_id: 0x0F, 6 | yaw: 0.0, 7 | pitch: 0.0, 8 | on_ground: true 9 | 10 | @type t :: %__MODULE__{ 11 | packet_id: 0x0F, 12 | yaw: float, 13 | pitch: float, 14 | on_ground: boolean 15 | } 16 | 17 | @spec serialize(t) :: {packet_id :: 0x0F, binary} 18 | def serialize(%__MODULE__{} = packet) do 19 | {0x0F, 20 | <>} 21 | end 22 | 23 | @spec deserialize(binary) :: {t, rest :: binary} 24 | def deserialize(data) do 25 | <> = data 26 | 27 | {on_ground, rest} = decode_bool(rest) 28 | 29 | {%__MODULE__{ 30 | yaw: yaw, 31 | pitch: pitch, 32 | on_ground: on_ground 33 | }, rest} 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/player_position.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.PlayerPosition do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_bool: 1, encode_bool: 1] 4 | 5 | defstruct packet_id: 0x0D, 6 | x: 0.0, 7 | y: 200.0, 8 | z: 0.0, 9 | on_ground: true 10 | 11 | @type t :: %__MODULE__{ 12 | packet_id: 0x0D, 13 | x: float, 14 | y: float, 15 | z: float, 16 | on_ground: boolean 17 | } 18 | 19 | @spec serialize(t) :: {packet_id :: 0x0D, binary} 20 | def serialize(%__MODULE__{} = packet) do 21 | {0x0D, 22 | <>} 24 | end 25 | 26 | @spec deserialize(binary) :: {t, rest :: binary} 27 | def deserialize(data) do 28 | <> = data 29 | 30 | {on_ground, rest} = decode_bool(rest) 31 | 32 | {%__MODULE__{ 33 | x: x, 34 | y: y, 35 | z: z, 36 | on_ground: on_ground 37 | }, rest} 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/player_position_and_look.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.PlayerPositionAndLook do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_bool: 1, encode_bool: 1] 4 | 5 | defstruct packet_id: 0x0E, 6 | x: 0.0, 7 | y: 200.0, 8 | z: 0.0, 9 | yaw: 0.0, 10 | pitch: 0.0, 11 | on_ground: true 12 | 13 | @type t :: %__MODULE__{ 14 | packet_id: 0x0E, 15 | x: float, 16 | y: float, 17 | z: float, 18 | yaw: float, 19 | pitch: float, 20 | on_ground: boolean 21 | } 22 | 23 | @spec serialize(t) :: {packet_id :: 0x0E, binary} 24 | def serialize(%__MODULE__{} = packet) do 25 | {0x0E, 26 | <>} 28 | end 29 | 30 | @spec deserialize(binary) :: {t, rest :: binary} 31 | def deserialize(data) do 32 | <> = data 33 | 34 | {on_ground, rest} = decode_bool(rest) 35 | 36 | {%__MODULE__{ 37 | x: x, 38 | y: y, 39 | z: z, 40 | yaw: yaw, 41 | pitch: pitch, 42 | on_ground: on_ground 43 | }, rest} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/plugin_message.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.PluginMessage do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [encode_string: 1, decode_string: 1] 4 | @type t :: %__MODULE__{packet_id: 9, channel: String.t(), data: binary} 5 | defstruct packet_id: 9, 6 | channel: nil, 7 | data: nil 8 | 9 | @spec serialize(t) :: {packet_id :: 9, binary} 10 | def serialize(%__MODULE__{channel: channel, data: data} = _packet) do 11 | {9, <>} 12 | end 13 | 14 | @spec deserialize(binary) :: {t, rest :: binary} 15 | def deserialize(data) do 16 | {channel, rest} = decode_string(data) 17 | 18 | {%__MODULE__{channel: channel, data: rest}, ""} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/play/teleport_confirm.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Play.TeleportConfirm do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [encode_varint: 1, decode_varint: 1] 4 | @type t :: %__MODULE__{packet_id: 0, teleport_id: integer} 5 | defstruct packet_id: 0, 6 | teleport_id: nil 7 | 8 | @spec serialize(t) :: {packet_id :: 0, binary} 9 | def serialize(%__MODULE__{teleport_id: teleport_id} = _packet) do 10 | {0, <>} 11 | end 12 | 13 | @spec deserialize(binary) :: {t, rest :: binary} 14 | def deserialize(data) do 15 | {teleport_id, rest} = decode_varint(data) 16 | 17 | {%__MODULE__{teleport_id: teleport_id}, rest} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/status/ping.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Status.Ping do 2 | @moduledoc false 3 | @type t :: %__MODULE__{packet_id: 1, payload: integer} 4 | defstruct packet_id: 1, 5 | payload: nil 6 | 7 | @spec serialize(t) :: {packet_id :: 1, binary} 8 | def serialize(%__MODULE__{payload: payload}) do 9 | {1, <>} 10 | end 11 | 12 | @spec deserialize(binary) :: {t, rest :: binary} 13 | def deserialize(data) do 14 | <> = data 15 | {%__MODULE__{payload: payload}, rest} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/minecraft/packet/client/status/request.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Client.Status.Request do 2 | @moduledoc false 3 | @type t :: %__MODULE__{packet_id: 0} 4 | defstruct packet_id: 0 5 | 6 | @spec serialize(t) :: {packet_id :: 0, binary} 7 | def serialize(%__MODULE__{}) do 8 | {0, ""} 9 | end 10 | 11 | @spec deserialize(binary) :: {t, rest :: binary} 12 | def deserialize(data) do 13 | {%__MODULE__{}, data} 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/login/encryption_request.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Login.EncryptionRequest do 2 | @moduledoc false 3 | import Minecraft.Packet, 4 | only: [decode_string: 1, decode_varint: 1, encode_string: 1, encode_varint: 1] 5 | 6 | @type t :: %__MODULE__{ 7 | packet_id: 1, 8 | server_id: String.t(), 9 | public_key: binary, 10 | verify_token: binary 11 | } 12 | 13 | defstruct packet_id: 1, 14 | server_id: nil, 15 | public_key: nil, 16 | verify_token: nil 17 | 18 | @spec serialize(t) :: {packet_id :: 1, binary} 19 | def serialize(%__MODULE__{} = packet) do 20 | public_key_len = encode_varint(byte_size(packet.public_key)) 21 | verify_token_len = encode_varint(byte_size(packet.verify_token)) 22 | 23 | {1, 24 | <>} 26 | end 27 | 28 | @spec deserialize(binary) :: {t, rest :: binary} 29 | def deserialize(data) do 30 | {server_id, rest} = decode_string(data) 31 | {public_key_len, rest} = decode_varint(rest) 32 | <> = rest 33 | {verify_token_len, rest} = decode_varint(rest) 34 | <> = rest 35 | 36 | {%__MODULE__{server_id: server_id, public_key: public_key, verify_token: verify_token}, rest} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/login/login_success.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Login.LoginSuccess do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_string: 1, encode_string: 1] 4 | 5 | @type t :: %__MODULE__{ 6 | packet_id: 2, 7 | uuid: String.t(), 8 | username: String.t() 9 | } 10 | 11 | defstruct packet_id: 2, 12 | uuid: nil, 13 | username: nil 14 | 15 | @spec serialize(t) :: {packet_id :: 2, binary} 16 | def serialize(%__MODULE__{uuid: uuid, username: username}) do 17 | {2, <>} 18 | end 19 | 20 | @spec deserialize(binary) :: {t, rest :: binary} 21 | def deserialize(data) do 22 | {uuid, rest} = decode_string(data) 23 | {username, rest} = decode_string(rest) 24 | {%__MODULE__{uuid: uuid, username: username}, rest} 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/chunk_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.ChunkData do 2 | @moduledoc false 3 | import Bitwise 4 | import Minecraft.Packet, only: [encode_bool: 1, encode_varint: 1] 5 | 6 | defstruct packet_id: 0x20, 7 | chunk_x: nil, 8 | chunk_z: nil, 9 | ground_up_continuous: true, 10 | chunk: nil, 11 | num_block_entities: 0, 12 | block_entities: nil 13 | 14 | @type t :: %__MODULE__{ 15 | packet_id: 0x20, 16 | chunk_x: integer, 17 | chunk_z: integer, 18 | ground_up_continuous: boolean, 19 | chunk: Minecraft.Chunk.t(), 20 | num_block_entities: integer, 21 | # TODO 22 | block_entities: nil 23 | } 24 | 25 | @spec serialize(t) :: {packet_id :: 0x20, binary} 26 | def serialize(%__MODULE__{} = packet) do 27 | data = Minecraft.Chunk.serialize(packet.chunk) 28 | num_sections = Minecraft.Chunk.num_sections(packet.chunk) 29 | biomes = Minecraft.Chunk.get_biome_data(packet.chunk) 30 | primary_bit_mask = 0xFFFF >>> (16 - num_sections) 31 | primary_bit_mask = encode_varint(primary_bit_mask) 32 | data = IO.iodata_to_binary([data, biomes]) 33 | size = encode_varint(byte_size(data)) 34 | 35 | res = 36 | <> 39 | 40 | {0x20, res} 41 | end 42 | 43 | @spec deserialize(binary) :: {t, rest :: binary} 44 | def deserialize(data) do 45 | # <<0::4, creative_mode::1, allow_flying::1, flying::1, invulnerable::1, rest::binary>> = data 46 | # invulnerable = if invulnerable == 1, do: true, else: false 47 | # flying = if flying == 1, do: true, else: false 48 | # allow_flying = if allow_flying == 1, do: true, else: false 49 | # creative_mode = if creative_mode == 1, do: true, else: false 50 | # <> = rest 51 | 52 | # {%__MODULE__{ 53 | # invulnerable: invulnerable, 54 | # flying: flying, 55 | # allow_flying: allow_flying, 56 | # creative_mode: creative_mode, 57 | # flying_speed: flying_speed, 58 | # field_of_view_modifier: field_of_view_modifier 59 | # }, rest} 60 | {%__MODULE__{}, data} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/join_game.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.JoinGame do 2 | @moduledoc false 3 | import Minecraft.Packet, 4 | only: [decode_bool: 1, decode_string: 1, encode_bool: 1, encode_string: 1] 5 | 6 | @type game_mode :: :survival | :creative 7 | @type dimension :: :overworld | :nether | :end 8 | @type difficulty :: :peaceful | :easy | :normal | :hard 9 | @type t :: %__MODULE__{ 10 | packet_id: 0x23, 11 | entity_id: integer, 12 | game_mode: game_mode, 13 | dimension: dimension, 14 | difficulty: difficulty, 15 | max_players: integer, 16 | level_type: String.t(), 17 | reduced_debug_info: boolean 18 | } 19 | 20 | defstruct packet_id: 0x23, 21 | entity_id: nil, 22 | game_mode: :survival, 23 | dimension: :overworld, 24 | difficulty: :peaceful, 25 | max_players: 0, 26 | level_type: "default", 27 | reduced_debug_info: false 28 | 29 | @spec serialize(t) :: {packet_id :: 0x23, binary} 30 | def serialize(%__MODULE__{} = packet) do 31 | game_mode = 32 | case packet.game_mode do 33 | :survival -> 0 34 | :creative -> 1 35 | end 36 | 37 | dimension = 38 | case packet.dimension do 39 | :nether -> -1 40 | :overworld -> 0 41 | :end -> 1 42 | end 43 | 44 | difficulty = 45 | case packet.difficulty do 46 | :peaceful -> 0 47 | :easy -> 1 48 | :normal -> 2 49 | :hard -> 3 50 | end 51 | 52 | rdi = encode_bool(packet.reduced_debug_info) 53 | 54 | {0x23, 55 | <>} 58 | end 59 | 60 | @spec deserialize(binary) :: {t, rest :: binary} 61 | def deserialize(data) do 62 | <> = data 64 | 65 | game_mode = 66 | case game_mode do 67 | 0 -> :survival 68 | 1 -> :creative 69 | end 70 | 71 | dimension = 72 | case dimension do 73 | -1 -> :nether 74 | 0 -> :overworld 75 | 1 -> :end 76 | end 77 | 78 | difficulty = 79 | case difficulty do 80 | 0 -> :peaceful 81 | 1 -> :easy 82 | 2 -> :normal 83 | 3 -> :hard 84 | end 85 | 86 | {level_type, rest} = decode_string(rest) 87 | {rdi, rest} = decode_bool(rest) 88 | 89 | {%__MODULE__{ 90 | entity_id: entity_id, 91 | game_mode: game_mode, 92 | dimension: dimension, 93 | difficulty: difficulty, 94 | max_players: max_players, 95 | level_type: level_type, 96 | reduced_debug_info: rdi 97 | }, rest} 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/keep_alive.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.KeepAlive do 2 | @moduledoc false 3 | 4 | defstruct packet_id: 0x1F, 5 | keep_alive_id: nil 6 | 7 | @type t :: %__MODULE__{packet_id: 0x1F, keep_alive_id: integer} 8 | 9 | @spec serialize(t) :: {packet_id :: 0x1F, binary} 10 | def serialize(%__MODULE__{keep_alive_id: keep_alive_id}) do 11 | {0x1F, <>} 12 | end 13 | 14 | @spec deserialize(binary) :: {t, rest :: binary} 15 | def deserialize(data) do 16 | <> = data 17 | {%__MODULE__{keep_alive_id: keep_alive_id}, rest} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/player_abilities.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.PlayerAbilities do 2 | @moduledoc false 3 | defstruct packet_id: 0x2C, 4 | invulnerable: false, 5 | flying: false, 6 | allow_flying: false, 7 | creative_mode: false, 8 | flying_speed: 0.0, 9 | field_of_view_modifier: 1.0 10 | 11 | @type t :: %__MODULE__{ 12 | packet_id: 0x2C, 13 | invulnerable: boolean, 14 | flying: boolean, 15 | allow_flying: boolean, 16 | creative_mode: boolean, 17 | flying_speed: float, 18 | field_of_view_modifier: float 19 | } 20 | 21 | @spec serialize(t) :: {packet_id :: 0x2C, binary} 22 | def serialize(%__MODULE__{} = packet) do 23 | invulnerable = if packet.invulnerable, do: 1, else: 0 24 | flying = if packet.flying, do: 1, else: 0 25 | allow_flying = if packet.allow_flying, do: 1, else: 0 26 | creative_mode = if packet.creative_mode, do: 1, else: 0 27 | flags = <<0::4, creative_mode::1, allow_flying::1, flying::1, invulnerable::1>> 28 | 29 | {0x2C, 30 | <>} 31 | end 32 | 33 | @spec deserialize(binary) :: {t, rest :: binary} 34 | def deserialize(data) do 35 | <<0::4, creative_mode::1, allow_flying::1, flying::1, invulnerable::1, rest::binary>> = data 36 | invulnerable = if invulnerable == 1, do: true, else: false 37 | flying = if flying == 1, do: true, else: false 38 | allow_flying = if allow_flying == 1, do: true, else: false 39 | creative_mode = if creative_mode == 1, do: true, else: false 40 | <> = rest 41 | 42 | {%__MODULE__{ 43 | invulnerable: invulnerable, 44 | flying: flying, 45 | allow_flying: allow_flying, 46 | creative_mode: creative_mode, 47 | flying_speed: flying_speed, 48 | field_of_view_modifier: field_of_view_modifier 49 | }, rest} 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/player_position_and_look.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.PlayerPositionAndLook do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_varint: 1, encode_varint: 1] 4 | 5 | defstruct packet_id: 0x2F, 6 | x: 0.0, 7 | y: 200.0, 8 | z: 0.0, 9 | yaw: 0.0, 10 | pitch: 0.0, 11 | flags: 0, 12 | teleport_id: 0 13 | 14 | @type t :: %__MODULE__{ 15 | packet_id: 0x2F, 16 | x: float, 17 | y: float, 18 | z: float, 19 | yaw: float, 20 | pitch: float, 21 | flags: integer, 22 | teleport_id: integer 23 | } 24 | 25 | @spec serialize(t) :: {packet_id :: 0x2F, binary} 26 | def serialize(%__MODULE__{} = packet) do 27 | {0x2F, 28 | <>} 30 | end 31 | 32 | @spec deserialize(binary) :: {t, rest :: binary} 33 | def deserialize(data) do 34 | <> = data 36 | 37 | {teleport_id, rest} = decode_varint(rest) 38 | 39 | {%__MODULE__{ 40 | x: x, 41 | y: y, 42 | z: z, 43 | yaw: yaw, 44 | pitch: pitch, 45 | flags: flags, 46 | teleport_id: teleport_id 47 | }, rest} 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/play/spawn_position.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Play.SpawnPosition do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_position: 1, encode_position: 1] 4 | 5 | defstruct packet_id: 0x46, 6 | position: nil 7 | 8 | @type t :: %__MODULE__{packet_id: 0x46, position: Minecraft.Packet.position()} 9 | 10 | @spec serialize(t) :: {packet_id :: 0x46, binary} 11 | def serialize(%__MODULE__{position: position}) do 12 | {0x46, encode_position(position)} 13 | end 14 | 15 | @spec deserialize(binary) :: {t, rest :: binary} 16 | def deserialize(data) do 17 | {position, rest} = decode_position(data) 18 | {%__MODULE__{position: position}, rest} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/status/pong.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Status.Pong do 2 | @moduledoc false 3 | defstruct packet_id: 1, 4 | payload: nil 5 | 6 | @type t :: %__MODULE__{packet_id: 1, payload: integer} 7 | 8 | @spec serialize(t) :: {packet_id :: 1, binary} 9 | def serialize(%__MODULE__{payload: payload}) do 10 | {1, <>} 11 | end 12 | 13 | @spec deserialize(binary) :: {t, rest :: binary} 14 | def deserialize(data) do 15 | <> = data 16 | {%__MODULE__{payload: payload}, rest} 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/minecraft/packet/server/status/response.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Packet.Server.Status.Response do 2 | @moduledoc false 3 | import Minecraft.Packet, only: [decode_string: 1, encode_string: 1] 4 | 5 | @type t :: %__MODULE__{packet_id: 0, json: String.t()} 6 | 7 | defstruct packet_id: 0, 8 | json: nil 9 | 10 | @spec serialize(t) :: {packet_id :: 0, binary} 11 | def serialize(%__MODULE__{json: json}) do 12 | {0, encode_string(json)} 13 | end 14 | 15 | @spec deserialize(binary) :: {t, rest :: binary} 16 | def deserialize(data) do 17 | {json, rest} = decode_string(data) 18 | {%__MODULE__{json: json}, rest} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/minecraft/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Protocol do 2 | @moduledoc """ 3 | A [`:ranch_protocol`](https://ninenines.eu/docs/en/ranch/1.5/guide/protocols/) implementation 4 | that forwards requests to `Minecraft.Protocol.Handler`. 5 | """ 6 | use GenServer 7 | require Logger 8 | alias Minecraft.Connection 9 | alias Minecraft.Protocol.Handler 10 | 11 | @behaviour :ranch_protocol 12 | 13 | @impl true 14 | def start_link(ref, socket, transport, protocol_opts) do 15 | pid = :proc_lib.spawn_link(__MODULE__, :init, [{ref, socket, transport, protocol_opts}]) 16 | {:ok, pid} 17 | end 18 | 19 | @doc """ 20 | Sends a packet to the connected client. 21 | """ 22 | @spec send_packet(pid, struct) :: :ok | {:error, term} 23 | def send_packet(pid, packet) do 24 | GenServer.call(pid, {:send_packet, packet}) 25 | end 26 | 27 | def get_conn(pid) do 28 | GenServer.call(pid, :get_conn) 29 | end 30 | 31 | # 32 | # Server Callbacks 33 | # 34 | 35 | @impl true 36 | def init({ref, socket, transport, _protocol_opts}) do 37 | :ok = :ranch.accept_ack(ref) 38 | conn = Connection.init(self(), socket, transport) 39 | :gen_server.enter_loop(__MODULE__, [], conn) 40 | end 41 | 42 | @impl true 43 | def handle_info({:tcp, socket, data}, conn) do 44 | conn 45 | |> Connection.put_socket(socket) 46 | |> Connection.put_data(data) 47 | |> handle_conn() 48 | end 49 | 50 | def handle_info({:tcp_closed, socket}, conn) do 51 | Logger.info(fn -> "Client #{conn.client_ip} disconnected." end) 52 | :ok = conn.transport.close(socket) 53 | {:stop, :normal, conn} 54 | end 55 | 56 | @impl true 57 | def handle_call({:send_packet, packet}, _from, conn) do 58 | conn = Connection.send_packet(conn, packet) 59 | {:reply, :ok, conn} 60 | end 61 | 62 | def handle_call(:get_conn, _from, conn) do 63 | {:reply, conn, conn} 64 | end 65 | 66 | # 67 | # Helpers 68 | # 69 | defp handle_conn(%Connection{join: true, state_machine: nil} = conn) do 70 | {:ok, state_machine} = Minecraft.StateMachine.start_link(self()) 71 | handle_conn(%Connection{conn | state_machine: state_machine}) 72 | end 73 | 74 | defp handle_conn(%Connection{data: ""} = conn) do 75 | conn = Connection.continue(conn) 76 | {:noreply, conn} 77 | end 78 | 79 | defp handle_conn(%Connection{} = conn) do 80 | case Connection.read_packet(conn) do 81 | {:ok, packet, conn} -> 82 | handle_packet(packet, conn) 83 | 84 | {:error, conn} -> 85 | conn = Connection.close(conn) 86 | {:stop, :normal, conn} 87 | end 88 | end 89 | 90 | defp handle_packet(packet, conn) do 91 | case Handler.handle(packet, conn) do 92 | {:ok, :noreply, conn} -> 93 | handle_conn(conn) 94 | 95 | {:ok, response, conn} -> 96 | conn 97 | |> Connection.send_packet(response) 98 | |> handle_conn() 99 | 100 | {:error, _, conn} = err -> 101 | Logger.error(fn -> "#{__MODULE__} error: #{inspect(err)}" end) 102 | conn = Connection.close(conn) 103 | {:stop, :normal, conn} 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/minecraft/protocol/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Protocol.Handler do 2 | @moduledoc """ 3 | Server-side handler for responding to client packets. 4 | """ 5 | alias Minecraft.Connection 6 | alias Minecraft.Crypto 7 | alias Minecraft.Packet.Client 8 | alias Minecraft.Packet.Server 9 | 10 | @doc """ 11 | Handles a packet from a client, and returns either a response packet, or `{:ok, :noreply}`. 12 | """ 13 | @spec handle(packet :: Minecraft.Packet.packet_types(), Connection.t()) :: 14 | {:ok, :noreply | struct, Connection.t()} 15 | | {:error, :unsupported_protocol, Connection.t()} 16 | def handle(%Client.Handshake{protocol_version: 340} = packet, conn) do 17 | conn = 18 | conn 19 | |> Connection.put_state(packet.next_state) 20 | |> Connection.put_protocol(340) 21 | |> Connection.assign(:server_addr, packet.server_addr) 22 | 23 | {:ok, :noreply, conn} 24 | end 25 | 26 | def handle(%Client.Handshake{protocol_version: _}, conn) do 27 | {:error, :unsupported_protocol, conn} 28 | end 29 | 30 | def handle(%Client.Status.Request{}, conn) do 31 | {:ok, json} = 32 | Poison.encode(%{ 33 | version: %{name: "1.12.2", protocol: 340}, 34 | players: %{max: 20, online: 0, sample: []}, 35 | description: %{text: "Elixir Minecraft"} 36 | }) 37 | 38 | {:ok, %Server.Status.Response{json: json}, conn} 39 | end 40 | 41 | def handle(%Client.Status.Ping{payload: payload}, conn) do 42 | {:ok, %Server.Status.Pong{payload: payload}, conn} 43 | end 44 | 45 | def handle(%Client.Login.LoginStart{username: username}, conn) do 46 | verify_token = :crypto.strong_rand_bytes(4) 47 | 48 | conn = 49 | conn 50 | |> Connection.assign(:username, username) 51 | |> Connection.assign(:verify_token, verify_token) 52 | 53 | response = %Server.Login.EncryptionRequest{ 54 | server_id: "", 55 | public_key: Crypto.get_public_key(), 56 | verify_token: verify_token 57 | } 58 | 59 | {:ok, response, conn} 60 | end 61 | 62 | def handle(%Client.Login.EncryptionResponse{} = packet, conn) do 63 | verify_token = Crypto.decrypt(packet.verify_token) 64 | 65 | case conn.assigns[:verify_token] do 66 | ^verify_token -> 67 | shared_secret = Crypto.decrypt(packet.shared_secret) 68 | 69 | conn = 70 | conn 71 | |> Connection.encrypt(shared_secret) 72 | |> Connection.verify_login() 73 | |> Connection.put_state(:play) 74 | |> Connection.join() 75 | 76 | response = %Server.Login.LoginSuccess{ 77 | uuid: conn.assigns[:uuid], 78 | username: conn.assigns[:username] 79 | } 80 | 81 | {:ok, response, conn} 82 | 83 | _ -> 84 | {:error, :bad_verify_token, conn} 85 | end 86 | end 87 | 88 | def handle(%Client.Play.ClientSettings{} = packet, conn) do 89 | conn = 90 | conn 91 | |> Connection.put_setting(:locale, packet.locale) 92 | |> Connection.put_setting(:view_distance, packet.view_distance) 93 | |> Connection.put_setting(:chat_mode, packet.chat_mode) 94 | |> Connection.put_setting(:chat_colors, packet.chat_colors) 95 | |> Connection.put_setting(:displayed_skin_parts, packet.displayed_skin_parts) 96 | |> Connection.put_setting(:main_hand, packet.main_hand) 97 | 98 | {:ok, :noreply, conn} 99 | end 100 | 101 | def handle(%Client.Play.PluginMessage{}, conn) do 102 | {:ok, :noreply, conn} 103 | end 104 | 105 | def handle(%Client.Play.TeleportConfirm{}, conn) do 106 | # TODO: Verify this matches the one sent to the client 107 | {:ok, :noreply, conn} 108 | end 109 | 110 | def handle(%Client.Play.PlayerPosition{} = packet, conn) do 111 | position = {packet.x, packet.y, packet.z} 112 | :ok = Minecraft.Users.update_position(conn.assigns[:uuid], position) 113 | {:ok, :noreply, conn} 114 | end 115 | 116 | def handle(%Client.Play.PlayerPositionAndLook{} = packet, conn) do 117 | position = {packet.x, packet.y, packet.z} 118 | look = {packet.yaw, packet.pitch} 119 | :ok = Minecraft.Users.update_position(conn.assigns[:uuid], position) 120 | :ok = Minecraft.Users.update_look(conn.assigns[:uuid], look) 121 | {:ok, :noreply, conn} 122 | end 123 | 124 | def handle(%Client.Play.PlayerLook{} = packet, conn) do 125 | look = {packet.yaw, packet.pitch} 126 | :ok = Minecraft.Users.update_look(conn.assigns[:uuid], look) 127 | {:ok, :noreply, conn} 128 | end 129 | 130 | def handle(%Client.Play.ClientStatus{}, conn) do 131 | # TODO: Send Statistics when packet.action == :request_stats 132 | {:ok, :noreply, conn} 133 | end 134 | 135 | def handle(%Client.Play.KeepAlive{}, conn) do 136 | # TODO: Should kick client if we don't get one of these for 30 seconds 137 | {:ok, :noreply, conn} 138 | end 139 | 140 | def handle(nil, conn) do 141 | {:ok, :noreply, conn} 142 | end 143 | 144 | def handle({:error, _}, conn) do 145 | {:ok, :noreply, conn} 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/minecraft/server.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Server do 2 | @moduledoc """ 3 | The core Minecraft server that listens on a TCP port. 4 | """ 5 | 6 | @type server_opt :: {:max_connections, non_neg_integer()} | {:port, 0..65535} 7 | @type server_opts :: [server_opt] 8 | 9 | @doc """ 10 | Returns a specification to start this module under a supervisor. See `Supervisor` for 11 | more information. 12 | 13 | Valid options are: 14 | * `:max_connections` - The maximum number of connections this server can handle. Default 15 | is 100. 16 | * `:port` - Which port to start the server on. Default is 25565. 17 | """ 18 | @spec child_spec(server_opts) :: Supervisor.child_spec() 19 | def child_spec(opts \\ []) do 20 | %{ 21 | id: __MODULE__, 22 | start: {__MODULE__, :start_link, [opts]} 23 | } 24 | end 25 | 26 | @doc """ 27 | Starts the server. 28 | """ 29 | @spec start_link(server_opts) :: {:ok, pid()} | {:error, term()} 30 | def start_link(opts \\ []) do 31 | max_connections = Keyword.get(opts, :max_connections, 100) 32 | port = Keyword.get(opts, :port, 25565) 33 | ranch_opts = [port: port, max_connections: max_connections] 34 | :ranch.start_listener(:minecraft_server, :ranch_tcp, ranch_opts, Minecraft.Protocol, []) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/minecraft/state_machine.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.StateMachine do 2 | @moduledoc """ 3 | Implements core Minecraft logic. 4 | 5 | Minecraft can be thought of as a finite state machine, where transitions occur based on 6 | client interactions and server intervention. This module implements the `:gen_statem` 7 | behaviour. 8 | """ 9 | alias Minecraft.Packet.Server 10 | alias Minecraft.Protocol 11 | @behaviour :gen_statem 12 | 13 | @doc """ 14 | Starts the state machine. 15 | """ 16 | @spec start_link(protocol :: pid) :: :gen_statem.start_ret() 17 | def start_link(protocol) do 18 | :gen_statem.start_link(__MODULE__, protocol, []) 19 | end 20 | 21 | @impl true 22 | def callback_mode() do 23 | [:state_functions, :state_enter] 24 | end 25 | 26 | @impl true 27 | def init(protocol) do 28 | {:ok, :join, protocol, [{:next_event, :internal, nil}]} 29 | end 30 | 31 | @impl true 32 | def terminate(_reason, _state, _data) do 33 | # We don't need to log errors here, since whatever killed this will log an error 34 | :ignored 35 | end 36 | 37 | @doc """ 38 | State entered when a client logs in and begins joining the server. 39 | """ 40 | @spec join(:internal, any, pid) :: {:keep_state, pid} 41 | def join(:internal, _, protocol) do 42 | conn = Protocol.get_conn(protocol) 43 | :ok = Minecraft.Users.join(conn.assigns[:uuid], conn.assigns[:username]) 44 | 45 | :ok = 46 | Protocol.send_packet(protocol, %Server.Play.JoinGame{entity_id: 123, game_mode: :creative}) 47 | 48 | :ok = Protocol.send_packet(protocol, %Server.Play.SpawnPosition{position: {0, 200, 0}}) 49 | 50 | :ok = 51 | Protocol.send_packet(protocol, %Server.Play.PlayerAbilities{ 52 | creative_mode: true, 53 | allow_flying: true, 54 | flying_speed: 0.1 55 | }) 56 | 57 | :ok = 58 | Protocol.send_packet(protocol, %Server.Play.PlayerPositionAndLook{ 59 | teleport_id: :rand.uniform(127) 60 | }) 61 | 62 | {:next_state, :spawn, protocol, [{:next_event, :internal, nil}]} 63 | end 64 | 65 | def join(:enter, _, protocol) do 66 | {:keep_state, protocol} 67 | end 68 | 69 | def spawn(:internal, _, protocol) do 70 | for r <- 0..32 do 71 | for x <- -r..r do 72 | for z <- -r..r do 73 | if (x * x + z * z <= r * r and x * x + z * z > (r - 1) * (r - 1)) or r == 0 do 74 | chunk = Minecraft.World.get_chunk(x, z) 75 | 76 | :ok = 77 | Protocol.send_packet(protocol, %Server.Play.ChunkData{ 78 | chunk_x: x, 79 | chunk_z: z, 80 | chunk: chunk 81 | }) 82 | end 83 | end 84 | end 85 | end 86 | 87 | {:next_state, :ready, protocol, [{:state_timeout, 1000, :keepalive}]} 88 | end 89 | 90 | def spawn(:enter, _, protocol) do 91 | {:keep_state, protocol} 92 | end 93 | 94 | def ready(:enter, _, protocol) do 95 | {:keep_state, protocol} 96 | end 97 | 98 | def ready(:state_timeout, :keepalive, protocol) do 99 | :ok = 100 | Protocol.send_packet(protocol, %Server.Play.KeepAlive{ 101 | keep_alive_id: System.system_time(:millisecond) 102 | }) 103 | 104 | {:keep_state, protocol, [{:state_timeout, 1000, :keepalive}]} 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/minecraft/users.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Users do 2 | @moduledoc """ 3 | Stores user data such as position, items, etc. 4 | """ 5 | use GenServer 6 | 7 | defmodule User do 8 | @type t :: %__MODULE__{ 9 | uuid: binary, 10 | username: binary, 11 | position: {float, float, float}, 12 | look: {float, float}, 13 | respawn_location: {float, float, float} 14 | } 15 | 16 | defstruct uuid: nil, 17 | username: nil, 18 | position: {0.0, 0.0, 0.0}, 19 | look: {0.0, 0.0}, 20 | respawn_location: {0.0, 0.0, 0.0} 21 | end 22 | 23 | defmodule State do 24 | @moduledoc false 25 | @type t :: %__MODULE__{users: %{binary => User.t()}, logged_in: MapSet.t()} 26 | defstruct users: %{}, 27 | logged_in: MapSet.new() 28 | end 29 | 30 | @doc """ 31 | Starts the user service. 32 | """ 33 | @spec start_link(Keyword.t()) :: GenServer.on_start() 34 | def start_link(opts \\ []) do 35 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 36 | end 37 | 38 | @spec get_by_uuid(uuid :: binary) :: User.t() | nil 39 | def get_by_uuid(uuid) do 40 | GenServer.call(__MODULE__, {:get_by_uuid, uuid}) 41 | end 42 | 43 | @spec get_by_username(username :: binary) :: User.t() | nil 44 | def get_by_username(username) do 45 | GenServer.call(__MODULE__, {:get_by_username, username}) 46 | end 47 | 48 | @spec update_look(binary, {float, float}) :: :ok 49 | def update_look(uuid, look) do 50 | GenServer.cast(__MODULE__, {:update_look, uuid, look}) 51 | end 52 | 53 | @spec update_position(binary, {float, float, float}) :: :ok 54 | def update_position(uuid, new_position) do 55 | GenServer.cast(__MODULE__, {:update_position, uuid, new_position}) 56 | end 57 | 58 | @spec join(binary, binary) :: :ok 59 | def join(uuid, username) do 60 | GenServer.cast(__MODULE__, {:join, uuid, username}) 61 | end 62 | 63 | # 64 | # Callbacks 65 | # 66 | 67 | @impl true 68 | def init(_opts) do 69 | {:ok, %State{}} 70 | end 71 | 72 | @impl true 73 | def handle_call({:get_by_uuid, uuid}, _from, %{users: users} = state) do 74 | {:reply, Map.get(users, uuid), state} 75 | end 76 | 77 | def handle_call({:get_by_username, username}, _from, %{users: users} = state) do 78 | {:reply, Enum.find(users, fn {_uuid, user} -> user.username == username end), state} 79 | end 80 | 81 | @impl true 82 | def handle_cast({:update_look, uuid, look}, state) do 83 | users = Map.update!(state.users, uuid, fn user -> %User{user | look: look} end) 84 | {:noreply, %{state | users: users}} 85 | end 86 | 87 | def handle_cast({:update_position, uuid, position}, state) do 88 | users = Map.update!(state.users, uuid, fn user -> %User{user | position: position} end) 89 | {:noreply, %{state | users: users}} 90 | end 91 | 92 | def handle_cast({:join, uuid, username}, state) do 93 | state = %{state | logged_in: MapSet.put(state.logged_in, uuid)} 94 | 95 | if is_nil(state.users[uuid]) do 96 | users = Map.put(state.users, uuid, %User{uuid: uuid, username: username}) 97 | {:noreply, %{state | users: users}} 98 | else 99 | {:noreply, state} 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/minecraft/world.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.World do 2 | @moduledoc """ 3 | Stores Minecraft world data. 4 | """ 5 | use GenServer 6 | alias Minecraft.NIF 7 | require Logger 8 | 9 | @type world_opts :: [{:seed, integer}] 10 | 11 | @doc """ 12 | Starts the Minecraft World, which will initialize the spawn area. 13 | """ 14 | @spec start_link(world_opts) :: GenServer.on_start() 15 | def start_link(opts \\ []) do 16 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 17 | end 18 | 19 | @doc """ 20 | Gets a specific chunk, loading it if necessary. 21 | 22 | The chunk will be already encoded into chunk sections for the client. 23 | """ 24 | @spec get_chunk(integer, integer) :: Minecraft.Chunk.t() 25 | def get_chunk(x, z) do 26 | GenServer.call(__MODULE__, {:get_chunk, x, z}) 27 | end 28 | 29 | # 30 | # Callbacks 31 | # 32 | 33 | @impl true 34 | def init(opts) do 35 | seed = Keyword.get(opts, :seed, 1230) 36 | :ok = NIF.set_random_seed(seed) 37 | init_spawn_area() 38 | {:ok, %{seed: seed, chunks: %{}}} 39 | end 40 | 41 | @impl true 42 | def handle_call({:get_chunk, x, z}, _from, %{chunks: chunks} = state) do 43 | case get_in(chunks, [x, z]) do 44 | nil -> 45 | {:ok, chunk} = NIF.generate_chunk(x, z) 46 | chunk = %Minecraft.Chunk{resource: chunk} 47 | chunks = Map.put_new(chunks, x, %{}) 48 | chunks = put_in(chunks, [x, z], chunk) 49 | {:reply, chunk, %{state | chunks: chunks}} 50 | 51 | chunk_sections -> 52 | {:reply, chunk_sections, state} 53 | end 54 | end 55 | 56 | @impl true 57 | def handle_info({:load_chunk, x, z}, %{chunks: chunks} = state) do 58 | case get_in(chunks, [x, z]) do 59 | nil -> 60 | {:ok, chunk} = NIF.generate_chunk(x, z) 61 | chunk = %Minecraft.Chunk{resource: chunk} 62 | chunks = Map.put_new(chunks, x, %{}) 63 | chunks = put_in(chunks, [x, z], chunk) 64 | {:noreply, %{state | chunks: chunks}} 65 | 66 | _chunk_sections -> 67 | {:noreply, state} 68 | end 69 | end 70 | 71 | # 72 | # Helpers 73 | # 74 | 75 | defp init_spawn_area() do 76 | for x <- -20..20 do 77 | for z <- -20..20 do 78 | send(self(), {:load_chunk, x, z}) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/mix/tasks/compile.nifs.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Nifs do 2 | use Mix.Task 3 | 4 | @spec run(OptionParser.argv()) :: :ok 5 | def run(_args) do 6 | try do 7 | {result, 0} = System.cmd("make", [], stderr_to_stdout: true) 8 | IO.binwrite(result) 9 | Mix.Shell.IO.info("Successfully compiled NIFs") 10 | rescue 11 | e in MatchError -> 12 | {result, exit_code} = e.term 13 | Mix.Shell.IO.info("Failed to compile NIFs, exit code #{exit_code}:\n#{result}") 14 | 15 | err -> 16 | Mix.Shell.IO.info("Failed to compile NIFs, error: #{inspect(err)}") 17 | end 18 | 19 | :ok 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.MixProject do 2 | use Mix.Project 3 | 4 | @description "A Minecraft server implementation in Elixir." 5 | @project_url "https://github.com/thecodeboss/minecraft" 6 | 7 | def project do 8 | [ 9 | app: :minecraft, 10 | version: "0.1.0", 11 | elixir: "~> 1.6", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | compilers: Mix.compilers() ++ [:nifs], 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | 17 | # URLs 18 | source_url: @project_url, 19 | homepage_url: @project_url, 20 | 21 | # Hex 22 | description: @description, 23 | package: package(), 24 | 25 | # Docs 26 | name: "Minecraft", 27 | docs: [ 28 | # The main page in the docs 29 | main: "readme", 30 | extras: ["README.md": [title: "Minecraft"]] 31 | ], 32 | 33 | # Coverage 34 | test_coverage: [tool: ExCoveralls], 35 | preferred_cli_env: [ 36 | coveralls: :test, 37 | "coveralls.detail": :test, 38 | "coveralls.post": :test, 39 | "coveralls.html": :test 40 | ] 41 | ] 42 | end 43 | 44 | # Run "mix help compile.app" to learn about applications. 45 | def application do 46 | [ 47 | extra_applications: [:logger], 48 | mod: {Minecraft.Application, []} 49 | ] 50 | end 51 | 52 | defp elixirc_paths(:test), do: ["lib", "test/support"] 53 | defp elixirc_paths(_), do: ["lib"] 54 | 55 | # Run "mix help deps" to learn about dependencies. 56 | defp deps do 57 | [ 58 | {:excoveralls, "~> 0.8", only: :test}, 59 | {:ex_doc, "~> 0.16", only: :dev, runtime: false}, 60 | {:httpoison, "~> 1.2"}, 61 | {:inch_ex, only: :docs}, 62 | {:mock, "~> 0.3.0", only: :test}, 63 | {:ranch, "~> 2.1"}, 64 | {:poison, "~> 5.0"} 65 | ] 66 | end 67 | 68 | defp package do 69 | [ 70 | maintainers: ["Michael Oliver"], 71 | licenses: ["MIT"], 72 | links: %{"GitHub" => @project_url}, 73 | files: ~w(README.md LICENSE mix.exs lib) 74 | ] 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, 4 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, 6 | "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, 7 | "excoveralls": {:hex, :excoveralls, "0.14.1", "14140e4ef343f2af2de33d35268c77bc7983d7824cb945e6c2af54235bc2e61f", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4a588f9f8cf9dc140cc1f3d0ea4d849b2f76d5d8bee66b73c304bb3d3689c8b0"}, 8 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, 9 | "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, 10 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 11 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, 12 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 13 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 16 | "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, 17 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 18 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 19 | "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, 20 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 21 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 22 | "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, 23 | "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 25 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 26 | } 27 | -------------------------------------------------------------------------------- /priv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodeboss/minecraft/325c6f5cfd9fc186b9677710276c75aad70b8156/priv/.gitkeep -------------------------------------------------------------------------------- /src/biome.c: -------------------------------------------------------------------------------- 1 | #include "biome.h" 2 | #include 3 | #include "perlin.h" 4 | 5 | extern int p[512]; 6 | extern int p2[512]; 7 | 8 | static double distance(double x1, double y1, double x2, double y2) { 9 | return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); 10 | } 11 | 12 | static const int chunks_per_zone = 40; 13 | 14 | struct Zone { 15 | int x[10]; 16 | int z[10]; 17 | uint8_t biome[10]; 18 | uint8_t count; 19 | }; 20 | 21 | static void init_zone(int zone_x, int zone_z, struct Zone* zone) { 22 | zone->count = p[xorhash(zone_x ^ zone_z)] / 26; 23 | for (int i = 0; i < zone->count; i++) { 24 | zone->x[i] = 25 | p2[xorhash(p[i] ^ p[zone_x * zone_z])] % (chunks_per_zone * 16); 26 | zone->z[i] = 27 | p2[xorhash((i * 97 - 2) ^ (zone_x * 131 - 17) ^ (zone_z * 29 - 89))] % 28 | (chunks_per_zone * 16); 29 | zone->biome[i] = 30 | p2[xorhash((i * 29 - 5) ^ (zone_x * 239 - 177) ^ (zone_z * 61 - 91))] % 31 | B_NUM_BIOMES; 32 | } 33 | } 34 | 35 | uint8_t get_biome(double x, double z) { 36 | int zone_x = (int)floor(x / (16 * chunks_per_zone)); 37 | int zone_z = (int)floor(z / (16 * chunks_per_zone)); 38 | struct Zone zones[9]; 39 | int count = 0; 40 | for (int i = zone_x - 1; i <= zone_x + 1; i++) { 41 | for (int j = zone_z - 1; j <= zone_z + 1; j++) { 42 | init_zone(i, j, &zones[count++]); 43 | } 44 | } 45 | 46 | uint8_t biome = 0; 47 | double min_distance = 10000000.0; 48 | count = 0; 49 | for (int i = zone_x - 1; i <= zone_x + 1; i++) { 50 | for (int j = zone_z - 1; j <= zone_z + 1; j++) { 51 | struct Zone zone = zones[count++]; 52 | for (int k = 0; k < zone.count; k++) { 53 | double d = distance(x, z, zone.x[k] + i * chunks_per_zone * 16, 54 | zone.z[k] + j * chunks_per_zone * 16); 55 | if (d < min_distance) { 56 | min_distance = d; 57 | biome = zone.biome[k]; 58 | } 59 | } 60 | } 61 | } 62 | 63 | switch (biome) { 64 | case 0: 65 | biome = B_OCEAN; 66 | break; 67 | case 1: 68 | biome = B_PLAINS; 69 | break; 70 | case 2: 71 | biome = B_DESERT; 72 | break; 73 | case 3: 74 | biome = B_FOREST; 75 | break; 76 | case 4: 77 | biome = B_TAIGA; 78 | break; 79 | case 5: 80 | biome = B_SWAMP; 81 | break; 82 | case 6: 83 | biome = B_ICE_PLAINS; 84 | break; 85 | case 7: 86 | biome = B_JUNGLE; 87 | break; 88 | case 8: 89 | biome = B_BIRCH_FOREST; 90 | break; 91 | default: 92 | biome = B_PLAINS; 93 | break; 94 | } 95 | 96 | return biome; 97 | } 98 | -------------------------------------------------------------------------------- /src/biome.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | #define B_OCEAN 0 5 | #define B_PLAINS 1 6 | #define B_DESERT 2 7 | #define B_FOREST 4 8 | #define B_TAIGA 5 9 | #define B_SWAMP 6 10 | #define B_ICE_PLAINS 12 11 | #define B_JUNGLE 21 12 | #define B_BIRCH_FOREST 27 13 | 14 | #define B_NUM_BIOMES 9 15 | 16 | uint8_t get_biome(double x, double z); 17 | -------------------------------------------------------------------------------- /src/chunk.c: -------------------------------------------------------------------------------- 1 | #include "chunk.h" 2 | #include 3 | #include 4 | 5 | static uint8_t rand1[64] = {3, 2, 1, 2, 2, 2, 1, 1, 2, 3, 2, 2, 3, 2, 2, 1, 6 | 2, 1, 2, 2, 1, 2, 3, 2, 3, 2, 1, 2, 3, 2, 1, 2, 7 | 1, 2, 2, 3, 3, 3, 2, 1, 2, 1, 1, 1, 2, 3, 2, 1, 8 | 2, 1, 1, 1, 2, 1, 2, 3, 2, 2, 2, 3, 3, 3, 2, 2}; 9 | 10 | static uint8_t rand2[64] = {2, 7, 7, 1, 7, 6, 9, 12, 4, 6, 12, 3, 4, 11 | 5, 6, 4, 2, 5, 7, 7, 15, 12, 1, 9, 12, 2, 12 | 4, 1, 7, 11, 4, 15, 5, 9, 9, 10, 12, 4, 11, 13 | 11, 12, 5, 1, 1, 4, 10, 12, 15, 13, 16, 15, 13, 14 | 7, 10, 5, 10, 3, 13, 5, 7, 13, 10, 1, 14}; 15 | 16 | struct ChunkSection *generate_chunk_section(uint8_t *heightmap, 17 | int32_t chunk_y) { 18 | struct ChunkSection *chunk_section = enif_alloc(sizeof(struct ChunkSection)); 19 | chunk_section->y = chunk_y; 20 | for (uint32_t y = 0; y < 16; y++) { 21 | unsigned block_y = chunk_y * 16 + y; 22 | for (uint32_t z = 0; z < 16; z++) { 23 | for (uint32_t x = 0; x < 16; x++) { 24 | size_t block_number = (((y * 16) + z) * 16) + x; 25 | uint8_t m = rand1[(x * 16 + z + heightmap[z * 16 + x]) % 64]; 26 | uint8_t n = rand2[(x * 16 + block_y + z + heightmap[x * 16 + z]) % 64]; 27 | uint16_t type; 28 | if (block_y == m) { 29 | type = MC_BEDROCK; 30 | } else if (block_y < (uint8_t)(heightmap[z * 16 + x] - m)) { 31 | type = MC_STONE; 32 | } else if (block_y < heightmap[z * 16 + x]) { 33 | if (block_y < 64) { 34 | type = MC_SAND; 35 | } else { 36 | type = MC_DIRT; 37 | } 38 | } else if (block_y == heightmap[z * 16 + x]) { 39 | if (block_y < 64) { 40 | type = MC_SAND; 41 | } else { 42 | type = MC_GRASS; 43 | } 44 | } else if (block_y < 64) { 45 | type = MC_STILL_WATER; 46 | } else if (block_y == (unsigned)heightmap[z * 16 + x] + 1 && n > 13) { 47 | if (block_y == 64) { 48 | type = MC_AIR; 49 | } else { 50 | type = MC_TALL_GRASS; 51 | } 52 | } else { 53 | type = MC_AIR; 54 | } 55 | chunk_section->blocks[block_number].type = type; 56 | chunk_section->blocks[block_number].block_light = 0; 57 | chunk_section->blocks[block_number].sky_light = 0xF; 58 | } 59 | } 60 | } 61 | 62 | return chunk_section; 63 | } 64 | 65 | ERL_NIF_TERM serialize_chunk_section(ErlNifEnv *env, 66 | struct ChunkSection *chunk_section) { 67 | ERL_NIF_TERM chunk_section_term; 68 | const uint8_t bits_per_block = 13; 69 | const uint64_t value_mask = (1UL << bits_per_block) - 1; 70 | const uint8_t palette = 0; 71 | const uint16_t data_array_length = 16 * 16 * 16 * bits_per_block / 64; 72 | const uint16_t data_array_length_encoded = bswap_16(0xC006); 73 | const size_t total_size_bytes = 74 | sizeof(uint8_t) // bits_per_block 75 | + sizeof(uint8_t) // palette 76 | + sizeof(uint16_t) // data_array_length 77 | + sizeof(uint64_t) * data_array_length // data 78 | + sizeof(uint8_t) * (16 * 16 * 16); // block light and sky light 79 | uint8_t *raw = (uint8_t *)enif_make_new_binary(env, total_size_bytes, 80 | &chunk_section_term); 81 | 82 | // Fill the chunk with air 83 | memset((void *)raw, 0, total_size_bytes); 84 | 85 | *raw++ = bits_per_block; 86 | *raw++ = palette; 87 | 88 | uint16_t *raw16 = (uint16_t *)raw; 89 | *raw16++ = data_array_length_encoded; 90 | 91 | uint64_t *data = (uint64_t *)raw16; 92 | 93 | for (uint32_t y = 0; y < 16; y++) { 94 | for (uint32_t z = 0; z < 16; z++) { 95 | for (uint32_t x = 0; x < 16; x++) { 96 | size_t block_number = (((y * 16) + z) * 16) + x; 97 | size_t start_long = (block_number * bits_per_block) / 64; 98 | size_t start_offset = (block_number * bits_per_block) % 64; 99 | size_t end_long = ((block_number + 1) * bits_per_block - 1) / 64; 100 | 101 | uint64_t value = chunk_section->blocks[block_number].type; 102 | value &= value_mask; 103 | 104 | data[start_long] |= (value << start_offset); 105 | 106 | if (start_long != end_long) { 107 | data[end_long] = (value >> (64 - start_offset)); 108 | } 109 | } 110 | } 111 | } 112 | 113 | for (uint16_t i = 0; i < data_array_length; i++) { 114 | data[i] = bswap_64(data[i]); 115 | } 116 | 117 | raw = (uint8_t *)(data + data_array_length); 118 | 119 | // Block Light 120 | for (uint32_t y = 0; y < 16; y++) { 121 | for (uint32_t z = 0; z < 16; z++) { 122 | for (uint32_t x = 0; x < 16; x += 2) { 123 | size_t block_number = (((y * 16) + z) * 16) + x; 124 | uint8_t light1 = chunk_section->blocks[block_number].block_light; 125 | uint8_t light2 = chunk_section->blocks[block_number + 1].block_light; 126 | *raw++ = light1 | (light2 << 4); 127 | } 128 | } 129 | } 130 | 131 | // Sky Light 132 | for (uint32_t y = 0; y < 16; y++) { 133 | for (uint32_t z = 0; z < 16; z++) { 134 | for (uint32_t x = 0; x < 16; x += 2) { 135 | size_t block_number = (((y * 16) + z) * 16) + x; 136 | uint8_t light1 = chunk_section->blocks[block_number].sky_light; 137 | uint8_t light2 = chunk_section->blocks[block_number + 1].sky_light; 138 | *raw++ = light1 | (light2 << 4); 139 | } 140 | } 141 | } 142 | 143 | return chunk_section_term; 144 | } 145 | -------------------------------------------------------------------------------- /src/chunk.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "erl_nif.h" 5 | 6 | #define MC_AIR 0ul 7 | #define MC_STONE 16ul 8 | #define MC_GRASS 32ul 9 | #define MC_DIRT 48ul 10 | #define MC_COBBLESTONE 64ul 11 | #define MC_BEDROCK 112ul 12 | #define MC_STILL_WATER 144ul 13 | #define MC_SAND 192ul 14 | #define MC_GRAVEL 208ul 15 | #define MC_OAK_WOOD 272ul 16 | #define MC_OAK_LEAVES 288ul 17 | 18 | #define MC_TALL_GRASS 497ul 19 | #define MC_DANDELION 592ul 20 | 21 | struct Block { 22 | uint16_t type; 23 | uint8_t block_light; 24 | uint8_t sky_light; 25 | }; 26 | 27 | struct ChunkSection { 28 | int32_t y; 29 | struct Block blocks[16 * 16 * 16]; 30 | }; 31 | 32 | struct Chunk { 33 | int32_t x; 34 | int32_t z; 35 | uint8_t *heightmap; 36 | uint8_t *biome; 37 | uint8_t num_sections; 38 | struct ChunkSection *chunk_sections[16]; 39 | }; 40 | 41 | struct ChunkSection *generate_chunk_section(uint8_t *heightmap, 42 | int32_t chunk_y); 43 | 44 | ERL_NIF_TERM serialize_chunk_section(ErlNifEnv *env, 45 | struct ChunkSection *chunk_section); -------------------------------------------------------------------------------- /src/nifs.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "biome.h" 8 | #include "chunk.h" 9 | #include "erl_nif.h" 10 | #include "perlin.h" 11 | 12 | #define max(X, Y) (((X) > (Y)) ? (X) : (Y)) 13 | 14 | static ErlNifResourceType *CHUNK_RES_TYPE; 15 | 16 | static ERL_NIF_TERM set_random_seed(ErlNifEnv *env, int argc, 17 | const ERL_NIF_TERM argv[]) { 18 | (void)argc; 19 | unsigned seed; 20 | 21 | enif_get_int(env, argv[0], (int *)&seed); 22 | initialize_random(seed); 23 | 24 | return enif_make_atom(env, "ok"); 25 | } 26 | 27 | static ERL_NIF_TERM serialize_chunk(ErlNifEnv *env, int argc, 28 | const ERL_NIF_TERM argv[]) { 29 | (void)argc; 30 | struct Chunk *chunk = NULL; 31 | if (!enif_get_resource(env, argv[0], CHUNK_RES_TYPE, (void **)&chunk)) { 32 | return enif_make_atom(env, "error"); 33 | } 34 | 35 | ERL_NIF_TERM chunk_sections[16]; 36 | for (int i = 0; i < chunk->num_sections; i++) { 37 | chunk_sections[i] = serialize_chunk_section(env, chunk->chunk_sections[i]); 38 | } 39 | 40 | ERL_NIF_TERM chunk_term = 41 | enif_make_list_from_array(env, chunk_sections, (int)chunk->num_sections); 42 | 43 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), chunk_term); 44 | } 45 | 46 | static ERL_NIF_TERM get_chunk_coordinates(ErlNifEnv *env, int argc, 47 | const ERL_NIF_TERM argv[]) { 48 | (void)argc; 49 | struct Chunk *chunk = NULL; 50 | if (!enif_get_resource(env, argv[0], CHUNK_RES_TYPE, (void **)&chunk)) { 51 | return enif_make_atom(env, "error"); 52 | } 53 | 54 | ERL_NIF_TERM coords = enif_make_tuple2(env, enif_make_int(env, chunk->x), 55 | enif_make_int(env, chunk->z)); 56 | 57 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), coords); 58 | } 59 | 60 | static ERL_NIF_TERM num_chunk_sections(ErlNifEnv *env, int argc, 61 | const ERL_NIF_TERM argv[]) { 62 | (void)argc; 63 | struct Chunk *chunk = NULL; 64 | if (!enif_get_resource(env, argv[0], CHUNK_RES_TYPE, (void **)&chunk)) { 65 | return enif_make_atom(env, "error"); 66 | } 67 | 68 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), 69 | enif_make_int(env, chunk->num_sections)); 70 | } 71 | 72 | static ERL_NIF_TERM chunk_biome_data(ErlNifEnv *env, int argc, 73 | const ERL_NIF_TERM argv[]) { 74 | (void)argc; 75 | struct Chunk *chunk = NULL; 76 | if (!enif_get_resource(env, argv[0], CHUNK_RES_TYPE, (void **)&chunk)) { 77 | return enif_make_atom(env, "error"); 78 | } 79 | 80 | ERL_NIF_TERM term; 81 | uint8_t *biome_data = enif_make_new_binary(env, 256, &term); 82 | memcpy(biome_data, chunk->biome, 256); 83 | 84 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), term); 85 | } 86 | 87 | static ERL_NIF_TERM generate_chunk(ErlNifEnv *env, int argc, 88 | const ERL_NIF_TERM argv[]) { 89 | (void)argc; 90 | int chunk_x, chunk_z; 91 | 92 | enif_get_int(env, argv[0], (int *)&chunk_x); 93 | enif_get_int(env, argv[1], (int *)&chunk_z); 94 | 95 | struct Chunk *chunk = 96 | enif_alloc_resource(CHUNK_RES_TYPE, sizeof(struct Chunk)); 97 | ERL_NIF_TERM chunk_term = enif_make_resource(env, chunk); 98 | uint8_t *heightmap = (uint8_t *)enif_alloc(sizeof(uint8_t) * 16 * 16); 99 | uint8_t *biome = (uint8_t *)enif_alloc(sizeof(uint8_t) * 16 * 16); 100 | chunk->x = chunk_x; 101 | chunk->z = chunk_z; 102 | chunk->heightmap = heightmap; 103 | chunk->biome = biome; 104 | 105 | double start_x = chunk_x * 16.0; 106 | double start_z = chunk_z * 16.0; 107 | unsigned max_height = 0; 108 | double x = start_x; 109 | double z = start_z; 110 | for (size_t i = 0; i < 16; z++, i++) { 111 | x = start_x; 112 | for (size_t j = 0; j < 16; x++, j++) { 113 | unsigned h = 125 * octave_perlin(x + 0.483, 28.237, z + 0.483, 6, 0.4); 114 | if (h > 255) h = 255; 115 | if (h > max_height) max_height = h; 116 | heightmap[i * 16 + j] = (uint8_t)h; 117 | biome[i * 16 + j] = get_biome(x, z); 118 | } 119 | } 120 | 121 | // Figure out how many chunk sections we need to output 122 | uint8_t num_chunk_sections = max(4, (uint8_t)ceil((max_height + 1) / 16.0)); 123 | chunk->num_sections = num_chunk_sections; 124 | 125 | for (uint8_t i = 0; i < num_chunk_sections; i++) { 126 | chunk->chunk_sections[i] = generate_chunk_section(heightmap, i); 127 | } 128 | 129 | enif_release_resource(chunk); 130 | return enif_make_tuple2(env, enif_make_atom(env, "ok"), chunk_term); 131 | } 132 | 133 | /* 134 | * Methods for dealing with Chunk resource types. 135 | */ 136 | 137 | // Called whenever Erlang destructs a Chunk resource 138 | void chunk_res_destructor(ErlNifEnv *env, void *resource) { 139 | (void)env; 140 | struct Chunk *chunk = (struct Chunk *)resource; 141 | enif_free((void *)chunk->heightmap); 142 | enif_free((void *)chunk->biome); 143 | for (uint8_t i = 0; i < chunk->num_sections; i++) { 144 | enif_free((void *)chunk->chunk_sections[i]); 145 | } 146 | } 147 | 148 | int nif_load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info) { 149 | (void)priv_data; 150 | (void)load_info; 151 | CHUNK_RES_TYPE = enif_open_resource_type( 152 | env, NULL, "chunk", chunk_res_destructor, ERL_NIF_RT_CREATE, NULL); 153 | return 0; 154 | } 155 | 156 | /* 157 | * NIF Boilerplate. 158 | */ 159 | 160 | static ErlNifFunc nif_funcs[] = { 161 | // {erl_function_name, erl_function_arity, c_function, flags} 162 | {"chunk_biome_data", 1, chunk_biome_data, 0}, 163 | {"generate_chunk", 2, generate_chunk, 0}, 164 | {"get_chunk_coordinates", 1, get_chunk_coordinates, 0}, 165 | {"num_chunk_sections", 1, num_chunk_sections, 0}, 166 | {"serialize_chunk", 1, serialize_chunk, 0}, 167 | {"set_random_seed", 1, set_random_seed, 0}}; 168 | 169 | ERL_NIF_INIT(Elixir.Minecraft.NIF, nif_funcs, nif_load, NULL, NULL, NULL) 170 | -------------------------------------------------------------------------------- /src/perlin.c: -------------------------------------------------------------------------------- 1 | #include "perlin.h" 2 | #include 3 | #include 4 | #include 5 | 6 | int p[512]; 7 | int p2[512]; 8 | 9 | void initialize_random(unsigned seed) { 10 | srand(seed); 11 | for (unsigned i = 0; i < 512; i++) { 12 | p[i] = rand() % 256; 13 | p2[i] = rand() % 4096; 14 | } 15 | } 16 | 17 | static double fade(double t) { 18 | return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); 19 | } 20 | 21 | static double lerp(double a, double b, double x) { return a + x * (b - a); } 22 | 23 | static double grad(int hash, double x, double y, double z) { 24 | switch (hash & 0xF) { 25 | case 0x0: 26 | return x + y; 27 | case 0x1: 28 | return -x + y; 29 | case 0x2: 30 | return x - y; 31 | case 0x3: 32 | return -x - y; 33 | case 0x4: 34 | return x + z; 35 | case 0x5: 36 | return -x + z; 37 | case 0x6: 38 | return x - z; 39 | case 0x7: 40 | return -x - z; 41 | case 0x8: 42 | return y + z; 43 | case 0x9: 44 | return -y + z; 45 | case 0xA: 46 | return y - z; 47 | case 0xB: 48 | return -y - z; 49 | case 0xC: 50 | return y + x; 51 | case 0xD: 52 | return -y + z; 53 | case 0xE: 54 | return y - x; 55 | case 0xF: 56 | return -y - z; 57 | default: 58 | return 0; // never happens 59 | } 60 | } 61 | 62 | int xorhash(int value) { 63 | return (value & 0xFF) ^ ((value >> 8) & 0xFF) ^ ((value >> 16) & 0xFF); 64 | } 65 | 66 | static double perlin(double x, double y, double z) { 67 | int floor_x = (int)floor(x); 68 | int floor_y = (int)floor(y); 69 | int floor_z = (int)floor(z); 70 | int xi = xorhash(floor_x); 71 | int xi1 = xorhash(floor_x + 1); 72 | int yi = xorhash(floor_y); 73 | int yi1 = xorhash(floor_y + 1); 74 | int zi = xorhash(floor_z); 75 | int zi1 = xorhash(floor_z + 1); 76 | double xf = x - floor_x; 77 | double yf = y - floor_y; 78 | double zf = z - floor_z; 79 | double u = fade(xf); 80 | double v = fade(yf); 81 | double w = fade(zf); 82 | int aaa = p[p[p[xi] + yi] + zi]; 83 | int aba = p[p[p[xi] + yi1] + zi]; 84 | int aab = p[p[p[xi] + yi] + zi1]; 85 | int abb = p[p[p[xi] + yi1] + zi1]; 86 | int baa = p[p[p[xi1] + yi] + zi]; 87 | int bba = p[p[p[xi1] + yi1] + zi]; 88 | int bab = p[p[p[xi1] + yi] + zi1]; 89 | int bbb = p[p[p[xi1] + yi1] + zi1]; 90 | double x1 = lerp(grad(aaa, xf, yf, zf), grad(baa, xf - 1.0, yf, zf), u); 91 | double x2 = 92 | lerp(grad(aba, xf, yf - 1.0, zf), grad(bba, xf - 1.0, yf - 1.0, zf), u); 93 | double y1 = lerp(x1, x2, v); 94 | 95 | x1 = lerp(grad(aab, xf, yf, zf - 1.0), grad(bab, xf - 1.0, yf, zf - 1.0), u); 96 | x2 = lerp(grad(abb, xf, yf - 1.0, zf - 1.0), 97 | grad(bbb, xf - 1.0, yf - 1.0, zf - 1.0), u); 98 | double y2 = lerp(x1, x2, v); 99 | 100 | return (lerp(y1, y2, w) + 1.0) / 2.0; 101 | } 102 | 103 | double octave_perlin(double x, double y, double z, int octaves, 104 | double persistence) { 105 | double total = 0; 106 | double frequency = 0.02; 107 | double amplitude = 1; 108 | double maxValue = 0; // Used for normalizing result to 0.0 - 1.0 109 | for (int i = 0; i < octaves; i++) { 110 | total += perlin(x * frequency, y * frequency, z * frequency) * amplitude; 111 | 112 | maxValue += amplitude; 113 | 114 | amplitude *= persistence; 115 | frequency *= 2; 116 | } 117 | 118 | return total / maxValue; 119 | } 120 | -------------------------------------------------------------------------------- /src/perlin.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void initialize_random(unsigned seed); 4 | 5 | double octave_perlin(double x, double y, double z, int octaves, 6 | double persistence); 7 | 8 | int xorhash(int value); 9 | -------------------------------------------------------------------------------- /test/minecraft/crypto/sha_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.Crypto.SHATest do 2 | use ExUnit.Case, async: true 3 | import Minecraft.Crypto.SHA 4 | 5 | test "sha" do 6 | assert sha("Notch") == "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48" 7 | assert sha("jeb_") == "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1" 8 | assert sha("simon") == "88e16a1019277b15d58faf0541e11910eb756f6" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/minecraft/crypto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.CryptoTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Stopping crypto module deletes temp dir" do 5 | {:ok, pid} = GenServer.start_link(Minecraft.Crypto, []) 6 | keys_dir = GenServer.call(pid, :get_keys_dir) 7 | assert {:ok, _} = File.stat(keys_dir) 8 | :ok = GenServer.stop(pid) 9 | assert {:error, _} = File.stat(keys_dir) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/minecraft/integration/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.HandshakeTest do 2 | use ExUnit.Case, async: false 3 | alias Minecraft.Packet.Client 4 | alias Minecraft.Packet.Server 5 | alias Minecraft.TestClient 6 | 7 | import Mock 8 | 9 | @fake_mojang_response %HTTPoison.Response{ 10 | body: ~S({"id": "f7fa50bd53384a68a9e5a601e24cdf8e", "name": "TheCodeBoss"}), 11 | status_code: 200 12 | } 13 | 14 | setup do 15 | {:ok, client} = TestClient.start_link(port: 25565) 16 | %{client: client} 17 | end 18 | 19 | test "status", %{client: client} do 20 | packet = %Client.Handshake{server_addr: "localhost", server_port: 25565, next_state: :status} 21 | assert :ok = TestClient.cast(client, packet) 22 | assert :ok = TestClient.set_state(client, :status) 23 | 24 | assert {:ok, %Server.Status.Response{} = response} = 25 | TestClient.send(client, %Client.Status.Request{}) 26 | 27 | assert Poison.decode!(response.json) == %{ 28 | "version" => %{"name" => "1.12.2", "protocol" => 340}, 29 | "players" => %{"max" => 20, "online" => 0, "sample" => []}, 30 | "description" => %{"text" => "Elixir Minecraft"} 31 | } 32 | 33 | assert {:ok, %Server.Status.Pong{payload: 12_345_678}} = 34 | TestClient.send(client, %Client.Status.Ping{payload: 12_345_678}) 35 | end 36 | 37 | test "login and play", %{client: client} do 38 | with_mock HTTPoison, get!: fn _url -> @fake_mojang_response end do 39 | packet = %Client.Handshake{server_addr: "localhost", server_port: 25565, next_state: :login} 40 | assert :ok = TestClient.cast(client, packet) 41 | assert :ok = TestClient.set_state(client, :login) 42 | 43 | assert {:ok, %Server.Login.EncryptionRequest{} = encryption_request} = 44 | TestClient.send(client, %Client.Login.LoginStart{username: "TheCodeBoss"}) 45 | 46 | shared_secret = :crypto.strong_rand_bytes(16) 47 | 48 | encryption_response = %Client.Login.EncryptionResponse{ 49 | shared_secret: Minecraft.Crypto.encrypt(shared_secret), 50 | verify_token: Minecraft.Crypto.encrypt(encryption_request.verify_token) 51 | } 52 | 53 | assert :ok = TestClient.cast(client, encryption_response) 54 | :ok = TestClient.encrypt(client, shared_secret) 55 | 56 | assert {:ok, %Server.Login.LoginSuccess{username: "TheCodeBoss"}} = 57 | TestClient.receive(client) 58 | 59 | assert :ok = TestClient.set_state(client, :play) 60 | assert :ok = TestClient.cast(client, %Client.Play.ClientSettings{}) 61 | 62 | assert :ok = 63 | TestClient.cast(client, %Client.Play.PluginMessage{ 64 | channel: "MC|Brand", 65 | data: "\avanilla" 66 | }) 67 | 68 | assert {:ok, %Server.Play.JoinGame{}} = TestClient.receive(client) 69 | assert {:ok, %Server.Play.SpawnPosition{}} = TestClient.receive(client) 70 | assert {:ok, %Server.Play.PlayerAbilities{}} = TestClient.receive(client) 71 | 72 | assert {:ok, %Server.Play.PlayerPositionAndLook{} = ppal} = TestClient.receive(client) 73 | 74 | assert :ok = 75 | TestClient.cast(client, %Client.Play.TeleportConfirm{teleport_id: ppal.teleport_id}) 76 | 77 | assert :ok = 78 | TestClient.cast(client, %Client.Play.PlayerPositionAndLook{ 79 | x: ppal.x, 80 | y: ppal.y, 81 | z: ppal.z, 82 | pitch: ppal.pitch, 83 | yaw: ppal.yaw 84 | }) 85 | 86 | assert :ok = TestClient.cast(client, %Client.Play.ClientStatus{}) 87 | end 88 | end 89 | 90 | test "invalid protocol", %{client: client} do 91 | packet = %Client.Handshake{ 92 | protocol_version: 123, 93 | server_addr: "localhost", 94 | server_port: 25565, 95 | next_state: :status 96 | } 97 | 98 | assert {:error, :closed} = TestClient.send(client, packet) 99 | end 100 | 101 | test "invalid packet results in socket closure", %{client: client} do 102 | assert {:error, :closed} = TestClient.send_raw(client, <<1, 2, 3>>) 103 | end 104 | 105 | test "invalid verify token", %{client: client} do 106 | packet = %Client.Handshake{server_addr: "localhost", server_port: 25565, next_state: :login} 107 | assert :ok = TestClient.cast(client, packet) 108 | assert :ok = TestClient.set_state(client, :login) 109 | 110 | assert {:ok, %Server.Login.EncryptionRequest{}} = 111 | TestClient.send(client, %Client.Login.LoginStart{username: "TheCodeBoss"}) 112 | 113 | shared_secret = :crypto.strong_rand_bytes(16) 114 | 115 | encryption_response = %Client.Login.EncryptionResponse{ 116 | shared_secret: Minecraft.Crypto.encrypt(shared_secret), 117 | verify_token: Minecraft.Crypto.encrypt(<<1, 2, 3, 4>>) 118 | } 119 | 120 | assert {:error, :closed} = TestClient.send(client, encryption_response) 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/minecraft/nif_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.NIFTest do 2 | use ExUnit.Case, async: true 3 | 4 | setup_all do 5 | :ok = Minecraft.NIF.set_random_seed(123) 6 | :ok 7 | end 8 | 9 | test "Generating chunks works" do 10 | assert {:ok, _chunk} = Minecraft.NIF.generate_chunk(-10, 53) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/minecraft/packet_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.PacketTest do 2 | use ExUnit.Case, async: true 3 | import Minecraft.Packet 4 | 5 | describe "varints" do 6 | test "basics" do 7 | assert {0, ""} = decode_varint(<<0>>) 8 | assert {1, ""} = decode_varint(<<1>>) 9 | assert {2, ""} = decode_varint(<<2>>) 10 | assert <<0>> = encode_varint(0) 11 | assert <<1>> = encode_varint(1) 12 | assert <<2>> = encode_varint(2) 13 | end 14 | 15 | test "first breakpoint" do 16 | assert {127, ""} = decode_varint(<<0x7F>>) 17 | assert {128, ""} = decode_varint(<<0x80, 0x01>>) 18 | assert {255, ""} = decode_varint(<<0xFF, 0x01>>) 19 | assert <<0x7F>> = encode_varint(127) 20 | assert <<0x80, 0x01>> = encode_varint(128) 21 | assert <<0xFF, 0x01>> = encode_varint(255) 22 | end 23 | 24 | test "limits" do 25 | assert {2_147_483_647, ""} = decode_varint(<<0xFF, 0xFF, 0xFF, 0xFF, 0x07>>) 26 | assert {-1, ""} = decode_varint(<<0xFF, 0xFF, 0xFF, 0xFF, 0x0F>>) 27 | assert {-2_147_483_648, ""} = decode_varint(<<0x80, 0x80, 0x80, 0x80, 0x08>>) 28 | assert <<0xFF, 0xFF, 0xFF, 0xFF, 0x07>> = encode_varint(2_147_483_647) 29 | assert <<0xFF, 0xFF, 0xFF, 0xFF, 0x0F>> = encode_varint(-1) 30 | assert <<0x80, 0x80, 0x80, 0x80, 0x08>> = encode_varint(-2_147_483_648) 31 | end 32 | 33 | test "extra data" do 34 | assert {0, <<1, 2, 3>>} = decode_varint(<<0, 1, 2, 3>>) 35 | assert {255, <<1, 2, 3>>} = decode_varint(<<0xFF, 0x01, 1, 2, 3>>) 36 | assert {-1, <<1, 2, 3>>} = decode_varint(<<0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 1, 2, 3>>) 37 | end 38 | 39 | test "errors" do 40 | assert {:error, :too_short} = decode_varint(<<0xFF>>) 41 | assert {:error, :too_long} = decode_varint(<<0xFF, 0xFF, 0xFF, 0xFF, 0xFF>>) 42 | assert {:error, :too_large} = encode_varint(2_147_483_648) 43 | assert {:error, :too_large} = encode_varint(-2_147_483_649) 44 | end 45 | end 46 | 47 | describe "varlongs" do 48 | test "basics" do 49 | assert {0, ""} = decode_varlong(<<0>>) 50 | assert {1, ""} = decode_varlong(<<1>>) 51 | assert {2, ""} = decode_varlong(<<2>>) 52 | end 53 | 54 | test "first breakpoint" do 55 | assert {127, ""} = decode_varlong(<<0x7F>>) 56 | assert {128, ""} = decode_varlong(<<0x80, 0x01>>) 57 | assert {255, ""} = decode_varlong(<<0xFF, 0x01>>) 58 | end 59 | 60 | test "limits" do 61 | assert {9_223_372_036_854_775_807, ""} = 62 | decode_varlong(<<0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F>>) 63 | 64 | assert {-1, ""} = 65 | decode_varlong(<<0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01>>) 66 | 67 | assert {-2_147_483_648, ""} = 68 | decode_varlong(<<0x80, 0x80, 0x80, 0x80, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0x01>>) 69 | 70 | assert {-9_223_372_036_854_775_808, ""} = 71 | decode_varlong(<<0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01>>) 72 | end 73 | 74 | test "extra data" do 75 | assert {0, <<1, 2, 3>>} = decode_varlong(<<0, 1, 2, 3>>) 76 | assert {255, <<1, 2, 3>>} = decode_varlong(<<0xFF, 0x01, 1, 2, 3>>) 77 | 78 | assert {-1, <<1, 2, 3>>} = 79 | decode_varlong( 80 | <<0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 1, 2, 3>> 81 | ) 82 | end 83 | 84 | test "errors" do 85 | assert {:error, :too_short} = decode_varlong(<<0xFF>>) 86 | 87 | assert {:error, :too_long} = 88 | decode_varlong(<<0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF>>) 89 | end 90 | end 91 | 92 | describe "strings" do 93 | test "basics" do 94 | assert {"", ""} = decode_string(<<0>>) 95 | assert {"a", ""} = decode_string(<<1, "a">>) 96 | assert {"ab", ""} = decode_string(<<2, "ab">>) 97 | assert <<0>> = encode_string("") 98 | assert <<1, "a">> = encode_string("a") 99 | assert <<2, "ab">> = encode_string("ab") 100 | end 101 | 102 | test "larger strings" do 103 | s = String.duplicate("a", 127) 104 | assert <<0x7F, s::binary>> == encode_string(s) 105 | assert {s, ""} == decode_string(encode_string(s)) 106 | s = String.duplicate("a", 128) 107 | assert <<0x80, 0x01, s::binary>> == encode_string(s) 108 | assert {s, ""} == decode_string(encode_string(s)) 109 | end 110 | 111 | test "extra data" do 112 | assert {"", <<1, 2, 3>>} = decode_string(<<0, 1, 2, 3>>) 113 | assert {"a", <<1, 2, 3>>} = decode_string(<<1, "a", 1, 2, 3>>) 114 | end 115 | end 116 | 117 | test "bools" do 118 | assert {false, ""} = decode_bool(<<0>>) 119 | assert {true, ""} = decode_bool(<<1>>) 120 | assert {false, <<1, 2, 3>>} = decode_bool(<<0, 1, 2, 3>>) 121 | assert {true, <<1, 2, 3>>} = decode_bool(<<1, 1, 2, 3>>) 122 | assert <<0>> = encode_bool(false) 123 | assert <<1>> = encode_bool(true) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/minecraft/world_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.WorldTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "Generating chunks works" do 5 | assert %Minecraft.Chunk{} = Minecraft.World.get_chunk(22, 59) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/support/test_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Minecraft.TestClient do 2 | @moduledoc """ 3 | A client for connecting to the Minecraft server. Note that this is only compiled in the test 4 | environment as this is the only place it should be used. 5 | """ 6 | use GenServer 7 | 8 | @type client_opt :: {:port, 0..65535} 9 | @type client_opts :: [client_opt] 10 | 11 | @doc """ 12 | Starts the client. 13 | """ 14 | @spec start_link(client_opts) :: GenServer.on_start() 15 | def start_link(opts \\ []) do 16 | GenServer.start_link(__MODULE__, [opts]) 17 | end 18 | 19 | def encrypt(pid, secret) do 20 | GenServer.call(pid, {:encrypt, secret}) 21 | end 22 | 23 | def receive(pid) do 24 | GenServer.call(pid, :receive) 25 | end 26 | 27 | @doc """ 28 | Sends a packet to the server. 29 | """ 30 | @spec send(pid, packet :: struct) :: {:ok, response :: term} | {:error, term} 31 | def send(pid, packet) do 32 | GenServer.call(pid, {:send, packet}) 33 | end 34 | 35 | @doc """ 36 | Sends raw data to the server. 37 | """ 38 | @spec send_raw(pid, data :: binary) :: {:ok, response :: term} | {:error, term} 39 | def send_raw(pid, data) do 40 | GenServer.call(pid, {:send_raw, data}) 41 | end 42 | 43 | @doc """ 44 | Sets the client's connection state. 45 | """ 46 | @spec set_state(pid, struct) :: :ok | {:error, term} 47 | def set_state(pid, state) do 48 | GenServer.call(pid, {:set_state, state}) 49 | end 50 | 51 | @doc """ 52 | Sends a message to the server without waiting for a response. 53 | """ 54 | @spec cast(pid, struct) :: :ok | {:error, term} 55 | def cast(pid, packet) do 56 | GenServer.cast(pid, {:cast, packet}) 57 | end 58 | 59 | @impl true 60 | def init(opts \\ []) do 61 | port = Keyword.get(opts, :port, 25565) 62 | tcp_opts = [:binary, active: false] 63 | {:ok, socket} = :gen_tcp.connect('localhost', port, tcp_opts) 64 | {:ok, {socket, :handshake, nil, nil, ""}} 65 | end 66 | 67 | @impl true 68 | def handle_call({:encrypt, secret}, _from, {socket, state, nil, nil, pending}) do 69 | encryptor = %Minecraft.Crypto.AES{key: secret, ivec: secret} 70 | decryptor = %Minecraft.Crypto.AES{key: secret, ivec: secret} 71 | {:reply, :ok, {socket, state, encryptor, decryptor, pending}} 72 | end 73 | 74 | def handle_call(:receive, _from, {socket, state, encryptor, decryptor, ""}) do 75 | case :gen_tcp.recv(socket, 0) do 76 | {:ok, response} -> 77 | {response, decryptor} = maybe_decrypt(response, decryptor) 78 | {response_packet, rest} = Minecraft.Packet.deserialize(response, state, :server) 79 | {:reply, {:ok, response_packet}, {socket, state, encryptor, decryptor, rest}} 80 | 81 | {:error, _} = err -> 82 | {:stop, :normal, err, {socket, state, encryptor, decryptor, ""}} 83 | end 84 | end 85 | 86 | def handle_call(:receive, _from, {socket, state, encryptor, decryptor, pending}) do 87 | {response_packet, rest} = Minecraft.Packet.deserialize(pending, state, :server) 88 | {:reply, {:ok, response_packet}, {socket, state, encryptor, decryptor, rest}} 89 | end 90 | 91 | def handle_call({:send, packet}, _from, {socket, state, encryptor, decryptor, pending}) do 92 | {:ok, request} = Minecraft.Packet.serialize(packet) 93 | {request, encryptor} = maybe_encrypt(request, encryptor) 94 | :ok = :gen_tcp.send(socket, request) 95 | 96 | case :gen_tcp.recv(socket, 0) do 97 | {:ok, response} -> 98 | {response, decryptor} = maybe_decrypt(response, decryptor) 99 | {response_packet, ""} = Minecraft.Packet.deserialize(response, state, :server) 100 | {:reply, {:ok, response_packet}, {socket, state, encryptor, decryptor, pending}} 101 | 102 | {:error, _} = err -> 103 | {:stop, :normal, err, {socket, state, encryptor, decryptor, pending}} 104 | end 105 | end 106 | 107 | @impl true 108 | def handle_call({:send_raw, data}, _from, {socket, state, encryptor, decryptor, pending}) do 109 | {data, encryptor} = maybe_encrypt(data, encryptor) 110 | :ok = :gen_tcp.send(socket, data) 111 | 112 | case :gen_tcp.recv(socket, 0) do 113 | {:ok, response} -> 114 | {response, decryptor} = maybe_decrypt(response, decryptor) 115 | {response_packet, ""} = Minecraft.Packet.deserialize(response, state, :server) 116 | {:reply, {:ok, response_packet}, {socket, state, encryptor, decryptor, pending}} 117 | 118 | {:error, _} = err -> 119 | {:stop, :normal, err, {socket, state, encryptor, decryptor, pending}} 120 | end 121 | end 122 | 123 | def handle_call({:set_state, state}, _from, {socket, _old_state, encryptor, decryptor, pending}) do 124 | {:reply, :ok, {socket, state, encryptor, decryptor, pending}} 125 | end 126 | 127 | @impl true 128 | def handle_cast({:cast, packet}, {socket, state, encryptor, decryptor, pending}) do 129 | {:ok, request} = Minecraft.Packet.serialize(packet) 130 | {request, encryptor} = maybe_encrypt(request, encryptor) 131 | :ok = :gen_tcp.send(socket, request) 132 | {:noreply, {socket, state, encryptor, decryptor, pending}} 133 | end 134 | 135 | defp maybe_decrypt(data, nil) do 136 | {data, nil} 137 | end 138 | 139 | defp maybe_decrypt(data, aes) do 140 | Minecraft.Crypto.AES.decrypt(data, aes) 141 | end 142 | 143 | defp maybe_encrypt(data, nil) do 144 | {data, nil} 145 | end 146 | 147 | defp maybe_encrypt(data, aes) do 148 | Minecraft.Crypto.AES.encrypt(data, aes) 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------