/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="indexPath">The path of indexes from the root <c>content.json</c> to this patch; see <see cref="IPatch.IndexPath"/>.</param> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The map area from which to read tiles.</param> /// <param name="patchMode">Indicates how the map should be patched.</param> /// <param name="toArea">The map area to overwrite.</param> /// <param name="mapProperties">The map properties to change when editing a map, if any.</param> /// <param name="textOperations">The text operations to apply to existing values.</param> /// <param name="mapTiles">The map tiles to change when editing a map.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="reflection">Simplifies access to private code.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> public EditMapPatch(int[] indexPath, LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle fromArea, TokenRectangle toArea, PatchMapMode patchMode, IEnumerable <EditMapPatchProperty> mapProperties, IEnumerable <EditMapPatchTile> mapTiles, IEnumerable <TextOperation> textOperations, UpdateRate updateRate, IContentPack contentPack, IPatch parentPatch, IMonitor monitor, IReflectionHelper reflection, Func <string, string> normalizeAssetName) : base( indexPath: indexPath, path: path, type: PatchType.EditMap, assetName: assetName, fromAsset: fromAsset, conditions: conditions, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch, normalizeAssetName: normalizeAssetName ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.MapProperties = mapProperties?.ToArray() ?? new EditMapPatchProperty[0]; this.MapTiles = mapTiles?.ToArray() ?? new EditMapPatchTile[0]; this.TextOperations = textOperations?.ToArray() ?? new TextOperation[0]; this.Monitor = monitor; this.Reflection = reflection; this.Contextuals .Add(this.FromArea) .Add(this.ToArea) .Add(this.MapProperties) .Add(this.MapTiles) .Add(this.TextOperations); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="indexPath">The path of indexes from the root <c>content.json</c> to this patch; see <see cref="IPatch.IndexPath"/>.</param> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The map area from which to read tiles.</param> /// <param name="patchMode">Indicates how the map should be patched.</param> /// <param name="toArea">The map area to overwrite.</param> /// <param name="mapProperties">The map properties to change when editing a map, if any.</param> /// <param name="mapTiles">The map tiles to change when editing a map.</param> /// <param name="addWarps">The warps to add to the location.</param> /// <param name="textOperations">The text operations to apply to existing values.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="parseAssetName">Parse an asset name.</param> public EditMapPatch(int[] indexPath, LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString?fromAsset, TokenRectangle?fromArea, TokenRectangle?toArea, PatchMapMode patchMode, IEnumerable <EditMapPatchProperty>?mapProperties, IEnumerable <EditMapPatchTile>?mapTiles, IEnumerable <IManagedTokenString>?addWarps, IEnumerable <ITextOperation>?textOperations, UpdateRate updateRate, IContentPack contentPack, IPatch?parentPatch, IMonitor monitor, Func <string, IAssetName> parseAssetName) : base( indexPath: indexPath, path: path, type: PatchType.EditMap, assetName: assetName, fromAsset: fromAsset, conditions: conditions, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch, parseAssetName: parseAssetName ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.MapProperties = mapProperties?.ToArray() ?? Array.Empty <EditMapPatchProperty>(); this.MapTiles = mapTiles?.ToArray() ?? Array.Empty <EditMapPatchTile>(); this.AddWarps = addWarps?.Reverse().ToArray() ?? Array.Empty <IManagedTokenString>(); // reversing the warps allows later ones to 'overwrite' earlier ones, since the game checks them in the listed order this.TextOperations = textOperations?.ToArray() ?? Array.Empty <ITextOperation>(); this.Monitor = monitor; this.Contextuals .Add(this.FromArea) .Add(this.ToArea) .Add(this.MapProperties) .Add(this.MapTiles) .Add(this.AddWarps) .Add(this.TextOperations); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="indexPath">The path of indexes from the root <c>content.json</c> to this patch; see <see cref="IPatch.IndexPath"/>.</param> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The map area from which to read tiles.</param> /// <param name="patchMode">Indicates how the map should be patched.</param> /// <param name="toArea">The map area to overwrite.</param> /// <param name="mapProperties">The map properties to change when editing a map, if any.</param> /// <param name="mapTiles">The map tiles to change when editing a map.</param> /// <param name="addWarps">The warps to add to the location.</param> /// <param name="textOperations">The text operations to apply to existing values.</param> /// <param name="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="reflection">Simplifies access to private code.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> public EditMapPatch(int[] indexPath, LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle fromArea, TokenRectangle toArea, PatchMapMode patchMode, IEnumerable <EditMapPatchProperty> mapProperties, IEnumerable <EditMapPatchTile> mapTiles, IEnumerable <IManagedTokenString> addWarps, IEnumerable <TextOperation> textOperations, UpdateRate updateRate, IContentPack contentPack, IPatch parentPatch, IMonitor monitor, IReflectionHelper reflection, Func <string, string> normalizeAssetName) : base( indexPath: indexPath, path: path, type: PatchType.EditMap, assetName: assetName, fromAsset: fromAsset, conditions: conditions, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch, normalizeAssetName: normalizeAssetName ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.MapProperties = mapProperties?.ToArray() ?? new EditMapPatchProperty[0]; this.MapTiles = mapTiles?.ToArray() ?? new EditMapPatchTile[0]; this.AddWarps = addWarps?.Reverse().ToArray() ?? new IManagedTokenString[0]; // reversing the warps allows later ones to 'overwrite' earlier ones, since the game checks them in the listed order this.TextOperations = textOperations?.ToArray() ?? new TextOperation[0]; this.Monitor = monitor; this.Reflection = reflection; this.Contextuals .Add(this.FromArea) .Add(this.ToArea) .Add(this.MapProperties) .Add(this.MapTiles) .Add(this.AddWarps) .Add(this.TextOperations); }
/// <summary>Copy layers, tiles, and tilesheets from another map onto the asset.</summary> /// <param name="asset">The asset being edited.</param> /// <param name="source">The map from which to copy.</param> /// <param name="patchMode">Indicates how the map should be patched.</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> /// <remarks> /// This is temporarily duplicated from SMAPI's <see cref="IAssetDataForMap"/>, to add map overlay support before the feature is added to SMAPI. /// </remarks> public void PatchMap(IAssetDataForMap asset, Map source, PatchMapMode patchMode, Rectangle?sourceArea = null, Rectangle?targetArea = null) { Map target = asset.Data; // get areas { Rectangle sourceBounds = this.GetMapArea(source); Rectangle targetBounds = this.GetMapArea(target); sourceArea ??= new Rectangle(0, 0, sourceBounds.Width, sourceBounds.Height); targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, targetBounds.Width), Math.Min(sourceArea.Value.Height, targetBounds.Height)); // validate if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > sourceBounds.Width || sourceArea.Value.Bottom > sourceBounds.Height) { throw new ArgumentOutOfRangeException(nameof(sourceArea), $"The source area ({sourceArea}) is outside the bounds of the source map ({sourceBounds})."); } if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > targetBounds.Width || targetArea.Value.Bottom > targetBounds.Height) { throw new ArgumentOutOfRangeException(nameof(targetArea), $"The target area ({targetArea}) is outside the bounds of the target map ({targetBounds})."); } if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) { throw new InvalidOperationException($"The source area ({sourceArea}) and target area ({targetArea}) must be the same size."); } } // apply tilesheets IDictionary <TileSheet, TileSheet> tilesheetMap = new Dictionary <TileSheet, TileSheet>(); foreach (TileSheet sourceSheet in source.TileSheets) { // copy tilesheets TileSheet targetSheet = target.GetTileSheet(sourceSheet.Id); if (targetSheet == null || this.NormalizeTilesheetPathForComparison(targetSheet.ImageSource) != this.NormalizeTilesheetPathForComparison(sourceSheet.ImageSource)) { // change ID if needed so new tilesheets are added after vanilla ones (to avoid errors in hardcoded game logic) string id = sourceSheet.Id; if (!id.StartsWith("z_", StringComparison.OrdinalIgnoreCase)) { id = $"z_{id}"; } // change ID if it conflicts with an existing tilesheet if (target.GetTileSheet(id) != null) { int disambiguator = Enumerable.Range(2, int.MaxValue - 1).First(p => target.GetTileSheet($"{id}_{p}") == null); id = $"{id}_{disambiguator}"; } // add tilesheet targetSheet = new TileSheet(id, target, sourceSheet.ImageSource, sourceSheet.SheetSize, sourceSheet.TileSize); for (int i = 0, tileCount = sourceSheet.TileCount; i < tileCount; ++i) { targetSheet.TileIndexProperties[i].CopyFrom(sourceSheet.TileIndexProperties[i]); } target.AddTileSheet(targetSheet); } tilesheetMap[sourceSheet] = targetSheet; } // 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++) { // calculate tile positions 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 = sourceToTargetLayers[sourceLayer]; if (targetLayer == null) { target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize)); sourceToTargetLayers[sourceLayer] = target.GetLayer(sourceLayer.Id); } // copy layer properties targetLayer.Properties.CopyFrom(sourceLayer.Properties); // create new tile Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y]; Tile newTile = sourceTile != null ? this.CreateTile(sourceTile, targetLayer, tilesheetMap[sourceTile.TileSheet]) : null; newTile?.Properties.CopyFrom(sourceTile.Properties); // replace tile if (newTile != null || replaceByLayer || replaceAll) { targetLayer.Tiles[targetPos.X, targetPos.Y] = newTile; } } } } }