From e7786068ce52dafcd03f644a07d89c46ce1dd841 Mon Sep 17 00:00:00 2001 From: Kalmarv <59708937+Kalmarv@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:49:56 -0600 Subject: [PATCH] Time of day: the new one loads only after the old one has unloaded Switching time of day could load the new layer while the old one was still loaded: the apply loop goes through the layers in catalog order and sends one event a pass, so going to an earlier time (WeatherDay -> Evening) sent the load first, and nothing waited for an unload to finish. Two times of day at once carry both times' lights, and a level with many lights overflowed the engine's light tables. A time-of-day layer is now asked to load only once every sibling time of day that is to be off reports requested and loaded both false; until then its row says "Waiting for the old time of day". An unload is no longer sent while the engine is still loading the layer itself. The engine ignored it, so at a level load the level's own time of day could stay loaded beside the saved one, and with the wait above the saved one never loaded. Co-Authored-By: Claude Opus 5.5 --- Engine/Game/World/world_layers.h | 23 +++++++++++++++++++++++ Extension/World/local_world_layers.cpp | 16 +++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Engine/Game/World/world_layers.h b/Engine/Game/World/world_layers.h index 88886f8..ab833a8 100644 --- a/Engine/Game/World/world_layers.h +++ b/Engine/Game/World/world_layers.h @@ -169,6 +169,29 @@ inline WorldLayerMapSelection select_world_layer_map( return result; } +// A TOD__ layer (or a lighting tier's _TOD__): one time of day among its siblings. +inline bool world_layer_time_of_day(std::size_t slot) { + const auto& bundle = world_layer_nodes()[slot].bundle; + auto leaf = bundle.substr(bundle.find_last_of('/') + 1); + for (auto& c : leaf) if (c >= 'A' && c <= 'Z') c = static_cast(c + ('a' - 'A')); + const auto mid = leaf.find("_tod_"); + const auto at = leaf.starts_with("tod_") ? 4 : mid == std::string::npos ? leaf.size() : mid + 5; + return at < leaf.size() && leaf[at] >= '1' && leaf[at] <= '9'; +} + +// A sibling time of day that is to be off but not fully unloaded yet, which `slot` must wait for; +// world_layer_nodes().size() when there is none. +template +std::size_t world_layer_old_time_of_day(std::size_t slot, const std::vector>& desired, FullyOff&& fully_off) { + const auto& nodes = world_layer_nodes(); + if (!world_layer_time_of_day(slot)) return nodes.size(); + for (std::size_t at = 0; at < nodes.size(); ++at) + if (at != slot && nodes[at].parent == nodes[slot].parent && nodes[at].map == nodes[slot].map && + desired[at] == false && world_layer_time_of_day(at) && !fully_off(at)) + return at; + return nodes.size(); +} + inline bool world_layer_supported( std::size_t choice, const std::vector& present) noexcept { diff --git a/Extension/World/local_world_layers.cpp b/Extension/World/local_world_layers.cpp index d1b9374..cef713c 100644 --- a/Extension/World/local_world_layers.cpp +++ b/Extension/World/local_world_layers.cpp @@ -171,8 +171,9 @@ bool ensure_seasonal_state(std::uintptr_t entity, bool wanted, std::uint64_t now } if (reached(wanted)) return true; // An engine-initiated auto-load is still in flight. Wait for it rather than - // claiming success from a non-null request handle or repeatedly reloading. - if (wanted && state.requested) { status = "Loading..."; return false; } + // claiming success from a non-null request handle, repeatedly reloading, or + // unloading it midway (the engine ignores that unload and finishes the load). + if (state.requested && !state.loaded) { status = "Loading..."; return false; } node.pending = wanted; node.requested_at = now; const auto slot = node.slot; SeasonalEvent event{local_runtime().base + addr::world_layers::subworld_event_vtable, wanted ? 0xd27bc17eu : 0x21f3f677u}; @@ -309,6 +310,10 @@ void update_world_layers(std::uintptr_t manager, std::uint64_t now) { std::vector done(targets.size()), inactive(targets.size()); std::vector status(targets.size(), "Waiting for layer"); bool issued{}; + const auto fully_off = [&](std::size_t at) { + SeasonalState live; + return !targets[at] || (seasonal_state(r.nodes.at(targets[at]), live) && !live.requested && !live.loaded); + }; for (unsigned slot = 0; slot < targets.size(); ++slot) { if (world_layer_nodes()[slot].map != map) continue; const auto parent = world_parent(static_cast(slot)); @@ -323,7 +328,12 @@ void update_world_layers(std::uintptr_t manager, std::uint64_t now) { if (!targets[slot]) continue; SeasonalState state; if (!seasonal_state(r.nodes.at(targets[slot]), state)) continue; - if (!desired[slot]) { + // Two times of day loaded at once can overflow the engine's light tables: the old one goes first. + const auto old = desired[slot] == true && !state.requested ? world_layer_old_time_of_day(slot, desired, fully_off) : targets.size(); + if (old < targets.size()) { + status[slot] = "Waiting for the old time of day"; + (void)ensure_seasonal_state(targets[old], false, now, issued, status[old]); + } else if (!desired[slot]) { done[slot] = state.requested == state.loaded; inactive[slot] = done[slot] && !state.loaded; status[slot] = done[slot] ? "Default" : "Loading...";