Compare commits
9 Commits
main
...
data_gener
Author | SHA1 | Date | |
---|---|---|---|
4a387ff296 | |||
e8f0e1b4c0 | |||
0b8bbca40b | |||
4e702caa6a | |||
ceb4daaf57 | |||
9a926d524e | |||
d6ea25a64e | |||
02c05122d7 | |||
827004e145 |
@ -30,12 +30,13 @@ defmodule Amethyst.Application do
|
|||||||
{PartitionSupervisor,
|
{PartitionSupervisor,
|
||||||
child_spec: DynamicSupervisor.child_spec([]),
|
child_spec: DynamicSupervisor.child_spec([]),
|
||||||
name: Amethyst.GameMetaSupervisor
|
name: Amethyst.GameMetaSupervisor
|
||||||
}
|
},
|
||||||
|
{Registry, keys: :unique, name: Amethyst.BlockStateRegistry},
|
||||||
]
|
]
|
||||||
|
|
||||||
children = case Application.fetch_env!(:amethyst, :port) do
|
children = case Application.fetch_env!(:amethyst, :port) do
|
||||||
:no_listen -> children
|
:no_listen -> children
|
||||||
port -> [Supervisor.child_spec({Task, fn -> Amethyst.TCPListener.accept(port) end}, restart: :permanent) | children]
|
port -> [Supervisor.child_spec({Task, fn -> Amethyst.TCPListener.accept(port) end}, restart: :permanent, id: Amethyst.TCPListener) | children]
|
||||||
end
|
end
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
99
apps/amethyst/lib/blockstates.ex
Normal file
99
apps/amethyst/lib/blockstates.ex
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Amethyst - An experimental Minecraft server written in Elixir.
|
||||||
|
# Copyright (C) 2024 KodiCraft
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
defmodule Amethyst.BlockStates do
|
||||||
|
use GenServer
|
||||||
|
@moduledoc """
|
||||||
|
GenServer which can be populated with block states and their corresponding IDs.
|
||||||
|
It can be queried by block identifier and state to get the corresponding ID.
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def start_link(map, name) do
|
||||||
|
GenServer.start_link(__MODULE__, map, name: name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(map) do
|
||||||
|
{:ok, map}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_cast({:add, id, bs, bsi}, state) do
|
||||||
|
state = Map.put_new(state, id, %{})
|
||||||
|
state = Map.put(state, id, Map.put(state[id], bs, bsi))
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:get, id, bs}, _from, state) do
|
||||||
|
case Map.get(state, id) do
|
||||||
|
nil -> {:reply, nil, state}
|
||||||
|
block_states -> {:reply, Map.get(block_states, bs), state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call(:debug, _from, state) do
|
||||||
|
Logger.debug("BlockRegistry state: #{inspect(state)}")
|
||||||
|
{:reply, :ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Adds a block state to the registry.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `version` - The Minecraft version
|
||||||
|
- `id` - The block identifier
|
||||||
|
- `bs` - The block state
|
||||||
|
- `bsi` - The block state ID
|
||||||
|
"""
|
||||||
|
@spec add(String.t(), String.t(), map(), integer()) :: :ok
|
||||||
|
def add(version, id, bs, bsi) do
|
||||||
|
ps = {:via, Registry, {Amethyst.BlockStateRegistry, version}}
|
||||||
|
GenServer.cast({:via, PartitionSupervisor, {ps, id}}, {:add, id, bs, bsi})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the block state ID for a given block identifier and block state.
|
||||||
|
If no data is known for this version, errors. Prefer using `try_get/3`
|
||||||
|
if you can wait for the data to be loaded.
|
||||||
|
- `version` - The Minecraft version
|
||||||
|
- `id` - The block identifier
|
||||||
|
- `bs` - The block state, nil if the block has no states
|
||||||
|
"""
|
||||||
|
@spec get(String.t(), String.t(), map() | nil) :: integer() | nil
|
||||||
|
def get(version, id, bs \\ %{}) do
|
||||||
|
ps = {:via, Registry, {Amethyst.BlockStateRegistry, version}}
|
||||||
|
GenServer.call({:via, PartitionSupervisor, {ps, id}}, {:get, id, bs})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Tries to get the block state ID for a given block identifier and block state.
|
||||||
|
If no data is known for this version, it will block until the data is loaded.
|
||||||
|
- `version` - The Minecraft version
|
||||||
|
- `id` - The block identifier
|
||||||
|
- `bs` - The block state, nil if the block has no states
|
||||||
|
"""
|
||||||
|
@spec try_get(String.t(), String.t(), map() | nil) :: integer() | nil
|
||||||
|
def try_get(version, id, bs \\ %{}) do
|
||||||
|
case Registry.lookup(Amethyst.BlockStateRegistry, version) do
|
||||||
|
[{_pid, _}] ->
|
||||||
|
ps = {:via, Registry, {Amethyst.BlockStateRegistry, version}}
|
||||||
|
GenServer.call({:via, PartitionSupervisor, {ps, id}}, {:get, id, bs})
|
||||||
|
[] ->
|
||||||
|
Logger.warning("BlockStateRegistry doesn't exist for version #{version}, generating it now!")
|
||||||
|
Amethyst.DataGenerator.generate_and_populate_data(version, "/tmp/minecraft_server_#{version}.jar", "/tmp/minecraft_data_#{version}")
|
||||||
|
try_get(version, id, bs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
141
apps/amethyst/lib/datagen.ex
Normal file
141
apps/amethyst/lib/datagen.ex
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# Amethyst - An experimental Minecraft server written in Elixir.
|
||||||
|
# Copyright (C) 2024 KodiCraft
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
defmodule Amethyst.DataGenerator do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
This module allows Amethyst to download the vanilla Minecraft server and generate
|
||||||
|
necessary data files.
|
||||||
|
"""
|
||||||
|
@spec get_version_meta(String.t() | :latest) :: {:ok, map()} | {:error, term}
|
||||||
|
def get_version_meta(version) do
|
||||||
|
version = if version == :latest, do: get_latest_version(), else: {:ok, version}
|
||||||
|
case version do
|
||||||
|
{:ok, version} ->
|
||||||
|
case Req.get(Application.fetch_env!(:amethyst, :mojang_game_meta)) do
|
||||||
|
{:ok, %{body: %{"versions" => versions}}} ->
|
||||||
|
case Enum.find(versions, fn %{"id" => id} -> id == version end) do
|
||||||
|
nil -> {:error, :version_not_found}
|
||||||
|
version_header ->
|
||||||
|
# Get the version manifest
|
||||||
|
case Req.get(version_header["url"]) do
|
||||||
|
{:ok, %{body: version_meta}} -> {:ok, version_meta}
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_server_jar(:latest | String.t(), path) :: {:ok, path} | {:error, term} when path: String.t()
|
||||||
|
def get_server_jar(version, path) do
|
||||||
|
case get_version_meta(version) do
|
||||||
|
{:ok, %{"downloads" => %{"server" => %{"url" => url, "sha1" => sha1}}}} ->
|
||||||
|
# Check if the file already exists and has the right hash
|
||||||
|
Logger.debug("Checking if server jar exists at #{path} with hash #{sha1}")
|
||||||
|
case File.read(path) do
|
||||||
|
{:ok, data} ->
|
||||||
|
if :crypto.hash(:sha, data) |> Base.encode16() |> String.downcase() == sha1 do
|
||||||
|
Logger.debug("Using cached server jar at #{path}")
|
||||||
|
{:ok, path}
|
||||||
|
else
|
||||||
|
Logger.debug("Mismatched hash, redownloading server jar")
|
||||||
|
download_jar(url, path)
|
||||||
|
end
|
||||||
|
{:error, _} -> download_jar(url, path)
|
||||||
|
end
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec generate_data_files(String.t(), out_path, nil | String.t()) :: {:error, pos_integer()} | {:ok, out_path} when out_path: String.t()
|
||||||
|
def generate_data_files(jar_path, out_path, java_bin \\ "java") do
|
||||||
|
Logger.info("Generating data files")
|
||||||
|
# mkdir the output directory since we cd into it
|
||||||
|
File.mkdir_p!(out_path)
|
||||||
|
case System.cmd(java_bin, ["-DbundlerMainClass=net.minecraft.data.Main", "-jar", jar_path, "--all", "--output", out_path], cd: out_path) do
|
||||||
|
{_, 0} -> {:ok, out_path}
|
||||||
|
{_, code} -> {:error, code}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec populate_blockstates(String.t(), String.t()) :: :ok | {:error, term}
|
||||||
|
def populate_blockstates(data_path, version) do
|
||||||
|
block_file = Path.join([data_path, "reports", "blocks.json"])
|
||||||
|
case File.read(block_file) do
|
||||||
|
{:ok, data} ->
|
||||||
|
case Jason.decode(data) do
|
||||||
|
{:ok, blocks} ->
|
||||||
|
Enum.each(blocks, fn {id, %{"states" => states}} ->
|
||||||
|
Enum.each(states, fn
|
||||||
|
%{"id" => bsi, "properties" => props} -> Amethyst.BlockStates.add(version, id, props, bsi)
|
||||||
|
%{"id" => bsi, "default" => true} -> Amethyst.BlockStates.add(version, id, %{}, bsi)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
:ok
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_blockregistry(version) do
|
||||||
|
# TODO: unsupervised
|
||||||
|
Amethyst.BlockStates.start_link(%{}, {:via, Registry, {Amethyst.BlockStateRegistry, version}})
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec generate_and_populate_data(String.t() | :latest, String.t(), String.t(), String.t() | nil) :: :ok | {:error, term}
|
||||||
|
def generate_and_populate_data(version, jar_path, data_dir, java_bin \\ "java") do
|
||||||
|
start_time = Time.utc_now()
|
||||||
|
{:ok, version} = case version do
|
||||||
|
:latest -> get_latest_version()
|
||||||
|
version -> {:ok, version}
|
||||||
|
end
|
||||||
|
case get_server_jar(version, jar_path) do
|
||||||
|
{:ok, jar_path} ->
|
||||||
|
case generate_data_files(jar_path, data_dir, java_bin) do
|
||||||
|
{:ok, data_path} ->
|
||||||
|
Logger.info("Finished generating data in #{Time.diff(Time.utc_now(), start_time, :millisecond)}ms")
|
||||||
|
create_blockregistry(version)
|
||||||
|
populate_blockstates(version, data_path)
|
||||||
|
Logger.info("Finished generating and populating data in #{Time.diff(Time.utc_now(), start_time, :millisecond)}ms")
|
||||||
|
{:error, code} -> {:error, code}
|
||||||
|
end
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp download_jar(url, path) do
|
||||||
|
Logger.info("Downloading server jar from #{url} to #{path}")
|
||||||
|
case Req.get(url, into: File.stream!(path)) do
|
||||||
|
{:ok, _} -> {:ok, path}
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_latest_version() ::
|
||||||
|
{:error, %{:__exception__ => true, :__struct__ => atom(), optional(atom()) => any()}}
|
||||||
|
| {:ok, any()}
|
||||||
|
def get_latest_version() do
|
||||||
|
case Req.get(Application.fetch_env!(:amethyst, :mojang_game_meta)) do
|
||||||
|
{:ok, %{body: %{"latest" => %{"release" => release}}}} -> {:ok, release}
|
||||||
|
{:error, err} -> {:error, err}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -12,3 +12,7 @@ import Config
|
|||||||
level: :debug,
|
level: :debug,
|
||||||
format: "$date $time [$level] $metadata$message\n",
|
format: "$date $time [$level] $metadata$message\n",
|
||||||
metadata: []
|
metadata: []
|
||||||
|
|
||||||
|
config :amethyst,
|
||||||
|
mojang_game_meta: "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json",
|
||||||
|
session_server: "https://sessionserver.mojang.com"
|
||||||
|
@ -4,7 +4,6 @@ config :amethyst,
|
|||||||
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
||||||
encryption: true, # Whether or not to request encryption from clients.
|
encryption: true, # Whether or not to request encryption from clients.
|
||||||
auth: true, # Whether or not users should be authenticated with Mojang.
|
auth: true, # Whether or not users should be authenticated with Mojang.
|
||||||
session_server: "https://sessionserver.mojang.com", # Base URL to the Mojang session server
|
|
||||||
compression: 256, # Packets larger than this amount are sent compressed. Set to nil to disable compression.
|
compression: 256, # Packets larger than this amount are sent compressed. Set to nil to disable compression.
|
||||||
default_game: Example.Game, # Which game new players should be sent to
|
default_game: Example.Game, # Which game new players should be sent to
|
||||||
release: config_env() == :prod # If this is set to false, Amethyst will perform additional checks at runtime and will handle errors more loosely
|
release: config_env() == :prod # If this is set to false, Amethyst will perform additional checks at runtime and will handle errors more loosely
|
||||||
|
Loading…
Reference in New Issue
Block a user