Skip to content

Commit

Permalink
Better Configuration (#437)
Browse files Browse the repository at this point in the history
Tides authored May 26, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 59596d7 commit f2704ef
Showing 29 changed files with 775 additions and 513 deletions.
279 changes: 279 additions & 0 deletions .schema/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "IServerConfiguration",
"type": "object",
"x-abstract": true,
"additionalProperties": false,
"properties": {
"Logging": {
"$ref": "#/definitions/logging"
},
"baah": {
"type": [
"boolean",
"null"
]
},
"allowLan": {
"type": "boolean"
},
"motd": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int32"
},
"address": {
"type": [
"null",
"string"
]
},
"onlineMode": {
"type": "boolean"
},
"maxPlayers": {
"type": "integer",
"format": "int32"
},
"pregenerateChunkRange": {
"type": "integer",
"format": "int32"
},
"serverListQuery": {
"$ref": "#/definitions/ServerListQuery"
},
"timeTickSpeedMultiplier": {
"type": "integer",
"format": "int32"
},
"allowOperatorRequests": {
"type": "boolean"
},
"enableRcon": {
"type": "boolean"
},
"whitelist": {
"type": "boolean"
},
"network": {
"$ref": "#/definitions/NetworkConfiguration"
},
"messages": {
"$ref": "#/definitions/MessagesConfiguration"
},
"rcon": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/RconConfiguration"
}
]
},
"viewDistance": {
"type": "integer",
"format": "byte"
}
},
"definitions": {
"logLevelThreshold": {
"description": "Log level threshold.",
"type": "string",
"enum": [
"Trace",
"Debug",
"Information",
"Warning",
"Error",
"Critical",
"None"
]
},
"logLevel": {
"title": "logging level options",
"description": "Log level configurations used when creating logs. Only logs that exceeds its matching log level will be enabled. Each log level configuration has a category specified by its JSON property name. For more information about configuring log levels, see https://docs.microsoft.com/aspnet/core/fundamentals/logging/#configure-logging.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/logLevelThreshold"
}
},
"logging": {
"title": "logging options",
"type": "object",
"description": "Configuration for Microsoft.Extensions.Logging.",
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
},
"Console": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
},
"FormatterName": {
"description": "Name of the log message formatter to use. Defaults to 'simple'.",
"type": "string",
"default": "simple"
},
"FormatterOptions": {
"title": "formatter options",
"description": "Log message formatter options. Additional properties are available on the options depending on the configured formatter. The formatter is specified by FormatterName.",
"type": "object",
"properties": {
"IncludeScopes": {
"description": "Include scopes when true. Defaults to false.",
"type": "boolean",
"default": false
},
"TimestampFormat": {
"description": "Format string used to format timestamp in logging messages. Defaults to null.",
"type": "string"
},
"UseUtcTimestamp": {
"description": "Indication whether or not UTC timezone should be used to for timestamps in logging messages. Defaults to false.",
"type": "boolean",
"default": false
}
}
},
"LogToStandardErrorThreshold": {
"$ref": "#/definitions/logLevelThreshold",
"description": "The minimum level of messages are written to Console.Error."
}
}
},
"EventSource": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
},
"Debug": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
},
"EventLog": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
},
"ElmahIo": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
},
"ElmahIoBreadcrumbs": {
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
}
},
"additionalProperties": {
"title": "provider logging settings",
"type": "object",
"description": "Logging configuration for a provider. The provider name must match the configuration's JSON property property name.",
"properties": {
"LogLevel": {
"$ref": "#/definitions/logLevel"
}
}
}
},
"ServerListQuery": {
"type": "string",
"description": "",
"x-enumNames": [
"Full",
"Anonymized",
"Disabled"
],
"enum": [
"Full",
"Anonymized",
"Disabled"
]
},
"NetworkConfiguration": {
"type": "object",
"additionalProperties": false,
"properties": {
"shouldThrottle": {
"type": "boolean"
},
"keepAliveInterval": {
"type": "integer",
"format": "int64"
},
"keepAliveTimeoutInterval": {
"type": "integer",
"format": "int64"
},
"connectionThrottle": {
"type": "integer",
"format": "int64"
},
"mulitplayerDebugMode": {
"type": "boolean"
}
}
},
"MessagesConfiguration": {
"type": "object",
"additionalProperties": false,
"properties": {
"join": {
"type": "string"
},
"leave": {
"type": "string"
},
"notWhitelisted": {
"type": "string"
},
"serverFull": {
"type": "string"
},
"outdatedClient": {
"type": "string"
},
"outdatedServer": {
"type": "string"
}
}
},
"RconConfiguration": {
"type": "object",
"additionalProperties": false,
"properties": {
"password": {
"type": [
"null",
"string"
]
},
"port": {
"type": "integer"
},
"broadcastToOps": {
"type": "boolean"
},
"requireEncryption": {
"type": "boolean"
}
}
}
}
}
35 changes: 35 additions & 0 deletions .schema/whitelist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "WhitelistConfiguration",
"type": "object",
"additionalProperties": false,
"properties": {
"whitelistedPlayers": {
"type": "array",
"items": {
"$ref": "#/definitions/WhitelistedPlayer"
}
},
"whitelistedIps": {
"type": "array",
"items": {
"type": "string"
}
}
},
"definitions": {
"WhitelistedPlayer": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"id": {
"type": "string",
"format": "guid"
}
}
}
}
}
15 changes: 15 additions & 0 deletions Obsidian.API/Configuration/MessagesConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Obsidian.API.Configuration;
public sealed record class MessagesConfiguration
{
public string Join { get; set; } = "&e{0} joined the game";

public string Leave { get; set; } = "&e{0} left the game";

public string NotWhitelisted { get; set; } = "You are not whitelisted on this server!";

public string ServerFull { get; set; } = "The server is full!";

public string OutdatedClient { get; set; } = "Outdated client! Please use {0}";
public string OutdatedServer { get; set; } = "Outdated server! I'm still on {0}";
}

