From d2e5fbca756a69f6d93458e70f96047e9ce99eb9 Mon Sep 17 00:00:00 2001 From: Batrachophreno Date: Sat, 1 Aug 2026 21:25:35 -0400 Subject: [PATCH] Fix All Exoplanet Atmosphere Lag (#22924) Y'know, probably. It certainly seems to work locally! changes: - code_imp: "Exoplanets now cache their main outdoor atmospheric region instead of trying to search their entire map once per second." - code_imp: "Exoplanet atmospheric calculations now run only when necessary using new update_revision counter." - code_imp: "Exoplanet atmospheres now wait until all random ruins have been placed before trying to resolve the air everywhere." - admin: "Adds new admin debug verb, 'Toggle Non-Horizon Temperature Graphics', which toggles all off-ship gas temp graphics to address lag in a pinch, if still necessary, without removing actual environmental hazard." --- code/ZAS/Turf.dm | 8 +- code/ZAS/Zone.dm | 4 + code/controllers/subsystems/air.dm | 34 +++++ code/modules/admin/admin_verbs.dm | 1 + code/modules/admin/verbs/diagnostics.dm | 18 +++ code/modules/overmap/exoplanets/exoplanet.dm | 126 +++++++++++++++++-- html/changelogs/Bat-AtmosOptimization.yml | 7 ++ 7 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 html/changelogs/Bat-AtmosOptimization.yml diff --git a/code/ZAS/Turf.dm b/code/ZAS/Turf.dm index 0a32f209a2f..889cb2f4a0c 100644 --- a/code/ZAS/Turf.dm +++ b/code/ZAS/Turf.dm @@ -6,7 +6,13 @@ /turf/simulated/proc/update_graphic(list/graphic_add = null, list/graphic_remove = null) if(graphic_add && LAZYLEN(graphic_add)) - add_vis_contents(graphic_add) + if(SSair.suppress_nonhorizon_temperature_graphics && !is_station_level(z)) + for(var/obj/gas_overlay/graphic in graphic_add) + if(graphic.gas_id == GAS_HEAT || graphic.gas_id == GAS_COLD) + continue + add_vis_contents(graphic) + else + add_vis_contents(graphic_add) if(graphic_remove && LAZYLEN(graphic_remove)) remove_vis_contents(graphic_remove) diff --git a/code/ZAS/Zone.dm b/code/ZAS/Zone.dm index a1701988045..cc00ddecece 100644 --- a/code/ZAS/Zone.dm +++ b/code/ZAS/Zone.dm @@ -8,6 +8,7 @@ Class Vars: name - A name of the format "Zone [#]", used for debugging. invalid - True if the zone has been erased and is no longer eligible for processing. needs_update - True if the zone has been added to the update list. + update_revision - Incremented when needs_update flips on, and retained after needs_update is cleared. edges - A list of edges that connect to this zone. air - The gas mixture that any turfs in this zone will return. Values are per-tile with a group multiplier. @@ -47,6 +48,8 @@ Class Procs: var/list/fire_tiles var/needs_update = 0 + /// Counter incremented when zone is dirtied for processing. + var/update_revision = 0 var/list/connection_edge/edges @@ -172,6 +175,7 @@ Class Procs: to_chat(M, "P: [XGM_PRESSURE(air)] kPa V: [air.volume]L T: [air.temperature]�K ([air.temperature - T0C]�C)") to_chat(M, "O2 per N2: [(air.gas[GAS_NITROGEN] ? air.gas[GAS_OXYGEN]/air.gas[GAS_NITROGEN] : "N/A")] Moles: [air.total_moles]") to_chat(M, "Simulated: [contents.len] ([air.group_multiplier])") + to_chat(M, "Update Revision: [update_revision]") //to_chat(M, "Unsimulated: [unsimulated_contents.len]") //to_chat(M, "Edges: [LAZYLEN(edges)]") if(invalid) to_chat(M, "Invalid!") diff --git a/code/controllers/subsystems/air.dm b/code/controllers/subsystems/air.dm index 8e02a940141..5a228dd6c92 100644 --- a/code/controllers/subsystems/air.dm +++ b/code/controllers/subsystems/air.dm @@ -88,6 +88,9 @@ SUBSYSTEM_DEF(air) var/active_zones = 0 var/next_id = 1 + /// Prevents gas temp graphics from being attached to turfs on non-Horizon z-levels. + var/suppress_nonhorizon_temperature_graphics = FALSE + #ifdef ZASDBG var/updated = 0 #endif @@ -297,6 +300,36 @@ Total Unsimulated Turfs: [world.maxx*world.maxy*world.maxz - simulated_turf_coun if (processing_zones) processing_zones -= z +/// Suppresses or restores temperature graphics on every currently loaded non-station z-level. +/datum/controller/subsystem/air/proc/set_nonhorizon_temperature_graphics_suppressed(suppress) + suppress_nonhorizon_temperature_graphics = suppress + . = 0 + + for(var/z_level in 1 to world.maxz) + if(is_station_level(z_level)) + continue + + for(var/turf/simulated/T in Z_TURFS(z_level)) + var/list/temperature_graphics + + if(suppress) + for(var/obj/gas_overlay/graphic in T.vis_contents) + if(graphic.gas_id == GAS_HEAT || graphic.gas_id == GAS_COLD) + LAZYADD(temperature_graphics, graphic) + else if(TURF_HAS_VALID_ZONE(T)) + for(var/obj/gas_overlay/graphic in T.zone.air.graphic) + if(graphic.gas_id == GAS_HEAT || graphic.gas_id == GAS_COLD) + LAZYADD(temperature_graphics, graphic) + + if(LAZYLEN(temperature_graphics)) + if(suppress) + T.update_graphic(graphic_remove = temperature_graphics) + else + T.update_graphic(graphic_add = temperature_graphics) + . += length(temperature_graphics) + + CHECK_TICK + /datum/controller/subsystem/air/proc/air_blocked(turf/A, turf/B) #ifdef ZASDBG ASSERT(isturf(A)) @@ -386,6 +419,7 @@ Total Unsimulated Turfs: [world.maxx*world.maxy*world.maxz - simulated_turf_coun return zones_to_update += Z Z.needs_update = 1 + Z.update_revision++ /datum/controller/subsystem/air/proc/mark_edge_sleeping(connection_edge/E) #ifdef ZASDBG diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index ccd6100a6bf..55fae9f08cf 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -205,6 +205,7 @@ GLOBAL_LIST_INIT(admin_verbs_debug, list( /client/proc/cmd_admin_delete, /client/proc/cmd_debug_del_all, /client/proc/air_report, + /client/proc/toggle_nonhorizon_temperature_graphics, /client/proc/reload_admins, /client/proc/print_random_map, /client/proc/create_random_map, diff --git a/code/modules/admin/verbs/diagnostics.dm b/code/modules/admin/verbs/diagnostics.dm index 6e207974fe5..a4be61f11c8 100644 --- a/code/modules/admin/verbs/diagnostics.dm +++ b/code/modules/admin/verbs/diagnostics.dm @@ -41,6 +41,24 @@ usr << browse(HTML_SKELETON(output), "window=airreport") +/client/proc/toggle_nonhorizon_temperature_graphics() + set category = "Debug" + set name = "Toggle Non-Horizon Temperature Graphics" + set desc = "Toggle gas temperature graphics on all non-Horizon z-levels." + + if(!check_rights(R_DEBUG)) + return + if(!SSair) + to_chat(src, SPAN_WARNING("The air subsystem is unavailable.")) + return + + var/suppress = !SSair.suppress_nonhorizon_temperature_graphics + to_chat(src, SPAN_NOTICE("[suppress ? "Disabling" : "Re-enabling"] non-Horizon temperature graphics. This may take several ticks.")) + + var/updated_graphics = SSair.set_nonhorizon_temperature_graphics_suppressed(suppress) + log_and_message_admins("has [suppress ? "disabled" : "enabled"] GAS_HEAT and GAS_COLD graphics on non-Horizon z-levels ([updated_graphics] graphic reference\s updated).") + feedback_add_details("admin_verb", "TNTG") + /client/proc/fix_next_move() set category = "Debug" set name = "Unfreeze Everyone" diff --git a/code/modules/overmap/exoplanets/exoplanet.dm b/code/modules/overmap/exoplanets/exoplanet.dm index 549d807adc8..b77d74be98a 100644 --- a/code/modules/overmap/exoplanets/exoplanet.dm +++ b/code/modules/overmap/exoplanets/exoplanet.dm @@ -7,6 +7,10 @@ var/list/animals = list() var/max_animal_count var/datum/gas_mixture/atmosphere + /// Cached main ZAS surface zone per generated z-level. Resolved lazily after ZAS finishes building. + var/list/main_planetary_zones = list() + /// Last ZAS update revision processed per generated z-level. + var/list/main_planetary_zone_atmosphere_revisions = list() var/list/breathgas = list() //list of gases animals/plants require to survive var/badgas //id of gas that is toxic to life here @@ -222,6 +226,7 @@ generate_features() theme.after_map_generation(src) generate_landing(2) + finalize_outdoor_atmosphere() update_biome() generate_planet_image() START_PROCESSING(SSprocessing, src) @@ -272,17 +277,74 @@ if(!atmosphere) continue - var/zone/Z - for(var/i = 1 to maxx) - var/turf/simulated/T = locate(i, 2, zlevel) - if(istype(T) && T.zone && T.zone.contents.len > (maxx*maxy*0.25)) //if it's a zone quarter of zlevel, good enough odds it's planetary main one - Z = T.zone - break - if(Z && !length(Z.fire_tiles) && !atmosphere.compare(Z.air)) //let fire die out first if there is one + var/zone/Z = get_main_planetary_zone(zlevel) + if(!Z) + continue + var/zlevel_key = "[zlevel]" + var/last_atmosphere_revision = main_planetary_zone_atmosphere_revisions[zlevel_key] + if(!isnull(last_atmosphere_revision) && last_atmosphere_revision == Z.update_revision) + continue + // Let ZAS settle and fire die out first if there is one + if(Z.needs_update || length(Z.fire_tiles)) + continue + if(!atmosphere.compare(Z.air)) var/datum/gas_mixture/daddy = new() //make a fake 'planet' zone gas daddy.copy_from(atmosphere) daddy.group_multiplier = Z.air.group_multiplier Z.air.equalize(daddy) + SSair.mark_zone_update(Z) + main_planetary_zone_atmosphere_revisions[zlevel_key] = Z.update_revision + +/// Attempts to not only identify the largest air zone, but also cache it for subsequent ticks. +/obj/effect/overmap/visitable/sector/exoplanet/proc/get_main_planetary_zone(zlevel) + var/zlevel_key = "[zlevel]" + var/zone/cached_zone = main_planetary_zones[zlevel_key] + // ZAS may invalidate and rebuild zones after terrain changes. Keep the cached zone only while it is still valid! + if(cached_zone && !cached_zone.invalid) + return cached_zone + + main_planetary_zones -= zlevel_key + main_planetary_zone_atmosphere_revisions -= zlevel_key + + // Exoplanet transition edges are unsimulated, so only scan the generated interior when resolving the main surface zone. + var/min_x = TRANSITIONEDGE + 1 + var/min_y = TRANSITIONEDGE + 1 + var/max_x = maxx - (TRANSITIONEDGE + 1) + var/max_y = maxy - (TRANSITIONEDGE + 1) + var/turf/lower_left = locate(min_x, min_y, zlevel) + var/turf/upper_right = locate(max_x, max_y, zlevel) + if(!lower_left || !upper_right) + return null + + // If it's a zone quarter of zlevel, good enough odds it's planetary main one. + var/minimum_zone_size = maxx * maxy * 0.25 + var/zone/best_zone + var/best_zone_size = 0 + var/list/checked_zones = list() + var/zas_pending = FALSE + for(var/turf/simulated/T in block(lower_left, upper_right)) + // Dynamic turf generation queues ZAS updates... Do not cache a main zone while the surface may still be settling! + if(T.needs_air_update) + zas_pending = TRUE + continue + if(!TURF_HAS_VALID_ZONE(T)) + continue + var/zone/current_zone = T.zone + if(current_zone in checked_zones) + continue + checked_zones += current_zone + var/current_zone_size = current_zone.contents.len + if(current_zone_size <= best_zone_size || current_zone_size <= minimum_zone_size) + continue + best_zone = current_zone + best_zone_size = current_zone_size + + // Try again on a later processing tick once ZAS has finished assigning/rebuilding the planet's surface zones + if(zas_pending) + return null + if(best_zone) + main_planetary_zones[zlevel_key] = best_zone + return best_zone /obj/effect/overmap/visitable/sector/exoplanet/proc/remove_animal(mob/M) animals -= M @@ -318,6 +380,56 @@ /obj/effect/overmap/visitable/sector/exoplanet/proc/generate_features() spawned_features = seedRuins(map_z, features_budget, possible_features, /area/exoplanet, maxx, maxy) +/** + * Applies the generated planetary atmosphere AFTER terrain and ruins finish loading. + * + * Exoplanet turf subtypes initialize themselves with the planet's air, but ruins and + * terrain generation may place ordinary simulated turfs which otherwise retain their + * type's standard atmosphere. The area's mapped is_outside value is authoritative: + * OUTSIDE_NO preserves sealed interiors, while every outdoor turf receives planetary air. + * + * Without this behavior, you see weird cross-contamination issues, especially in cases + * where maps don't have their /areas configured properly. + */ +/obj/effect/overmap/visitable/sector/exoplanet/proc/finalize_outdoor_atmosphere() + if(!atmosphere) + return + + var/list/outdoor_zones = list() + for(var/zlevel in map_z) + var/turf/lower_left = locate(TRANSITIONEDGE + 1, TRANSITIONEDGE + 1, zlevel) + var/turf/upper_right = locate(maxx - (TRANSITIONEDGE + 1), maxy - (TRANSITIONEDGE + 1), zlevel) + if(!lower_left || !upper_right) + continue + + for(var/turf/simulated/T in block(lower_left, upper_right)) + var/area/A = get_area(T) + if(!A || A.is_outside == OUTSIDE_NO) + continue + + T.initial_gas = atmosphere.gas.Copy() + T.temperature = atmosphere.temperature + + // Dynamic z-levels are created after SSair initializes. Usually these turfs are + // still unzoned... but if ZAS already found them, take care not to break them. + if(TURF_HAS_VALID_ZONE(T)) + outdoor_zones |= T.zone + else if(T.air) + T.air.group_multiplier = 1 + T.air.copy_from(atmosphere) + else + T.make_air() + + CHECK_TICK + + // A zone stores ONE shared mixture, so update it once rather than once per member turf. + for(var/zone/Z as anything in outdoor_zones) + if(Z.invalid) + continue + Z.air.copy_from(atmosphere) + SSair.mark_zone_update(Z) + CHECK_TICK + /obj/effect/overmap/visitable/sector/exoplanet/proc/update_biome() for(var/datum/seed/S as anything in seeds) adapt_seed(S) diff --git a/html/changelogs/Bat-AtmosOptimization.yml b/html/changelogs/Bat-AtmosOptimization.yml new file mode 100644 index 00000000000..583ab6fc7e9 --- /dev/null +++ b/html/changelogs/Bat-AtmosOptimization.yml @@ -0,0 +1,7 @@ +author: Batrachophrenoboocosmomachia +delete-after: True +changes: + - code_imp: "Exoplanets now cache their main outdoor atmospheric region instead of trying to search their entire map once per second." + - code_imp: "Exoplanet atmospheric calculations now run only when necessary using new update_revision counter." + - code_imp: "Exoplanet atmospheres now wait until all random ruins have been placed before trying to resolve the air everywhere." + - admin: "Adds new admin debug verb, 'Toggle Non-Horizon Temperature Graphics', which toggles all off-ship gas temp graphics to address lag in a pinch, if still necessary, without removing actual environmental hazard."