mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 17:19:15 -10:00
Native mods: a server's levels loading and unloading (LevelEvent)
native_level_load when a level opens, before its chunks; native_level_unload once it has closed, after its last save: a level idle for 300 ticks, and every level still open when the server stops or fails. Fixes #6154. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
975c07d915
commit
c60b4c8d46
5 files changed
+85
No files matched your search
@@ -11768,6 +11768,8 @@ impl Game<'_> {
|
||||
self.streaming.sky_light = dim != Dim::Nether;
|
||||
self.streaming.nether_shading = dim == Dim::Nether;
|
||||
self.load_world_state();
|
||||
// The level the world opens in, before any of its chunks.
|
||||
self.native_level_opened(dim);
|
||||
Ok(())
|
||||
}
|
||||
/// Streaming and region storage for one dimension of a world.
|
||||
|
||||
@@ -118,6 +118,9 @@ impl Default for Pass {
|
||||
#[derive(Default)]
|
||||
pub(super) struct Levels {
|
||||
parked: HashMap<Dim, Parked>,
|
||||
/// The levels whose open the mods heard (`native_level_load`), each to
|
||||
/// be told closed once.
|
||||
heard_open: Vec<Dim>,
|
||||
}
|
||||
|
||||
impl Levels {
|
||||
@@ -316,6 +319,7 @@ impl Game<'_> {
|
||||
if opened {
|
||||
// The new streaming's radius and meshing options.
|
||||
self.apply_settings();
|
||||
self.native_level_opened(target);
|
||||
}
|
||||
}
|
||||
let host = match host {
|
||||
@@ -617,10 +621,42 @@ impl Game<'_> {
|
||||
self.save_level(true)?;
|
||||
self.enter_level(None, back)?;
|
||||
self.levels.parked.remove(&dim);
|
||||
self.native_level_closed(dim);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The mods hear `dim`'s level open (`LevelEvent.Load`), on a server.
|
||||
pub(super) fn native_level_opened(&mut self, dim: Dim) {
|
||||
if self.remote.is_some()
|
||||
|| self.options.execution_role.is_client()
|
||||
|| self.levels.heard_open.contains(&dim)
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.levels.heard_open.push(dim);
|
||||
self.native_level_load(dim.id());
|
||||
}
|
||||
|
||||
/// The mods hear `dim`'s level close (`LevelEvent.Unload`), once for
|
||||
/// each open they heard.
|
||||
fn native_level_closed(&mut self, dim: Dim) {
|
||||
let Some(i) = self.levels.heard_open.iter().position(|d| *d == dim) else {
|
||||
return;
|
||||
};
|
||||
self.levels.heard_open.remove(i);
|
||||
self.native_level_unload(dim.id());
|
||||
}
|
||||
|
||||
/// Every level still open closes with the server, however it ends
|
||||
/// (`MinecraftServer.stopServer`'s `level.close()` loop), in vanilla's
|
||||
/// order of its levels: the Overworld, the Nether, the End.
|
||||
pub(super) fn native_levels_closing(&mut self) {
|
||||
for dim in [Dim::Overworld, Dim::Nether, Dim::End] {
|
||||
self.native_level_closed(dim);
|
||||
}
|
||||
}
|
||||
|
||||
/// Save every parked level (`MinecraftServer.saveAllChunks`); the level
|
||||
/// in place is the caller's to save.
|
||||
pub(super) fn save_parked_levels(&mut self, wait: bool) -> Result<()> {
|
||||
|
||||
@@ -2583,6 +2583,28 @@ impl Game<'_> {
|
||||
pub(super) fn native_net_from_server(&mut self, channel: &str, data: &[u8]) {
|
||||
let _ = (channel, data);
|
||||
}
|
||||
/// `dimension`'s level opened on the server (NeoForge's `LevelEvent.Load`,
|
||||
/// posted in `MinecraftServer.createLevels`): before any of its chunks
|
||||
/// (`native_chunk_load`), so a mod sets up what it keeps for the level
|
||||
/// here. The level the world opens in comes before `WorldOpenReady`; the
|
||||
/// others open when someone first goes there (a player, a portal, an
|
||||
/// `execute in` command), with the level in place. Once for each open:
|
||||
/// `native_level_unload` follows. Server-side. API @API:level_events@.
|
||||
pub(super) fn native_level_load(&mut self, dimension: &str) {
|
||||
let _ = dimension;
|
||||
}
|
||||
/// `dimension`'s level closed (NeoForge's `LevelEvent.Unload`, posted after
|
||||
/// `level.close()` in `MinecraftServer.stopServer`), after its last save:
|
||||
/// drop what the mod kept for it. A level other than the Overworld closes
|
||||
/// once nobody has been in it for 300 ticks, after its chunks' unloads;
|
||||
/// every level still open closes when the server stops, before
|
||||
/// `WorldCloseFinal`, and when its open or its loop fails, before
|
||||
/// `WorldOpenFailed` or `WorldCloseFailed`. Once for each
|
||||
/// `native_level_load`. Don't read its blocks. Server-side. API
|
||||
/// @API:level_events@.
|
||||
pub(super) fn native_level_unload(&mut self, dimension: &str) {
|
||||
let _ = dimension;
|
||||
}
|
||||
/// Chunk `chunk` (`[x, z]`, in chunks) of `dimension` came into the world
|
||||
/// (NeoForge's `ChunkEvent.Load`): `new` when it was just generated, false
|
||||
/// when read from the save. A flat world's chunk that leaves before it's
|
||||
|
||||
@@ -239,6 +239,8 @@ impl Game<'_> {
|
||||
|
||||
/// `error_text` is the engine's own error text, unchanged.
|
||||
pub(super) fn native_lifecycle_world_failed(&mut self, error_text: &str) {
|
||||
// The levels that opened before the failure close first.
|
||||
self.native_levels_closing();
|
||||
if let Some(context) = self.native_lifecycle.world_open_failed() {
|
||||
self.native_world_lifecycle(&context, WorldPhase::WorldOpenFailed, error_text);
|
||||
}
|
||||
@@ -246,6 +248,7 @@ impl Game<'_> {
|
||||
|
||||
/// Report a server-loop or shutdown failure once and retire its epoch.
|
||||
pub(super) fn native_lifecycle_close_failed(&mut self, error_text: &str) {
|
||||
self.native_levels_closing();
|
||||
if let Some(context) = self.native_lifecycle.world_close_failed() {
|
||||
self.native_world_lifecycle(&context, WorldPhase::WorldCloseFailed, error_text);
|
||||
}
|
||||
@@ -256,6 +259,8 @@ impl Game<'_> {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
// Each level closes after the final save (`LevelEvent.Unload`).
|
||||
self.native_levels_closing();
|
||||
if let Some(context) = self.native_lifecycle.world_close_final() {
|
||||
self.native_world_lifecycle(&context, WorldPhase::WorldCloseFinal, "");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "level_load",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_level_load",
|
||||
"neoforge": "LevelEvent.Load",
|
||||
"side": "server",
|
||||
"cancellable": false
|
||||
},
|
||||
{
|
||||
"name": "level_unload",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_level_unload",
|
||||
"neoforge": "LevelEvent.Unload",
|
||||
"side": "server",
|
||||
"cancellable": false
|
||||
}
|
||||
],
|
||||
"rust_summary": "a server's levels opening and closing (`native_level_load`, `native_level_unload`).",
|
||||
"doc_summary": "a server's levels opening and closing, on the single player server thread and on a dedicated server: `native_level_load(dimension)` when a level opens, before any of its chunks (NeoForge's `LevelEvent.Load`), and `native_level_unload(dimension)` once it has closed, after its last save (`LevelEvent.Unload`). The level the world opens in comes before `WorldOpenReady`; the Nether and the End open when someone first goes there and close after 300 ticks with nobody in them. Every level still open closes when the server stops, before `WorldCloseFinal`, or when its open or its loop fails, so each load has one unload. Bundles that use them need \"hooks\": @API:level_events@."
|
||||
}
|
||||
Reference in new issue
Block a user