22 changes: 22 additions & 0 deletions Obsidian.API/Configuration/NetworkConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Obsidian.API.Configuration;
public sealed record class NetworkConfiguration
{
/// <summary>
/// Returns true if <see cref="ConnectionThrottle"/> has a value greater than 0.
/// </summary>
public bool ShouldThrottle => this.ConnectionThrottle > 0;

public long KeepAliveInterval { get; set; } = 10_000;

public long KeepAliveTimeoutInterval { get; set; } = 30_000;

/// <summary>
/// The time in milliseconds to wait before an ip is allowed to try and connect again.
/// </summary>
public long ConnectionThrottle { get; set; } = 15_000;

/// <summary>
/// If true, each login/client gets a random username where multiple connections from the same host will be allowed.
/// </summary>
public bool MulitplayerDebugMode { get; set; } = false;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace Obsidian.API.Config;
namespace Obsidian.API.Configuration;

public sealed class RconConfig
public sealed record class RconConfiguration
{
/// <summary>
/// Password to access the RCON.
/// </summary>
public string Password { get; set; }
public string? Password { get; set; }

/// <summary>
/// Port on which RCON server listens.
117 changes: 117 additions & 0 deletions Obsidian.API/Configuration/ServerConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Obsidian.API.Configuration;
using System.Text.Json.Serialization;

namespace Obsidian.API.Configuration;

public sealed class ServerConfiguration
{
private byte viewDistance = 10;

// Anything lower than 3 will cause weird artifacts on the client.
private const byte MinimumViewDistance = 3;

/// <summary>
/// Enabled Remote Console operation.
/// </summary>
/// <remarks>See more at https://wiki.vg/RCON</remarks>
public bool EnableRcon => Rcon is not null;

/// <summary>
/// Server description.
/// </summary>
public string Motd { get; set; } = $"§k||||§r §5Obsidian §cPre§r-§cRelease §r§k||||§r \n§r§lRunning on .NET §l§c{Environment.Version} §r§l<3";

/// <summary>
/// The port on which to listen for incoming connection attempts.
/// </summary>
public int Port { get; set; } = 25565;

/// <summary>
/// Whether the server uses MojangAPI for loading skins etc.
/// </summary>
public bool OnlineMode { get; set; } = true;

/// <summary>
/// Maximum amount of players that is allowed to be connected at the same time.
/// </summary>
public int MaxPlayers { get; set; } = 25;

/// <summary>
/// Allow people to requests to become an operator.
/// </summary>
public bool AllowOperatorRequests { get; set; } = true;

public bool ServerShutdownStopsProgram { get; set; } = true;

/// <summary>
/// Whether to allow the server to load untrusted(unsigned) plugins
/// </summary>
public bool AllowUntrustedPlugins { get; set; } = true;

public bool? Baah { get; set; }

public bool Whitelist { get; set; }

/// <summary>
/// Network Configuration
/// </summary>
public NetworkConfiguration Network { get; set; } = new();

/// <summary>
/// Remote Console configuration
/// </summary>
public RconConfiguration? Rcon { get; set; }

/// <summary>
/// Messages that the server will use by default for various actions.
/// </summary>
public MessagesConfiguration Messages { get; set; } = new();

/// <summary>
/// Allows the server to advertise itself as a LAN server to devices on your network.
/// </summary>
public bool AllowLan { get; set; } = true; // Enabled because it's super useful for debugging tbh

/// <summary>
/// The view distance of the server.
/// </summary>
/// <remarks>
/// Players with higher view distance will use the server's view distance.
/// </remarks>
public byte ViewDistance
{
get => viewDistance;
set => viewDistance = value >= MinimumViewDistance ? value : MinimumViewDistance;
}

public int PregenerateChunkRange { get; set; } = 15; // by default, pregenerate range from -15 to 15;

public ServerListQuery ServerListQuery { get; set; } = ServerListQuery.Full;

/// <summary>
/// The speed at which world time & rain time go by.
/// </summary>
public int TimeTickSpeedMultiplier { get; set; } = 1;
}

public sealed class ServerWorld
{
public string Name { get; set; } = "overworld";
public string Generator { get; set; } = "overworld";

public string Seed { get; set; } = default!;

public bool Default { get; set; }

public string DefaultDimension { get; set; } = "minecraft:overworld";

public List<string> ChildDimensions { get; set; } = new();
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ServerListQuery
{
Full,
Anonymized,
Disabled
}
7 changes: 7 additions & 0 deletions Obsidian.API/Configuration/WhitelistConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Obsidian.API.Configuration;
public sealed class WhitelistConfiguration
{
public List<WhitelistedPlayer> WhitelistedPlayers { get; set; } = [];

public List<string> WhitelistedIps { get; set; } = [];
}
5 changes: 3 additions & 2 deletions Obsidian.API/_Interfaces/IServer.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using Obsidian.API.Boss;
using Obsidian.API.Configuration;
using Obsidian.API.Crafting;

namespace Obsidian.API;

public interface IServer
public interface IServer : IDisposable
{
public string Version { get; }
public int Port { get; }
@@ -13,7 +14,7 @@ public interface IServer
public IEnumerable<IPlayer> Players { get; }
public IOperatorList Operators { get; }
public IWorld DefaultWorld { get; }
public IServerConfiguration Configuration { get; }
public ServerConfiguration Configuration { get; }

public IScoreboardManager ScoreboardManager { get; }

127 changes: 0 additions & 127 deletions Obsidian.API/_Interfaces/IServerConfiguration.cs

This file was deleted.

47 changes: 22 additions & 25 deletions Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Logging\**" />
<EmbeddedResource Remove="Logging\**" />
<None Remove="Logging\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="config\**\*.*" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Obsidian\Obsidian.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="accepted_keys\obsidian.pub.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Obsidian\Obsidian.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="accepted_keys\obsidian.pub.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
32 changes: 32 additions & 0 deletions Obsidian.ConsoleApp/Program.Functions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Reflection;

public partial class Program
{
private static async ValueTask GenerateConfigFiles()
{
const string path = "config";

Directory.CreateDirectory(path);

var serverJsonFile = Path.Combine(path, "server.json");
var whitelistJsonFile = Path.Combine(path, "whitelist.json");

if (!File.Exists(serverJsonFile))
{
await using var file = File.Create(serverJsonFile);

await using var embeddedFile = Assembly.GetExecutingAssembly().GetManifestResourceStream("Obsidian.ConsoleApp.config.server.json");

await embeddedFile!.CopyToAsync(file);
}

if (!File.Exists(whitelistJsonFile))
{
await using var file = File.Create(whitelistJsonFile);

await using var embeddedFile = Assembly.GetExecutingAssembly().GetManifestResourceStream("Obsidian.ConsoleApp.config.whitelist.json");

await embeddedFile!.CopyToAsync(file);
}
}
}
16 changes: 6 additions & 10 deletions Obsidian.ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Obsidian;
using Obsidian.API.Logging;
using Obsidian.Hosting;

// Cool startup console logo because that's cool
@@ -22,23 +22,19 @@
Console.WriteLine(asciilogo);
Console.ResetColor();

var env = await IServerEnvironment.CreateDefaultAsync();
await GenerateConfigFiles();

var builder = Host.CreateApplicationBuilder();

builder.ConfigureObsidian();

builder.Services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddProvider(new LoggerProvider(env.Configuration.LogLevel));
loggingBuilder.SetMinimumLevel(env.Configuration.LogLevel);
});

builder.Logging.AddFilter((provider, category, logLevel) =>
{
return !category.Contains("Microsoft") || logLevel != LogLevel.Debug;
loggingBuilder.AddSimpleConsole(x => x.ColorBehavior = LoggerColorBehavior.Enabled);
});

builder.Services.AddObsidian(env);
builder.AddObsidian();

// Give the server some time to shut down after CTRL-C or SIGTERM.
builder.Services.Configure<HostOptions>(opts =>
30 changes: 30 additions & 0 deletions Obsidian.ConsoleApp/config/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"$schema": "https://raw.githubusercontent.com/ObsidianMC/Obsidian/master/.schema/server.json",

"allowLan": true,
"allowOperatorRequests": true,

"maxPlayers": 25,
"motd": "�k||||�r �5Obsidian �cPre�r-�cRelease �r�k||||�r \n�r�lRunning on .NET �l�c8 �r�l<3",

"onlineMode": true,
"port": 25565,
"pregenerateChunkRange": 15,
"serverListQuery": "Full",
"timeTickSpeedMultiplier": 1,
"whitelist": false,

"network": {
"connectionThrottle": 15000,
"keepAliveInterval": 10000,
"keepAliveTimeoutInterval": 30000,
"mulitplayerDebugMode": false
},

"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}
5 changes: 5 additions & 0 deletions Obsidian.ConsoleApp/config/whitelist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/ObsidianMC/Obsidian/master/.schema/whitelist.json",
"whitelistedPlayers": [],
"whitelistedIps": []
}
21 changes: 6 additions & 15 deletions Obsidian/Client.cs
Original file line number Diff line number Diff line change
@@ -270,7 +270,7 @@ public async Task StartConnectionAsync()
{
case 0x00:
{
if (this.server.Configuration.CanThrottle)
if (this.server.Configuration.Network.ShouldThrottle)
{
string ip = ((IPEndPoint)connectionContext.RemoteEndPoint!).Address.ToString();

@@ -285,7 +285,7 @@ public async Task StartConnectionAsync()
}
else
{
Server.throttler.TryAdd(ip, DateTimeOffset.UtcNow.AddMilliseconds(this.server.Configuration.ConnectionThrottle));
Server.throttler.TryAdd(ip, DateTimeOffset.UtcNow.AddMilliseconds(this.server.Configuration.Network.ConnectionThrottle));
}
}

@@ -411,7 +411,7 @@ private async Task HandleHandshakeAsync(byte[] data)
private async Task HandleLoginStartAsync(byte[] data)
{
var loginStart = LoginStart.Deserialize(data);
var username = this.server.Configuration.MulitplayerDebugMode ? $"Player{Globals.Random.Next(1, 999)}" : loginStart.Username;
var username = this.server.Configuration.Network.MulitplayerDebugMode ? $"Player{Globals.Random.Next(1, 999)}" : loginStart.Username;
var world = (World)this.server.DefaultWorld;

Logger.LogDebug("Received login request from user {Username}", username);
@@ -426,7 +426,7 @@ private async Task HandleLoginStartAsync(byte[] data)
await DisconnectAsync("Account not found in the Mojang database");
return;
}
else if (this.server.Configuration.WhitelistEnabled && !this.server.Configuration.Whitelisted.Any(x => x.Id == cachedUser.Uuid))
else if (this.server.Configuration.Whitelist && !this.server.WhitelistConfiguration.CurrentValue.WhitelistedPlayers.Any(x => x.Id == cachedUser.Uuid))
{
await DisconnectAsync("You are not whitelisted on this server\nContact server administrator");
return;
@@ -445,7 +445,7 @@ private async Task HandleLoginStartAsync(byte[] data)
VerifyToken = randomToken
});
}
else if (this.server.Configuration.WhitelistEnabled && !this.server.Configuration.Whitelisted.Any(x => x.Name == username))
else if (this.server.Configuration.Whitelist && !this.server.WhitelistConfiguration.CurrentValue.WhitelistedPlayers.Any(x => x.Name == username))
{
await DisconnectAsync("You are not whitelisted on this server\nContact server administrator");
}
@@ -551,7 +551,6 @@ await QueuePacketAsync(new UpdateRecipeBookPacket
SecondRecipeIds = RecipesRegistry.Recipes.Keys.ToList()
});

