From 5dda05aa635c4974000ab191dda9803c80c3e1e3 Mon Sep 17 00:00:00 2001 From: Will <7099514+Willburd@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:36:46 -0400 Subject: [PATCH] Solarpanel Tracker Subsystem (#19252) * look at this chonker * documenting * various fixes * use planet's sub position * fix that up * occlusion isn't needed on planet, use weather light * respect outdoors * fix * hardref cleanup --- code/__defines/subsystems.dm | 1 + code/controllers/subsystems/solars.dm | 91 +++++++++++++++++++++++++++ code/controllers/subsystems/sun.dm | 15 +---- code/modules/planet/planet.dm | 4 ++ code/modules/power/solar.dm | 77 ++++++++++++++--------- vorestation.dme | 1 + 6 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 code/controllers/subsystems/solars.dm diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index c29c93af6c6..7329808b5b6 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -137,6 +137,7 @@ #define FIRE_PRIORITY_DEFAULT 50 #define FIRE_PRIORITY_TICKER 60 #define FIRE_PRIORITY_PLANETS 75 +#define FIRE_PRIORITY_SOLARS 76 #define FIRE_PRIORITY_PRIORITY_EFFECTS 90 #define FIRE_PRIORITY_EXPLOSIONS 90 #define FIRE_PRIORITY_MACHINES 100 diff --git a/code/controllers/subsystems/solars.dm b/code/controllers/subsystems/solars.dm new file mode 100644 index 00000000000..0aafc488f16 --- /dev/null +++ b/code/controllers/subsystems/solars.dm @@ -0,0 +1,91 @@ +SUBSYSTEM_DEF(solars) + name = "Solars" + priority = FIRE_PRIORITY_SOLARS + wait = 1 MINUTE + flags = SS_NO_INIT + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + dependencies = list( + /datum/controller/subsystem/planets, + /datum/controller/subsystem/sun + ) + + // List of solar controllers that need to be prepared for the second half of processing + var/list/current_run + + // Each list has a key of its controller, for each subrun of the subsystem + var/list/controller_run = list() + var/list/panel_run = list() + var/list/panel_sum = list() + +/datum/controller/subsystem/solars/fire(resumed) + if(!resumed) + // Get the list of controllers we need to process + current_run = GLOB.solars_list.Copy() + // Clear secondary process lists so they're fresh for the impending run ahead + controller_run.Cut() + panel_run.Cut() + panel_sum.Cut() + + //////////////////////////////////////////////////////////////////////////////// + // First processing cycle collects the controllers we'll be processing + //////////////////////////////////////////////////////////////////////////////// + while(length(current_run)) + var/obj/machinery/power/solar_control/SC = current_run[length(current_run)] + current_run.len-- + + // Controllers with no network are ignored + if(!SC.powernet) + GLOB.solars_list.Remove(SC) + if(MC_TICK_CHECK) + return + continue + + // Update the controller and prepare each of the solar array lists it needs + SC.update() + controller_run[REF(SC)] = WEAKREF(SC) + panel_run[REF(SC)] = SC.get_connected_panels().Copy() + panel_sum[REF(SC)] = 0 + + if(MC_TICK_CHECK) + return + + //////////////////////////////////////////////////////////////////////////////// + // Second processing cycle handles all of the panels for each controller! + //////////////////////////////////////////////////////////////////////////////// + while(length(controller_run)) + var/conkey = controller_run[length(controller_run)] + var/datum/weakref/conref= controller_run[conkey] + + // Check if the controller still exists + var/obj/machinery/power/solar_control/SC = conref?.resolve() + if(!SC) + controller_run -= conkey + if(MC_TICK_CHECK) + return + continue + + // Handle all solar panels for this controller. + var/list/handling_panels = panel_run[conkey] + while(length(handling_panels)) + var/obj/machinery/power/solar/S = handling_panels[length(handling_panels)] + panel_sum[conkey] += S.update_power_generation(SC) + handling_panels.len-- + + if(MC_TICK_CHECK) + return + + // Update the controller + SC.connected_power = panel_sum[conkey] + SC.update_icon() + controller_run.len-- + + if(MC_TICK_CHECK) + return + +/datum/controller/subsystem/solars/proc/get_solar_angle(turf/our_t) + if(!our_t || our_t.z > length(SSplanets.z_to_planet) || !SSplanets.z_to_planet[our_t.z]) + return SSsun.sun.angle // standard in space solar panels use the global SSsun angle + + // On planets, use the daynight cycle + var/datum/planet/our_planet = SSplanets.z_to_planet[our_t.z] + return our_planet.get_sun_solar_position() diff --git a/code/controllers/subsystems/sun.dm b/code/controllers/subsystems/sun.dm index 00b0c6bec14..99c2de976d0 100644 --- a/code/controllers/subsystems/sun.dm +++ b/code/controllers/subsystems/sun.dm @@ -1,22 +1,9 @@ SUBSYSTEM_DEF(sun) name = "Sun" - wait = 600 + wait = 1 MINUTE flags = SS_NO_INIT var/static/datum/sun/sun = new - var/list/current_run /datum/controller/subsystem/sun/fire(resumed) if(!resumed) - current_run = GLOB.solars_list.Copy() sun.calc_position() - - //now tell the solar control computers to update their status and linked devices - while(length(current_run)) - var/obj/machinery/power/solar_control/SC = current_run[length(current_run)] - current_run.len-- - if(!SC.powernet) - GLOB.solars_list.Remove(SC) - continue - SC.update() - if(MC_TICK_CHECK) - return diff --git a/code/modules/planet/planet.dm b/code/modules/planet/planet.dm index 2b516844325..ab4316ffc4a 100644 --- a/code/modules/planet/planet.dm +++ b/code/modules/planet/planet.dm @@ -66,3 +66,7 @@ sun["brightness"] = CLAMP01(new_brightness) sun["color"] = new_color needs_work |= PLANET_PROCESS_SUN + +/// Override for unique sun angle handling for stuff like northern/southern hemisphere sun angles during the day cycle +/datum/planet/proc/get_sun_solar_position() + return 220 - (sun_position * 80) // this base version doesn't know how long a planet's day is, so just goes back and forth facing south-eastish based on midnight to noon intensity diff --git a/code/modules/power/solar.dm b/code/modules/power/solar.dm index f0abe82a9ab..2541b2307a2 100644 --- a/code/modules/power/solar.dm +++ b/code/modules/power/solar.dm @@ -108,8 +108,8 @@ GLOBAL_LIST_EMPTY(solars_list) return //find the smaller angle between the direction the panel is facing and the direction of the sun (the sign is not important here) - var/p_angle = min(abs(adir - SSsun.sun.angle), 360 - abs(adir - SSsun.sun.angle)) - + var/source_angle = SSsolars.get_solar_angle(get_turf(src)) + var/p_angle = min(abs(adir - source_angle), 360 - abs(adir - source_angle)) if(p_angle > 90) // if facing more than 90deg from sun, zero output sunfrac = 0 return @@ -160,33 +160,56 @@ GLOBAL_LIST_EMPTY(solars_list) //trace towards sun to see if we're in shadow /obj/machinery/power/solar/proc/occlusion() + var/turf/our_t = get_turf(src) + var/datum/planet/our_planet + if(!our_t || our_t.z > length(SSplanets.z_to_planet) || !SSplanets.z_to_planet[our_t.z]) + // If we are NOT on a planet, we check toward the edge of the map, otherwise we're going to assume the sun is above us on a planet + var/ax = x // start at the solar panel + var/ay = y + var/turf/T = null - var/ax = x // start at the solar panel - var/ay = y - var/turf/T = null + for(var/i = 1 to 20) // 20 steps is enough + ax += SSsun.sun.dx // do step + ay += SSsun.sun.dy - for(var/i = 1 to 20) // 20 steps is enough - ax += SSsun.sun.dx // do step - ay += SSsun.sun.dy + T = locate( round(ax,0.5),round(ay,0.5),z) - T = locate( round(ax,0.5),round(ay,0.5),z) + if(!T || T.x == 1 || T.x==world.maxx || T.y==1 || T.y==world.maxy) // not obscured if we reach the edge + break - if(!T || T.x == 1 || T.x==world.maxx || T.y==1 || T.y==world.maxy) // not obscured if we reach the edge - break - - if(T.opacity) // if we hit a solid turf, panel is obscured - obscured = 1 - return + if(T.opacity) // if we hit a solid turf, panel is obscured + obscured = 1 + return + else + // If we are on a planet, get it for later so we can change the intensity of the light we recieve + our_planet = SSplanets.z_to_planet[our_t.z] obscured = 0 // if hit the edge or stepped 20 times, not obscured update_solar_exposure() + // Use the actual brightness of time and weather if we are on a planet, check if we're not blocked above by seeing what our outdoors status is + if(our_planet) + sunfrac *= our_t.is_outdoors() ? our_planet.sun["brightness"] : 0 + +/// Updates the power generation of a solar panel. +/obj/machinery/power/solar/proc/update_power_generation(obj/machinery/power/solar_control/SC) + adir = SC.cdir //instantly rotates the panel + occlusion()//and + update_icon() //update it + var/sgen = get_power_supplied() + var/list/panel_list = SC.get_connected_panels() + panel_list[src] = sgen + return sgen + /// Looks nice but doesn't generate power. /obj/machinery/power/solar/fake /obj/machinery/power/solar/fake/get_power_supplied() return 0 +/obj/machinery/power/solar/fake/update_power_generation() + return 0 + // // Solar Assembly - For construction of solar arrays. // @@ -305,7 +328,7 @@ GLOBAL_LIST_EMPTY(solars_list) track = 2 // Auto tracking mode. search_for_connected() if(connected_tracker) - connected_tracker.set_angle(SSsun.sun.angle) + connected_tracker.set_angle(SSsolars.get_solar_angle(get_turf(src))) set_panels(cdir) /obj/machinery/power/solar_control/proc/add_panel(var/obj/machinery/power/solar/P) @@ -317,6 +340,11 @@ GLOBAL_LIST_EMPTY(solars_list) /obj/machinery/power/solar_control/proc/remove_panel(var/obj/machinery/power/solar/P) connected_power -= connected_panels[P] connected_panels.Remove(P) + SSsolars.panel_run[REF(src)] -= P // clear hardref in subsystem + +/obj/machinery/power/solar_control/proc/get_connected_panels() + RETURN_TYPE(/list) + return connected_panels /obj/machinery/power/solar_control/drain_power() return -1 @@ -359,9 +387,7 @@ GLOBAL_LIST_EMPTY(solars_list) cdir = targetdir //...the current direction is the targetted one (and rotates panels to it) if(2) // auto-tracking if(connected_tracker) - connected_tracker.set_angle(SSsun.sun.angle) - - set_panels(cdir) + connected_tracker.set_angle(SSsolars.get_solar_angle(get_turf(src))) /obj/machinery/power/solar_control/update_icon() if(stat & BROKEN) @@ -395,7 +421,7 @@ GLOBAL_LIST_EMPTY(solars_list) data["generated"] = round(connected_power) data["generated_ratio"] = data["generated"] / round(max(connected_panels.len, 1) * GLOB.solar_gen_rate) - data["sun_angle"] = SSsun.sun.angle + data["sun_angle"] = SSsolars.get_solar_angle(get_turf(src)) data["array_angle"] = cdir data["rotation_rate"] = trackrate data["max_rotation_rate"] = 7200 @@ -488,7 +514,7 @@ GLOBAL_LIST_EMPTY(solars_list) track = mode if(track == 2) if(connected_tracker) - connected_tracker.set_angle(SSsun.sun.angle) + connected_tracker.set_angle(SSsolars.get_solar_angle(get_turf(src))) set_panels(cdir) else if(track == 1) //begin manual tracking targetdir = cdir @@ -501,16 +527,11 @@ GLOBAL_LIST_EMPTY(solars_list) search_for_connected() return TRUE -//rotates the panel to the passed angle +/// rotates all connected panels to the passed angle, very expensive as it does them all at once in a single frame. This is what SSsolars does, but much more rude about it. /obj/machinery/power/solar_control/proc/set_panels(var/cdir) var/sum = 0 for(var/obj/machinery/power/solar/S in connected_panels) - S.adir = cdir //instantly rotates the panel - S.occlusion()//and - S.update_icon() //update it - var/sgen = S.get_power_supplied() - connected_panels[S] = sgen - sum += sgen + sum += S.update_power_generation(src) connected_power = sum update_icon() diff --git a/vorestation.dme b/vorestation.dme index 30eb2efe513..f06ca4c7fca 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -600,6 +600,7 @@ #include "code\controllers\subsystems\server_maint.dm" #include "code\controllers\subsystems\shuttles.dm" #include "code\controllers\subsystems\skybox.dm" +#include "code\controllers\subsystems\solars.dm" #include "code\controllers\subsystems\sounds.dm" #include "code\controllers\subsystems\speech_controller.dm" #include "code\controllers\subsystems\sqlite.dm"