diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 327f69b8f..e40ec678c 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -21,6 +21,8 @@ jobs:
steps:
- uses: actions/checkout@v5
+ with:
+ submodules: recursive
- name: xXx_set_env_variables_xXx
run: echo "GIT_SHA=${GITHUB_SHA}" >> $GITHUB_ENV
- name: Setup .NET
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..255b6cf14
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "Obsidian.NBT"]
+ path = Obsidian.Nbt
+ url = https://github.com/ObsidianMC/Nbt
diff --git a/.vscode/launch.json b/.vscode/launch.json
index a088a3ed4..1d61b4673 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,62 +1,62 @@
{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Launch Obsidian Server",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "clean-and-build-console-app",
- "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll",
- "args": [],
- "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0",
- "console": "integratedTerminal",
- "stopAtEntry": false,
- "internalConsoleOptions": "neverOpen"
- },
- {
- "name": "Launch Obsidian Server (New World)",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "clean-build-and-delete-world",
- "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll",
- "args": [],
- "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0",
- "console": "integratedTerminal",
- "stopAtEntry": false,
- "internalConsoleOptions": "neverOpen"
- },
- {
- "name": "Launch Obsidian Server (Release)",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-console-app-release",
- "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Release/net10.0/Obsidian.ConsoleApp.dll",
- "args": [],
- "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Release/net10.0",
- "console": "integratedTerminal",
- "stopAtEntry": false,
- "internalConsoleOptions": "neverOpen"
- },
- {
- "name": "Attach to Process",
- "type": "coreclr",
- "request": "attach"
- },
- {
- "name": "Debug Tests",
- "type": "coreclr",
- "request": "launch",
- "preLaunchTask": "build-tests",
- "program": "dotnet",
- "args": [
- "test",
- "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
- "--no-build"
- ],
- "cwd": "${workspaceFolder}",
- "console": "integratedTerminal",
- "stopAtEntry": false,
- "internalConsoleOptions": "neverOpen"
- }
- ]
-}
\ No newline at end of file
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch Obsidian Server",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "clean-and-build-console-app",
+ "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0",
+ "console": "integratedTerminal",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch Obsidian Server (New World)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "clean-build-and-delete-world",
+ "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0",
+ "console": "integratedTerminal",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch Obsidian Server (Release)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build-console-app-release",
+ "program": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Release/net10.0/Obsidian.ConsoleApp.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Release/net10.0",
+ "console": "integratedTerminal",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Attach to Process",
+ "type": "coreclr",
+ "request": "attach"
+ },
+ {
+ "name": "Debug Tests",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build-tests",
+ "program": "dotnet",
+ "args": [
+ "test",
+ "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
+ "--no-build"
+ ],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "stopAtEntry": false,
+ "internalConsoleOptions": "neverOpen"
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 23a086156..13b6c27e4 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,210 +1,188 @@
{
- "version": "2.0.0",
- "tasks": [
- {
- "label": "build-solution",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/Obsidian.sln",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
- ],
- "problemMatcher": "$msCompile",
- "group": "build"
- },
- {
- "label": "build-console-app",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary",
- "-c",
- "Debug"
- ],
- "problemMatcher": "$msCompile",
- "group": {
- "kind": "build",
- "isDefault": true
- }
- },
- {
- "label": "clean-and-build-console-app",
- "dependsOn": [
- "clean",
- "build-console-app"
- ],
- "dependsOrder": "sequence",
- "problemMatcher": []
- },
- {
- "label": "delete-world",
- "type": "shell",
- "command": "rm",
- "args": [
- "-rf",
- "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/worlds"
- ],
- "problemMatcher": []
- },
- {
- "label": "clean-build-and-delete-world",
- "dependsOn": [
- "delete-world",
- "clean",
- "build-console-app"
- ],
- "dependsOrder": "sequence",
- "problemMatcher": []
- },
- {
- "label": "build-console-app-release",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary",
- "-c",
- "Release"
- ],
- "problemMatcher": "$msCompile",
- "group": "build"
- },
- {
- "label": "build-tests",
- "command": "dotnet",
- "type": "process",
- "args": [
- "build",
- "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
- ],
- "problemMatcher": "$msCompile",
- "group": "build"
- },
- {
- "label": "clean",
- "command": "dotnet",
- "type": "process",
- "args": [
- "clean",
- "${workspaceFolder}/Obsidian.sln",
- "/property:GenerateFullPaths=true",
- "/consoleloggerparameters:NoSummary"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "restore",
- "command": "dotnet",
- "type": "process",
- "args": [
- "restore",
- "${workspaceFolder}/Obsidian.sln"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "test",
- "command": "dotnet",
- "type": "process",
- "args": [
- "test",
- "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
- "--no-build",
- "--verbosity",
- "normal"
- ],
- "problemMatcher": "$msCompile",
- "group": {
- "kind": "test",
- "isDefault": true
- },
- "dependsOn": [
- "build-tests"
- ]
- },
- {
- "label": "test-verbose",
- "command": "dotnet",
- "type": "process",
- "args": [
- "test",
- "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
- "--verbosity",
- "detailed"
- ],
- "problemMatcher": "$msCompile",
- "group": "test"
- },
- {
- "label": "run-server",
- "command": "dotnet",
- "type": "process",
- "args": [
- "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll"
- ],
- "options": {
- "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0"
- },
- "problemMatcher": "$msCompile",
- "isBackground": true,
- "presentation": {
- "reveal": "always",
- "panel": "new"
- },
- "dependsOn": [
- "build-console-app"
- ]
- },
- {
- "label": "publish-release",
- "command": "dotnet",
- "type": "process",
- "args": [
- "publish",
- "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
- "-c",
- "Release",
- "-o",
- "${workspaceFolder}/publish"
- ],
- "problemMatcher": "$msCompile"
- },
- {
- "label": "docker-build",
- "command": "docker",
- "type": "shell",
- "args": [
- "build",
- ".",
- "-t",
- "obsidian:latest"
- ],
- "options": {
- "cwd": "${workspaceFolder}"
- },
- "problemMatcher": []
- },
- {
- "label": "docker-compose-up",
- "command": "docker-compose",
- "type": "shell",
- "args": [
- "up",
- "-d"
- ],
- "options": {
- "cwd": "${workspaceFolder}"
- },
- "problemMatcher": []
- }
- ]
-}
\ No newline at end of file
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build-solution",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/Obsidian.slnx",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "build"
+ },
+ {
+ "label": "build-console-app",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary",
+ "-c",
+ "Debug"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ }
+ },
+ {
+ "label": "clean-and-build-console-app",
+ "dependsOn": ["clean", "build-console-app"],
+ "dependsOrder": "sequence",
+ "problemMatcher": []
+ },
+ {
+ "label": "delete-world",
+ "type": "shell",
+ "command": "rm",
+ "args": [
+ "-rf",
+ "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/worlds"
+ ],
+ "problemMatcher": []
+ },
+ {
+ "label": "clean-build-and-delete-world",
+ "dependsOn": ["delete-world", "clean", "build-console-app"],
+ "dependsOrder": "sequence",
+ "problemMatcher": []
+ },
+ {
+ "label": "build-console-app-release",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary",
+ "-c",
+ "Release"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "build"
+ },
+ {
+ "label": "build-tests",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "build"
+ },
+ {
+ "label": "clean",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "clean",
+ "${workspaceFolder}/Obsidian.slnx",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "restore",
+ "command": "dotnet",
+ "type": "process",
+ "args": ["restore", "${workspaceFolder}/Obsidian.sln"],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "test",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "test",
+ "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
+ "--no-build",
+ "--verbosity",
+ "normal"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": {
+ "kind": "test",
+ "isDefault": true
+ },
+ "dependsOn": ["build-tests"]
+ },
+ {
+ "label": "test-verbose",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "test",
+ "${workspaceFolder}/Obsidian.Tests/Obsidian.Tests.csproj",
+ "--verbosity",
+ "detailed"
+ ],
+ "problemMatcher": "$msCompile",
+ "group": "test"
+ },
+ {
+ "label": "run-server",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0/Obsidian.ConsoleApp.dll"
+ ],
+ "options": {
+ "cwd": "${workspaceFolder}/Obsidian.ConsoleApp/bin/Debug/net10.0"
+ },
+ "problemMatcher": "$msCompile",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "always",
+ "panel": "new"
+ },
+ "dependsOn": ["build-console-app"]
+ },
+ {
+ "label": "publish-release",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/Obsidian.ConsoleApp/Obsidian.ConsoleApp.csproj",
+ "-c",
+ "Release",
+ "-o",
+ "${workspaceFolder}/publish"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "docker-build",
+ "command": "docker",
+ "type": "shell",
+ "args": ["build", ".", "-t", "obsidian:latest"],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ },
+ {
+ "label": "docker-compose-up",
+ "command": "docker-compose",
+ "type": "shell",
+ "args": ["up", "-d"],
+ "options": {
+ "cwd": "${workspaceFolder}"
+ },
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/Obsidian.API/ChunkData/IBlockUpdate.cs b/Obsidian.API/ChunkData/IBlockUpdate.cs
index e98823d5c..640e18a32 100644
--- a/Obsidian.API/ChunkData/IBlockUpdate.cs
+++ b/Obsidian.API/ChunkData/IBlockUpdate.cs
@@ -4,7 +4,7 @@ public interface IBlockUpdate
{
public IBlock? Block { get; set; }
- public IWorld World { get; }
+ public ILevel Level { get; }
public Vector Position { get; set; }
public int Delay { get; set; }
diff --git a/Obsidian.API/Configuration/ServerConfiguration.cs b/Obsidian.API/Configuration/ServerConfiguration.cs
index 3e5773268..d6ae00836 100644
--- a/Obsidian.API/Configuration/ServerConfiguration.cs
+++ b/Obsidian.API/Configuration/ServerConfiguration.cs
@@ -10,6 +10,7 @@ public sealed class ServerConfiguration
// Anything lower than 3 will cause weird artifacts on the client.
private const byte MinimumViewDistance = 3;
private const byte MinimumSimulationDistance = 5;
+
///
/// Enabled Remote Console operation.
///
@@ -97,6 +98,8 @@ public ushort EntityBroadcastRangePercentage
set => field = Math.Max((ushort)10, value);
}
+ public int SpawnChunkRadius { get; set; } = 12;
+
public int PregenerateChunkRange { get; set; } = 15; // by default, pregenerate range from -15 to 15;
public ServerListQuery ServerListQuery { get; set; } = ServerListQuery.Full;
diff --git a/Obsidian.API/Events/BlockBreakEventArgs.cs b/Obsidian.API/Events/BlockBreakEventArgs.cs
index 2adb99bcb..f2c5a626e 100644
--- a/Obsidian.API/Events/BlockBreakEventArgs.cs
+++ b/Obsidian.API/Events/BlockBreakEventArgs.cs
@@ -10,7 +10,7 @@ public class BlockBreakEventArgs : BlockEventArgs, ICancellable
///
public bool IsCancelled { get; private set; }
- internal BlockBreakEventArgs(IServer server, IPlayer player, IBlock block, Vector location, IWorld world) : base(server, block, location, world)
+ internal BlockBreakEventArgs(IServer server, IPlayer player, IBlock block, Vector location, ILevel level) : base(server, block, location, level)
{
Player = player;
}
diff --git a/Obsidian.API/Events/BlockEventArgs.cs b/Obsidian.API/Events/BlockEventArgs.cs
index 35abe1f53..db79cb942 100644
--- a/Obsidian.API/Events/BlockEventArgs.cs
+++ b/Obsidian.API/Events/BlockEventArgs.cs
@@ -1,6 +1,6 @@
namespace Obsidian.API.Events;
-public abstract class BlockEventArgs(IServer server, IBlock block, Vector location, IWorld world) : BaseMinecraftEventArgs(server)
+public abstract class BlockEventArgs(IServer server, IBlock block, Vector location, ILevel level) : BaseMinecraftEventArgs(server)
{
///
/// The impacted block.
@@ -13,9 +13,9 @@ public abstract class BlockEventArgs(IServer server, IBlock block, Vector locati
public Vector Location { get; } = location;
///
- /// World where the event took place.
+ /// Level where the event took place.
///
- public IWorld World { get; } = world;
+ public ILevel Level { get; } = level;
public int Sequence { get; init; }
diff --git a/Obsidian.API/Obsidian.API.csproj b/Obsidian.API/Obsidian.API.csproj
index 4f964cd61..167d977a4 100644
--- a/Obsidian.API/Obsidian.API.csproj
+++ b/Obsidian.API/Obsidian.API.csproj
@@ -87,7 +87,7 @@
-
+
diff --git a/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs
new file mode 100644
index 000000000..96bc4e736
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs
@@ -0,0 +1,8 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionGeneratorBiomeEntry
+{
+ public string Biome { get; set; }
+
+ public DimensionGeneratorBiomeParameters Parameters { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs
new file mode 100644
index 000000000..a39cce57f
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs
@@ -0,0 +1,25 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionGeneratorBiomeParameters
+{
+ public DimensionGeneratorParameterValue Offset { get; set; }
+
+ public DimensionGeneratorParameterValue Continentalness { get; set; }
+
+ public DimensionGeneratorParameterValue Weirdness { get; set; }
+
+ public DimensionGeneratorParameterValue Erosion { get; set; }
+
+ public DimensionGeneratorParameterValue Depth { get; set; }
+
+ public DimensionGeneratorParameterValue Humidity { get; set; }
+
+ public DimensionGeneratorParameterValue Temperature { get; set; }
+}
+
+public sealed record class DimensionGeneratorParameterValue
+{
+ public float? Value { get; set; }
+
+ public DimensionGeneratorValueRange? Range { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeSource.cs b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeSource.cs
new file mode 100644
index 000000000..4e5229e03
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeSource.cs
@@ -0,0 +1,12 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionGeneratorBiomeSource
+{
+ public string Type { get; set; }
+
+ public string? Preset { get; set; }
+
+ public string? Biome { get; set; }
+
+ public List? Biomes { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs b/Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs
new file mode 100644
index 000000000..9b1e00bae
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs
@@ -0,0 +1,11 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionGeneratorSettings
+{
+ public IDimensionSetting Settings { get; set; }
+ public string Type { get; set; }
+
+ public string? Dimension { get; set; }
+
+ public DimensionGeneratorBiomeSource BiomeSource { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/DimensionGeneratorValueRange.cs b/Obsidian.API/World/DimensionSettings/DimensionGeneratorValueRange.cs
new file mode 100644
index 000000000..d9f3a164e
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionGeneratorValueRange.cs
@@ -0,0 +1,8 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionGeneratorValueRange
+{
+ public float Min { get; set; }
+
+ public float Max { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/DimensionSetting.cs b/Obsidian.API/World/DimensionSettings/DimensionSetting.cs
new file mode 100644
index 000000000..2f1c9be0b
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/DimensionSetting.cs
@@ -0,0 +1,8 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public sealed record class DimensionSetting
+{
+ public string Type { get; set; }
+
+ public DimensionGeneratorSettings Generator { get; set; }
+}
diff --git a/Obsidian.API/World/DimensionSettings/IDimensionSetting.cs b/Obsidian.API/World/DimensionSettings/IDimensionSetting.cs
new file mode 100644
index 000000000..c4995dec1
--- /dev/null
+++ b/Obsidian.API/World/DimensionSettings/IDimensionSetting.cs
@@ -0,0 +1,6 @@
+namespace Obsidian.API.World.DimensionSettings;
+
+public interface IDimensionSetting
+{
+ public string Type { get; set; }
+}
diff --git a/Obsidian.API/World/ILevel.cs b/Obsidian.API/World/ILevel.cs
new file mode 100644
index 000000000..80d6cd5df
--- /dev/null
+++ b/Obsidian.API/World/ILevel.cs
@@ -0,0 +1,106 @@
+using Obsidian.API.ChunkData;
+using Obsidian.API.Entities;
+using Obsidian.API.Registry.Codecs.Dimensions;
+using System.Collections.Concurrent;
+
+namespace Obsidian.API.World;
+
+public interface ILevel : IAsyncDisposable
+{
+ public string Name { get; }
+ public string Seed { get; }
+ public string FolderPath { get; }
+ public string DimensionName { get; }
+ public string LevelDataFilePath { get; }
+
+ public bool Loaded { get; }
+
+ public long Time { get; set; }
+ public int DayTime { get; set; }
+
+ public LevelData LevelData { get; }
+
+ public Gamemode DefaultGamemode { get; }
+
+ public int RegionCount { get; }
+ public int LoadedChunkCount { get; }
+ public int ChunksToGenCount { get; }
+
+ public IPacketBroadcaster PacketBroadcaster { get; }
+ public IEventDispatcher EventDispatcher { get; }
+
+ public ConcurrentDictionary Players { get; }
+
+ public IEntitySpawner GetNewEntitySpawner();
+
+ public IEnumerable GetNonPlayerEntitiesInRange(VectorF location, float distance);
+ public IEnumerable GetEntitiesInRange(VectorF location, float distance);
+ public IEnumerable GetPlayersInRange(VectorF location, float distance);
+ public IEnumerable GetPlayersInChunkRange(Vector worldPosition);
+
+ ///
+ /// Gets a Chunk from a Region.
+ /// If the Chunk doesn't exist, it will be scheduled for generation unless scheduleGeneration is false.
+ ///
+ ///
+ /// Whether to enqueue a job to generate the chunk if it doesn't exist and return null.
+ /// When set to false, a partial Chunk is returned.
+ /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk.
+ public ValueTask GetChunkAsync(int x, int z, bool scheduleGeneration = true);
+
+ ///
+ /// Gets a Chunk from a Region.
+ /// If the Chunk doesn't exist, it will be scheduled for generation unless scheduleGeneration is false.
+ ///
+ ///
+ /// Whether to enqueue a job to generate the chunk if it doesn't exist and return null.
+ /// When set to false, a partial Chunk is returned.
+ /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk.
+ public ValueTask GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true);
+
+ public ValueTask DestroyEntityAsync(IEntity entity);
+ public ValueTask GetBlockAsync(Vector location);
+ public ValueTask GetBlockAsync(int x, int y, int z);
+ public ValueTask SetBlockAsync(Vector location, IBlock block);
+ public ValueTask SetBlockAsync(int x, int y, int z, IBlock block);
+
+ public ValueTask SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate);
+ public ValueTask SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate);
+ public bool TryRemovePlayer(IPlayer player);
+ public ValueTask HandleBlockUpdateAsync(IBlockUpdate update);
+ public ValueTask BlockUpdateNeighborsAsync(IBlockUpdate update);
+ public ValueTask ScheduleBlockUpdateAsync(IBlockUpdate blockUpdate);
+
+ public ValueTask SetBlockEntity(Vector blockPosition, IBlockEntity tileEntityData);
+ public ValueTask SetBlockEntity(int x, int y, int z, IBlockEntity tileEntityData);
+
+ public ValueTask GetBlockEntityAsync(Vector blockPosition);
+ public ValueTask GetBlockEntityAsync(int x, int y, int z);
+
+ public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false);
+ public ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false);
+
+ public ValueTask GetWorldSurfaceHeightAsync(int x, int z);
+
+ public bool TryAddEntity(IEntity entity);
+ public bool TryAddPlayer(IPlayer player);
+
+ public IEntity SpawnEntity(VectorF position, EntityType type);
+ public IEntity SpawnEntity(IEntity entity);
+ public IEntity SpawnFallingBlock(VectorF position, Material mat);
+ public void SpawnExperienceOrbs(VectorF position, short count);
+ public IEnumerable PlayersInRange(Vector location);
+ public Task DoWorldTickAsync();
+ public Task FlushRegionsAsync();
+ public Task LoadAsync(DimensionCodec codec);
+ public Task SaveAsync();
+
+ public Task GenerateAsync();
+
+ ///
+ /// Initializes the level with the given dimension codec.
+ /// This is only used when a new world is being created, otherwise LoadAsync is used.
+ ///
+ /// The dimension codec to use for initialization.
+ public void Initialize(DimensionCodec codec);
+}
diff --git a/Obsidian.API/World/ILevelFactory.cs b/Obsidian.API/World/ILevelFactory.cs
new file mode 100644
index 000000000..501609d3f
--- /dev/null
+++ b/Obsidian.API/World/ILevelFactory.cs
@@ -0,0 +1,34 @@
+namespace Obsidian.API.World;
+
+public interface ILevelFactory
+{
+ ///
+ /// Creates a new world with the specified name and generator.
+ ///
+ /// The name of the world to create.
+ /// The seed to use for world generation.
+ /// The namspaced ID of the world generator to use.
+ /// The created world.
+ public IWorld CreateWorld(string name, string seed, string generatorId);
+
+ ///
+ /// Creates a new dimension within the specified parent world with the given name and generator.
+ ///
+ /// The parent world in which to create the dimension.
+ /// The name of the dimension to create.
+ /// The ID of the dimension generator to use.
+ /// The created dimension.
+ public IDimension CreateDimension(IWorld parentWorld, string name, string generatorId);
+
+ ///
+ /// Initializes the level factory, preparing it for use.
+ /// This may involve setting up internal data structures, registering default generators, or performing any necessary setup tasks to ensure that the factory is ready to create worlds and dimensions.
+ ///
+ public void Initialize();
+
+ ///
+ /// Registers a new level generator of the specified type.
+ ///
+ /// The type of the level generator to register.
+ public void RegisterGenerator() where T : ILevelGenerator, new();
+}
diff --git a/Obsidian/WorldData/IWorldGenerator.cs b/Obsidian.API/World/ILevelGenerator.cs
similarity index 61%
rename from Obsidian/WorldData/IWorldGenerator.cs
rename to Obsidian.API/World/ILevelGenerator.cs
index ada65f6a3..1b771eb56 100644
--- a/Obsidian/WorldData/IWorldGenerator.cs
+++ b/Obsidian.API/World/ILevelGenerator.cs
@@ -1,10 +1,10 @@
-namespace Obsidian.WorldData;
+namespace Obsidian.API.World;
-public interface IWorldGenerator
+public interface ILevelGenerator
{
public string Id { get; }
- public void Init(IWorld world);
+ public void Init(ILevel level);
public ValueTask GenerateChunkAsync(int x, int z, IChunk? chunk = null, ChunkGenStage stage = ChunkGenStage.full);
}
diff --git a/Obsidian.API/World/WorldGenSetting.cs b/Obsidian.API/World/WorldGenSetting.cs
new file mode 100644
index 000000000..0ad4a86af
--- /dev/null
+++ b/Obsidian.API/World/WorldGenSetting.cs
@@ -0,0 +1,13 @@
+using Obsidian.API.World.DimensionSettings;
+
+namespace Obsidian.API.World;
+
+public sealed class WorldGenSetting
+{
+ public bool BonusChest { get; set; }
+ public bool GenerateFeatures { get; set; }
+
+ public long Seed { get; set; }
+
+ public Dictionary Dimensions { get; set; }
+}
diff --git a/Obsidian.API/_Interfaces/IDimension.cs b/Obsidian.API/_Interfaces/IDimension.cs
new file mode 100644
index 000000000..015f50f93
--- /dev/null
+++ b/Obsidian.API/_Interfaces/IDimension.cs
@@ -0,0 +1,6 @@
+namespace Obsidian.API;
+
+public interface IDimension : ILevel
+{
+ public IWorld ParentWorld { get; }
+}
diff --git a/Obsidian.API/_Interfaces/IEntity.cs b/Obsidian.API/_Interfaces/IEntity.cs
index 8c57f4a80..5aced90b2 100644
--- a/Obsidian.API/_Interfaces/IEntity.cs
+++ b/Obsidian.API/_Interfaces/IEntity.cs
@@ -4,7 +4,7 @@ namespace Obsidian.API;
public interface IEntity
{
- public IWorld World { get; }
+ public ILevel Level { get; }
public INavigator? Navigator { get; set; }
public IGoalController? GoalController { get; set; }
diff --git a/Obsidian.API/_Interfaces/IPacketBroadcaster.cs b/Obsidian.API/_Interfaces/IPacketBroadcaster.cs
index 4489515b3..f0cc128d0 100644
--- a/Obsidian.API/_Interfaces/IPacketBroadcaster.cs
+++ b/Obsidian.API/_Interfaces/IPacketBroadcaster.cs
@@ -12,25 +12,25 @@ public interface IPacketBroadcaster
///
/// Sends the packets directly to connected clients without processing in a queue.
///
- /// The world to broadcast this packet to.
+ /// The level to broadcast this packet to.
/// The packet to send.
/// The list of entity ids to exlude from the broadcast.
- public void BroadcastToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds);
+ public void BroadcastToLevel(ILevel toLevel, IClientboundPacket packet, params int[] excludedIds);
- public void BroadcastToWorldInRange(IWorld world, VectorF location, IClientboundPacket packet, params int[] excludedIds);
+ public void BroadcastToLevelInRange(ILevel level, VectorF location, IClientboundPacket packet, params int[] excludedIds);
public void QueuePacketTo(IClientboundPacket packet, params int[] ids);
public void QueuePacketTo(IClientboundPacket packet, int priority, params int[] ids);
///
/// Puts the packet in a priority queue for processing then broadcasting when dequeued.
///
- /// The world to broadcast this packet to.
+ /// The level to broadcast this packet to.
/// The packet to send.
/// The list of entity ids to exlude from the broadcast.
/// /// Packets queued without a priority set will be queued up with a priority of 1.
- public void QueuePacketToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds);
+ public void QueuePacketToLevel(ILevel toLevel, IClientboundPacket packet, params int[] excludedIds);
- public void QueuePacketToWorldInRange(IWorld world, VectorF location, IClientboundPacket packet, params int[] excludedIds);
+ public void QueuePacketToLevelInRange(ILevel level, VectorF location, IClientboundPacket packet, params int[] excludedIds);
///
/// Puts the packet in a priority queue for processing then broadcasting when dequeued.
@@ -43,11 +43,11 @@ public interface IPacketBroadcaster
///
/// Puts the packet in a priority queue for processing then broadcasting when dequeued.
///
- /// The world to broadcast this packet to.
+ /// The level to broadcast this packet to.
/// The priority to set the packet in the queue. Higher priority = better
/// The packet to send.
/// The list of entity ids to exlude from the broadcast.
- public void QueuePacketToWorld(IWorld toWorld, int priority, IClientboundPacket packet, params int[] excludedIds);
+ public void QueuePacketToLevel(ILevel toLevel, int priority, IClientboundPacket packet, params int[] excludedIds);
///
/// Puts the packet in a priority queue for processing then broadcasting when dequeued.
diff --git a/Obsidian.API/_Interfaces/IServer.cs b/Obsidian.API/_Interfaces/IServer.cs
index aff2144fd..8d9546195 100644
--- a/Obsidian.API/_Interfaces/IServer.cs
+++ b/Obsidian.API/_Interfaces/IServer.cs
@@ -25,6 +25,7 @@ public interface IServer : IDisposable
public ICommandHandler CommandHandler { get; }
public IScoreboardManager ScoreboardManager { get; }
public IEventDispatcher EventDispatcher { get; }
+ public IWorldManager WorldManager { get; }
public Task RunAsync();
diff --git a/Obsidian.API/_Interfaces/IWorld.cs b/Obsidian.API/_Interfaces/IWorld.cs
index ef5e74202..68571f746 100644
--- a/Obsidian.API/_Interfaces/IWorld.cs
+++ b/Obsidian.API/_Interfaces/IWorld.cs
@@ -1,96 +1,11 @@
-using Obsidian.API.ChunkData;
-using Obsidian.API.Entities;
-using System.Collections.Concurrent;
+using Obsidian.API.Registry.Codecs.Dimensions;
namespace Obsidian.API;
-public interface IWorld : IAsyncDisposable
+public interface IWorld : ILevel
{
- public string Name { get; }
- public string FolderPath { get; }
public string PlayerDataPath { get; }
- public string LevelDataFilePath { get; }
- public string DimensionName { get; }
- public string? ParentWorldName { get; }
-
- public bool Loaded { get; }
-
- public long Time { get; set; }
- public int DayTime { get; set; }
- public string Seed { get; }
-
- public Level LevelData { get; }
-
- public Gamemode DefaultGamemode { get; }
-
- public int RegionCount { get; }
- public int LoadedChunkCount { get; }
- public int ChunksToGenCount { get; }
-
- public IPacketBroadcaster PacketBroadcaster { get; }
- public IEventDispatcher EventDispatcher { get; }
public IWorldManager WorldManager { get; }
- public ConcurrentDictionary Players { get; }
-
- public IEntitySpawner GetNewEntitySpawner();
-
- public IEnumerable GetNonPlayerEntitiesInRange(VectorF location, float distance);
- public IEnumerable GetEntitiesInRange(VectorF location, float distance);
- public IEnumerable GetPlayersInRange(VectorF location, float distance);
- public IEnumerable GetPlayersInChunkRange(Vector worldPosition);
-
- ///
- /// Gets a Chunk from a Region.
- /// If the Chunk doesn't exist, it will be scheduled for generation unless scheduleGeneration is false.
- ///
- ///
- /// Whether to enqueue a job to generate the chunk if it doesn't exist and return null.
- /// When set to false, a partial Chunk is returned.
- /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk.
- public ValueTask GetChunkAsync(int x, int z, bool scheduleGeneration = true);
-
- ///
- /// Gets a Chunk from a Region.
- /// If the Chunk doesn't exist, it will be scheduled for generation unless scheduleGeneration is false.
- ///
- ///
- /// Whether to enqueue a job to generate the chunk if it doesn't exist and return null.
- /// When set to false, a partial Chunk is returned.
- /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk.
- public ValueTask GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true);
-
- public ValueTask DestroyEntityAsync(IEntity entity);
- public ValueTask GetBlockAsync(Vector location);
- public ValueTask GetBlockAsync(int x, int y, int z);
- public ValueTask SetBlockAsync(Vector location, IBlock block);
- public ValueTask SetBlockAsync(int x, int y, int z, IBlock block);
-
- public ValueTask SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate);
- public ValueTask SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate);
- public bool TryRemovePlayer(IPlayer player);
- public ValueTask HandleBlockUpdateAsync(IBlockUpdate update);
- public ValueTask BlockUpdateNeighborsAsync(IBlockUpdate update);
- public ValueTask ScheduleBlockUpdateAsync(IBlockUpdate blockUpdate);
-
- public ValueTask SetBlockEntity(Vector blockPosition, IBlockEntity tileEntityData);
- public ValueTask SetBlockEntity(int x, int y, int z, IBlockEntity tileEntityData);
-
- public ValueTask GetBlockEntityAsync(Vector blockPosition);
- public ValueTask GetBlockEntityAsync(int x, int y, int z);
-
- public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false);
- public ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false);
-
- public ValueTask GetWorldSurfaceHeightAsync(int x, int z);
-
- public bool TryAddEntity(IEntity entity);
- public bool TryAddPlayer(IPlayer player);
-
- public IEntity SpawnEntity(VectorF position, EntityType type);
- public IEntity SpawnFallingBlock(VectorF position, Material mat);
- public void SpawnExperienceOrbs(VectorF position, short count);
- public IEnumerable PlayersInRange(Vector location);
- public Task DoWorldTickAsync();
- public Task FlushRegionsAsync();
+ public void RegisterDimension(DimensionCodec codec, IDimension dimension);
}
diff --git a/Obsidian.API/_Interfaces/IWorldManager.cs b/Obsidian.API/_Interfaces/IWorldManager.cs
index 38e3a89a3..caf3d2c5b 100644
--- a/Obsidian.API/_Interfaces/IWorldManager.cs
+++ b/Obsidian.API/_Interfaces/IWorldManager.cs
@@ -5,8 +5,6 @@ public interface IWorldManager : IAsyncDisposable
{
public bool ReadyToJoin { get; }
- public Dictionary WorldGenerators { get; }
-
public int GeneratingChunkCount { get; }
public int LoadedChunkCount { get; }
diff --git a/Obsidian.API/_Types/Level.cs b/Obsidian.API/_Types/LevelData.cs
similarity index 97%
rename from Obsidian.API/_Types/Level.cs
rename to Obsidian.API/_Types/LevelData.cs
index 925386534..864785447 100644
--- a/Obsidian.API/_Types/Level.cs
+++ b/Obsidian.API/_Types/LevelData.cs
@@ -3,7 +3,7 @@ namespace Obsidian.API;
///
/// https://minecraft.wiki/w/Java_Edition_level_format
///
-public sealed class Level
+public sealed class LevelData
{
public const int DataVersion = 3337;
@@ -67,10 +67,7 @@ public sealed class Level
///
public int DayTime
{
- get
- {
- return (int)(this.Time % 24000); // day time is based on server time
- }
+ get => (int)(this.Time % 24000); // day time is based on server time
set
{
var startOfDay = this.Time - (this.Time % 24000);
@@ -186,4 +183,7 @@ public int DayTime
/// The amount of ticks until another wandering trader is attempted to spawn
///
public int WanderingTraderSpawnDelay { get; set; }
+
+ public WorldGenSetting WorldGenSettings { get; set; }
}
+
diff --git a/Obsidian.Nbt b/Obsidian.Nbt
new file mode 160000
index 000000000..bae7d6a06
--- /dev/null
+++ b/Obsidian.Nbt
@@ -0,0 +1 @@
+Subproject commit bae7d6a06312393a84b64fea08e7502019a15c8a
diff --git a/Obsidian.Nbt/Exceptions/TagNotFoundException.cs b/Obsidian.Nbt/Exceptions/TagNotFoundException.cs
deleted file mode 100644
index e409aa2d1..000000000
--- a/Obsidian.Nbt/Exceptions/TagNotFoundException.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Obsidian.Nbt.Exceptions;
-public sealed class TagNotFoundException : Exception
-{
- public TagNotFoundException(string? message) : base(message)
- {
- }
-
- public TagNotFoundException(string? message, Exception? innerException) : base(message, innerException) { }
-}
diff --git a/Obsidian.Nbt/GlobalUsings.cs b/Obsidian.Nbt/GlobalUsings.cs
deleted file mode 100644
index dbcde0ccc..000000000
--- a/Obsidian.Nbt/GlobalUsings.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-global using System;
-global using System.Collections.Generic;
-global using System.Linq;
-global using System.Threading.Tasks;
diff --git a/Obsidian.Nbt/INbtTag.cs b/Obsidian.Nbt/INbtTag.cs
deleted file mode 100644
index b5add9cc5..000000000
--- a/Obsidian.Nbt/INbtTag.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Obsidian.Nbt;
-public interface INbtTag
-{
- public NbtTagType Type { get; }
-
- public string? Name { get; set; }
-
- public INbtTag? Parent { get; set; }
-
- public string PrettyString(int depth = 2, int addBraceDepth = 1);
-}
-
diff --git a/Obsidian.Nbt/Interfaces/INbtWriter.cs b/Obsidian.Nbt/Interfaces/INbtWriter.cs
deleted file mode 100644
index 827ed6d2d..000000000
--- a/Obsidian.Nbt/Interfaces/INbtWriter.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace Obsidian.Nbt.Interfaces;
-public interface INbtWriter : IDisposable, IAsyncDisposable
-{
- public NbtTagType? RootType { get; }
-
- public bool Networked { get; }
-
- public void WriteCompoundStart(string name = "");
- public void WriteListStart(string name, NbtTagType listType, int length, bool writeName = true);
- public void EndList();
- public void EndCompound();
-
- public void WriteByte(byte value);
- public void WriteByte(string name, byte value);
-
- public void WriteShort(short value);
- public void WriteShort(string name, short value);
-
- public void WriteInt(int value);
- public void WriteInt(string name, int value);
-
- public void WriteLong(long value);
- public void WriteLong(string name, long value);
-
- public void WriteDouble(double value);
- public void WriteDouble(string name, double value);
-
- public void WriteFloat(float value);
- public void WriteFloat(string name, float value);
-
- public void WriteBool(bool value);
- public void WriteBool(string name, bool value);
-
- public void WriteString(string value);
- public void WriteString(string name, string value);
-
- public void Write(NbtTagType tagType);
- public void WriteTag(INbtTag tag);
-
- public void WriteListTag(INbtTag tag);
-
- public void WriteArray(string? name, ReadOnlySpan values);
- public void WriteArray(string? name, ReadOnlySpan values);
- public void WriteArray(string? name, ReadOnlySpan values);
-
- public void TryFinish();
-
- public Task TryFinishAsync();
-}
diff --git a/Obsidian.Nbt/InternalsVisibleTo.cs b/Obsidian.Nbt/InternalsVisibleTo.cs
deleted file mode 100644
index b2efcfd4e..000000000
--- a/Obsidian.Nbt/InternalsVisibleTo.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-using System.Runtime.CompilerServices;
-
-[assembly: InternalsVisibleTo("Obsidian")]
-[assembly: InternalsVisibleTo("Obsidian.Tests")]
diff --git a/Obsidian.Nbt/ModifiedUtf8.cs b/Obsidian.Nbt/ModifiedUtf8.cs
deleted file mode 100644
index e83d3ac8a..000000000
--- a/Obsidian.Nbt/ModifiedUtf8.cs
+++ /dev/null
@@ -1,459 +0,0 @@
-using Obsidian.Nbt.Utilities;
-using System.Buffers;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.Intrinsics;
-using System.Runtime.Intrinsics.X86;
-
-namespace Obsidian.Nbt;
-
-// Encoding specification: https://web.archive.org/web/20211117120323/https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html
-// +-----------------------------+-------------------------------------------------------------------+
-// | Character range | Bit values |
-// +-----------------------------+-------------------------------------------------------------------+
-// | \u0001 to \u007F | 0 | bits 6-0 |......................................|
-// | \u0080 to \u07FF and \u0000 | 1 | 1 | 0 | bits 10-6 | 1 | 0 | bits 5-0 |..................|
-// | \u0800 to \uFFFF | 1 | 1 | 1 | 0 | bits 15-12 | 1 | 0 | bits 11-6 | 1 | 0 | bits 5-0 |
-// +-----------------------------+-------------------------------------------------------------------+
-
-///
-/// Provides methods for working with a modification of UTF-8 encoding used by the NBT format.
-///
-public static class ModifiedUtf8
-{
- ///
- /// Encodes a span of characters into an array of bytes.
- ///
- /// The span of characters to encode.
- /// An array of bytes containing the results of encoding the specified sequence of characters.
- /// true if encoding the characters was successful; otherwise false
- public static bool TryGetBytes(ReadOnlySpan chars, [NotNullWhen(true)] out byte[]? bytes)
- {
- if (!TryGetByteCount(chars, out int byteCount))
- {
- bytes = null;
- return false;
- }
-
- if (byteCount == 0)
- {
- bytes = [];
- return true;
- }
-
- bytes = GC.AllocateUninitializedArray(byteCount);
- GetBytesCommon(chars, bytes);
- return true;
- }
-
- ///
- /// Encodes a span of characters into a sequence of bytes obtained from a buffer writer.
- ///
- /// The span of characters to encode.
- /// Bytes output sink.
- /// true if encoding the characters was successful; otherwise false
- public static bool TryGetBytes(ReadOnlySpan chars, IBufferWriter bufferWriter)
- {
- if (!TryGetByteCount(chars, out int byteCount))
- return false;
-
- Span sink = bufferWriter.GetSpan(byteCount);
- if (sink.Length < byteCount)
- ThrowHelper.ThrowException_InsufficientBufferSize();
-
- GetBytesCommon(chars, sink);
- bufferWriter.Advance(byteCount);
- return true;
- }
-
- internal static void GetBytesCommon(ReadOnlySpan chars, Span bytes)
- {
- if (chars.Length == bytes.Length)
- {
- if (BitConverter.IsLittleEndian && Sse2.IsSupported && bytes.Length >= 32)
- {
- GetBytesAsciiSse2(chars, bytes);
- }
- else
- {
- GetBytesAsciiScalar(chars, bytes);
- }
- }
- else
- {
- GetBytesScalar(chars, bytes);
- }
- }
-
- private static void GetBytesScalar(ReadOnlySpan chars, Span bytes)
- {
- ref byte destination = ref MemoryMarshal.GetReference(bytes);
- for (int i = 0; i < chars.Length; i++)
- {
- char c = chars[i];
- if (c < 0x80 && c != 0)
- {
- destination = (byte)c;
- }
- else if (c >= 0x800)
- {
- destination = (byte)(0xE0 | ((c >> 12) & 0x0F));
- destination = ref Unsafe.Add(ref destination, 1);
- destination = (byte)(0x80 | ((c >> 6) & 0x3F));
- destination = ref Unsafe.Add(ref destination, 1);
- destination = (byte)(0x80 | ((c >> 0) & 0x3F));
- }
- else
- {
- destination = (byte)(0xC0 | ((c >> 6) & 0x1F));
- destination = ref Unsafe.Add(ref destination, 1);
- destination = (byte)(0x80 | ((c >> 0) & 0x3F));
- }
- destination = ref Unsafe.Add(ref destination, 1);
- }
- }
-
- private static void GetBytesAsciiSse2(ReadOnlySpan chars, Span bytes)
- {
- Debug.Assert(BitConverter.IsLittleEndian);
- Debug.Assert(Sse2.IsSupported);
-
- ref byte destination = ref MemoryMarshal.GetReference(bytes);
- ref byte source = ref Unsafe.As(ref MemoryMarshal.GetReference(chars));
- ref byte sourceEnd = ref Unsafe.Add(ref source, chars.Length * sizeof(char) - 31);
-
- while (Unsafe.IsAddressLessThan(ref source, ref sourceEnd))
- {
- Vector128 first = Unsafe.As>(ref source);
- source = ref Unsafe.Add(ref source, 16);
- Vector128 second = Unsafe.As>(ref source);
- source = ref Unsafe.Add(ref source, 16);
-
- Vector128 packed = Sse2.PackUnsignedSaturate(first, second);
-
- Unsafe.WriteUnaligned(ref destination, packed);
- destination = ref Unsafe.Add(ref destination, 16);
- }
-
- sourceEnd = ref Unsafe.Add(ref sourceEnd, 31);
- while (Unsafe.IsAddressLessThan(ref source, ref sourceEnd))
- {
- destination = source;
-
- destination = ref Unsafe.Add(ref destination, 1);
- source = ref Unsafe.Add(ref source, 2);
- }
- }
-
- private static void GetBytesAsciiScalar(ReadOnlySpan chars, Span bytes)
- {
- ref byte destination = ref MemoryMarshal.GetReference(bytes);
- for (int i = 0; i < chars.Length; i++)
- {
- destination = (byte)chars[i];
- destination = ref Unsafe.Add(ref destination, 1);
- }
- }
-
- ///
- /// Decodes a span of bytes into a string.
- ///
- /// The span of bytes to decode.
- /// A containing the results of decoding the specified sequence of bytes.
- /// Sequence contained incorrectly formatted bytes.
- public static string GetString(ReadOnlySpan bytes)
- {
- if (TryGetString(bytes, out string? @string))
- {
- return @string;
- }
-
- throw new FormatException("Input data contained invalid bytes.");
- }
-
- ///
- /// Decodes a span of bytes into a string and returns a value indicating whether the conversion was successfull.
- ///
- /// The span of bytes to decode.
- /// A containing the results of decoding the specified sequence of bytes.
- /// true if decoding the bytes was successful; otherwise false
- public static bool TryGetString(ReadOnlySpan bytes, [NotNullWhen(true)] out string? @string)
- {
- if (bytes.IsEmpty)
- {
- @string = string.Empty;
- return true;
- }
-
- if (TryGetCharCount(bytes, out int length))
- {
- @string = new string('\0', length);
- ref char stringRef = ref Unsafe.AsRef(in @string.GetPinnableReference());
- GetStringCommon(bytes, length, ref stringRef);
- return true;
- }
-
- @string = null;
- return false;
- }
-
- private static bool TryGetCharCount(ReadOnlySpan bytes, out int charCount)
- {
- Debug.Assert(!bytes.IsEmpty);
-
- charCount = 0;
-
- if (bytes.Length > ushort.MaxValue)
- {
- return false;
- }
-
- // Make sure that the last byte(s) is not partial
- int last = bytes[^1] >> 6;
- if (last > 2) // First byte of a byte group
- {
- return false;
- }
- else if (last == 2) // Part of a byte group
- {
- if (bytes.Length < 2)
- return false;
-
- last = bytes[^2] >> 5;
- if (last == 0b100 || last == 0b101) // Three byte group
- {
- if (bytes.Length < 3)
- return false;
-
- last = bytes[^3] >> 4;
- if (last != 0b1110)
- return false;
- }
- else if (last != 0b110) // Two byte group
- {
- return false;
- }
- }
-
- ref byte @ref = ref MemoryMarshal.GetReference(bytes);
- ref byte end = ref Unsafe.Add(ref @ref, bytes.Length);
- while (Unsafe.IsAddressLessThan(ref @ref, ref end))
- {
- int header = @ref >> 4;
- if (header < 0b1000) // One byte
- {
- }
- else if (header < 0b1110) // Two bytes
- {
- @ref = ref Unsafe.Add(ref @ref, 1);
- if ((@ref >> 6) != 0b10)
- return false;
- }
- else if (header == 0b1110) // Three bytes
- {
- @ref = ref Unsafe.Add(ref @ref, 1);
- if ((@ref >> 6) != 0b10)
- return false;
- @ref = ref Unsafe.Add(ref @ref, 1);
- if ((@ref >> 6) != 0b10)
- return false;
- }
- else // Invalid header
- {
- return false;
- }
- charCount++;
- @ref = ref Unsafe.Add(ref @ref, 1);
- }
-
- return true;
- }
-
- private static void GetStringCommon(ReadOnlySpan bytes, int stringLength, ref char destination)
- {
- if (bytes.Length == stringLength)
- {
- GetStringAsciiScalar(bytes, ref destination);
- }
- else
- {
- GetStringScalar(bytes, ref destination);
- }
- }
-
- private static void GetStringAsciiAvx2(ReadOnlySpan bytes, ref char destination)
- {
- Debug.Assert(BitConverter.IsLittleEndian);
- Debug.Assert(Avx2.IsSupported);
-
- ref byte target = ref Unsafe.As(ref destination);
- ref byte source = ref MemoryMarshal.GetReference(bytes);
- ref byte sourceEnd = ref Unsafe.Add(ref source, bytes.Length - 31);
- while (Unsafe.IsAddressLessThan(ref source, ref sourceEnd))
- {
- Vector256 vector = Unsafe.As>(ref source);
-
- Vector256 low = Avx2.UnpackLow(vector, Vector256.Zero);
- Unsafe.WriteUnaligned(ref target, low);
- target = ref Unsafe.Add(ref target, 32);
-
- Vector256 high = Avx2.UnpackHigh(vector, Vector256.Zero);
- Unsafe.WriteUnaligned(ref target, high);
- target = ref Unsafe.Add(ref target, 32);
-
- source = ref Unsafe.Add(ref source, 32);
- }
-
- sourceEnd = ref Unsafe.Add(ref sourceEnd, 31);
- destination = ref Unsafe.As(ref target);
- while (Unsafe.IsAddressLessThan(ref source, ref sourceEnd))
- {
- destination = (char)source;
-
- source = ref Unsafe.Add(ref source, 1);
- destination = ref Unsafe.Add(ref destination, 1);
- }
- }
-
- private static void GetStringAsciiScalar(ReadOnlySpan bytes, ref char destination)
- {
- for (int i = 0; i < bytes.Length; i++)
- {
- destination = (char)bytes[i];
- destination = ref Unsafe.Add(ref destination, 1);
- }
- }
-
- private static void GetStringScalar(ReadOnlySpan bytes, ref char destination)
- {
- ref byte source = ref MemoryMarshal.GetReference(bytes);
- ref byte sourceEnd = ref Unsafe.Add(ref source, bytes.Length);
- while (Unsafe.IsAddressLessThan(ref source, ref sourceEnd))
- {
- int c = source;
- int header = c >> 4;
- if (header < 0b1000) // One byte
- {
- destination = (char)c;
- }
- else if (header < 0b1110) // Two bytes
- {
- c = (c & 0b0001_1111) << 6;
-
- source = ref Unsafe.Add(ref source, 1);
- c |= source & 0b0011_1111;
-
- destination = (char)c;
- }
- else // Three bytes
- {
- c = (c & 0b0000_1111) << 12;
-
- source = ref Unsafe.Add(ref source, 1);
- c |= (source & 0b0011_1111) << 6;
-
- source = ref Unsafe.Add(ref source, 1);
- c |= source & 0b0011_1111;
-
- destination = (char)c;
- }
- source = ref Unsafe.Add(ref source, 1);
- destination = ref Unsafe.Add(ref destination, 1);
- }
- }
-
- ///
- /// Calculates the number of bytes produced by encoding the specified character span, unless the number of bytes is more than the encoding supports.
- ///
- /// The span that contains the set of characters to encode.
- /// The number of bytes produced by encoding the specified character span.
- /// true if calculating the number of bytes was successful; otherwise false
- public static bool TryGetByteCount(ReadOnlySpan chars, out int byteCount)
- {
- if (chars.Length > ushort.MaxValue) // Even for all-ASCII inputs, this produces > 65535 bytes
- {
- byteCount = default;
- return false;
- }
-
- if (chars.IsEmpty)
- {
- byteCount = 0;
- return true;
- }
-
- if (Avx2.IsSupported)
- {
- byteCount = GetByteCountAvx2(chars);
- return byteCount <= ushort.MaxValue;
- }
- else
- {
- byteCount = GetByteCountScalar(chars);
- return byteCount <= ushort.MaxValue;
- }
- }
-
- private static int GetByteCountAvx2(ReadOnlySpan chars)
- {
- Debug.Assert(Avx2.IsSupported);
- Debug.Assert(chars.Length <= ushort.MaxValue); // Ensure that the counter can't overflow
-
- const short TwoBytesBorder = (0x0080 >> 1) - 1;
- const short ThreeBytesBorder = (0x0800 >> 1) - 1;
-
- int byteCount = chars.Length; // Count all characters that will produce at least one byte
-
- ref byte ptr = ref Unsafe.As(ref MemoryMarshal.GetReference(chars));
- ref byte ptrEnd = ref Unsafe.Add(ref ptr, chars.Length * sizeof(char) - 31);
-
- Vector256 counter = Vector256.Zero;
- for (; Unsafe.IsAddressLessThan(ref ptr, ref ptrEnd); ptr = ref Unsafe.Add(ref ptr, 32))
- {
- // Transform all 0x0000s to 0x0080s
- Vector256 vustr = Unsafe.ReadUnaligned>(ref ptr);
- Vector256 mask = Avx2.CompareEqual(vustr, Vector256.Zero);
- Vector256 blend = Avx2.BlendVariable(vustr, Vector256.Create((ushort)0x0080), mask);
-
- Vector256 vstr = Avx2.ShiftRightLogical(blend, 1).AsInt16();
- counter = Avx2.Subtract(counter, Avx2.CompareGreaterThan(vstr, Vector256.Create(TwoBytesBorder))); // Count all characters that will produce at least two bytes
- counter = Avx2.Subtract(counter, Avx2.CompareGreaterThan(vstr, Vector256.Create(ThreeBytesBorder))); // Count all characters that will produce three bytes
- }
-
- counter = Avx2.HorizontalAdd(counter, counter); // 128
- counter = Avx2.HorizontalAdd(counter, counter); // 64
-
- // Here we must add by hand to avoid overflowing int16
- // Indexes of summing results are based on https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm256_hadd_epi16&ig_expand=3839
- ref short counterRef = ref Unsafe.As, short>(ref counter);
- byteCount += counterRef;
- byteCount += Unsafe.Add(ref counterRef, 1);
- byteCount += Unsafe.Add(ref counterRef, 8);
- byteCount += Unsafe.Add(ref counterRef, 9);
-
- // Count the rest as scalars
- ptrEnd = ref Unsafe.Add(ref ptrEnd, 31);
- while (Unsafe.IsAddressLessThan(ref ptr, ref ptrEnd))
- {
- ushort c = Unsafe.ReadUnaligned(ref ptr);
- if (c >= 0x80 || c == 0)
- byteCount += (c >= 0x800) ? 2 : 1;
- ptr = ref Unsafe.Add(ref ptr, sizeof(ushort));
- }
- return byteCount;
- }
-
- private static int GetByteCountScalar(ReadOnlySpan chars)
- {
- int byteCount = chars.Length;
- for (int i = 0; i < chars.Length; i++)
- {
- char c = chars[i];
- if (c >= 0x80 || c == 0)
- byteCount += (c >= 0x800) ? 2 : 1;
- }
- return byteCount;
- }
-}
diff --git a/Obsidian.Nbt/NbtArray.cs b/Obsidian.Nbt/NbtArray.cs
deleted file mode 100644
index 64769beb4..000000000
--- a/Obsidian.Nbt/NbtArray.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System.Collections;
-
-namespace Obsidian.Nbt;
-
-public sealed class NbtArray : INbtTag, IEnumerable, ICollection where T : struct
-{
- private readonly T[] array;
- public int Count => this.array.Length;
-
- public bool IsReadOnly => false;
-
- public NbtTagType Type { get; private set; }
-
- public string? Name { get; set; }
-
- public INbtTag? Parent { get; set; }
-
- public bool IsSynchronized => false;
-
- public object SyncRoot => this.array;
-
- public T this[int index] { get => this.array[index]; set => this.array[index] = value; }
-
- public NbtArray(string? name, T[] array)
- {
- (this.Name, this.array) = (name, array);
-
- this.Type = this.DetermingType();
- }
-
- public void CopyTo(Array array, int index) => this.array.CopyTo(array, index);
-
- public IEnumerator GetEnumerator() => this.array.GetEnumerator();
-
- public bool Contains(T item) => Array.IndexOf(this.array, item) >= 0;
-
- public T[] GetArray() => this.array;
-
- public override string ToString() => $"TAG_{this.Type}('{this.Name}'): {this.Count} Values";
-
- public string PrettyString(int depth = 2, int addBraceDepth = 1)
- {
- var name = $"TAG_{this.Type}('{this.Name}'): {this.Count} Values";
- return name.PadLeft(name.Length + depth);
- }
-
- private NbtTagType DetermingType() => typeof(T) switch
- {
- Type t when t == typeof(int) => NbtTagType.IntArray,
- Type t when t == typeof(long) => NbtTagType.LongArray,
- Type t when t == typeof(byte) => NbtTagType.ByteArray,
- _ => throw new NotSupportedException($"Type {nameof(T)} is not supported.")
- };
-}
diff --git a/Obsidian.Nbt/NbtCompound.cs b/Obsidian.Nbt/NbtCompound.cs
deleted file mode 100644
index 347d6b3b8..000000000
--- a/Obsidian.Nbt/NbtCompound.cs
+++ /dev/null
@@ -1,153 +0,0 @@
-using Obsidian.Nbt.Exceptions;
-using System.Collections;
-using System.Diagnostics.CodeAnalysis;
-using System.Text;
-
-namespace Obsidian.Nbt;
-
-public sealed class NbtCompound : INbtTag, IEnumerable>
-{
- private readonly Dictionary children = [];
-
- public int Count => this.children.Count;
-
- public NbtTagType Type => NbtTagType.Compound;
-
- public string? Name { get; set; }
-
- public INbtTag? Parent { get; set; }
-
- public INbtTag this[string name] { get => this.children[name]; set => this.Add(name, value); }
-
- public NbtCompound(string name = "")
- {
- if (this.Parent?.Type == NbtTagType.Compound && string.IsNullOrEmpty(name))
- throw new ArgumentNullException(nameof(name), "Tags within a compound must be named.");
-
- this.Name = name;
- }
-
- public NbtCompound(List children) : this()
- {
- foreach (var child in children)
- this.Add(child.Name, child);
- }
-
- public NbtCompound(string name, List children) : this(name)
- {
- foreach (var child in children)
- this.Add(child.Name, child);
- }
-
- public bool Remove(string name) => this.children.Remove(name);
-
- public bool HasTag(string name) => this.children.ContainsKey(name);
-
- public bool TryGetTag(string name, [MaybeNullWhen(false)] out INbtTag tag) => this.children.TryGetValue(name, out tag);
- public bool TryGetTag(string name, [MaybeNullWhen(false)] out T tag) where T : INbtTag
- {
- if (this.TryGetTag(name, out var childTag) && childTag is T matchedTag)
- {
- tag = matchedTag;
- return true;
- }
-
- tag = default;
- return false;
- }
-
- public bool TryGetTagValue(string name, [MaybeNullWhen(false)] out TValue value)
- {
- if (this.TryGetTag>(name, out var tag))
- {
- value = tag.Value;
- return true;
- }
-
- value = default;
- return false;
- }
-
- public byte GetByte(string name) => this.GetTagValue(name);
-
- public short GetShort(string name) => this.GetTagValue(name);
-
- public int GetInt(string name) => this.GetTagValue(name);
-
- public long GetLong(string name) => this.GetTagValue(name);
-
- public float GetFloat(string name) => this.GetTagValue(name);
-
- public double GetDouble(string name) => this.GetTagValue(name);
-
- public string? GetString(string name) => this.GetTagValue(name);
-
- public bool GetBool(string name) =>
- this.TryGetTag>(name, out var tag) && tag.Value == 1;
-
- public void Clear() => this.children.Clear();
-
- public override string ToString()
- {
- var sb = new StringBuilder();
- var count = this.Count;
-
- sb.AppendLine($"TAG_Compound('{this.Name}'): {count} {(count > 1 ? "entries" : "entry")}")
- .AppendLine("{");
-
- foreach (var (_, tag) in this)
- sb.AppendLine(tag.PrettyString());
-
- sb.AppendLine("}");
-
- return sb.ToString();
- }
-
- public string PrettyString(int depth = 2, int addBraceDepth = 1)
- {
- var sb = new StringBuilder();
- var count = this.Count;
-
- var name = $"TAG_Compound('{this.Name}'): {count} {(count > 1 ? "entries" : "entry")}";
-
- sb.AppendLine(name.PadLeft(name.Length + depth))
- .AppendLine("{".PadLeft(depth + addBraceDepth));
-
- foreach (var (_, tag) in this)
- {
- var tagString = tag.PrettyString(depth + 1, addBraceDepth + 2);
- sb.AppendLine(tagString.PadLeft(tagString.Length + depth));
- }
-
- sb.AppendLine("}".PadLeft(depth + addBraceDepth));
-
- return sb.ToString();
- }
-
- public void Add(string name, INbtTag tag)
- {
- if (string.IsNullOrEmpty(name))
- throw new ArgumentNullException(nameof(name), "Tags inside a compound must be named.");
-
- tag.Parent = this;
-
- this.children.Add(name, tag);
- }
-
- public void Add(INbtTag tag)
- {
- if (string.IsNullOrEmpty(tag.Name))
- throw new InvalidOperationException("Tags inside a compound must be named.");
-
- tag.Parent = this;
-
- this.children.Add(tag.Name, tag);
- }
-
- public IEnumerator> GetEnumerator() =>
- this.children.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
-
- private T? GetTagValue(string name) => this.TryGetTag(name, out var tag) && tag is NbtTag actualTag ? actualTag.Value : throw new TagNotFoundException(name);
-}
diff --git a/Obsidian.Nbt/NbtCompression.cs b/Obsidian.Nbt/NbtCompression.cs
deleted file mode 100644
index e10d5d034..000000000
--- a/Obsidian.Nbt/NbtCompression.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Obsidian.Nbt;
-
-public enum NbtCompression : byte
-{
- None,
-
- GZip,
-
- ZLib,
-
- Zstd,
-
- Brotli
-}
diff --git a/Obsidian.Nbt/NbtList.cs b/Obsidian.Nbt/NbtList.cs
deleted file mode 100644
index c556c7c15..000000000
--- a/Obsidian.Nbt/NbtList.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-using System.Collections;
-using System.Text;
-
-namespace Obsidian.Nbt;
-
-public sealed class NbtList : INbtTag, IList
-{
- private readonly List baseList = [];
-
- public int Count => this.baseList.Count;
-
- public bool IsReadOnly => false;
-
- public NbtTagType ListType { get; }
-
- public NbtTagType Type => NbtTagType.List;
-
- public string? Name { get; set; }
- public INbtTag? Parent { get; set; }
-
- public NbtList(NbtTagType listType, string name = "")
- {
- this.Name = name;
- this.ListType = listType;
- }
-
- public INbtTag this[int index] { get => this.baseList[index]; set => this.baseList[index] = value; }
-
- public void Add(INbtTag item)
- {
- item.Parent = this;
-
- this.baseList.Add(item);
- }
-
- public void Clear() => this.baseList.Clear();
-
- public bool Contains(INbtTag item) => this.baseList.Contains(item);
-
- public void CopyTo(INbtTag[] array, int arrayIndex) => this.baseList.CopyTo(array, arrayIndex);
-
- public int IndexOf(INbtTag item) => this.baseList.IndexOf(item);
-
- public void Insert(int index, INbtTag item) => this.baseList.Insert(index, item);
-
- public bool Remove(INbtTag item) => this.baseList.Remove(item);
-
- public void RemoveAt(int index) => this.baseList.RemoveAt(index);
-
- public IEnumerator GetEnumerator() => this.baseList.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
-
- public override string ToString()
- {
- var sb = new StringBuilder();
- var count = this.Count;
-
- sb.AppendLine($"TAG_List('{this.Name}'): {count} {(count > 1 ? "entries" : "entry")}").AppendLine("{");
-
- foreach (var tag in this)
- sb.AppendLine($"{tag.PrettyString()}");
-
- sb.AppendLine("}");
-
- return sb.ToString();
- }
-
- public string PrettyString(int depth = 2, int addBraceDepth = 1)
- {
- var sb = new StringBuilder();
- var count = this.Count;
-
- var name = $"TAG_List('{this.Name}'): {count} {(count > 1 ? "entries" : "entry")}";
-
- sb.AppendLine(name.PadLeft(name.Length + depth))
- .AppendLine("{".PadLeft(depth + addBraceDepth));
-
- foreach (var tag in this)
- {
- var tagString = tag.PrettyString(depth + 1, addBraceDepth + 2);
- sb.AppendLine(tagString.PadLeft(tagString.Length + depth));
- }
-
- sb.AppendLine("}".PadLeft(depth + addBraceDepth));
-
- return sb.ToString();
- }
-}
diff --git a/Obsidian.Nbt/NbtReader.Primitives.cs b/Obsidian.Nbt/NbtReader.Primitives.cs
deleted file mode 100644
index a45a24ef8..000000000
--- a/Obsidian.Nbt/NbtReader.Primitives.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Buffers.Binary;
-
-namespace Obsidian.Nbt;
-public partial struct NbtReader
-{
- public byte ReadByte() => (byte)this.BaseStream.ReadByte();
-
- public string ReadString()
- {
- var length = this.ReadInt16();
-
- if (length <= 0)
- return string.Empty;
-
- Span buffer = stackalloc byte[length];
-
- this.BaseStream.ReadExactly(buffer);
-
- return ModifiedUtf8.GetString(buffer);
- }
-
- public short ReadInt16()
- {
- Span buffer = stackalloc byte[2];
- this.BaseStream.ReadExactly(buffer);
-
- return BinaryPrimitives.ReadInt16BigEndian(buffer);
- }
-
- public int ReadInt32()
- {
- Span buffer = stackalloc byte[4];
- this.BaseStream.ReadExactly(buffer);
-
- return BinaryPrimitives.ReadInt32BigEndian(buffer);
- }
-
- public long ReadInt64()
- {
- Span buffer = stackalloc byte[8];
- this.BaseStream.ReadExactly(buffer);
-
- return BinaryPrimitives.ReadInt64BigEndian(buffer);
- }
-
- public float ReadSingle()
- {
- Span buffer = stackalloc byte[4];
- this.BaseStream.ReadExactly(buffer);
-
- return BinaryPrimitives.ReadSingleBigEndian(buffer);
- }
-
- public double ReadDouble()
- {
- Span buffer = stackalloc byte[8];
- this.BaseStream.ReadExactly(buffer);
-
- return BinaryPrimitives.ReadDoubleBigEndian(buffer);
- }
-}
diff --git a/Obsidian.Nbt/NbtReader.cs b/Obsidian.Nbt/NbtReader.cs
deleted file mode 100644
index 55725f459..000000000
--- a/Obsidian.Nbt/NbtReader.cs
+++ /dev/null
@@ -1,195 +0,0 @@
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.IO.Compression;
-
-namespace Obsidian.Nbt;
-
-public readonly partial struct NbtReader(Stream input, NbtCompression compressionMode = NbtCompression.None)
-{
- public Stream BaseStream { get; } = compressionMode switch
- {
- NbtCompression.GZip => new GZipStream(input, CompressionMode.Decompress),
- NbtCompression.ZLib => new ZLibStream(input, CompressionMode.Decompress),
- _ => input
- };
-
-
- public INbtTag? ReadNextTag(bool readName = true)
- {
- var firstType = this.ReadTagType();
-
- string tagName = readName ? this.ReadString() : string.Empty;
-
- return firstType switch
- {
- NbtTagType.End => null,
- NbtTagType.List => ReadListTag(tagName),
- NbtTagType.Compound => ReadCompoundTag(tagName),
- NbtTagType.ByteArray => ReadArray(tagName, ReadByte),
- NbtTagType.IntArray => ReadArray(tagName, ReadInt32),
- NbtTagType.LongArray => ReadArray(tagName, ReadInt64),
- _ => GetCurrentTag(firstType, tagName, !readName)
- };
- }
-
- public bool TryReadNextTag(bool readName, [MaybeNullWhen(false)] out INbtTag tag)
- {
- var nextTag = this.ReadNextTag(readName);
-
- if (nextTag != null)
- {
- tag = nextTag;
- return true;
- }
-
- tag = default;
- return false;
- }
-
- public bool TryReadNextTag(bool readName, [MaybeNullWhen(false)] out T tag) where T : INbtTag
- {
- if (this.TryReadNextTag(readName, out INbtTag newTag) && newTag is T matchedTag)
- {
- tag = matchedTag;
- return true;
- }
-
- tag = default;
- return false;
- }
-
- public bool TryReadNextTag([MaybeNullWhen(false)] out INbtTag tag)
- {
- var nextTag = this.ReadNextTag();
-
- if (nextTag != null)
- {
- tag = nextTag;
- return true;
- }
-
- tag = default;
- return false;
- }
-
- public bool TryReadNextTag([MaybeNullWhen(false)] out T tag) where T : INbtTag
- {
- if (this.TryReadNextTag(out INbtTag? newTag) && newTag is T matchedTag)
- {
- tag = matchedTag;
- return true;
- }
-
- tag = default;
- return false;
- }
-
- private INbtTag? GetCurrentTag(NbtTagType type, bool readName = true)
- {
- string name = readName ? this.ReadString() : string.Empty;
-
- INbtTag? tag = type switch
- {
- NbtTagType.Byte => new NbtTag(name, this.ReadByte()),
- NbtTagType.Short => new NbtTag(name, this.ReadInt16()),
- NbtTagType.Int => new NbtTag(name, this.ReadInt32()),
- NbtTagType.Long => new NbtTag(name, this.ReadInt64()),
- NbtTagType.Float => new NbtTag(name, this.ReadSingle()),
- NbtTagType.Double => new NbtTag(name, this.ReadDouble()),
- NbtTagType.String => new NbtTag(name, this.ReadString()),
- NbtTagType.Compound => this.ReadCompoundTag(name),
- NbtTagType.List => this.ReadListTag(name),
- NbtTagType.ByteArray => this.ReadArray(name, ReadByte),
- NbtTagType.IntArray => this.ReadArray(name, ReadInt32),
- NbtTagType.LongArray => this.ReadArray(name, ReadInt64),
- _ => null
- };
-
- return tag;
- }
-
- private INbtTag? GetCurrentTag(NbtTagType type, string name, bool readName = true)
- {
- name = readName ? this.ReadString() : name;
-
- INbtTag? tag = type switch
- {
- NbtTagType.Byte => new NbtTag(name, this.ReadByte()),
- NbtTagType.Short => new NbtTag(name, this.ReadInt16()),
- NbtTagType.Int => new NbtTag(name, this.ReadInt32()),
- NbtTagType.Long => new NbtTag(name, this.ReadInt64()),
- NbtTagType.Float => new NbtTag(name, this.ReadSingle()),
- NbtTagType.Double => new NbtTag(name, this.ReadDouble()),
- NbtTagType.String => new NbtTag(name, this.ReadString()),
- NbtTagType.Compound => this.ReadCompoundTag(name),
- NbtTagType.List => this.ReadListTag(name),
- NbtTagType.ByteArray => this.ReadArray(name, ReadByte),
- NbtTagType.IntArray => this.ReadArray(name, ReadInt32),
- NbtTagType.LongArray => this.ReadArray(name, ReadInt64),
- _ => null
- };
-
- return tag;
- }
-
- private INbtTag ReadArray(string name, Func readElement) where T : struct
- {
- int length = ReadInt32();
- if (length < 0)
- throw new UnreachableException("Array length should never be below 0.");
-
- var array = new T[length];
- for (int i = 0; i < length; i++)
- {
- array[i] = readElement();
- }
-
- return new NbtArray(name, array);
- }
-
- private NbtList ReadListTag(string name)
- {
- var listType = this.ReadTagType();
-
- var length = this.ReadInt32();
-
- if (length <= 0)
- return new NbtList(listType, name);
-
- var list = new NbtList(listType, name);
- for (var i = 0; i < length; i++)
- list.Add(this.GetCurrentTag(listType, false));
-
- return list;
- }
-
- private NbtCompound ReadCompoundTag(string name)
- {
- var compound = new NbtCompound(name);
-
- NbtTagType type;
- while ((type = this.ReadTagType()) != NbtTagType.End)
- {
- var tag = this.GetCurrentTag(type);
-
- compound.Add(tag);
- }
-
- return compound;
- }
-
- private NbtTagType ReadTagType()
- {
- var type = this.BaseStream.ReadByte();
-
- return type switch
- {
- <= 0 => NbtTagType.End,
- > (byte)NbtTagType.LongArray => throw new ArgumentOutOfRangeException(
- $"Tag is out of range: {(NbtTagType)type}"),
- _ => (NbtTagType)type
- };
- }
-
-}
diff --git a/Obsidian.Nbt/NbtTag.cs b/Obsidian.Nbt/NbtTag.cs
deleted file mode 100644
index 5e5d77bd2..000000000
--- a/Obsidian.Nbt/NbtTag.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-namespace Obsidian.Nbt;
-
-public sealed class NbtTag : INbtTag
-{
- public NbtTagType Type { get; }
-
- public string? Name { get; set; }
-
- ///
- /// This is either null, a compound or list
- ///
- public INbtTag? Parent { get; set; }
-
- public T? Value { get; }
-
- public NbtTag(string name, T? value, INbtTag parent = null)
- {
- this.Name = name;
- this.Parent = parent;
- this.Value = value;
-
- //Some structure files from minecraft have properties with names but null values???
- if (value is null)
- return;
-
- this.Type = value switch
- {
- bool => NbtTagType.Byte,
- byte => NbtTagType.Byte,
- short => NbtTagType.Short,
- int => NbtTagType.Int,
- long => NbtTagType.Long,
- float => NbtTagType.Float,
- double => NbtTagType.Double,
- string => NbtTagType.String,
- _ => throw new InvalidOperationException()
- };
- }
-
- public override string ToString()
- {
- switch (this.Type)
- {
- case NbtTagType.Byte:
- case NbtTagType.Short:
- case NbtTagType.Int:
- case NbtTagType.Long:
- case NbtTagType.Float:
- case NbtTagType.Double:
- case NbtTagType.String:
- return $"TAG_{this.Type}('{this.Name}'): {this.Value}";
- default:
- return string.Empty;
- }
- }
-
- public string PrettyString(int depth = 2, int addBraceDepth = 1)
- {
- switch (this.Type)
- {
- case NbtTagType.Byte:
- case NbtTagType.Short:
- case NbtTagType.Int:
- case NbtTagType.Long:
- case NbtTagType.Float:
- case NbtTagType.Double:
- case NbtTagType.String:
- {
- var name = $"TAG_{this.Type}('{this.Name}'): {this.Value}";
- return name.PadLeft(name.Length + depth);
- }
- default:
- return string.Empty;
- }
- }
-}
diff --git a/Obsidian.Nbt/NbtTagType.cs b/Obsidian.Nbt/NbtTagType.cs
deleted file mode 100644
index 07c31da3a..000000000
--- a/Obsidian.Nbt/NbtTagType.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-namespace Obsidian.Nbt;
-
-public enum NbtTagType : byte
-{
- End,
- Byte,
- Short,
- Int,
- Long,
- Float,
- Double,
- ByteArray,
- String,
- List,
- Compound,
- IntArray,
- LongArray,
-
- Unknown = 255
-}
diff --git a/Obsidian.Nbt/NbtWriterStream.Primitives.cs b/Obsidian.Nbt/NbtWriterStream.Primitives.cs
deleted file mode 100644
index 2d2708c5c..000000000
--- a/Obsidian.Nbt/NbtWriterStream.Primitives.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using Obsidian.Nbt.Utilities;
-using System.Buffers.Binary;
-using System.Text;
-
-namespace Obsidian.Nbt;
-
-public partial struct NbtWriterStream
-{
- public void WriteString(string value)
- {
- this.Validate(null, NbtTagType.String);
- this.WriteStringInternal(value);
- }
-
- public void WriteString(string name, string value)
- {
- this.Validate(name, NbtTagType.String);
-
- this.Write(NbtTagType.String);
- this.WriteStringInternal(name);
- this.WriteStringInternal(value);
- }
-
- public void WriteByte(byte value)
- {
- this.Validate(null, NbtTagType.Byte);
- this.WriteByteInternal(value);
- }
-
- public void WriteByte(string name, byte value)
- {
- this.Validate(name, NbtTagType.Byte);
-
- this.Write(NbtTagType.Byte);
- this.WriteStringInternal(name);
- this.WriteByteInternal(value);
- }
-
- public void WriteBool(bool value)
- {
- this.Validate(null, NbtTagType.Byte);
- this.WriteByteInternal((byte)(value ? 1 : 0));
- }
-
- public void WriteBool(string name, bool value)
- {
- this.Validate(name, NbtTagType.Byte);
-
- this.Write(NbtTagType.Byte);
- this.WriteStringInternal(name);
- this.WriteByteInternal((byte)(value ? 1 : 0));
- }
-
- public void WriteShort(short value)
- {
- this.Validate(null, NbtTagType.Short);
- this.WriteShortInternal(value);
- }
-
- public void WriteShort(string name, short value)
- {
- this.Validate(name, NbtTagType.Short);
-
- this.Write(NbtTagType.Short);
- this.WriteStringInternal(name);
- this.WriteShortInternal(value);
- }
-
- public void WriteInt(int value)
- {
- this.Validate(null, NbtTagType.Int);
- this.WriteIntInternal(value);
- }
-
- public void WriteInt(string name, int value)
- {
- this.Validate(name, NbtTagType.Int);
-
- this.Write(NbtTagType.Int);
- this.WriteStringInternal(name);
- this.WriteIntInternal(value);
- }
-
- public void WriteLong(long value)
- {
- this.Validate(null, NbtTagType.Long);
- this.WriteLongInternal(value);
- }
-
- public void WriteLong(string name, long value)
- {
- this.Validate(name, NbtTagType.Long);
-
- this.Write(NbtTagType.Long);
- this.WriteStringInternal(name);
- this.WriteLongInternal(value);
- }
-
- public void WriteFloat(float value)
- {
- this.Validate(null, NbtTagType.Float);
- this.WriteFloatInternal(value);
- }
-
- public void WriteFloat(string name, float value)
- {
- this.Validate(name, NbtTagType.Float);
-
- this.Write(NbtTagType.Float);
- this.WriteStringInternal(name);
- this.WriteFloatInternal(value);
- }
-
- public void WriteDouble(double value)
- {
- this.Validate(null, NbtTagType.Double);
- this.WriteDoubleInternal(value);
- }
-
- public void WriteDouble(string name, double value)
- {
- this.Validate(name, NbtTagType.Double);
-
- this.Write(NbtTagType.Double);
- this.WriteStringInternal(name);
-
- this.WriteDoubleInternal(value);
- }
-
- public void Write(NbtTagType tagType) => this.WriteByteInternal((byte)tagType);
-
- private void WriteByteInternal(byte value) => this.BaseStream.WriteByte(value);
-
- private void WriteStringInternal(string value)
- {
- ArgumentNullException.ThrowIfNull(value);
-
- if (value.Length > short.MaxValue)
- throw new InvalidOperationException($"value length must be less than {short.MaxValue}");
-
- if (!ModifiedUtf8.TryGetBytes(value, out var buffer))
- throw new InvalidOperationException("Failed to get bytes from string.");
-
- this.WriteShortInternal((short)buffer.Length);
- this.BaseStream.Write(buffer);
- }
- private void WriteShortInternal(short value)
- {
- Span buffer = stackalloc byte[2];
-
- BinaryPrimitives.WriteInt16BigEndian(buffer, value);
-
- this.BaseStream.Write(buffer);
- }
-
- private void WriteIntInternal(int value)
- {
- Span buffer = stackalloc byte[4];
-
- BinaryPrimitives.WriteInt32BigEndian(buffer, value);
-
- this.BaseStream.Write(buffer);
- }
-
- private void WriteFloatInternal(float value)
- {
- Span buffer = stackalloc byte[4];
-
- BinaryPrimitives.WriteSingleBigEndian(buffer, value);
-
- this.BaseStream.Write(buffer);
- }
-
- private void WriteLongInternal(long value)
- {
- Span buffer = stackalloc byte[8];
-
- BinaryPrimitives.WriteInt64BigEndian(buffer, value);
-
- this.BaseStream.Write(buffer);
- }
-
- private void WriteDoubleInternal(double value)
- {
- Span buffer = stackalloc byte[8];
-
- BinaryPrimitives.WriteDoubleBigEndian(buffer, value);
-
- this.BaseStream.Write(buffer);
- }
-}
diff --git a/Obsidian.Nbt/NbtWriterStream.cs b/Obsidian.Nbt/NbtWriterStream.cs
deleted file mode 100644
index 35d61f8d7..000000000
--- a/Obsidian.Nbt/NbtWriterStream.cs
+++ /dev/null
@@ -1,373 +0,0 @@
-using Obsidian.Nbt.Interfaces;
-using Obsidian.Nbt.Utilities;
-using System.IO;
-using System.IO.Compression;
-
-namespace Obsidian.Nbt;
-
-public partial struct NbtWriterStream(Stream outstream, NbtCompression compressionMode = NbtCompression.None) : INbtWriter
-{
- private NbtWriterState? currentState;
- public NbtTagType? RootType { get; private set; }
-
- public Stream BaseStream { get; } = compressionMode switch
- {
- NbtCompression.GZip => new GZipStream(outstream, CompressionMode.Compress),
- NbtCompression.ZLib => new ZLibStream(outstream, CompressionMode.Compress),
- _ => outstream
- };
-
- public bool Networked { get; }
-
- public NbtWriterStream(Stream outstream, string name) : this(outstream)
- {
- this.Write(NbtTagType.Compound);
- this.WriteStringInternal(name);
-
- this.SetRootTag(NbtTagType.Compound);
- }
-
- public NbtWriterStream(Stream outstream, bool networked) : this(outstream)
- {
- this.Networked = networked;
-
- this.Write(NbtTagType.Compound);
-
- this.SetRootTag(NbtTagType.Compound);
- }
-
- public NbtWriterStream(Stream outstream, NbtCompression compressionMode, string name) : this(outstream, compressionMode)
- {
- this.Write(NbtTagType.Compound);
- this.WriteStringInternal(name);
-
- this.SetRootTag(NbtTagType.Compound);
- }
-
- private void SetRootTag(NbtTagType type, bool addRoot = true)
- {
- if (addRoot)
- {
- this.currentState = new()
- {
- PreviousState = this.currentState,
- ExpectedListType = null,
- ParentTagType = this.RootType ?? type,
- ChildrenAdded = []
- };
- }
-
- this.RootType = type;
- }
-
- private void SetRootTag(NbtTagType type, int listSize, NbtTagType listType, bool addRoot = true)
- {
- if (addRoot)
- {
- this.currentState = new()
- {
- ExpectedListType = listType,
- ListSize = listSize,
- ListIndex = 0,
- PreviousState = this.currentState,
- ParentTagType = this.RootType ?? type
- };
- }
-
- this.RootType = type;
- }
-
- public void WriteCompoundStart(string name = "")
- {
- this.Validate(name, NbtTagType.Compound);
-
- //Lists don't write tag type or tag name for its children
- if (this.RootType == NbtTagType.List)
- {
- this.SetRootTag(NbtTagType.Compound);
- return;
- }
-
- this.SetRootTag(NbtTagType.Compound);
-
- this.Write(NbtTagType.Compound);
- this.WriteStringInternal(name);
- }
-
- public void WriteListStart(string name, NbtTagType listType, int length, bool writeName = true)
- {
- this.Validate(name, NbtTagType.List);
-
- this.SetRootTag(NbtTagType.List, length, listType);
-
- this.Write(NbtTagType.List);
-
- if (writeName)
- this.WriteStringInternal(name);
-
- this.Write(listType);
- this.WriteIntInternal(length);
- }
-
- public void EndList()
- {
- if (this.currentState!.ListIndex < this.currentState?.ListSize)
- throw new InvalidOperationException("List cannot end because its size is smaller than the pre-defined size.");
-
- if (this.RootType != NbtTagType.List)
- throw new InvalidOperationException();
-
- this.RootType = this.currentState?.ParentTagType ?? NbtTagType.End;
-
- this.currentState = this.currentState.PreviousState;
- }
-
- public void EndCompound()
- {
- if (this.RootType != NbtTagType.Compound)
- throw new InvalidOperationException();
-
- this.RootType = this.currentState?.ParentTagType ?? NbtTagType.End;
- this.currentState = this.currentState.PreviousState;
-
- if (this.currentState != null && this.currentState.ExpectedListType != null)
- {
- this.SetRootTag(NbtTagType.List, false);
- this.Write(NbtTagType.End);
-
- return;
- }
-
- this.Write(NbtTagType.End);
- }
-
- public void WriteTag(INbtTag tag)
- {
- var name = tag.Name;
-
- switch (tag.Type)
- {
- case NbtTagType.End:
- throw new InvalidOperationException("Use writer.EndCompound() instead.");
- case NbtTagType.Byte:
- if (tag is NbtTag byteTag)
- {
- this.WriteByte(name, byteTag.Value);
- }
- else if (tag is NbtTag boolValue)
- {
- this.WriteByte(name, (byte)(boolValue.Value ? 1 : 0));
- }
- break;
- case NbtTagType.Short:
- this.WriteShort(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Int:
- this.WriteInt(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Long:
- this.WriteLong(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Float:
- this.WriteFloat(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Double:
- this.WriteDouble(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.String:
- this.WriteString(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.List:
- var list = (NbtList)tag;
-
- this.WriteListStart(name, list.ListType, list.Count);
-
- foreach (var child in list)
- this.WriteListTag(child);
-
- this.EndList();
- break;
- case NbtTagType.Compound:
- this.WriteCompoundStart(name);
-
- foreach (var (_, child) in (NbtCompound)tag)
- this.WriteTag(child);
-
- this.EndCompound();
- break;
- case NbtTagType.ByteArray:
- case NbtTagType.IntArray:
- case NbtTagType.LongArray:
- this.WriteArray(tag);
- break;
- case NbtTagType.Unknown:
- default:
- throw new InvalidOperationException("Unknown tag type");
- }
- }
-
- public void WriteListTag(INbtTag tag)
- {
- var name = tag.Name;
-
- switch (tag.Type)
- {
- case NbtTagType.End:
- throw new InvalidOperationException("Use writer.EndCompound() instead.");
- case NbtTagType.Byte:
- if (tag is NbtTag byteTag)
- {
- this.WriteByte(byteTag.Value);
- }
- else if (tag is NbtTag boolValue)
- {
- this.WriteByte((byte)(boolValue.Value ? 1 : 0));
- }
- break;
- case NbtTagType.Short:
- this.WriteShort(((NbtTag)tag).Value);
- break;
- case NbtTagType.Int:
- this.WriteInt(((NbtTag)tag).Value);
- break;
- case NbtTagType.Long:
- this.WriteLong(((NbtTag)tag).Value);
- break;
- case NbtTagType.Float:
- this.WriteFloat(((NbtTag)tag).Value);
- break;
- case NbtTagType.Double:
- this.WriteDouble(((NbtTag)tag).Value);
- break;
- case NbtTagType.String:
- this.WriteString(((NbtTag)tag).Value);
- break;
- case NbtTagType.List:
- var list = (NbtList)tag;
-
- this.WriteListStart(name, list.ListType, list.Count, false);
-
- foreach (var child in list)
- this.WriteListTag(child);
-
- this.EndList();
- break;
- case NbtTagType.Compound:
- this.WriteCompoundStart();
-
- foreach (var (_, child) in (NbtCompound)tag)
- this.WriteTag(child);
-
- this.EndCompound();
- break;
- case NbtTagType.ByteArray:
- case NbtTagType.IntArray:
- case NbtTagType.LongArray:
- this.WriteArray(tag);
- break;
- case NbtTagType.Unknown:
- default:
- throw new InvalidOperationException("Unknown tag type");
- }
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.IntArray);
- this.WriteStringInternal(name);
- this.WriteIntInternal(values.Length);
-
- for (int i = 0; i < values.Length; i++)
- this.WriteIntInternal(values[i]);
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.LongArray);
- this.WriteStringInternal(name);
- this.WriteIntInternal(values.Length);
-
- for (int i = 0; i < values.Length; i++)
- this.WriteLongInternal(values[i]);
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.ByteArray);
- this.WriteStringInternal(name);
- this.WriteIntInternal(values.Length);
-
- this.BaseStream.Write(values);
- }
-
- private void Validate(string name, NbtTagType type)
- {
- if (this.TryValidateList(name, type))
- return;
-
- if (string.IsNullOrWhiteSpace(name))
- throw new ArgumentException($"Tags inside a compound tag must have a name. Tag({type})");
-
- if (this.currentState.ChildrenAdded.Contains(name))
- throw new ArgumentException($"Tag with name {name} already exists.");
-
- this.currentState.ChildrenAdded.Add(name);
- }
-
- private bool TryValidateList(string name, NbtTagType type)
- {
- if (this.RootType != NbtTagType.List)
- return false;
-
- if (!string.IsNullOrWhiteSpace(name))
- throw new InvalidOperationException("Tags inside lists cannot be named.");
-
- if (!this.currentState!.HasExpectedListType(type))
- throw new InvalidOperationException($"Expected list type: {this.currentState!.ExpectedListType}. Got: {type}");
- else if (!string.IsNullOrEmpty(name))
- throw new InvalidOperationException("Tags inside lists must be nameless.");
- else if (this.currentState!.ListIndex > this.currentState!.ListSize)
- throw new IndexOutOfRangeException("Exceeded pre-defined list size");
-
- this.currentState!.ListIndex++;
-
- return true;
- }
-
- public readonly void TryFinish()
- {
- if (this.currentState != null)
- throw new InvalidOperationException($"Unable to close writer. Root tag has yet to be closed.");//TODO maybe more info here??
-
- this.BaseStream.Flush();
- }
-
- public readonly async Task TryFinishAsync()
- {
- if (this.currentState != null)
- throw new InvalidOperationException("Unable to close writer. Root tag has yet to be closed.");//TODO maybe more info here??
-
- await this.BaseStream.FlushAsync();
- }
-
- public readonly ValueTask DisposeAsync() => this.BaseStream.DisposeAsync();
- public readonly void Dispose() => this.BaseStream.Dispose();
-
- private void WriteArray(INbtTag array)
- {
- this.Validate(array.Name, array.Type);
-
- if (array is NbtArray intArray)
- {
- this.WriteArray(intArray.Name, intArray.GetArray());
- }
- else if (array is NbtArray longArray)
- {
- this.WriteArray(longArray.Name, longArray.GetArray());
- }
- else if (array is NbtArray byteArray)
- {
- this.WriteArray(byteArray.Name, byteArray.GetArray());
- }
- }
-}
diff --git a/Obsidian.Nbt/Obsidian.Nbt.csproj b/Obsidian.Nbt/Obsidian.Nbt.csproj
deleted file mode 100644
index 079c49f2d..000000000
--- a/Obsidian.Nbt/Obsidian.Nbt.csproj
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
- net10.0
- annotations
-
-
-
- true
-
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Obsidian.Nbt/RawNbtWriter.Primitives.cs b/Obsidian.Nbt/RawNbtWriter.Primitives.cs
deleted file mode 100644
index facf9b154..000000000
--- a/Obsidian.Nbt/RawNbtWriter.Primitives.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using System.Buffers;
-using System.Buffers.Binary;
-using System.Diagnostics;
-using CommunityToolkit.HighPerformance;
-
-namespace Obsidian.Nbt;
-public partial class RawNbtWriter
-{
- public void WriteString(string value)
- {
- this.Validate(null, NbtTagType.String);
- this.Write(value);
- }
-
- public void WriteString(string name, string value)
- {
- this.Validate(name, NbtTagType.String);
-
- this.Write(NbtTagType.String);
- this.Write(name);
- this.Write(value);
- }
-
- public void WriteByte(byte value)
- {
- this.Validate(null, NbtTagType.Byte);
- this.Write(value);
- }
-
- public void WriteByte(string name, byte value)
- {
- this.Validate(name, NbtTagType.Byte);
-
- this.Write(NbtTagType.Byte);
- this.Write(name);
- this.Write(value);
- }
-
- public unsafe void WriteBool(bool value) => this.WriteByte(*(byte*)&value);
-
- public unsafe void WriteBool(string name, bool value) => this.WriteByte(name, *(byte*)&value);
-
- public void WriteShort(short value)
- {
- this.Validate(null, NbtTagType.Short);
- this.Write(value);
- }
-
- public void WriteShort(string name, short value)
- {
- this.Validate(name, NbtTagType.Short);
-
- this.Write(NbtTagType.Short);
- this.Write(name);
- this.Write(value);
- }
-
- public void WriteInt(int value)
- {
- this.Validate(null, NbtTagType.Int);
- this.Write(value);
- }
-
- public void WriteInt(string name, int value)
- {
- this.Validate(name, NbtTagType.Int);
-
- this.Write(NbtTagType.Int);
- this.Write(name);
- this.Write(value);
- }
-
- public void WriteLong(long value)
- {
- this.Validate(null, NbtTagType.Long);
- this.Write(value);
- }
-
- public void WriteLong(string name, long value)
- {
- this.Validate(name, NbtTagType.Long);
-
- this.Write(NbtTagType.Long);
- this.Write(name);
- this.Write(value);
- }
-
- public void WriteFloat(float value)
- {
- this.Validate(null, NbtTagType.Float);
- this.Write(value);
- }
-
- public void WriteFloat(string name, float value)
- {
- this.Validate(name, NbtTagType.Float);
-
- this.Write(NbtTagType.Float);
- this.Write(name);
- this.Write(value);
- }
-
- public void WriteDouble(double value)
- {
- this.Validate(null, NbtTagType.Double);
- this.Write(value);
- }
-
- public void WriteDouble(string name, double value)
- {
- this.Validate(name, NbtTagType.Double);
-
- this.Write(NbtTagType.Double);
- this.Write(name);
- this.Write(value);
- }
-
-
- #region primitive writing
- private void Write(string value)
- {
- if (value.Length > short.MaxValue)
- throw new InvalidOperationException($"value length must be less than {short.MaxValue}");
-
- if (!ModifiedUtf8.TryGetBytes(value, out var buffer))
- throw new InvalidOperationException("Failed to get bytes from string.");
-
- this.Write((short)buffer.Length);
- this.Write(buffer);
- }
-
- private void Write(byte value) => this.Write([value]);
-
- private void Write(short value)
- {
- Span span = stackalloc byte[2];
- BinaryPrimitives.WriteInt16BigEndian(span, value);
- this.Write(span);
- }
-
- private void Write(int value)
- {
- Span span = stackalloc byte[4];
- BinaryPrimitives.WriteInt32BigEndian(span, value);
- this.Write(span);
- }
-
- private void Write(long value)
- {
- Span span = stackalloc byte[8];
- BinaryPrimitives.WriteInt64BigEndian(span, value);
- this.Write(span);
- }
-
- private void Write(double value)
- {
- Span span = stackalloc byte[8];
- BinaryPrimitives.WriteDoubleBigEndian(span, value);
- this.Write(span);
- }
-
- private void Write(float value)
- {
- Span span = stackalloc byte[4];
- BinaryPrimitives.WriteSingleBigEndian(span, value);
- this.Write(span);
- }
-
- private void Write(ReadOnlySpan buffer)
- {
- Reserve(buffer.Length);
- buffer.CopyTo(new Span(this.data, this.offset, buffer.Length));
- this.offset += buffer.Length;
- }
-
-
-
- ///
- /// Reserve the buffer of the given capacity
- ///
- public void Reserve(int capacity)
- {
- Debug.Assert(capacity >= 0);
- var required = offset + capacity;
-
- if (required > data.LongLength)
- {
- var newCapacity = Math.Max(required, data.Length * 2);
- ArrayPool.Shared.Resize(ref data, newCapacity);
- }
- }
- #endregion
-
- private void Validate(string name, NbtTagType type)
- {
- if (this.TryValidateList(name, type))
- return;
-
- if (string.IsNullOrWhiteSpace(name))
- throw new ArgumentException($"Tags inside a compound tag must have a name. Tag({type})");
-
- if (this.currentState.ChildrenAdded.Contains(name))
- throw new ArgumentException($"Tag with name {name} already exists.");
-
- this.currentState.ChildrenAdded.Add(name);
- }
-
- private bool TryValidateList(string name, NbtTagType type)
- {
- if (this.RootType != NbtTagType.List)
- return false;
-
- if (!string.IsNullOrWhiteSpace(name))
- throw new InvalidOperationException("Tags inside lists cannot be named.");
-
- if (!this.currentState!.HasExpectedListType(type))
- throw new InvalidOperationException($"Expected list type: {this.currentState!.ExpectedListType}. Got: {type}");
- else if (!string.IsNullOrEmpty(name))
- throw new InvalidOperationException("Tags inside lists must be nameless.");
- else if (this.currentState!.ListIndex > this.currentState!.ListSize)
- throw new IndexOutOfRangeException("Exceeded pre-defined list size");
-
- this.currentState!.ListIndex++;
-
- return true;
- }
-}
diff --git a/Obsidian.Nbt/RawNbtWriter.cs b/Obsidian.Nbt/RawNbtWriter.cs
deleted file mode 100644
index bebf82e3a..000000000
--- a/Obsidian.Nbt/RawNbtWriter.cs
+++ /dev/null
@@ -1,346 +0,0 @@
-using Obsidian.Nbt.Interfaces;
-using Obsidian.Nbt.Utilities;
-using System.Buffers;
-
-namespace Obsidian.Nbt;
-public sealed partial class RawNbtWriter : INbtWriter
-{
- private const int InitialBufferSize = 256;
-
- private bool disposed;
-
- private byte[] data;
- private NbtWriterState? currentState;
- private int offset;
-
- public NbtTagType? RootType { get; private set; }
-
- public bool Networked { get; }
-
- public Span Data => this.AsSpan();
-
- public int Offset => this.offset;
-
- public RawNbtWriter(string name)
- {
- this.data = ArrayPool.Shared.Rent(InitialBufferSize);
-
- this.Write(NbtTagType.Compound);
- this.Write(name);
-
- this.SetRootTag(NbtTagType.Compound);
- }
-
- public RawNbtWriter(bool networked)
- {
- this.data = ArrayPool.Shared.Rent(InitialBufferSize);
- this.Networked = networked;
-
- this.Write(NbtTagType.Compound);
- this.SetRootTag(NbtTagType.Compound);
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.IntArray);
- this.Write(name);
- this.Write(values.Length);
-
- for (int i = 0; i < values.Length; i++)
- this.Write(values[i]);
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.LongArray);
- this.Write(name);
- this.Write(values.Length);
-
- for (int i = 0; i < values.Length; i++)
- this.Write(values[i]);
- }
-
- public void WriteArray(string? name, ReadOnlySpan values)
- {
- this.Write(NbtTagType.ByteArray);
- this.Write(name);
- this.Write(values.Length);
-
- this.Write(values);
- }
-
- public void WriteCompoundStart(string name = "")
- {
- this.Validate(name, NbtTagType.Compound);
-
- if (this.RootType == NbtTagType.List)
- {
- this.SetRootTag(NbtTagType.Compound);
- return;
- }
-
- this.SetRootTag(NbtTagType.Compound);
-
- this.Write(NbtTagType.Compound);
- this.Write(name);
- }
-
- public void WriteListStart(string name, NbtTagType listType, int length, bool writeName = true)
- {
- this.Validate(name, NbtTagType.List);
-
- this.SetRootTag(NbtTagType.List, length, listType);
-
- this.Write(NbtTagType.List);
-
- if (writeName)
- this.Write(name);
-
- this.Write(listType);
- this.Write(length);
- }
-
- public void WriteListTag(INbtTag tag)
- {
- var name = tag.Name;
-
- switch (tag.Type)
- {
- case NbtTagType.End:
- throw new InvalidOperationException("Use writer.EndCompound() instead.");
- case NbtTagType.Byte:
- if (tag is NbtTag byteTag)
- {
- this.WriteByte(byteTag.Value);
- }
- else if (tag is NbtTag boolValue)
- {
- this.WriteByte((byte)(boolValue.Value ? 1 : 0));
- }
- break;
- case NbtTagType.Short:
- this.WriteShort(((NbtTag)tag).Value);
- break;
- case NbtTagType.Int:
- this.WriteInt(((NbtTag)tag).Value);
- break;
- case NbtTagType.Long:
- this.WriteLong(((NbtTag)tag).Value);
- break;
- case NbtTagType.Float:
- this.WriteFloat(((NbtTag)tag).Value);
- break;
- case NbtTagType.Double:
- this.WriteDouble(((NbtTag)tag).Value);
- break;
- case NbtTagType.String:
- this.WriteString(((NbtTag)tag).Value);
- break;
- case NbtTagType.List:
- var list = (NbtList)tag;
-
- this.WriteListStart(name, list.ListType, list.Count, false);
-
- foreach (var child in list)
- this.WriteListTag(child);
-
- this.EndList();
- break;
- case NbtTagType.Compound:
- foreach (var (_, child) in (NbtCompound)tag)
- this.WriteTag(child);
-
- this.EndCompound();
- break;
- case NbtTagType.ByteArray:
- case NbtTagType.IntArray:
- case NbtTagType.LongArray:
- this.WriteArray(tag);
- break;
- case NbtTagType.Unknown:
- default:
- throw new InvalidOperationException("Unknown tag type");
- }
- }
-
- public void WriteTag(INbtTag tag)
- {
- var name = tag.Name;
-
- switch (tag.Type)
- {
- case NbtTagType.End:
- throw new InvalidOperationException("Use writer.EndCompound() instead.");
- case NbtTagType.Byte:
- if (tag is NbtTag byteTag)
- {
- this.WriteByte(name, byteTag.Value);
- }
- else if (tag is NbtTag boolValue)
- {
- this.WriteByte(name, (byte)(boolValue.Value ? 1 : 0));
- }
- break;
- case NbtTagType.Short:
- this.WriteShort(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Int:
- this.WriteInt(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Long:
- this.WriteLong(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Float:
- this.WriteFloat(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.Double:
- this.WriteDouble(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.String:
- this.WriteString(name, ((NbtTag)tag).Value);
- break;
- case NbtTagType.List:
- var list = (NbtList)tag;
-
- this.WriteListStart(name, list.ListType, list.Count);
-
- foreach (var child in list)
- this.WriteListTag(child);
-
- this.EndList();
- break;
- case NbtTagType.Compound:
- this.WriteCompoundStart(name);
-
- foreach (var (_, child) in (NbtCompound)tag)
- this.WriteTag(child);
-
- this.EndCompound();
- break;
- case NbtTagType.ByteArray:
- case NbtTagType.IntArray:
- case NbtTagType.LongArray:
- this.WriteArray(tag);
- break;
- case NbtTagType.Unknown:
- default:
- throw new InvalidOperationException("Unknown tag type");
- }
- }
-
- public void EndCompound()
- {
- if (this.RootType != NbtTagType.Compound)
- throw new InvalidOperationException();
-
- this.RootType = this.currentState?.ParentTagType ?? NbtTagType.End;
- this.currentState = this.currentState.PreviousState;
-
- if (this.currentState != null && this.currentState.ExpectedListType != null)
- {
- this.SetRootTag(NbtTagType.List, false);
- this.Write(NbtTagType.End);
-
- return;
- }
-
- this.Write(NbtTagType.End);
- }
-
- public void EndList()
- {
- if (this.currentState!.ListIndex < this.currentState?.ListSize)
- throw new InvalidOperationException("List cannot end because its size is smaller than the pre-defined size.");
-
- if (this.RootType != NbtTagType.List)
- throw new InvalidOperationException();
-
- this.RootType = this.currentState?.ParentTagType ?? NbtTagType.End;
-
- this.currentState = this.currentState.PreviousState;
- }
-
- public void TryFinish()
- {
- if (this.currentState != null)
- throw new InvalidOperationException($"Unable to close writer. Root tag has yet to be closed.");//TODO maybe more info here??
- }
-
- public Task TryFinishAsync()
- {
- this.TryFinish();
- return Task.CompletedTask;
- }
-
- public Span AsSpan() => new(data, 0, offset);
-
- public void Dispose()
- {
- if (this.disposed)
- throw new ObjectDisposedException(nameof(RawNbtWriter));
-
- this.disposed = true;
-
- ArrayPool.Shared.Return(this.data);
- }
-
- public ValueTask DisposeAsync()
- {
- this.Dispose();
-
- return default;
- }
-
- public void Write(NbtTagType tagType) => this.Write((byte)tagType);
-
- private void WriteArray(INbtTag array)
- {
- this.Validate(array.Name, array.Type);
-
- if (array is NbtArray intArray)
- {
- this.WriteArray(intArray.Name, intArray.GetArray());
- }
- else if (array is NbtArray longArray)
- {
- this.WriteArray(longArray.Name, longArray.GetArray());
- }
- else if (array is NbtArray byteArray)
- {
- this.WriteArray(byteArray.Name, byteArray.GetArray());
- }
- }
-
- private void SetRootTag(NbtTagType type, bool addRoot = true)
- {
- if (addRoot)
- {
- this.currentState = new()
- {
- PreviousState = this.currentState,
- ExpectedListType = null,
- ParentTagType = this.RootType ?? type,
- ChildrenAdded = []
- };
- }
-
- this.RootType = type;
- }
-
- private void SetRootTag(NbtTagType type, int listSize, NbtTagType listType, bool addRoot = true)
- {
- if (addRoot)
- {
- this.currentState = new()
- {
- ExpectedListType = listType,
- ListSize = listSize,
- ListIndex = 0,
- PreviousState = this.currentState,
- ParentTagType = this.RootType ?? type
- };
- }
-
- this.RootType = type;
- }
-}
diff --git a/Obsidian.Nbt/Utilities/NbtWriterState.cs b/Obsidian.Nbt/Utilities/NbtWriterState.cs
deleted file mode 100644
index e718284ab..000000000
--- a/Obsidian.Nbt/Utilities/NbtWriterState.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Obsidian.Nbt.Utilities;
-internal sealed class NbtWriterState
-{
- public int ListSize { get; init; }
-
- public int ListIndex { get; set; }
-
- public NbtTagType? ExpectedListType { get; init; }
-
- public required NbtWriterState? PreviousState { get; init; }
-
- public required NbtTagType? ParentTagType { get; init; }
-
- public List ChildrenAdded { get; init; }
-
- public bool HasExpectedListType(NbtTagType type) =>
- this.ExpectedListType == type || (this.PreviousState?.HasExpectedListType(type) ?? false);
-}
diff --git a/Obsidian.Nbt/Utilities/ThrowHelper.cs b/Obsidian.Nbt/Utilities/ThrowHelper.cs
deleted file mode 100644
index a0106243c..000000000
--- a/Obsidian.Nbt/Utilities/ThrowHelper.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-namespace Obsidian.Nbt.Utilities;
-
-internal class ThrowHelper
-{
- internal static void ThrowInvalidOperationException_StringTooLong()
- {
- throw new InvalidOperationException("Received string is longer than allowed.");
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static void ThrowOutOfRangeException_IfNegative(int value, [CallerArgumentExpression("value")] string? paramName = null)
- {
- if (value < 0)
- {
- ThrowOutOfRangeException_Negative(value, paramName!);
- }
- }
-
- internal static void ThrowOutOfRangeException_Negative(int value, string paramName)
- {
- throw new ArgumentOutOfRangeException($"Value of {paramName} must be positive or zero.");
- }
-
- [DoesNotReturn]
- internal static void ThrowInvalidOperationException_NotEnoughData()
- {
- throw new InvalidOperationException("There isn't enough buffered data for this operation.");
- }
-
- internal static void ThrowInvalidOperationException_InvalidInstance()
- {
- throw new InvalidOperationException("Instance was not properly initialized.");
- }
-
- internal static void ThrowInvalidOperationException_IncorrectTagType()
- {
- throw new InvalidOperationException("Tag type doesn't match requested data type.");
- }
-
- internal static void ThrowException_InsufficientBufferSize()
- {
- throw new Exception("Acquired buffer did not have sufficient size.");
- }
-}
diff --git a/Obsidian.Tests/Fakes/FakePlayer.cs b/Obsidian.Tests/Fakes/FakePlayer.cs
index 838802996..c183e73fe 100644
--- a/Obsidian.Tests/Fakes/FakePlayer.cs
+++ b/Obsidian.Tests/Fakes/FakePlayer.cs
@@ -3,6 +3,7 @@
using Obsidian.API.Effects;
using Obsidian.API.Inventory;
using Obsidian.API.Utilities.Concurrency;
+using Obsidian.API.World;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
@@ -77,7 +78,7 @@ public sealed class FakePlayer : IPlayer
public IReadOnlyDictionary ActivePotionEffects => throw new NotImplementedException();
- public IWorld World => throw new NotImplementedException();
+ public ILevel Level => throw new NotImplementedException();
public INavigator Navigator { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public IGoalController GoalController { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
diff --git a/Obsidian.Tests/Fakes/FakeServer.cs b/Obsidian.Tests/Fakes/FakeServer.cs
index be1d2133b..3c08b93eb 100644
--- a/Obsidian.Tests/Fakes/FakeServer.cs
+++ b/Obsidian.Tests/Fakes/FakeServer.cs
@@ -40,6 +40,7 @@ public sealed class FakeServer : IServer
public IScoreboardManager ScoreboardManager => throw new NotImplementedException();
public IEventDispatcher EventDispatcher => throw new NotImplementedException();
+ public IWorldManager WorldManager => throw new NotImplementedException();
public bool AddPlayer(IPlayer player) => throw new NotImplementedException();
public void BroadcastMessage(ChatMessage message) => throw new NotImplementedException();
diff --git a/Obsidian.slnx b/Obsidian.slnx
index d8b418551..f096f38ab 100644
--- a/Obsidian.slnx
+++ b/Obsidian.slnx
@@ -1,11 +1,13 @@
-
+
-
+
+
+
diff --git a/Obsidian/BlockUpdate.cs b/Obsidian/BlockUpdate.cs
index 4ad26e54a..b41601af7 100644
--- a/Obsidian/BlockUpdate.cs
+++ b/Obsidian/BlockUpdate.cs
@@ -1,8 +1,10 @@
-namespace Obsidian;
+using Obsidian.API.World;
+
+namespace Obsidian;
public struct BlockUpdate : IBlockUpdate
{
- public IWorld World { get; }
+ public ILevel Level { get; }
public Vector Position { get; set; }
public int Delay { get; set; }
@@ -33,9 +35,9 @@ public IBlock? Block
}
}
- public BlockUpdate(IWorld w, Vector pos, IBlock? blk = null)
+ public BlockUpdate(ILevel level, Vector pos, IBlock? blk = null)
{
- World = w;
+ Level = level;
Position = pos;
Delay = 0;
DelayCounter = Delay;
diff --git a/Obsidian/Commands/Modules/MainCommandModule.cs b/Obsidian/Commands/Modules/MainCommandModule.cs
index 949aede60..86b7ba606 100644
--- a/Obsidian/Commands/Modules/MainCommandModule.cs
+++ b/Obsidian/Commands/Modules/MainCommandModule.cs
@@ -164,7 +164,7 @@ public async Task PluginsAsync()
[CommandInfo("Save World", "/save")]
public async Task SaveAsync()
{
- if (this.Player?.World is World world)
+ if (this.Player?.Level is World world)
{
await world.FlushRegionsAsync();
}
@@ -363,7 +363,7 @@ public async Task SpawnEntityAsync(string entityType)
return;
}
- var builder = player.World.GetNewEntitySpawner()
+ var builder = player.Level.GetNewEntitySpawner()
.WithEntityType(type)
.AtPosition(player.Position)
.Spawn();
@@ -386,7 +386,7 @@ public async Task DerpAsync(string entityType)
return;
}
- var frogge = player.World.GetNewEntitySpawner()
+ var frogge = player.Level.GetNewEntitySpawner()
.WithEntityType(type)
.AtPosition(player.Position)
.WithCustomName("Derpy Derp")
@@ -430,7 +430,7 @@ public async Task WeatherAsync()
{
if (this.Player is Player player)
{
- player.World.LevelData.RainTime = 0;
+ player.Level.LevelData.RainTime = 0;
await this.Sender.SendMessageAsync("Toggled weather for this world.");
}
}
@@ -443,7 +443,7 @@ public async Task WorldAsync(string worldname)
if (server.WorldManager.TryGetWorld(worldname, out World? world))
{
- if (player.World.Name.EqualsIgnoreCase(worldname))
+ if (player.Level.Name.EqualsIgnoreCase(worldname))
{
await player.SendMessageAsync("You can't switch to a world you're already in!");
return;
diff --git a/Obsidian/Entities/Animal.cs b/Obsidian/Entities/Animal.cs
index aecd86bac..868d56e62 100644
--- a/Obsidian/Entities/Animal.cs
+++ b/Obsidian/Entities/Animal.cs
@@ -7,7 +7,7 @@ public class Animal : AgeableMob
public async override ValueTask TickAsync()
{
// TODO obby doesn't properly spawn entities yet
- var players = World.PlayersInRange((Vector)Position);
+ var players = Level.PlayersInRange((Vector)Position);
if (players.Any())
{
var closest = players.OrderBy(p => VectorF.Distance(Position, p.Position)).First();
diff --git a/Obsidian/Entities/Entity.cs b/Obsidian/Entities/Entity.cs
index be7cb9b41..9bcfd7d0c 100644
--- a/Obsidian/Entities/Entity.cs
+++ b/Obsidian/Entities/Entity.cs
@@ -1,4 +1,5 @@
using Obsidian.API.AI;
+using Obsidian.API.World;
using Obsidian.Net.Packets.Play.Clientbound;
using System.Diagnostics.CodeAnalysis;
@@ -10,10 +11,10 @@ public class Entity : IEquatable, IEntity
protected byte MetadataIndex { get; set; }
- public required IWorld World { get; set; }
+ public required ILevel Level { get; set; }
- public IPacketBroadcaster PacketBroadcaster => this.World.PacketBroadcaster;
- public IEventDispatcher EventDispatcher => this.World.EventDispatcher;
+ public IPacketBroadcaster PacketBroadcaster => this.Level.PacketBroadcaster;
+ public IEventDispatcher EventDispatcher => this.Level.EventDispatcher;
#region Location properties
public VectorF LastPosition { get; set; }
@@ -75,7 +76,7 @@ public virtual async ValueTask UpdateAsync(VectorF position, MovementFlags movem
{
var delta = (Vector)((position * 32 - Position * 32) * 128);
- this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new MoveEntityPosPacket
+ this.PacketBroadcaster.BroadcastToLevelInRange(this.Level, position, new MoveEntityPosPacket
{
EntityId = EntityId,
@@ -99,7 +100,7 @@ public virtual async ValueTask UpdateAsync(VectorF position, Angle yaw, Angle pi
if (isNewRotation)
{
- this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new MoveEntityPosRotPacket
+ this.PacketBroadcaster.BroadcastToLevelInRange(this.Level, position, new MoveEntityPosRotPacket
{
EntityId = EntityId,
@@ -115,7 +116,7 @@ public virtual async ValueTask UpdateAsync(VectorF position, Angle yaw, Angle pi
}
else
{
- this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new MoveEntityPosPacket
+ this.PacketBroadcaster.BroadcastToLevelInRange(this.Level, position, new MoveEntityPosPacket
{
EntityId = EntityId,
@@ -144,7 +145,7 @@ public virtual ValueTask UpdateAsync(Angle yaw, Angle pitch, MovementFlags movem
public bool IsInRange(IEntity entity, float distance)
{
- if (this.World != entity.World)
+ if (this.Level != entity.Level)
return false;
var locationDifference = LocationDiff.GetDifference(this.Position, entity.Position);
@@ -156,7 +157,7 @@ public bool IsInRange(IEntity entity, float distance)
public void SetHeadRotation(Angle headYaw) =>
- this.PacketBroadcaster.BroadcastToWorldInRange(this.World, this.Position, new RotateHeadPacket
+ this.PacketBroadcaster.BroadcastToLevelInRange(this.Level, this.Position, new RotateHeadPacket
{
EntityId = EntityId,
HeadYaw = headYaw
@@ -164,7 +165,7 @@ public void SetHeadRotation(Angle headYaw) =>
public void SetRotation(Angle yaw, Angle pitch, MovementFlags movementFlags)
{
- this.PacketBroadcaster.BroadcastToWorldInRange(this.World, this.Position, new MoveEntityRotPacket
+ this.PacketBroadcaster.BroadcastToLevelInRange(this.Level, this.Position, new MoveEntityRotPacket
{
EntityId = EntityId,
OnGround = movementFlags.HasFlag(MovementFlags.OnGround),
@@ -178,7 +179,7 @@ public void SetRotation(Angle yaw, Angle pitch, MovementFlags movementFlags)
public async Task UpdatePositionAsync(VectorF pos, MovementFlags movementFlags)
{
var (x, z) = pos.ToChunkCoord();
- var chunk = await this.World.GetChunkAsync(x, z, false);
+ var chunk = await this.Level.GetChunkAsync(x, z, false);
if (chunk != null && chunk.IsGenerated)
{
Position = pos;
@@ -193,7 +194,7 @@ public async Task UpdatePositionAsync(VectorF pos, MovementFlags movementFlags)
public async Task UpdatePositionAsync(VectorF pos, Angle yaw, Angle pitch, MovementFlags movementFlags = MovementFlags.OnGround)
{
var (x, z) = pos.ToChunkCoord();
- var chunk = await World.GetChunkAsync(x, z, false);
+ var chunk = await Level.GetChunkAsync(x, z, false);
if (chunk is { IsGenerated: true })
{
Position = pos;
@@ -226,7 +227,7 @@ public VectorF GetLookDirection()
return new(-cosPitch * sinYaw, -sinPitch, cosPitch * cosYaw);
}
- public async virtual ValueTask RemoveAsync() => await this.World.DestroyEntityAsync(this);
+ public async virtual ValueTask RemoveAsync() => await this.Level.DestroyEntityAsync(this);
protected virtual EntityBitMask GenerateBitmask()
{
@@ -292,7 +293,7 @@ public virtual void Write(INetStreamWriter writer)
protected void WriteEntityMetadataType(INetStreamWriter writer, EntityMetadataType type) =>
writer.WriteEntityMetadataType(this.MetadataIndex++, type);
- public IEnumerable GetEntitiesNear(float distance) => World.GetEntitiesInRange(Position, distance).Where(x => x != this);
+ public IEnumerable GetEntitiesNear(float distance) => Level.GetEntitiesInRange(Position, distance).Where(x => x != this);
//TODO GRAVITY
public virtual ValueTask TickAsync() => default;
@@ -304,7 +305,7 @@ public async ValueTask DamageAsync(IEntity source, float amount = 1.0f)
if (this is ILiving living)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new AnimatePacket
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new AnimatePacket
{
EntityId = EntityId,
Animation = EntityAnimationType.CriticalEffect
@@ -357,12 +358,12 @@ public async virtual ValueTask TeleportAsync(IEntity to)
if (to is not Entity target)
return;
- if (to.World != World)
+ if (to.Level != Level)
{
- await World.DestroyEntityAsync(this);
+ await Level.DestroyEntityAsync(this);
- World = target.World;
- World.SpawnEntity(to.Position, Type);
+ Level = target.Level;
+ Level.SpawnEntity(to.Position, Type);
return;
}
@@ -374,7 +375,7 @@ public virtual ValueTask TeleportAsync(VectorF pos)
{
if (VectorF.Distance(Position, pos) > 8)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, 0, new TeleportEntityPacket
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, 0, new TeleportEntityPacket
{
EntityId = EntityId,
OnGround = MovementFlags.HasFlag(MovementFlags.OnGround),
@@ -388,7 +389,7 @@ public virtual ValueTask TeleportAsync(VectorF pos)
var delta = (Vector)(pos * 32 - Position * 32) * 128;
- this.PacketBroadcaster.QueuePacketToWorld(this.World, 0, new MoveEntityPosRotPacket
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, 0, new MoveEntityPosRotPacket
{
EntityId = EntityId,
Delta = delta,
@@ -402,7 +403,7 @@ public virtual ValueTask TeleportAsync(VectorF pos)
public virtual void SpawnEntity(Velocity? velocity = null, int additionalData = 0)
{
- this.PacketBroadcaster.QueuePacketToWorldInRange(this.World, this.Position, new BundledPacket
+ this.PacketBroadcaster.QueuePacketToLevelInRange(this.Level, this.Position, new BundledPacket
(
[
new AddEntityPacket
diff --git a/Obsidian/Entities/Factories/EntitySpawner.cs b/Obsidian/Entities/Factories/EntitySpawner.cs
index 7e06c39a4..aec2d2ee8 100644
--- a/Obsidian/Entities/Factories/EntitySpawner.cs
+++ b/Obsidian/Entities/Factories/EntitySpawner.cs
@@ -1,10 +1,12 @@
using Obsidian.API.Entities;
+using Obsidian.API.World;
using Obsidian.WorldData;
namespace Obsidian.Entities.Factories;
-internal class EntitySpawner(IWorld world) : IEntitySpawner
+
+internal class EntitySpawner(ILevel level) : IEntitySpawner
{
- private readonly IWorld world = world;
+ private readonly ILevel level = level;
private EntityType? entityType = null;
@@ -80,27 +82,27 @@ public IEntity Spawn()
{
EntityType.Pig => new Pig()
{
- World = world,
+ Level = level,
},
EntityType.Horse => new Horse()
{
- World = world,
+ Level = level,
},
EntityType.Llama => new Llama()
{
- World = world,
+ Level = level,
},
EntityType.Donkey => new Donkey()
{
- World = world
+ Level = level
},
EntityType.SkeletonHorse => new SkeletonHorse()
{
- World = world
+ Level = level
},
EntityType.ZombieHorse => new ZombieHorse()
{
- World = world
+ Level = level
},
null => throw new InvalidOperationException("Entity type must be set"),
@@ -108,11 +110,11 @@ public IEntity Spawn()
_ => entityType.Value.IsNonLiving() ?
new Entity()
{
- World = world,
+ Level = level,
} :
new Living()
{
- World = world
+ Level = level
}
};
@@ -137,6 +139,6 @@ public IEntity Spawn()
entity.Burning = burning;
entity.Glowing = glowing;
- return (world as World).SpawnEntity(entity);
+ return level.SpawnEntity(entity);
}
}
diff --git a/Obsidian/Entities/FallingBlock.cs b/Obsidian/Entities/FallingBlock.cs
index 6d516f903..04f6dfe48 100644
--- a/Obsidian/Entities/FallingBlock.cs
+++ b/Obsidian/Entities/FallingBlock.cs
@@ -44,7 +44,7 @@ public async override ValueTask TickAsync()
{
checkedBlocks.Add(upcomingBlockPos);
- var upcomingBlock = await World.GetBlockAsync(upcomingBlockPos);
+ var upcomingBlock = await Level.GetBlockAsync(upcomingBlockPos);
if (upcomingBlock is IBlock && !TagsRegistry.Block.ReplaceableByLiquid.Entries.Contains(upcomingBlock.RegistryId) && !upcomingBlock.IsLiquid)
{
await ConvertToBlock(upcomingBlockPos + Vector.Up);
@@ -54,8 +54,8 @@ public async override ValueTask TickAsync()
private async Task ConvertToBlock(Vector loc)
{
- await World.SetBlockAsync(loc, this.Block);
+ await Level.SetBlockAsync(loc, this.Block);
- await World.DestroyEntityAsync(this);
+ await Level.DestroyEntityAsync(this);
}
}
diff --git a/Obsidian/Entities/ItemEntity.cs b/Obsidian/Entities/ItemEntity.cs
index b116c91b3..e8ae8f244 100644
--- a/Obsidian/Entities/ItemEntity.cs
+++ b/Obsidian/Entities/ItemEntity.cs
@@ -30,7 +30,7 @@ public async override ValueTask TickAsync()
if (!CanPickup && DateTimeOffset.UtcNow - this.TimeDropped > DropWaitTime)
this.CanPickup = true;
- foreach (var ent in this.World.GetNonPlayerEntitiesInRange(this.Position, 0.5f))
+ foreach (var ent in this.Level.GetNonPlayerEntitiesInRange(this.Position, 0.5f))
{
if (ent is not ItemEntity itemEntity)
continue;
diff --git a/Obsidian/Entities/Living.cs b/Obsidian/Entities/Living.cs
index 878f782ee..e18ee4670 100644
--- a/Obsidian/Entities/Living.cs
+++ b/Obsidian/Entities/Living.cs
@@ -57,7 +57,7 @@ public void ClearPotionEffects()
public void AddPotionEffect(int effectId, int duration, int amplifier = 0, EntityEffectFlags effect = EntityEffectFlags.None)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new UpdateMobEffectPacket(EntityId, effectId, duration)
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new UpdateMobEffectPacket(EntityId, effectId, duration)
{
Amplifier = amplifier,
Flags = effect
@@ -82,7 +82,7 @@ public void AddPotionEffect(int effectId, int duration, int amplifier = 0, Entit
public void RemovePotionEffect(int effectId)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new RemoveMobEffectPacket(EntityId, effectId));
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new RemoveMobEffectPacket(EntityId, effectId));
activePotionEffects.TryRemove(effectId, out _);
}
public override void Write(INetStreamWriter writer)
diff --git a/Obsidian/Entities/Player.Helpers.cs b/Obsidian/Entities/Player.Helpers.cs
index 6cc33edef..aec62b71f 100644
--- a/Obsidian/Entities/Player.Helpers.cs
+++ b/Obsidian/Entities/Player.Helpers.cs
@@ -9,19 +9,13 @@
using System.IO;
namespace Obsidian.Entities;
+
public partial class Player
{
public async Task SaveAsync()
{
- var playerDataFile = new FileInfo(GetPlayerDataPath());
var persistentDataFile = new FileInfo(PersistentDataFile);
- if (playerDataFile.Exists)
- {
- playerDataFile.CopyTo(GetPlayerDataPath(true), true);
- playerDataFile.Delete();
- }
-
if (persistentDataFile.Exists)
{
persistentDataFile.CopyTo(PersistentDataBackupFile, true);
@@ -30,13 +24,26 @@ public async Task SaveAsync()
await using var persistentDataStream = persistentDataFile.Create();
await using var persistentDataWriter = new NbtWriterStream(persistentDataStream, NbtCompression.GZip, "");
-
- persistentDataWriter.WriteString("worldName", World.ParentWorldName ?? World.Name);
+
+ var level = this.Level is IDimension dimension ? dimension.ParentWorld : this.Level as IWorld;
+ var worldName = level.Name;
+
+ persistentDataWriter.WriteString("worldName", worldName);
//TODO make sure to save inventory in the right location if has using global data set to true
persistentDataWriter.EndCompound();
await persistentDataWriter.TryFinishAsync();
+ var playerDatPath = level.GetPlayerDataPath(this.Uuid);
+
+ var playerDataFile = new FileInfo(playerDatPath);
+
+ if (playerDataFile.Exists)
+ {
+ playerDataFile.CopyTo(level.GetPlayerDataPath(this.Uuid, true), true);
+ playerDataFile.Delete();
+ }
+
await using var playerFileStream = playerDataFile.Create();
await using var writer = new NbtWriterStream(playerFileStream, NbtCompression.GZip, "");
@@ -59,7 +66,7 @@ public async Task SaveAsync()
writer.WriteFloat("foodExhaustionLevel", FoodExhaustionLevel);
writer.WriteFloat("foodSaturationLevel", FoodSaturationLevel);
- writer.WriteString("Dimension", World.DimensionName);
+ writer.WriteString("Dimension", Level.DimensionName);
writer.WriteListStart("Pos", NbtTagType.Double, 3);
@@ -100,24 +107,22 @@ public async Task LoadAsync(bool loadFromPersistentWorld = true)
{
var worldName = persistentDataCompound.GetString("worldName")!;
- Logger.LogInformation("persistent world: {worldName}", worldName);
-
- if (loadFromPersistentWorld && this.World.WorldManager.TryGetWorld(worldName, out var world))
+ if (loadFromPersistentWorld && this.Server.WorldManager.TryGetWorld(worldName, out var resolvedWorld))
{
- World = world;
- Logger.LogInformation("Loading from persistent world: {worldName}", worldName);
+ this.Level = resolvedWorld;
}
}
}
+ var world = this.Level as IWorld;
// Then read player data
- var playerDataFile = new FileInfo(GetPlayerDataPath());
+ var playerDataFile = new FileInfo(world.GetPlayerDataPath(this.Uuid));
await LoadPermsAsync();
if (!playerDataFile.Exists)
{
- Position = World.LevelData.SpawnPosition;
+ Position = Level.LevelData.SpawnPosition;
return;
}
@@ -132,7 +137,7 @@ public async Task LoadAsync(bool loadFromPersistentWorld = true)
catch (Exception ex)
{
this.Logger.LogWarning(ex, "Player has invalid saved data.");
- Position = World.LevelData.SpawnPosition;//Set spawn here cause the data loaded was invalid
+ Position = Level.LevelData.SpawnPosition;//Set spawn here cause the data loaded was invalid
}
if (!Alive)
@@ -214,10 +219,9 @@ await Client.QueuePacketAsync(new SetEntityDataPacket
private async ValueTask TrySpawnPlayerAsync(VectorF position)
{
- //TODO PROPER DISTANCE CALCULATION
var entityBroadcastDistance = this.Server.Configuration.EntityBroadcastRangePercentage;
- foreach (var player in World.GetPlayersInRange(position, entityBroadcastDistance))
+ foreach (var player in Level.GetPlayersInRange(position, entityBroadcastDistance))
{
if (player.EntityId == this.EntityId)
continue;
@@ -254,7 +258,7 @@ private async ValueTask TrySpawnPlayerAsync(VectorF position)
private async Task PickupNearbyItemsAsync(float distance = 1.5f)
{
- foreach (var entity in World.GetNonPlayerEntitiesInRange(Position, distance))
+ foreach (var entity in Level.GetNonPlayerEntitiesInRange(Position, distance))
{
if (entity is not ItemEntity itemEntity)
continue;
@@ -262,7 +266,7 @@ private async Task PickupNearbyItemsAsync(float distance = 1.5f)
if (!itemEntity.CanPickup)
continue;
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new TakeItemEntityPacket
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new TakeItemEntityPacket
{
CollectedEntityId = itemEntity.EntityId,
CollectorEntityId = EntityId,
@@ -322,8 +326,6 @@ private void WriteItems(INbtWriter writer, bool inventory = true)
{ Uuid, actions.ToList() }
};
- private string GetPlayerDataPath(bool isOld = false) => Path.Join(World.PlayerDataPath, isOld ? $"{Uuid}.dat.old" : $"{Uuid}.dat");
-
private void InitializePlayer(NbtCompound compound)
{
MovementFlags = (MovementFlags)compound.GetByte("MovementFlags");
@@ -353,8 +355,8 @@ private void InitializePlayer(NbtCompound compound)
compound.TryGetTag("Pos", out var posTag);
Position = (posTag as NbtList) switch
{
- [NbtTag a, NbtTag b, NbtTag c, ..] => new VectorF((float)a.Value, (float)b.Value, (float)c.Value),
- _ => World.LevelData.SpawnPosition
+ [NbtTag a, NbtTag b, NbtTag c, ..] => new VectorF((float)a.Value, (float)b.Value, (float)c.Value),
+ _ => Level.LevelData.SpawnPosition
};
if (compound.TryGetTag("Rotation", out var rotationTag))
diff --git a/Obsidian/Entities/Player.cs b/Obsidian/Entities/Player.cs
index a5205cb99..3d739ab6c 100644
--- a/Obsidian/Entities/Player.cs
+++ b/Obsidian/Entities/Player.cs
@@ -177,7 +177,7 @@ internal Player(Guid uuid, string username, IClient client, IWorld world)
Title = "Ender Chest"
};
- World = world;
+ Level = world;
Type = EntityType.Player;
PersistentDataFile = Path.Combine(ServerConstants.PersistentDataPath, $"{Uuid}.dat");
@@ -289,10 +289,10 @@ public async override ValueTask TeleportAsync(IWorld world)
// save current world/persistent data
await SaveAsync();
- World.TryRemovePlayer(this);
+ Level.TryRemovePlayer(this);
w.TryAddPlayer(this);
- World = w;
+ Level = w;
// resync player data
await LoadAsync(false);
@@ -352,20 +352,20 @@ public async Task RespawnAsync(DataKept dataKept = DataKept.Metadata)
{
// if unalive, reset health and set location to world spawn
Health = 20f;
- Position = World.LevelData.SpawnPosition;
+ Position = Level.LevelData.SpawnPosition;
}
- CodecRegistry.TryGetDimension(World.DimensionName, out var codec);
+ CodecRegistry.TryGetDimension(Level.DimensionName, out var codec);
Debug.Assert(codec is not null); // TODO Handle missing codec
- Logger.LogDebug("Loading into world: {}", World.Name);
+ Logger.LogDebug("Loading into world: {}", Level.Name);
await Client.QueuePacketAsync(new RespawnPacket
{
CommonPlayerSpawnInfo = new()
{
DimensionType = codec.Id,
- DimensionName = World.DimensionName,
+ DimensionName = Level.DimensionName,
Gamemode = Gamemode,
PreviousGamemode = Gamemode,
HashedSeed = 0,
@@ -440,7 +440,7 @@ public override void Write(INetStreamWriter writer)
public async ValueTask SetGamemodeAsync(Gamemode gamemode)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new PlayerInfoUpdatePacket(CompilePlayerInfo(new UpdateGamemodeInfoAction(gamemode))));
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new PlayerInfoUpdatePacket(CompilePlayerInfo(new UpdateGamemodeInfoAction(gamemode))));
await Client.QueuePacketAsync(new GameEventPacket(gamemode));
@@ -449,7 +449,7 @@ public async ValueTask SetGamemodeAsync(Gamemode gamemode)
public ValueTask UpdateDisplayNameAsync(string newDisplayName)
{
- this.PacketBroadcaster.QueuePacketToWorld(this.World, new PlayerInfoUpdatePacket(CompilePlayerInfo(new UpdateDisplayNameInfoAction(newDisplayName))));
+ this.PacketBroadcaster.QueuePacketToLevel(this.Level, new PlayerInfoUpdatePacket(CompilePlayerInfo(new UpdateDisplayNameInfoAction(newDisplayName))));
CustomName = newDisplayName;
@@ -690,7 +690,7 @@ public async Task UpdateChunksAsync(bool unloadAll = false, int distance =
foreach (var value in clientNeededChunks)
{
NumericsHelper.LongToInts(value, out var x, out var z);
- var chunk = await World.GetChunkAsync(x, z);
+ var chunk = await Level.GetChunkAsync(x, z);
if (chunk is not null && chunk.IsGenerated)
{
await Client.QueuePacketAsync(new LevelChunkWithLightPacket(chunk));
diff --git a/Obsidian/Events/MainEventHandler.Inventory.cs b/Obsidian/Events/MainEventHandler.Inventory.cs
index 0f667e809..2a307e44d 100644
--- a/Obsidian/Events/MainEventHandler.Inventory.cs
+++ b/Obsidian/Events/MainEventHandler.Inventory.cs
@@ -328,7 +328,7 @@ private static void SpawnThrownItem(IPlayer player, ItemStack? thrownItem)
EntityId = Server.GetNextEntityId(),
Item = thrownItem,
Glowing = true,
- World = player.World,
+ Level = player.Level,
Position = loc
};
@@ -336,7 +336,7 @@ private static void SpawnThrownItem(IPlayer player, ItemStack? thrownItem)
var vel = Velocity.FromDirection(loc, lookDir);
//TODO Get this shooting out from the player properly.
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new AddEntityPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new AddEntityPacket
{
EntityId = item.EntityId,
Uuid = item.Uuid,
@@ -347,7 +347,7 @@ private static void SpawnThrownItem(IPlayer player, ItemStack? thrownItem)
Data = 1,
Velocity = vel
});
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEntityDataPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new SetEntityDataPacket
{
EntityId = item.EntityId,
Entity = item
diff --git a/Obsidian/Events/MainEventHandler.World.cs b/Obsidian/Events/MainEventHandler.World.cs
index 15138a412..136a22db9 100644
--- a/Obsidian/Events/MainEventHandler.World.cs
+++ b/Obsidian/Events/MainEventHandler.World.cs
@@ -12,7 +12,7 @@ public async ValueTask OnBlockBreak(BlockBreakEventArgs args)
var player = args.Player;
var sequence = args.Sequence;
var block = args.Block;
- var world = player.World;
+ var world = player.Level;
var location = args.Location;
player.Client.SendPacket(new BlockChangedAckPacket
@@ -30,7 +30,7 @@ public async ValueTask OnBlockBreak(BlockBreakEventArgs args)
player.Client.SendPacket(new BlockUpdatePacket(location, BlocksRegistry.Air.GetHashCode()));
- world.PacketBroadcaster.QueuePacketToWorld(world, 0, new BlockDestructionPacket
+ world.PacketBroadcaster.QueuePacketToLevel(world, 0, new BlockDestructionPacket
{
EntityId = player.EntityId,
Position = location,
@@ -46,11 +46,11 @@ public async ValueTask OnBlockBreak(BlockBreakEventArgs args)
{
EntityId = Server.GetNextEntityId(),
Item = droppedItem,
- World = player.World,
+ Level = player.Level,
Position = (VectorF)location + 0.5f,
};
- player.World.TryAddEntity(item);
+ player.Level.TryAddEntity(item);
var power = GetRandDropVelocity();
var direction = Globals.Random.NextFloat() * 6.2f;
diff --git a/Obsidian/Events/MainEventHandler.cs b/Obsidian/Events/MainEventHandler.cs
index 99ac2886d..ddd6fa9d2 100644
--- a/Obsidian/Events/MainEventHandler.cs
+++ b/Obsidian/Events/MainEventHandler.cs
@@ -55,7 +55,7 @@ public async Task OnContainerClosed(ContainerClosedEventArgs e)
return;
var position = blockEntity.BlockPosition;
- var block = await e.Player.World.GetBlockAsync(position);
+ var block = await e.Player.Level.GetBlockAsync(position);
if (block is null)
return;
@@ -278,13 +278,13 @@ await player.Client.QueuePacketAsync(new BlockEventPacket
if (container is IBlockEntity containerTileEntity)
{
- var tileEntity = await player.World.GetBlockEntityAsync(blockPosition);
+ var tileEntity = await player.Level.GetBlockEntityAsync(blockPosition);
if (tileEntity == null)
{
tileEntity = containerTileEntity.Clone();
- await player.World.SetBlockEntity(blockPosition, tileEntity);
+ await player.Level.SetBlockEntity(blockPosition, tileEntity);
}
else if (tileEntity is BaseContainer tileEntityContainer)
{
@@ -311,7 +311,7 @@ public async Task OnPlayerLeave(PlayerLeaveEventArgs e)
var player = e.Player;
var server = e.Server;
- var packetBroadcaster = player.World.PacketBroadcaster;
+ var packetBroadcaster = player.Level.PacketBroadcaster;
await player.SaveAsync();
@@ -329,10 +329,10 @@ public ValueTask OnPlayerJoin(PlayerJoinEventArgs e)
var joined = e.Player;
var server = e.Server;
- var packetBroadcaster = joined.World.PacketBroadcaster;
+ var packetBroadcaster = joined.Level.PacketBroadcaster;
- joined!.World.TryAddPlayer(joined);
- joined!.World.TryAddEntity(joined);
+ joined!.Level.TryAddPlayer(joined);
+ joined!.Level.TryAddEntity(joined);
server!.BroadcastMessage(new ChatMessage
{
diff --git a/Obsidian/GlobalUsings.cs b/Obsidian/GlobalUsings.cs
index 2c6a34e7d..3f34b4290 100644
--- a/Obsidian/GlobalUsings.cs
+++ b/Obsidian/GlobalUsings.cs
@@ -1,5 +1,6 @@
global using Obsidian.API;
global using Obsidian.API.ChunkData;
+global using Obsidian.API.World;
global using Obsidian.API.World.Features;
global using Obsidian.API.Registries;
global using Obsidian.API.Utilities;
diff --git a/Obsidian/Hosting/DependencyInjection.cs b/Obsidian/Hosting/DependencyInjection.cs
index 527dcfffb..8fc8347b8 100644
--- a/Obsidian/Hosting/DependencyInjection.cs
+++ b/Obsidian/Hosting/DependencyInjection.cs
@@ -57,12 +57,12 @@ public static IHostApplicationBuilder AddObsidian(this IHostApplicationBuilder b
builder.Services.AddSingleton();
builder.Services.AddSingleton();
- //builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddHttpClient();
diff --git a/Obsidian/Net/Packets/Configuration/Serverbound/FinishConfigurationPacket.cs b/Obsidian/Net/Packets/Configuration/Serverbound/FinishConfigurationPacket.cs
index 0f39fbe9c..df8351601 100644
--- a/Obsidian/Net/Packets/Configuration/Serverbound/FinishConfigurationPacket.cs
+++ b/Obsidian/Net/Packets/Configuration/Serverbound/FinishConfigurationPacket.cs
@@ -22,7 +22,7 @@ public async override ValueTask HandleAsync(IServer server, IPlayer player)
return;
}
- if (!CodecRegistry.TryGetDimension(player.World.DimensionName, out var codec) || !CodecRegistry.TryGetDimension("minecraft:overworld", out codec))
+ if (!CodecRegistry.TryGetDimension(player.Level.DimensionName, out var codec) || !CodecRegistry.TryGetDimension("minecraft:overworld", out codec))
throw new UnreachableException("Failed to retrieve proper dimension for player.");
await client.QueuePacketAsync(new LoginPacket
@@ -44,10 +44,10 @@ await client.QueuePacketAsync(new LoginPacket
await client.QueuePacketAsync(new SetDefaultSpawnPositionPacket(new()
{
DimensionName = codec.Name,
- Position = (Vector)player.World.LevelData.SpawnPosition
+ Position = (Vector)player.Level.LevelData.SpawnPosition
}, 0, 0));
- await client.QueuePacketAsync(new SetTimePacket(player.World.LevelData.Time, player.World.LevelData.DayTime, true));
- await client.QueuePacketAsync(new GameEventPacket(player.World.LevelData.Raining ? ChangeGameStateReason.BeginRaining : ChangeGameStateReason.EndRaining));
+ await client.QueuePacketAsync(new SetTimePacket(player.Level.LevelData.Time, player.Level.LevelData.DayTime, true));
+ await client.QueuePacketAsync(new GameEventPacket(player.Level.LevelData.Raining ? ChangeGameStateReason.BeginRaining : ChangeGameStateReason.EndRaining));
await client.QueuePacketAsync(CustomPayloadPacket.ClientboundPlay with { Channel = "minecraft:brand", PluginData = server.BrandData });
await client.QueuePacketAsync(CommandsRegistry.Packet);
diff --git a/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs
index 4c459b757..aafb545cb 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs
@@ -30,12 +30,12 @@ public override void Populate(INetStreamReader reader)
public async override ValueTask HandleAsync(IServer server, IPlayer player)
{
- if (await player.World.GetBlockAsync(Position) is not IBlock block)
+ if (await player.Level.GetBlockAsync(Position) is not IBlock block)
return;
if (Status == PlayerActionStatus.FinishedDigging || (Status == PlayerActionStatus.StartedDigging && player.Gamemode == Gamemode.Creative))
{
- var args = new BlockBreakEventArgs(server, player, block, Position, player.World)
+ var args = new BlockBreakEventArgs(server, player, block, Position, player.Level)
{
Sequence = this.Sequence
};
@@ -92,11 +92,11 @@ private static void DropItem(IPlayer player, sbyte amountToRemove)
{
EntityId = Server.GetNextEntityId(),
Item = droppedItem,
- World = player.World,
+ Level = player.Level,
Position = loc
};
- player.World.TryAddEntity(item);
+ player.Level.TryAddEntity(item);
var vel = Velocity.FromBlockPerTick(lookDir.X * 0.45f, lookDir.Y * 0.45f + 0.1f, lookDir.Z * 0.45f);
diff --git a/Obsidian/Net/Packets/Play/Serverbound/PlayerCommandPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/PlayerCommandPacket.cs
index c2aacfe89..7af0566ca 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/PlayerCommandPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/PlayerCommandPacket.cs
@@ -25,7 +25,7 @@ public override void Populate(INetStreamReader reader)
public async override ValueTask HandleAsync(IServer server, IPlayer player)
{
- var block = await player.World.GetBlockAsync((int)player.Position.X, (int)player.HeadY, (int)player.Position.Z);
+ var block = await player.Level.GetBlockAsync((int)player.Position.X, (int)player.HeadY, (int)player.Position.Z);
switch (Action)
{
@@ -56,7 +56,7 @@ public async override ValueTask HandleAsync(IServer server, IPlayer player)
break;
}
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEntityDataPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new SetEntityDataPacket
{
EntityId = player.EntityId,
Entity = player
diff --git a/Obsidian/Net/Packets/Play/Serverbound/PlayerInputPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/PlayerInputPacket.cs
index 60458fb35..06000cdab 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/PlayerInputPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/PlayerInputPacket.cs
@@ -20,7 +20,7 @@ public override ValueTask HandleAsync(IServer server, IPlayer player)
if (player.Sneaking != wasSneaking)
{
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEntityDataPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new SetEntityDataPacket
{
EntityId = player.EntityId,
Entity = player
diff --git a/Obsidian/Net/Packets/Play/Serverbound/SetCarriedItemPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/SetCarriedItemPacket.cs
index 75e87cd09..0fa5f67bf 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/SetCarriedItemPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/SetCarriedItemPacket.cs
@@ -19,7 +19,7 @@ public override ValueTask HandleAsync(IServer server, IPlayer player)
var heldItem = player.GetHeldItem();
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEquipmentPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new SetEquipmentPacket
{
EntityId = player.EntityId,
Equipment = new()
diff --git a/Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs
index 281f46f82..7cc24fc48 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs
@@ -33,7 +33,7 @@ public override ValueTask HandleAsync(IServer server, IPlayer player)
{
var heldItem = player.GetHeldItem();
- player.World.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEquipmentPacket
+ player.Level.PacketBroadcaster.QueuePacketToLevel(player.Level, new SetEquipmentPacket
{
EntityId = player.EntityId,
Equipment =
diff --git a/Obsidian/Net/Packets/Play/Serverbound/UseItemOnPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/UseItemOnPacket.cs
index d6af2cf90..d109827c5 100644
--- a/Obsidian/Net/Packets/Play/Serverbound/UseItemOnPacket.cs
+++ b/Obsidian/Net/Packets/Play/Serverbound/UseItemOnPacket.cs
@@ -44,7 +44,7 @@ public async override ValueTask HandleAsync(IServer server, IPlayer player)
var currentItem = player.GetHeldItem() ?? player.GetOffHandItem();
var position = this.Position;
- var b = await player.World.GetBlockAsync(position);
+ var b = await player.Level.GetBlockAsync(position);
if (b is null)
return;
@@ -122,20 +122,20 @@ await server.EventDispatcher.ExecuteEventAsync(new PlayerInteractEventArgs(playe
if (TagsRegistry.Block.GravityAffected.Entries.Contains(block.RegistryId))
{
- if (await player.World.GetBlockAsync(position + Vector.Down) is IBlock below &&
+ if (await player.Level.GetBlockAsync(position + Vector.Down) is IBlock below &&
(TagsRegistry.Block.ReplaceableByLiquid.Entries.Contains(below.RegistryId) || below.IsLiquid))
{
- await player.World.SetBlockAsync(position, BlocksRegistry.Air, true);
+ await player.Level.SetBlockAsync(position, BlocksRegistry.Air, true);
player.Client.SendPacket(new BlockChangedAckPacket
{
SequenceID = Sequence
});
- player.World.SpawnFallingBlock(position, block.Material);
+ player.Level.SpawnFallingBlock(position, block.Material);
return;
}
}
- await player.World.SetBlockAsync(position, block, doBlockUpdate: true);
+ await player.Level.SetBlockAsync(position, block, doBlockUpdate: true);
player.Client.SendPacket(new BlockChangedAckPacket
{
SequenceID = Sequence
diff --git a/Obsidian/Obsidian.csproj b/Obsidian/Obsidian.csproj
index b9525b9c0..cf27abb1b 100644
--- a/Obsidian/Obsidian.csproj
+++ b/Obsidian/Obsidian.csproj
@@ -71,7 +71,7 @@
+
-
diff --git a/Obsidian/Server.cs b/Obsidian/Server.cs
index af4590095..399816df1 100644
--- a/Obsidian/Server.cs
+++ b/Obsidian/Server.cs
@@ -12,8 +12,8 @@
using Obsidian.Net.Packets.Play.Clientbound;
using Obsidian.Plugins;
using Obsidian.Services;
-using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
@@ -138,7 +138,7 @@ public void RegisterRecipes(params IRecipe[] recipes)
return null;
}
- public bool TryGetPlayer(string username, out IPlayer? player)
+ public bool TryGetPlayer(string username, [NotNullWhen(true)] out IPlayer? player)
{
if (this.GetPlayer(username) is IPlayer foundPlayer)
{
@@ -150,9 +150,10 @@ public bool TryGetPlayer(string username, out IPlayer? player)
return false;
}
- public bool TryGetPlayer(Guid uuid, out IPlayer? player) => this.OnlinePlayers.TryGetValue(uuid, out player);
+
+ public bool TryGetPlayer(Guid uuid, [NotNullWhen(true)] out IPlayer? player) => this.OnlinePlayers.TryGetValue(uuid, out player);
- public bool TryGetPlayer(int entityId, out IPlayer? player)
+ public bool TryGetPlayer(int entityId, [NotNullWhen(true)] out IPlayer? player)
{
if (this.GetPlayer(entityId) is IPlayer foundPlayer)
{
@@ -172,7 +173,7 @@ public void BroadcastMessage(ChatMessage message)
public void BroadcastMessage(IWorld world, ChatMessage message)
{
- this.DefaultWorld.PacketBroadcaster.QueuePacketToWorld(world, new SystemChatPacket(message, false));
+ this.DefaultWorld.PacketBroadcaster.QueuePacketToLevel(world, new SystemChatPacket(message, false));
logger.LogInformation("{message}", message.Text);
}
@@ -191,7 +192,7 @@ public async Task RunAsync()
Directory.CreateDirectory(ServerConstants.PermissionPath);
Directory.CreateDirectory(ServerConstants.PersistentDataPath);
Directory.CreateDirectory(ServerConstants.AcceptedKeysPath);
- Directory.CreateDirectory("plugins");
+ Directory.CreateDirectory(ServerConstants.PluginsPath);
StartTime = DateTimeOffset.Now;
this.Connections = new ConcurrentDictionary(-1, this.MaxConnections);
@@ -306,7 +307,7 @@ public bool RemovePlayer(IPlayer player)
{
this.UsernameToUuidMappings.Remove(player.Username, out _);
- player.World.TryRemovePlayer(player);
+ player.Level.TryRemovePlayer(player);
return this.OnlinePlayers.Remove(player.Uuid, out _);
}
diff --git a/Obsidian/ServerConstants.cs b/Obsidian/ServerConstants.cs
index 1de8f4fd5..3b36cf71a 100644
--- a/Obsidian/ServerConstants.cs
+++ b/Obsidian/ServerConstants.cs
@@ -30,4 +30,5 @@ public static string VERSION
public const string PersistentDataPath = "persistentdata";
public const string PermissionPath = "permissions";
public const string AcceptedKeysPath = "accepted_keys";
+ public const string PluginsPath = "plugins";
}
diff --git a/Obsidian/Services/PacketBroadcaster.cs b/Obsidian/Services/PacketBroadcaster.cs
index ce43ddbe0..541a6e2b5 100644
--- a/Obsidian/Services/PacketBroadcaster.cs
+++ b/Obsidian/Services/PacketBroadcaster.cs
@@ -1,17 +1,18 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Obsidian.API.World;
using Obsidian.Entities;
using Obsidian.Hosting;
using Obsidian.WorldData;
using System.Threading;
namespace Obsidian.Services;
-public sealed class PacketBroadcaster(IServer server, ILoggerFactory loggerFactory, IServerEnvironment environment) : BackgroundService, IPacketBroadcaster
+
+public sealed class PacketBroadcaster(IServer server, ILogger logger, IServerEnvironment environment) : BackgroundService, IPacketBroadcaster
{
private readonly IServer server = server;
private readonly IServerEnvironment environment = environment;
private readonly PriorityQueue priorityQueue = new();
- private readonly ILogger logger = loggerFactory.CreateLogger();
public void QueuePacketTo(IClientboundPacket packet, params int[] ids)
{
@@ -30,14 +31,11 @@ public void QueuePacketTo(IClientboundPacket packet, int priority, params int[]
public void QueuePacket(IClientboundPacket packet, params int[] excludedIds) =>
this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds }, 1);
- public void QueuePacketToWorld(IWorld world, IClientboundPacket packet, params int[] excludedIds)
- {
- this.priorityQueue.Enqueue(new() { Packet = packet, ToWorld = world, ExcludedIds = excludedIds }, 1);
- }
-
- public void QueuePacketToWorld(IWorld world, int priority, IClientboundPacket packet, params int[] excludedIds) =>
- this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds, ToWorld = world }, priority);
+ public void QueuePacketToLevel(ILevel level, IClientboundPacket packet, params int[] excludedIds) =>
+ this.priorityQueue.Enqueue(new() { Packet = packet, ToLevel = level, ExcludedIds = excludedIds }, 1);
+ public void QueuePacketToLevel(ILevel level, int priority, IClientboundPacket packet, params int[] excludedIds) =>
+ this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds, ToLevel = level }, priority);
public void QueuePacket(IClientboundPacket packet, int priority, params int[] excludedIds) =>
this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds }, priority);
@@ -53,18 +51,18 @@ public void BroadcastTo(IClientboundPacket packet, params int[] ids)
player.Client.SendPacket(packet);
}
- public void BroadcastToWorldInRange(IWorld toWorld, VectorF location, IClientboundPacket packet, params int[] excludedIds)
+ public void BroadcastToLevelInRange(ILevel toLevel, VectorF location, IClientboundPacket packet, params int[] excludedIds)
{
- if (toWorld is not World world)
+ if (toLevel is not AbstractLevel world)
return;
- foreach (var player in world.GetPlayersInRange(location, world.Configuration.EntityBroadcastRangePercentage).Cast())
+ foreach (var player in world.GetPlayersInRange(location, world.Configuration.EntityBroadcastRangePercentage).Cast().Where(x => !excludedIds.Contains(x.EntityId)))
player.Client.SendPacket(packet);
}
- public void QueuePacketToWorldInRange(IWorld toWorld, VectorF location, IClientboundPacket packet, params int[] excludedIds)
+ public void QueuePacketToLevelInRange(ILevel toLevel, VectorF location, IClientboundPacket packet, params int[] excludedIds)
{
- if (toWorld is not World world)
+ if (toLevel is not AbstractLevel world)
return;
var includedIDs = world.GetPlayersInRange(location, world.Configuration.EntityBroadcastRangePercentage)
@@ -79,15 +77,15 @@ public void QueuePacketToWorldInRange(IWorld toWorld, VectorF location, IClientb
this.priorityQueue.Enqueue(new()
{
Packet = packet,
- ToWorld = world,
+ ToLevel = world,
ExcludedIds = excludedIds,
}, 1);
}
- public void BroadcastToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds)
+ public void BroadcastToLevel(ILevel toLevel, IClientboundPacket packet, params int[] excludedIds)
{
- if (toWorld is not World world)
+ if (toLevel is not AbstractLevel world)
return;
foreach (var player in world.Players.Values.Where(player => !excludedIds.Contains(player.EntityId)))
@@ -105,9 +103,9 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken)
if (!this.priorityQueue.TryDequeue(out var queuedPacket, out var priority))
continue;
- if (queuedPacket.ToWorld is IWorld toWorld)
+ if (queuedPacket.ToLevel is AbstractLevel toLevel)
{
- foreach (var player in toWorld.Players.Values.Where(player => ShouldGetPacket(player, queuedPacket)))
+ foreach (var player in toLevel.Players.Values.Where(player => ShouldGetPacket(player, queuedPacket)))
await player.Client.QueuePacketAsync(queuedPacket.Packet);
continue;
@@ -139,6 +137,6 @@ private readonly struct QueuedPacket
public int[]? ExcludedIds { get; init; }
public int[]? IncludedIds { get; init; }
- public IWorld? ToWorld { get; init; }
+ public ILevel? ToLevel { get; init; }
}
}
diff --git a/Obsidian/Utilities/Extensions.cs b/Obsidian/Utilities/Extensions.cs
index 39264480f..cedc3097e 100644
--- a/Obsidian/Utilities/Extensions.cs
+++ b/Obsidian/Utilities/Extensions.cs
@@ -55,6 +55,8 @@ public static partial class Extensions
EntityType.FishingBobber,
EntityType.EyeOfEnder];
+ public static string GetPlayerDataPath(this IWorld world, Guid uuid, bool isOld = false) => Path.Combine(world.PlayerDataPath, isOld ? $"{uuid}.dat.old" : $"{uuid}.dat");
+
public static async ValueTask DisconnectPlayerIfConnectedAsync(this IServer server, string username, ChatMessage? reason = null)
{
if (!server.TryGetPlayer(username, out var player))
diff --git a/Obsidian/WorldData/AbstractLevel.cs b/Obsidian/WorldData/AbstractLevel.cs
new file mode 100644
index 000000000..c31c98c80
--- /dev/null
+++ b/Obsidian/WorldData/AbstractLevel.cs
@@ -0,0 +1,654 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Obsidian.API.Configuration;
+using Obsidian.API.Entities;
+using Obsidian.API.Registry.Codecs.Dimensions;
+using Obsidian.Entities;
+using Obsidian.Entities.Factories;
+using Obsidian.Net.Packets.Play.Clientbound;
+using System.Diagnostics;
+using System.Threading;
+
+namespace Obsidian.WorldData;
+
+public abstract class AbstractLevel : ILevel
+{
+ private bool generated;
+
+ public LevelData LevelData { get; internal set; } = default!;
+
+ public ConcurrentDictionary Players { get; protected set; } = [];
+
+ public ILevelGenerator Generator { get; internal set; }
+
+ public ConcurrentDictionary Regions { get; protected set; } = [];
+
+ public ConcurrentQueue ChunksToGen { get; protected set; } = [];
+
+ public long[] SpawnChunks { get; }
+
+ public ConcurrentHashSet LoadedChunks { get; protected set; } = [];
+
+ public string Name { get; }
+ public string Seed { get; }
+ public string FolderPath { get; protected set; } = string.Empty;
+
+ public bool Loaded { get; protected set; }
+
+ public long Time
+ {
+ get => LevelData.Time;
+ set
+ {
+ LevelData.Time = value;
+ this.BroadcastTime();
+ }
+ }
+
+ public int DayTime
+ {
+ get => LevelData.DayTime;
+ set
+ {
+ LevelData.DayTime = value;
+ this.BroadcastTime();
+ }
+ }
+
+ public int RegionCount => this.Regions.Count;
+ public int ChunksToGenCount => this.ChunksToGen.Count;
+ public int LoadedChunkCount => this.Regions.Values.Sum(x => x.LoadedChunkCount);
+
+ public IPacketBroadcaster PacketBroadcaster { get; }
+ public IEventDispatcher EventDispatcher { get; }
+ public ServerConfiguration Configuration { get; private set; }
+ public Gamemode DefaultGamemode => LevelData.DefaultGamemode;
+
+ public string DimensionName { get; protected set; } = string.Empty;
+
+ public string LevelDataFilePath { get; protected set; }
+
+ protected ILogger Logger { get; }
+
+ private readonly IDisposable optionsMonitor;
+ private readonly Lock regionLock = new();
+
+ public AbstractLevel(ILogger logger, IPacketBroadcaster packetBroadcaster, IOptionsMonitor configuration,
+ IEventDispatcher eventDispatcher, ILevelGenerator worldGenerator, string name, string seed)
+ {
+ this.optionsMonitor = configuration.OnChange(newConfig =>
+ {
+ this.Configuration = newConfig;
+ });
+
+ this.Logger = logger;
+ this.PacketBroadcaster = packetBroadcaster;
+ this.Configuration = configuration.CurrentValue;
+ this.EventDispatcher = eventDispatcher;
+ this.Generator = worldGenerator;
+ this.Name = name;
+ this.Seed = seed;
+
+ var spawnChunkCount = 2 * this.Configuration.SpawnChunkRadius + 1;
+ this.SpawnChunks = new long[spawnChunkCount * spawnChunkCount];
+
+ this.Generator.Init(this);
+ }
+
+ public ValueTask DestroyEntityAsync(IEntity entity)
+ {
+ var destroyed = new RemoveEntitiesPacket(entity.EntityId);
+
+ this.PacketBroadcaster.QueuePacketToLevel(this, destroyed);
+
+ var (chunkX, chunkZ) = entity.Position.ToChunkCoord();
+
+ var region = GetRegionForChunk(chunkX, chunkZ);
+
+ return region is null ? ValueTask.FromResult(false) : ValueTask.FromResult(region.Entities.TryRemove(entity.EntityId, out _));
+ }
+
+ public abstract Task LoadAsync(DimensionCodec codec);
+ public abstract Task SaveAsync();
+
+ public IRegion? GetRegionForLocation(VectorF location)
+ {
+ (int chunkX, int chunkZ) = location.ToChunkCoord();
+ long key = NumericsHelper.IntsToLong(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
+ Regions.TryGetValue(key, out var region);
+ return region;
+ }
+
+ public IRegion? GetRegionForChunk(int chunkX, int chunkZ)
+ {
+ long value = NumericsHelper.IntsToLong(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
+
+ return Regions.TryGetValue(value, out var region) ? region : null;
+ }
+
+ public IRegion? GetRegionForChunk(Vector location) => GetRegionForChunk(location.X, location.Z);
+
+ public async ValueTask GetChunkAsync(int chunkX, int chunkZ, bool scheduleGeneration = true)
+ {
+ var region = GetRegionForChunk(chunkX, chunkZ) ?? LoadRegion(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
+
+ if (region is null)
+ return null;
+
+ var (x, z) = (NumericsHelper.Modulo(chunkX, Region.CubicRegionSize), NumericsHelper.Modulo(chunkZ, Region.CubicRegionSize));
+ var packedXZ = NumericsHelper.IntsToLong(chunkX, chunkZ);
+
+ var chunk = await region.GetChunkAsync(x, z);
+
+ if (chunk is not null)
+ {
+ if (!chunk.IsGenerated && scheduleGeneration)
+ {
+ if (!ChunksToGen.Contains(packedXZ))
+ ChunksToGen.Enqueue(packedXZ);
+ return null;
+ }
+
+ LoadedChunks.Add(packedXZ);
+ return chunk;
+ }
+
+ if (scheduleGeneration)
+ {
+ if (!ChunksToGen.Contains(packedXZ))
+ ChunksToGen.Enqueue(packedXZ);
+ return null;
+ }
+
+ chunk = new Chunk(chunkX, chunkZ, ChunkGenStage.structure_starts);
+ region.SetChunk(chunk);
+ return chunk;
+ }
+
+ public async ValueTask GetBlockAsync(int x, int y, int z)
+ {
+ var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
+ return c?.GetBlock(x, y, z);
+ }
+
+ public async ValueTask GetWorldSurfaceHeightAsync(int x, int z)
+ {
+ var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
+ return c?.Heightmaps[HeightmapType.WorldSurface]
+ .GetHeight(NumericsHelper.Modulo(x, 16), NumericsHelper.Modulo(z, 16));
+ }
+
+ public async ValueTask SetBlockAsync(int x, int y, int z, IBlock block)
+ {
+ await SetBlockUntrackedAsync(x, y, z, block);
+ this.BroadcastBlockChange(block, new(x, y, z));
+ }
+
+ public async ValueTask SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate)
+ {
+ await SetBlockUntrackedAsync(x, y, z, block, doBlockUpdate);
+ this.BroadcastBlockChange(block, new(x, y, z));
+ }
+
+ private void BroadcastBlockChange(IBlock block, Vector location)
+ {
+ var packet = new BlockUpdatePacket(location, block.GetHashCode());
+ foreach (Player player in PlayersInRange(location).Cast())
+ {
+ player.Client.SendPacket(packet);
+ }
+ }
+
+ public IEnumerable PlayersInRange(Vector location)
+ {
+ var (x, z) = location.ToChunkCoord();
+ var packedXZ = NumericsHelper.IntsToLong(x, z);
+
+ return this.Players.Values.Where(player => player.LoadedChunks.Contains(packedXZ));
+ }
+
+ public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false) => SetBlockUntrackedAsync(location.X, location.Y, location.Z, block, doBlockUpdate);
+
+ public async ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false)
+ {
+ if (doBlockUpdate)
+ {
+ await ScheduleBlockUpdateAsync(new BlockUpdate(this, new Vector(x, y, z), block));
+ await BlockUpdateNeighborsAsync(new BlockUpdate(this, new Vector(x, y, z), block));
+ }
+ var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
+ c?.SetBlock(x, y, z, block);
+ }
+
+ public IEnumerable GetEntitiesInRange(VectorF location, float distance = 10f)
+ {
+ foreach (IPlayer player in GetPlayersInRange(location, distance))
+ {
+ yield return player;
+ }
+
+ foreach (IEntity entity in GetNonPlayerEntitiesInRange(location, distance))
+ {
+ yield return entity;
+ }
+ }
+
+ public IEnumerable GetNonPlayerEntitiesInRange(VectorF location, float distance)
+ {
+ if (float.IsNaN(distance) || distance < 0f)
+ {
+ yield break;
+ }
+
+ (int left, int top) = (location - new VectorF(distance)).ToChunkCoord();
+ (int right, int bottom) = (location + new VectorF(distance)).ToChunkCoord();
+
+ distance *= distance;
+
+ for (int x = left; x <= right; x += Region.CubicRegionSize)
+ {
+ for (int z = top; z >= bottom; z -= Region.CubicRegionSize)
+ {
+ if (GetRegionForChunk(x, z) is not Region region)
+ continue;
+
+ foreach (var entity in region.Entities.Values)
+ {
+ if (entity.Type == EntityType.Player)
+ continue;
+
+ var locationDifference = LocationDiff.GetDifference(entity.Position, location);
+
+ if (locationDifference.CalculatedDifference <= distance)
+ {
+ yield return entity;
+ }
+ }
+ }
+ }
+ }
+
+ public IEnumerable GetPlayersInRange(VectorF location, float distance)
+ {
+ if (float.IsNaN(distance) || distance < 0f)
+ {
+ yield break;
+ }
+
+ if (distance == 0f)
+ {
+ foreach (var player in Players.Values)
+ {
+ if (player.Position == location)
+ {
+ yield return player;
+ }
+ }
+ yield break;
+ }
+
+ distance *= distance;
+
+ foreach (var player in Players.Values)
+ {
+ var locationDifference = LocationDiff.GetDifference(player.Position, location);
+
+ if (locationDifference.CalculatedDifference <= distance)
+ {
+ yield return player;
+ }
+ }
+ }
+
+ public IEnumerable GetPlayersInChunkRange(Vector worldPosition)
+ {
+ var (x, z) = worldPosition.ToChunkCoord();
+
+ var packedXZ = NumericsHelper.IntsToLong(x, z);
+ return this.Players.Values.Where(player => player.LoadedChunks.Contains(packedXZ));
+ }
+
+ public bool TryAddPlayer(IPlayer player) => Players.TryAdd(player.Uuid, player);
+
+ public bool TryRemovePlayer(IPlayer player) => Players.TryRemove(player.Uuid, out _);
+
+ public async virtual Task DoWorldTickAsync()
+ {
+ if (LevelData is null)
+ return;
+
+ LevelData.Time += this.Configuration.TimeTickSpeedMultiplier;
+ LevelData.RainTime -= this.Configuration.TimeTickSpeedMultiplier;
+
+ await Task.WhenAll(this.Regions.Values.Select(r => r.BeginTickAsync()));
+ }
+
+ public IRegion LoadRegionByChunk(int chunkX, int chunkZ)
+ {
+ int regionX = chunkX >> Region.CubicRegionSizeShift, regionZ = chunkZ >> Region.CubicRegionSizeShift;
+ return LoadRegion(regionX, regionZ);
+ }
+
+ public IRegion LoadRegion(int regionX, int regionZ)
+ {
+ long value = NumericsHelper.IntsToLong(regionX, regionZ);
+
+ if (Regions.TryGetValue(value, out var region))
+ return region;
+
+ using (regionLock.EnterScope())
+ {
+ if (Regions.TryGetValue(value, out region))
+ return region;
+
+ region = new Region(regionX, regionZ, FolderPath, logger: this.Logger);
+ this.Logger.LogDebug("Trying to add {x}:{z} to {path}", regionX, regionZ, region.RegionFolder);
+
+ if (this.Regions.TryAdd(value, region))
+ _ = region.InitAsync();
+ else
+ {
+ // Another thread added the region first; discard our copy and return existing
+ this.Logger.LogDebug("Region {x}:{z} already exists, using existing region", regionX, regionZ);
+ region = Regions[value]!;
+ }
+
+ return region;
+ }
+ }
+
+ public async Task UnloadRegionAsync(int regionX, int regionZ)
+ {
+ long value = NumericsHelper.IntsToLong(regionX, regionZ);
+ if (Regions.TryRemove(value, out var r))
+ await r.FlushAsync();
+ }
+
+ public async ValueTask ScheduleBlockUpdateAsync(IBlockUpdate blockUpdate)
+ {
+ blockUpdate.Block ??= await GetBlockAsync(blockUpdate.Position);
+ (int chunkX, int chunkZ) = blockUpdate.Position.ToChunkCoord();
+ var region = GetRegionForChunk(chunkX, chunkZ);
+ region?.AddBlockUpdate(blockUpdate);
+ }
+
+ public async Task ManageChunksAsync()
+ {
+ if (LevelData.Time > 0 && LevelData.Time % (20 * 30) == 0)
+ {
+ var chunksToKeep = new List();
+ Players.Values.ForEach(p =>
+ {
+ chunksToKeep.AddRange(p.LoadedChunks);
+ });
+
+ foreach (var chunk in LoadedChunks.Except(chunksToKeep).Except(SpawnChunks))
+ {
+ if (LoadedChunks.TryRemove(chunk))
+ {
+ NumericsHelper.LongToInts(chunk, out var cx, out var cz);
+ var r = GetRegionForChunk(cx, cz);
+ await r.UnloadChunk(cx, cz);
+ }
+ }
+ }
+
+ if (ChunksToGen.IsEmpty)
+ return;
+
+ var jobs = new List();
+ for (int a = 0; a < Environment.ProcessorCount; a++)
+ {
+ if (ChunksToGen.TryDequeue(out var job))
+ jobs.Add(job);
+ }
+
+ await Parallel.ForEachAsync(jobs, async (job, _) =>
+ {
+ NumericsHelper.LongToInts(job, out var jobX, out var jobZ);
+ var region = GetRegionForChunk(jobX, jobZ) ?? LoadRegionByChunk(jobX, jobZ);
+
+ var (x, z) = (NumericsHelper.Modulo(jobX, Region.CubicRegionSize), NumericsHelper.Modulo(jobZ, Region.CubicRegionSize));
+
+ var c = await region.GetChunkAsync(x, z);
+ if (c is null)
+ {
+ c = new Chunk(jobX, jobZ, ChunkGenStage.structure_starts);
+ region.SetChunk(c);
+ }
+ if (!c.IsGenerated)
+ {
+ c = await Generator.GenerateChunkAsync(jobX, jobZ, c);
+ }
+ region.SetChunk(c);
+ });
+ }
+
+ public Task FlushRegionsAsync() => Task.WhenAll(Regions.Select(pair => pair.Value.FlushAsync()));
+
+ public IEntity SpawnFallingBlock(VectorF position, Material mat)
+ {
+ position.X += 0.5f;
+ position.Z += 0.5f;
+
+ FallingBlock entity = new(position)
+ {
+ Type = EntityType.FallingBlock,
+ EntityId = Server.GetNextEntityId(),
+ Level = this,
+ Block = BlocksRegistry.Get(mat),
+ };
+
+ entity.SpawnEntity(null, entity.Block.GetHashCode());
+
+ TryAddEntity(entity);
+
+ return entity;
+ }
+
+ public ValueTask GetBlockEntityAsync(Vector blockPosition) => GetBlockEntityAsync(blockPosition.X, blockPosition.Y, blockPosition.Z);
+
+ public async ValueTask GetBlockEntityAsync(int x, int y, int z)
+ {
+ var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
+ return c?.GetBlockEntity(x, y, z);
+ }
+
+ public ValueTask SetBlockEntity(Vector blockPosition, IBlockEntity tileEntityData) => SetBlockEntity(blockPosition.X, blockPosition.Y, blockPosition.Z, tileEntityData);
+ public async ValueTask SetBlockEntity(int x, int y, int z, IBlockEntity tileEntityData)
+ {
+ var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
+ c?.SetBlockEntity(x, y, z, tileEntityData);
+ }
+
+ public IEntity SpawnEntity(VectorF position, EntityType type)
+ {
+ if (type == EntityType.ExperienceOrb)
+ throw new NotImplementedException($"EntityType {type} is not supported.");
+
+ if (type == EntityType.FallingBlock)
+ return SpawnFallingBlock(position + (0, 20, 0), Material.Sand);
+
+ return GetNewEntitySpawner()
+ .WithEntityType(type)
+ .AtPosition(position)
+ .Spawn();
+ }
+
+ public IEntity SpawnEntity(IEntity entity)
+ {
+ entity.SpawnEntity();
+ TryAddEntity(entity as Entity);
+ return entity;
+ }
+
+ public void SpawnExperienceOrbs(VectorF position, short count = 1)
+ {
+ }
+
+ public async ValueTask HandleBlockUpdateAsync(IBlockUpdate update)
+ {
+ if (update.Block is not IBlock block)
+ return false;
+
+ if (TagsRegistry.Block.GravityAffected.Entries.Contains(block.RegistryId))
+ return await BlockUpdates.HandleFallingBlock(update);
+
+ if (block.IsLiquid)
+ return await BlockUpdates.HandleLiquidPhysicsAsync(update);
+
+ return false;
+ }
+
+ public async ValueTask BlockUpdateNeighborsAsync(IBlockUpdate update)
+ {
+ update.Block = null;
+
+ Vector[] directions = Vector.AllDirections;
+ for (int i = 0; i < directions.Length; i++)
+ {
+ update.Position = update.Position + directions[i];
+ await ScheduleBlockUpdateAsync(update);
+ }
+ }
+
+ public abstract void Initialize(DimensionCodec codec);
+
+ ///
+ /// Starts the initial generation of the world, which includes pregenerating chunks in a square around the spawn and loading their regions,
+ /// as well as setting the world spawn if specified. This should be called after Initialize and before allowing players to join.
+ ///
+ /// Whether to set the world spawn after generation.
+ public async Task GenerateAsync()
+ {
+ if (this.generated)
+ return;
+
+ Logger.LogInformation("Generating world... (Config pregeneration size is {pregenRange})", this.Configuration.PregenerateChunkRange);
+ int pregenerationRange = this.Configuration.PregenerateChunkRange;
+
+ int regionPregenRange = (pregenerationRange >> Region.CubicRegionSizeShift) + 1;
+
+ foreach (var x in Enumerable.Range(-regionPregenRange, regionPregenRange * 2 + 1))
+ {
+ for (int z = -regionPregenRange; z < regionPregenRange; z++)
+ LoadRegion(x, z);
+ }
+
+ for (int x = -pregenerationRange; x < pregenerationRange; x++)
+ {
+ for (int z = -pregenerationRange; z < pregenerationRange; z++)
+ {
+ ChunksToGen.Enqueue(NumericsHelper.IntsToLong(x, z));
+ }
+ }
+
+ float startChunks = ChunksToGenCount;
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+ Logger.LogInformation("{startChunks} chunks to generate...", startChunks);
+ while (!ChunksToGen.IsEmpty)
+ {
+ await ManageChunksAsync();
+ var pctComplete = (int)((1.0 - ChunksToGenCount / startChunks) * 100);
+ var completedChunks = startChunks - ChunksToGenCount;
+ var cps = completedChunks / (stopwatch.ElapsedMilliseconds / 1000.0);
+ int remain = ChunksToGenCount / (int)Math.Max(cps, 1);
+ Console.Write("\r{0} chunks/second - {1}% complete - {2} seconds remaining ", cps.ToString("###.00"), pctComplete, remain);
+ if (completedChunks % 1024 == 0)
+ {
+ await FlushRegionsAsync();
+ }
+ }
+ Console.WriteLine();
+
+ await FlushRegionsAsync();
+ await SetWorldSpawnAsync();
+
+ {
+ var index = 0;
+ var (x, z) = LevelData.SpawnPosition.ToChunkCoord();
+ for (var cx = x - this.Configuration.SpawnChunkRadius; cx < x + this.Configuration.SpawnChunkRadius; cx++)
+ for (var cz = z - this.Configuration.SpawnChunkRadius; cz < z + this.Configuration.SpawnChunkRadius; cz++)
+ SpawnChunks[index++] = NumericsHelper.IntsToLong(cx, cz);
+ }
+
+ this.generated = true;
+ }
+
+ private async Task SetWorldSpawnAsync()
+ {
+ if (LevelData.SpawnPosition.Y != 0)
+ return;
+
+ var pregenRange = this.Configuration.PregenerateChunkRange;
+ var region = GetRegionForLocation(VectorF.Zero)!;
+ foreach (var chunk in region.GeneratedChunks())
+ {
+ for (int bx = 0; bx < 16; bx++)
+ {
+ for (int bz = 0; bz < 16; bz++)
+ {
+ var by = chunk.Heightmaps[HeightmapType.MotionBlocking].GetHeight(bx, bz);
+ IBlock block = chunk.GetBlock(bx, by, bz);
+
+ if (by < 64 || !block.Is(Material.GrassBlock) && !block.Is(Material.Sand))
+ {
+ continue;
+ }
+
+ if (!chunk.GetBlock(bx, by + 1, bz).IsAir || !chunk.GetBlock(bx, by + 2, bz).IsAir)
+ {
+ continue;
+ }
+
+ var worldPos = new VectorF(bx + 0.5f + (chunk.X * 16), by + 1, bz + 0.5f + (chunk.Z * 16));
+ LevelData.SpawnPosition = worldPos;
+ Logger.LogInformation("World Spawn set to {worldPos}", worldPos);
+
+ for (int x = chunk.X - pregenRange; x < chunk.X + pregenRange; x++)
+ {
+ for (int z = chunk.Z - pregenRange; z < chunk.Z + pregenRange; z++)
+ {
+ await GetChunkAsync(x, z);
+ }
+ }
+
+ return;
+ }
+ }
+ }
+ Logger.LogWarning("Failed to set World Spawn.");
+ }
+
+ public bool TryAddEntity(IEntity entity)
+ {
+ var (chunkX, chunkZ) = entity.Position.ToChunkCoord();
+
+ var region = GetRegionForChunk(chunkX, chunkZ);
+
+ return region is not null && region.Entities.TryAdd(entity.EntityId, entity);
+ }
+
+ protected void BroadcastTime() => this.PacketBroadcaster.QueuePacketToLevel(this, new SetTimePacket(LevelData.Time, LevelData.Time % 24000, true));
+
+ public async ValueTask DisposeAsync()
+ {
+ GC.SuppressFinalize(this);
+
+ this.optionsMonitor.Dispose();
+
+ foreach (var region in Regions.Values)
+ {
+ await region.DisposeAsync();
+ }
+ }
+
+ public IEntitySpawner GetNewEntitySpawner() => new EntitySpawner(this);
+ public ValueTask GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true) =>
+ this.GetChunkAsync(worldLocation.X, worldLocation.Z, scheduleGeneration);
+
+ public ValueTask GetBlockAsync(Vector location) => this.GetBlockAsync(location.X, location.Y, location.Z);
+ public ValueTask SetBlockAsync(Vector location, IBlock block) => this.SetBlockAsync(location.X, location.Y, location.Z, block);
+ public ValueTask SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate) => this.SetBlockAsync(location.X, location.Y, location.Z, block, doBlockUpdate);
+}
diff --git a/Obsidian/WorldData/BlockUpdates.cs b/Obsidian/WorldData/BlockUpdates.cs
index bbed76b19..68b4ca477 100644
--- a/Obsidian/WorldData/BlockUpdates.cs
+++ b/Obsidian/WorldData/BlockUpdates.cs
@@ -14,7 +14,7 @@ internal static async Task HandleFallingBlock(IBlockUpdate blockUpdate)
{
if (blockUpdate.Block is null) { return false; }
- var world = blockUpdate.World;
+ var world = blockUpdate.Level;
var position = blockUpdate.Position;
var material = blockUpdate.Block.Material;
if (await world.GetBlockAsync(position + Vector.Down) is IBlock below &&
@@ -38,7 +38,7 @@ internal static async Task HandleLiquidPhysicsAsync(IBlockUpdate blockUpda
if (blockUpdate.Block is null) { return false; }
var block = blockUpdate.Block;
- var world = blockUpdate.World;
+ var world = blockUpdate.Level;
var position = blockUpdate.Position;
int liquidLevel = GetLiquidState(block);
Vector belowPos = position + Vector.Down;
diff --git a/Obsidian/WorldData/Dimension.cs b/Obsidian/WorldData/Dimension.cs
new file mode 100644
index 000000000..04112eb64
--- /dev/null
+++ b/Obsidian/WorldData/Dimension.cs
@@ -0,0 +1,35 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Obsidian.API.Configuration;
+using Obsidian.API.Registry.Codecs.Dimensions;
+using System.IO;
+
+namespace Obsidian.WorldData;
+
+internal sealed class Dimension(ILogger logger, IPacketBroadcaster packetBroadcaster, IOptionsMonitor configuration,
+ IEventDispatcher eventDispatcher, ILevelGenerator worldGenerator, string name, IWorld world) :
+ AbstractLevel(logger, packetBroadcaster, configuration, eventDispatcher, worldGenerator, name, world.Seed), IDimension
+{
+ public IWorld ParentWorld { get; } = world;
+
+ public override void Initialize(DimensionCodec codec)
+ {
+ this.FolderPath = Path.Combine("worlds", ParentWorld.Name, "dimensions", this.Name.TrimResourceTag(true));
+
+ this.DimensionName = codec.Name;
+
+ this.LevelData = new LevelData
+ {
+ Time = codec.Element.FixedTime ?? 0,
+ DefaultGamemode = Gamemode.Survival,
+ GeneratorName = Generator.Id
+ };
+
+ this.LevelDataFilePath = Path.Combine(this.FolderPath, "level.dat");
+
+ Directory.CreateDirectory(this.FolderPath);
+ }
+
+ public override Task LoadAsync(DimensionCodec codec) => Task.FromResult(false);
+ public override Task SaveAsync() => Task.CompletedTask;
+}
diff --git a/Obsidian/WorldData/Generators/EmptyWorldGenerator.cs b/Obsidian/WorldData/Generators/EmptyWorldGenerator.cs
index 25d08798c..e4d805439 100644
--- a/Obsidian/WorldData/Generators/EmptyWorldGenerator.cs
+++ b/Obsidian/WorldData/Generators/EmptyWorldGenerator.cs
@@ -1,6 +1,8 @@
-namespace Obsidian.WorldData.Generators;
+using Obsidian.API.World;
-public class EmptyWorldGenerator : IWorldGenerator
+namespace Obsidian.WorldData.Generators;
+
+public class EmptyWorldGenerator : ILevelGenerator
{
private static readonly Chunk empty;
private static readonly Chunk spawn;
@@ -56,5 +58,5 @@ public ValueTask GenerateChunkAsync(int x, int z, IChunk? chunk = null,
return x == 0 && z == 0 ? ValueTask.FromResult(spawn.Clone(x, z)) : ValueTask.FromResult(empty.Clone(x, z));
}
- public void Init(IWorld world) { }
+ public void Init(ILevel level) { }
}
diff --git a/Obsidian/WorldData/Generators/GenHelper.cs b/Obsidian/WorldData/Generators/GenHelper.cs
index fd0e16a35..f672f780c 100644
--- a/Obsidian/WorldData/Generators/GenHelper.cs
+++ b/Obsidian/WorldData/Generators/GenHelper.cs
@@ -1,4 +1,5 @@
using Obsidian.WorldData.Generators.Overworld;
+using Obsidian.API.World;
using System.Security.Cryptography;
using System.Text;
@@ -6,17 +7,18 @@ namespace Obsidian.WorldData.Generators;
public class GenHelper
{
- private readonly IWorld world;
+ private readonly ILevel level;
internal int Seed { get; private set; }
internal OverworldTerrainNoise Noise { get; private set; }
- public IWorld World => this.world;
+ public IWorld World { get; }
- public GenHelper(IWorld world)
+ public GenHelper(ILevel world)
{
- this.world = world;
+ this.level = world;
+ this.World = world as IWorld ?? (world as IDimension)?.ParentWorld ?? throw new ArgumentException("Level must be a world or dimension.", nameof(world));
if (!int.TryParse(world.Seed, out int seedHash))
seedHash = BitConverter.ToInt32(MD5.HashData(Encoding.UTF8.GetBytes(world.Seed)));
Seed = seedHash;
@@ -31,15 +33,15 @@ public async ValueTask SetBlockAsync(Vector position, IBlock block, IChunk? chun
}
else
{
- await world.SetBlockUntrackedAsync(position, block, false);
+ await level.SetBlockUntrackedAsync(position, block, false);
}
}
public ValueTask SetBlockAsync(int x, int y, int z, IBlock block, IChunk? chunk) => SetBlockAsync(new Vector(x, y, z), block, chunk);
- public ValueTask SetBlockAsync(int x, int y, int z, IBlock block) => world.SetBlockUntrackedAsync(x, y, z, block, false);
+ public ValueTask SetBlockAsync(int x, int y, int z, IBlock block) => level.SetBlockUntrackedAsync(x, y, z, block, false);
- public ValueTask SetBlockAsync(Vector position, IBlock block) => world.SetBlockUntrackedAsync(position, block, false);
+ public ValueTask SetBlockAsync(Vector position, IBlock block) => level.SetBlockUntrackedAsync(position, block, false);
public async ValueTask GetBlockAsync(Vector position, IChunk? chunk)
{
@@ -47,14 +49,14 @@ public async ValueTask SetBlockAsync(Vector position, IBlock block, IChunk? chun
{
return c.GetBlock(position);
}
- return await world.GetBlockAsync(position);
+ return await level.GetBlockAsync(position);
}
public ValueTask GetBlockAsync(int x, int y, int z, IChunk? chunk) => GetBlockAsync(new Vector(x, y, z), chunk);
- public ValueTask GetBlockAsync(int x, int y, int z) => world.GetBlockAsync(x, y, z);
+ public ValueTask GetBlockAsync(int x, int y, int z) => level.GetBlockAsync(x, y, z);
- public ValueTask GetBlockAsync(Vector position) => world.GetBlockAsync(position);
+ public ValueTask GetBlockAsync(Vector position) => level.GetBlockAsync(position);
public async ValueTask GetWorldHeightAsync(int x, int z, IChunk? chunk)
{
@@ -62,6 +64,6 @@ public async ValueTask SetBlockAsync(Vector position, IBlock block, IChunk? chun
{
return c.Heightmaps[HeightmapType.MotionBlocking].GetHeight(NumericsHelper.Modulo(x, 16), NumericsHelper.Modulo(z, 16));
}
- return await world.GetWorldSurfaceHeightAsync(x, z);
+ return await level.GetWorldSurfaceHeightAsync(x, z);
}
}
diff --git a/Obsidian/WorldData/Generators/IslandGenerator.cs b/Obsidian/WorldData/Generators/IslandGenerator.cs
index 1dc9e566c..347e013fc 100644
--- a/Obsidian/WorldData/Generators/IslandGenerator.cs
+++ b/Obsidian/WorldData/Generators/IslandGenerator.cs
@@ -1,14 +1,15 @@
using Obsidian.API.Registry.Codecs.Biomes;
+using Obsidian.API.World;
using Obsidian.WorldData.Decorators;
using SharpNoise.Modules;
namespace Obsidian.WorldData.Generators;
-public sealed class IslandGenerator : IWorldGenerator
+public sealed class IslandGenerator : ILevelGenerator
{
public string Id => "islands";
private GenHelper? helper;
- private IWorld? world;
+ private ILevel? world;
private Module? noiseGenerator;
private Random? r;
private static readonly BiomeCodec[] biomes = [
@@ -144,7 +145,7 @@ public async ValueTask GenerateChunkAsync(int cx, int cz, IChunk? chunk
return chunk;
}
- public void Init(IWorld world)
+ public void Init(ILevel world)
{
this.world = world;
helper = new GenHelper(world);
diff --git a/Obsidian/WorldData/Generators/Mojang/ChunkBuilder.cs b/Obsidian/WorldData/Generators/Mojang/ChunkBuilder.cs
index febcb7dff..89eed8983 100644
--- a/Obsidian/WorldData/Generators/Mojang/ChunkBuilder.cs
+++ b/Obsidian/WorldData/Generators/Mojang/ChunkBuilder.cs
@@ -1,4 +1,5 @@
using Obsidian.API.Registries;
+using Obsidian.API.World;
using Obsidian.API.World.Generator.Noise;
using System.Security.Cryptography;
using System.Text;
@@ -11,7 +12,7 @@ namespace Obsidian.WorldData.Generators.Mojang;
///
internal class ChunkBuilder
{
- private readonly IWorld world;
+ private readonly ILevel world;
private readonly NoiseSetting settings;
private readonly TerrainGenerator terrainGenerator;
private readonly AquiferSystem aquiferSystem;
@@ -20,7 +21,7 @@ internal class ChunkBuilder
internal int Seed { get; private set; }
- public ChunkBuilder(IWorld world)
+ public ChunkBuilder(ILevel world)
{
this.world = world;
diff --git a/Obsidian/WorldData/Generators/MojangGenerator.cs b/Obsidian/WorldData/Generators/MojangGenerator.cs
index be899a414..ccd5cc801 100644
--- a/Obsidian/WorldData/Generators/MojangGenerator.cs
+++ b/Obsidian/WorldData/Generators/MojangGenerator.cs
@@ -1,15 +1,16 @@
-using Obsidian.WorldData.Generators.Mojang;
+using Obsidian.API.World;
+using Obsidian.WorldData.Generators.Mojang;
namespace Obsidian.WorldData.Generators;
-internal class MojangGenerator : IWorldGenerator
+internal class MojangGenerator : ILevelGenerator
{
public string Id => "minecraft:mojang_generator";
private ChunkBuilder _builder;
- private IWorld _world;
+ private ILevel _world;
public async ValueTask GenerateChunkAsync(int cx, int cz, IChunk? chunk = null, ChunkGenStage stage = ChunkGenStage.full)
{
@@ -89,7 +90,7 @@ public async ValueTask GenerateChunkAsync(int cx, int cz, IChunk? chunk
chunk.SetChunkStatus(ChunkGenStage.full);
return chunk;
}
- public void Init(IWorld world)
+ public void Init(ILevel world)
{
_world = world;
_builder = new ChunkBuilder(world);
diff --git a/Obsidian/WorldData/Generators/OverworldGenerator.cs b/Obsidian/WorldData/Generators/OverworldGenerator.cs
index dbffe068b..84af958fd 100644
--- a/Obsidian/WorldData/Generators/OverworldGenerator.cs
+++ b/Obsidian/WorldData/Generators/OverworldGenerator.cs
@@ -3,10 +3,10 @@
namespace Obsidian.WorldData.Generators;
-public sealed class OverworldGenerator : IWorldGenerator
+public sealed class OverworldGenerator : ILevelGenerator
{
private GenHelper helper;
- private IWorld world;
+ private ILevel world;
public string Id => "overworld";
@@ -66,7 +66,7 @@ public async ValueTask GenerateChunkAsync(int cx, int cz, IChunk? chunk
return chunk;
}
- public void Init(IWorld world)
+ public void Init(ILevel world)
{
this.world = world;
helper = new GenHelper(world);
diff --git a/Obsidian/WorldData/Generators/SuperflatGenerator.cs b/Obsidian/WorldData/Generators/SuperflatGenerator.cs
index 9ba8198b0..cee9dba9f 100644
--- a/Obsidian/WorldData/Generators/SuperflatGenerator.cs
+++ b/Obsidian/WorldData/Generators/SuperflatGenerator.cs
@@ -1,6 +1,6 @@
namespace Obsidian.WorldData.Generators;
-public class SuperflatGenerator : IWorldGenerator
+public class SuperflatGenerator : ILevelGenerator
{
private static readonly Chunk model;
@@ -45,6 +45,6 @@ static SuperflatGenerator()
public ValueTask GenerateChunkAsync(int x, int z, IChunk? chunk = null, ChunkGenStage status = ChunkGenStage.full) =>
chunk is { IsGenerated: true } ? ValueTask.FromResult(chunk) : ValueTask.FromResult(model.Clone(x, z));
- public void Init(IWorld world) { }
+ public void Init(ILevel level) { }
}
diff --git a/Obsidian/WorldData/LevelFactory.cs b/Obsidian/WorldData/LevelFactory.cs
new file mode 100644
index 000000000..511c1ebfe
--- /dev/null
+++ b/Obsidian/WorldData/LevelFactory.cs
@@ -0,0 +1,86 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Obsidian.WorldData.Generators;
+
+namespace Obsidian.WorldData;
+
+public sealed class LevelFactory(ILogger logger, IServiceScopeFactory serviceScopeFactory) : ILevelFactory
+{
+ private static readonly Type[] worldArgTypes = [typeof(ILevelGenerator), typeof(string), typeof(string)];
+ private static readonly Type[] dimensionArgTypes = [typeof(ILevelGenerator), typeof(string), typeof(IWorld)];
+
+ private ObjectFactory worldFactory;
+ private ObjectFactory dimensionFactory;
+
+ private readonly ILogger logger = logger;
+ private readonly IServiceScopeFactory serviceScopeFactory = serviceScopeFactory;
+
+ public Dictionary LevelGenerators { get; } = [];
+
+ public IDimension CreateDimension(IWorld parentWorld, string name, string generatorId)
+ {
+ Type? generatorType;
+ if (!this.LevelGenerators.TryGetValue(generatorId, out generatorType))
+ this.LevelGenerators.TryGetValue("overworld", out generatorType);
+
+ if(generatorType == null)
+ throw new InvalidOperationException($"Failed to find generator with id: {generatorId}");
+
+ using var serviceProviderScope = this.serviceScopeFactory.CreateScope();
+
+ var generator = (ILevelGenerator)ActivatorUtilities.CreateInstance(serviceProviderScope.ServiceProvider, generatorType);
+
+ return this.dimensionFactory.Invoke(serviceProviderScope.ServiceProvider, [generator, name, parentWorld]);
+ }
+
+ public IWorld CreateWorld(string name, string seed, string generatorId)
+ {
+ Type? generatorType;
+ if (!this.LevelGenerators.TryGetValue(generatorId, out generatorType))
+ this.LevelGenerators.TryGetValue("overworld", out generatorType);
+
+ if(generatorType == null)
+ throw new InvalidOperationException($"Failed to find generator with id: {generatorId}");
+
+ using var serviceProviderScope = this.serviceScopeFactory.CreateScope();
+
+ var generator = (ILevelGenerator)ActivatorUtilities.CreateInstance(serviceProviderScope.ServiceProvider, generatorType);
+
+ return this.worldFactory.Invoke(serviceProviderScope.ServiceProvider, [generator, name, seed]);
+ }
+
+ public void Initialize()
+ {
+ this.worldFactory = ActivatorUtilities.CreateFactory(worldArgTypes);
+ this.dimensionFactory = ActivatorUtilities.CreateFactory(dimensionArgTypes);
+
+ this.RegisterDefaults();
+ }
+
+ ///
+ /// Registers the "obsidian-vanilla" entities and objects.
+ ///
+ /// Might be used for more stuff later so I'll leave this here - tides
+ private void RegisterDefaults()
+ {
+ this.RegisterGenerator();
+ this.RegisterGenerator();
+ this.RegisterGenerator();
+ this.RegisterGenerator();
+ this.RegisterGenerator();
+ }
+
+ ///
+ /// Registers new world generator(s) to the server.
+ ///
+ /// A compatible list of entries.
+ public void RegisterGenerator() where T : ILevelGenerator, new()
+ {
+ var gen = new T();
+ if (string.IsNullOrWhiteSpace(gen.Id))
+ throw new InvalidOperationException($"Failed to get id for generator: {gen.Id}");
+
+ if (this.LevelGenerators.TryAdd(gen.Id, typeof(T)))
+ this.logger.LogDebug("Registered {generatorId}...", gen.Id);
+ }
+}
diff --git a/Obsidian/WorldData/Lighting.cs b/Obsidian/WorldData/Lighting.cs
index 32d8d1d90..90375acf5 100644
--- a/Obsidian/WorldData/Lighting.cs
+++ b/Obsidian/WorldData/Lighting.cs
@@ -1,4 +1,6 @@
-namespace Obsidian.WorldData;
+using Obsidian.API.World;
+
+namespace Obsidian.WorldData;
internal static class Lighting
{
@@ -61,7 +63,7 @@ public static void InitialFillSkyLight(IChunk chunk)
}
}
- public static async Task LightFromNeighbors(IChunk chunk, IWorld world)
+ public static async Task LightFromNeighbors(IChunk chunk, ILevel world)
{
foreach (var (dx, dz, dir, edgeX, edgeZ) in FromNeighborOffsets)
{
@@ -79,7 +81,7 @@ public static async Task LightFromNeighbors(IChunk chunk, IWorld world)
}
}
- public static async Task LightToNeighbors(IChunk chunk, IWorld world)
+ public static async Task LightToNeighbors(IChunk chunk, ILevel world)
{
foreach (var (dx, dz, dir, sourceEdgeX, sourceEdgeZ) in ToNeighborOffsets)
{
diff --git a/Obsidian/WorldData/Region.cs b/Obsidian/WorldData/Region.cs
index a0a840298..1a5e295a3 100644
--- a/Obsidian/WorldData/Region.cs
+++ b/Obsidian/WorldData/Region.cs
@@ -1,12 +1,10 @@
-using Microsoft.AspNetCore.Connections.Features;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Obsidian.API.Registry.Codecs.Biomes;
using Obsidian.ChunkData;
using Obsidian.Nbt;
using Obsidian.Utilities.Collections;
using System.Diagnostics;
using System.IO;
-using System.Runtime.CompilerServices;
using System.Threading;
namespace Obsidian.WorldData;
@@ -43,6 +41,8 @@ internal Region(int x, int z, string worldFolderPath, NbtCompression chunkCompre
RegionFolder = Path.Join(worldFolderPath, "regions");
Directory.CreateDirectory(RegionFolder);
var filePath = Path.Join(RegionFolder, $"r.{X}.{Z}.mca");
+
+ logger?.LogInformation("Loading region file {RegionFile} with compression {Compression}", filePath, chunkCompression);
regionFile = new RegionFile(filePath, chunkCompression, CubicRegionSize, logger);
ChunkCompression = chunkCompression;
}
@@ -149,12 +149,12 @@ public async Task BeginTickAsync(CancellationToken cts = default)
}
else
{
- bool updateNeighbor = await bu.World.HandleBlockUpdateAsync(bu);
+ bool updateNeighbor = await bu.Level.HandleBlockUpdateAsync(bu);
if (updateNeighbor) { neighborUpdates.Add(bu); }
}
}
delayed.ForEach(AddBlockUpdate);
- neighborUpdates.ForEach(async u => await u.World.BlockUpdateNeighborsAsync(u));
+ neighborUpdates.ForEach(async u => await u.Level.BlockUpdateNeighborsAsync(u));
}
#region NBT Ops
diff --git a/Obsidian/WorldData/RegionFile.cs b/Obsidian/WorldData/RegionFile.cs
index b95002936..56b90bd5f 100644
--- a/Obsidian/WorldData/RegionFile.cs
+++ b/Obsidian/WorldData/RegionFile.cs
@@ -76,7 +76,7 @@ public async Task InitializeAsync()
{
using var num = new RentedArray(4);
- await this.regionFileStream.ReadAsync(num);
+ await regionFileStream.ReadExactlyAsync(num);
this.Locations[index] = BinaryPrimitives.ReadInt32BigEndian(num);
}
@@ -85,7 +85,7 @@ public async Task InitializeAsync()
{
using var num = new RentedArray(4);
- await this.regionFileStream.ReadAsync(num);
+ await regionFileStream.ReadExactlyAsync(num);
this.Timestamps[index] = BinaryPrimitives.ReadInt32BigEndian(num);
}
diff --git a/Obsidian/WorldData/World.cs b/Obsidian/WorldData/World.cs
index b58219297..bdb98233a 100644
--- a/Obsidian/WorldData/World.cs
+++ b/Obsidian/WorldData/World.cs
@@ -1,403 +1,35 @@
using Microsoft.Extensions.Logging;
-using Obsidian.API;
+using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
-using Obsidian.API.Entities;
using Obsidian.API.Registry.Codecs.Dimensions;
-using Obsidian.Entities;
-using Obsidian.Entities.Factories;
using Obsidian.Nbt;
-using Obsidian.Net.Packets.Play.Clientbound;
-using System.Diagnostics;
using System.IO;
-using System.Threading;
namespace Obsidian.WorldData;
-public sealed partial class World : IWorld
+public sealed class World(ILogger logger, IWorldManager worldManager, IPacketBroadcaster packetBroadcaster, IOptionsMonitor configuration,
+ IEventDispatcher eventDispatcher, ILevelGenerator worldGenerator, string name, string seed) :
+ AbstractLevel(logger, packetBroadcaster, configuration, eventDispatcher, worldGenerator, name, seed), IWorld
{
- private const int SpawnChunkRadius = 12;
+ public IWorldManager WorldManager { get; } = worldManager;
- public IWorldManager WorldManager { get; }
+ internal Dictionary dimensions = [];
- private float rainLevel = 0f;
- private bool initialized = false;
+ public string PlayerDataPath { get; private set; } = string.Empty;
- internal Dictionary dimensions = [];
-
- public Level LevelData { get; internal set; }
-
- public ConcurrentDictionary Players { get; private set; } = [];
-
- public IWorldGenerator Generator { get; internal set; }
-
- public ConcurrentDictionary Regions { get; private set; } = [];
-
- public ConcurrentQueue ChunksToGen { get; private set; } = [];
-
- public long[] SpawnChunks { get; } = new long[SpawnChunkRadius * 48];
-
- public ConcurrentHashSet LoadedChunks { get; private set; } = [];
-
- public required string Name { get; init; }
- public required string Seed { get; init; }
- public string FolderPath { get; private set; }
- public string PlayerDataPath { get; private set; }
- public string LevelDataFilePath { get; private set; }
-
- public bool Loaded { get; private set; }
-
- public long Time
- {
- get => LevelData.Time;
- set
- {
- LevelData.Time = value;
-
- this.BroadcastTime();
- }
- }
-
- public int DayTime
- {
- get => LevelData.DayTime;
- set
- {
- LevelData.DayTime = value;
-
- this.BroadcastTime();
- }
- }
-
- public int RegionCount => this.Regions.Count;
- public int ChunksToGenCount => this.ChunksToGen.Count;
- public int LoadedChunkCount => this.Regions.Values.Sum(x => x.LoadedChunkCount);
-
- public required IPacketBroadcaster PacketBroadcaster { get; init; }
- public required IEventDispatcher EventDispatcher { get; init; }
- public required ServerConfiguration Configuration { get; init; }
-
- public Gamemode DefaultGamemode => LevelData.DefaultGamemode;
-
- public string DimensionName { get; private set; }
-
- public string? ParentWorldName { get; private set; }
-
- private static Semaphore _regionLock;
-
- private ILogger Logger { get; }
-
- internal World(ILogger logger, Type generatorType, IWorldManager worldManager)
- {
- Logger = logger;
- Generator = Activator.CreateInstance(generatorType) as IWorldGenerator ?? throw new ArgumentException("Invalid generator type.", nameof(generatorType));
- _regionLock = new(1, 1);
-
- this.WorldManager = worldManager;
- }
-
- public void InitGenerator() => this.Generator.Init(this);
-
- public ValueTask DestroyEntityAsync(IEntity entity)
- {
- var destroyed = new RemoveEntitiesPacket(entity.EntityId);
-
- this.PacketBroadcaster.QueuePacketToWorld(this, destroyed);
-
- var (chunkX, chunkZ) = entity.Position.ToChunkCoord();
-
- var region = GetRegionForChunk(chunkX, chunkZ);
-
- if (region is null)
- throw new InvalidOperationException("Region is null this wasn't supposed to happen.");
-
- return ValueTask.FromResult(region.Entities.TryRemove(entity.EntityId, out _));
- }
-
- public IRegion? GetRegionForLocation(VectorF location)
- {
- (int chunkX, int chunkZ) = location.ToChunkCoord();
- long key = NumericsHelper.IntsToLong(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
- Regions.TryGetValue(key, out var region);
- return region;
- }
-
- public IRegion? GetRegionForChunk(int chunkX, int chunkZ)
- {
- long value = NumericsHelper.IntsToLong(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
-
- return Regions.TryGetValue(value, out var region) ? region : null;
- }
-
- public IRegion? GetRegionForChunk(Vector location) => GetRegionForChunk(location.X, location.Z);
-
- public async ValueTask GetChunkAsync(int chunkX, int chunkZ, bool scheduleGeneration = true)
- {
- var region = GetRegionForChunk(chunkX, chunkZ) ?? LoadRegion(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift);
-
- if (region is null)
- return null;
-
- var (x, z) = (NumericsHelper.Modulo(chunkX, Region.CubicRegionSize), NumericsHelper.Modulo(chunkZ, Region.CubicRegionSize));
- var packedXZ = NumericsHelper.IntsToLong(chunkX, chunkZ);
-
- var chunk = await region.GetChunkAsync(x, z);
-
- if (chunk is not null)
- {
- if (!chunk.IsGenerated && scheduleGeneration)
- {
- if (!ChunksToGen.Contains(packedXZ))
- ChunksToGen.Enqueue(packedXZ);
- return null;
- }
-
- LoadedChunks.Add(packedXZ);
- return chunk;
- }
-
- // Chunk hasn't been generated yet.
- if (scheduleGeneration)
- {
- if (!ChunksToGen.Contains(packedXZ))
- ChunksToGen.Enqueue(packedXZ);
- return null;
- }
-
- // Create a partial chunk.
- chunk = new Chunk(chunkX, chunkZ, ChunkGenStage.structure_starts);
- region.SetChunk(chunk);
- return chunk;
- }
-
- public async ValueTask GetBlockAsync(int x, int y, int z)
- {
- var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
- return c?.GetBlock(x, y, z);
- }
-
- public async ValueTask GetWorldSurfaceHeightAsync(int x, int z)
- {
- var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
- return c?.Heightmaps[HeightmapType.WorldSurface]
- .GetHeight(NumericsHelper.Modulo(x, 16), NumericsHelper.Modulo(z, 16));
- }
-
- public async ValueTask SetBlockAsync(int x, int y, int z, IBlock block)
- {
- await SetBlockUntrackedAsync(x, y, z, block);
-
- this.BroadcastBlockChange(block, new(x, y, z));
- }
-
- public async ValueTask SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate)
- {
- await SetBlockUntrackedAsync(x, y, z, block, doBlockUpdate);
- this.BroadcastBlockChange(block, new(x, y, z));
- }
-
- //TODO ?????
- private void BroadcastBlockChange(IBlock block, Vector location)
- {
- var packet = new BlockUpdatePacket(location, block.GetHashCode());
- foreach (Player player in PlayersInRange(location).Cast())
- {
- player.Client.SendPacket(packet);
- }
- }
-
- public IEnumerable PlayersInRange(Vector location)
- {
- var (x, z) = location.ToChunkCoord();
- var packedXZ = NumericsHelper.IntsToLong(x, z);
-
- return this.Players.Values.Where(player => player.LoadedChunks.Contains(packedXZ));
- }
-
- public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false) => SetBlockUntrackedAsync(location.X, location.Y, location.Z, block, doBlockUpdate);
-
- public async ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false)
- {
- if (doBlockUpdate)
- {
- await ScheduleBlockUpdateAsync(new BlockUpdate(this, new Vector(x, y, z), block));
- await BlockUpdateNeighborsAsync(new BlockUpdate(this, new Vector(x, y, z), block));
- }
- var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
- c?.SetBlock(x, y, z, block);
- }
-
- public IEnumerable GetEntitiesInRange(VectorF location, float distance = 10f)
- {
- foreach (IPlayer player in GetPlayersInRange(location, distance))
- {
- yield return player;
- }
-
- foreach (IEntity entity in GetNonPlayerEntitiesInRange(location, distance))
- {
- yield return entity;
- }
- }
-
- public IEnumerable GetNonPlayerEntitiesInRange(VectorF location, float distance)
- {
- if (float.IsNaN(distance) || distance < 0f)
- {
- yield break;
- }
-
- // Get corner chunk coordinates
- (int left, int top) = (location - new VectorF(distance)).ToChunkCoord();
- (int right, int bottom) = (location + new VectorF(distance)).ToChunkCoord();
-
- distance *= distance; // distance^2 <= deltaX^2 + deltaY^2
-
- // Iterate over chunks, taking one from each region, then getting the region itself
- for (int x = left; x <= right; x += Region.CubicRegionSize)
- {
- for (int z = top; z >= bottom; z -= Region.CubicRegionSize)
- {
- if (GetRegionForChunk(x, z) is not Region region)
- continue;
-
- // Return entities in range
- foreach (var entity in region.Entities.Values)
- {
- if (entity.Type == EntityType.Player)
- continue;
-
- var locationDifference = LocationDiff.GetDifference(entity.Position, location);
-
- if (locationDifference.CalculatedDifference <= distance)
- {
- yield return entity;
- }
- }
- }
- }
- }
-
- public IEnumerable GetPlayersInRange(VectorF location, float distance)
- {
- if (float.IsNaN(distance) || distance < 0f)
- {
- yield break;
- }
-
- if (distance == 0f)
- {
- foreach (var player in Players.Values)
- {
- if (player.Position == location)
- {
- yield return player;
- }
- }
- yield break;
- }
-
- distance *= distance; // distance^2 <= deltaX^2 + deltaY^2
-
- foreach (var player in Players.Values)
- {
- var locationDifference = LocationDiff.GetDifference(player.Position, location);
-
- if (locationDifference.CalculatedDifference <= distance)
- {
- yield return player;
- }
- }
- }
-
- public IEnumerable GetPlayersInChunkRange(Vector worldPosition)
- {
- var (x, z) = worldPosition.ToChunkCoord();
-
- var packedXZ = NumericsHelper.IntsToLong(x, z);
- return this.Players.Values.Where(player => player.LoadedChunks.Contains(packedXZ));
- }
-
- public bool TryAddPlayer(IPlayer player) => Players.TryAdd(player.Uuid, player);
-
- public bool TryRemovePlayer(IPlayer player) => Players.TryRemove(player.Uuid, out _);
-
- ///
- /// Method that handles world-specific tick behavior.
- ///
- ///
- public async Task DoWorldTickAsync()
- {
- if (LevelData is null)
- return;
-
- LevelData.Time += this.Configuration.TimeTickSpeedMultiplier;
- LevelData.RainTime -= this.Configuration.TimeTickSpeedMultiplier;
-
- // if (LevelData.RainTime < 1)
- // {
- // // Raintime passed, toggle weather
- // LevelData.Raining = !LevelData.Raining;
-
- // int rainTime;
- // // amount of ticks in a day is 24000
- // if (LevelData.Raining)
- // {
- // rainTime = Globals.Random.Next(12000, 24000); // rain lasts 0.5 - 1 day
- // }
- // else
- // {
- // rainTime = Globals.Random.Next(12000, 180000); // clear lasts 0.5 - 7.5 day
- // }
- // LevelData.RainTime = rainTime;
-
- // Logger.LogInformation("Toggled rain: {raining} for {rainTime} ticks.", LevelData.Raining, LevelData.RainTime);
- // }
-
- // // Gradually increase and decrease rain levels based on
- // // whether value is in range and what weather is active
- // var oldLevel = rainLevel;
- // if (!LevelData.Raining && rainLevel > 0f)
- // rainLevel -= 0.01f;
- // else if (LevelData.Raining && rainLevel < 1f)
- // rainLevel += 0.01f;
-
- // if (oldLevel != rainLevel)
- // {
- // // send new level if updated
- // this.PacketBroadcaster.QueuePacketToWorld(this, new GameEventPacket(ChangeGameStateReason.RainLevelChange, rainLevel));
- // if (rainLevel < 0.3f && rainLevel > 0.1f)
- // this.PacketBroadcaster.QueuePacketToWorld(this, new GameEventPacket(LevelData.Raining ? ChangeGameStateReason.BeginRaining : ChangeGameStateReason.EndRaining));
- // }
-
- // if (LevelData.Time % (20 * this.Configuration.TimeTickSpeedMultiplier) == 0)
- // {
- // // Update client time every second / 20 ticks
- // this.BroadcastTime();
- // }
-
- //Tick regions within the world manager
- await Task.WhenAll(this.Regions.Values.Select(r => r.BeginTickAsync()));
- }
-
- #region world loading/saving
-
- public async Task LoadAsync(DimensionCodec codec)
+ public async override Task LoadAsync(DimensionCodec codec)
{
- DimensionName = codec.Name;
-
- Init(codec);
-
- var dataPath = Path.Combine("worlds", ParentWorldName ?? Name, "level.dat");
-
- var fi = new FileInfo(dataPath);
+ this.Initialize(codec);
+ var fi = new FileInfo(this.LevelDataFilePath);
if (!fi.Exists)
return false;
- var reader = new NbtReader(fi.OpenRead(), NbtCompression.GZip);
+ await using var fs = fi.OpenRead();
+ var reader = new NbtReader(fs, NbtCompression.GZip);
var levelCompound = (reader.ReadNextTag() as NbtCompound)!;
- LevelData = new Level()
+ LevelData = new LevelData()
{
Hardcore = levelCompound.GetBool("hardcore"),
MapFeatures = levelCompound.GetBool("MapFeatures"),
@@ -416,39 +48,28 @@ public async Task LoadAsync(DimensionCodec codec)
LevelName = levelCompound.GetString("LevelName")
};
- //if (levelCompound.TryGetTag("Version", out var tag))
- // LevelData.VersionData = tag as NbtCompound;
-
Logger.LogInformation("Loading spawn chunks into memory...");
for (int rx = -1; rx < 1; rx++)
for (int rz = -1; rz < 1; rz++)
LoadRegion(rx, rz);
var (x, z) = LevelData.SpawnPosition.ToChunkCoord();
- var spawnChunks = new List();
var index = 0;
- for (var cx = x - SpawnChunkRadius; cx < x + SpawnChunkRadius; cx++)
- for (var cz = z - SpawnChunkRadius; cz < z + SpawnChunkRadius; cz++)
- {
+ for (var cx = x - this.Configuration.SpawnChunkRadius; cx < x + this.Configuration.SpawnChunkRadius; cx++)
+ for (var cz = z - this.Configuration.SpawnChunkRadius; cz < z + this.Configuration.SpawnChunkRadius; cz++)
SpawnChunks[index++] = NumericsHelper.IntsToLong(cx, cz);
- }
- await Parallel.ForEachAsync(SpawnChunks, async (c, cts) =>
+ await Parallel.ForEachAsync(SpawnChunks, async (c, _) =>
{
NumericsHelper.LongToInts(c, out var cx, out var cz);
await GetChunkAsync(cx, cz);
- //// Update status occasionally so we're not destroying consoleio
- //// Removing this for now
- //if (c.X % 5 == 0)
- // Server.UpdateStatusConsole();
});
Loaded = true;
return true;
}
- //TODO save world generator settings properly
- public async Task SaveAsync()
+ public override async Task SaveAsync()
{
var worldFile = new FileInfo(LevelDataFilePath);
@@ -465,7 +86,6 @@ public async Task SaveAsync()
writer.WriteBool("MapFeatures", LevelData.MapFeatures);
writer.WriteBool("raining", LevelData.Raining);
writer.WriteBool("thundering", LevelData.Thundering);
-
writer.WriteInt("GameType", (int)LevelData.DefaultGamemode);
writer.WriteInt("generatorVersion", LevelData.GeneratorVersion);
writer.WriteInt("rainTime", LevelData.RainTime);
@@ -474,16 +94,11 @@ public async Task SaveAsync()
writer.WriteInt("SpawnZ", (int)LevelData.SpawnPosition.Z);
writer.WriteInt("thunderTime", LevelData.ThunderTime);
writer.WriteInt("version", LevelData.Version);
-
writer.WriteLong("LastPlayed", DateTimeOffset.Now.ToUnixTimeMilliseconds());
writer.WriteLong("RandomSeed", LevelData.RandomSeed);
writer.WriteLong("Time", Time);
-
- //this.WriteWorldGenSettings(writer);
-
writer.WriteString("generatorName", Generator.Id);
writer.WriteString("LevelName", Name);
-
writer.EndCompound();
await writer.TryFinishAsync();
@@ -492,407 +107,41 @@ public async Task SaveAsync()
public async Task UnloadPlayerAsync(Guid uuid)
{
Players.TryRemove(uuid, out var player);
-
await player.SaveAsync();
}
- #endregion
-
- public IRegion LoadRegionByChunk(int chunkX, int chunkZ)
- {
- int regionX = chunkX >> Region.CubicRegionSizeShift, regionZ = chunkZ >> Region.CubicRegionSizeShift;
- return LoadRegion(regionX, regionZ);
- }
-
- public IRegion LoadRegion(int regionX, int regionZ)
- {
- _regionLock.WaitOne();
- long value = NumericsHelper.IntsToLong(regionX, regionZ);
-
- if (Regions.TryGetValue(value, out var region))
- {
- _regionLock.Release();
- return region;
- }
-
- this.Logger.LogDebug("Trying to add {x}:{z}", regionX, regionZ);
-
- region = new Region(regionX, regionZ, FolderPath);
-
- if (Regions.TryAdd(value, region))
- this.Logger.LogDebug("Added region {x}:{z}", regionX, regionZ);
-
- //DOesn't need to be blocking
- _ = region.InitAsync();
-
- _regionLock.Release();
- return region;
- }
-
- public async Task UnloadRegionAsync(int regionX, int regionZ)
- {
- long value = NumericsHelper.IntsToLong(regionX, regionZ);
- if (Regions.TryRemove(value, out var r))
- await r.FlushAsync();
- }
-
- public async ValueTask ScheduleBlockUpdateAsync(IBlockUpdate blockUpdate)
- {
- blockUpdate.Block ??= await GetBlockAsync(blockUpdate.Position);
- (int chunkX, int chunkZ) = blockUpdate.Position.ToChunkCoord();
- var region = GetRegionForChunk(chunkX, chunkZ);
- region?.AddBlockUpdate(blockUpdate);
- }
-
-
- public async Task ManageChunksAsync()
- {
- // Check for chunks to unload every 30 seconds
- if (LevelData.Time > 0 && LevelData.Time % (20 * 30) == 0)
- {
- var chunksToKeep = new List();
- Players.Values.ForEach(p =>
- {
- chunksToKeep.AddRange(p.LoadedChunks);
- });
- //TODO: Task.WhenAll for the slow IO ops
- LoadedChunks.Except(chunksToKeep).Except(SpawnChunks).ForEach(async c =>
- {
- if (LoadedChunks.TryRemove(c))
- {
- NumericsHelper.LongToInts(c, out var cx, out var cz);
- var r = GetRegionForChunk(cx, cz);
- await r.UnloadChunk(cx, cz);
- }
- });
- }
-
- if (ChunksToGen.IsEmpty) { return; }
-
- // Pull some jobs out of the queue
- var jobs = new List();
- for (int a = 0; a < Environment.ProcessorCount; a++)
- {
- if (ChunksToGen.TryDequeue(out var job))
- jobs.Add(job);
- }
-
- await Parallel.ForEachAsync(jobs, async (job, _) =>
- {
- NumericsHelper.LongToInts(job, out var jobX, out var jobZ);
- var region = GetRegionForChunk(jobX, jobZ) ?? LoadRegionByChunk(jobX, jobZ);
-
- var (x, z) = (NumericsHelper.Modulo(jobX, Region.CubicRegionSize), NumericsHelper.Modulo(jobZ, Region.CubicRegionSize));
-
- var c = await region.GetChunkAsync(x, z);
- if (c is null)
- {
- c = new Chunk(jobX, jobZ, ChunkGenStage.structure_starts);
- // Set chunk now so that it no longer comes back as null. #threadlyfe
- region.SetChunk(c);
- }
- if (!c.IsGenerated)
- {
- c = await Generator.GenerateChunkAsync(jobX, jobZ, c);
- }
- region.SetChunk(c);
- });
- }
-
- public Task FlushRegionsAsync() => Task.WhenAll(Regions.Select(pair => pair.Value.FlushAsync()));
-
- public IEntity SpawnFallingBlock(VectorF position, Material mat)
- {
- // offset position so it spawns in the right spot
- position.X += 0.5f;
- position.Z += 0.5f;
-
- FallingBlock entity = new(position)
- {
- Type = EntityType.FallingBlock,
- EntityId = Server.GetNextEntityId(),
- World = this,
- Block = BlocksRegistry.Get(mat),
- };
-
- entity.SpawnEntity(null, entity.Block.GetHashCode());
-
- TryAddEntity(entity);
-
- return entity;
- }
-
- public ValueTask GetBlockEntityAsync(Vector blockPosition) => GetBlockEntityAsync(blockPosition.X, blockPosition.Y, blockPosition.Z);
-
- public async ValueTask GetBlockEntityAsync(int x, int y, int z)
- {
- var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
- return c?.GetBlockEntity(x, y, z);
- }
-
- public ValueTask SetBlockEntity(Vector blockPosition, IBlockEntity tileEntityData) => SetBlockEntity(blockPosition.X, blockPosition.Y, blockPosition.Z, tileEntityData);
- public async ValueTask SetBlockEntity(int x, int y, int z, IBlockEntity tileEntityData)
- {
- var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false);
- c?.SetBlockEntity(x, y, z, tileEntityData);
- }
-
- public IEntity SpawnEntity(VectorF position, EntityType type)
- {
- if (type == EntityType.ExperienceOrb)
- throw new NotImplementedException($"EntityType {type} is not supported.");
-
- if (type == EntityType.FallingBlock)
- return SpawnFallingBlock(position + (0, 20, 0), Material.Sand);
-
- return GetNewEntitySpawner()
- .WithEntityType(type)
- .AtPosition(position)
- .Spawn();
- }
- public IEntity SpawnEntity(IEntity entity)
- {
- entity.SpawnEntity();
- TryAddEntity(entity as Entity);
- return entity;
- }
-
- public void RegisterDimension(DimensionCodec codec, string? worldGeneratorId = null)
+ public void RegisterDimension(DimensionCodec codec, IDimension dimension)
{
if (dimensions.ContainsKey(codec.Name))
throw new ArgumentException($"World already contains dimension with name: {codec.Name}");
- if (!this.WorldManager.WorldGenerators.TryGetValue(worldGeneratorId ?? codec.Name.TrimResourceTag(true), out var generatorType))
- throw new ArgumentException($"Failed to find generator with id: {worldGeneratorId}.");
-
- //TODO CREATE NEW TYPE CALLED DIMENSION AND IDIMENSION
- var dimensionWorld = new World(this.Logger, generatorType, this.WorldManager)
- {
- PacketBroadcaster = this.PacketBroadcaster,
- EventDispatcher = this.EventDispatcher,
- Configuration = this.Configuration,
- Name = codec.Name.TrimResourceTag(true),
- Seed = this.Seed
- };
-
- dimensionWorld.Init(codec, Name);
-
- dimensions.Add(codec.Name, dimensionWorld);
+ dimensions.Add(codec.Name, dimension);
}
- public void SpawnExperienceOrbs(VectorF position, short count = 1)
+ public override void Initialize(DimensionCodec codec)
{
- // this.PacketBroadcaster.QueuePacketToWorld(this, new AddExperienceOrbPacket(count, position));
- }
-
- ///
- ///
- ///
- ///
- /// Whether to update neighbor blocks.
- public async ValueTask HandleBlockUpdateAsync(IBlockUpdate update)
- {
- if (update.Block is not IBlock block)
- return false;
-
- // Todo: this better
- if (TagsRegistry.Block.GravityAffected.Entries.Contains(block.RegistryId))
- return await BlockUpdates.HandleFallingBlock(update);
+ this.FolderPath = Path.Combine("worlds", Name);
- if (block.IsLiquid)
- return await BlockUpdates.HandleLiquidPhysicsAsync(update);
-
- return false;
- }
-
- public async ValueTask BlockUpdateNeighborsAsync(IBlockUpdate update)
- {
- update.Block = null;
+ this.DimensionName = codec.Name;
-
- Vector[] directions = Vector.AllDirections;
- for (int i = 0; i < directions.Length; i++)
- {
- update.Position = update.Position + directions[i];
-
- await ScheduleBlockUpdateAsync(update);
- }
- }
-
- ///
- /// Initilizes properties, creates directories and registers default dimensions if necessary.
- ///
- /// The dimension this world will represent.
- /// The parent world name of this world(dimension). Only used if this world is a child dimension of another world.
- /// Thrown when no valid dimensions were found with the supplied .
- /// This method should be called before and not after trying to generate the world.
- internal void Init(DimensionCodec codec, string? parentWorldName = null)
- {
- // Make sure we set the right paths
- if (string.IsNullOrWhiteSpace(parentWorldName))
- {
- FolderPath = Path.Combine("worlds", Name);
- LevelDataFilePath = Path.Combine(FolderPath, "level.dat");
- PlayerDataPath = Path.Combine(FolderPath, "playerdata");
- }
- else
- {
- FolderPath = Path.Combine("worlds", parentWorldName, Name);
- LevelDataFilePath = Path.Combine("worlds", parentWorldName, "level.dat");
- PlayerDataPath = Path.Combine("worlds", parentWorldName, "playerdata");
- }
-
- ParentWorldName = parentWorldName;
- DimensionName = codec.Name;
-
- //TODO configure all dim data
- LevelData = new Level
+ this.LevelData = new LevelData
{
Time = codec.Element.FixedTime ?? 0,
DefaultGamemode = Gamemode.Survival,
GeneratorName = Generator.Id
};
- Directory.CreateDirectory(FolderPath);
- Directory.CreateDirectory(PlayerDataPath);
-
- initialized = true;
- }
-
- internal async Task GenerateWorldAsync(bool setWorldSpawn = false)
- {
- if (!initialized)
- throw new InvalidOperationException("World hasn't been initialized please call World.Init() before trying to generate the world.");
-
- Logger.LogInformation("Generating world... (Config pregeneration size is {pregenRange})", this.Configuration.PregenerateChunkRange);
- int pregenerationRange = this.Configuration.PregenerateChunkRange;
-
- int regionPregenRange = (pregenerationRange >> Region.CubicRegionSizeShift) + 1;
-
- await Parallel.ForEachAsync(Enumerable.Range(-regionPregenRange, regionPregenRange * 2 + 1), (x, _) =>
- {
- for (int z = -regionPregenRange; z < regionPregenRange; z++)
- LoadRegion(x, z);
-
- return ValueTask.CompletedTask;
- });
-
- for (int x = -pregenerationRange; x < pregenerationRange; x++)
- {
- for (int z = -pregenerationRange; z < pregenerationRange; z++)
- {
- ChunksToGen.Enqueue(NumericsHelper.IntsToLong(x, z));
- }
- }
-
- float startChunks = ChunksToGenCount;
- var stopwatch = new Stopwatch();
- stopwatch.Start();
- Logger.LogInformation("{startChunks} chunks to generate...", startChunks);
- while (!ChunksToGen.IsEmpty)
- {
- await ManageChunksAsync();
- var pctComplete = (int)((1.0 - ChunksToGenCount / startChunks) * 100);
- var completedChunks = startChunks - ChunksToGenCount;
- var cps = completedChunks / (stopwatch.ElapsedMilliseconds / 1000.0);
- int remain = ChunksToGenCount / (int)Math.Max(cps, 1);
- Console.Write("\r{0} chunks/second - {1}% complete - {2} seconds remaining ", cps.ToString("###.00"), pctComplete, remain);
- if (completedChunks % 1024 == 0)
- { // For Jon when he's doing large world gens
- await FlushRegionsAsync();
- }
- }
- Console.WriteLine();
- await FlushRegionsAsync();
+ this.PlayerDataPath = Path.Combine(this.FolderPath, "playerdata");
+ this.LevelDataFilePath = Path.Combine(this.FolderPath, "level.dat");
- if (setWorldSpawn)
- {
- await SetWorldSpawnAsync();
- var index = 0;
- var (x, z) = LevelData.SpawnPosition.ToChunkCoord();
- var spawnChunks = new List();
- for (var cx = x - SpawnChunkRadius; cx < x + SpawnChunkRadius; cx++)
- for (var cz = z - SpawnChunkRadius; cz < z + SpawnChunkRadius; cz++)
- SpawnChunks[index++] = NumericsHelper.IntsToLong(cx, cz);
- }
+ Directory.CreateDirectory(this.PlayerDataPath);
+ Directory.CreateDirectory(this.FolderPath);
}
- internal async Task SetWorldSpawnAsync()
+ public async override Task DoWorldTickAsync()
{
- if (LevelData.SpawnPosition.Y != 0) { return; }
-
- var pregenRange = this.Configuration.PregenerateChunkRange;
- var region = GetRegionForLocation(VectorF.Zero)!;
- foreach (var chunk in region.GeneratedChunks())
- {
- for (int bx = 0; bx < 16; bx++)
- {
- for (int bz = 0; bz < 16; bz++)
- {
- // Get topmost block
- var by = chunk.Heightmaps[HeightmapType.MotionBlocking].GetHeight(bx, bz);
- IBlock block = chunk.GetBlock(bx, by, bz);
-
- // Block must be high enough and either grass or sand
- if (by < 64 || !block.Is(Material.GrassBlock) && !block.Is(Material.Sand))
- {
- continue;
- }
-
- // Block must have enough empty space above for player to spawn in
- if (!chunk.GetBlock(bx, by + 1, bz).IsAir || !chunk.GetBlock(bx, by + 2, bz).IsAir)
- {
- continue;
- }
+ await base.DoWorldTickAsync();
- var worldPos = new VectorF(bx + 0.5f + (chunk.X * 16), by + 1, bz + 0.5f + (chunk.Z * 16));
- LevelData.SpawnPosition = worldPos;
- Logger.LogInformation("World Spawn set to {worldPos}", worldPos);
-
- // Should spawn be far from (0,0), queue up chunks in generation range.
- // Just feign a request for a chunk and if it doesn't exist, it'll get queued for gen.
- for (int x = chunk.X - pregenRange; x < chunk.X + pregenRange; x++)
- {
- for (int z = chunk.Z - pregenRange; z < chunk.Z + pregenRange; z++)
- {
- await GetChunkAsync(x, z);
- }
- }
-
- return;
- }
- }
- }
- Logger.LogWarning("Failed to set World Spawn.");
- }
-
- public bool TryAddEntity(IEntity entity)
- {
- var (chunkX, chunkZ) = entity.Position.ToChunkCoord();
-
- var region = GetRegionForChunk(chunkX, chunkZ);
-
- if (region is null)
- throw new InvalidOperationException("Region is null, this wasn't supposed to happen.");
-
- return region.Entities.TryAdd(entity.EntityId, entity);
+ await Task.WhenAll(this.dimensions.Values.Select(d => d.DoWorldTickAsync()));
}
-
- private void BroadcastTime() => this.PacketBroadcaster.QueuePacketToWorld(this, new SetTimePacket(LevelData.Time, LevelData.Time % 24000, true));
-
- public async ValueTask DisposeAsync()
- {
- foreach (var region in Regions.Values)
- {
- await region.DisposeAsync();
- }
- }
-
- public IEntitySpawner GetNewEntitySpawner() => new EntitySpawner(this);
- public ValueTask GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true) =>
- this.GetChunkAsync(worldLocation.X, worldLocation.Z, scheduleGeneration);
-
- public ValueTask GetBlockAsync(Vector location) => this.GetBlockAsync(location.X, location.Y, location.Z);
- public ValueTask SetBlockAsync(Vector location, IBlock block) => this.SetBlockAsync(location.X, location.Y, location.Z, block);
- public ValueTask SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate) => this.SetBlockAsync(location.X, location.Y, location.Z, block, doBlockUpdate);
}
diff --git a/Obsidian/WorldData/WorldManager.cs b/Obsidian/WorldData/WorldManager.cs
index 06f887119..eba54891a 100644
--- a/Obsidian/WorldData/WorldManager.cs
+++ b/Obsidian/WorldData/WorldManager.cs
@@ -4,7 +4,6 @@
using Microsoft.Extensions.Options;
using Obsidian.API.Configuration;
using Obsidian.Hosting;
-using Obsidian.WorldData.Generators;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
@@ -12,15 +11,14 @@
namespace Obsidian.WorldData;
-public sealed class WorldManager(ILoggerFactory loggerFactory, IServiceProvider serviceProvider, IOptionsMonitor configuration,
- IServerEnvironment serverEnvironment) : BackgroundService, IWorldManager
+public sealed class WorldManager(ILogger logger, IServiceProvider serviceProvider, IOptionsMonitor configuration,
+ IServerEnvironment serverEnvironment, ILevelFactory levelFactory) : BackgroundService, IWorldManager
{
- private readonly ILogger logger = loggerFactory.CreateLogger();
- private readonly Dictionary worlds = new();
- private readonly ILoggerFactory loggerFactory = loggerFactory;
- private readonly IServiceProvider serviceProvider = serviceProvider;
+ private readonly ILogger logger = logger;
+ private readonly Dictionary worlds = [];
private readonly IOptionsMonitor configuration = configuration;
private readonly IServerEnvironment serverEnvironment = serverEnvironment;
+ private readonly ILevelFactory levelFactory = levelFactory;
private readonly IServiceScope serviceScope = serviceProvider.CreateScope();
public bool ReadyToJoin { get; private set; }
@@ -31,20 +29,15 @@ public sealed class WorldManager(ILoggerFactory loggerFactory, IServiceProvider
public IWorld DefaultWorld { get; private set; } = default!;
- public Dictionary WorldGenerators { get; } = new();
-
protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
var timer = new BalancingTimer(20, stoppingToken);
try
{
- this.RegisterDefaults();
- // TODO: This should defenitly accept a cancellation token.
- // If Cancel is called, this method should stop within the configured timeout, otherwise code execution will simply stop here,
- // and server shutdown will not be handled correctly.
- // Load worlds on startup.
- await this.LoadWorldsAsync();
+ this.levelFactory.Initialize();
+
+ await this.LoadWorldsAsync(stoppingToken);
while (await timer.WaitForNextTickAsync())
{
@@ -58,43 +51,12 @@ protected async override Task ExecuteAsync(CancellationToken stoppingToken)
}
- ///
- /// Registers new world generator(s) to the server.
- ///
- /// A compatible list of entries.
- public void RegisterGenerator() where T : IWorldGenerator, new()
- {
- var gen = new T();
- if (string.IsNullOrWhiteSpace(gen.Id))
- throw new InvalidOperationException($"Failed to get id for generator: {gen.Id}");
-
- if (this.WorldGenerators.TryAdd(gen.Id, typeof(T)))
- this.logger.LogDebug("Registered {generatorId}...", gen.Id);
- }
-
- public async Task LoadWorldsAsync()
+ public async Task LoadWorldsAsync(CancellationToken cancellationToken = default)
{
- var worlds = await LoadServerWorldsAsync();
+ var worlds = await LoadServerWorldsAsync(cancellationToken);
foreach (var serverWorld in worlds)
{
- //var server = (Server)this.server;
- if (!this.WorldGenerators.TryGetValue(serverWorld.Generator, out var generatorType))
- {
- this.logger.LogError("Unknown generator type {generator} for world {worldName}", serverWorld.Generator, serverWorld.Name);
- continue;
- }
-
- //TODO fix
- var world = new World(this.loggerFactory.CreateLogger($"World [{serverWorld.Name}]"), generatorType, this)
- {
- Configuration = this.configuration.CurrentValue,
- PacketBroadcaster = this.serviceScope.ServiceProvider.GetRequiredService(),
- EventDispatcher = this.serviceScope.ServiceProvider.GetRequiredService(),
- Name = serverWorld.Name,
- Seed = serverWorld.Seed
- };
-
- world.InitGenerator();
+ var world = this.levelFactory.CreateWorld(serverWorld.Name, serverWorld.Seed, serverWorld.Generator);
this.worlds.Add(world.Name, world);
@@ -105,9 +67,6 @@ public async Task LoadWorldsAsync()
{
this.logger.LogInformation("Creating new world: {worldName}...", serverWorld.Name);
- world.Init(defaultCodec);
-
- //TODO maybe make a method that takes in params
foreach (var dimensionName in serverWorld.ChildDimensions)
{
if (!CodecRegistry.TryGetDimension(dimensionName, out var codec))
@@ -116,13 +75,16 @@ public async Task LoadWorldsAsync()
continue;
}
- // Don't have any dimension generators yet so we'll just stick with overworld
- // TODO create dimension generators
- world.RegisterDimension(codec, "overworld");
- }
+ var dimension = this.levelFactory.CreateDimension(world, codec.Name, dimensionName);
- await world.GenerateWorldAsync(true);
+ dimension.Initialize(codec);
+ world.RegisterDimension(codec, dimension);
+ await dimension.GenerateAsync();
+ await dimension.SaveAsync();
+ }
+
+ await world.GenerateAsync();
await world.SaveAsync();
}
@@ -151,8 +113,8 @@ public bool TryGetWorld(string name, [NotNullWhen(true)] out TWorld? wor
return false;
}
- public Task TickWorldsAsync() => Task.WhenAll(this.worlds.Select(pair => pair.Value.DoWorldTickAsync()));
- public Task FlushLoadedWorldsAsync() => Task.WhenAll(this.worlds.Select(pair => pair.Value.FlushRegionsAsync()));
+ public Task TickWorldsAsync() => Task.WhenAll(this.worlds.Values.Select(world => world.DoWorldTickAsync()));
+ public Task FlushLoadedWorldsAsync() => Task.WhenAll(this.worlds.Values.Select(world => world.FlushRegionsAsync()));
public async ValueTask DisposeAsync()
{
@@ -166,27 +128,14 @@ public async ValueTask DisposeAsync()
this.Dispose();
}
- ///
- /// Registers the "obsidian-vanilla" entities and objects.
- ///
- /// Might be used for more stuff later so I'll leave this here - tides
- private void RegisterDefaults()
- {
- this.RegisterGenerator();
- this.RegisterGenerator();
- this.RegisterGenerator();
- this.RegisterGenerator();
- this.RegisterGenerator();
- }
-
- private static async Task> LoadServerWorldsAsync()
+ private static async Task> LoadServerWorldsAsync(CancellationToken cancellationToken = default)
{
var worldsFile = new FileInfo(Path.Combine("config", "worlds.json"));
if (worldsFile.Exists)
{
await using var worldsFileStream = worldsFile.OpenRead();
- return await worldsFileStream.FromJsonAsync>()
+ return await worldsFileStream.FromJsonAsync>(cancellationToken: cancellationToken)
?? throw new Exception("A worlds file does exist, but is invalid. Is it corrupt?");
}
@@ -204,7 +153,7 @@ private static async Task> LoadServerWorldsAsync()
};
await using var fileStream = worldsFile.Create();
- await worlds.ToJsonAsync(fileStream);
+ await worlds.ToJsonAsync(fileStream, cancellationToken: cancellationToken);
return worlds;
}
diff --git a/README.md b/README.md
index 66f4e8169..8bfb78f81 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Easy, isn't it?
You can now run Obsidian using Docker! As of right now, no image is available on DockerHub yet, but it will be sometime soon.
For now, to run Obsidian on Docker you will have to follow the following steps:
-1. Clone Obsidian `git clone https://github.com/ObsidianMC/Obsidian.git`
+1. Clone Obsidian `git clone --recurse-submodules https://github.com/ObsidianMC/Obsidian.git`
2. Go to Obsidian's cloned directory `cd Obsidian`
3. Build the docker image `docker build . -t obsidian`
4. Run the container `docker run -d -p YOUR_HOST_PORT:25565 -v YOUR_SERVERFILES_PATH:/files --name YOUR_CONTAINER_NAME obsidian`
@@ -55,7 +55,7 @@ For now, to run Obsidian on Docker you will have to follow the following steps:
### Docker Compose
There's also docker-compose support.
-1. Clone Obsidian `git clone https://github.com/ObsidianMC/Obsidian.git`
+1. Clone Obsidian `git clone --recurse-submodules https://github.com/ObsidianMC/Obsidian.git`
2. Go to Obsidian's cloned directory `cd Obsidian`
3. Run `docker-compose up -V` to generate the `config.json`
4. Edit your `docker-compose.yml` file, along with `files/config.json`