using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Buildings;
using StardewValley.Characters;
using StardewValley.GameData.Movies;
using StardewValley.Locations;
using StardewValley.Menus;
using StardewValley.Objects;
using StardewValley.Projectiles;
using StardewValley.TerrainFeatures;
using xTile;
using xTile.Tiles;
namespace StardewModdingAPI.Metadata
{
/// Propagates changes to core assets to the game state.
internal class CoreAssetPropagator
{
/*********
** Fields
*********/
/// The main content manager through which to reload assets.
private readonly LocalizedContentManager MainContentManager;
/// An internal content manager used only for asset propagation. See remarks on .
private readonly GameContentManagerForAssetPropagation DisposableContentManager;
/// Writes messages to the console.
private readonly IMonitor Monitor;
/// Simplifies access to private game code.
private readonly Reflector Reflection;
/// Whether to enable more aggressive memory optimizations.
private readonly bool AggressiveMemoryOptimizations;
/// Normalizes an asset key to match the cache key and assert that it's valid.
private readonly Func AssertAndNormalizeAssetName;
/// Optimized bucket categories for batch reloading assets.
private enum AssetBucket
{
/// NPC overworld sprites.
Sprite,
/// Villager dialogue portraits.
Portrait,
/// Any other asset.
Other
};
/*********
** Public methods
*********/
/// Initialize the core asset data.
/// The main content manager through which to reload assets.
/// An internal content manager used only for asset propagation.
/// Writes messages to the console.
/// Simplifies access to private code.
/// Whether to enable more aggressive memory optimizations.
public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, bool aggressiveMemoryOptimizations)
{
this.MainContentManager = mainContent;
this.DisposableContentManager = disposableContent;
this.Monitor = monitor;
this.Reflection = reflection;
this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
this.AssertAndNormalizeAssetName = disposableContent.AssertAndNormalizeAssetName;
}
/// Reload one of the game's core assets (if applicable).
/// The asset keys and types to reload.
/// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.
/// A lookup of asset names to whether they've been propagated.
/// Whether the NPC pathfinding cache was reloaded.
public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
{
if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters"))
return AssetBucket.Sprite;
if (this.IsInFolder(p.Key, "Portraits"))
return AssetBucket.Portrait;
return AssetBucket.Other;
});
// reload assets
propagatedAssets = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase);
updatedNpcWarps = false;
foreach (var bucket in buckets)
{
switch (bucket.Key)
{
case AssetBucket.Sprite:
if (!ignoreWorld)
this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets);
break;
case AssetBucket.Portrait:
if (!ignoreWorld)
this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets);
break;
default:
foreach (var entry in bucket)
{
bool changed = false;
bool curChangedMapWarps = false;
try
{
changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapWarps);
}
catch (Exception ex)
{
this.Monitor.Log($"An error occurred while propagating asset changes. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
propagatedAssets[entry.Key] = changed;
updatedNpcWarps = updatedNpcWarps || curChangedMapWarps;
}
break;
}
}
// reload NPC pathfinding cache if any map changed
if (updatedNpcWarps)
NPC.populateRoutesFromLocationToLocationList();
}
/*********
** Private methods
*********/
/// Reload one of the game's core assets (if applicable).
/// The asset key to reload.
/// The asset type to reload.
/// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.
/// Whether any map warps were changed as part of this propagation.
/// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")]
private bool PropagateOther(string key, Type type, bool ignoreWorld, out bool changedWarps)
{
var content = this.MainContentManager;
key = this.AssertAndNormalizeAssetName(key);
changedWarps = false;
/****
** Special case: current map tilesheet
** We only need to do this for the current location, since tilesheets are reloaded when you enter a location.
** Just in case, we should still propagate by key even if a tilesheet is matched.
****/
if (!ignoreWorld && Game1.currentLocation?.map?.TileSheets != null)
{
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
if (this.NormalizeAssetNameIgnoringEmpty(tilesheet.ImageSource) == key)
Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
}
}
/****
** Propagate map changes
****/
if (type == typeof(Map))
{
bool anyChanged = false;
if (!ignoreWorld)
{
foreach (LocationInfo info in this.GetLocationsWithInfo())
{
GameLocation location = info.Location;
if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
{
static ISet GetWarpSet(GameLocation location)
{
return new HashSet(
location.warps.Select(p => $"{p.X} {p.Y} {p.TargetName} {p.TargetX} {p.TargetY}")
);
}
var oldWarps = GetWarpSet(location);
this.ReloadMap(info);
var newWarps = GetWarpSet(location);
changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
anyChanged = true;
}
}
}
return anyChanged;
}
/****
** Propagate by key
****/
Reflector reflection = this.Reflection;
switch (key.ToLower().Replace("/", "\\")) // normalized key so we can compare statically
{
/****
** Animals
****/
case "animals\\horse":
return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key);
/****
** Buildings
****/
case "buildings\\houses": // Farm
Farm.houseTextures = this.LoadAndDisposeIfNeeded(Farm.houseTextures, key);
return true;
case "buildings\\houses_paintmask": // Farm
{
bool removedFromCache = this.RemoveFromPaintMaskCache(key);
Farm farm = Game1.getFarm();
farm?.ApplyHousePaint();
return removedFromCache || farm != null;
}
/****
** Content\Characters\Farmer
****/
case "characters\\farmer\\accessories": // Game1.LoadContent
FarmerRenderer.accessoriesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.accessoriesTexture, key);
return true;
case "characters\\farmer\\farmer_base": // Farmer
case "characters\\farmer\\farmer_base_bald":
case "characters\\farmer\\farmer_girl_base":
case "characters\\farmer\\farmer_girl_base_bald":
return !ignoreWorld && this.ReloadPlayerSprites(key);
case "characters\\farmer\\hairstyles": // Game1.LoadContent
FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key);
return true;
case "characters\\farmer\\hats": // Game1.LoadContent
FarmerRenderer.hatsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hatsTexture, key);
return true;
case "characters\\farmer\\pants": // Game1.LoadContent
FarmerRenderer.pantsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.pantsTexture, key);
return true;
case "characters\\farmer\\shirts": // Game1.LoadContent
FarmerRenderer.shirtsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.shirtsTexture, key);
return true;
/****
** Content\Data
****/
case "data\\achievements": // Game1.LoadContent
Game1.achievements = content.Load>(key);
return true;
case "data\\bigcraftablesinformation": // Game1.LoadContent
Game1.bigCraftablesInformation = content.Load>(key);
return true;
case "data\\clothinginformation": // Game1.LoadContent
Game1.clothingInformation = content.Load>(key);
return true;
case "data\\concessions": // MovieTheater.GetConcessions
MovieTheater.ClearCachedLocalizedData();
return true;
case "data\\concessiontastes": // MovieTheater.GetConcessionTasteForCharacter
this.Reflection
.GetField>(typeof(MovieTheater), "_concessionTastes")
.SetValue(content.Load>(key));
return true;
case "data\\cookingrecipes": // CraftingRecipe.InitShared
CraftingRecipe.cookingRecipes = content.Load>(key);
return true;
case "data\\craftingrecipes": // CraftingRecipe.InitShared
CraftingRecipe.craftingRecipes = content.Load>(key);
return true;
case "data\\farmanimals": // FarmAnimal constructor
return !ignoreWorld && this.ReloadFarmAnimalData();
case "data\\hairdata": // Farmer.GetHairStyleMetadataFile
return this.ReloadHairData();
case "data\\movies": // MovieTheater.GetMovieData
case "data\\moviesreactions": // MovieTheater.GetMovieReactions
MovieTheater.ClearCachedLocalizedData();
return true;
case "data\\npcdispositions": // NPC constructor
return !ignoreWorld && this.ReloadNpcDispositions(content, key);
case "data\\npcgifttastes": // Game1.LoadContent
Game1.NPCGiftTastes = content.Load>(key);
return true;
case "data\\objectcontexttags": // Game1.LoadContent
Game1.objectContextTags = content.Load>(key);
return true;
case "data\\objectinformation": // Game1.LoadContent
Game1.objectInformation = content.Load>(key);
return true;
/****
** Content\Fonts
****/
case "fonts\\spritefont1": // Game1.LoadContent
Game1.dialogueFont = content.Load(key);
return true;
case "fonts\\smallfont": // Game1.LoadContent
Game1.smallFont = content.Load(key);
return true;
case "fonts\\tinyfont": // Game1.LoadContent
Game1.tinyFont = content.Load(key);
return true;
case "fonts\\tinyfontborder": // Game1.LoadContent
Game1.tinyFontBorder = content.Load(key);
return true;
/****
** Content\LooseSprites\Lighting
****/
case "loosesprites\\lighting\\greenlight": // Game1.LoadContent
Game1.cauldronLight = content.Load(key);
return true;
case "loosesprites\\lighting\\indoorwindowlight": // Game1.LoadContent
Game1.indoorWindowLight = content.Load(key);
return true;
case "loosesprites\\lighting\\lantern": // Game1.LoadContent
Game1.lantern = content.Load(key);
return true;
case "loosesprites\\lighting\\sconcelight": // Game1.LoadContent
Game1.sconceLight = content.Load(key);
return true;
case "loosesprites\\lighting\\windowlight": // Game1.LoadContent
Game1.windowLight = content.Load(key);
return true;
/****
** Content\LooseSprites
****/
case "loosesprites\\birds": // Game1.LoadContent
Game1.birdsSpriteSheet = content.Load(key);
return true;
case "loosesprites\\concessions": // Game1.LoadContent
Game1.concessionsSpriteSheet = content.Load(key);
return true;
case "loosesprites\\controllermaps": // Game1.LoadContent
Game1.controllerMaps = content.Load(key);
return true;
case "loosesprites\\cursors": // Game1.LoadContent
Game1.mouseCursors = content.Load(key);
foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType())
{
foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton })
button.texture = Game1.mouseCursors;
}
if (!ignoreWorld)
this.ReloadDoorSprites(content, key);
return true;
case "loosesprites\\cursors2": // Game1.LoadContent
Game1.mouseCursors2 = content.Load(key);
return true;
case "loosesprites\\daybg": // Game1.LoadContent
Game1.daybg = content.Load(key);
return true;
case "loosesprites\\font_bold": // Game1.LoadContent
SpriteText.spriteTexture = content.Load(key);
return true;
case "loosesprites\\font_colored": // Game1.LoadContent
SpriteText.coloredTexture = content.Load(key);
return true;
case "loosesprites\\giftbox": // Game1.LoadContent
Game1.giftboxTexture = content.Load(key);
return true;
case "loosesprites\\nightbg": // Game1.LoadContent
Game1.nightbg = content.Load(key);
return true;
case "loosesprites\\shadow": // Game1.LoadContent
Game1.shadowTexture = content.Load(key);
return true;
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
return !ignoreWorld && this.ReloadSuspensionBridges(content, key);
/****
** Content\Maps
****/
case "maps\\menutiles": // Game1.LoadContent
Game1.menuTexture = content.Load(key);
return true;
case "maps\\menutilesuncolored": // Game1.LoadContent
Game1.uncoloredMenuTexture = content.Load(key);
return true;
case "maps\\springobjects": // Game1.LoadContent
Game1.objectSpriteSheet = content.Load(key);
return true;
/****
** Content\Minigames
****/
case "minigames\\clouds": // TitleMenu
{
if (Game1.activeClickableMenu is TitleMenu titleMenu)
{
titleMenu.cloudsTexture = content.Load(key);
return true;
}
}
return false;
case "minigames\\titlebuttons": // TitleMenu
return this.ReloadTitleButtons(content, key);
/****
** Content\Strings
****/
case "strings\\stringsfromcsfiles":
return this.ReloadStringsFromCsFiles(content);
/****
** Content\TileSheets
****/
case "tilesheets\\animations": // Game1.LoadContent
Game1.animations = content.Load(key);
return true;
case "tilesheets\\buffsicons": // Game1.LoadContent
Game1.buffsIcons = content.Load(key);
return true;
case "tilesheets\\bushes": // new Bush()
Bush.texture = new Lazy(() => content.Load(key));
return true;
case "tilesheets\\chairtiles": // Game1.LoadContent
return this.ReloadChairTiles(content, key, ignoreWorld);
case "tilesheets\\craftables": // Game1.LoadContent
Game1.bigCraftableSpriteSheet = content.Load(key);
return true;
case "tilesheets\\critters": // Critter constructor
return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0;
case "tilesheets\\crops": // Game1.LoadContent
Game1.cropSpriteSheet = content.Load(key);
return true;
case "tilesheets\\debris": // Game1.LoadContent
Game1.debrisSpriteSheet = content.Load(key);
return true;
case "tilesheets\\emotes": // Game1.LoadContent
Game1.emoteSpriteSheet = content.Load(key);
return true;
case "tilesheets\\fruittrees": // FruitTree
FruitTree.texture = content.Load(key);
return true;
case "tilesheets\\furniture": // Game1.LoadContent
Furniture.furnitureTexture = content.Load(key);
return true;
case "tilesheets\\furniturefront": // Game1.LoadContent
Furniture.furnitureFrontTexture = content.Load(key);
return true;
case "tilesheets\\projectiles": // Game1.LoadContent
Projectile.projectileSheet = content.Load(key);
return true;
case "tilesheets\\rain": // Game1.LoadContent
Game1.rainTexture = content.Load(key);
return true;
case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
Game1.ResetToolSpriteSheet();
return true;
case "tilesheets\\weapons": // Game1.LoadContent
Tool.weaponsTexture = content.Load(key);
return true;
/****
** Content\TerrainFeatures
****/
case "terrainfeatures\\flooring": // from Flooring
Flooring.floorsTexture = content.Load(key);
return true;
case "terrainfeatures\\flooring_winter": // from Flooring
Flooring.floorsTextureWinter = content.Load(key);
return true;
case "terrainfeatures\\grass": // from Grass
return !ignoreWorld && this.ReloadGrassTextures(content, key);
case "terrainfeatures\\hoedirt": // from HoeDirt
HoeDirt.lightTexture = content.Load(key);
return true;
case "terrainfeatures\\hoedirtdark": // from HoeDirt
HoeDirt.darkTexture = content.Load(key);
return true;
case "terrainfeatures\\hoedirtsnow": // from HoeDirt
HoeDirt.snowTexture = content.Load(key);
return true;
case "terrainfeatures\\mushroom_tree": // from Tree
return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree);
case "terrainfeatures\\tree_palm": // from Tree
return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.palmTree);
case "terrainfeatures\\tree1_fall": // from Tree
case "terrainfeatures\\tree1_spring": // from Tree
case "terrainfeatures\\tree1_summer": // from Tree
case "terrainfeatures\\tree1_winter": // from Tree
return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.bushyTree);
case "terrainfeatures\\tree2_fall": // from Tree
case "terrainfeatures\\tree2_spring": // from Tree
case "terrainfeatures\\tree2_summer": // from Tree
case "terrainfeatures\\tree2_winter": // from Tree
return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.leafyTree);
case "terrainfeatures\\tree3_fall": // from Tree
case "terrainfeatures\\tree3_spring": // from Tree
case "terrainfeatures\\tree3_winter": // from Tree
return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.pineTree);
}
/****
** Dynamic assets
****/
if (!ignoreWorld)
{
// dynamic textures
if (this.KeyStartsWith(key, "animals\\cat"))
return this.ReloadPetOrHorseSprites(content, key);
if (this.KeyStartsWith(key, "animals\\dog"))
return this.ReloadPetOrHorseSprites(content, key);
if (this.IsInFolder(key, "Animals"))
return this.ReloadFarmAnimalSprites(content, key);
if (this.IsInFolder(key, "Buildings"))
return this.ReloadBuildings(key);
if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
return this.ReloadFenceTextures(key);
// dynamic data
if (this.IsInFolder(key, "Characters\\Dialogue"))
return this.ReloadNpcDialogue(key);
if (this.IsInFolder(key, "Characters\\schedules"))
return this.ReloadNpcSchedules(key);
}
return false;
}
/*********
** Private methods
*********/
/****
** Reload texture methods
****/
/// Reload buttons on the title screen.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
/// Derived from the constructor and .
private bool ReloadTitleButtons(LocalizedContentManager content, string key)
{
if (Game1.activeClickableMenu is TitleMenu titleMenu)
{
Texture2D texture = content.Load(key);
titleMenu.titleButtonsTexture = texture;
titleMenu.backButton.texture = texture;
titleMenu.aboutButton.texture = texture;
titleMenu.languageButton.texture = texture;
foreach (ClickableTextureComponent button in titleMenu.buttons)
button.texture = texture;
foreach (TemporaryAnimatedSprite bird in titleMenu.birds)
bird.texture = texture;
return true;
}
return false;
}
/// Reload the sprites for matching pets or horses.
/// The animal type.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
private bool ReloadPetOrHorseSprites(LocalizedContentManager content, string key)
where TAnimal : NPC
{
// find matches
TAnimal[] animals = this.GetCharacters()
.OfType()
.Where(p => key == this.NormalizeAssetNameIgnoringEmpty(p.Sprite?.Texture?.Name))
.ToArray();
if (!animals.Any())
return false;
// update sprites
Texture2D texture = content.Load(key);
foreach (TAnimal animal in animals)
animal.Sprite.spriteTexture = texture;
return true;
}
/// Reload the sprites for matching farm animals.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
/// Derived from .
private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key)
{
// find matches
FarmAnimal[] animals = this.GetFarmAnimals().ToArray();
if (!animals.Any())
return false;
// update sprites
Lazy texture = new Lazy(() => content.Load(key));
foreach (FarmAnimal animal in animals)
{
// get expected key
string expectedKey = animal.age.Value < animal.ageWhenMature.Value
? $"Baby{(animal.type.Value == "Duck" ? "White Chicken" : animal.type.Value)}"
: animal.type.Value;
if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0)
expectedKey = $"Sheared{expectedKey}";
expectedKey = $"Animals\\{expectedKey}";
// reload asset
if (expectedKey == key)
animal.Sprite.spriteTexture = texture.Value;
}
return texture.IsValueCreated;
}
/// Reload building textures.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
private bool ReloadBuildings(string key)
{
// get paint mask info
const string paintMaskSuffix = "_PaintMask";
bool isPaintMask = key.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase);
// get building type
string type = Path.GetFileName(key);
if (isPaintMask)
type = type.Substring(0, type.Length - paintMaskSuffix.Length);
// get buildings
Building[] buildings = this.GetLocations(buildingInteriors: false)
.OfType()
.SelectMany(p => p.buildings)
.Where(p => p.buildingType.Value == type)
.ToArray();
// remove from paint mask cache
bool removedFromCache = this.RemoveFromPaintMaskCache(key);
// reload textures
if (buildings.Any())
{
foreach (Building building in buildings)
building.resetTexture();
return true;
}
return removedFromCache;
}
/// Reload map seat textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world.
/// Returns whether any textures were reloaded.
private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load(key);
if (!ignoreWorld)
{
foreach (var location in this.GetLocations())
{
foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
{
string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile);
if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase))
seat.overlayTexture = MapSeat.mapChairTexture;
}
}
}
return true;
}
/// Reload critter textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns the number of reloaded assets.
private int ReloadCritterTextures(LocalizedContentManager content, string key)
{
// get critters
Critter[] critters =
(
from location in this.GetLocations()
where location.critters != null
from Critter critter in location.critters
where this.NormalizeAssetNameIgnoringEmpty(critter.sprite?.Texture?.Name) == key
select critter
)
.ToArray();
if (!critters.Any())
return 0;
// update sprites
Texture2D texture = content.Load(key);
foreach (var entry in critters)
entry.sprite.spriteTexture = texture;
return critters.Length;
}
/// Reload the sprites for interior doors.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any doors were affected.
private bool ReloadDoorSprites(LocalizedContentManager content, string key)
{
Lazy texture = new Lazy(() => content.Load(key));
foreach (GameLocation location in this.GetLocations())
{
IEnumerable doors = location.interiorDoors?.Doors;
if (doors == null)
continue;
foreach (InteriorDoor door in doors)
{
if (door?.Sprite == null)
continue;
string textureName = this.NormalizeAssetNameIgnoringEmpty(this.Reflection.GetField(door.Sprite, "textureName").GetValue());
if (textureName != key)
continue;
door.Sprite.texture = texture.Value;
}
}
return texture.IsValueCreated;
}
/// Reload the data for matching farm animals.
/// Returns whether any farm animals were affected.
/// Derived from the constructor.
private bool ReloadFarmAnimalData()
{
bool changed = false;
foreach (FarmAnimal animal in this.GetFarmAnimals())
{
animal.reloadData();
changed = true;
}
return changed;
}
/// Reload the sprites for a fence type.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
private bool ReloadFenceTextures(string key)
{
// get fence type
if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType))
return false;
// get fences
Fence[] fences =
(
from location in this.GetLocations()
from fence in location.Objects.Values.OfType()
where
fence.whichType.Value == fenceType
|| (fence.isGate.Value && fenceType == 1) // gates are hardcoded to draw fence type 1
select fence
)
.ToArray();
// update fence textures
foreach (Fence fence in fences)
fence.fenceTexture = new Lazy(fence.loadFenceTexture);
return true;
}
/// Reload tree textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
private bool ReloadGrassTextures(LocalizedContentManager content, string key)
{
Grass[] grasses =
(
from location in this.GetLocations()
from grass in location.terrainFeatures.Values.OfType()
where this.NormalizeAssetNameIgnoringEmpty(grass.textureName()) == key
select grass
)
.ToArray();
if (grasses.Any())
{
Lazy texture = new Lazy(() => content.Load(key));
foreach (Grass grass in grasses)
grass.texture = texture;
return true;
}
return false;
}
/// Reload hair style metadata.
/// Returns whether any assets were reloaded.
/// Derived from the and .
private bool ReloadHairData()
{
if (Farmer.hairStyleMetadataFile == null)
return false;
Farmer.hairStyleMetadataFile = null;
Farmer.allHairStyleIndices = null;
Farmer.hairStyleMetadata.Clear();
return true;
}
/// Reload the map for a location.
/// The location whose map to reload.
private void ReloadMap(LocationInfo locationInfo)
{
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
if (this.AggressiveMemoryOptimizations)
location.map.DisposeTileSheets(Game1.mapDisplayDevice);
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
// reload interior doors
location.interiorDoors.Clear();
location.interiorDoors.ResetSharedState(); // load doors from map properties
location.interiorDoors.ResetLocalState(); // reapply door tiles
// reapply map changes (after reloading doors so they apply theirs too)
location.MakeMapModifications(force: true);
// update for changes
location.updateWarps();
location.updateDoors();
locationInfo.ParentBuilding?.updateInteriorWarps();
// reset player position
// The game may move the player as part of the map changes, even if they're not in that
// location. That's not needed in this case, and it can have weird effects like players
// warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse
// map on location change.
if (playerPos.HasValue)
Game1.player.Position = playerPos.Value;
}
/// Reload the disposition data for matching NPCs.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any NPCs were affected.
private bool ReloadNpcDispositions(LocalizedContentManager content, string key)
{
IDictionary data = content.Load>(key);
bool changed = false;
foreach (NPC npc in this.GetCharacters())
{
if (npc.isVillager() && data.ContainsKey(npc.Name))
{
npc.reloadData();
changed = true;
}
}
return changed;
}
/// Reload the sprites for matching NPCs.
/// The asset keys to reload.
/// The asset keys which have been propagated.
private void ReloadNpcSprites(IEnumerable keys, IDictionary propagated)
{
// get NPCs
HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase);
var characters =
(
from npc in this.GetCharacters()
let key = this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name)
where key != null && lookup.Contains(key)
select new { Npc = npc, Key = key }
)
.ToArray();
if (!characters.Any())
return;
// update sprite
foreach (var target in characters)
{
target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.Key);
propagated[target.Key] = true;
}
}
/// Reload the portraits for matching NPCs.
/// The asset key to reload.
/// The asset keys which have been propagated.
private void ReloadNpcPortraits(IEnumerable keys, IDictionary propagated)
{
// get NPCs
HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase);
var characters =
(
from npc in this.GetCharacters()
where npc.isVillager()
let key = this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name)
where key != null && lookup.Contains(key)
select new { Npc = npc, Key = key }
)
.ToList();
// special case: Gil is a private NPC field on the AdventureGuild class (only used for the portrait)
{
string gilKey = this.NormalizeAssetNameIgnoringEmpty("Portraits/Gil");
if (lookup.Contains(gilKey))
{
GameLocation adventureGuild = Game1.getLocationFromName("AdventureGuild");
if (adventureGuild != null)
characters.Add(new { Npc = this.Reflection.GetField(adventureGuild, "Gil").GetValue(), Key = gilKey });
}
}
// update portrait
foreach (var target in characters)
{
target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.Key);
propagated[target.Key] = true;
}
}
/// Reload the sprites for matching players.
/// The asset key to reload.
private bool ReloadPlayerSprites(string key)
{
Farmer[] players =
(
from player in Game1.getOnlineFarmers()
where key == this.NormalizeAssetNameIgnoringEmpty(player.getTexture())
select player
)
.ToArray();
foreach (Farmer player in players)
{
this.Reflection.GetField>>>(typeof(FarmerRenderer), "_recolorOffsets").GetValue().Remove(player.getTexture());
player.FarmerRenderer.MarkSpriteDirty();
}
return players.Any();
}
/// Reload suspension bridge textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// Returns whether any textures were reloaded.
private bool ReloadSuspensionBridges(LocalizedContentManager content, string key)
{
Lazy texture = new Lazy(() => content.Load(key));
foreach (GameLocation location in this.GetLocations(buildingInteriors: false))
{
// get suspension bridges field
var field = this.Reflection.GetField>(location, nameof(IslandNorth.suspensionBridges), required: false);
if (field == null || !typeof(IEnumerable).IsAssignableFrom(field.FieldInfo.FieldType))
continue;
// update textures
foreach (SuspensionBridge bridge in field.GetValue())
this.Reflection.GetField(bridge, "_texture").SetValue(texture.Value);
}
return texture.IsValueCreated;
}
/// Reload tree textures.
/// The content manager through which to reload the asset.
/// The asset key to reload.
/// The type to reload.
/// Returns whether any textures were reloaded.
private bool ReloadTreeTextures(LocalizedContentManager content, string key, int type)
{
Tree[] trees = this.GetLocations()
.SelectMany(p => p.terrainFeatures.Values.OfType())
.Where(tree => tree.treeType.Value == type)
.ToArray();
if (trees.Any())
{
Lazy texture = new Lazy(() => content.Load(key));
foreach (Tree tree in trees)
tree.texture = texture;
return true;
}
return false;
}
/****
** Reload data methods
****/
/// Reload the dialogue data for matching NPCs.
/// The asset key to reload.
/// Returns whether any assets were reloaded.
private bool ReloadNpcDialogue(string key)
{
// get NPCs
string name = Path.GetFileName(key);
NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
if (!villagers.Any())
return false;
// update dialogue
// Note that marriage dialogue isn't reloaded after reset, but it doesn't need to be
// propagated anyway since marriage dialogue keys can't be added/removed and the field
// doesn't store the text itself.
foreach (NPC villager in villagers)
{
bool shouldSayMarriageDialogue = villager.shouldSayMarriageDialogue.Value;
MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray();
villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
villager.resetCurrentDialogue();
villager.shouldSayMarriageDialogue.Set(shouldSayMarriageDialogue);
villager.currentMarriageDialogue.Set(marriageDialogue);
}
return true;
}
/// Reload the schedules for matching NPCs.
/// The asset key to reload.
/// Returns whether any assets were reloaded.
private bool ReloadNpcSchedules(string key)
{
// get NPCs
string name = Path.GetFileName(key);
NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray();
if (!villagers.Any())
return false;
// update schedule
foreach (NPC villager in villagers)
{
// reload schedule
this.Reflection.GetField(villager, "_hasLoadedMasterScheduleData").SetValue(false);
this.Reflection.GetField>(villager, "_masterScheduleData").SetValue(null);
villager.Schedule = villager.getSchedule(Game1.dayOfMonth);
// switch to new schedule if needed
if (villager.Schedule != null)
{
int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault();
if (lastScheduleTime != 0)
{
villager.queuedSchedulePaths.Clear();
villager.lastAttemptedSchedule = 0;
villager.checkSchedule(lastScheduleTime);
}
}
}
return true;
}
/// Reload cached translations from the Strings\StringsFromCSFiles asset.
/// The content manager through which to reload the asset.
/// Returns whether any data was reloaded.
/// Derived from the .
private bool ReloadStringsFromCsFiles(LocalizedContentManager content)
{
Game1.samBandName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2156");
Game1.elliottBookName = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.2157");
string[] dayNames = this.Reflection.GetField(typeof(Game1), "_shortDayDisplayName").GetValue();
dayNames[0] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3042");
dayNames[1] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3043");
dayNames[2] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3044");
dayNames[3] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3045");
dayNames[4] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3046");
dayNames[5] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3047");
dayNames[6] = content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3048");
return true;
}
/****
** Helpers
****/
/// Get all NPCs in the game (excluding farm animals).
private IEnumerable GetCharacters()
{
foreach (NPC character in this.GetLocations().SelectMany(p => p.characters))
yield return character;
if (Game1.CurrentEvent?.actors != null)
{
foreach (NPC character in Game1.CurrentEvent.actors)
yield return character;
}
}
/// Get all farm animals in the game.
private IEnumerable GetFarmAnimals()
{
foreach (GameLocation location in this.GetLocations())
{
if (location is Farm farm)
{
foreach (FarmAnimal animal in farm.animals.Values)
yield return animal;
}
else if (location is AnimalHouse animalHouse)
foreach (FarmAnimal animal in animalHouse.animals.Values)
yield return animal;
}
}
/// Get all locations in the game.
/// Whether to also get the interior locations for constructable buildings.
private IEnumerable GetLocations(bool buildingInteriors = true)
{
return this.GetLocationsWithInfo(buildingInteriors).Select(info => info.Location);
}
/// Get all locations in the game.
/// Whether to also get the interior locations for constructable buildings.
private IEnumerable GetLocationsWithInfo(bool buildingInteriors = true)
{
// get available root locations
IEnumerable rootLocations = Game1.locations;
if (SaveGame.loaded?.locations != null)
rootLocations = rootLocations.Concat(SaveGame.loaded.locations);
// yield root + child locations
foreach (GameLocation location in rootLocations)
{
yield return new LocationInfo(location, null);
if (buildingInteriors && location is BuildableGameLocation buildableLocation)
{
foreach (Building building in buildableLocation.buildings)
{
GameLocation indoors = building.indoors.Value;
if (indoors != null)
yield return new LocationInfo(indoors, building);
}
}
}
}
/// Normalize an asset key to match the cache key and assert that it's valid, but don't raise an error for null or empty values.
/// The asset key to normalize.
private string NormalizeAssetNameIgnoringEmpty(string path)
{
if (string.IsNullOrWhiteSpace(path))
return null;
return this.AssertAndNormalizeAssetName(path);
}
/// Get whether a key starts with a substring after the substring is normalized.
/// The key to check.
/// The substring to normalize and find.
private bool KeyStartsWith(string key, string rawSubstring)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(rawSubstring))
return false;
return key.StartsWith(this.NormalizeAssetNameIgnoringEmpty(rawSubstring), StringComparison.OrdinalIgnoreCase);
}
/// Get whether a normalized asset key is in the given folder.
/// The normalized asset key (like Animals/cat).
/// The key folder (like Animals); doesn't need to be normalized.
/// Whether to return true if the key is inside a subfolder of the .
private bool IsInFolder(string key, string folder, bool allowSubfolders = false)
{
return
this.KeyStartsWith(key, $"{folder}\\")
&& (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1);
}
/// Get the segments in a path (e.g. 'a/b' is 'a' and 'b').
/// The path to check.
private string[] GetSegments(string path)
{
return path != null
? PathUtilities.GetSegments(path)
: new string[0];
}
/// Count the number of segments in a path (e.g. 'a/b' is 2).
/// The path to check.
private int CountSegments(string path)
{
return this.GetSegments(path).Length;
}
/// Load a texture, and dispose the old one if is enabled and it's different from the new instance.
/// The previous texture to dispose.
/// The asset key to load.
private Texture2D LoadAndDisposeIfNeeded(Texture2D oldTexture, string key)
{
// if aggressive memory optimizations are enabled, load the asset from the disposable
// content manager and dispose the old instance if needed.
if (this.AggressiveMemoryOptimizations)
{
GameContentManagerForAssetPropagation content = this.DisposableContentManager;
Texture2D newTexture = content.Load(key);
if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsResponsibleFor(oldTexture))
oldTexture.Dispose();
return newTexture;
}
// else just (re)load it from the main content manager
return this.MainContentManager.Load(key);
}
/// Remove a case-insensitive key from the paint mask cache.
/// The paint mask asset key.
private bool RemoveFromPaintMaskCache(string key)
{
// make cache case-insensitive
// This is needed for cache invalidation since mods may specify keys with a different capitalization
if (!object.ReferenceEquals(BuildingPainter.paintMaskLookup.Comparer, StringComparer.OrdinalIgnoreCase))
BuildingPainter.paintMaskLookup = new Dictionary>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase);
// remove key from cache
return BuildingPainter.paintMaskLookup.Remove(key);
}
/// Metadata about a location used in asset propagation.
private readonly struct LocationInfo
{
/*********
** Accessors
*********/
/// The location instance.
public GameLocation Location { get; }
/// The building which contains the location, if any.
public Building ParentBuilding { get; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The location instance.
/// The building which contains the location, if any.
public LocationInfo(GameLocation location, Building parentBuilding)
{
this.Location = location;
this.ParentBuilding = parentBuilding;
}
}
}
}