await SendPlayerListDecoration();
await SendPlayerInfoAsync();
await this.QueuePacketAsync(new GameEventPacket(ChangeGameStateReason.StartWaitingForLevelChunks));

@@ -621,7 +620,7 @@ internal void SendKeepAlive(DateTimeOffset time)
{
long keepAliveId = time.ToUnixTimeMilliseconds();
// first, check if there's any KeepAlives that are older than 30 seconds
if (missedKeepAlives.Any(x => keepAliveId - x > this.server.Configuration.KeepAliveTimeoutInterval))
if (missedKeepAlives.Any(x => keepAliveId - x > this.server.Configuration.Network.KeepAliveTimeoutInterval))
{
// kick player, failed to respond within 30s
cancellationSource.Cancel();
@@ -772,14 +771,6 @@ private async Task SendServerBrand()
Logger.LogDebug("Sent server brand.");
}

private async Task SendPlayerListDecoration()
{
var header = string.IsNullOrWhiteSpace(this.server.Configuration.Header) ? null : ChatMessage.Simple(this.server.Configuration.Header);
var footer = string.IsNullOrWhiteSpace(this.server.Configuration.Footer) ? null : ChatMessage.Simple(this.server.Configuration.Footer);

await QueuePacketAsync(new SetTabListHeaderAndFooterPacket(header, footer));
Logger.LogDebug("Sent player list decoration");
}
#endregion Packet sending

internal void Disconnect()
4 changes: 2 additions & 2 deletions Obsidian/Events/MainEventHandler.cs
Original file line number Diff line number Diff line change
@@ -340,7 +340,7 @@ public async Task OnPlayerLeave(PlayerLeaveEventArgs e)
await other.client.QueuePacketAsync(destroy);
}

