Skip to content

WorldGen API v4#514

Open
Tides wants to merge 17 commits into
1.21.xfrom
worldgen-api-v4
Open

WorldGen API v4#514
Tides wants to merge 17 commits into
1.21.xfrom
worldgen-api-v4

Conversation

@Tides

@Tides Tides commented Apr 6, 2026

Copy link
Copy Markdown
Member

Introduces a major refactor of the world and dimension system in the API, replacing the previous IWorld-centric model with a new ILevel abstraction.

The most important changes are:

World/Level Abstraction Refactor

  • Introduced the new ILevel interface to represent a game level (world or dimension), replacing many usages of IWorld. Updated related interfaces and classes to use ILevel instead of IWOrld.

  • Added IDimension interface extending ILevel, with a reference to its parent IWorld.

  • Updated IPacketBroadcaster methods to use ILevel instead of IWorld and renamed parameters accordingly.

World and Dimension Management

  • Added ILevelFactory interface for creating worlds and dimensions, registering generators, and initialization logic.

  • Updated and renamed the world generator interface to ILevelGenerator (from IWorldGenerator), with methods now taking ILevel instead of IWorld.

  • Added ILevelManager property to the IServer interface for managing worlds.

Dimension and World Generation Settings

  • These settings are just following what I've seen in the level.dat file for mojang/modded mc world saves, I won't document much on these as its subject to change

These changes lay the groundwork for more robust handling of multiple dimensions and advanced world generation, and should make future extensions and maintenance easier~

I'm not quite done with these changes yet but I should have something ready soon ^ ^

Summary by CodeRabbit

  • New Features
    • Added configurable spawn chunk radius for world generation.
    • Extended world/dimension generation configuration with richer biome/dimension generator settings.
    • Improved repository submodule fetching for builds and container runs.
  • Bug Fixes
    • Updated gameplay, networking, and server logic to consistently use level-scoped state for blocks, entities, and packets.
    • Refreshed save/load and dimension initialization to align with level data.
  • Breaking Changes
    • Major public API shifts from world-based to level-based contracts, plus a large NBT library surface removal.

@github-actions github-actions Bot added api Relates to Obsidian.API commands Relates to Obsidian.Commands nbt Relates to Obsidian.Nbt tests Relates to Obsidian.Tests labels Apr 6, 2026
@Tides Tides added epic Very cool server Relates to the server implementation world generation Relates to world generation performance Performance related issue priority: high Very important and removed tests Relates to Obsidian.Tests nbt Relates to Obsidian.Nbt commands Relates to Obsidian.Commands labels Apr 6, 2026
@github-actions github-actions Bot added commands Relates to Obsidian.Commands github-actions Relates to the project workflow nbt Relates to Obsidian.Nbt tests Relates to Obsidian.Tests labels Apr 6, 2026
@Tides Tides removed tests Relates to Obsidian.Tests nbt Relates to Obsidian.Nbt labels Apr 6, 2026
@Tides Tides removed commands Relates to Obsidian.Commands github-actions Relates to the project workflow labels Apr 6, 2026
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a level-centric world abstraction, introduces shared world/dimension creation through a factory, and migrates world-facing consumers to Level-based APIs. It also moves Obsidian.Nbt into a git submodule and updates related build and workspace references.

Changes

Nbt Submodule Migration and Build Config

Layer / File(s) Summary
CI and repository submodule setup
.github/workflows/dotnet.yml, .gitmodules, README.md
Recursive submodule checkout is enabled, the Nbt submodule is declared, and clone instructions use --recurse-submodules.
Solution and editor tooling updates
Obsidian.slnx, Obsidian.API/Obsidian.API.csproj, Obsidian/Obsidian.csproj, .vscode/launch.json, .vscode/tasks.json
Solution and workspace references point to the relocated Nbt project, and tasks/config files are reformatted with solution paths updated to Obsidian.slnx where applicable.
Removal of in-repo Nbt implementation
Obsidian.Nbt/Interfaces/INbtWriter.cs, Obsidian.Nbt
The embedded Nbt interface is removed and the submodule pointer is advanced.

ILevel Abstraction and World/Dimension Refactor

