From de6664dd08e6644844c797aeb32525617be41998 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:16:04 -0400 Subject: [PATCH 1/8] Add files via upload --- DynamicReflections/DynamicReflections.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 0d449e2..c3aca73 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -64,6 +64,10 @@ public class DynamicReflections : Mod internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; internal static bool shouldSkipWaterOverlay; + internal static bool shouldDeferWaterReflectionPresentation; + internal static bool shouldDeferSkyReflectionPresentation; + internal static bool isRenderingTopmostBackgroundWaterMask; + internal static readonly HashSet originalWaterMaskTiles = new HashSet(); // Puddle reflection variables internal static bool shouldDrawPuddlesReflection; @@ -101,6 +105,7 @@ public class DynamicReflections : Mod internal static RenderTarget2D mirrorsLayerRenderTarget; internal static RenderTarget2D mirrorsFurnitureRenderTarget; internal static RenderTarget2D puddlesRenderTarget; + internal static RenderTarget2D backgroundWaterMaskRenderTarget; internal static RasterizerState rasterizer; public override void Entry(IModHelper helper) @@ -1373,6 +1378,7 @@ internal void LoadRenderers() RegenerateRenderer(ref playerWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref playerPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref puddlesRenderTarget, shouldUseScreenDimensions); + RegenerateRenderer(ref backgroundWaterMaskRenderTarget, shouldUseScreenDimensions); RegenerateRenderer(ref mirrorsLayerRenderTarget, shouldUseScreenDimensions); RegenerateRenderer(ref mirrorsFurnitureRenderTarget, shouldUseScreenDimensions); From 271c388ae62ae1ca58729ea3a2a4f4085c2c38c6 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:22:30 -0400 Subject: [PATCH 2/8] Add files via upload --- .../Patches/Locations/GameLocationPatch.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs index a68b2a2..753deae 100644 --- a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs +++ b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs @@ -32,6 +32,7 @@ internal void Apply(Harmony harmony) { harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.drawWater), new[] { typeof(SpriteBatch) }), prefix: new HarmonyMethod(GetType(), nameof(DrawWaterPrefix))); harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.drawWater), new[] { typeof(SpriteBatch) }), prefix: new HarmonyMethod(GetType(), nameof(VisibleFishDrawPrefix))); + harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.isWaterTile), new[] { typeof(int), typeof(int) }), prefix: new HarmonyMethod(GetType(), nameof(IsWaterTilePrefix))); harmony.CreateReversePatcher(AccessTools.Method(_type, nameof(GameLocation.drawWater), new[] { typeof(SpriteBatch) }), new HarmonyMethod(GetType(), nameof(DrawWaterReversePatch))).Patch(); harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.UpdateWhenCurrentLocation), new[] { typeof(GameTime) }), postfix: new HarmonyMethod(GetType(), nameof(UpdateWhenCurrentLocationPostfix))); @@ -63,6 +64,18 @@ private static bool DrawWaterPrefix(GameLocation __instance, SpriteBatch b) return true; } + private static bool IsWaterTilePrefix(GameLocation __instance, int xTile, int yTile, ref bool __result) + { + if (DynamicReflections.isRenderingTopmostBackgroundWaterMask is false || !ReferenceEquals(__instance, Game1.currentLocation)) + { + return true; + } + + __result = DynamicReflections.originalWaterMaskTiles.Contains(new Point(xTile, yTile)) + && LayerToolkit.IsTopmostVisibleBackgroundWater(__instance, xTile, yTile); + return false; + } + internal static void DrawWaterReversePatch(GameLocation __instance, SpriteBatch b) { new NotImplementedException("It's a stub!"); From 09d53e7f76d3ad4829d64db7e6ccfb4028f777a3 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:24:33 -0400 Subject: [PATCH 3/8] Add files via upload --- .../Framework/Patches/xTile/LayerPatch.cs | 130 +++++++++++++----- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 6557a9c..41bb9c6 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -4,18 +4,11 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewValley; -using StardewValley.Buildings; -using StardewValley.Locations; -using StardewValley.Menus; using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; +using System.Reflection; using xTile.Dimensions; using xTile.Display; using xTile.Layers; -using xTile.Tiles; -using Object = StardewValley.Object; namespace DynamicReflections.Framework.Patches.Tiles { @@ -31,41 +24,57 @@ internal LayerPatch(IMonitor modMonitor, IModHelper modHelper) : base(modMonitor internal void Apply(Harmony harmony) { - harmony.Patch(AccessTools.Method(_object, "DrawNormal", new[] { typeof(IDisplayDevice), typeof(xTile.Dimensions.Rectangle), typeof(xTile.Dimensions.Location), typeof(int), typeof(float) }), prefix: new HarmonyMethod(GetType(), nameof(DrawNormalPrefix))); - harmony.Patch(AccessTools.Method(_object, "DrawNormal", new[] { typeof(IDisplayDevice), typeof(xTile.Dimensions.Rectangle), typeof(xTile.Dimensions.Location), typeof(int), typeof(float) }), postfix: new HarmonyMethod(GetType(), nameof(DrawNormalPostfix))); + MethodInfo drawNormal = AccessTools.Method(_object, "DrawNormal", new[] + { + typeof(IDisplayDevice), + typeof(xTile.Dimensions.Rectangle), + typeof(xTile.Dimensions.Location), + typeof(int), + typeof(float) + }); + + harmony.Patch(drawNormal, prefix: new HarmonyMethod(GetType(), nameof(DrawNormalPrefix))); + harmony.Patch(drawNormal, postfix: new HarmonyMethod(GetType(), nameof(DrawNormalPostfix))); + harmony.CreateReversePatcher(drawNormal, new HarmonyMethod(GetType(), nameof(DrawNormalReversePatch))).Patch(); + + PatchPyTkLayerDraw(harmony, "Platonymous.Toolkit", "PyTK.Extensions.PyMaps, PyTK", "PyTK"); + PatchPyTkLayerDraw(harmony, "Platonymous.TMXLoader", "TMXLoader.PyMaps, TMXLoader", "TMXLoader"); + } - harmony.CreateReversePatcher(AccessTools.Method(_object, "DrawNormal", new[] { typeof(IDisplayDevice), typeof(xTile.Dimensions.Rectangle), typeof(xTile.Dimensions.Location), typeof(int), typeof(float) }), new HarmonyMethod(GetType(), nameof(DrawNormalReversePatch))).Patch(); + private void PatchPyTkLayerDraw(Harmony harmony, string modId, string typeName, string displayName) + { + if (!DynamicReflections.modHelper.ModRegistry.IsLoaded(modId)) + { + return; + } - // Perform PyTK related patches - if (DynamicReflections.modHelper.ModRegistry.IsLoaded("Platonymous.Toolkit")) + try { - try + Type pyTkType = Type.GetType(typeName); + if (pyTkType is null) { - if (Type.GetType("PyTK.Extensions.PyMaps, PyTK") is Type PyTK && PyTK != null) - { - harmony.Patch(AccessTools.Method(PyTK, "drawLayer", new[] { typeof(Layer), typeof(IDisplayDevice), typeof(xTile.Dimensions.Rectangle), typeof(int), typeof(Location), typeof(bool) }), prefix: new HarmonyMethod(GetType(), nameof(PyTKDrawLayerPrefix))); - } + return; } - catch (Exception ex) + + MethodInfo drawLayer = AccessTools.Method(pyTkType, "drawLayer", new[] { - _monitor.Log($"Failed to patch PyTK in {this.GetType().Name}: DR may not properly display reflections!", LogLevel.Warn); - _monitor.Log($"Patch for PyTK failed in {this.GetType().Name}: {ex}", LogLevel.Trace); + typeof(Layer), + typeof(IDisplayDevice), + typeof(xTile.Dimensions.Rectangle), + typeof(int), + typeof(Location), + typeof(bool) + }); + + if (drawLayer is not null) + { + harmony.Patch(drawLayer, prefix: new HarmonyMethod(GetType(), nameof(PyTKDrawLayerPrefix))); } } - if (DynamicReflections.modHelper.ModRegistry.IsLoaded("Platonymous.TMXLoader")) + catch (Exception ex) { - try - { - if (Type.GetType("TMXLoader.PyMaps, TMXLoader") is Type PyTK && PyTK != null) - { - harmony.Patch(AccessTools.Method(PyTK, "drawLayer", new[] { typeof(Layer), typeof(IDisplayDevice), typeof(xTile.Dimensions.Rectangle), typeof(int), typeof(Location), typeof(bool) }), prefix: new HarmonyMethod(GetType(), nameof(PyTKDrawLayerPrefix))); - } - } - catch (Exception ex) - { - _monitor.Log($"Failed to patch TMXLoader in {this.GetType().Name}: DR may not properly display reflections!", LogLevel.Warn); - _monitor.Log($"Patch for TMXLoader failed in {this.GetType().Name}: {ex}", LogLevel.Trace); - } + _monitor.Log($"Failed to patch {displayName} in {GetType().Name}: DR may not properly display reflections!", LogLevel.Warn); + _monitor.Log($"Patch for {displayName} failed in {GetType().Name}: {ex}", LogLevel.Trace); } } @@ -80,8 +89,13 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev DynamicReflections.isDrawingWaterReflection = false; DynamicReflections.isDrawingMirrorReflection = false; - if (__instance.Equals(LayerToolkit.GetLowestBackgroundLayer(Game1.currentLocation)) is true) + var lowestBackgroundLayer = LayerToolkit.GetLowestBackgroundLayer(Game1.currentLocation); + bool hasMultipleBackgroundLayers = LayerToolkit.HasMultipleBackgroundLayers(Game1.currentLocation); + + if (__instance.Equals(lowestBackgroundLayer) is true) { + DynamicReflections.shouldDeferWaterReflectionPresentation = false; + DynamicReflections.shouldDeferSkyReflectionPresentation = false; SpriteBatchToolkit.CacheSpriteBatchSettings(Game1.spriteBatch, endSpriteBatch: true); // Pre-render the Mirrors layer (this should always be done, regardless of DynamicReflections.shouldDrawMirrorReflection) @@ -134,8 +148,17 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev // Resume previous SpriteBatch SpriteBatchToolkit.ResumeCachedSpriteBatch(Game1.spriteBatch); - if (DynamicReflections.isFilteringWater is false && DynamicReflections.isFilteringSky is false) + if (DynamicReflections.isFilteringWater is false && DynamicReflections.shouldDrawNightSky is false) + { + return true; + } + + if (hasMultipleBackgroundLayers is true) { + DynamicReflections.shouldDeferSkyReflectionPresentation = DynamicReflections.shouldDrawNightSky; + DynamicReflections.shouldDeferWaterReflectionPresentation = DynamicReflections.isFilteringWater; + DynamicReflections.isFilteringSky = false; + DynamicReflections.isFilteringWater = false; return true; } @@ -204,7 +227,11 @@ private static void DrawNormalPostfix(Layer __instance, IDisplayDevice displayDe return; } - if (__instance.Equals(LayerToolkit.GetLowestBackgroundLayer(Game1.currentLocation)) is true) + var lowestBackgroundLayer = LayerToolkit.GetLowestBackgroundLayer(Game1.currentLocation); + var highestBackgroundLayer = LayerToolkit.GetHighestBackgroundLayer(Game1.currentLocation); + bool hasMultipleBackgroundLayers = LayerToolkit.HasMultipleBackgroundLayers(Game1.currentLocation); + + if (__instance.Equals(lowestBackgroundLayer) is true) { if (DynamicReflections.isDrawingPuddles is true) { @@ -223,6 +250,35 @@ private static void DrawNormalPostfix(Layer __instance, IDisplayDevice displayDe } } + + if (hasMultipleBackgroundLayers is true && __instance.Equals(highestBackgroundLayer) is true && (DynamicReflections.shouldDeferWaterReflectionPresentation is true || DynamicReflections.shouldDeferSkyReflectionPresentation is true)) + { + SpriteBatchToolkit.CacheSpriteBatchSettings(Game1.spriteBatch, endSpriteBatch: true); + + SpriteBatchToolkit.RenderTopmostBackgroundWaterMask(); + + if (DynamicReflections.shouldDeferSkyReflectionPresentation is true) + { + SpriteBatchToolkit.DrawMaskedNightSky(); + + if (DynamicReflections.shouldDeferWaterReflectionPresentation is true) + { + DynamicReflections.shouldDeferWaterReflectionPresentation = false; + DynamicReflections.isDrawingWaterReflection = true; + } + + DynamicReflections.shouldDeferSkyReflectionPresentation = false; + } + else if (DynamicReflections.shouldDeferWaterReflectionPresentation is true) + { + SpriteBatchToolkit.DrawMaskedRenderedCharacters(isWavy: DynamicReflections.currentWaterSettings.IsReflectionWavy); + + DynamicReflections.shouldDeferWaterReflectionPresentation = false; + DynamicReflections.isDrawingWaterReflection = true; + } + + SpriteBatchToolkit.ResumeCachedSpriteBatch(Game1.spriteBatch); + } } internal static void DrawNormalReversePatch(Layer __instance, IDisplayDevice displayDevice, xTile.Dimensions.Rectangle mapViewport, Location displayOffset, int pixelZoom, float sort_offset = 0f) From fb9731e98b19346457cc39bf56104bd3b4311b6a Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:28:52 -0400 Subject: [PATCH 4/8] Add files via upload --- .../Framework/Utilities/LayerToolkit.cs | 36 ++++++++ .../Framework/Utilities/SpriteBatchToolkit.cs | 92 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/DynamicReflections/Framework/Utilities/LayerToolkit.cs b/DynamicReflections/Framework/Utilities/LayerToolkit.cs index a472c41..1e36f8e 100644 --- a/DynamicReflections/Framework/Utilities/LayerToolkit.cs +++ b/DynamicReflections/Framework/Utilities/LayerToolkit.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using xTile.Dimensions; using xTile.Layers; +using xTile.Tiles; namespace DynamicReflections.Framework.Utilities { @@ -36,5 +37,40 @@ internal static Layer GetHighestBackgroundLayer(GameLocation location) // GameLocation.backgroundLayers should be automatically sorted via StardewValley.GameLocation.SortLayers return location.backgroundLayers[location.backgroundLayers.Count - 1].Key; } + + internal static bool HasMultipleBackgroundLayers(GameLocation location) + { + var lowest = GetLowestBackgroundLayer(location); + var highest = GetHighestBackgroundLayer(location); + + return lowest is not null && highest is not null && !lowest.Equals(highest); + } + + internal static bool IsTopmostVisibleBackgroundWater(GameLocation location, int x, int y) + { + if (location is null || x < 0 || y < 0 || location.backgroundLayers is null || location.backgroundLayers.Count == 0) + { + return false; + } + + for (int index = location.backgroundLayers.Count - 1; index >= 0; index--) + { + var layer = location.backgroundLayers[index].Key; + if (layer is null || x >= layer.LayerWidth || y >= layer.LayerHeight) + { + continue; + } + + Tile tile = layer.Tiles[x, y]; + if (tile is null) + { + continue; + } + + return tile.Properties.TryGetValue("Water", out _) || tile.TileIndexProperties.TryGetValue("Water", out _); + } + + return false; + } } } diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 49c503f..2e0d29c 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -1,5 +1,6 @@ using DynamicReflections.Framework.Models.Reflections; using DynamicReflections.Framework.Patches.Tiles; +using DynamicReflections.Framework.Patches.Tools; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; @@ -384,6 +385,97 @@ internal static void RenderWaterReflectionNightSky() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderTopmostBackgroundWaterMask() + { + SpriteBatchToolkit.StartRendering(DynamicReflections.backgroundWaterMaskRenderTarget); + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + var location = Game1.currentLocation; + var map = location?.Map; + if (location is null || map is null || map.Layers is null || map.Layers.Count == 0) + { + SpriteBatchToolkit.StopRendering(); + return; + } + + int maxWidth = map.Layers.Max(layer => layer.LayerWidth); + int maxHeight = map.Layers.Max(layer => layer.LayerHeight); + int startX = Math.Max(0, (Game1.viewport.X / Game1.tileSize) - 1); + int startY = Math.Max(0, (Game1.viewport.Y / Game1.tileSize) - 1); + int endX = Math.Min(maxWidth - 1, ((Game1.viewport.X + Game1.viewport.Width) / Game1.tileSize) + 1); + int endY = Math.Min(maxHeight - 1, ((Game1.viewport.Y + Game1.viewport.Height) / Game1.tileSize) + 1); + + DynamicReflections.originalWaterMaskTiles.Clear(); + for (int tileX = startX; tileX <= endX; tileX++) + { + for (int tileY = startY; tileY <= endY; tileY++) + { + if (location.isWaterTile(tileX, tileY)) + { + DynamicReflections.originalWaterMaskTiles.Add(new Point(tileX, tileY)); + } + } + } + + if (DynamicReflections.originalWaterMaskTiles.Count == 0) + { + SpriteBatchToolkit.StopRendering(); + return; + } + + Color cachedWaterColor = location.waterColor.Value; + DynamicReflections.isRenderingTopmostBackgroundWaterMask = true; + location.waterColor.Value = Color.White; + + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + GameLocationPatch.DrawWaterReversePatch(location, Game1.spriteBatch); + Game1.spriteBatch.End(); + + location.waterColor.Value = cachedWaterColor; + DynamicReflections.isRenderingTopmostBackgroundWaterMask = false; + DynamicReflections.originalWaterMaskTiles.Clear(); + + SpriteBatchToolkit.StopRendering(); + } + + internal static void DrawMaskedNightSky() + { + DrawMaskedTexture(DynamicReflections.nightSkyRenderTarget); + } + + internal static void DrawMaskedRenderedCharacters(bool isWavy = false) + { + Texture2D source = DynamicReflections.playerWaterReflectionRender; + if (isWavy) + { + SpriteBatchToolkit.StartRendering(DynamicReflections.inBetweenRenderTarget); + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: DynamicReflections.waterReflectionEffect); + Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + Game1.spriteBatch.End(); + + SpriteBatchToolkit.StopRendering(); + source = DynamicReflections.inBetweenRenderTarget; + } + + DrawMaskedTexture(source); + } + + private static void DrawMaskedTexture(Texture2D source) + { + if (source is null || DynamicReflections.backgroundWaterMaskRenderTarget is null || DynamicReflections.mirrorReflectionEffect is null) + { + return; + } + + DynamicReflections.mirrorReflectionEffect.Parameters["Mask"].SetValue(DynamicReflections.backgroundWaterMaskRenderTarget); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.NonPremultiplied, SamplerState.PointClamp, effect: DynamicReflections.mirrorReflectionEffect); + Game1.spriteBatch.Draw(source, Vector2.Zero, Color.White); + Game1.spriteBatch.End(); + } + internal static void DrawNightSky() { Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); From fd18460c111218f2fe0f38235c5e7b5cda77f76c Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:25:07 -0400 Subject: [PATCH 5/8] Add files via upload --- DynamicReflections/DynamicReflections.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index c3aca73..30452db 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -67,7 +67,6 @@ public class DynamicReflections : Mod internal static bool shouldDeferWaterReflectionPresentation; internal static bool shouldDeferSkyReflectionPresentation; internal static bool isRenderingTopmostBackgroundWaterMask; - internal static readonly HashSet originalWaterMaskTiles = new HashSet(); // Puddle reflection variables internal static bool shouldDrawPuddlesReflection; @@ -171,6 +170,7 @@ public override object GetApi() private void OnWindowResized(object sender, StardewModdingAPI.Events.WindowResizedEventArgs e) { LoadRenderers(); + LayerToolkit.InvalidateCaches(); } private void OnButtonPressed(object sender, StardewModdingAPI.Events.ButtonPressedEventArgs e) @@ -245,6 +245,8 @@ private void OnFurnitureListChanged(object sender, StardewModdingAPI.Events.Furn private void OnWarped(object sender, StardewModdingAPI.Events.WarpedEventArgs e) { + LayerToolkit.InvalidateCaches(); + SetSkyReflectionSettings(); SetPuddleReflectionSettings(); SetWaterReflectionSettings(); From b9c441915105c99db17b2619b4b7949a10af55d6 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:30:54 -0400 Subject: [PATCH 6/8] Add files via upload --- .../Framework/Patches/Locations/GameLocationPatch.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs index 753deae..c250e40 100644 --- a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs +++ b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs @@ -71,8 +71,7 @@ private static bool IsWaterTilePrefix(GameLocation __instance, int xTile, int yT return true; } - __result = DynamicReflections.originalWaterMaskTiles.Contains(new Point(xTile, yTile)) - && LayerToolkit.IsTopmostVisibleBackgroundWater(__instance, xTile, yTile); + __result = LayerToolkit.IsTopmostVisibleBackgroundWater(__instance, xTile, yTile); return false; } From 0d6af0916fba4660e7ebf8c367a56e5c6a719b58 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:31:37 -0400 Subject: [PATCH 7/8] Add files via upload --- DynamicReflections/Framework/Patches/xTile/LayerPatch.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 41bb9c6..5d09726 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -153,6 +153,8 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev return true; } + // Keep the original single-Back behavior intact. + // Only multi-Back* maps defer the final water/sky presentation until the top of the background stack. if (hasMultipleBackgroundLayers is true) { DynamicReflections.shouldDeferSkyReflectionPresentation = DynamicReflections.shouldDrawNightSky; @@ -251,6 +253,8 @@ private static void DrawNormalPostfix(Layer __instance, IDisplayDevice displayDe } } + // Present the already-rendered water/sky reflections after the highest background layer. + // The explicit water mask keeps the original placement behavior on maps that use Back* layer stacks. if (hasMultipleBackgroundLayers is true && __instance.Equals(highestBackgroundLayer) is true && (DynamicReflections.shouldDeferWaterReflectionPresentation is true || DynamicReflections.shouldDeferSkyReflectionPresentation is true)) { SpriteBatchToolkit.CacheSpriteBatchSettings(Game1.spriteBatch, endSpriteBatch: true); From ed134a758e7a94ce75c8dc09a5be79c674411077 Mon Sep 17 00:00:00 2001 From: SoulSilverJD <165866563+SoulSilverJD@users.noreply.github.com> Date: Sun, 22 Mar 2026 04:32:45 -0400 Subject: [PATCH 8/8] Add files via upload --- .../Framework/Utilities/LayerToolkit.cs | 190 +++++++++++++++--- .../Framework/Utilities/SpriteBatchToolkit.cs | 33 +-- 2 files changed, 170 insertions(+), 53 deletions(-) diff --git a/DynamicReflections/Framework/Utilities/LayerToolkit.cs b/DynamicReflections/Framework/Utilities/LayerToolkit.cs index 1e36f8e..c2906bf 100644 --- a/DynamicReflections/Framework/Utilities/LayerToolkit.cs +++ b/DynamicReflections/Framework/Utilities/LayerToolkit.cs @@ -1,14 +1,6 @@ -using DynamicReflections.Framework.Patches.Tiles; using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI; -using StardewValley; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using xTile.Dimensions; +using StardewValley; using xTile.Layers; using xTile.Tiles; @@ -16,37 +8,187 @@ namespace DynamicReflections.Framework.Utilities { public static class LayerToolkit { + private static GameLocation _cachedMetadataLocation; + private static xTile.Map _cachedMetadataMap; + private static Layer _cachedLowestBackgroundLayer; + private static Layer _cachedHighestBackgroundLayer; + private static bool _cachedHasMultipleBackgroundLayers; + private static int _cachedMaxLayerWidth; + private static int _cachedMaxLayerHeight; + + private static GameLocation _cachedViewportLocation; + private static xTile.Map _cachedViewportMap; + private static int _cachedViewportStartX; + private static int _cachedViewportStartY; + private static int _cachedViewportEndX = -1; + private static int _cachedViewportEndY = -1; + private static readonly HashSet _cachedTopmostVisibleBackgroundWaterTiles = new HashSet(); + + internal static void InvalidateCaches() + { + _cachedMetadataLocation = null; + _cachedMetadataMap = null; + _cachedLowestBackgroundLayer = null; + _cachedHighestBackgroundLayer = null; + _cachedHasMultipleBackgroundLayers = false; + _cachedMaxLayerWidth = 0; + _cachedMaxLayerHeight = 0; + + _cachedViewportLocation = null; + _cachedViewportMap = null; + _cachedViewportStartX = 0; + _cachedViewportStartY = 0; + _cachedViewportEndX = -1; + _cachedViewportEndY = -1; + _cachedTopmostVisibleBackgroundWaterTiles.Clear(); + } + internal static Layer GetLowestBackgroundLayer(GameLocation location) { - if (location is null || location.backgroundLayers.Count == 0) + EnsureBackgroundLayerMetadata(location); + return _cachedLowestBackgroundLayer; + } + + internal static Layer GetHighestBackgroundLayer(GameLocation location) + { + EnsureBackgroundLayerMetadata(location); + return _cachedHighestBackgroundLayer; + } + + internal static bool HasMultipleBackgroundLayers(GameLocation location) + { + EnsureBackgroundLayerMetadata(location); + return _cachedHasMultipleBackgroundLayers; + } + + internal static bool HasAnyTopmostVisibleBackgroundWater(GameLocation location) + { + EnsureTopmostVisibleBackgroundWaterCache(location); + return _cachedTopmostVisibleBackgroundWaterTiles.Count > 0; + } + + internal static bool IsTopmostVisibleBackgroundWater(GameLocation location, int x, int y) + { + EnsureTopmostVisibleBackgroundWaterCache(location); + + if (ReferenceEquals(location, _cachedViewportLocation) && x >= _cachedViewportStartX && x <= _cachedViewportEndX && y >= _cachedViewportStartY && y <= _cachedViewportEndY) { - return null; + return _cachedTopmostVisibleBackgroundWaterTiles.Contains(new Point(x, y)); } - // GameLocation.backgroundLayers should be automatically sorted via StardewValley.GameLocation.SortLayers - return location.backgroundLayers[0].Key; + return IsTopmostVisibleBackgroundWaterCore(location, x, y); } - internal static Layer GetHighestBackgroundLayer(GameLocation location) + private static void EnsureBackgroundLayerMetadata(GameLocation location) + { + xTile.Map map = location?.Map; + if (ReferenceEquals(location, _cachedMetadataLocation) && ReferenceEquals(map, _cachedMetadataMap)) + { + return; + } + + _cachedMetadataLocation = location; + _cachedMetadataMap = map; + _cachedLowestBackgroundLayer = null; + _cachedHighestBackgroundLayer = null; + _cachedHasMultipleBackgroundLayers = false; + _cachedMaxLayerWidth = 0; + _cachedMaxLayerHeight = 0; + + if (location is null || map is null || location.backgroundLayers is null || location.backgroundLayers.Count == 0) + { + return; + } + + // GameLocation.backgroundLayers should already be sorted by GameLocation.SortLayers. + _cachedLowestBackgroundLayer = location.backgroundLayers[0].Key; + _cachedHighestBackgroundLayer = location.backgroundLayers[location.backgroundLayers.Count - 1].Key; + _cachedHasMultipleBackgroundLayers = _cachedLowestBackgroundLayer is not null + && _cachedHighestBackgroundLayer is not null + && _cachedLowestBackgroundLayer.Equals(_cachedHighestBackgroundLayer) is false; + + for (int i = 0; i < map.Layers.Count; i++) + { + Layer layer = map.Layers[i]; + if (layer.LayerWidth > _cachedMaxLayerWidth) + { + _cachedMaxLayerWidth = layer.LayerWidth; + } + + if (layer.LayerHeight > _cachedMaxLayerHeight) + { + _cachedMaxLayerHeight = layer.LayerHeight; + } + } + } + + private static void EnsureTopmostVisibleBackgroundWaterCache(GameLocation location) { - if (location is null || location.backgroundLayers.Count == 0) + EnsureBackgroundLayerMetadata(location); + + int startX; + int startY; + int endX; + int endY; + GetBufferedViewportTileBounds(location, out startX, out startY, out endX, out endY); + + if (ReferenceEquals(location, _cachedViewportLocation) + && ReferenceEquals(location?.Map, _cachedViewportMap) + && startX == _cachedViewportStartX + && startY == _cachedViewportStartY + && endX == _cachedViewportEndX + && endY == _cachedViewportEndY) { - return null; + return; } - // GameLocation.backgroundLayers should be automatically sorted via StardewValley.GameLocation.SortLayers - return location.backgroundLayers[location.backgroundLayers.Count - 1].Key; + _cachedViewportLocation = location; + _cachedViewportMap = location?.Map; + _cachedViewportStartX = startX; + _cachedViewportStartY = startY; + _cachedViewportEndX = endX; + _cachedViewportEndY = endY; + _cachedTopmostVisibleBackgroundWaterTiles.Clear(); + + if (location is null || endX < startX || endY < startY) + { + return; + } + + // Cache the final, authoritative visible water surface once per buffered viewport. + // The deferred Back* path can then reuse it without re-scanning every background layer during drawWater(). + for (int tileX = startX; tileX <= endX; tileX++) + { + for (int tileY = startY; tileY <= endY; tileY++) + { + if (location.isWaterTile(tileX, tileY) && IsTopmostVisibleBackgroundWaterCore(location, tileX, tileY)) + { + _cachedTopmostVisibleBackgroundWaterTiles.Add(new Point(tileX, tileY)); + } + } + } } - internal static bool HasMultipleBackgroundLayers(GameLocation location) + private static void GetBufferedViewportTileBounds(GameLocation location, out int startX, out int startY, out int endX, out int endY) { - var lowest = GetLowestBackgroundLayer(location); - var highest = GetHighestBackgroundLayer(location); + EnsureBackgroundLayerMetadata(location); - return lowest is not null && highest is not null && !lowest.Equals(highest); + if (location is null || _cachedMaxLayerWidth <= 0 || _cachedMaxLayerHeight <= 0) + { + startX = 0; + startY = 0; + endX = -1; + endY = -1; + return; + } + + startX = System.Math.Max(0, (Game1.viewport.X / Game1.tileSize) - 1); + startY = System.Math.Max(0, (Game1.viewport.Y / Game1.tileSize) - 1); + endX = System.Math.Min(_cachedMaxLayerWidth - 1, ((Game1.viewport.X + Game1.viewport.Width) / Game1.tileSize) + 1); + endY = System.Math.Min(_cachedMaxLayerHeight - 1, ((Game1.viewport.Y + Game1.viewport.Height) / Game1.tileSize) + 1); } - internal static bool IsTopmostVisibleBackgroundWater(GameLocation location, int x, int y) + private static bool IsTopmostVisibleBackgroundWaterCore(GameLocation location, int x, int y) { if (location is null || x < 0 || y < 0 || location.backgroundLayers is null || location.backgroundLayers.Count == 0) { @@ -55,7 +197,7 @@ internal static bool IsTopmostVisibleBackgroundWater(GameLocation location, int for (int index = location.backgroundLayers.Count - 1; index >= 0; index--) { - var layer = location.backgroundLayers[index].Key; + Layer layer = location.backgroundLayers[index].Key; if (layer is null || x >= layer.LayerWidth || y >= layer.LayerHeight) { continue; diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 2e0d29c..a40ac25 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -390,39 +390,15 @@ internal static void RenderTopmostBackgroundWaterMask() SpriteBatchToolkit.StartRendering(DynamicReflections.backgroundWaterMaskRenderTarget); Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - var location = Game1.currentLocation; - var map = location?.Map; - if (location is null || map is null || map.Layers is null || map.Layers.Count == 0) - { - SpriteBatchToolkit.StopRendering(); - return; - } - - int maxWidth = map.Layers.Max(layer => layer.LayerWidth); - int maxHeight = map.Layers.Max(layer => layer.LayerHeight); - int startX = Math.Max(0, (Game1.viewport.X / Game1.tileSize) - 1); - int startY = Math.Max(0, (Game1.viewport.Y / Game1.tileSize) - 1); - int endX = Math.Min(maxWidth - 1, ((Game1.viewport.X + Game1.viewport.Width) / Game1.tileSize) + 1); - int endY = Math.Min(maxHeight - 1, ((Game1.viewport.Y + Game1.viewport.Height) / Game1.tileSize) + 1); - - DynamicReflections.originalWaterMaskTiles.Clear(); - for (int tileX = startX; tileX <= endX; tileX++) - { - for (int tileY = startY; tileY <= endY; tileY++) - { - if (location.isWaterTile(tileX, tileY)) - { - DynamicReflections.originalWaterMaskTiles.Add(new Point(tileX, tileY)); - } - } - } - - if (DynamicReflections.originalWaterMaskTiles.Count == 0) + GameLocation location = Game1.currentLocation; + if (location is null || location.Map is null || LayerToolkit.HasAnyTopmostVisibleBackgroundWater(location) is false) { SpriteBatchToolkit.StopRendering(); return; } + // For multi-Back* maps, build the water-shaped mask from the final visible water surface only. + // This preserves the original mod's placement rules while avoiding repeated viewport scans each frame. Color cachedWaterColor = location.waterColor.Value; DynamicReflections.isRenderingTopmostBackgroundWaterMask = true; location.waterColor.Value = Color.White; @@ -433,7 +409,6 @@ internal static void RenderTopmostBackgroundWaterMask() location.waterColor.Value = cachedWaterColor; DynamicReflections.isRenderingTopmostBackgroundWaterMask = false; - DynamicReflections.originalWaterMaskTiles.Clear(); SpriteBatchToolkit.StopRendering(); }