server.BroadcastMessage(string.Format(server.Configuration.LeaveMessage, e.Player.Username));
server.BroadcastMessage(string.Format(server.Configuration.Messages.Leave, e.Player.Username));
}

[EventPriority(Priority = Priority.Internal)]
@@ -353,7 +353,7 @@ public async Task OnPlayerJoin(PlayerJoinEventArgs e)

server.BroadcastMessage(new ChatMessage
{
Text = string.Format(server.Configuration.JoinMessage, e.Player.Username),
Text = string.Format(server.Configuration.Messages.Join, e.Player.Username),
Color = HexColor.Yellow
});

95 changes: 10 additions & 85 deletions Obsidian/Hosting/DefaultServerEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IO;
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using System.Threading;

namespace Obsidian.Hosting;
@@ -12,18 +12,11 @@ namespace Obsidian.Hosting;
///
/// Use the <see cref="CreateAsync"/> method to create an instance.
/// </summary>
public sealed class DefaultServerEnvironment : IServerEnvironment
internal sealed class DefaultServerEnvironment(IOptionsMonitor<ServerConfiguration> serverConfig, ILogger<DefaultServerEnvironment> logger) : IServerEnvironment, IDisposable
{
public bool ServerShutdownStopsProgram { get; } = true;
public ServerConfiguration Configuration { get; }
public List<ServerWorld> ServerWorlds { get; }
private readonly ILogger<DefaultServerEnvironment> logger = logger;

private DefaultServerEnvironment(bool serverShutdownStopsProgram, ServerConfiguration configuration, List<ServerWorld> serverWorlds)
{
ServerShutdownStopsProgram = serverShutdownStopsProgram;
Configuration = configuration;
ServerWorlds = serverWorlds;
}
public IOptionsMonitor<ServerConfiguration> ServerConfig { get; } = serverConfig;

/// <summary>
/// Provide server commands using the Console.
@@ -41,12 +34,12 @@ public async Task ProvideServerCommandsAsync(Server server, CancellationToken cT
}
}

Task IServerEnvironment.OnServerStoppedGracefullyAsync(ILogger logger)
Task IServerEnvironment.OnServerStoppedGracefullyAsync()
{
logger.LogInformation("Goodbye!");
return Task.CompletedTask;
}
Task IServerEnvironment.OnServerCrashAsync(ILogger logger, Exception e)
Task IServerEnvironment.OnServerCrashAsync(Exception e)
{
// Write crash log somewhere?
var byeMessages = new[]
@@ -68,78 +61,10 @@ Task IServerEnvironment.OnServerCrashAsync(ILogger logger, Exception e)
return Task.CompletedTask;
}

/// <summary>
/// Create a <see cref="DefaultServerEnvironment"/> asynchronously.
/// </summary>
/// <returns></returns>
public static async Task<DefaultServerEnvironment> CreateAsync()
{
var config = await LoadServerConfigurationAsync();
var worlds = await LoadServerWorldsAsync();
return new DefaultServerEnvironment(true, config, worlds);
}
private static async Task<ServerConfiguration> LoadServerConfigurationAsync()
{
if (!Directory.Exists("config"))
Directory.CreateDirectory("config");

var configFile = new FileInfo(Path.Combine("config", "main.json"));

if (configFile.Exists)
{
await using var configFileStream = configFile.OpenRead();
return await configFileStream.FromJsonAsync<ServerConfiguration>()
?? throw new Exception("Server config file exists, but is invalid. Is it corrupt?");
}

var config = new ServerConfiguration();

await using var fileStream = configFile.Create();

await config.ToJsonAsync(fileStream);
await fileStream.FlushAsync();

Console.WriteLine($"Created new configuration file for Server");
Console.WriteLine($"Please fill in your config with the values you wish to use for your server.");
Console.WriteLine(configFile.FullName);

Console.ReadKey();
Environment.Exit(0);

throw new UnreachableException();
}
private static async Task<List<ServerWorld>> LoadServerWorldsAsync()
public void Dispose()
{
if (!Directory.Exists("config"))
Directory.CreateDirectory("config");

var worldsFile = new FileInfo(Path.Combine("config", "worlds.json"));

if (worldsFile.Exists)
{
await using var worldsFileStream = worldsFile.OpenRead();
return await worldsFileStream.FromJsonAsync<List<ServerWorld>>()
?? throw new Exception("A worlds file does exist, but is invalid. Is it corrupt?");
}

var worlds = new List<ServerWorld>()
{
new()
{
ChildDimensions =
{
"minecraft:the_nether",
"minecraft:the_end"
}
}
};

await using var fileStream = worldsFile.Create();
await worlds.ToJsonAsync(fileStream);

return worlds;
GC.SuppressFinalize(this);

}


}

59 changes: 36 additions & 23 deletions Obsidian/Hosting/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,49 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Obsidian.API.Configuration;
using Obsidian.Commands.Framework;
using Obsidian.Net.Rcon;
using Obsidian.Services;
using Obsidian.WorldData;
using System.IO;