Layer / File(s) Summary
Core contracts and data shapes
Obsidian.API/World/ILevel.cs, .../ILevelFactory.cs, .../ILevelGenerator.cs, .../DimensionSettings/*, .../WorldGenSetting.cs, _Interfaces/IWorld.cs, IDimension.cs, IEntity.cs, IPacketBroadcaster.cs, IServer.cs, IWorldManager.cs, _Types/LevelData.cs, Configuration/ServerConfiguration.cs, Events/BlockEventArgs.cs, BlockBreakEventArgs.cs, ChunkData/IBlockUpdate.cs
Defines the level/dimension API surface, updates world-facing interfaces to level-oriented contracts, renames Level to LevelData, and adds spawn chunk radius configuration.
AbstractLevel, World, Dimension, and WorldManager
Obsidian/WorldData/AbstractLevel.cs, World.cs, Dimension.cs, LevelFactory.cs, WorldManager.cs, Hosting/DependencyInjection.cs
Implements shared level behavior, refactors world and dimension creation, and wires the new factory into startup and DI.
Generators and lighting migrated to ILevel
Obsidian/WorldData/Generators/*, Lighting.cs
Updates generators and lighting helpers to operate on ILevel.
Region storage, block updates, and packet broadcasting
Obsidian/WorldData/Region.cs, RegionFile.cs, BlockUpdates.cs, BlockUpdate.cs, Services/PacketBroadcaster.cs, Utilities/Extensions.cs, GlobalUsings.cs
Routes region persistence, block updates, packet routing, and player data path helpers through level-aware APIs.
Entity and player runtime uses Level
Obsidian/Entities/*
Moves entity movement, spawning, teleportation, and player lifecycle logic onto Level.
Commands, events, packets, and server wiring
Obsidian/Commands/Modules/MainCommandModule.cs, Events/MainEventHandler*.cs, Net/Packets/**, Server.cs, ServerConstants.cs
Updates command handlers, event handlers, packet handlers, and server logic to use Level and WorldManager.
Test fakes
Obsidian.Tests/Fakes/FakePlayer.cs, FakeServer.cs
Updates fake implementations to match the new API surface.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement, networking

Suggested reviewers: Jonpro03, Naamloos

Poem

A bunny hops through worlds made new,
With levels bright and packets too.
Submodules tucked in rows so neat,
While blocks and chunks keep perfect beat.
I thump for builds that stay in tune! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is short and clearly points to the WorldGen API v4 refactor reflected by the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worldgen-api-v4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added commands Relates to Obsidian.Commands github-actions Relates to the project workflow nbt Relates to Obsidian.Nbt tests Relates to Obsidian.Tests labels Apr 20, 2026
@Tides Tides marked this pull request as ready for review July 4, 2026 07:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Obsidian/Entities/Entity.cs (1)

361-372: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Cross-level teleport should move the existing entity, not spawn a detached copy. When to.Level != Level, this removes this from the old level, switches Level, then calls Level.SpawnEntity(to.Position, Type) and drops the returned entity. That path creates and registers a new instance, so this is never added to the target level and the spawned entity is lost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/Entities/Entity.cs` around lines 361 - 372, Cross-level teleport in
Entity.TeleportAsync is spawning a new detached entity instead of moving the
current one. In the branch where to.Level != Level, remove this from the old
level, update the current entity’s Level to the target, and place/move this
entity into the new level rather than calling Level.SpawnEntity and ignoring its
return value. Use the existing Entity.TeleportAsync flow and
Level.DestroyEntityAsync/SpawnEntity symbols to locate and replace the
cross-level transfer logic so the same entity instance is preserved.
Obsidian/Events/MainEventHandler.Inventory.cs (1)

319-355: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Register the thrown item with the level before broadcasting it. SpawnThrownItem only sends AddEntityPacket/SetEntityDataPacket; unlike the other item-spawn paths, it never calls player.Level.TryAddEntity(item). That leaves the item out of the level’s entity set, so ItemEntity.TickAsync won’t run for merging or pickup timing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/Events/MainEventHandler.Inventory.cs` around lines 319 - 355,
SpawnThrownItem broadcasts the new ItemEntity but never registers it with the
level, so it won’t participate in ticking, merging, or pickup timing. Update
SpawnThrownItem in MainEventHandler.Inventory.cs to add the created item via
player.Level.TryAddEntity(item) before queueing the AddEntityPacket and
SetEntityDataPacket, matching the other item spawn paths.
Obsidian/Entities/Player.cs (1)

349-368: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Pre-existing Debug.Assert risk carried over into the Level migration.

Debug.Assert(codec is not null) (Line 359) is a no-op in Release builds; if CodecRegistry.TryGetDimension(Level.DimensionName, ...) fails to resolve, codec.Id on Line 367 will throw an NRE in production. The // TODO Handle missing codec comment already acknowledges this gap.

This isn't introduced by the World→Level rename itself, but since this block is being touched now, want to flag it — happy to help wire up proper handling (e.g., fallback dimension type or explicit error) if useful.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/Entities/Player.cs` around lines 349 - 368, RespawnAsync still
relies on Debug.Assert(codec is not null), which won’t protect Release builds if
CodecRegistry.TryGetDimension(Level.DimensionName, out var codec) fails. Update
the RespawnAsync flow to handle a missing codec explicitly before building the
RespawnPacket, using a clear fallback or failing fast with a logged error
instead of letting codec.Id throw. Keep the fix localized to RespawnAsync and
the codec lookup/packet construction path.
Obsidian/Commands/Modules/MainCommandModule.cs (1)

163-171: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

/save should handle Dimension-backed levels this.Player?.Level is World world skips the flush when the player is in a Dimension, so the command no-ops without feedback. Resolve the owning World via IDimension.ParentWorld here too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/Commands/Modules/MainCommandModule.cs` around lines 163 - 171, The
/save command only flushes when this.Player?.Level is a World, so it silently
does nothing for Dimension-backed levels. Update SaveAsync in MainCommandModule
to resolve the owning World from IDimension.ParentWorld when the player level is
a Dimension, then call FlushRegionsAsync on that world as well; keep the
existing World path intact and use the current Player/Level checks to locate the
level type.
🧹 Nitpick comments (7)
.github/workflows/dotnet.yml (1)

23-25: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Recursive submodule checkout looks correct.

Matches the Obsidian.slnx reference to Obsidian.Nbt/Nbt/Obsidian.Nbt.csproj.

Static analysis flags that the checkout step doesn't set persist-credentials: false, which leaves the GitHub token persisted in the local git config for the remainder of the job (artipacked pattern).

🔒️ Optional hardening
     - uses: actions/checkout@v5
       with:
         submodules: recursive
+        persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/dotnet.yml around lines 23 - 25, The checkout step in the
dotnet workflow should be hardened by disabling credential persistence. Update
the actions/checkout configuration used for recursive submodule checkout to
explicitly set persist-credentials to false so the GitHub token is not left in
the local git config. Locate the change in the checkout step that already uses
submodules: recursive and add the missing security setting there.

Source: Linters/SAST tools

Obsidian/Events/MainEventHandler.World.cs (1)

15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale variable name after Level rename.

world now holds player.Level (an ILevel), not an IWorld. Consider renaming to level for clarity given the rest of the codebase is migrating to this terminology.

♻️ Proposed rename
-        var world = player.Level;
+        var level = player.Level;

(and update subsequent references to world in this method accordingly)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/Events/MainEventHandler.World.cs` at line 15, Rename the local
variable in MainEventHandler.World from world to level since it now stores
player.Level (an ILevel rather than an IWorld), and update all subsequent
references in the same method to match the new name so the terminology stays
consistent with the Level migration.
Obsidian/WorldData/Region.cs (1)

44-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider LogDebug instead of LogInformation for region-load logging.

AbstractLevel.LoadRegion logs the equivalent "region loaded/added" event at LogDebug, while this constructor logs at LogInformation for every region file loaded — this can get noisy as regions load frequently during world traversal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/WorldData/Region.cs` around lines 44 - 45, The region-load message
in Region’s constructor is too noisy at LogInformation; change the logger call
in the Region file-loading path to LogDebug to match AbstractLevel.LoadRegion
and avoid flooding normal logs. Update the logging in the constructor that
records the region file and compression so it still includes the same context,
just at debug level.
Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs (1)

3-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Non-nullable properties lack initializers — possible CS8618 warnings.

Biome and Parameters are non-nullable reference types without defaults or required. Sibling type DimensionGeneratorBiomeSource.Biomes uses a nullable annotation (List<DimensionGeneratorBiomeEntry>?), indicating nullable reference types are enabled for this project, which would flag these properties at compile time.

♻️ Suggested fix
 public sealed record class DimensionGeneratorBiomeEntry
 {
-    public string Biome { get; set; }
+    public required string Biome { get; set; }

-    public DimensionGeneratorBiomeParameters Parameters { get; set; }
+    public required DimensionGeneratorBiomeParameters Parameters { get; set; }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs` around
lines 3 - 8, The DimensionGeneratorBiomeEntry properties are non-nullable but
have no initialization, which can trigger CS8618. Update Biome and Parameters in
DimensionGeneratorBiomeEntry to either be marked required or initialized with
safe defaults so the record class satisfies nullable reference type checks; keep
the change aligned with the existing nullable annotations used by
DimensionGeneratorBiomeSource.Biomes.
Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs (1)

3-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Same nullable-initialization concern as DimensionGeneratorBiomeEntry.

All seven DimensionGeneratorParameterValue-typed properties are non-nullable without defaults/required, inconsistent with the nullable annotation style used elsewhere (e.g. DimensionGeneratorBiomeSource.Biomes).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs`
around lines 3 - 18, The seven DimensionGeneratorBiomeParameters properties are
declared non-nullable without initialization, which is inconsistent with the
nullable style used elsewhere. Update the Offset, Continentalness, Weirdness,
Erosion, Depth, Humidity, and Temperature properties in
DimensionGeneratorBiomeParameters to be explicitly nullable or
required/initialized, matching the pattern used by DimensionGeneratorBiomeEntry
and DimensionGeneratorBiomeSource.Biomes. Make the choice consistent with the
surrounding model types so callers can tell whether values must be provided or
may be absent.
Obsidian.API/World/ILevel.cs (2)

83-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Naming nit: GetWorldSurfaceHeightAsync still uses "World" terminology.

Given the rest of the interface has been renamed to Level-centric naming (ILevel, LevelData, etc.), consider GetLevelSurfaceHeightAsync for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian.API/World/ILevel.cs` at line 83, The interface method name still
uses World terminology, so update ILevel’s GetWorldSurfaceHeightAsync to a
Level-centric name like GetLevelSurfaceHeightAsync for consistency with the
surrounding API naming. Make sure any implementation, call sites, and related
references to GetWorldSurfaceHeightAsync are renamed together so the
Level/LevelData naming stays uniform.

32-32: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider not exposing the raw ConcurrentDictionary for Players.

Returning the live ConcurrentDictionary<Guid, IPlayer> lets callers mutate it directly (add/remove/clear), bypassing TryAddPlayer/TryRemovePlayer (lines 69, 86), which per downstream usage (e.g. Player.TeleportAsync calling Level.TryRemovePlayer/w.TryAddPlayer) are the intended controlled mutation path. Exposing IReadOnlyDictionary<Guid, IPlayer> instead would enforce that invariant at the type level.

♻️ Suggested change
-    public ConcurrentDictionary<Guid, IPlayer> Players { get; }
+    public IReadOnlyDictionary<Guid, IPlayer> Players { get; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian.API/World/ILevel.cs` at line 32, The ILevel.Players property is
exposing the mutable ConcurrentDictionary directly, allowing callers to bypass
the controlled player management flow. Update the ILevel contract to return a
read-only view such as IReadOnlyDictionary<Guid, IPlayer>, and adjust any
implementations/usages of Players to preserve access while keeping TryAddPlayer
and TryRemovePlayer as the only mutation paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.vscode/tasks.json:
- Around line 97-103: The restore task still points to the old solution file, so
update the dotnet restore target in the tasks configuration to match the
migrated solution name used by the other tasks. Check the restore task alongside
build-solution and clean in the tasks definition, and change the Obsidian.sln
reference to Obsidian.slnx so all solution-related tasks are consistent.

In `@Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs`:
- Around line 5-6: The DimensionGeneratorSettings DTO leaves Settings as an
interface type, so deserialization has no way to create a concrete instance.
Update DimensionGeneratorSettings to use a concrete mapping path between Type
and Settings, such as a custom converter or discriminator-based factory, and
ensure the deserialization logic resolves IDimensionSetting to the correct
implementation before materializing the object.

In `@Obsidian.API/World/WorldGenSetting.cs`:
- Line 12: The WorldGenSetting.Dimensions property is left null on new
instances, so initialize it to an empty dictionary by default in WorldGenSetting
to make it safe to enumerate without null checks. Update the property
declaration or constructor so callers always get a usable Dictionary<string,
DimensionSetting> when creating a new WorldGenSetting.

In `@Obsidian/Entities/Factories/EntitySpawner.cs`:
- Line 141: The EntitySpawner spawn path is casting the level to World even
though SpawnEntity(IEntity) is defined on AbstractLevel, which breaks non-World
levels. Update the return in EntitySpawner to call the shared spawn method on
the level’s AbstractLevel reference instead of using a World cast, and keep the
logic aligned with the Dimension/AbstractLevel hierarchy so all level types can
spawn entities correctly.

In `@Obsidian/Services/PacketBroadcaster.cs`:
- Around line 53-60: `BroadcastToLevelInRange` is ignoring its `excludedIds`
parameter, so players that should be skipped still receive packets. Update the
`PacketBroadcaster.BroadcastToLevelInRange` loop to filter the result of
`world.GetPlayersInRange(...)` the same way `BroadcastToLevel` and
`QueuePacketToLevelInRange` do, excluding any `Player` whose id is present in
`excludedIds` before calling `Client.SendPacket(packet)`.

In `@Obsidian/WorldData/AbstractLevel.cs`:
- Around line 385-393: The chunk unloading logic in ManageChunksAsync uses
LoadedChunks.Except(...).ForEach(async c => ...), which creates detached async
void work and can hide exceptions. Replace the ForEach lambda with an awaited
loop over the materialized set so each unload is awaited in sequence or with
controlled concurrency, and keep the LoadedChunks.TryRemove and
NumericsHelper.LongToInts/GetRegionForChunk flow inside that loop. Also add a
null check for the region returned by GetRegionForChunk before calling
UnloadChunk.

In `@Obsidian/WorldData/Dimension.cs`:
- Around line 28-34: Persist dimension LevelData in Dimension by implementing
the stubbed LoadAsync and SaveAsync methods so each dimension’s level.dat is
actually written and read back. Use the existing Dimension members like
FolderPath, LevelDataFilePath, and LevelData to serialize on SaveAsync and
restore on LoadAsync, and make sure the logic handles missing files gracefully.
Keep the behavior aligned with WorldManager.LoadWorldsAsync so existing worlds
can recreate or reload child dimensions on startup instead of losing
per-dimension state.

In `@Obsidian/WorldData/Region.cs`:
- Around line 152-157: The neighbor update loop in BeginTickAsync is using
List<T>.ForEach with an async lambda, which creates an unawaited async-void call
and can hide exceptions from BlockUpdateNeighborsAsync. Replace the
neighborUpdates.ForEach(async u => await u.Level.BlockUpdateNeighborsAsync(u))
pattern with an awaited asynchronous iteration so each neighbor update is
properly awaited before BeginTickAsync completes. Keep the existing
delayed/AddBlockUpdate flow intact, and make sure the fix preserves the use of
neighborUpdates and BlockUpdateNeighborsAsync as the key symbols.

In `@Obsidian/WorldData/RegionFile.cs`:
- Around line 79-82: GetChunkBytesAsync in RegionFile still uses a
partial-read-prone ReadAsync when filling the chunk buffer, which can leave
truncated data before BinaryPrimitives.ReadInt32BigEndian is applied. Update the
GetChunkBytesAsync read path to use ReadExactlyAsync on the regionFileStream for
the chunk buffer, matching the fix already applied earlier in RegionFile, so the
full requested byte count is guaranteed before parsing.

In `@Obsidian/WorldData/World.cs`:
- Around line 30-31: The World loading path in World and its NbtReader usage
leaves the FileStream from fi.OpenRead() undisposed, which can leak handles and
interfere with later file operations. Update the load logic to wrap both the
stream and the NbtReader in using statements (or equivalent disposal handling)
around the ReadNextTag call, keeping the existing levelCompound parsing intact.
- Around line 56-66: World.LoadAsync is still using a hardcoded spawn-chunk
radius instead of the configured value, which can overflow SpawnChunks or leave
default entries processed. Update the chunk range loops in World.LoadAsync to
use this.Configuration.SpawnChunkRadius, matching GenerateAsync and the
SpawnChunks sizing from Configuration.SpawnChunkRadius, and keep the SpawnChunks
fill and Parallel.ForEachAsync traversal aligned with that configured radius.

---

Outside diff comments:
In `@Obsidian/Commands/Modules/MainCommandModule.cs`:
- Around line 163-171: The /save command only flushes when this.Player?.Level is
a World, so it silently does nothing for Dimension-backed levels. Update
SaveAsync in MainCommandModule to resolve the owning World from
IDimension.ParentWorld when the player level is a Dimension, then call
FlushRegionsAsync on that world as well; keep the existing World path intact and
use the current Player/Level checks to locate the level type.

In `@Obsidian/Entities/Entity.cs`:
- Around line 361-372: Cross-level teleport in Entity.TeleportAsync is spawning
a new detached entity instead of moving the current one. In the branch where
to.Level != Level, remove this from the old level, update the current entity’s
Level to the target, and place/move this entity into the new level rather than
calling Level.SpawnEntity and ignoring its return value. Use the existing
Entity.TeleportAsync flow and Level.DestroyEntityAsync/SpawnEntity symbols to
locate and replace the cross-level transfer logic so the same entity instance is
preserved.

In `@Obsidian/Entities/Player.cs`:
- Around line 349-368: RespawnAsync still relies on Debug.Assert(codec is not
null), which won’t protect Release builds if
CodecRegistry.TryGetDimension(Level.DimensionName, out var codec) fails. Update
the RespawnAsync flow to handle a missing codec explicitly before building the
RespawnPacket, using a clear fallback or failing fast with a logged error
instead of letting codec.Id throw. Keep the fix localized to RespawnAsync and
the codec lookup/packet construction path.

In `@Obsidian/Events/MainEventHandler.Inventory.cs`:
- Around line 319-355: SpawnThrownItem broadcasts the new ItemEntity but never
registers it with the level, so it won’t participate in ticking, merging, or
pickup timing. Update SpawnThrownItem in MainEventHandler.Inventory.cs to add
the created item via player.Level.TryAddEntity(item) before queueing the
AddEntityPacket and SetEntityDataPacket, matching the other item spawn paths.

---

Nitpick comments:
In @.github/workflows/dotnet.yml:
- Around line 23-25: The checkout step in the dotnet workflow should be hardened
by disabling credential persistence. Update the actions/checkout configuration
used for recursive submodule checkout to explicitly set persist-credentials to
false so the GitHub token is not left in the local git config. Locate the change
in the checkout step that already uses submodules: recursive and add the missing
security setting there.

In `@Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs`:
- Around line 3-8: The DimensionGeneratorBiomeEntry properties are non-nullable
but have no initialization, which can trigger CS8618. Update Biome and
Parameters in DimensionGeneratorBiomeEntry to either be marked required or
initialized with safe defaults so the record class satisfies nullable reference
type checks; keep the change aligned with the existing nullable annotations used
by DimensionGeneratorBiomeSource.Biomes.

In `@Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs`:
- Around line 3-18: The seven DimensionGeneratorBiomeParameters properties are
declared non-nullable without initialization, which is inconsistent with the
nullable style used elsewhere. Update the Offset, Continentalness, Weirdness,
Erosion, Depth, Humidity, and Temperature properties in
DimensionGeneratorBiomeParameters to be explicitly nullable or
required/initialized, matching the pattern used by DimensionGeneratorBiomeEntry
and DimensionGeneratorBiomeSource.Biomes. Make the choice consistent with the
surrounding model types so callers can tell whether values must be provided or
may be absent.

In `@Obsidian.API/World/ILevel.cs`:
- Line 83: The interface method name still uses World terminology, so update
ILevel’s GetWorldSurfaceHeightAsync to a Level-centric name like
GetLevelSurfaceHeightAsync for consistency with the surrounding API naming. Make
sure any implementation, call sites, and related references to
GetWorldSurfaceHeightAsync are renamed together so the Level/LevelData naming
stays uniform.
- Line 32: The ILevel.Players property is exposing the mutable
ConcurrentDictionary directly, allowing callers to bypass the controlled player
management flow. Update the ILevel contract to return a read-only view such as
IReadOnlyDictionary<Guid, IPlayer>, and adjust any implementations/usages of
Players to preserve access while keeping TryAddPlayer and TryRemovePlayer as the
only mutation paths.

In `@Obsidian/Events/MainEventHandler.World.cs`:
- Line 15: Rename the local variable in MainEventHandler.World from world to
level since it now stores player.Level (an ILevel rather than an IWorld), and
update all subsequent references in the same method to match the new name so the
terminology stays consistent with the Level migration.

In `@Obsidian/WorldData/Region.cs`:
- Around line 44-45: The region-load message in Region’s constructor is too
noisy at LogInformation; change the logger call in the Region file-loading path
to LogDebug to match AbstractLevel.LoadRegion and avoid flooding normal logs.
Update the logging in the constructor that records the region file and
compression so it still includes the same context, just at debug level.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 60477044-6ce2-414e-8b37-c054c535b697

📥 Commits

Reviewing files that changed from the base of the PR and between 6afd297 and 5b9fc36.

📒 Files selected for processing (96)
  • .github/workflows/dotnet.yml
  • .gitmodules
  • .vscode/launch.json
  • .vscode/tasks.json
  • Obsidian.API/ChunkData/IBlockUpdate.cs
  • Obsidian.API/Configuration/ServerConfiguration.cs
  • Obsidian.API/Events/BlockBreakEventArgs.cs
  • Obsidian.API/Events/BlockEventArgs.cs
  • Obsidian.API/Obsidian.API.csproj
  • Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeEntry.cs
  • Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeParameters.cs
  • Obsidian.API/World/DimensionSettings/DimensionGeneratorBiomeSource.cs
  • Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs
  • Obsidian.API/World/DimensionSettings/DimensionGeneratorValueRange.cs
  • Obsidian.API/World/DimensionSettings/DimensionSetting.cs
  • Obsidian.API/World/DimensionSettings/IDimensionSetting.cs
  • Obsidian.API/World/ILevel.cs
  • Obsidian.API/World/ILevelFactory.cs
  • Obsidian.API/World/ILevelGenerator.cs
  • Obsidian.API/World/WorldGenSetting.cs
  • Obsidian.API/_Interfaces/IDimension.cs
  • Obsidian.API/_Interfaces/IEntity.cs
  • Obsidian.API/_Interfaces/IPacketBroadcaster.cs
  • Obsidian.API/_Interfaces/IServer.cs
  • Obsidian.API/_Interfaces/IWorld.cs
  • Obsidian.API/_Interfaces/IWorldManager.cs
  • Obsidian.API/_Types/LevelData.cs
  • Obsidian.Nbt
  • Obsidian.Nbt/Exceptions/TagNotFoundException.cs
  • Obsidian.Nbt/GlobalUsings.cs
  • Obsidian.Nbt/INbtTag.cs
  • Obsidian.Nbt/Interfaces/INbtWriter.cs
  • Obsidian.Nbt/InternalsVisibleTo.cs
  • Obsidian.Nbt/ModifiedUtf8.cs
  • Obsidian.Nbt/NbtArray.cs
  • Obsidian.Nbt/NbtCompound.cs
  • Obsidian.Nbt/NbtCompression.cs
  • Obsidian.Nbt/NbtList.cs
  • Obsidian.Nbt/NbtReader.Primitives.cs
  • Obsidian.Nbt/NbtReader.cs
  • Obsidian.Nbt/NbtTag.cs
  • Obsidian.Nbt/NbtTagType.cs
  • Obsidian.Nbt/NbtWriterStream.Primitives.cs
  • Obsidian.Nbt/NbtWriterStream.cs
  • Obsidian.Nbt/Obsidian.Nbt.csproj
  • Obsidian.Nbt/RawNbtWriter.Primitives.cs
  • Obsidian.Nbt/RawNbtWriter.cs
  • Obsidian.Nbt/Utilities/NbtWriterState.cs
  • Obsidian.Nbt/Utilities/ThrowHelper.cs
  • Obsidian.Tests/Fakes/FakePlayer.cs
  • Obsidian.Tests/Fakes/FakeServer.cs
  • Obsidian.slnx
  • Obsidian/BlockUpdate.cs
  • Obsidian/Commands/Modules/MainCommandModule.cs
  • Obsidian/Entities/Animal.cs
  • Obsidian/Entities/Entity.cs
  • Obsidian/Entities/Factories/EntitySpawner.cs
  • Obsidian/Entities/FallingBlock.cs
  • Obsidian/Entities/ItemEntity.cs
  • Obsidian/Entities/Living.cs
  • Obsidian/Entities/Player.Helpers.cs
  • Obsidian/Entities/Player.cs
  • Obsidian/Events/MainEventHandler.Inventory.cs
  • Obsidian/Events/MainEventHandler.World.cs
  • Obsidian/Events/MainEventHandler.cs
  • Obsidian/GlobalUsings.cs
  • Obsidian/Hosting/DependencyInjection.cs
  • Obsidian/Net/Packets/Configuration/Serverbound/FinishConfigurationPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/PlayerCommandPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/PlayerInputPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/SetCarriedItemPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/SetCreativeModeSlotPacket.cs
  • Obsidian/Net/Packets/Play/Serverbound/UseItemOnPacket.cs
  • Obsidian/Obsidian.csproj
  • Obsidian/Server.cs
  • Obsidian/ServerConstants.cs
  • Obsidian/Services/PacketBroadcaster.cs
  • Obsidian/Utilities/Extensions.cs
  • Obsidian/WorldData/AbstractLevel.cs
  • Obsidian/WorldData/BlockUpdates.cs
  • Obsidian/WorldData/Dimension.cs
  • Obsidian/WorldData/Generators/EmptyWorldGenerator.cs
  • Obsidian/WorldData/Generators/GenHelper.cs
  • Obsidian/WorldData/Generators/IslandGenerator.cs
  • Obsidian/WorldData/Generators/Mojang/ChunkBuilder.cs
  • Obsidian/WorldData/Generators/MojangGenerator.cs
  • Obsidian/WorldData/Generators/OverworldGenerator.cs
  • Obsidian/WorldData/Generators/SuperflatGenerator.cs
  • Obsidian/WorldData/LevelFactory.cs
  • Obsidian/WorldData/Lighting.cs
  • Obsidian/WorldData/Region.cs
  • Obsidian/WorldData/RegionFile.cs
  • Obsidian/WorldData/World.cs
  • Obsidian/WorldData/WorldManager.cs
  • README.md
💤 Files with no reviewable changes (22)
  • Obsidian.Nbt/Exceptions/TagNotFoundException.cs
  • Obsidian.Nbt/Obsidian.Nbt.csproj
  • Obsidian.Nbt/ModifiedUtf8.cs
  • Obsidian.Nbt/InternalsVisibleTo.cs
  • Obsidian.Nbt/NbtCompound.cs
  • Obsidian.Nbt/NbtTagType.cs
  • Obsidian.Nbt/Interfaces/INbtWriter.cs
  • Obsidian.Nbt/NbtTag.cs
  • Obsidian.API/_Interfaces/IWorldManager.cs
  • Obsidian.Nbt/Utilities/NbtWriterState.cs
  • Obsidian.Nbt/NbtReader.Primitives.cs
  • Obsidian.Nbt/NbtReader.cs
  • Obsidian.Nbt/RawNbtWriter.Primitives.cs
  • Obsidian.Nbt/Utilities/ThrowHelper.cs
  • Obsidian.Nbt/NbtList.cs
  • Obsidian.Nbt/NbtCompression.cs
  • Obsidian.Nbt/GlobalUsings.cs
  • Obsidian.Nbt/NbtArray.cs
  • Obsidian.Nbt/INbtTag.cs
  • Obsidian.Nbt/NbtWriterStream.cs
  • Obsidian.Nbt/NbtWriterStream.Primitives.cs
  • Obsidian.Nbt/RawNbtWriter.cs

Comment thread .vscode/tasks.json
Comment thread Obsidian.API/World/DimensionSettings/DimensionGeneratorSettings.cs
Comment thread Obsidian.API/World/WorldGenSetting.cs
Comment thread Obsidian/Entities/Factories/EntitySpawner.cs Outdated
Comment thread Obsidian/Services/PacketBroadcaster.cs
Comment thread Obsidian/WorldData/Dimension.cs
Comment thread Obsidian/WorldData/Region.cs
Comment thread Obsidian/WorldData/RegionFile.cs
Comment thread Obsidian/WorldData/World.cs Outdated
Comment thread Obsidian/WorldData/World.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Obsidian/WorldData/AbstractLevel.cs (1)

248-268: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

z loop should iterate upward through the chunk range
ToChunkCoord() maps X and Z directly, so top is the min chunk z and bottom is the max. As written, z >= bottom with z -= ... skips every multi-row range here.

🐛 Proposed fix
-            for (int z = top; z >= bottom; z -= Region.CubicRegionSize)
+            for (int z = top; z <= bottom; z += Region.CubicRegionSize)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/WorldData/AbstractLevel.cs` around lines 248 - 268, The chunk scan
in AbstractLevel’s region iteration is using the wrong z-direction, causing
multi-row ranges to be skipped. Update the loop in the entity search logic that
uses GetRegionForChunk and Region.CubicRegionSize so that z advances upward from
bottom to top instead of decrementing from top, and keep the bounds consistent
with ToChunkCoord()’s mapping of top/min and bottom/max.
♻️ Duplicate comments (1)
Obsidian/WorldData/AbstractLevel.cs (1)

385-393: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Null-check the region before calling UnloadChunk.

GetRegionForChunk(cx, cz) returns IRegion?, but line 391 dereferences r unconditionally. If the region has already been unloaded/removed while the chunk was still tracked in LoadedChunks, this throws an NPE and aborts ManageChunksAsync.

🛡️ Proposed guard
                 if (LoadedChunks.TryRemove(chunk))
                 {
                     NumericsHelper.LongToInts(chunk, out var cx, out var cz);
                     var r = GetRegionForChunk(cx, cz);
-                    await r.UnloadChunk(cx, cz);
+                    if (r is not null)
+                        await r.UnloadChunk(cx, cz);
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/WorldData/AbstractLevel.cs` around lines 385 - 393, The chunk
cleanup loop in ManageChunksAsync can dereference a null region because
GetRegionForChunk(cx, cz) returns IRegion? and r is used unconditionally before
calling UnloadChunk. Update the LoadedChunks.TryRemove branch to guard the
region lookup result from GetRegionForChunk, and only call r.UnloadChunk(cx, cz)
when the region is non-null so the cleanup loop does not abort on a missing
region.
🧹 Nitpick comments (1)
Obsidian/WorldData/AbstractLevel.cs (1)

478-483: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Drop the unnecessary as Entity cast.

TryAddEntity already accepts IEntity, so casting with as Entity adds nothing except a silent-null hazard: if entity isn't an Entity, entity as Entity yields null, and TryAddEntity then dereferences entity.Position, throwing an NPE. Pass entity directly.

♻️ Proposed change
     public IEntity SpawnEntity(IEntity entity)
     {
         entity.SpawnEntity();
-        TryAddEntity(entity as Entity);
+        TryAddEntity(entity);
         return entity;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Obsidian/WorldData/AbstractLevel.cs` around lines 478 - 483, The SpawnEntity
flow is using an unnecessary and unsafe cast when calling TryAddEntity; since
TryAddEntity already accepts IEntity, remove the as Entity conversion and pass
the entity reference directly. Update the SpawnEntity method in AbstractLevel so
it spawns the entity and then adds the same IEntity instance without risking a
null from the cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Obsidian/WorldData/AbstractLevel.cs`:
- Around line 248-268: The chunk scan in AbstractLevel’s region iteration is
using the wrong z-direction, causing multi-row ranges to be skipped. Update the
loop in the entity search logic that uses GetRegionForChunk and
Region.CubicRegionSize so that z advances upward from bottom to top instead of
decrementing from top, and keep the bounds consistent with ToChunkCoord()’s
mapping of top/min and bottom/max.

---

Duplicate comments:
In `@Obsidian/WorldData/AbstractLevel.cs`:
- Around line 385-393: The chunk cleanup loop in ManageChunksAsync can
dereference a null region because GetRegionForChunk(cx, cz) returns IRegion? and
r is used unconditionally before calling UnloadChunk. Update the
LoadedChunks.TryRemove branch to guard the region lookup result from
GetRegionForChunk, and only call r.UnloadChunk(cx, cz) when the region is
non-null so the cleanup loop does not abort on a missing region.

---

Nitpick comments:
In `@Obsidian/WorldData/AbstractLevel.cs`:
- Around line 478-483: The SpawnEntity flow is using an unnecessary and unsafe
cast when calling TryAddEntity; since TryAddEntity already accepts IEntity,
remove the as Entity conversion and pass the entity reference directly. Update
the SpawnEntity method in AbstractLevel so it spawns the entity and then adds
the same IEntity instance without risking a null from the cast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d974a021-2a36-4a6a-a2d5-75537f5b4d3b

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9fc36 and 07e205b.

📒 Files selected for processing (5)
  • Obsidian.API/World/ILevel.cs
  • Obsidian/Entities/Factories/EntitySpawner.cs
  • Obsidian/Services/PacketBroadcaster.cs
  • Obsidian/WorldData/AbstractLevel.cs
  • Obsidian/WorldData/World.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • Obsidian/Entities/Factories/EntitySpawner.cs
  • Obsidian/Services/PacketBroadcaster.cs
  • Obsidian/WorldData/World.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Relates to Obsidian.API commands Relates to Obsidian.Commands epic Very cool github-actions Relates to the project workflow nbt Relates to Obsidian.Nbt performance Performance related issue priority: high Very important server Relates to the server implementation tests Relates to Obsidian.Tests world generation Relates to world generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant