/// <summary>Apply the patch to a loaded asset.</summary> /// <typeparam name="T">The asset type.</typeparam> /// <param name="asset">The asset to edit.</param> public override void Edit <T>(IAssetData asset) { string errorPrefix = $"Can't apply map patch \"{this.LogName}\" to {this.TargetAsset}"; // validate if (typeof(T) != typeof(Map)) { this.Monitor.Log($"{errorPrefix}: this file isn't a map file (found {typeof(T)}).", LogLevel.Warn); return; } if (this.AppliesMapPatch && !this.FromAssetExists()) { this.Monitor.Log($"{errorPrefix}: the {nameof(PatchConfig.FromFile)} file '{this.FromAsset}' doesn't exist.", LogLevel.Warn); return; } // get map IAssetDataForMap targetAsset = asset.AsMap(); Map target = targetAsset.Data; // apply map area patch if (this.AppliesMapPatch) { Map source = this.ContentPack.Load <Map>(this.FromAsset); if (!this.TryApplyMapPatch(source, targetAsset, out string error)) { this.Monitor.Log($"{errorPrefix}: map patch couldn't be applied: {error}", LogLevel.Warn); } } // patch map tiles if (this.AppliesTilePatches) { int i = 0; foreach (EditMapPatchTile tilePatch in this.MapTiles) { i++; if (!this.TryApplyTile(target, tilePatch, out string error)) { this.Monitor.Log($"{errorPrefix}: {nameof(PatchConfig.MapTiles)} > entry {i + 1} couldn't be applied: {error}", LogLevel.Warn); } } } // patch map properties foreach (EditMapPatchProperty property in this.MapProperties) { string key = property.Key.Value; string value = property.Value.Value; if (value == null) { target.Properties.Remove(key); } else { target.Properties[key] = value; } } }
/********* ** Private methods *********/ /// <summary>Try to apply a map overlay patch.</summary> /// <param name="source">The source map to overlay.</param> /// <param name="targetAsset">The target map to overlay.</param> /// <param name="error">An error indicating why applying the patch failed, if applicable.</param> /// <returns>Returns whether applying the patch succeeded.</returns> private bool TryApplyMapPatch(Map source, IAssetDataForMap targetAsset, out string error) { Map target = targetAsset.Data; // read data Rectangle mapBounds = this.GetMapArea(source); if (!this.TryReadArea(this.FromArea, 0, 0, mapBounds.Width, mapBounds.Height, out Rectangle sourceArea, out error)) return this.Fail($"the source area is invalid: {error}.", out error); if (!this.TryReadArea(this.ToArea, 0, 0, sourceArea.Width, sourceArea.Height, out Rectangle targetArea, out error)) return this.Fail($"the target area is invalid: {error}.", out error); // validate area values string sourceAreaLabel = this.FromArea != null ? $"{nameof(this.FromArea)}" : "source map"; string targetAreaLabel = this.ToArea != null ? $"{nameof(this.ToArea)}" : "target map"; Point sourceMapSize = new Point(source.Layers.Max(p => p.LayerWidth), source.Layers.Max(p => p.LayerHeight)); if (!this.TryValidateArea(sourceArea, sourceMapSize, "source", out error)) return this.Fail(error, out error); if (!this.TryValidateArea(targetArea, null, "target", out error)) return this.Fail(error, out error); if (sourceArea.Width != targetArea.Width || sourceArea.Height != targetArea.Height) return this.Fail($"{sourceAreaLabel} size (Width:{sourceArea.Width}, Height:{sourceArea.Height}) doesn't match {targetAreaLabel} size (Width:{targetArea.Width}, Height:{targetArea.Height}).", out error); // apply source map this.ExtendMap(target, minWidth: targetArea.Right, minHeight: targetArea.Bottom); this.PatchMap(targetAsset, source: source, patchMode: this.PatchMode, sourceArea: sourceArea, targetArea: targetArea); error = null; return true; }
/// <summary>Edit a matched asset.</summary> /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param> public void Edit <T>(IAssetData asset) { Monitor.Log("Editing asset: " + asset.AssetName); string mapName = asset.AssetName.Replace("Maps/", "").Replace("Maps\\", ""); if (false && changeLocations.ContainsKey(mapName)) { IAssetDataForMap map = asset.AsMap(); for (int x = 0; x < map.Data.Layers[0].LayerWidth; x++) { for (int y = 0; y < map.Data.Layers[0].LayerHeight; y++) { if (SwimUtils.doesTileHaveProperty(map.Data, x, y, "Water", "Back") != null) { Tile tile = map.Data.GetLayer("Back").PickTile(new Location(x, y) * Game1.tileSize, Game1.viewport.Size); if (tile != null && (((mapName == "Beach" || mapName == "UnderwaterBeach") && x > 58 && x < 61 && y > 11 && y < 15) || mapName != "Beach")) { if (tile.TileIndexProperties.ContainsKey("Passable")) { tile.TileIndexProperties.Remove("Passable"); } } tile = map.Data.GetLayer("Front").PickTile(new Location(x, y) * Game1.tileSize, Game1.viewport.Size); if (tile != null) { if (tile.TileIndexProperties.ContainsKey("Passable")) { //tile.TileIndexProperties.Remove("Passable"); } } if (map.Data.GetLayer("AlwaysFront") != null) { tile = map.Data.GetLayer("AlwaysFront").PickTile(new Location(x, y) * Game1.tileSize, Game1.viewport.Size); if (tile != null) { if (tile.TileIndexProperties.ContainsKey("Passable")) { //tile.TileIndexProperties.Remove("Passable"); } } } tile = map.Data.GetLayer("Buildings").PickTile(new Location(x, y) * Game1.tileSize, Game1.viewport.Size); if (tile != null) { if ( ((mapName == "Beach" || mapName == "UnderwaterBeach") && x > 58 && x < 61 && y > 11 && y < 15) || (mapName != "Beach" && mapName != "UnderwaterBeach" && ((tile.TileIndex > 1292 && tile.TileIndex < 1297) || (tile.TileIndex > 1317 && tile.TileIndex < 1322) || (tile.TileIndex % 25 > 17 && tile.TileIndex / 25 < 53 && tile.TileIndex / 25 > 48) || (tile.TileIndex % 25 > 1 && tile.TileIndex % 25 < 7 && tile.TileIndex / 25 < 53 && tile.TileIndex / 25 > 48) || (tile.TileIndex % 25 > 11 && tile.TileIndex / 25 < 51 && tile.TileIndex / 25 > 48) || (tile.TileIndex % 25 > 10 && tile.TileIndex % 25 < 14 && tile.TileIndex / 25 < 49 && tile.TileIndex / 25 > 46) || tile.TileIndex == 734 || tile.TileIndex == 759 || tile.TileIndex == 628 || tile.TileIndex == 629 || (mapName == "Forest" && x == 119 && ((y > 42 && y < 48) || (y > 104 && y < 119))) ) ) ) { if (tile.TileIndexProperties.ContainsKey("Passable")) { tile.TileIndexProperties["Passable"] = "T"; } else { tile.TileIndexProperties.Add("Passable", "T"); } } else if (mapName == "Beach" && tile.TileIndex == 76) { if (x > 58 && x < 61 && y > 11 && y < 15) { Game1.getLocationFromName(mapName).removeTile(x, y, "Buildings"); } if (tile.TileIndexProperties.ContainsKey("Passable")) { tile.TileIndexProperties.Remove("Passable"); } } } } } } } }
/// <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; } } } } }
/// <summary>Edit a matched asset.</summary> /// <param name="asset">A helper which encapsulates metadata about an asset and enables changes to it.</param> public void Edit <T>(IAssetData asset) { IAssetDataForMap mapAsset = asset.AsMap(); Map map = mapAsset.Data; }
/// <inheritdoc /> public override void Edit <T>(IAssetData asset) { // validate if (typeof(T) != typeof(Map)) { this.WarnForPatch($"this file isn't a map file (found {typeof(T)})."); return; } if (this.AppliesMapPatch && !this.FromAssetExists()) { this.WarnForPatch($"the {nameof(PatchConfig.FromFile)} file '{this.FromAsset}' doesn't exist."); return; } // get map IAssetDataForMap targetAsset = asset.AsMap(); Map target = targetAsset.Data; // apply map area patch if (this.AppliesMapPatch) { Map source = this.ContentPack.ModContent.Load <Map>(this.FromAsset !); if (!this.TryApplyMapPatch(source, targetAsset, out string?error)) { this.WarnForPatch($"map patch couldn't be applied: {error}"); } } // patch map tiles if (this.AppliesTilePatches) { int i = 0; foreach (EditMapPatchTile tilePatch in this.MapTiles) { i++; if (!this.TryApplyTile(target, tilePatch, out string?error)) { this.WarnForPatch($"{nameof(PatchConfig.MapTiles)} > entry {i} couldn't be applied: {error}"); } } } // patch map properties foreach (EditMapPatchProperty property in this.MapProperties) { string key = property.Key.Value !; string?value = property.Value?.Value; if (value == null) { target.Properties.Remove(key); } else { target.Properties[key] = value; } } // apply map warps if (this.AddWarps.Any()) { this.ApplyWarps(target, out IDictionary <string, string> errors); foreach ((string warp, string error) in errors) { this.WarnForPatch($"{nameof(PatchConfig.AddWarps)} > warp '{warp}' couldn't be applied: {error}"); } } // apply text operations for (int i = 0; i < this.TextOperations.Length; i++) { if (!this.TryApplyTextOperation(target, this.TextOperations[i], out string?error)) { this.WarnForPatch($"{nameof(PatchConfig.TextOperations)} > entry {i} couldn't be applied: {error}"); } } }
/********* ** Private methods *********/ /// <inheritdoc cref="IContentEvents.AssetRequested"/> /// <param name="sender">The event sender.</param> /// <param name="e">The event data.</param> private void OnAssetRequested(object?sender, AssetRequestedEventArgs e) { // add farm type if (e.NameWithoutLocale.IsEquivalentTo("Data/AdditionalFarms")) { e.Edit(editor => { var data = editor.GetData <List <ModFarmType> >(); data.Add(new() { ID = this.ModManifest.UniqueID, TooltipStringPath = "Strings/UI:Pathoschild_BeachFarm_Description", MapName = "Pathoschild_SmallBeachFarm" }); }); } // add farm description else if (e.NameWithoutLocale.IsEquivalentTo("Strings/UI")) { e.Edit(editor => { var data = editor.AsDictionary <string, string>().Data; data["Pathoschild_BeachFarm_Description"] = $"{I18n.Farm_Name()}_{I18n.Farm_Description()}"; }); } // load map else if (e.NameWithoutLocale.IsEquivalentTo("Maps/Pathoschild_SmallBeachFarm")) { e.LoadFrom( () => { // load map Map map = this.Helper.ModContent.Load <Map>("assets/farm.tmx"); IAssetDataForMap editor = this.Helper.ModContent.GetPatchHelper(map).AsMap(); TileSheet outdoorTilesheet = map.GetTileSheet("untitled tile sheet"); Layer buildingsLayer = map.GetLayer("Buildings"); Layer backLayer = map.GetLayer("Back"); // add islands if (this.Config.EnableIslands) { Map islands = this.Helper.ModContent.Load <Map>("assets/overlay_islands.tmx"); Size size = islands.GetSizeInTiles(); editor.PatchMap(source: islands, targetArea: new Rectangle(0, 26, size.Width, size.Height)); } // add campfire if (this.Config.AddCampfire) { buildingsLayer.Tiles[65, 23] = new StaticTile(buildingsLayer, map.GetTileSheet("zbeach"), BlendMode.Alpha, 157); // driftwood pile buildingsLayer.Tiles[64, 22] = new StaticTile(buildingsLayer, outdoorTilesheet, BlendMode.Alpha, 242); // campfire } // remove shipping bin path if (!this.Config.ShippingBinPath) { for (int x = 71; x <= 72; x++) { for (int y = 14; y <= 15; y++) { backLayer.Tiles[x, y] = new StaticTile(backLayer, outdoorTilesheet, BlendMode.Alpha, 175); // grass tile } } } // add fishing pier if (this.Config.AddFishingPier) { // load overlay Map pier = this.Helper.ModContent.Load <Map>("assets/overlay_pier.tmx"); Size size = pier.GetSizeInTiles(); // get target position Point position = this.Config.CustomFishingPierPosition; if (position == Point.Zero) { position = new Point(70, 26); } // remove building tiles which block movement on the pier { var pierBack = pier.GetLayer("Back"); for (int x = 0; x < size.Width; x++) { for (int y = 0; y < size.Height; y++) { if (pierBack.Tiles[x, y] is not null) { buildingsLayer.Tiles[position.X + x, position.Y + y] = null; } } } } // apply overlay editor.PatchMap(source: pier, targetArea: new Rectangle(position.X, position.Y, size.Width, size.Height)); } // apply tilesheet recolors foreach (TileSheet tilesheet in map.TileSheets) { IAssetName imageSource = this.Helper.GameContent.ParseAssetName(tilesheet.ImageSource); if (imageSource.StartsWith($"{this.TilesheetsPath}/_default/")) { tilesheet.ImageSource = PathUtilities.NormalizeAssetName($"{this.FakeAssetPrefix}/{Path.GetFileNameWithoutExtension(tilesheet.ImageSource)}"); } } return(map); }, AssetLoadPriority.Exclusive ); } // load tilesheet else if (e.NameWithoutLocale.StartsWith(this.FakeAssetPrefix)) { e.LoadFrom( () => { string filename = Path.GetFileName(e.NameWithoutLocale.Name); if (!Path.HasExtension(filename)) { filename += ".png"; } // get relative path to load string?relativePath = new DirectoryInfo(this.GetFullPath(this.TilesheetsPath)) .EnumerateDirectories() .FirstOrDefault(p => p.Name != "_default" && this.Helper.ModRegistry.IsLoaded(p.Name)) ?.Name; relativePath = Path.Combine(this.TilesheetsPath, relativePath ?? "_default", filename); // load asset Texture2D tilesheet = this.Helper.ModContent.Load <Texture2D>(relativePath); return(tilesheet); }, AssetLoadPriority.Exclusive ); } }