add map overlay patches

This commit is contained in:
Jesse Plamondon-Willard 2021-09-25 01:28:15 -04:00
parent cb378a1c55
commit fe675540b5
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
4 changed files with 72 additions and 35 deletions

View File

@ -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

View File

@ -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) { }
/// <inheritdoc />
/// <remarks>Derived from <see cref="StardewValley.GameLocation.ApplyMapOverride"/> with a few changes:
/// <remarks>Derived from <see cref="GameLocation.ApplyMapOverride(Map,string,Rectangle?,Rectangle?)"/> 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.
/// </remarks>
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<Layer, Layer> layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
// get target layers
IDictionary<Layer, Layer> sourceToTargetLayers = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
HashSet<Layer> orphanedTargetLayers = new HashSet<Layer>(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
*********/
/// <summary>Create a new tile for the target map.</summary>
/// <param name="sourceTile">The source tile to copy.</param>
/// <param name="targetLayer">The target layer.</param>
/// <param name="targetSheet">The target tilesheet.</param>
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;
}
}
/// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary>
/// <param name="path">The path to normalize.</param>
private string NormalizeTilesheetPathForComparison(string path)

View File

@ -13,6 +13,7 @@ namespace StardewModdingAPI
/// <param name="source">The map from which to copy.</param>
/// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param>
/// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null);
/// <param name="patchMode">Indicates how the map should be patched.</param>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMapMode patchMode = PatchMapMode.Overlay);
}
}

15
src/SMAPI/PatchMapMode.cs Normal file
View File

@ -0,0 +1,15 @@
namespace StardewModdingAPI
{
/// <summary>Indicates how a map should be patched.</summary>
public enum PatchMapMode
{
/// <summary>Replace matching tiles. Target tiles missing in the source area are kept as-is.</summary>
Overlay,
/// <summary>Replace all tiles on layers that exist in the source map.</summary>
ReplaceByLayer,
/// <summary>Replace all tiles with the source map.</summary>
Replace
}
}