namespace Obsidian.Hosting;
public static class DependencyInjection
{
public static IServiceCollection AddObsidian(this IServiceCollection services, IServerEnvironment env)
public static IHostApplicationBuilder ConfigureObsidian(this IHostApplicationBuilder builder)
{
services.AddSingleton(env);
services.AddSingleton(env.Configuration);
services.AddSingleton<IServerConfiguration>(f => f.GetRequiredService<ServerConfiguration>());

services.AddSingleton<CommandHandler>();
services.AddSingleton<RconServer>();
services.AddSingleton<WorldManager>();
services.AddSingleton<PacketBroadcaster>();
services.AddSingleton<IServer, Server>();
services.AddSingleton<IUserCache, UserCache>();
services.AddSingleton<EventDispatcher>();

services.AddHttpClient();

services.AddHostedService(sp => sp.GetRequiredService<PacketBroadcaster>());
services.AddHostedService<ObsidianHostingService>();
services.AddHostedService(sp => sp.GetRequiredService<WorldManager>());

services.AddSingleton<IWorldManager>(sp => sp.GetRequiredService<WorldManager>());
services.AddSingleton<IPacketBroadcaster>(sp => sp.GetRequiredService<PacketBroadcaster>());
builder.Configuration.AddJsonFile(Path.Combine("config", "server.json"), optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile(Path.Combine("config", "whitelist.json"), optional: false, reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();

return builder;
}

public static IHostApplicationBuilder AddObsidian(this IHostApplicationBuilder builder)
{
builder.Services.Configure<ServerConfiguration>(builder.Configuration);
builder.Services.Configure<WhitelistConfiguration>(builder.Configuration);

builder.Services.AddSingleton<IServerEnvironment, DefaultServerEnvironment>();
builder.Services.AddSingleton<CommandHandler>();
builder.Services.AddSingleton<RconServer>();
builder.Services.AddSingleton<WorldManager>();
builder.Services.AddSingleton<PacketBroadcaster>();
builder.Services.AddSingleton<IServer, Server>();
builder.Services.AddSingleton<IUserCache, UserCache>();
builder.Services.AddSingleton<EventDispatcher>();

builder.Services.AddHttpClient();

builder.Services.AddHostedService(sp => sp.GetRequiredService<PacketBroadcaster>());
builder.Services.AddHostedService<ObsidianHostingService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<WorldManager>());

builder.Services.AddSingleton<IWorldManager>(sp => sp.GetRequiredService<WorldManager>());
builder.Services.AddSingleton<IPacketBroadcaster>(sp => sp.GetRequiredService<PacketBroadcaster>());

return services;
return builder;
}

}
17 changes: 2 additions & 15 deletions Obsidian/Hosting/IServerEnvironment.cs
Original file line number Diff line number Diff line change
@@ -10,13 +10,6 @@ namespace Obsidian.Hosting;
/// </summary>
public interface IServerEnvironment
{
/// <summary>
/// If set to true, after the server shuts down, the application will stop running as well.
/// </summary>
public bool ServerShutdownStopsProgram { get; }
public ServerConfiguration Configuration { get; }
public List<ServerWorld> ServerWorlds { get; }

/// <summary>
/// Execute commands on the server. This task will run for the lifetime of the server.
/// </summary>
@@ -29,20 +22,14 @@ public interface IServerEnvironment
/// </summary>
/// <param name="logger"></param>
/// <returns></returns>
public Task OnServerStoppedGracefullyAsync(ILogger logger);
public Task OnServerStoppedGracefullyAsync();

/// <summary>
/// Called when the server stopped due to a crash.
/// </summary>
/// <param name="logger"></param>
/// <param name="e"></param>
/// <returns></returns>
public Task OnServerCrashAsync(ILogger logger, Exception e);

/// <summary>
/// Create a <see cref="DefaultServerEnvironment"/> asynchronously, which is aimed for use in Console Applications.
/// </summary>
/// <returns></returns>
public static Task<DefaultServerEnvironment> CreateDefaultAsync() => DefaultServerEnvironment.CreateAsync();
public Task OnServerCrashAsync(Exception e);

}
35 changes: 14 additions & 21 deletions Obsidian/Hosting/ObsidianHostingService.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using System.Threading;

namespace Obsidian.Hosting;
internal sealed class ObsidianHostingService : BackgroundService
internal sealed class ObsidianHostingService(
IHostApplicationLifetime lifetime,
IServer server,
IServerEnvironment env,
IOptionsMonitor<ServerConfiguration> serverConfiguration) : BackgroundService
{
private readonly IHostApplicationLifetime _lifetime;
private readonly IServerEnvironment _environment;
private readonly IServer _server;
private readonly ILogger _logger;

public ObsidianHostingService(
IHostApplicationLifetime lifetime,
IServer server,
IServerEnvironment env,
ILogger<ObsidianHostingService> logger)
{
_server = server;
_lifetime = lifetime;
_environment = env;
_logger = logger;
}
private readonly IHostApplicationLifetime _lifetime = lifetime;
private readonly IServerEnvironment _environment = env;
private readonly IServer _server = server;
private readonly IOptionsMonitor<ServerConfiguration> serverConfiguration = serverConfiguration;

protected async override Task ExecuteAsync(CancellationToken cToken)
{
try
{
await _server.RunAsync();
await _environment.OnServerStoppedGracefullyAsync(_logger);
await _environment.OnServerStoppedGracefullyAsync();
}
catch (Exception e)
{
await _environment.OnServerCrashAsync(_logger, e);
await _environment.OnServerCrashAsync(e);
}

if (_environment.ServerShutdownStopsProgram)
if (serverConfiguration.CurrentValue.ServerShutdownStopsProgram)
_lifetime.StopApplication();
}

40 changes: 19 additions & 21 deletions Obsidian/Net/ClientHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Obsidian.API.Configuration;
using Obsidian.API.Logging;
using Obsidian.Net.Packets;
using Obsidian.Net.Packets.Configuration;
@@ -12,10 +13,10 @@ namespace Obsidian.Net;
public sealed class ClientHandler
{
private ConcurrentDictionary<int, IServerboundPacket> Packets { get; } = new ConcurrentDictionary<int, IServerboundPacket>();
private IServerConfiguration config;
private ServerConfiguration config;
private readonly ILogger _logger;

public ClientHandler(IServerConfiguration config)
public ClientHandler(ServerConfiguration config)
{
this.config = config;
var loggerProvider = new LoggerProvider(LogLevel.Error);
@@ -97,23 +98,22 @@ public async Task HandleConfigurationPackets(int id, byte[] data, Client client)
await HandleFromPoolAsync<ResourcePackResponse>(data, client);
break;
default:
{
if (!Packets.TryGetValue(id, out var packet))
return;

try
{
packet.Populate(data);
await packet.HandleAsync(client.server, client.Player);
}
catch (Exception e)
{
if (config.VerboseExceptionLogging)
_logger.LogError(e, e.Message);
if (!Packets.TryGetValue(id, out var packet))
return;

try
{
packet.Populate(data);
await packet.HandleAsync(client.server, client.Player);
}
catch (Exception e)
{
_logger.LogCritical(e, "{exceptionMessage}", e.Message);
}

break;
}

break;
}
}
}

@@ -214,8 +214,7 @@ public async Task HandlePlayPackets(int id, byte[] data, Client client)
}
catch (Exception e)
{
if (config.VerboseExceptionLogging)
_logger.LogError(e, e.Message);
_logger.LogCritical(e, "{exceptionMessage}", e.Message);
}
break;
}
@@ -231,8 +230,7 @@ public async Task HandlePlayPackets(int id, byte[] data, Client client)
}
catch (Exception e)
{
if (client.server.Configuration.VerboseExceptionLogging)
_logger.LogError(e, "{message}", e.Message);
_logger.LogCritical(e, "{exceptionMessage}", e.Message);
}
ObjectPool<T>.Shared.Return(packet);
}
26 changes: 10 additions & 16 deletions Obsidian/Net/Rcon/RconServer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using Obsidian.Commands.Framework;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
@@ -9,31 +11,23 @@
using System.Threading;

