From 0636ff6e8a74bd65060156fddfd38598fc1f1cc7 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 21:44:04 -0600 Subject: [PATCH 01/22] Working tree, bush and grass reflections --- DynamicReflections/DynamicReflections.cs | 57 ++++++++- .../GenericModConfigMenu/ModConfig.cs | 4 +- .../Framework/Patches/xTile/LayerPatch.cs | 14 +++ .../Framework/Utilities/SpriteBatchToolkit.cs | 112 +++++++++++++++++- 4 files changed, 181 insertions(+), 6 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 35dff92..d84e5d0 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -20,6 +20,7 @@ using StardewValley.Locations; using StardewValley.Menus; using DynamicReflections.Framework.Interfaces.Internal; +using StardewValley.TerrainFeatures; namespace DynamicReflections { @@ -47,8 +48,9 @@ public class DynamicReflections : Mod // Water reflection variables internal static Dictionary npcToWaterReflectionPosition = new Dictionary(); - internal static readonly Dictionary waterTileCache = new(); internal static Vector2? waterReflectionPosition; - internal static Vector2? waterReflectionTilePosition; + internal static readonly Dictionary waterTileCache = new Dictionary(); + internal static Vector2? waterReflectionPosition; + internal static readonly Dictionary> locationToTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -86,6 +88,8 @@ public class DynamicReflections : Mod internal static RenderTarget2D[] maskedPlayerMirrorReflectionRenders; internal static RenderTarget2D npcWaterReflectionRender; internal static RenderTarget2D npcPuddleReflectionRender; + internal static RenderTarget2D terrainWaterReflectionRender; + internal static RenderTarget2D grassWaterReflectionRender; internal static RenderTarget2D inBetweenRenderTarget; internal static RenderTarget2D mirrorsLayerRenderTarget; internal static RenderTarget2D mirrorsFurnitureRenderTarget; @@ -138,6 +142,8 @@ public override void Entry(IModHelper helper) helper.Events.GameLoop.DayStarted += OnDayStarted; helper.Events.GameLoop.DayEnding += OnDayEnding; helper.Events.GameLoop.GameLaunched += OnGameLaunched; + helper.Events.World.TerrainFeatureListChanged += OnTerrainFeatureListChanged; + helper.Events.World.LargeTerrainFeatureListChanged += OnLargeTerrainFeatureChanged; } public override object GetApi() @@ -675,6 +681,26 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched LoadRenderers(); } + private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events.TerrainFeatureListChangedEventArgs e) + { + if (e.Location is null) + { + return; + } + + ResetLocationTerrainCache(e.Location); + } + + private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Events.LargeTerrainFeatureListChangedEventArgs e) + { + if (e.Location is null) + { + return; + } + + ResetLocationTerrainCache(e.Location); + } + private void LoadContentPacks(bool silent = false) { // Clear the existing cache of custom buildings @@ -1236,6 +1262,8 @@ internal void LoadRenderers() RegenerateRenderer(ref playerPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcPuddleReflectionRender, shouldUseScreenDimensions); + RegenerateRenderer(ref terrainWaterReflectionRender, shouldUseScreenDimensions); + RegenerateRenderer(ref grassWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref puddlesRenderTarget, shouldUseScreenDimensions); RegenerateRenderer(ref mirrorsLayerRenderTarget, shouldUseScreenDimensions); @@ -1538,5 +1566,30 @@ internal static IEnumerable GetActiveNPCs(GameLocation location) return Array.Empty(); } + + internal static IEnumerable GetTerrainFeatures(GameLocation location) + { + if (location is null) + { + return Array.Empty(); + } + + if (locationToTerrainFeatures.ContainsKey(location) is false) + { + ResetLocationTerrainCache(location); + } + + return locationToTerrainFeatures[location]; + } + + private static void ResetLocationTerrainCache(GameLocation location) + { + locationToTerrainFeatures[location] = location.terrainFeatures.Values.ToList(); + + foreach (var largeTerrainFeature in location.largeTerrainFeatures) + { + locationToTerrainFeatures[location].Add(largeTerrainFeature); + } + } } } diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs index 6de3b1f..22156d4 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs @@ -14,7 +14,9 @@ public class ModConfig public bool AreWaterReflectionsEnabled { get; set; } = true; public bool AreMirrorReflectionsEnabled { get; set; } = true; public bool ArePuddleReflectionsEnabled { get; set; } = true; - public bool AreNPCReflectionsEnabled { get; set; } = true; + public bool AreNPCReflectionsEnabled { get; set; } = true; + public bool AreGrassReflectionsEnabled { get; set; } = true; + public bool AreTerrainReflectionsEnabled { get; set; } = true; public bool AreCompanionReflectionsEnabled { get; set; } = true; public bool AreSkyReflectionsEnabled { get; set; } = true; diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index c1835ff..3bbca53 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -109,6 +109,20 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev SpriteBatchToolkit.RenderPuddleReflectionNPCs(); } + // Handle preliminary grass reflection logic + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) + { + DynamicReflections.isFilteringWater = true; + SpriteBatchToolkit.RenderWaterReflectionGrass(); + } + + // Handle preliminary NPC reflection logic + if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true) + { + DynamicReflections.isFilteringWater = true; + SpriteBatchToolkit.RenderWaterReflectionTerrain(); + } + _waterColor = Game1.currentLocation.waterColor.Value; if (DynamicReflections.modConfig.AreSkyReflectionsEnabled is true) { diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index c8e9f47..2f34a9c 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewValley; +using StardewValley.TerrainFeatures; using System; using System.Collections.Generic; using System.Linq; @@ -438,7 +439,6 @@ internal static void RenderWaterReflectionNPCs() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void RenderPuddleReflectionNPCs() { if (Game1.currentLocation is null || Game1.currentLocation.characters is null) @@ -545,6 +545,93 @@ internal static void RenderPuddleReflectionNPCs() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderWaterReflectionTerrain() + { + if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) + { + return; + } + + // Set the render target + SpriteBatchToolkit.StartRendering(DynamicReflections.terrainWaterReflectionRender); + + // Draw the scene + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + foreach (TerrainFeature terrainFeature in DynamicReflections.GetTerrainFeatures(Game1.currentLocation)) + { + if (terrainFeature is not Tree) + { + continue; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 72) * 2, 0); + + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + terrainFeature.draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + + // Drop the render target + SpriteBatchToolkit.StopRendering(); + + Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); + } + + internal static void RenderWaterReflectionGrass() + { + if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) + { + return; + } + + // Set the render target + SpriteBatchToolkit.StartRendering(DynamicReflections.grassWaterReflectionRender); + + // Draw the scene + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + foreach (var terrainFeature in Game1.currentLocation.terrainFeatures.Values) + { + if (terrainFeature is not Grass) + { + continue; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 72) * 2, 0); + + // Using SpriteSortMode.BackToFront for Grass to properly draw any "flowers" that should be on top + Game1.spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + terrainFeature.draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + + // Drop the render target + SpriteBatchToolkit.StopRendering(); + + Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); + } + internal static void DrawPuddleReflection(Texture2D mask) { DynamicReflections.mirrorReflectionEffect.Parameters["Mask"].SetValue(mask); @@ -709,11 +796,30 @@ internal static void DrawReflectionViaMatrix() internal static void DrawRenderedCharacters(bool isWavy = false) { - if (DynamicReflections.shouldDrawWaterReflection is true) + + if (DynamicReflections.shouldDrawWaterReflection is true || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) { DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); - Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + + if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) + { + // Draw trees + Game1.spriteBatch.Draw(DynamicReflections.terrainWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + + if (DynamicReflections.shouldDrawWaterReflection is true) + { + // Draw the player + Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + // Draw terrain features (such as grass, but not trees) + Game1.spriteBatch.Draw(DynamicReflections.grassWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + Game1.spriteBatch.End(); } From c24a4511446862853ec385e7c0565e3cd78fc97c Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:09:26 -0600 Subject: [PATCH 02/22] Simplified reflective tile check into IsTileReflective --- DynamicReflections/DynamicReflections.cs | 55 ++++++++++++------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index d84e5d0..7702c84 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -325,17 +325,9 @@ private void OnUpdateTicked(object sender, StardewModdingAPI.Events.UpdateTicked // Hide the reflection if it will show up out of bounds on the map or not drawn on a water tile var waterReflectionPosition = DynamicReflections.waterReflectionTilePosition.Value; - for (int yOffset = -1; yOffset <= Math.Ceiling(playerOffset.Y); yOffset++) + if (IsTileReflective(waterReflectionPosition, (int)Math.Ceiling(playerOffset.Y))) { - var tilePosition = waterReflectionPosition + new Vector2(0, yOffset); - - if (IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) - { - DynamicReflections.shouldDrawWaterReflection = true; - break; - } + DynamicReflections.shouldDrawWaterReflection = true; } // Handle the wavy effect if enabled @@ -448,25 +440,16 @@ private void OnUpdateTicked(object sender, StardewModdingAPI.Events.UpdateTicked // Hide the reflection if it will show up out of bounds on the map // or not drawn on water tiles var waterReflectionPosition = npcPosition / 64f; - for (int yOffset = -1; yOffset <= Math.Ceiling(npcOffset.Y); yOffset++) + if (IsTileReflective(waterReflectionPosition, (int)Math.Ceiling(npcOffset.Y))) { - var tilePosition = waterReflectionPosition + new Vector2(0, yOffset); - - if (IsWaterReflectiveTile(location, (int)tilePosition.X - 1, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(location, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + npcToWaterReflectionPosition[npc] = npcPosition; + if (isCompanion) { - npcToWaterReflectionPosition[npc] = npcPosition; - - if (isCompanion) - { - companionCount++; - } - else - { - npcCount++; - } - - break; + companionCount++; + } + else + { + npcCount++; } } } @@ -1443,7 +1426,7 @@ private Vector2 GetMirrorOffset(GameLocation location, int x, int y) return new Vector2(xOffsetValue, yOffsetValue); } - private bool IsWaterReflectiveTile(GameLocation location, int x, int y) + private static bool IsWaterReflectiveTile(GameLocation location, int x, int y) { if (location is null) { @@ -1591,5 +1574,21 @@ private static void ResetLocationTerrainCache(GameLocation location) locationToTerrainFeatures[location].Add(largeTerrainFeature); } } + private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) + { + for (int yOffset = -1; yOffset <= yTileOffset; yOffset++) + { + var tilePosition = startPosition + new Vector2(0, yOffset); + + if (IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true + || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true + || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + { + return true; + } + } + + return false; + } } } From 33e44d806aea2f9ab414a25301b76589a4805eee Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:49:55 -0600 Subject: [PATCH 03/22] Added caching for terrain reflections --- DynamicReflections/DynamicReflections.cs | 54 +++++++++++++++---- .../Framework/Utilities/SpriteBatchToolkit.cs | 14 +++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 7702c84..cb03278 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -50,7 +50,8 @@ public class DynamicReflections : Mod internal static Dictionary npcToWaterReflectionPosition = new Dictionary(); internal static readonly Dictionary waterTileCache = new Dictionary(); internal static Vector2? waterReflectionPosition; - internal static readonly Dictionary> locationToTerrainFeatures = new Dictionary>(); + internal static Vector2? waterReflectionTilePosition; + internal static readonly Dictionary> locationToReflectableTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -666,22 +667,44 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events.TerrainFeatureListChangedEventArgs e) { - if (e.Location is null) + if (e.Location is null || locationToReflectableTerrainFeatures.ContainsKey(e.Location) is false) { return; } - ResetLocationTerrainCache(e.Location); + foreach (var addedTerrainFeature in e.Added) + { + if (IsTileReflective(addedTerrainFeature.Value.Tile, 3)) + { + locationToReflectableTerrainFeatures[e.Location].Add(addedTerrainFeature.Value); + } + } + + foreach (var removedTerrainFeature in e.Removed) + { + locationToReflectableTerrainFeatures[e.Location].Remove(removedTerrainFeature.Value); + } } private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Events.LargeTerrainFeatureListChangedEventArgs e) { - if (e.Location is null) + if (e.Location is null || locationToReflectableTerrainFeatures.ContainsKey(e.Location) is false) { return; } - ResetLocationTerrainCache(e.Location); + foreach (var addedTerrainFeature in e.Added) + { + if (IsTileReflective(addedTerrainFeature.Tile, 3)) + { + locationToReflectableTerrainFeatures[e.Location].Add(addedTerrainFeature); + } + } + + foreach (var removedTerrainFeature in e.Removed) + { + locationToReflectableTerrainFeatures[e.Location].Remove(removedTerrainFeature); + } } private void LoadContentPacks(bool silent = false) @@ -1550,30 +1573,41 @@ internal static IEnumerable GetActiveNPCs(GameLocation location) return Array.Empty(); } - internal static IEnumerable GetTerrainFeatures(GameLocation location) + internal static IEnumerable GetReflectableTerrainFeatures(GameLocation location) { if (location is null) { return Array.Empty(); } - if (locationToTerrainFeatures.ContainsKey(location) is false) + if (locationToReflectableTerrainFeatures.ContainsKey(location) is false) { ResetLocationTerrainCache(location); } - return locationToTerrainFeatures[location]; + return locationToReflectableTerrainFeatures[location]; } private static void ResetLocationTerrainCache(GameLocation location) { - locationToTerrainFeatures[location] = location.terrainFeatures.Values.ToList(); + locationToReflectableTerrainFeatures[location] = new HashSet(); + foreach (var terrainFeature in location.terrainFeatures.Values) + { + if (IsTileReflective(terrainFeature.Tile, 3)) + { + locationToReflectableTerrainFeatures[location].Add(terrainFeature); + } + } foreach (var largeTerrainFeature in location.largeTerrainFeatures) { - locationToTerrainFeatures[location].Add(largeTerrainFeature); + if (IsTileReflective(largeTerrainFeature.Tile, 3)) + { + locationToReflectableTerrainFeatures[location].Add(largeTerrainFeature); + } } } + private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) { for (int yOffset = -1; yOffset <= yTileOffset; yOffset++) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 2f34a9c..a064539 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -558,17 +558,23 @@ internal static void RenderWaterReflectionTerrain() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (TerrainFeature terrainFeature in DynamicReflections.GetTerrainFeatures(Game1.currentLocation)) + foreach (TerrainFeature terrainFeature in DynamicReflections.GetReflectableTerrainFeatures(Game1.currentLocation)) { - if (terrainFeature is not Tree) + if (terrainFeature is not Tree && terrainFeature is not Bush) { continue; } + int yOffset = 48; + if (terrainFeature is Tree) + { + yOffset = 72; + } + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 72) * 2, 0); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + yOffset) * 2, 0); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); } @@ -601,7 +607,7 @@ internal static void RenderWaterReflectionGrass() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (var terrainFeature in Game1.currentLocation.terrainFeatures.Values) + foreach (var terrainFeature in DynamicReflections.GetReflectableTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Grass) { From 41cffa39bfa4b358c8f0cf3d08089b4e15f60ee7 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:59:02 -0600 Subject: [PATCH 04/22] Fixed water reflection not resetting player's height offset correctly --- DynamicReflections/DynamicReflections.cs | 4 ++-- DynamicReflections/Framework/Models/Settings/WaterSettings.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index cb03278..85a9670 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -643,9 +643,9 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched if (isFreshInstall || isNewerVersion) { // Handle new version behavior - if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.1.0"))) + if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.2.0"))) { - // Reset the WaterReflectionSettings + // Reset the default WaterReflectionSettings modConfig.WaterReflectionSettings.Reset(); } if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.1.1"))) diff --git a/DynamicReflections/Framework/Models/Settings/WaterSettings.cs b/DynamicReflections/Framework/Models/Settings/WaterSettings.cs index f7151b4..4aab1ff 100644 --- a/DynamicReflections/Framework/Models/Settings/WaterSettings.cs +++ b/DynamicReflections/Framework/Models/Settings/WaterSettings.cs @@ -57,7 +57,7 @@ public void Reset(WaterSettings referencedSettings = null) AreReflectionsEnabled = true; ReflectionDirection = Direction.South; ReflectionOverlay = Color.White; - PlayerReflectionOffset = new Vector2(0f, 0.5f); + PlayerReflectionOffset = new Vector2(0f, 1.5f); NPCReflectionOffset = new Vector2(0f, 0.7f); CompanionReflectionOffset = new Vector2(0f, 0.3f); IsReflectionWavy = true; From 7a4f8e16f6e8b245c3bf84c01f9f25c0b12a7e2a Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 23:17:13 -0600 Subject: [PATCH 05/22] Now sorting locationToReflectableTerrainFeatures by Tile.Y --- DynamicReflections/DynamicReflections.cs | 7 +++++-- .../Framework/Utilities/SpriteBatchToolkit.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 85a9670..d3b86e8 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -51,7 +51,7 @@ public class DynamicReflections : Mod internal static readonly Dictionary waterTileCache = new Dictionary(); internal static Vector2? waterReflectionPosition; internal static Vector2? waterReflectionTilePosition; - internal static readonly Dictionary> locationToReflectableTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToReflectableTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -677,6 +677,7 @@ private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events if (IsTileReflective(addedTerrainFeature.Value.Tile, 3)) { locationToReflectableTerrainFeatures[e.Location].Add(addedTerrainFeature.Value); + locationToReflectableTerrainFeatures[e.Location] = locationToReflectableTerrainFeatures[e.Location].OrderBy(t => t.Tile.Y).ToList(); } } @@ -1590,7 +1591,7 @@ internal static IEnumerable GetReflectableTerrainFeatures(GameLo private static void ResetLocationTerrainCache(GameLocation location) { - locationToReflectableTerrainFeatures[location] = new HashSet(); + locationToReflectableTerrainFeatures[location] = new List(); foreach (var terrainFeature in location.terrainFeatures.Values) { if (IsTileReflective(terrainFeature.Tile, 3)) @@ -1606,6 +1607,8 @@ private static void ResetLocationTerrainCache(GameLocation location) locationToReflectableTerrainFeatures[location].Add(largeTerrainFeature); } } + + locationToReflectableTerrainFeatures[location] = locationToReflectableTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index a064539..af9e1ac 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -810,7 +810,7 @@ internal static void DrawRenderedCharacters(bool isWavy = false) if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) { - // Draw trees + // Draw terrain (tree / bushes) Game1.spriteBatch.Draw(DynamicReflections.terrainWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); } From 6269f3de63519d48cac4dc6895266e264b702d3d Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Fri, 2 Jan 2026 23:52:44 -0600 Subject: [PATCH 06/22] Added puddle reflections for terrain objects --- DynamicReflections/DynamicReflections.cs | 113 +++++++++++++---- .../Framework/Patches/xTile/LayerPatch.cs | 2 + .../Framework/Utilities/SpriteBatchToolkit.cs | 117 +++++++++++++++++- 3 files changed, 203 insertions(+), 29 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index d3b86e8..51fc541 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -51,7 +51,8 @@ public class DynamicReflections : Mod internal static readonly Dictionary waterTileCache = new Dictionary(); internal static Vector2? waterReflectionPosition; internal static Vector2? waterReflectionTilePosition; - internal static readonly Dictionary> locationToReflectableTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToWaterReflectionTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToPuddleReflectionTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -90,7 +91,9 @@ public class DynamicReflections : Mod internal static RenderTarget2D npcWaterReflectionRender; internal static RenderTarget2D npcPuddleReflectionRender; internal static RenderTarget2D terrainWaterReflectionRender; + internal static RenderTarget2D terrainPuddleReflectionRender; internal static RenderTarget2D grassWaterReflectionRender; + internal static RenderTarget2D grassPuddleReflectionRender; internal static RenderTarget2D inBetweenRenderTarget; internal static RenderTarget2D mirrorsLayerRenderTarget; internal static RenderTarget2D mirrorsFurnitureRenderTarget; @@ -667,47 +670,61 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events.TerrainFeatureListChangedEventArgs e) { - if (e.Location is null || locationToReflectableTerrainFeatures.ContainsKey(e.Location) is false) + foreach (var addedTerrainFeature in e.Added) { - return; + HandleTerrainFeatureAddition(e.Location, addedTerrainFeature.Value); } + foreach (var removedTerrainFeature in e.Removed) + { + HandleTerrainFeatureRemoval(e.Location, removedTerrainFeature.Value); + } + } + + private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Events.LargeTerrainFeatureListChangedEventArgs e) + { foreach (var addedTerrainFeature in e.Added) { - if (IsTileReflective(addedTerrainFeature.Value.Tile, 3)) - { - locationToReflectableTerrainFeatures[e.Location].Add(addedTerrainFeature.Value); - locationToReflectableTerrainFeatures[e.Location] = locationToReflectableTerrainFeatures[e.Location].OrderBy(t => t.Tile.Y).ToList(); - } + HandleTerrainFeatureAddition(e.Location, addedTerrainFeature); } foreach (var removedTerrainFeature in e.Removed) { - locationToReflectableTerrainFeatures[e.Location].Remove(removedTerrainFeature.Value); + HandleTerrainFeatureRemoval(e.Location, removedTerrainFeature); } } - private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Events.LargeTerrainFeatureListChangedEventArgs e) + private void HandleTerrainFeatureAddition(GameLocation location, TerrainFeature terrainFeature) { - if (e.Location is null || locationToReflectableTerrainFeatures.ContainsKey(e.Location) is false) + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) { return; } - foreach (var addedTerrainFeature in e.Added) + if (IsTileReflective(terrainFeature.Tile, 3)) { - if (IsTileReflective(addedTerrainFeature.Tile, 3)) - { - locationToReflectableTerrainFeatures[e.Location].Add(addedTerrainFeature); - } + locationToWaterReflectionTerrainFeatures[location].Add(terrainFeature); + locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } - foreach (var removedTerrainFeature in e.Removed) + if (IsTilePuddle(terrainFeature.Tile, 3)) { - locationToReflectableTerrainFeatures[e.Location].Remove(removedTerrainFeature); + locationToPuddleReflectionTerrainFeatures[location].Add(terrainFeature); + locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } } + private void HandleTerrainFeatureRemoval(GameLocation location, TerrainFeature terrainFeature) + { + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + return; + } + + locationToWaterReflectionTerrainFeatures[location].Remove(terrainFeature); + locationToPuddleReflectionTerrainFeatures[location].Remove(terrainFeature); + } + private void LoadContentPacks(bool silent = false) { // Clear the existing cache of custom buildings @@ -1270,7 +1287,9 @@ internal void LoadRenderers() RegenerateRenderer(ref npcWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref terrainWaterReflectionRender, shouldUseScreenDimensions); + RegenerateRenderer(ref terrainPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref grassWaterReflectionRender, shouldUseScreenDimensions); + RegenerateRenderer(ref grassPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref puddlesRenderTarget, shouldUseScreenDimensions); RegenerateRenderer(ref mirrorsLayerRenderTarget, shouldUseScreenDimensions); @@ -1574,29 +1593,50 @@ internal static IEnumerable GetActiveNPCs(GameLocation location) return Array.Empty(); } - internal static IEnumerable GetReflectableTerrainFeatures(GameLocation location) + internal static IEnumerable GetWaterReflectionTerrainFeatures(GameLocation location) + { + if (location is null) + { + return Array.Empty(); + } + + if (locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false) + { + ResetLocationTerrainCache(location); + } + + return locationToWaterReflectionTerrainFeatures[location]; + } + + internal static IEnumerable GetPuddleReflectionTerrainFeatures(GameLocation location) { if (location is null) { return Array.Empty(); } - if (locationToReflectableTerrainFeatures.ContainsKey(location) is false) + if (locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) { ResetLocationTerrainCache(location); } - return locationToReflectableTerrainFeatures[location]; + return locationToPuddleReflectionTerrainFeatures[location]; } private static void ResetLocationTerrainCache(GameLocation location) { - locationToReflectableTerrainFeatures[location] = new List(); + locationToWaterReflectionTerrainFeatures[location] = new List(); + locationToPuddleReflectionTerrainFeatures[location] = new List(); foreach (var terrainFeature in location.terrainFeatures.Values) { if (IsTileReflective(terrainFeature.Tile, 3)) { - locationToReflectableTerrainFeatures[location].Add(terrainFeature); + locationToWaterReflectionTerrainFeatures[location].Add(terrainFeature); + } + + if (IsTilePuddle(terrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(terrainFeature); } } @@ -1604,11 +1644,17 @@ private static void ResetLocationTerrainCache(GameLocation location) { if (IsTileReflective(largeTerrainFeature.Tile, 3)) { - locationToReflectableTerrainFeatures[location].Add(largeTerrainFeature); + locationToWaterReflectionTerrainFeatures[location].Add(largeTerrainFeature); + } + + if (IsTilePuddle(largeTerrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(largeTerrainFeature); } } - locationToReflectableTerrainFeatures[location] = locationToReflectableTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) @@ -1627,5 +1673,22 @@ private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) return false; } + + private static bool IsTilePuddle(Vector2 startPosition, int yTileOffset, bool checkPuddles = false) + { + for (int yOffset = -1; yOffset <= yTileOffset; yOffset++) + { + var tilePosition = startPosition + new Vector2(0, yOffset); + + if (puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true + || puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true + || puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + { + return true; + } + } + + return false; + } } } diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 3bbca53..f2ff886 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -114,6 +114,7 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev { DynamicReflections.isFilteringWater = true; SpriteBatchToolkit.RenderWaterReflectionGrass(); + SpriteBatchToolkit.RenderPuddleReflectionGrass(); } // Handle preliminary NPC reflection logic @@ -121,6 +122,7 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev { DynamicReflections.isFilteringWater = true; SpriteBatchToolkit.RenderWaterReflectionTerrain(); + SpriteBatchToolkit.RenderPuddleReflectionTerrain(); } _waterColor = Game1.currentLocation.waterColor.Value; diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index af9e1ac..9659edb 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -558,7 +558,7 @@ internal static void RenderWaterReflectionTerrain() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (TerrainFeature terrainFeature in DynamicReflections.GetReflectableTerrainFeatures(Game1.currentLocation)) + foreach (TerrainFeature terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Tree && terrainFeature is not Bush) { @@ -594,6 +594,55 @@ internal static void RenderWaterReflectionTerrain() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderPuddleReflectionTerrain() + { + if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) + { + return; + } + + // Set the render target + SpriteBatchToolkit.StartRendering(DynamicReflections.terrainPuddleReflectionRender); + + // Draw the scene + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + foreach (TerrainFeature terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) + { + if (terrainFeature is not Tree && terrainFeature is not Bush) + { + continue; + } + + int yOffset = 16; + if (terrainFeature is Tree) + { + yOffset = 8; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + yOffset) * 2, 0); + + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + terrainFeature.draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + + // Drop the render target + SpriteBatchToolkit.StopRendering(); + + Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); + } + internal static void RenderWaterReflectionGrass() { if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) @@ -607,7 +656,7 @@ internal static void RenderWaterReflectionGrass() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (var terrainFeature in DynamicReflections.GetReflectableTerrainFeatures(Game1.currentLocation)) + foreach (var terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Grass) { @@ -638,6 +687,50 @@ internal static void RenderWaterReflectionGrass() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderPuddleReflectionGrass() + { + if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) + { + return; + } + + // Set the render target + SpriteBatchToolkit.StartRendering(DynamicReflections.grassPuddleReflectionRender); + + // Draw the scene + Game1.graphics.GraphicsDevice.Clear(Color.Transparent); + + foreach (var terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) + { + if (terrainFeature is not Grass) + { + continue; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 48) * 2, 0); + + // Using SpriteSortMode.BackToFront for Grass to properly draw any "flowers" that should be on top + Game1.spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + terrainFeature.draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + + // Drop the render target + SpriteBatchToolkit.StopRendering(); + + Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); + } + internal static void DrawPuddleReflection(Texture2D mask) { DynamicReflections.mirrorReflectionEffect.Parameters["Mask"].SetValue(mask); @@ -648,7 +741,23 @@ internal static void DrawPuddleReflection(Texture2D mask) Game1.spriteBatch.Draw(DynamicReflections.nightSkyRenderTarget, Vector2.Zero, Color.White); } - Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); + if (DynamicReflections.shouldDrawWaterReflection is true || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) + { + if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) + { + // Draw terrain (tree / bushes) + Game1.spriteBatch.Draw(DynamicReflections.terrainPuddleReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + + // Draw the player + Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); + + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + // Draw grass + Game1.spriteBatch.Draw(DynamicReflections.grassPuddleReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + } Game1.spriteBatch.Draw(DynamicReflections.npcPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); @@ -822,7 +931,7 @@ internal static void DrawRenderedCharacters(bool isWavy = false) if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) { - // Draw terrain features (such as grass, but not trees) + // Draw grass Game1.spriteBatch.Draw(DynamicReflections.grassWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); } From 1126b99cc23c3560f0a699648d38b0ba067550bc Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:00:45 -0600 Subject: [PATCH 07/22] Added GMCM config options for grass and terrain reflections --- .../Framework/External/GenericModConfigMenu/GMCMHelper.cs | 2 ++ .../Framework/External/GenericModConfigMenu/ModConfig.cs | 6 +++--- DynamicReflections/i18n/default.json | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs index f53aeaa..0b0da99 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs @@ -37,6 +37,8 @@ public static void Register(IGenericModConfigMenuApi configApi, DynamicReflectio configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.ArePuddleReflectionsEnabled, value => DynamicReflections.modConfig.ArePuddleReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.puddle_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreNPCReflectionsEnabled, value => DynamicReflections.modConfig.AreNPCReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.npc_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreCompanionReflectionsEnabled, value => DynamicReflections.modConfig.AreCompanionReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.companion_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreGrassReflectionsEnabled, value => DynamicReflections.modConfig.AreGrassReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.grass_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreTerrainReflectionsEnabled, value => DynamicReflections.modConfig.AreTerrainReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.terrain_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreSkyReflectionsEnabled, value => DynamicReflections.modConfig.AreSkyReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.sky_reflections")); configApi.AddKeybind(ModManifest, () => DynamicReflections.modConfig.QuickMenuKey, value => DynamicReflections.modConfig.QuickMenuKey = value, () => Helper.Translation.Get("config.general_settings.shortcut_key"), () => Helper.Translation.Get("config.general_settings.shortcut_key.description")); diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs index 22156d4..e9adba6 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs @@ -14,10 +14,10 @@ public class ModConfig public bool AreWaterReflectionsEnabled { get; set; } = true; public bool AreMirrorReflectionsEnabled { get; set; } = true; public bool ArePuddleReflectionsEnabled { get; set; } = true; - public bool AreNPCReflectionsEnabled { get; set; } = true; - public bool AreGrassReflectionsEnabled { get; set; } = true; - public bool AreTerrainReflectionsEnabled { get; set; } = true; + public bool AreNPCReflectionsEnabled { get; set; } = true; public bool AreCompanionReflectionsEnabled { get; set; } = true; + public bool AreGrassReflectionsEnabled { get; set; } = true; + public bool AreTerrainReflectionsEnabled { get; set; } = true; public bool AreSkyReflectionsEnabled { get; set; } = true; public WaterSettings WaterReflectionSettings { get; set; } = new WaterSettings(); diff --git a/DynamicReflections/i18n/default.json b/DynamicReflections/i18n/default.json index 17649ba..cc19a27 100644 --- a/DynamicReflections/i18n/default.json +++ b/DynamicReflections/i18n/default.json @@ -8,6 +8,8 @@ "config.general_settings.puddle_reflections": "Enable Puddle Reflections", "config.general_settings.npc_reflections": "Enable NPC Reflections", "config.general_settings.companion_reflections": "Enable Companion Reflections", + "config.general_settings.grass_reflections": "Enable Grass Reflections", + "config.general_settings.terrain_reflections": "Enable Terrain Reflections", "config.general_settings.sky_reflections": "Enable Sky Reflections", "config.general_settings.link.click_here": "Click Here", "config.general_settings.link.return_main": "Return to the Main Page", From 88be46b8b9579a31a34eca13dbf5b1e4ff87db1c Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:31:30 -0600 Subject: [PATCH 08/22] Added player building reflections Condensed building draws into playerWaterReflectionRender and playerPuddleReflectionRender to allow proper layering. --- .../GenericModConfigMenu/GMCMHelper.cs | 1 + .../GenericModConfigMenu/ModConfig.cs | 1 + .../Framework/Patches/xTile/LayerPatch.cs | 2 +- .../Framework/Utilities/SpriteBatchToolkit.cs | 172 +++++++++++++----- DynamicReflections/i18n/default.json | 1 + 5 files changed, 134 insertions(+), 43 deletions(-) diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs index 0b0da99..cc8addf 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs @@ -39,6 +39,7 @@ public static void Register(IGenericModConfigMenuApi configApi, DynamicReflectio configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreCompanionReflectionsEnabled, value => DynamicReflections.modConfig.AreCompanionReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.companion_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreGrassReflectionsEnabled, value => DynamicReflections.modConfig.AreGrassReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.grass_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreTerrainReflectionsEnabled, value => DynamicReflections.modConfig.AreTerrainReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.terrain_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled, value => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.player_buildings_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreSkyReflectionsEnabled, value => DynamicReflections.modConfig.AreSkyReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.sky_reflections")); configApi.AddKeybind(ModManifest, () => DynamicReflections.modConfig.QuickMenuKey, value => DynamicReflections.modConfig.QuickMenuKey = value, () => Helper.Translation.Get("config.general_settings.shortcut_key"), () => Helper.Translation.Get("config.general_settings.shortcut_key.description")); diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs index e9adba6..8e52b3b 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs @@ -18,6 +18,7 @@ public class ModConfig public bool AreCompanionReflectionsEnabled { get; set; } = true; public bool AreGrassReflectionsEnabled { get; set; } = true; public bool AreTerrainReflectionsEnabled { get; set; } = true; + public bool ArePlayerBuildingReflectionsEnabled { get; set; } = true; public bool AreSkyReflectionsEnabled { get; set; } = true; public WaterSettings WaterReflectionSettings { get; set; } = new WaterSettings(); diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index f2ff886..45e3ba2 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -95,7 +95,7 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev } // Handle preliminary water reflection logic - if (DynamicReflections.shouldDrawWaterReflection is true) + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { DynamicReflections.isFilteringWater = true; SpriteBatchToolkit.RenderWaterReflectionPlayerSprite(); diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 9659edb..4fc6269 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewValley; +using StardewValley.Buildings; using StardewValley.TerrainFeatures; using System; using System.Collections.Generic; @@ -388,7 +389,23 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - DrawReflectionViaMatrix(); + // Draw buildings before player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionPlayerBuildings(16, afterPlayer: false); + } + + // Draw player reflection (if near water tile) + if (DynamicReflections.shouldDrawWaterReflection) + { + DrawReflectionViaMatrix(); + } + + // Draw buildings after player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionPlayerBuildings(16, beforePlayer: false); + } // Drop the render target SpriteBatchToolkit.StopRendering(); @@ -731,6 +748,53 @@ internal static void RenderPuddleReflectionGrass() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderWaterReflectionPlayerBuildings(int yOffsetBase, bool beforePlayer = true, bool afterPlayer = true) + { + if (Game1.currentLocation is null || Game1.currentLocation.buildings is null) + { + return; + } + + foreach (var building in Game1.currentLocation.buildings) + { + if (building is null || building is GreenhouseBuilding) + { + continue; + } + else if (beforePlayer is false && building.tileY.Value < Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && building.tileY.Value > Game1.player.Tile.Y) + { + continue; + } + + int yOffset = (building.tilesHigh.Value * 64) - yOffsetBase; + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, new Vector2(building.tileX.Value, building.tileY.Value) * 64).Y + yOffset) * 2, 0); + + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + // Ignore building's transparency when doing reflecitons + var cachedBuildingAlpha = building.alpha; + building.alpha = 1f; + + building.draw(Game1.spriteBatch); + + building.alpha = cachedBuildingAlpha; + + Game1.spriteBatch.End(); + } + } + internal static void DrawPuddleReflection(Texture2D mask) { DynamicReflections.mirrorReflectionEffect.Parameters["Mask"].SetValue(mask); @@ -799,46 +863,20 @@ internal static void RenderPuddleReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - var oldDirection = Game1.player.FacingDirection; - var oldSprite = Game1.player.FarmerSprite; - - // Original world position - var oldPosition = Game1.player.Position; - - // Where the reflection was previously drawn (world space) - var worldOffset = DynamicReflections.currentPuddleSettings.ReflectionOffset * 64f; - var targetWorld = oldPosition - worldOffset; - - // Convert both positions to screen space to build an equivalent translation - var playerScreen = Game1.GlobalToLocal(Game1.viewport, oldPosition); - var targetScreen = Game1.GlobalToLocal(Game1.viewport, targetWorld); - var delta = targetScreen - playerScreen; - - // Same vertical flip & pivot as before (across the player's original local Y) - var scale = Matrix.CreateScale(1f, -1f, 1f); - var pivot = Matrix.CreateTranslation(0f, playerScreen.Y * 2f, 0f); - - // Apply the offset as a pre-translation, then the original reflection matrix - var preTranslation = Matrix.CreateTranslation(delta.X, delta.Y, 0f); - var transform = preTranslation * scale * pivot; - - Game1.spriteBatch.Begin( - SpriteSortMode.FrontToBack, - BlendState.AlphaBlend, - SamplerState.PointClamp, - depthStencilState: null, - rasterizerState: DynamicReflections.rasterizer, - effect: null, - transformMatrix: transform - ); - - // Draw the player at their real position; transform handles reflection+offset - Game1.player.draw(Game1.spriteBatch); + // Draw buildings before player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionPlayerBuildings(1326, afterPlayer: false); + } - Game1.player.FacingDirection = oldDirection; - Game1.player.FarmerSprite = oldSprite; + // Draw player reflection + DrawPlayerPuddleReflection(); - Game1.spriteBatch.End(); + // Draw buildings after player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionPlayerBuildings(32, beforePlayer: false); + } // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); @@ -853,7 +891,6 @@ internal static void RenderPuddleReflectionPlayerSprite() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void DrawReflectionViaMatrix() { // Cache what we’re going to touch so we can restore it @@ -909,21 +946,72 @@ internal static void DrawReflectionViaMatrix() Game1.spriteBatch.End(); } + + internal static void DrawPlayerPuddleReflection() + { + var oldDirection = Game1.player.FacingDirection; + var oldSprite = Game1.player.FarmerSprite; + + // Original world position + var oldPosition = Game1.player.Position; + + // Where the reflection was previously drawn (world space) + var worldOffset = DynamicReflections.currentPuddleSettings.ReflectionOffset * 64f; + var targetWorld = oldPosition - worldOffset; + + // Convert both positions to screen space to build an equivalent translation + var playerScreen = Game1.GlobalToLocal(Game1.viewport, oldPosition); + var targetScreen = Game1.GlobalToLocal(Game1.viewport, targetWorld); + var delta = targetScreen - playerScreen; + + // Same vertical flip & pivot as before (across the player's original local Y) + var scale = Matrix.CreateScale(1f, -1f, 1f); + var pivot = Matrix.CreateTranslation(0f, playerScreen.Y * 2f, 0f); + + // Apply the offset as a pre-translation, then the original reflection matrix + var preTranslation = Matrix.CreateTranslation(delta.X, delta.Y, 0f); + var transform = preTranslation * scale * pivot; + + Game1.spriteBatch.Begin( + SpriteSortMode.FrontToBack, + BlendState.AlphaBlend, + SamplerState.PointClamp, + depthStencilState: null, + rasterizerState: DynamicReflections.rasterizer, + effect: null, + transformMatrix: transform + ); + + // Draw the player at their real position; transform handles reflection+offset + Game1.player.draw(Game1.spriteBatch); + + Game1.player.FacingDirection = oldDirection; + Game1.player.FarmerSprite = oldSprite; + + Game1.spriteBatch.End(); + } + internal static void DrawRenderedCharacters(bool isWavy = false) { - if (DynamicReflections.shouldDrawWaterReflection is true || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) { DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + // Draw player buildings + //Game1.spriteBatch.Draw(DynamicReflections.buildingWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + } + if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) { // Draw terrain (tree / bushes) Game1.spriteBatch.Draw(DynamicReflections.terrainWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); } - if (DynamicReflections.shouldDrawWaterReflection is true) + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { // Draw the player Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); diff --git a/DynamicReflections/i18n/default.json b/DynamicReflections/i18n/default.json index cc19a27..37fbd24 100644 --- a/DynamicReflections/i18n/default.json +++ b/DynamicReflections/i18n/default.json @@ -10,6 +10,7 @@ "config.general_settings.companion_reflections": "Enable Companion Reflections", "config.general_settings.grass_reflections": "Enable Grass Reflections", "config.general_settings.terrain_reflections": "Enable Terrain Reflections", + "config.general_settings.player_buildings_reflections": "Enable Player Building Reflections", "config.general_settings.sky_reflections": "Enable Sky Reflections", "config.general_settings.link.click_here": "Click Here", "config.general_settings.link.return_main": "Return to the Main Page", From cf32b08ed32d4c94ca80cf31ce78cb4518931f02 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:36:19 -0600 Subject: [PATCH 09/22] Refactored DrawReflectionViaMatrix to DrawPlayerWaterReflection --- DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 4fc6269..2e998a4 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -398,7 +398,7 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw player reflection (if near water tile) if (DynamicReflections.shouldDrawWaterReflection) { - DrawReflectionViaMatrix(); + DrawPlayerWaterReflection(); } // Draw buildings after player @@ -891,7 +891,7 @@ internal static void RenderPuddleReflectionPlayerSprite() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void DrawReflectionViaMatrix() + internal static void DrawPlayerWaterReflection() { // Cache what we’re going to touch so we can restore it var oldDirection = Game1.player.FacingDirection; @@ -946,7 +946,6 @@ internal static void DrawReflectionViaMatrix() Game1.spriteBatch.End(); } - internal static void DrawPlayerPuddleReflection() { var oldDirection = Game1.player.FacingDirection; From 6bd9b43f2359d9e0df6b27b687d2c87d1fc613e6 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sat, 3 Jan 2026 14:37:17 -0600 Subject: [PATCH 10/22] Minor code cleanup --- .../Framework/Utilities/SpriteBatchToolkit.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 2e998a4..9a65ab2 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -866,7 +866,7 @@ internal static void RenderPuddleReflectionPlayerSprite() // Draw buildings before player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { - RenderWaterReflectionPlayerBuildings(1326, afterPlayer: false); + RenderWaterReflectionPlayerBuildings(32, afterPlayer: false); } // Draw player reflection @@ -998,12 +998,6 @@ internal static void DrawRenderedCharacters(bool isWavy = false) DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - // Draw player buildings - //Game1.spriteBatch.Draw(DynamicReflections.buildingWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } - if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) { // Draw terrain (tree / bushes) From afbb7101b6a8a85901242611c6fb33e00ae1a280 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:40:34 -0600 Subject: [PATCH 11/22] Condensed terrain draw into playerWaterReflectionRender / playerPuddleReflectionRender --- DynamicReflections/DynamicReflections.cs | 4 - .../Framework/Patches/xTile/LayerPatch.cs | 8 -- .../Framework/Utilities/SpriteBatchToolkit.cs | 82 ++++++++++--------- 3 files changed, 44 insertions(+), 50 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 51fc541..3794eb5 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -90,8 +90,6 @@ public class DynamicReflections : Mod internal static RenderTarget2D[] maskedPlayerMirrorReflectionRenders; internal static RenderTarget2D npcWaterReflectionRender; internal static RenderTarget2D npcPuddleReflectionRender; - internal static RenderTarget2D terrainWaterReflectionRender; - internal static RenderTarget2D terrainPuddleReflectionRender; internal static RenderTarget2D grassWaterReflectionRender; internal static RenderTarget2D grassPuddleReflectionRender; internal static RenderTarget2D inBetweenRenderTarget; @@ -1286,8 +1284,6 @@ internal void LoadRenderers() RegenerateRenderer(ref playerPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcPuddleReflectionRender, shouldUseScreenDimensions); - RegenerateRenderer(ref terrainWaterReflectionRender, shouldUseScreenDimensions); - RegenerateRenderer(ref terrainPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref grassWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref grassPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref puddlesRenderTarget, shouldUseScreenDimensions); diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 45e3ba2..12a94f7 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -117,14 +117,6 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev SpriteBatchToolkit.RenderPuddleReflectionGrass(); } - // Handle preliminary NPC reflection logic - if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true) - { - DynamicReflections.isFilteringWater = true; - SpriteBatchToolkit.RenderWaterReflectionTerrain(); - SpriteBatchToolkit.RenderPuddleReflectionTerrain(); - } - _waterColor = Game1.currentLocation.waterColor.Value; if (DynamicReflections.modConfig.AreSkyReflectionsEnabled is true) { diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 9a65ab2..1e5a952 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -392,7 +392,13 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw buildings before player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { - RenderWaterReflectionPlayerBuildings(16, afterPlayer: false); + RenderWaterReflectionPlayerBuildings(20, afterPlayer: false); + } + + // Draw terrain before player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionTerrain(afterPlayer: false); } // Draw player reflection (if near water tile) @@ -404,7 +410,13 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw buildings after player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { - RenderWaterReflectionPlayerBuildings(16, beforePlayer: false); + RenderWaterReflectionPlayerBuildings(20, beforePlayer: false); + } + + // Draw terrain after player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderWaterReflectionTerrain(beforePlayer: false); } // Drop the render target @@ -562,25 +574,27 @@ internal static void RenderPuddleReflectionNPCs() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void RenderWaterReflectionTerrain() + internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) { if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) { return; } - // Set the render target - SpriteBatchToolkit.StartRendering(DynamicReflections.terrainWaterReflectionRender); - - // Draw the scene - Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (TerrainFeature terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Tree && terrainFeature is not Bush) { continue; } + else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + { + continue; + } int yOffset = 48; if (terrainFeature is Tree) @@ -604,32 +618,29 @@ internal static void RenderWaterReflectionTerrain() Game1.spriteBatch.End(); } - - // Drop the render target - SpriteBatchToolkit.StopRendering(); - - Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void RenderPuddleReflectionTerrain() + internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) { if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) { return; } - // Set the render target - SpriteBatchToolkit.StartRendering(DynamicReflections.terrainPuddleReflectionRender); - - // Draw the scene - Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (TerrainFeature terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Tree && terrainFeature is not Bush) { continue; } + else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + { + continue; + } int yOffset = 16; if (terrainFeature is Tree) @@ -653,11 +664,6 @@ internal static void RenderPuddleReflectionTerrain() Game1.spriteBatch.End(); } - - // Drop the render target - SpriteBatchToolkit.StopRendering(); - - Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } internal static void RenderWaterReflectionGrass() @@ -807,12 +813,6 @@ internal static void DrawPuddleReflection(Texture2D mask) if (DynamicReflections.shouldDrawWaterReflection is true || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) { - if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) - { - // Draw terrain (tree / bushes) - Game1.spriteBatch.Draw(DynamicReflections.terrainPuddleReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } - // Draw the player Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); @@ -869,6 +869,12 @@ internal static void RenderPuddleReflectionPlayerSprite() RenderWaterReflectionPlayerBuildings(32, afterPlayer: false); } + // Draw terrain before player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderPuddleReflectionTerrain(afterPlayer: false); + } + // Draw player reflection DrawPlayerPuddleReflection(); @@ -878,6 +884,12 @@ internal static void RenderPuddleReflectionPlayerSprite() RenderWaterReflectionPlayerBuildings(32, beforePlayer: false); } + // Draw terrain after player + if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) + { + RenderPuddleReflectionTerrain(beforePlayer: false); + } + // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); foreach (var rippleSprite in DynamicReflections.puddleManager.puddleRippleSprites.ToList()) @@ -998,12 +1010,6 @@ internal static void DrawRenderedCharacters(bool isWavy = false) DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); - if (DynamicReflections.modConfig.AreTerrainReflectionsEnabled) - { - // Draw terrain (tree / bushes) - Game1.spriteBatch.Draw(DynamicReflections.terrainWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } - if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { // Draw the player From 223743f60a44f432479a44124d4af63726a0f160 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:35:43 -0600 Subject: [PATCH 12/22] Condensed grass draw into playerWaterReflectionRender / playerPuddleReflectionRender --- DynamicReflections/DynamicReflections.cs | 4 - .../Framework/Patches/xTile/LayerPatch.cs | 8 -- .../Framework/Utilities/SpriteBatchToolkit.cs | 78 ++++++++++--------- 3 files changed, 42 insertions(+), 48 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 3794eb5..2966ab8 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -90,8 +90,6 @@ public class DynamicReflections : Mod internal static RenderTarget2D[] maskedPlayerMirrorReflectionRenders; internal static RenderTarget2D npcWaterReflectionRender; internal static RenderTarget2D npcPuddleReflectionRender; - internal static RenderTarget2D grassWaterReflectionRender; - internal static RenderTarget2D grassPuddleReflectionRender; internal static RenderTarget2D inBetweenRenderTarget; internal static RenderTarget2D mirrorsLayerRenderTarget; internal static RenderTarget2D mirrorsFurnitureRenderTarget; @@ -1284,8 +1282,6 @@ internal void LoadRenderers() RegenerateRenderer(ref playerPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcWaterReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref npcPuddleReflectionRender, shouldUseScreenDimensions); - RegenerateRenderer(ref grassWaterReflectionRender, shouldUseScreenDimensions); - RegenerateRenderer(ref grassPuddleReflectionRender, shouldUseScreenDimensions); RegenerateRenderer(ref puddlesRenderTarget, shouldUseScreenDimensions); RegenerateRenderer(ref mirrorsLayerRenderTarget, shouldUseScreenDimensions); diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index 12a94f7..ef376f7 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -109,14 +109,6 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev SpriteBatchToolkit.RenderPuddleReflectionNPCs(); } - // Handle preliminary grass reflection logic - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) - { - DynamicReflections.isFilteringWater = true; - SpriteBatchToolkit.RenderWaterReflectionGrass(); - SpriteBatchToolkit.RenderPuddleReflectionGrass(); - } - _waterColor = Game1.currentLocation.waterColor.Value; if (DynamicReflections.modConfig.AreSkyReflectionsEnabled is true) { diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 1e5a952..480b8f0 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -401,6 +401,12 @@ internal static void RenderWaterReflectionPlayerSprite() RenderWaterReflectionTerrain(afterPlayer: false); } + // Draw grass before player + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + RenderWaterReflectionGrass(afterPlayer: false); + } + // Draw player reflection (if near water tile) if (DynamicReflections.shouldDrawWaterReflection) { @@ -419,6 +425,12 @@ internal static void RenderWaterReflectionPlayerSprite() RenderWaterReflectionTerrain(beforePlayer: false); } + // Draw grass after player + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + RenderWaterReflectionGrass(beforePlayer: false); + } + // Drop the render target SpriteBatchToolkit.StopRendering(); @@ -666,25 +678,27 @@ internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, boo } } - internal static void RenderWaterReflectionGrass() + internal static void RenderWaterReflectionGrass(bool beforePlayer = true, bool afterPlayer = true) { if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) { return; } - // Set the render target - SpriteBatchToolkit.StartRendering(DynamicReflections.grassWaterReflectionRender); - - // Draw the scene - Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (var terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Grass) { continue; } + else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + { + continue; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -703,32 +717,29 @@ internal static void RenderWaterReflectionGrass() Game1.spriteBatch.End(); } - - // Drop the render target - SpriteBatchToolkit.StopRendering(); - - Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void RenderPuddleReflectionGrass() + internal static void RenderPuddleReflectionGrass(bool beforePlayer = true, bool afterPlayer = true) { if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) { return; } - // Set the render target - SpriteBatchToolkit.StartRendering(DynamicReflections.grassPuddleReflectionRender); - - // Draw the scene - Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - foreach (var terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) { if (terrainFeature is not Grass) { continue; } + else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + { + continue; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -747,11 +758,6 @@ internal static void RenderPuddleReflectionGrass() Game1.spriteBatch.End(); } - - // Drop the render target - SpriteBatchToolkit.StopRendering(); - - Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } internal static void RenderWaterReflectionPlayerBuildings(int yOffsetBase, bool beforePlayer = true, bool afterPlayer = true) @@ -815,12 +821,6 @@ internal static void DrawPuddleReflection(Texture2D mask) { // Draw the player Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); - - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - // Draw grass - Game1.spriteBatch.Draw(DynamicReflections.grassPuddleReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } } Game1.spriteBatch.Draw(DynamicReflections.npcPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); @@ -875,6 +875,12 @@ internal static void RenderPuddleReflectionPlayerSprite() RenderPuddleReflectionTerrain(afterPlayer: false); } + // Draw grass before player + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + RenderPuddleReflectionGrass(afterPlayer: false); + } + // Draw player reflection DrawPlayerPuddleReflection(); @@ -890,6 +896,12 @@ internal static void RenderPuddleReflectionPlayerSprite() RenderPuddleReflectionTerrain(beforePlayer: false); } + // Draw grass after player + if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) + { + RenderPuddleReflectionGrass(beforePlayer: false); + } + // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); foreach (var rippleSprite in DynamicReflections.puddleManager.puddleRippleSprites.ToList()) @@ -1016,12 +1028,6 @@ internal static void DrawRenderedCharacters(bool isWavy = false) Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); } - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - // Draw grass - Game1.spriteBatch.Draw(DynamicReflections.grassWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } - Game1.spriteBatch.End(); } From 57f2a030d1b343d6d7dd547af388a849d386e803 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:43:31 -0600 Subject: [PATCH 13/22] Now only rendering terrain and buildings if they are on screen --- .../Framework/Utilities/SpriteBatchToolkit.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 480b8f0..b4e2e04 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -607,6 +607,10 @@ internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool { continue; } + else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + { + continue; + } int yOffset = 48; if (terrainFeature is Tree) @@ -653,6 +657,10 @@ internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, boo { continue; } + else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + { + continue; + } int yOffset = 16; if (terrainFeature is Tree) @@ -699,6 +707,10 @@ internal static void RenderWaterReflectionGrass(bool beforePlayer = true, bool a { continue; } + else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + { + continue; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -740,6 +752,10 @@ internal static void RenderPuddleReflectionGrass(bool beforePlayer = true, bool { continue; } + else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + { + continue; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -781,6 +797,10 @@ internal static void RenderWaterReflectionPlayerBuildings(int yOffsetBase, bool { continue; } + else if (Utility.isOnScreen(new Vector2(building.tileX.Value, building.tileY.Value) * 64, building.tilesWide.Value * 64) is false) + { + continue; + } int yOffset = (building.tilesHigh.Value * 64) - yOffsetBase; if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) From 0338dec3486dc6358ecdadb0f5b1c55440ca8be8 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 18:44:11 -0600 Subject: [PATCH 14/22] Simplified all terrain and building objects grabs and sorting via ReflectableObject --- DynamicReflections/DynamicReflections.cs | 99 +++++-- .../Models/Reflections/ReflectableBuilding.cs | 28 ++ .../Models/Reflections/ReflectableObject.cs | 13 + .../Models/Reflections/ReflectableTerrain.cs | 28 ++ .../Framework/Utilities/SpriteBatchToolkit.cs | 277 ++++-------------- 5 files changed, 201 insertions(+), 244 deletions(-) create mode 100644 DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs create mode 100644 DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs create mode 100644 DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 2966ab8..c1e718b 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -21,6 +21,8 @@ using StardewValley.Menus; using DynamicReflections.Framework.Interfaces.Internal; using StardewValley.TerrainFeatures; +using DynamicReflections.Framework.Models.Reflections; +using StardewValley.Extensions; namespace DynamicReflections { @@ -51,8 +53,8 @@ public class DynamicReflections : Mod internal static readonly Dictionary waterTileCache = new Dictionary(); internal static Vector2? waterReflectionPosition; internal static Vector2? waterReflectionTilePosition; - internal static readonly Dictionary> locationToWaterReflectionTerrainFeatures = new Dictionary>(); - internal static readonly Dictionary> locationToPuddleReflectionTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToWaterReflectionTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToPuddleReflectionTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -144,6 +146,7 @@ public override void Entry(IModHelper helper) helper.Events.GameLoop.GameLaunched += OnGameLaunched; helper.Events.World.TerrainFeatureListChanged += OnTerrainFeatureListChanged; helper.Events.World.LargeTerrainFeatureListChanged += OnLargeTerrainFeatureChanged; + helper.Events.World.BuildingListChanged += OnBuildingListChanged; } public override object GetApi() @@ -668,7 +671,7 @@ private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events { foreach (var addedTerrainFeature in e.Added) { - HandleTerrainFeatureAddition(e.Location, addedTerrainFeature.Value); + HandleTerrainFeatureAddition(e.Location, new ReflectableTerrain(addedTerrainFeature.Value)); } foreach (var removedTerrainFeature in e.Removed) @@ -681,7 +684,7 @@ private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Event { foreach (var addedTerrainFeature in e.Added) { - HandleTerrainFeatureAddition(e.Location, addedTerrainFeature); + HandleTerrainFeatureAddition(e.Location, new ReflectableTerrain(addedTerrainFeature)); } foreach (var removedTerrainFeature in e.Removed) @@ -690,22 +693,35 @@ private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Event } } - private void HandleTerrainFeatureAddition(GameLocation location, TerrainFeature terrainFeature) + private void OnBuildingListChanged(object sender, StardewModdingAPI.Events.BuildingListChangedEventArgs e) + { + foreach (var addedBuilding in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableBuilding(addedBuilding)); + } + + if (e.Removed.Count() > 0) + { + ResetLocationTerrainCache(e.Location); + } + } + + private void HandleTerrainFeatureAddition(GameLocation location, ReflectableObject reflectableObject) { if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) { return; } - if (IsTileReflective(terrainFeature.Tile, 3)) + if (IsTileReflective(reflectableObject.Tile, 3)) { - locationToWaterReflectionTerrainFeatures[location].Add(terrainFeature); + locationToWaterReflectionTerrainFeatures[location].Add(reflectableObject); locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } - if (IsTilePuddle(terrainFeature.Tile, 3)) + if (IsTilePuddle(reflectableObject.Tile, 3)) { - locationToPuddleReflectionTerrainFeatures[location].Add(terrainFeature); + locationToPuddleReflectionTerrainFeatures[location].Add(reflectableObject); locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } } @@ -717,8 +733,8 @@ private void HandleTerrainFeatureRemoval(GameLocation location, TerrainFeature t return; } - locationToWaterReflectionTerrainFeatures[location].Remove(terrainFeature); - locationToPuddleReflectionTerrainFeatures[location].Remove(terrainFeature); + locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); + locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); } private void LoadContentPacks(bool silent = false) @@ -1585,11 +1601,11 @@ internal static IEnumerable GetActiveNPCs(GameLocation location) return Array.Empty(); } - internal static IEnumerable GetWaterReflectionTerrainFeatures(GameLocation location) + internal static IEnumerable GetWaterReflectionTerrainFeatures(GameLocation location) { if (location is null) { - return Array.Empty(); + return Array.Empty(); } if (locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false) @@ -1600,11 +1616,11 @@ internal static IEnumerable GetWaterReflectionTerrainFeatures(Ga return locationToWaterReflectionTerrainFeatures[location]; } - internal static IEnumerable GetPuddleReflectionTerrainFeatures(GameLocation location) + internal static IEnumerable GetPuddleReflectionTerrainFeatures(GameLocation location) { if (location is null) { - return Array.Empty(); + return Array.Empty(); } if (locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) @@ -1617,31 +1633,56 @@ internal static IEnumerable GetPuddleReflectionTerrainFeatures(G private static void ResetLocationTerrainCache(GameLocation location) { - locationToWaterReflectionTerrainFeatures[location] = new List(); - locationToPuddleReflectionTerrainFeatures[location] = new List(); - foreach (var terrainFeature in location.terrainFeatures.Values) + locationToWaterReflectionTerrainFeatures[location] = new List(); + locationToPuddleReflectionTerrainFeatures[location] = new List(); + + if (location.terrainFeatures is not null) { - if (IsTileReflective(terrainFeature.Tile, 3)) + foreach (var terrainFeature in location.terrainFeatures.Values) { - locationToWaterReflectionTerrainFeatures[location].Add(terrainFeature); - } + if (IsTileReflective(terrainFeature.Tile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableTerrain(terrainFeature)); + } - if (IsTilePuddle(terrainFeature.Tile, 3)) - { - locationToPuddleReflectionTerrainFeatures[location].Add(terrainFeature); + if (IsTilePuddle(terrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableTerrain(terrainFeature)); + } } } - foreach (var largeTerrainFeature in location.largeTerrainFeatures) + if (location.largeTerrainFeatures is not null) { - if (IsTileReflective(largeTerrainFeature.Tile, 3)) + foreach (var largeTerrainFeature in location.largeTerrainFeatures) { - locationToWaterReflectionTerrainFeatures[location].Add(largeTerrainFeature); + if (IsTileReflective(largeTerrainFeature.Tile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableTerrain(largeTerrainFeature)); + } + + if (IsTilePuddle(largeTerrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableTerrain(largeTerrainFeature)); + } } + } - if (IsTilePuddle(largeTerrainFeature.Tile, 3)) + // Add buildings + if (location.buildings is not null) + { + foreach (var building in location.buildings) { - locationToPuddleReflectionTerrainFeatures[location].Add(largeTerrainFeature); + var buildingTile = new Vector2(building.tileX.Value, building.tileY.Value); + if (IsTileReflective(buildingTile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); + } + + if (IsTilePuddle(buildingTile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); + } } } diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs new file mode 100644 index 0000000..d76c061 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Buildings; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableBuilding : ReflectableObject + { + public Building Building; + + public ReflectableBuilding(Building building) + { + Building = building; + Tile = new Vector2(building.tileX.Value, building.tileY.Value); + } + + public override void Draw(SpriteBatch spriteBatch) + { + Building.draw(spriteBatch); + } + + public override bool IsOnScreen() + { + return Utility.isOnScreen(Tile * 64, Building.tilesWide.Value * 64); + } + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs new file mode 100644 index 0000000..d7414a1 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs @@ -0,0 +1,13 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public abstract class ReflectableObject + { + public Vector2 Tile { get; set; } + + public abstract void Draw(SpriteBatch spriteBatch); + public abstract bool IsOnScreen(); + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs new file mode 100644 index 0000000..4068c0f --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableTerrain : ReflectableObject + { + public TerrainFeature Terrain; + + public ReflectableTerrain(TerrainFeature terrain) + { + Terrain = terrain; + Tile = terrain.Tile; + } + + public override void Draw(SpriteBatch spriteBatch) + { + Terrain.draw(spriteBatch); + } + + public override bool IsOnScreen() + { + // Allow for three tile (3 * 64) spacing for trees and bushes + return Utility.isOnScreen(Tile * 64, 3 * 64); + } + } +} diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index b4e2e04..e75704b 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -1,3 +1,4 @@ +using DynamicReflections.Framework.Models.Reflections; using DynamicReflections.Framework.Patches.Tiles; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -389,48 +390,24 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - // Draw buildings before player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionPlayerBuildings(20, afterPlayer: false); - } - // Draw terrain before player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { RenderWaterReflectionTerrain(afterPlayer: false); } - // Draw grass before player - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - RenderWaterReflectionGrass(afterPlayer: false); - } - // Draw player reflection (if near water tile) if (DynamicReflections.shouldDrawWaterReflection) { DrawPlayerWaterReflection(); } - // Draw buildings after player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionPlayerBuildings(20, beforePlayer: false); - } - // Draw terrain after player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { RenderWaterReflectionTerrain(beforePlayer: false); } - // Draw grass after player - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - RenderWaterReflectionGrass(beforePlayer: false); - } - // Drop the render target SpriteBatchToolkit.StopRendering(); @@ -588,240 +565,134 @@ internal static void RenderPuddleReflectionNPCs() internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) { - if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) + if (Game1.currentLocation is null) { return; } - foreach (TerrainFeature terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) + foreach (ReflectableObject reflectableObject in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { - if (terrainFeature is not Tree && terrainFeature is not Bush) - { - continue; - } - else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + if (beforePlayer is false && reflectableObject.Tile.Y < Game1.player.Tile.Y) { continue; } - else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + else if (afterPlayer is false && reflectableObject.Tile.Y > Game1.player.Tile.Y) { continue; } - else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + else if (reflectableObject.IsOnScreen() is false) { continue; } - int yOffset = 48; - if (terrainFeature is Tree) + int yOffset = 0; + var spriteSortMode = SpriteSortMode.FrontToBack; + if (reflectableObject is ReflectableTerrain reflectableTerrain) { - yOffset = 72; - } - - if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) - { - var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + yOffset) * 2, 0); - - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); - } - else - { - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - } - - terrainFeature.draw(Game1.spriteBatch); - - Game1.spriteBatch.End(); - } - } - - internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) - { - if (Game1.currentLocation is null || Game1.currentLocation.largeTerrainFeatures is null) - { - return; - } - - foreach (TerrainFeature terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) - { - if (terrainFeature is not Tree && terrainFeature is not Bush) - { - continue; - } - else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) - { - continue; - } - else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) - { - continue; - } - else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) - { - continue; - } - - int yOffset = 16; - if (terrainFeature is Tree) - { - yOffset = 8; - } - - if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) - { - var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + yOffset) * 2, 0); - - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); - } - else - { - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - } - - terrainFeature.draw(Game1.spriteBatch); - - Game1.spriteBatch.End(); - } - } + if (reflectableTerrain.Terrain is not Tree && reflectableTerrain.Terrain is not Bush && reflectableTerrain.Terrain is not Grass) + { + continue; + } - internal static void RenderWaterReflectionGrass(bool beforePlayer = true, bool afterPlayer = true) - { - if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) - { - return; - } + yOffset = 48; + if (reflectableTerrain.Terrain is Tree) + { + yOffset = 72; + } + else if (reflectableTerrain.Terrain is Grass) + { + yOffset = 80; + spriteSortMode = SpriteSortMode.BackToFront; + } - foreach (var terrainFeature in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) - { - if (terrainFeature is not Grass) - { - continue; - } - else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) - { - continue; - } - else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) - { - continue; + if (reflectableTerrain.Terrain is Grass) + { + spriteSortMode = SpriteSortMode.BackToFront; + } } - else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) + else if (reflectableObject is ReflectableBuilding reflectableBuilding) { - continue; + yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 20; } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 72) * 2, 0); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, reflectableObject.Tile * 64).Y + yOffset) * 2, 0); - // Using SpriteSortMode.BackToFront for Grass to properly draw any "flowers" that should be on top - Game1.spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); } else { - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp); } - terrainFeature.draw(Game1.spriteBatch); + reflectableObject.Draw(Game1.spriteBatch); Game1.spriteBatch.End(); } } - internal static void RenderPuddleReflectionGrass(bool beforePlayer = true, bool afterPlayer = true) + internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) { - if (Game1.currentLocation is null || Game1.currentLocation.terrainFeatures is null) + if (Game1.currentLocation is null) { return; } - foreach (var terrainFeature in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) + foreach (ReflectableObject reflectableObject in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) { - if (terrainFeature is not Grass) + if (beforePlayer is false && reflectableObject.Tile.Y < Game1.player.Tile.Y) { continue; } - else if (beforePlayer is false && terrainFeature.Tile.Y < Game1.player.Tile.Y) + else if (afterPlayer is false && reflectableObject.Tile.Y > Game1.player.Tile.Y) { continue; } - else if (afterPlayer is false && terrainFeature.Tile.Y > Game1.player.Tile.Y) + else if (reflectableObject.IsOnScreen() is false) { continue; } - else if (Utility.isOnScreen(terrainFeature.Tile * 64, 64) is false) - { - continue; - } - - if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) - { - var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, terrainFeature.Tile * 64).Y + 48) * 2, 0); - // Using SpriteSortMode.BackToFront for Grass to properly draw any "flowers" that should be on top - Game1.spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); - } - else + int yOffset = 0; + var spriteSortMode = SpriteSortMode.FrontToBack; + if (reflectableObject is ReflectableTerrain reflectableTerrain) { - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); - } - - terrainFeature.draw(Game1.spriteBatch); - - Game1.spriteBatch.End(); - } - } - - internal static void RenderWaterReflectionPlayerBuildings(int yOffsetBase, bool beforePlayer = true, bool afterPlayer = true) - { - if (Game1.currentLocation is null || Game1.currentLocation.buildings is null) - { - return; - } + if (reflectableTerrain.Terrain is not Tree && reflectableTerrain.Terrain is not Bush && reflectableTerrain.Terrain is not Grass) + { + continue; + } - foreach (var building in Game1.currentLocation.buildings) - { - if (building is null || building is GreenhouseBuilding) - { - continue; - } - else if (beforePlayer is false && building.tileY.Value < Game1.player.Tile.Y) - { - continue; - } - else if (afterPlayer is false && building.tileY.Value > Game1.player.Tile.Y) - { - continue; + yOffset = 16; + if (reflectableTerrain.Terrain is Tree) + { + yOffset = 8; + } + else if (reflectableTerrain.Terrain is Grass) + { + yOffset = 48; + spriteSortMode = SpriteSortMode.BackToFront; + } } - else if (Utility.isOnScreen(new Vector2(building.tileX.Value, building.tileY.Value) * 64, building.tilesWide.Value * 64) is false) + else if (reflectableObject is ReflectableBuilding reflectableBuilding) { - continue; + yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 32; } - int yOffset = (building.tilesHigh.Value * 64) - yOffsetBase; if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { var scale = Matrix.CreateScale(1, -1, 1); - var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, new Vector2(building.tileX.Value, building.tileY.Value) * 64).Y + yOffset) * 2, 0); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, reflectableObject.Tile * 64).Y + yOffset) * 2, 0); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); } else { - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp); } - // Ignore building's transparency when doing reflecitons - var cachedBuildingAlpha = building.alpha; - building.alpha = 1f; - - building.draw(Game1.spriteBatch); - - building.alpha = cachedBuildingAlpha; + reflectableObject.Draw(Game1.spriteBatch); Game1.spriteBatch.End(); } @@ -883,45 +754,21 @@ internal static void RenderPuddleReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - // Draw buildings before player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionPlayerBuildings(32, afterPlayer: false); - } - // Draw terrain before player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { RenderPuddleReflectionTerrain(afterPlayer: false); } - // Draw grass before player - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - RenderPuddleReflectionGrass(afterPlayer: false); - } - // Draw player reflection DrawPlayerPuddleReflection(); - // Draw buildings after player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionPlayerBuildings(32, beforePlayer: false); - } - // Draw terrain after player if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) { RenderPuddleReflectionTerrain(beforePlayer: false); } - // Draw grass after player - if (DynamicReflections.modConfig.AreGrassReflectionsEnabled) - { - RenderPuddleReflectionGrass(beforePlayer: false); - } - // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); foreach (var rippleSprite in DynamicReflections.puddleManager.puddleRippleSprites.ToList()) From 48ea6905df834c8a0be42b4bfba54ddb1ddd2469 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:17:29 -0600 Subject: [PATCH 15/22] Added support for furniture reflections --- DynamicReflections/DynamicReflections.cs | 71 +++++++++++++++---- .../Reflections/ReflectableFurniture.cs | 30 ++++++++ .../Models/Reflections/ReflectableTerrain.cs | 13 +++- .../Framework/Utilities/SpriteBatchToolkit.cs | 18 +++-- 4 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index c1e718b..24701af 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -1,28 +1,30 @@ +using DynamicReflections.Framework.External.GenericModConfigMenu; +using DynamicReflections.Framework.Interfaces.Internal; +using DynamicReflections.Framework.Managers; +using DynamicReflections.Framework.Models; +using DynamicReflections.Framework.Models.Reflections; +using DynamicReflections.Framework.Models.Settings; +using DynamicReflections.Framework.Patches.Objects; +using DynamicReflections.Framework.Patches.SMAPI; +using DynamicReflections.Framework.Patches.Tiles; +using DynamicReflections.Framework.Patches.Tools; +using DynamicReflections.Framework.Utilities; using HarmonyLib; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; -using DynamicReflections.Framework.Models; using StardewValley; +using StardewValley.Buildings; +using StardewValley.Extensions; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Objects; +using StardewValley.TerrainFeatures; using System; using System.Collections.Generic; using System.IO; -using DynamicReflections.Framework.Patches.SMAPI; -using DynamicReflections.Framework.Patches.Tiles; -using DynamicReflections.Framework.Patches.Tools; using System.Linq; -using DynamicReflections.Framework.Patches.Objects; -using DynamicReflections.Framework.Utilities; -using DynamicReflections.Framework.Managers; -using DynamicReflections.Framework.Models.Settings; using System.Text.Json; -using DynamicReflections.Framework.External.GenericModConfigMenu; -using StardewValley.Locations; -using StardewValley.Menus; -using DynamicReflections.Framework.Interfaces.Internal; -using StardewValley.TerrainFeatures; -using DynamicReflections.Framework.Models.Reflections; -using StardewValley.Extensions; namespace DynamicReflections { @@ -217,6 +219,17 @@ private void OnFurnitureListChanged(object sender, StardewModdingAPI.Events.Furn } } } + + // Handle cached reflections + foreach (var addedFurniture in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableFurniture(addedFurniture)); + } + + foreach (var removedFurniture in e.Removed) + { + HandleFurnitureRemoval(e.Location, removedFurniture); + } } private void OnWarped(object sender, StardewModdingAPI.Events.WarpedEventArgs e) @@ -737,6 +750,17 @@ private void HandleTerrainFeatureRemoval(GameLocation location, TerrainFeature t locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); } + private void HandleFurnitureRemoval(GameLocation location, Furniture furniture) + { + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + return; + } + + locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); + locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); + } + private void LoadContentPacks(bool silent = false) { // Clear the existing cache of custom buildings @@ -1686,6 +1710,23 @@ private static void ResetLocationTerrainCache(GameLocation location) } } + // Add furniture + if (location.furniture is not null) + { + foreach (var furniture in location.furniture) + { + if (IsTileReflective(furniture.TileLocation, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableFurniture(furniture)); + } + + if (IsTilePuddle(furniture.TileLocation, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableFurniture(furniture)); + } + } + } + locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs new file mode 100644 index 0000000..bb4aaf3 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Objects; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableFurniture : ReflectableObject + { + public Furniture Furniture; + + public ReflectableFurniture(Furniture furniture) + { + Furniture = furniture; + Tile = furniture.TileLocation; + } + + public override void Draw(SpriteBatch spriteBatch) + { + Furniture.isDrawingLocationFurniture = true; + Furniture.draw(spriteBatch, -1, -1); + Furniture.isDrawingLocationFurniture = false; + } + + public override bool IsOnScreen() + { + // Allow for three tile (3 * 64) spacing for trees and bushes + return Utility.isOnScreen(Tile * 64, 3 * 64); + } + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs index 4068c0f..f26e152 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs @@ -16,7 +16,18 @@ public ReflectableTerrain(TerrainFeature terrain) public override void Draw(SpriteBatch spriteBatch) { - Terrain.draw(spriteBatch); + if (Terrain is Tree tree) + { + float alphaCache = tree.alpha; + tree.alpha = 1f; + + Terrain.draw(spriteBatch); + tree.alpha = alphaCache; + } + else + { + Terrain.draw(spriteBatch); + } } public override bool IsOnScreen() diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index e75704b..eddf5d9 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -572,7 +572,7 @@ internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool foreach (ReflectableObject reflectableObject in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { - if (beforePlayer is false && reflectableObject.Tile.Y < Game1.player.Tile.Y) + if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) { continue; } @@ -614,6 +614,10 @@ internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool { yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 20; } + else if (reflectableObject is ReflectableFurniture reflectableFurniture) + { + yOffset = reflectableFurniture.Furniture.getTilesHigh() * 64; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -642,7 +646,7 @@ internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, boo foreach (ReflectableObject reflectableObject in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) { - if (beforePlayer is false && reflectableObject.Tile.Y < Game1.player.Tile.Y) + if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) { continue; } @@ -679,6 +683,10 @@ internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, boo { yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 32; } + else if (reflectableObject is ReflectableFurniture reflectableFurniture) + { + yOffset = reflectableFurniture.Furniture.getTilesHigh() * 16; + } if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) { @@ -797,8 +805,9 @@ internal static void DrawPlayerWaterReflection() var scale = Matrix.CreateScale(1f, -1f, 1f); // Pivot at the water reflection line (already computed in world space, convert to screen). + float yOffset = Game1.player.IsSitting() ? 16f : 0f; float pivotY = Game1.GlobalToLocal(Game1.viewport, DynamicReflections.waterReflectionPosition.Value).Y; - var position = Matrix.CreateTranslation(0f, pivotY * 2f, 0f); + var position = Matrix.CreateTranslation(0f, (pivotY + yOffset) * 2f, 0f); Game1.spriteBatch.Begin( SpriteSortMode.FrontToBack, @@ -855,8 +864,9 @@ internal static void DrawPlayerPuddleReflection() var delta = targetScreen - playerScreen; // Same vertical flip & pivot as before (across the player's original local Y) + float yOffset = Game1.player.IsSitting() ? 32f : 0f; var scale = Matrix.CreateScale(1f, -1f, 1f); - var pivot = Matrix.CreateTranslation(0f, playerScreen.Y * 2f, 0f); + var pivot = Matrix.CreateTranslation(0f, (playerScreen.Y + yOffset) * 2f, 0f); // Apply the offset as a pre-translation, then the original reflection matrix var preTranslation = Matrix.CreateTranslation(delta.X, delta.Y, 0f); From 195a5756bb7d1bc790980700a4acf8c846bd2934 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:36:23 -0600 Subject: [PATCH 16/22] Fixed cache list not sorting after removal of furniture or terrain --- DynamicReflections/DynamicReflections.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 24701af..aaed5c5 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -748,6 +748,9 @@ private void HandleTerrainFeatureRemoval(GameLocation location, TerrainFeature t locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); + + locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } private void HandleFurnitureRemoval(GameLocation location, Furniture furniture) @@ -759,6 +762,9 @@ private void HandleFurnitureRemoval(GameLocation location, Furniture furniture) locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); + + locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); } private void LoadContentPacks(bool silent = false) From fd0a75b6d46270ca454b560ea2d02cdbb5ecc998 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:36:29 -0600 Subject: [PATCH 17/22] Minor code cleanup --- .../Framework/Utilities/SpriteBatchToolkit.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index eddf5d9..2393db5 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -601,12 +601,7 @@ internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool } else if (reflectableTerrain.Terrain is Grass) { - yOffset = 80; - spriteSortMode = SpriteSortMode.BackToFront; - } - - if (reflectableTerrain.Terrain is Grass) - { + yOffset = 96; spriteSortMode = SpriteSortMode.BackToFront; } } From 5b222e728c6f371259056d571b8ed3bbe0c29698 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:45:43 -0600 Subject: [PATCH 18/22] Added furniture reflection config option --- .../Framework/External/GenericModConfigMenu/GMCMHelper.cs | 1 + .../Framework/External/GenericModConfigMenu/ModConfig.cs | 1 + DynamicReflections/i18n/default.json | 1 + 3 files changed, 3 insertions(+) diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs index cc8addf..ad30b35 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs @@ -40,6 +40,7 @@ public static void Register(IGenericModConfigMenuApi configApi, DynamicReflectio configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreGrassReflectionsEnabled, value => DynamicReflections.modConfig.AreGrassReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.grass_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreTerrainReflectionsEnabled, value => DynamicReflections.modConfig.AreTerrainReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.terrain_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled, value => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.player_buildings_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreFurnitureReflectionsEnabled, value => DynamicReflections.modConfig.AreFurnitureReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.furniture_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreSkyReflectionsEnabled, value => DynamicReflections.modConfig.AreSkyReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.sky_reflections")); configApi.AddKeybind(ModManifest, () => DynamicReflections.modConfig.QuickMenuKey, value => DynamicReflections.modConfig.QuickMenuKey = value, () => Helper.Translation.Get("config.general_settings.shortcut_key"), () => Helper.Translation.Get("config.general_settings.shortcut_key.description")); diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs index 8e52b3b..708a574 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs @@ -19,6 +19,7 @@ public class ModConfig public bool AreGrassReflectionsEnabled { get; set; } = true; public bool AreTerrainReflectionsEnabled { get; set; } = true; public bool ArePlayerBuildingReflectionsEnabled { get; set; } = true; + public bool AreFurnitureReflectionsEnabled { get; set; } = true; public bool AreSkyReflectionsEnabled { get; set; } = true; public WaterSettings WaterReflectionSettings { get; set; } = new WaterSettings(); diff --git a/DynamicReflections/i18n/default.json b/DynamicReflections/i18n/default.json index 37fbd24..cce25d4 100644 --- a/DynamicReflections/i18n/default.json +++ b/DynamicReflections/i18n/default.json @@ -11,6 +11,7 @@ "config.general_settings.grass_reflections": "Enable Grass Reflections", "config.general_settings.terrain_reflections": "Enable Terrain Reflections", "config.general_settings.player_buildings_reflections": "Enable Player Building Reflections", + "config.general_settings.furniture_reflections": "Enable Furniture Reflections", "config.general_settings.sky_reflections": "Enable Sky Reflections", "config.general_settings.link.click_here": "Click Here", "config.general_settings.link.return_main": "Return to the Main Page", From 75e22a3b7ff5401668193847bdabb61546dda243 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 19:46:09 -0600 Subject: [PATCH 19/22] Simplified the check for enabled reflection types --- .../Models/Reflections/ReflectableBuilding.cs | 5 +++ .../Reflections/ReflectableFurniture.cs | 5 +++ .../Models/Reflections/ReflectableObject.cs | 1 + .../Models/Reflections/ReflectableTerrain.cs | 10 +++++ .../Framework/Utilities/SpriteBatchToolkit.cs | 43 ++++++++----------- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs index d76c061..daa634d 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs @@ -24,5 +24,10 @@ public override bool IsOnScreen() { return Utility.isOnScreen(Tile * 64, Building.tilesWide.Value * 64); } + + public override bool IsEnabled() + { + return DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled; + } } } diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs index bb4aaf3..92f8e4b 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs @@ -26,5 +26,10 @@ public override bool IsOnScreen() // Allow for three tile (3 * 64) spacing for trees and bushes return Utility.isOnScreen(Tile * 64, 3 * 64); } + + public override bool IsEnabled() + { + return DynamicReflections.modConfig.AreFurnitureReflectionsEnabled; + } } } diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs index d7414a1..7df0051 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs @@ -9,5 +9,6 @@ public abstract class ReflectableObject public abstract void Draw(SpriteBatch spriteBatch); public abstract bool IsOnScreen(); + public abstract bool IsEnabled(); } } diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs index f26e152..e9958b6 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs @@ -35,5 +35,15 @@ public override bool IsOnScreen() // Allow for three tile (3 * 64) spacing for trees and bushes return Utility.isOnScreen(Tile * 64, 3 * 64); } + + public override bool IsEnabled() + { + if (Terrain is Grass) + { + return DynamicReflections.modConfig.AreGrassReflectionsEnabled; + } + + return DynamicReflections.modConfig.AreTerrainReflectionsEnabled; + } } } diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index 2393db5..0dd3ef1 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -391,10 +391,7 @@ internal static void RenderWaterReflectionPlayerSprite() Game1.graphics.GraphicsDevice.Clear(Color.Transparent); // Draw terrain before player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionTerrain(afterPlayer: false); - } + RenderWaterReflectionTerrain(afterPlayer: false); // Draw player reflection (if near water tile) if (DynamicReflections.shouldDrawWaterReflection) @@ -403,10 +400,7 @@ internal static void RenderWaterReflectionPlayerSprite() } // Draw terrain after player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderWaterReflectionTerrain(beforePlayer: false); - } + RenderWaterReflectionTerrain(beforePlayer: false); // Drop the render target SpriteBatchToolkit.StopRendering(); @@ -572,7 +566,11 @@ internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool foreach (ReflectableObject reflectableObject in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) { - if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) + if (reflectableObject.IsEnabled() is false) + { + continue; + } + else if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) { continue; } @@ -641,7 +639,11 @@ internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, boo foreach (ReflectableObject reflectableObject in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) { - if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) + if (reflectableObject.IsEnabled() is false) + { + continue; + } + else if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) { continue; } @@ -711,7 +713,7 @@ internal static void DrawPuddleReflection(Texture2D mask) Game1.spriteBatch.Draw(DynamicReflections.nightSkyRenderTarget, Vector2.Zero, Color.White); } - if (DynamicReflections.shouldDrawWaterReflection is true || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) + if (DynamicReflections.modConfig.ArePuddleReflectionsEnabled is true) { // Draw the player Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); @@ -758,19 +760,13 @@ internal static void RenderPuddleReflectionPlayerSprite() Game1.graphics.GraphicsDevice.Clear(Color.Transparent); // Draw terrain before player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderPuddleReflectionTerrain(afterPlayer: false); - } + RenderPuddleReflectionTerrain(afterPlayer: false); // Draw player reflection DrawPlayerPuddleReflection(); // Draw terrain after player - if (DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled) - { - RenderPuddleReflectionTerrain(beforePlayer: false); - } + RenderPuddleReflectionTerrain(beforePlayer: false); // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); @@ -889,16 +885,13 @@ internal static void DrawPlayerPuddleReflection() internal static void DrawRenderedCharacters(bool isWavy = false) { - if (DynamicReflections.modConfig.AreWaterReflectionsEnabled || DynamicReflections.modConfig.AreTerrainReflectionsEnabled is true || DynamicReflections.modConfig.AreGrassReflectionsEnabled is true) + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); - if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) - { - // Draw the player - Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); - } + // Draw the player + Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); Game1.spriteBatch.End(); } From 0c0fac141802ab80a58b5db6e0ae2806e4017634 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:06:07 -0600 Subject: [PATCH 20/22] Increased IsOnScreen max distance for terrain and buildings --- .../Framework/Models/Reflections/ReflectableBuilding.cs | 2 +- .../Framework/Models/Reflections/ReflectableTerrain.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs index daa634d..c0a4471 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs @@ -22,7 +22,7 @@ public override void Draw(SpriteBatch spriteBatch) public override bool IsOnScreen() { - return Utility.isOnScreen(Tile * 64, Building.tilesWide.Value * 64); + return Utility.isOnScreen(Tile * 64, 8 * 64); } public override bool IsEnabled() diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs index e9958b6..662bdc7 100644 --- a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs @@ -33,7 +33,7 @@ public override void Draw(SpriteBatch spriteBatch) public override bool IsOnScreen() { // Allow for three tile (3 * 64) spacing for trees and bushes - return Utility.isOnScreen(Tile * 64, 3 * 64); + return Utility.isOnScreen(Tile * 64, 8 * 64); } public override bool IsEnabled() From ee428c26ce420100af31dc07fbf31b17906eb0be Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:02:55 -0600 Subject: [PATCH 21/22] Adjusted buildings to check tile reflection based on height --- DynamicReflections/DynamicReflections.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index aaed5c5..d056960 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -1703,13 +1703,13 @@ private static void ResetLocationTerrainCache(GameLocation location) { foreach (var building in location.buildings) { - var buildingTile = new Vector2(building.tileX.Value, building.tileY.Value); - if (IsTileReflective(buildingTile, 3)) + var buildingTile = new Vector2(building.tileX.Value, building.tileY.Value + building.tilesHigh.Value); + if (IsTileReflective(buildingTile, 2)) { locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); } - if (IsTilePuddle(buildingTile, 3)) + if (IsTilePuddle(buildingTile, 2)) { locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); } From 6962c5df9f5aa095302f29e105e5acd6deb429e2 Mon Sep 17 00:00:00 2001 From: Floogen <31755155+Floogen@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:03:13 -0600 Subject: [PATCH 22/22] Fixed building reflections not updating when moving buildings --- DynamicReflections/DynamicReflections.cs | 2 +- .../Framework/Patches/Locations/GameLocationPatch.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index d056960..ee6df89 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -1661,7 +1661,7 @@ internal static IEnumerable GetPuddleReflectionTerrainFeature return locationToPuddleReflectionTerrainFeatures[location]; } - private static void ResetLocationTerrainCache(GameLocation location) + internal static void ResetLocationTerrainCache(GameLocation location) { locationToWaterReflectionTerrainFeatures[location] = new List(); locationToPuddleReflectionTerrainFeatures[location] = new List(); diff --git a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs index e5b4f8f..4038cfe 100644 --- a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs +++ b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs @@ -35,6 +35,7 @@ internal void Apply(Harmony harmony) 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))); + harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.OnBuildingMoved), new[] { typeof(Building) }), postfix: new HarmonyMethod(GetType(), nameof(OnBuildingMovedPostfix))); } [HarmonyBefore(new string[] { "shekurika.WaterFish" })] @@ -154,6 +155,11 @@ private static void UpdateWhenCurrentLocationPostfix(GameLocation __instance, Ga } } + private static void OnBuildingMovedPostfix(GameLocation __instance, Building building) + { + DynamicReflections.ResetLocationTerrainCache(__instance); + } + private static void GenerateRipple(GameLocation location, Point puddleTile, bool playSound = false) { TemporaryAnimatedSprite splashSprite = new TemporaryAnimatedSprite("TileSheets\\animations", new Microsoft.Xna.Framework.Rectangle(0, 0, 64, 64), Game1.random.Next(50, 100), 9, 1, new Vector2(puddleTile.X, puddleTile.Y) * 64f, flicker: false, flipped: false, 0f, 0f, DynamicReflections.currentPuddleSettings.RippleColor, 1f, 0f, 0f, 0f);