diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 0d449e2..30452db 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -64,6 +64,9 @@ 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; // Puddle reflection variables internal static bool shouldDrawPuddlesReflection; @@ -101,6 +104,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) @@ -166,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) @@ -240,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(); @@ -1373,6 +1380,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); diff --git a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs index a68b2a2..c250e40 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,17 @@ 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 = LayerToolkit.IsTopmostVisibleBackgroundWater(__instance, xTile, yTile); + return false; + } + internal static void DrawWaterReversePatch(GameLocation __instance, SpriteBatch b) { new NotImplementedException("It's a stub!"); diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 6557a9c..5d09726 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,19 @@ 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; + } + + // 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; + DynamicReflections.shouldDeferWaterReflectionPresentation = DynamicReflections.isFilteringWater; + DynamicReflections.isFilteringSky = false; + DynamicReflections.isFilteringWater = false; return true; } @@ -204,7 +229,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 +252,37 @@ 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); + + 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) diff --git a/DynamicReflections/Framework/Utilities/LayerToolkit.cs b/DynamicReflections/Framework/Utilities/LayerToolkit.cs index a472c41..c2906bf 100644 --- a/DynamicReflections/Framework/Utilities/LayerToolkit.cs +++ b/DynamicReflections/Framework/Utilities/LayerToolkit.cs @@ -1,40 +1,218 @@ -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; 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) { - if (location is null || location.backgroundLayers.Count == 0) + 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++) { - return null; + 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) + { + 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; + } + + _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)); + } + } + } + } + + private static void GetBufferedViewportTileBounds(GameLocation location, out int startX, out int startY, out int endX, out int endY) + { + EnsureBackgroundLayerMetadata(location); + + 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); + } + + 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) + { + return false; + } + + for (int index = location.backgroundLayers.Count - 1; index >= 0; index--) + { + Layer 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 _); } - // GameLocation.backgroundLayers should be automatically sorted via StardewValley.GameLocation.SortLayers - return location.backgroundLayers[location.backgroundLayers.Count - 1].Key; + return false; } } } diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 49c503f..a40ac25 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,72 @@ internal static void RenderWaterReflectionNightSky() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderTopmostBackgroundWaterMask() + { + SpriteBatchToolkit.StartRendering(DynamicReflections.backgroundWaterMaskRenderTarget); + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + 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; + + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + GameLocationPatch.DrawWaterReversePatch(location, Game1.spriteBatch); + Game1.spriteBatch.End(); + + location.waterColor.Value = cachedWaterColor; + DynamicReflections.isRenderingTopmostBackgroundWaterMask = false; + + 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);