var/global/datum/lighting_corner/dummy/dummy_lighting_corner = new // Because we can control each corner of every lighting overlay. // And corners get shared between multiple turfs (unless you're on the corners of the map, then 1 corner doesn't). // For the record: these should never ever ever be deleted, even if the turf doesn't have dynamic lighting. // This list is what the code that assigns corners listens to, the order in this list is the order in which corners are added to the /turf/corners list. var/global/list/LIGHTING_CORNER_DIAGONAL = list(NORTHEAST, SOUTHEAST, SOUTHWEST, NORTHWEST) // This is the reverse of the above - the position in the array is a dir. Update this if the above changes. var/global/list/REVERSE_LIGHTING_CORNER_DIAGONAL = list(0, 0, 0, 0, 3, 4, 0, 0, 2, 1) /datum/lighting_corner // t1 through t4 are our masters, in no particular order. // They are split into vars like this in the interest of reducing memory usage. // tX is the turf itself, tXi is the index of this corner in that turf's corners list. var/turf/t1 var/t1i var/turf/t2 var/t2i var/turf/t3 var/t3i var/turf/t4 var/t4i var/list/datum/light_source/affecting // Light sources affecting us. var/active = FALSE // TRUE if one of our masters has dynamic lighting. var/x = 0 var/y = 0 var/z = 0 // Our own intensity, from lights directly shining on us. var/self_r = 0 var/self_g = 0 var/self_b = 0 // The intensity we're inheriting from the turfs below us, if we're a Z-turf. This is a sum of all below Z-turfs. var/below_r = 0 var/below_g = 0 var/below_b = 0 // Ambient turf lighting that's not inherited from a light source. These are updated as absolute values. var/ambient_r = 0 var/ambient_g = 0 var/ambient_b = 0 // The turf above us' ambient var/above_ambient_r = 0 var/above_ambient_g = 0 var/above_ambient_b = 0 // The final intensity, all things considered. var/apparent_r = 0 var/apparent_g = 0 var/apparent_b = 0 var/needs_update = FALSE var/cache_r = 0 var/cache_g = 0 var/cache_b = 0 var/cache_mx = 0 /// Used for planet lighting. Probably needs a better system to prevent over-updating when not needed at some point. var/update_gen = 0 /datum/lighting_corner/New(turf/new_turf, diagonal, oi) SSlighting.total_lighting_corners += 1 var/has_ambience = FALSE t1 = new_turf z = new_turf.z t1i = oi if (TURF_IS_AMBIENT_LIT_UNSAFE(new_turf)) has_ambience = TRUE var/vertical = diagonal & ~(diagonal - 1) // The horizontal directions (4 and 8) are bigger than the vertical ones (1 and 2), so we can reliably say the lsb is the horizontal direction. var/horizontal = diagonal & ~vertical // Now that we know the horizontal one we can get the vertical one. x = new_turf.x + (horizontal == EAST ? 0.5 : -0.5) y = new_turf.y + (vertical == NORTH ? 0.5 : -0.5) // My initial plan was to make this loop through a list of all the dirs (horizontal, vertical, diagonal). // Issue being that the only way I could think of doing it was very messy, slow and honestly overengineered. // So we'll have this hardcode instead. var/turf/T // Diagonal one is easy. T = get_step(new_turf, diagonal) if (T) // In case we're on the map's border. if (!T.corners) T.corners = new(4) t2 = T t2i = REVERSE_LIGHTING_CORNER_DIAGONAL[diagonal] T.corners[t2i] = src if (TURF_IS_AMBIENT_LIT_UNSAFE(T)) has_ambience = TRUE // Now the horizontal one. T = get_step(new_turf, horizontal) if (T) // Ditto. if (!T.corners) T.corners = new(4) t3 = T t3i = REVERSE_LIGHTING_CORNER_DIAGONAL[((T.x > x) ? EAST : WEST) | ((T.y > y) ? NORTH : SOUTH)] // Get the dir based on coordinates. T.corners[t3i] = src if (TURF_IS_AMBIENT_LIT_UNSAFE(T)) has_ambience = TRUE // And finally the vertical one. T = get_step(new_turf, vertical) if (T) if (!T.corners) T.corners = new(4) t4 = T t4i = REVERSE_LIGHTING_CORNER_DIAGONAL[((T.x > x) ? EAST : WEST) | ((T.y > y) ? NORTH : SOUTH)] // Get the dir based on coordinates. T.corners[t4i] = src if (TURF_IS_AMBIENT_LIT_UNSAFE(T)) has_ambience = TRUE update_active() if (has_ambience) init_ambient() #define OVERLAY_PRESENT(T) (T && T.lighting_overlay) /datum/lighting_corner/proc/update_active() active = FALSE if (OVERLAY_PRESENT(t1) || OVERLAY_PRESENT(t2) || OVERLAY_PRESENT(t3) || OVERLAY_PRESENT(t4)) active = TRUE #undef OVERLAY_PRESENT #define GET_ABOVE(T) (T.above()) #define GET_BELOW(T) (T.below()) #define UPDATE_APPARENT(T, CH) T.apparent_##CH = T.self_##CH + T.below_##CH + T.ambient_##CH + T.above_ambient_##CH /datum/lighting_corner/proc/init_ambient() var/sum_r = 0 var/sum_g = 0 var/sum_b = 0 var/turf/T for (var/i in 1 to 4) // this is ugly as fuck, but it's still more legible than doing this with a macro switch (i) if (1) T = t1 if (2) T = t2 if (3) T = t3 if (4) T = t4 if (!T || !T.ambient_light) continue var/list/parts = rgb2num(T.ambient_light) sum_r += (parts[1] / 255) * T.ambient_light_multiplier sum_g += (parts[2] / 255) * T.ambient_light_multiplier sum_b += (parts[3] / 255) * T.ambient_light_multiplier sum_r /= 4 sum_g /= 4 sum_b /= 4 update_ambient_lumcount(sum_r, sum_g, sum_b) // God that was a mess, now to do the rest of the corner code! Hooray! /datum/lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b, now = FALSE) if (!(delta_r + delta_g + delta_b)) return self_r += delta_r self_g += delta_g self_b += delta_b UPDATE_APPARENT(src, r) UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) var/turf/T var/Ti // Grab the first master that's a Z-turf, if one exists. // The above var cannot be relied on due to init ordering, but we can use it if it is set. if (t1 && (T = t1.above || GET_ABOVE(t1)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = t1i else if (t2 && (T = t2.above || GET_ABOVE(t2)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = t2i else if (t3 && (T = t3.above || GET_ABOVE(t3)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = t3i else if (t4 && (T = t4.above || GET_ABOVE(t4)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = t4i else // Nothing above us that cares about below light. T = null if (T) do if (!T.corners || !T.corners[Ti]) T.generate_missing_corners() // These never get instant updates; they're less important, so better to avoid risk of lag. T.corners[Ti].update_below_lumcount(delta_r, delta_g, delta_b) while ((T = T.above) && (T.mz_flags & MZ_ALLOW_LIGHTING)) // This needs to be down here instead of the above if so the lum values are properly updated. if (needs_update) return if (now) update_overlays(TRUE) else needs_update = TRUE SSlighting.corner_queue += src /datum/lighting_corner/proc/update_below_lumcount(delta_r, delta_g, delta_b, now = FALSE) if (!(delta_r + delta_g + delta_b)) return below_r += delta_r below_g += delta_g below_b += delta_b UPDATE_APPARENT(src, r) UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) // This needs to be down here instead of the above if so the lum values are properly updated. if (needs_update) return if (!now) needs_update = TRUE SSlighting.corner_queue += src else update_overlays(TRUE) /datum/lighting_corner/proc/update_ambient_lumcount(delta_r, delta_g, delta_b, skip_update = FALSE) ambient_r += delta_r ambient_g += delta_g ambient_b += delta_b UPDATE_APPARENT(src, r) UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) var/turf/T var/Ti if (t1) T = t1 Ti = t1i else if (t2) T = t2 Ti = t2i else if (t3) T = t3 Ti = t3i else if (t4) T = t4 Ti = t4i else // This should be impossible to reach -- how do we exist without at least one master turf? CRASH("Corner has no masters!") var/datum/lighting_corner/below = src var/turf/lasT // We init before Z-Mimic, cannot rely on above/below. while ((lasT = T) && (T = T.below || GET_BELOW(T)) && (lasT.mz_flags & MZ_ALLOW_LIGHTING) && TURF_IS_DYNAMICALLY_LIT_UNSAFE(T)) T.ambient_has_indirect = TRUE if (!T.corners || !T.corners[Ti]) T.generate_missing_corners() ASSERT(T.corners?.len) below = T.corners[Ti] below.above_ambient_r += delta_r below.above_ambient_g += delta_g below.above_ambient_b += delta_b UPDATE_APPARENT(below, r) UPDATE_APPARENT(below, g) UPDATE_APPARENT(below, b) if (!skip_update && !below.needs_update) below.needs_update = TRUE SSlighting.corner_queue += below if (needs_update || skip_update) return // Always queue for this, not important enough to hit the synchronous path. needs_update = TRUE SSlighting.corner_queue += src //? This tonemap is nice, but rounding kills a lot of the details. // #define ACES_RRT_ODT_TONEMAP(X) clamp((X * (X + 0.0245786) - 0.000090537) / (X * (0.983729 * X + 0.4329510) + 0.238081), 0, 1) //? Don't even attempt to use with current darksight... // #define ACES_TONEMAP(X) clamp((X * (2.51 * X + 0.03)) / (X * (2.43 * X + 0.59) + 0.14), 0, 1) /datum/lighting_corner/proc/update_overlays(now = FALSE) var/lr = apparent_r var/lg = apparent_g var/lb = apparent_b // Cache these values a head of time so 4 individual lighting overlays don't all calculate them individually. var/mx = max(lr, lg, lb) // Scale it so 1 is the strongest lum, if it is above 1. . = 1 // factor if (mx > 1) . = 1 / mx cache_r = round(lr * ., LIGHTING_ROUND_VALUE) cache_g = round(lg * ., LIGHTING_ROUND_VALUE) cache_b = round(lb * ., LIGHTING_ROUND_VALUE) cache_mx = round(mx, LIGHTING_ROUND_VALUE) var/turf/T for (var/i in 1 to 4) // this is ugly as fuck, but it's still more legible than doing this with a macro switch (i) if (1) T = t1 if (2) T = t2 if (3) T = t3 if (4) T = t4 var/atom/movable/lighting_overlay/Ov if (T && (Ov = T.lighting_overlay)) if (now) Ov.update_overlay() else if (!Ov.needs_update) Ov.needs_update = TRUE SSlighting.overlay_queue += Ov /// This is called when our turf's downward Z-opacity changes. /datum/lighting_corner/proc/rebuild_ztraversal(new_opacity) /* If opacity transitions to off: - Look down stack, removing below lights contributing to us - Look up stack, updating turfs with our new below lighting value If opacity transitions to on: - Look down stack, add below contributors - Look up stack, updating turfs with our new below lighting value */ below_r = below_g = below_b = 0 var/turf/T = null var/datum/lighting_corner/Tcorn = src var/Ti if (!new_opacity) for (;;) if (Tcorn.t1 && (T = Tcorn.t1.below || GET_BELOW(Tcorn.t1)) && (T.above?.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t1i else if (Tcorn.t2 && (T = Tcorn.t2.below || GET_BELOW(Tcorn.t2)) && (T.above?.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t2i else if (Tcorn.t3 && (T = Tcorn.t3.below || GET_BELOW(Tcorn.t3)) && (T.above?.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t3i else if (Tcorn.t4 && (T = Tcorn.t4.below || GET_BELOW(Tcorn.t4)) && (T.above?.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t4i else // Nothing above us that cares about below light. break Tcorn = T.corners[Ti] below_r += Tcorn.apparent_r below_g += Tcorn.apparent_g below_b += Tcorn.apparent_b UPDATE_APPARENT(src, r) UPDATE_APPARENT(src, g) UPDATE_APPARENT(src, b) if (!needs_update) needs_update = TRUE SSlighting.corner_queue += src T = null Tcorn = src for (;;) if (Tcorn.t1 && (T = Tcorn.t1.above || GET_ABOVE(Tcorn.t1)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t1i else if (Tcorn.t2 && (T = Tcorn.t2.above || GET_ABOVE(Tcorn.t2)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t2i else if (Tcorn.t3 && (T = Tcorn.t3.above || GET_ABOVE(Tcorn.t3)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t3i else if (Tcorn.t4 && (T = Tcorn.t4.above || GET_ABOVE(Tcorn.t4)) && (T.mz_flags & MZ_ALLOW_LIGHTING)) Ti = Tcorn.t4i else // Nothing above us that cares about below light. break Tcorn = T.corners[Ti] Tcorn.below_r += apparent_r Tcorn.below_g += apparent_g Tcorn.below_b += apparent_b UPDATE_APPARENT(Tcorn, r) UPDATE_APPARENT(Tcorn, g) UPDATE_APPARENT(Tcorn, b) if (!Tcorn.needs_update) Tcorn.needs_update = TRUE SSlighting.corner_queue += Tcorn #undef UPDATE_APPARENT /datum/lighting_corner/Destroy(force = FALSE) stack_trace("Someone [force ? "force-" : ""]deleted a lighting corner.") if (!force) return QDEL_HINT_LETMELIVE SSlighting.total_lighting_corners -= 1 return ..() /datum/lighting_corner/dummy/New() return