Compare commits

..

9 Commits

Author SHA1 Message Date
4a387ff296
Attempt at implementing versioned block registry
All checks were successful
Build & Test / nix-build (push) Successful in 1m47s
2024-10-16 09:58:24 +02:00
e8f0e1b4c0
Minor
All checks were successful
Build & Test / nix-build (push) Successful in 1m48s
2024-10-15 11:58:14 +02:00
0b8bbca40b
Remove needless empty line
All checks were successful
Build & Test / nix-build (push) Successful in 1m46s
2024-10-15 11:29:51 +02:00
4e702caa6a
Parallelize BlockRegistry 2024-10-15 11:29:23 +02:00
ceb4daaf57
Implement block registry
All checks were successful
Build & Test / nix-build (push) Successful in 2m41s
2024-10-15 11:10:33 +02:00
9a926d524e
Implement generating data files
All checks were successful
Build & Test / nix-build (push) Successful in 1m46s
2024-10-11 19:56:06 +02:00
d6ea25a64e
Try implementing keeping old server jars
All checks were successful
Build & Test / nix-build (push) Successful in 1m46s
2024-10-10 17:55:43 +02:00
02c05122d7
Implement downloading server jar
All checks were successful
Build & Test / nix-build (push) Successful in 1m48s
2024-10-10 15:46:28 +02:00
827004e145
Begin work on data generator
All checks were successful
Build & Test / nix-build (push) Successful in 1m47s
2024-10-10 14:42:40 +02:00
5 changed files with 247 additions and 3 deletions

View File

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

View 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

View 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

View File

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

View File

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