diff --git a/docs/release-notes.md b/docs/release-notes.md index f8c1b495..69eab3ec 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * For mod authors: * Migrated to 64-bit MonoGame and .NET 5 on all platforms (see [migration guide for mod authors](https://stardewvalleywiki.com/Modding:Migrate_to_Stardew_Valley_1.5.5)). + * Added support for [map overlays via `asset.AsMap().PatchMap`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Content#Edit_a_map). **Update note for players with older systems:** The game now has two branches: the _main branch_ which you'll get by default, and an optional diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs index 4f810948..0a5fa7e7 100644 --- a/src/SMAPI/Framework/Content/AssetDataForMap.cs +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Toolkit.Utilities; +using StardewValley; using xTile; using xTile.Layers; using xTile.Tiles; @@ -25,18 +26,17 @@ namespace StardewModdingAPI.Framework.Content : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// - /// Derived from with a few changes: + /// Derived from with a few changes: /// - can be applied directly to the maps when loading, before the location is created; - /// - added support for source/target areas; + /// - added support for patch modes (overlay, replace by layer, or fully replace); /// - added disambiguation if source has a modified version of the same tilesheet, instead of copying tiles into the target tilesheet; - /// - changed to always overwrite tiles within the target area (to avoid edge cases where some tiles are only partly applied); /// - fixed copying tilesheets (avoid "The specified TileSheet was not created for use with this map" error); /// - fixed tilesheets not added at the end (via z_ prefix), which can cause crashes in game code which depends on hardcoded tilesheet indexes; /// - fixed issue where different tilesheets are linked by ID. /// - public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null) + public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay) { - var target = this.Data; + Map target = this.Data; // get areas { @@ -84,10 +84,13 @@ namespace StardewModdingAPI.Framework.Content tilesheetMap[sourceSheet] = targetSheet; } - // get layer map - IDictionary layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id)); + // get target layers + IDictionary sourceToTargetLayers = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id)); + HashSet orphanedTargetLayers = new HashSet(target.Layers.Except(sourceToTargetLayers.Values)); // apply tiles + bool replaceAll = patchMode == PatchMapMode.Replace; + bool replaceByLayer = patchMode == PatchMapMode.ReplaceByLayer; for (int x = 0; x < sourceArea.Value.Width; x++) { for (int y = 0; y < sourceArea.Value.Height; y++) @@ -96,47 +99,37 @@ namespace StardewModdingAPI.Framework.Content Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y); Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y); + // replace tiles on target-only layers + if (replaceAll) + { + foreach (Layer targetLayer in orphanedTargetLayers) + targetLayer.Tiles[targetPos.X, targetPos.Y] = null; + } + // merge layers foreach (Layer sourceLayer in source.Layers) { // get layer - Layer targetLayer = layerMap[sourceLayer]; + Layer targetLayer = sourceToTargetLayers[sourceLayer]; if (targetLayer == null) { target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize)); - layerMap[sourceLayer] = target.GetLayer(sourceLayer.Id); + sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id); } // copy layer properties targetLayer.Properties.CopyFrom(sourceLayer.Properties); - // copy tiles + // create new tile Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y]; - Tile targetTile; - switch (sourceTile) - { - case StaticTile _: - targetTile = new StaticTile(targetLayer, tilesheetMap[sourceTile.TileSheet], sourceTile.BlendMode, sourceTile.TileIndex); - break; + Tile newTile = sourceTile != null + ? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet]) + : null; + newTile?.Properties.CopyFrom(sourceTile.Properties); - case AnimatedTile animatedTile: - { - StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length]; - for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame) - { - StaticTile frameTile = animatedTile.TileFrames[frame]; - tileFrames[frame] = new StaticTile(targetLayer, tilesheetMap[frameTile.TileSheet], frameTile.BlendMode, frameTile.TileIndex); - } - targetTile = new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval); - } - break; - - default: // null or unhandled type - targetTile = null; - break; - } - targetTile?.Properties.CopyFrom(sourceTile.Properties); - targetLayer.Tiles[targetPos.X, targetPos.Y] = targetTile; + // replace tile + if (newTile != null || replaceByLayer || replaceAll) + targetLayer.Tiles[targetPos.X, targetPos.Y] = newTile; } } } @@ -146,6 +139,33 @@ namespace StardewModdingAPI.Framework.Content /********* ** Private methods *********/ + /// Create a new tile for the target map. + /// The source tile to copy. + /// The target layer. + /// The target tilesheet. + private Tile CreateTile(Tile sourceTile, Layer targetLayer, TileSheet targetSheet) + { + switch (sourceTile) + { + case StaticTile _: + return new StaticTile(targetLayer, targetSheet, sourceTile.BlendMode, sourceTile.TileIndex); + + case AnimatedTile animatedTile: + { + StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length]; + for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame) + { + StaticTile frameTile = animatedTile.TileFrames[frame]; + tileFrames[frame] = new StaticTile(targetLayer, targetSheet, frameTile.BlendMode, frameTile.TileIndex); + } + + return new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval); + } + + default: // null or unhandled type + return null; + } + } /// Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path. /// The path to normalize. private string NormalizeTilesheetPathForComparison(string path) diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs index bfaba9ba..47a33de8 100644 --- a/src/SMAPI/IAssetDataForMap.cs +++ b/src/SMAPI/IAssetDataForMap.cs @@ -13,6 +13,7 @@ namespace StardewModdingAPI /// The map from which to copy. /// The tile area within the source map to copy, or null for the entire source map size. This must be within the bounds of the map. /// The tile area within the target map to overwrite, or null to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map. - void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null); + /// Indicates how the map should be patched. + void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay); } } diff --git a/src/SMAPI/PatchMapMode.cs b/src/SMAPI/PatchMapMode.cs new file mode 100644 index 00000000..1f1ac6a9 --- /dev/null +++ b/src/SMAPI/PatchMapMode.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI +{ + /// Indicates how a map should be patched. + public enum PatchMapMode + { + /// Replace matching tiles. Target tiles missing in the source area are kept as-is. + Overlay, + + /// Replace all tiles on layers that exist in the source map. + ReplaceByLayer, + + /// Replace all tiles with the source map. + Replace + } +}