namespace Obsidian.Net.Rcon;
public sealed class RconServer
public sealed class RconServer(ILogger<RconServer> logger, IOptions<ServerConfiguration> config, CommandHandler commandHandler)
{
private const int KEY_SIZE = 256;
private const int CERTAINTY = 5;

private readonly ILogger _logger;
private readonly IServerConfiguration _config;
private readonly CommandHandler _cmdHandler;
private readonly List<RconConnection> _connections;

public RconServer(ILogger<RconServer> logger, IServerConfiguration config, CommandHandler commandHandler)
{
_logger = logger;
_config = config;
_cmdHandler = commandHandler;
_connections = new();
}
private readonly ILogger _logger = logger;
private readonly IOptions<ServerConfiguration> _config = config;
private readonly CommandHandler _cmdHandler = commandHandler;
private readonly List<RconConnection> _connections = new();

public async Task RunAsync(Server server, CancellationToken cToken)
{
_logger.LogInformation(message: "Generating keys for RCON");
var data = GenerateKeys(server);
_logger.LogInformation("Done generating keys for RCON");

var tcpListener = TcpListener.Create(_config.Rcon?.Port ?? 25575);
var tcpListener = TcpListener.Create(_config.Value.Rcon?.Port ?? 25575);

_ = Task.Run(async () =>
{
@@ -71,7 +65,7 @@ public async Task RunAsync(Server server, CancellationToken cToken)

private InitData GenerateKeys(Server server)
{
string password = _config.Rcon?.Password ??
string password = _config.Value.Rcon?.Password ??
throw new InvalidOperationException("You can't start a RconServer without setting a password in the configuration.");


@@ -87,7 +81,7 @@ private InitData GenerateKeys(Server server)
return new InitData(server,
_cmdHandler,
password,
_config.Rcon.RequireEncryption,
_config.Value.Rcon.RequireEncryption,
dhParameters,
keyPair);
}
19 changes: 11 additions & 8 deletions Obsidian/Plugins/PluginManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Obsidian.API.Logging;
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using Obsidian.API.Plugins;
using Obsidian.Commands.Framework;
using Obsidian.Hosting;
@@ -19,6 +21,7 @@ namespace Obsidian.Plugins;
public sealed class PluginManager
{
internal readonly ILogger logger;
private readonly IConfiguration configuration;
internal readonly IServer server;

private static PackedPluginProvider packedPluginProvider = default!;
@@ -54,19 +57,20 @@ public sealed class PluginManager
public IServiceProvider PluginServiceProvider { get; private set; } = default!;

public PluginManager(IServiceProvider serverProvider, IServer server,
EventDispatcher eventDispatcher, CommandHandler commandHandler, ILogger logger)
EventDispatcher eventDispatcher, CommandHandler commandHandler, ILogger logger, IConfiguration configuration)
{
var env = serverProvider.GetRequiredService<IServerEnvironment>();

this.server = server;
this.commandHandler = commandHandler;
this.logger = logger;
this.configuration = configuration;
this.serverProvider = serverProvider;
this.pluginRegistry = new PluginRegistry(this, eventDispatcher, commandHandler, logger);

packedPluginProvider = new(this, logger);

ConfigureInitialServices(env);
ConfigureInitialServices();

DirectoryWatcher.Filters = [".obby"];
DirectoryWatcher.FileChanged += async (path) =>
@@ -220,15 +224,14 @@ public async ValueTask OnServerReadyAsync()
public PluginContainer GetPluginContainerByAssembly(Assembly? assembly = null) =>
this.Plugins.First(x => x.PluginAssembly == (assembly ?? Assembly.GetCallingAssembly()));

private void ConfigureInitialServices(IServerEnvironment env)
private void ConfigureInitialServices()
{
this.pluginServiceDescriptors.AddLogging((builder) =>
{
builder.ClearProviders();
builder.AddProvider(new LoggerProvider(env.Configuration.LogLevel));
builder.SetMinimumLevel(env.Configuration.LogLevel);
builder.AddConfiguration(this.configuration);
});
this.pluginServiceDescriptors.AddSingleton<IServerConfiguration>(x => env.Configuration);
this.pluginServiceDescriptors.AddSingleton(serverProvider.GetRequiredService<IOptionsMonitor<ServerConfiguration>>());
}

private async ValueTask<PluginContainer> HandlePluginAsync(PluginContainer pluginContainer)
51 changes: 37 additions & 14 deletions Obsidian/Server.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Connections;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Obsidian.API.Boss;
using Obsidian.API.Builders;
using Obsidian.API.Configuration;
using Obsidian.API.Crafting;
using Obsidian.API.Events;
using Obsidian.API.Utilities;
@@ -13,7 +16,6 @@
using Obsidian.Concurrency;
using Obsidian.Entities;
using Obsidian.Events;
using Obsidian.Hosting;
using Obsidian.Net;
using Obsidian.Net.Packets;
using Obsidian.Net.Packets.Play.Clientbound;
@@ -67,8 +69,11 @@ public static string VERSION
internal readonly ILogger _logger;
private readonly IServiceProvider serviceProvider;

private IDisposable? configWatcher;
private IConnectionListener? _tcpListener;

public IOptionsMonitor<WhitelistConfiguration> WhitelistConfiguration { get; }

public ProtocolVersion Protocol => DefaultProtocol;

public int Tps { get; private set; }
@@ -85,20 +90,23 @@ public static string VERSION

public HashSet<string> RegisteredChannels { get; } = new();
public CommandHandler CommandsHandler { get; }
public IServerConfiguration Configuration { get; }
public ServerConfiguration Configuration { get; set; }
public string Version => VERSION;

public string Brand { get; } = "obsidian";
public int Port { get; }
public IWorld DefaultWorld => WorldManager.DefaultWorld;
public IEnumerable<IPlayer> Players => GetPlayers();



/// <summary>
/// Creates a new instance of <see cref="Server"/>.
/// </summary>
public Server(
IHostApplicationLifetime lifetime,
IServerEnvironment environment,
IOptionsMonitor<ServerConfiguration> configuration,
IOptionsMonitor<WhitelistConfiguration> whitelistConfiguration,
ILoggerFactory loggerFactory,
IWorldManager worldManager,
RconServer rconServer,
@@ -107,16 +115,19 @@ public Server(
CommandHandler commandHandler,
IServiceProvider serviceProvider)
{
Configuration = environment.Configuration;
_logger = loggerFactory.CreateLogger<Server>();
_logger.LogInformation("SHA / Version: {VERSION}", VERSION);
_cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(lifetime.ApplicationStopping);
_cancelTokenSource.Token.Register(() => _logger.LogWarning("Obsidian is shutting down..."));
_rconServer = rconServer;

this.serviceProvider = serviceProvider;
this.configWatcher = configuration.OnChange(this.ConfigChanged);

Port = Configuration.Port;
var config = configuration.CurrentValue;

Configuration = config;
Port = config.Port;

Operators = new OperatorList(this, loggerFactory);
ScoreboardManager = new ScoreboardManager(this, loggerFactory);
@@ -125,7 +136,8 @@ public Server(

CommandsHandler = commandHandler;

PluginManager = new PluginManager(this.serviceProvider, this, eventDispatcher, CommandsHandler, loggerFactory.CreateLogger<PluginManager>());
PluginManager = new PluginManager(this.serviceProvider, this, eventDispatcher, CommandsHandler, loggerFactory.CreateLogger<PluginManager>(),
serviceProvider.GetRequiredService<IConfiguration>());

_logger.LogDebug("Registering commands...");
CommandsHandler.RegisterCommandClass<MainCommandModule>(null);
@@ -136,13 +148,15 @@ public Server(

this.userCache = playerCache;
this.EventDispatcher = eventDispatcher;
this.WhitelistConfiguration = whitelistConfiguration;
this.loggerFactory = loggerFactory;
this.WorldManager = worldManager;

Directory.CreateDirectory(PermissionPath);
Directory.CreateDirectory(PersistentDataPath);

if (Configuration.AllowLan)
//TODO turn this into a hosted service
if (config.AllowLan)
{
_ = Task.Run(async () =>
{
@@ -152,17 +166,19 @@ public Server(
byte[] bytes = []; // Cached motd as utf-8 bytes
while (await timer.WaitForNextTickAsync(_cancelTokenSource.Token))
{
if (Configuration.Motd != lastMotd)
if (config.Motd != lastMotd)
{
lastMotd = Configuration.Motd;
bytes = Encoding.UTF8.GetBytes($"[MOTD]{Configuration.Motd.Replace('[', '(').Replace(']', ')')}[/MOTD][AD]{Configuration.Port}[/AD]");
lastMotd = config.Motd;
bytes = Encoding.UTF8.GetBytes($"[MOTD]{config.Motd.Replace('[', '(').Replace(']', ')')}[/MOTD][AD]{config.Port}[/AD]");
}
await udpClient.SendAsync(bytes, bytes.Length);
}
});
}
}

private void ConfigChanged(ServerConfiguration configuration) => this.Configuration = configuration;

// TODO make sure to re-send recipes
public void RegisterRecipes(params IRecipe[] recipes)
{
@@ -234,7 +250,7 @@ public async Task RunAsync()
var loadTimeStopwatch = Stopwatch.StartNew();

// Check if MPDM and OM are enabled, if so, we can't handle connections
if (Configuration.MulitplayerDebugMode && Configuration.OnlineMode)
if (Configuration.Network.MulitplayerDebugMode && Configuration.OnlineMode)
{
_logger.LogError("Incompatible Config: Multiplayer debug mode can't be enabled at the same time as online mode since usernames will be overwritten");
await StopAsync();
@@ -354,14 +370,14 @@ private async Task AcceptClientsAsync()

string ip = ((IPEndPoint)connection.RemoteEndPoint!).Address.ToString();

if (Configuration.IpWhitelistEnabled && !Configuration.WhitelistedIPs.Contains(ip))
if (Configuration.Whitelist && !WhitelistConfiguration.CurrentValue.WhitelistedIps.Contains(ip))
{
_logger.LogInformation("{ip} is not whitelisted. Closing connection", ip);
connection.Abort();
return;
}

if (this.Configuration.CanThrottle)
if (this.Configuration.Network.ShouldThrottle)
{
if (throttler.TryGetValue(ip, out var time) && time <= DateTimeOffset.UtcNow)
{
@@ -529,7 +545,7 @@ private async Task LoopAsync()
while (await timer.WaitForNextTickAsync())
{
keepAliveTicks++;
if (keepAliveTicks > (Configuration.KeepAliveInterval / 50)) // to clarify: one tick is 50 milliseconds. 50 * 200 = 10000 millis means 10 seconds
if (keepAliveTicks > (Configuration.Network.KeepAliveInterval / 50)) // to clarify: one tick is 50 milliseconds. 50 * 200 = 10000 millis means 10 seconds
{
var keepAliveTime = DateTimeOffset.Now;

@@ -587,4 +603,11 @@ internal void UpdateStatusConsole()
var status = $" tps:{Tps} c:{WorldManager.GeneratingChunkCount}/{WorldManager.LoadedChunkCount} r:{WorldManager.RegionCount}";
ConsoleIO.UpdateStatusLine(status);
}

public void Dispose()
{
GC.SuppressFinalize(this);

this.configWatcher?.Dispose();
}
}
2 changes: 1 addition & 1 deletion Obsidian/Services/PacketBroadcaster.cs
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
catch (Exception e) when (e is not OperationCanceledException)
{
await this.environment.OnServerCrashAsync(this.logger, e);
await this.environment.OnServerCrashAsync(e);
}
}

102 changes: 0 additions & 102 deletions Obsidian/Utilities/ServerConfiguration.cs

This file was deleted.

7 changes: 3 additions & 4 deletions Obsidian/Utilities/ServerStatus.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Obsidian.API.Configuration;
using Obsidian.API.Logging;
using Obsidian.API.Utilities;
using Obsidian.Entities;
@@ -109,15 +110,13 @@ public void AddPlayer(string username, Guid uuid) => Sample.Add(new
});
}

public sealed class ServerDescription : IServerDescription
public sealed class ServerDescription(ServerConfiguration configuration) : IServerDescription
{
[JsonIgnore]
public string Text { get => text; set => text = FormatText(value); }

[JsonInclude]
private string text;

public ServerDescription(IServerConfiguration configuration) => this.text = FormatText(configuration.Motd);
private string text = FormatText(configuration.Motd);

private static string FormatText(string text) => text.Replace('&', '§');
}
3 changes: 2 additions & 1 deletion Obsidian/WorldData/World.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Obsidian.API.Configuration;
using Obsidian.API.Registry.Codecs.Dimensions;
using Obsidian.API.Utilities;
using Obsidian.Blocks;
@@ -55,7 +56,7 @@ public sealed class World : IWorld
public int LoadedChunkCount => this.Regions.Values.Sum(x => x.LoadedChunkCount);

public required IPacketBroadcaster PacketBroadcaster { get; init; }
public required IServerConfiguration Configuration { get; init; }
public required ServerConfiguration Configuration { get; init; }

public Gamemode DefaultGamemode => LevelData.DefaultGamemode;

64 changes: 46 additions & 18 deletions Obsidian/WorldData/WorldManager.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using Obsidian.Hosting;
using Obsidian.Registries;
using Obsidian.Services;
using Obsidian.WorldData.Generators;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;

namespace Obsidian.WorldData;

public sealed class WorldManager : BackgroundService, IWorldManager
public sealed class WorldManager(ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IOptionsMonitor<ServerConfiguration> configuration,
IServerEnvironment serverEnvironment) : BackgroundService, IWorldManager
{
private readonly ILogger logger;
private readonly ILogger logger = loggerFactory.CreateLogger<WorldManager>();
private readonly Dictionary<string, IWorld> worlds = new();
private readonly List<ServerWorld> serverWorlds;
private readonly ILoggerFactory loggerFactory;
private readonly IServerEnvironment serverEnvironment;
private readonly IServiceScope serviceScope;
private readonly ILoggerFactory loggerFactory = loggerFactory;
private readonly IServiceProvider serviceProvider = serviceProvider;
private readonly IOptionsMonitor<ServerConfiguration> configuration = configuration;
private readonly IServerEnvironment serverEnvironment = serverEnvironment;
private readonly IServiceScope serviceScope = serviceProvider.CreateScope();

public bool ReadyToJoin { get; private set; }

@@ -30,15 +35,6 @@ public sealed class WorldManager : BackgroundService, IWorldManager

public Dictionary<string, Type> WorldGenerators { get; } = new();

public WorldManager(ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IServerEnvironment serverEnvironment)
{
this.logger = loggerFactory.CreateLogger<WorldManager>();
this.serverWorlds = serverEnvironment.ServerWorlds;
this.loggerFactory = loggerFactory;
this.serverEnvironment = serverEnvironment;
this.serviceScope = serviceProvider.CreateScope();
}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var timer = new BalancingTimer(20, stoppingToken);
@@ -59,7 +55,7 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken)
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
await this.serverEnvironment.OnServerCrashAsync(this.logger, ex);
await this.serverEnvironment.OnServerCrashAsync(ex);
}

}
@@ -80,7 +76,9 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken)

public async Task LoadWorldsAsync()
{
foreach (var serverWorld in this.serverWorlds)
var worlds = await LoadServerWorldsAsync();

foreach (var serverWorld in worlds)
{
//var server = (Server)this.server;
if (!this.WorldGenerators.TryGetValue(serverWorld.Generator, out var generatorType))
@@ -92,7 +90,7 @@ public async Task LoadWorldsAsync()
//TODO fix
var world = new World(this.loggerFactory.CreateLogger($"World [{serverWorld.Name}]"), generatorType, this)
{
Configuration = this.serverEnvironment.Configuration,
Configuration = this.configuration.CurrentValue,
PacketBroadcaster = this.serviceScope.ServiceProvider.GetRequiredService<IPacketBroadcaster>(),
Name = serverWorld.Name,
Seed = serverWorld.Seed
@@ -181,4 +179,34 @@ private void RegisterDefaults()
this.RegisterGenerator<IslandGenerator>();
this.RegisterGenerator<EmptyWorldGenerator>();
}

private static async Task<List<ServerWorld>> LoadServerWorldsAsync()
{
var worldsFile = new FileInfo(Path.Combine("config", "worlds.json"));

if (worldsFile.Exists)
{
await using var worldsFileStream = worldsFile.OpenRead();
return await worldsFileStream.FromJsonAsync<List<ServerWorld>>()
?? throw new Exception("A worlds file does exist, but is invalid. Is it corrupt?");
}

var worlds = new List<ServerWorld>()
{
new()
{
ChildDimensions =
{
"minecraft:the_nether",
"minecraft:the_end"
},
Seed = Globals.Random.Next().ToString()
}
};

await using var fileStream = worldsFile.Create();
await worlds.ToJsonAsync(fileStream);

return worlds;
}
}

0 comments on commit f2704ef

Please sign in to comment.