From 4da9e954df3846d01aa0536f4e8143466a1d62f3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 11 Feb 2022 00:49:49 -0500 Subject: use Array.Empty to avoid unneeded array allocations --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Metadata') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 552bc000..73c212a4 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -1275,7 +1275,7 @@ namespace StardewModdingAPI.Metadata { return path != null ? PathUtilities.GetSegments(path) - : new string[0]; + : Array.Empty(); } /// Count the number of segments in a path (e.g. 'a/b' is 2). -- cgit From f53ace623523ea1c304f59b187c478634481cbd2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 13 Feb 2022 00:41:44 -0500 Subject: simplify and standardize key comparison in asset propagator --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 37 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) (limited to 'src/SMAPI/Metadata') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 73c212a4..9089a1e9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -170,7 +170,7 @@ namespace StardewModdingAPI.Metadata { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { - if (this.NormalizeAssetNameIgnoringEmpty(tilesheet.ImageSource) == key) + if (this.IsSameAssetKey(tilesheet.ImageSource, key)) Game1.mapDisplayDevice.LoadTileSheet(tilesheet); } } @@ -188,7 +188,7 @@ namespace StardewModdingAPI.Metadata { GameLocation location = info.Location; - if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key) + if (this.IsSameAssetKey(location.mapPath.Value, key)) { static ISet GetWarpSet(GameLocation location) { @@ -653,7 +653,7 @@ namespace StardewModdingAPI.Metadata // find matches TAnimal[] animals = this.GetCharacters() .OfType() - .Where(p => key == this.NormalizeAssetNameIgnoringEmpty(p.Sprite?.Texture?.Name)) + .Where(p => this.IsSameAssetKey(p.Sprite?.Texture?.Name, key)) .ToArray(); if (!animals.Any()) return false; @@ -690,7 +690,7 @@ namespace StardewModdingAPI.Metadata expectedKey = $"Animals\\{expectedKey}"; // reload asset - if (expectedKey == key) + if (this.IsSameAssetKey(expectedKey, key)) animal.Sprite.spriteTexture = texture.Value; } return texture.IsValueCreated; @@ -747,9 +747,7 @@ namespace StardewModdingAPI.Metadata { foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) { - string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile); - - if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase)) + if (this.IsSameAssetKey(seat._loadedTextureFile, key)) seat.overlayTexture = MapSeat.mapChairTexture; } } @@ -770,7 +768,7 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() where location.critters != null from Critter critter in location.critters - where this.NormalizeAssetNameIgnoringEmpty(critter.sprite?.Texture?.Name) == key + where this.IsSameAssetKey(critter.sprite?.Texture?.Name, key) select critter ) .ToArray(); @@ -804,11 +802,9 @@ namespace StardewModdingAPI.Metadata 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; + string curKey = this.Reflection.GetField(door.Sprite, "textureName").GetValue(); + if (this.IsSameAssetKey(curKey, key)) + door.Sprite.texture = texture.Value; } } @@ -867,7 +863,7 @@ namespace StardewModdingAPI.Metadata ( from location in this.GetLocations() from grass in location.terrainFeatures.Values.OfType() - where this.NormalizeAssetNameIgnoringEmpty(grass.textureName()) == key + where this.IsSameAssetKey(grass.textureName(), key) select grass ) .ToArray(); @@ -1024,7 +1020,7 @@ namespace StardewModdingAPI.Metadata Farmer[] players = ( from player in Game1.getOnlineFarmers() - where key == this.NormalizeAssetNameIgnoringEmpty(player.getTexture()) + where this.IsSameAssetKey(player.getTexture(), key) select player ) .ToArray(); @@ -1247,6 +1243,17 @@ namespace StardewModdingAPI.Metadata return this.AssertAndNormalizeAssetName(path); } + /// Get whether a given asset key is equivalent to a normalized asset key, ignoring unimportant differences like capitalization and formatting. + /// The actual key to check. + /// The key to match, already normalized via or . + private bool IsSameAssetKey(string actualKey, string normalizedKey) + { + if (actualKey is null || normalizedKey is null) + return false; + + return normalizedKey.Equals(PathUtilities.NormalizeAssetName(actualKey), StringComparison.OrdinalIgnoreCase); + } + /// Get whether a key starts with a substring after the substring is normalized. /// The key to check. /// The substring to normalize and find. -- cgit From 315926de25dc41c97a9a2351c92d5983a2e8df3e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 13 Feb 2022 00:43:35 -0500 Subject: flip slashes in asset propagator to match MonoGame The game now uses MonoGame on all platforms. --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 214 +++++++++++++++--------------- 1 file changed, 107 insertions(+), 107 deletions(-) (limited to 'src/SMAPI/Metadata') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 9089a1e9..a6c4bb24 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -92,7 +92,7 @@ namespace StardewModdingAPI.Metadata // group into optimized lists var buckets = assets.GroupBy(p => { - if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters")) + if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters/Monsters")) return AssetBucket.Sprite; if (this.IsInFolder(p.Key, "Portraits")) @@ -214,22 +214,22 @@ namespace StardewModdingAPI.Metadata ** Propagate by key ****/ Reflector reflection = this.Reflection; - switch (key.ToLower().Replace("/", "\\")) // normalized key so we can compare statically + switch (key.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { /**** ** Animals ****/ - case "animals\\horse": + case "animals/horse": return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key); /**** ** Buildings ****/ - case "buildings\\houses": // Farm + case "buildings/houses": // Farm Farm.houseTextures = this.LoadAndDisposeIfNeeded(Farm.houseTextures, key); return true; - case "buildings\\houses_paintmask": // Farm + case "buildings/houses_paintmask": // Farm { bool removedFromCache = this.RemoveFromPaintMaskCache(key); @@ -242,149 +242,149 @@ namespace StardewModdingAPI.Metadata /**** ** Content\Characters\Farmer ****/ - case "characters\\farmer\\accessories": // Game1.LoadContent + 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": + 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 + case "characters/farmer/hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); return true; - case "characters\\farmer\\hats": // Game1.LoadContent + case "characters/farmer/hats": // Game1.LoadContent FarmerRenderer.hatsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hatsTexture, key); return true; - case "characters\\farmer\\pants": // Game1.LoadContent + case "characters/farmer/pants": // Game1.LoadContent FarmerRenderer.pantsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.pantsTexture, key); return true; - case "characters\\farmer\\shirts": // Game1.LoadContent + case "characters/farmer/shirts": // Game1.LoadContent FarmerRenderer.shirtsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.shirtsTexture, key); return true; /**** ** Content\Data ****/ - case "data\\achievements": // Game1.LoadContent + case "data/achievements": // Game1.LoadContent Game1.achievements = content.Load>(key); return true; - case "data\\bigcraftablesinformation": // Game1.LoadContent + case "data/bigcraftablesinformation": // Game1.LoadContent Game1.bigCraftablesInformation = content.Load>(key); return true; - case "data\\clothinginformation": // Game1.LoadContent + case "data/clothinginformation": // Game1.LoadContent Game1.clothingInformation = content.Load>(key); return true; - case "data\\concessions": // MovieTheater.GetConcessions + case "data/concessions": // MovieTheater.GetConcessions MovieTheater.ClearCachedLocalizedData(); return true; - case "data\\concessiontastes": // MovieTheater.GetConcessionTasteForCharacter + case "data/concessiontastes": // MovieTheater.GetConcessionTasteForCharacter this.Reflection .GetField>(typeof(MovieTheater), "_concessionTastes") .SetValue(content.Load>(key)); return true; - case "data\\cookingrecipes": // CraftingRecipe.InitShared + case "data/cookingrecipes": // CraftingRecipe.InitShared CraftingRecipe.cookingRecipes = content.Load>(key); return true; - case "data\\craftingrecipes": // CraftingRecipe.InitShared + case "data/craftingrecipes": // CraftingRecipe.InitShared CraftingRecipe.craftingRecipes = content.Load>(key); return true; - case "data\\farmanimals": // FarmAnimal constructor + case "data/farmanimals": // FarmAnimal constructor return !ignoreWorld && this.ReloadFarmAnimalData(); - case "data\\hairdata": // Farmer.GetHairStyleMetadataFile + case "data/hairdata": // Farmer.GetHairStyleMetadataFile return this.ReloadHairData(); - case "data\\movies": // MovieTheater.GetMovieData - case "data\\moviesreactions": // MovieTheater.GetMovieReactions + case "data/movies": // MovieTheater.GetMovieData + case "data/moviesreactions": // MovieTheater.GetMovieReactions MovieTheater.ClearCachedLocalizedData(); return true; - case "data\\npcdispositions": // NPC constructor + case "data/npcdispositions": // NPC constructor return !ignoreWorld && this.ReloadNpcDispositions(content, key); - case "data\\npcgifttastes": // Game1.LoadContent + case "data/npcgifttastes": // Game1.LoadContent Game1.NPCGiftTastes = content.Load>(key); return true; - case "data\\objectcontexttags": // Game1.LoadContent + case "data/objectcontexttags": // Game1.LoadContent Game1.objectContextTags = content.Load>(key); return true; - case "data\\objectinformation": // Game1.LoadContent + case "data/objectinformation": // Game1.LoadContent Game1.objectInformation = content.Load>(key); return true; /**** ** Content\Fonts ****/ - case "fonts\\spritefont1": // Game1.LoadContent + case "fonts/spritefont1": // Game1.LoadContent Game1.dialogueFont = content.Load(key); return true; - case "fonts\\smallfont": // Game1.LoadContent + case "fonts/smallfont": // Game1.LoadContent Game1.smallFont = content.Load(key); return true; - case "fonts\\tinyfont": // Game1.LoadContent + case "fonts/tinyfont": // Game1.LoadContent Game1.tinyFont = content.Load(key); return true; - case "fonts\\tinyfontborder": // Game1.LoadContent + case "fonts/tinyfontborder": // Game1.LoadContent Game1.tinyFontBorder = content.Load(key); return true; /**** ** Content\LooseSprites\Lighting ****/ - case "loosesprites\\lighting\\greenlight": // Game1.LoadContent + case "loosesprites/lighting/greenlight": // Game1.LoadContent Game1.cauldronLight = content.Load(key); return true; - case "loosesprites\\lighting\\indoorwindowlight": // Game1.LoadContent + case "loosesprites/lighting/indoorwindowlight": // Game1.LoadContent Game1.indoorWindowLight = content.Load(key); return true; - case "loosesprites\\lighting\\lantern": // Game1.LoadContent + case "loosesprites/lighting/lantern": // Game1.LoadContent Game1.lantern = content.Load(key); return true; - case "loosesprites\\lighting\\sconcelight": // Game1.LoadContent + case "loosesprites/lighting/sconcelight": // Game1.LoadContent Game1.sconceLight = content.Load(key); return true; - case "loosesprites\\lighting\\windowlight": // Game1.LoadContent + case "loosesprites/lighting/windowlight": // Game1.LoadContent Game1.windowLight = content.Load(key); return true; /**** ** Content\LooseSprites ****/ - case "loosesprites\\birds": // Game1.LoadContent + case "loosesprites/birds": // Game1.LoadContent Game1.birdsSpriteSheet = content.Load(key); return true; - case "loosesprites\\concessions": // Game1.LoadContent + case "loosesprites/concessions": // Game1.LoadContent Game1.concessionsSpriteSheet = content.Load(key); return true; - case "loosesprites\\controllermaps": // Game1.LoadContent + case "loosesprites/controllermaps": // Game1.LoadContent Game1.controllerMaps = content.Load(key); return true; - case "loosesprites\\cursors": // Game1.LoadContent + case "loosesprites/cursors": // Game1.LoadContent Game1.mouseCursors = content.Load(key); foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType()) { @@ -396,56 +396,56 @@ namespace StardewModdingAPI.Metadata this.ReloadDoorSprites(content, key); return true; - case "loosesprites\\cursors2": // Game1.LoadContent + case "loosesprites/cursors2": // Game1.LoadContent Game1.mouseCursors2 = content.Load(key); return true; - case "loosesprites\\daybg": // Game1.LoadContent + case "loosesprites/daybg": // Game1.LoadContent Game1.daybg = content.Load(key); return true; - case "loosesprites\\font_bold": // Game1.LoadContent + case "loosesprites/font_bold": // Game1.LoadContent SpriteText.spriteTexture = content.Load(key); return true; - case "loosesprites\\font_colored": // Game1.LoadContent + case "loosesprites/font_colored": // Game1.LoadContent SpriteText.coloredTexture = content.Load(key); return true; - case "loosesprites\\giftbox": // Game1.LoadContent + case "loosesprites/giftbox": // Game1.LoadContent Game1.giftboxTexture = content.Load(key); return true; - case "loosesprites\\nightbg": // Game1.LoadContent + case "loosesprites/nightbg": // Game1.LoadContent Game1.nightbg = content.Load(key); return true; - case "loosesprites\\shadow": // Game1.LoadContent + case "loosesprites/shadow": // Game1.LoadContent Game1.shadowTexture = content.Load(key); return true; - case "loosesprites\\suspensionbridge": // SuspensionBridge constructor + case "loosesprites/suspensionbridge": // SuspensionBridge constructor return !ignoreWorld && this.ReloadSuspensionBridges(content, key); /**** ** Content\Maps ****/ - case "maps\\menutiles": // Game1.LoadContent + case "maps/menutiles": // Game1.LoadContent Game1.menuTexture = content.Load(key); return true; - case "maps\\menutilesuncolored": // Game1.LoadContent + case "maps/menutilesuncolored": // Game1.LoadContent Game1.uncoloredMenuTexture = content.Load(key); return true; - case "maps\\springobjects": // Game1.LoadContent + case "maps/springobjects": // Game1.LoadContent Game1.objectSpriteSheet = content.Load(key); return true; /**** ** Content\Minigames ****/ - case "minigames\\clouds": // TitleMenu + case "minigames/clouds": // TitleMenu { if (Game1.activeClickableMenu is TitleMenu titleMenu) { @@ -455,127 +455,127 @@ namespace StardewModdingAPI.Metadata } return false; - case "minigames\\titlebuttons": // TitleMenu + case "minigames/titlebuttons": // TitleMenu return this.ReloadTitleButtons(content, key); /**** ** Content\Strings ****/ - case "strings\\stringsfromcsfiles": + case "strings/stringsfromcsfiles": return this.ReloadStringsFromCsFiles(content); /**** ** Content\TileSheets ****/ - case "tilesheets\\animations": // Game1.LoadContent + case "tilesheets/animations": // Game1.LoadContent Game1.animations = content.Load(key); return true; - case "tilesheets\\buffsicons": // Game1.LoadContent + case "tilesheets/buffsicons": // Game1.LoadContent Game1.buffsIcons = content.Load(key); return true; - case "tilesheets\\bushes": // new Bush() + case "tilesheets/bushes": // new Bush() Bush.texture = new Lazy(() => content.Load(key)); return true; - case "tilesheets\\chairtiles": // Game1.LoadContent + case "tilesheets/chairtiles": // Game1.LoadContent return this.ReloadChairTiles(content, key, ignoreWorld); - case "tilesheets\\craftables": // Game1.LoadContent + case "tilesheets/craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load(key); return true; - case "tilesheets\\critters": // Critter constructor + case "tilesheets/critters": // Critter constructor return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0; - case "tilesheets\\crops": // Game1.LoadContent + case "tilesheets/crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); return true; - case "tilesheets\\debris": // Game1.LoadContent + case "tilesheets/debris": // Game1.LoadContent Game1.debrisSpriteSheet = content.Load(key); return true; - case "tilesheets\\emotes": // Game1.LoadContent + case "tilesheets/emotes": // Game1.LoadContent Game1.emoteSpriteSheet = content.Load(key); return true; - case "tilesheets\\fruittrees": // FruitTree + case "tilesheets/fruittrees": // FruitTree FruitTree.texture = content.Load(key); return true; - case "tilesheets\\furniture": // Game1.LoadContent + case "tilesheets/furniture": // Game1.LoadContent Furniture.furnitureTexture = content.Load(key); return true; - case "tilesheets\\furniturefront": // Game1.LoadContent + case "tilesheets/furniturefront": // Game1.LoadContent Furniture.furnitureFrontTexture = content.Load(key); return true; - case "tilesheets\\projectiles": // Game1.LoadContent + case "tilesheets/projectiles": // Game1.LoadContent Projectile.projectileSheet = content.Load(key); return true; - case "tilesheets\\rain": // Game1.LoadContent + case "tilesheets/rain": // Game1.LoadContent Game1.rainTexture = content.Load(key); return true; - case "tilesheets\\tools": // Game1.ResetToolSpriteSheet + case "tilesheets/tools": // Game1.ResetToolSpriteSheet Game1.ResetToolSpriteSheet(); return true; - case "tilesheets\\weapons": // Game1.LoadContent + case "tilesheets/weapons": // Game1.LoadContent Tool.weaponsTexture = content.Load(key); return true; /**** ** Content\TerrainFeatures ****/ - case "terrainfeatures\\flooring": // from Flooring + case "terrainfeatures/flooring": // from Flooring Flooring.floorsTexture = content.Load(key); return true; - case "terrainfeatures\\flooring_winter": // from Flooring + case "terrainfeatures/flooring_winter": // from Flooring Flooring.floorsTextureWinter = content.Load(key); return true; - case "terrainfeatures\\grass": // from Grass + case "terrainfeatures/grass": // from Grass return !ignoreWorld && this.ReloadGrassTextures(content, key); - case "terrainfeatures\\hoedirt": // from HoeDirt + case "terrainfeatures/hoedirt": // from HoeDirt HoeDirt.lightTexture = content.Load(key); return true; - case "terrainfeatures\\hoedirtdark": // from HoeDirt + case "terrainfeatures/hoedirtdark": // from HoeDirt HoeDirt.darkTexture = content.Load(key); return true; - case "terrainfeatures\\hoedirtsnow": // from HoeDirt + case "terrainfeatures/hoedirtsnow": // from HoeDirt HoeDirt.snowTexture = content.Load(key); return true; - case "terrainfeatures\\mushroom_tree": // from Tree + case "terrainfeatures/mushroom_tree": // from Tree return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree); - case "terrainfeatures\\tree_palm": // from Tree + 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 + 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 + 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 + 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); } @@ -585,9 +585,9 @@ namespace StardewModdingAPI.Metadata if (!ignoreWorld) { // dynamic textures - if (this.KeyStartsWith(key, "animals\\cat")) + if (this.KeyStartsWith(key, "animals/cat")) return this.ReloadPetOrHorseSprites(content, key); - if (this.KeyStartsWith(key, "animals\\dog")) + if (this.KeyStartsWith(key, "animals/dog")) return this.ReloadPetOrHorseSprites(content, key); if (this.IsInFolder(key, "Animals")) return this.ReloadFarmAnimalSprites(content, key); @@ -595,14 +595,14 @@ namespace StardewModdingAPI.Metadata if (this.IsInFolder(key, "Buildings")) return this.ReloadBuildings(key); - if (this.KeyStartsWith(key, "LooseSprites\\Fence")) + if (this.KeyStartsWith(key, "LooseSprites/Fence")) return this.ReloadFenceTextures(key); // dynamic data - if (this.IsInFolder(key, "Characters\\Dialogue")) + if (this.IsInFolder(key, "Characters/Dialogue")) return this.ReloadNpcDialogue(key); - if (this.IsInFolder(key, "Characters\\schedules")) + if (this.IsInFolder(key, "Characters/schedules")) return this.ReloadNpcSchedules(key); } @@ -687,7 +687,7 @@ namespace StardewModdingAPI.Metadata : animal.type.Value; if (animal.showDifferentTextureWhenReadyForHarvest.Value && animal.currentProduce.Value <= 0) expectedKey = $"Sheared{expectedKey}"; - expectedKey = $"Animals\\{expectedKey}"; + expectedKey = $"Animals/{expectedKey}"; // reload asset if (this.IsSameAssetKey(expectedKey, key)) @@ -1153,17 +1153,17 @@ namespace StardewModdingAPI.Metadata /// 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"); + 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"); + 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; } @@ -1272,7 +1272,7 @@ namespace StardewModdingAPI.Metadata private bool IsInFolder(string key, string folder, bool allowSubfolders = false) { return - this.KeyStartsWith(key, $"{folder}\\") + this.KeyStartsWith(key, $"{folder}/") && (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1); } -- cgit From a2190df08cc3f1b4a8dcb394056d65921d10702e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 18 Feb 2022 15:39:49 -0500 Subject: add AssetName to encapsulate asset name handling (#766) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 261 +++++++++++++----------------- 1 file changed, 110 insertions(+), 151 deletions(-) (limited to 'src/SMAPI/Metadata') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a6c4bb24..e7fac578 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -87,22 +87,22 @@ namespace StardewModdingAPI.Metadata /// 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) + 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")) + if (p.Key.IsDirectlyUnderPath("Characters") || p.Key.IsDirectlyUnderPath("Characters/Monsters")) return AssetBucket.Sprite; - if (this.IsInFolder(p.Key, "Portraits")) + if (p.Key.IsDirectlyUnderPath("Portraits")) return AssetBucket.Portrait; return AssetBucket.Other; }); // reload assets - propagatedAssets = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase); + propagatedAssets = assets.ToDictionary(p => p.Key, _ => false); updatedNpcWarps = false; foreach (var bucket in buckets) { @@ -149,16 +149,16 @@ namespace StardewModdingAPI.Metadata ** Private methods *********/ /// Reload one of the game's core assets (if applicable). - /// The asset key to reload. + /// The asset name 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) + private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps) { var content = this.MainContentManager; - key = this.AssertAndNormalizeAssetName(key); + string key = assetName.Name; changedWarps = false; /**** @@ -170,7 +170,7 @@ namespace StardewModdingAPI.Metadata { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { - if (this.IsSameAssetKey(tilesheet.ImageSource, key)) + if (assetName.IsEquivalentTo(tilesheet.ImageSource)) Game1.mapDisplayDevice.LoadTileSheet(tilesheet); } } @@ -188,7 +188,7 @@ namespace StardewModdingAPI.Metadata { GameLocation location = info.Location; - if (this.IsSameAssetKey(location.mapPath.Value, key)) + if (assetName.IsEquivalentTo(location.mapPath.Value)) { static ISet GetWarpSet(GameLocation location) { @@ -213,14 +213,13 @@ namespace StardewModdingAPI.Metadata /**** ** Propagate by key ****/ - Reflector reflection = this.Reflection; - switch (key.ToLower().Replace("\\", "/")) // normalized key so we can compare statically + switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { /**** ** Animals ****/ case "animals/horse": - return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key); + return !ignoreWorld && this.ReloadPetOrHorseSprites(content, assetName); /**** ** Buildings @@ -231,7 +230,7 @@ namespace StardewModdingAPI.Metadata case "buildings/houses_paintmask": // Farm { - bool removedFromCache = this.RemoveFromPaintMaskCache(key); + bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); Farm farm = Game1.getFarm(); farm?.ApplyHousePaint(); @@ -250,7 +249,7 @@ namespace StardewModdingAPI.Metadata case "characters/farmer/farmer_base_bald": case "characters/farmer/farmer_girl_base": case "characters/farmer/farmer_girl_base_bald": - return !ignoreWorld && this.ReloadPlayerSprites(key); + return !ignoreWorld && this.ReloadPlayerSprites(assetName); case "characters/farmer/hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); @@ -313,7 +312,7 @@ namespace StardewModdingAPI.Metadata return true; case "data/npcdispositions": // NPC constructor - return !ignoreWorld && this.ReloadNpcDispositions(content, key); + return !ignoreWorld && this.ReloadNpcDispositions(content, assetName); case "data/npcgifttastes": // Game1.LoadContent Game1.NPCGiftTastes = content.Load>(key); @@ -393,7 +392,7 @@ namespace StardewModdingAPI.Metadata } if (!ignoreWorld) - this.ReloadDoorSprites(content, key); + this.ReloadDoorSprites(content, assetName); return true; case "loosesprites/cursors2": // Game1.LoadContent @@ -425,7 +424,7 @@ namespace StardewModdingAPI.Metadata return true; case "loosesprites/suspensionbridge": // SuspensionBridge constructor - return !ignoreWorld && this.ReloadSuspensionBridges(content, key); + return !ignoreWorld && this.ReloadSuspensionBridges(content, assetName); /**** ** Content\Maps @@ -456,7 +455,7 @@ namespace StardewModdingAPI.Metadata return false; case "minigames/titlebuttons": // TitleMenu - return this.ReloadTitleButtons(content, key); + return this.ReloadTitleButtons(content, assetName); /**** ** Content\Strings @@ -480,14 +479,14 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets/chairtiles": // Game1.LoadContent - return this.ReloadChairTiles(content, key, ignoreWorld); + return this.ReloadChairTiles(content, assetName, 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; + return !ignoreWorld && this.ReloadCritterTextures(content, assetName) > 0; case "tilesheets/crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); @@ -541,7 +540,7 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/grass": // from Grass - return !ignoreWorld && this.ReloadGrassTextures(content, key); + return !ignoreWorld && this.ReloadGrassTextures(content, assetName); case "terrainfeatures/hoedirt": // from HoeDirt HoeDirt.lightTexture = content.Load(key); @@ -556,27 +555,27 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/mushroom_tree": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.mushroomTree); case "terrainfeatures/tree_palm": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.palmTree); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, 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); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, 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); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, 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); + return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.pineTree); } /**** @@ -585,25 +584,25 @@ namespace StardewModdingAPI.Metadata 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 (assetName.StartsWith("animals/cat")) + return this.ReloadPetOrHorseSprites(content, assetName); + if (assetName.StartsWith("animals/dog")) + return this.ReloadPetOrHorseSprites(content, assetName); + if (assetName.IsDirectlyUnderPath("Animals")) + return this.ReloadFarmAnimalSprites(content, assetName); - if (this.IsInFolder(key, "Buildings")) - return this.ReloadBuildings(key); + if (assetName.IsDirectlyUnderPath("Buildings")) + return this.ReloadBuildings(assetName); - if (this.KeyStartsWith(key, "LooseSprites/Fence")) - return this.ReloadFenceTextures(key); + if (assetName.StartsWith("LooseSprites/Fence")) + return this.ReloadFenceTextures(assetName); // dynamic data - if (this.IsInFolder(key, "Characters/Dialogue")) - return this.ReloadNpcDialogue(key); + if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) + return this.ReloadNpcDialogue(assetName); - if (this.IsInFolder(key, "Characters/schedules")) - return this.ReloadNpcSchedules(key); + if (assetName.IsDirectlyUnderPath("Characters/schedules")) + return this.ReloadNpcSchedules(assetName); } return false; @@ -618,14 +617,14 @@ namespace StardewModdingAPI.Metadata ****/ /// Reload buttons on the title screen. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. /// Derived from the constructor and . - private bool ReloadTitleButtons(LocalizedContentManager content, string key) + private bool ReloadTitleButtons(LocalizedContentManager content, IAssetName assetName) { if (Game1.activeClickableMenu is TitleMenu titleMenu) { - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); titleMenu.titleButtonsTexture = texture; titleMenu.backButton.texture = texture; @@ -645,21 +644,21 @@ namespace StardewModdingAPI.Metadata /// 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. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadPetOrHorseSprites(LocalizedContentManager content, string key) + private bool ReloadPetOrHorseSprites(LocalizedContentManager content, IAssetName assetName) where TAnimal : NPC { // find matches TAnimal[] animals = this.GetCharacters() .OfType() - .Where(p => this.IsSameAssetKey(p.Sprite?.Texture?.Name, key)) + .Where(p => assetName.IsEquivalentTo(p.Sprite?.Texture?.Name)) .ToArray(); if (!animals.Any()) return false; // update sprites - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); foreach (TAnimal animal in animals) animal.Sprite.spriteTexture = texture; return true; @@ -667,10 +666,10 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching farm animals. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. /// Derived from . - private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key) + private bool ReloadFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName) { // find matches FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); @@ -678,7 +677,7 @@ namespace StardewModdingAPI.Metadata return false; // update sprites - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (FarmAnimal animal in animals) { // get expected key @@ -690,23 +689,23 @@ namespace StardewModdingAPI.Metadata expectedKey = $"Animals/{expectedKey}"; // reload asset - if (this.IsSameAssetKey(expectedKey, key)) + if (assetName.IsEquivalentTo(expectedKey)) animal.Sprite.spriteTexture = texture.Value; } return texture.IsValueCreated; } /// Reload building textures. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadBuildings(string key) + private bool ReloadBuildings(IAssetName assetName) { // get paint mask info const string paintMaskSuffix = "_PaintMask"; - bool isPaintMask = key.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); + bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); // get building type - string type = Path.GetFileName(key); + string type = Path.GetFileName(assetName.Name)!; if (isPaintMask) type = type.Substring(0, type.Length - paintMaskSuffix.Length); @@ -718,7 +717,7 @@ namespace StardewModdingAPI.Metadata .ToArray(); // remove from paint mask cache - bool removedFromCache = this.RemoveFromPaintMaskCache(key); + bool removedFromCache = this.RemoveFromPaintMaskCache(assetName); // reload textures if (buildings.Any()) @@ -734,12 +733,12 @@ namespace StardewModdingAPI.Metadata /// Reload map seat textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name 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) + private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) { - MapSeat.mapChairTexture = content.Load(key); + MapSeat.mapChairTexture = content.Load(assetName.Name); if (!ignoreWorld) { @@ -747,7 +746,7 @@ namespace StardewModdingAPI.Metadata { foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) { - if (this.IsSameAssetKey(seat._loadedTextureFile, key)) + if (assetName.IsEquivalentTo(seat._loadedTextureFile)) seat.overlayTexture = MapSeat.mapChairTexture; } } @@ -758,9 +757,9 @@ namespace StardewModdingAPI.Metadata /// Reload critter textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns the number of reloaded assets. - private int ReloadCritterTextures(LocalizedContentManager content, string key) + private int ReloadCritterTextures(LocalizedContentManager content, IAssetName assetName) { // get critters Critter[] critters = @@ -768,7 +767,7 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() where location.critters != null from Critter critter in location.critters - where this.IsSameAssetKey(critter.sprite?.Texture?.Name, key) + where assetName.IsEquivalentTo(critter.sprite?.Texture?.Name) select critter ) .ToArray(); @@ -776,7 +775,7 @@ namespace StardewModdingAPI.Metadata return 0; // update sprites - Texture2D texture = content.Load(key); + Texture2D texture = content.Load(assetName.Name); foreach (var entry in critters) entry.sprite.spriteTexture = texture; @@ -785,11 +784,11 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for interior doors. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any doors were affected. - private bool ReloadDoorSprites(LocalizedContentManager content, string key) + private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (GameLocation location in this.GetLocations()) { @@ -803,7 +802,7 @@ namespace StardewModdingAPI.Metadata continue; string curKey = this.Reflection.GetField(door.Sprite, "textureName").GetValue(); - if (this.IsSameAssetKey(curKey, key)) + if (assetName.IsEquivalentTo(curKey)) door.Sprite.texture = texture.Value; } } @@ -827,12 +826,12 @@ namespace StardewModdingAPI.Metadata } /// Reload the sprites for a fence type. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadFenceTextures(string key) + private bool ReloadFenceTextures(IAssetName assetName) { - // get fence type - if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType)) + // get fence type (e.g. LooseSprites/Fence3 => 3) + if (!int.TryParse(this.GetSegments(assetName.BaseName)[1].Substring("Fence".Length), out int fenceType)) return false; // get fences @@ -855,22 +854,22 @@ namespace StardewModdingAPI.Metadata /// Reload tree textures. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any textures were reloaded. - private bool ReloadGrassTextures(LocalizedContentManager content, string key) + private bool ReloadGrassTextures(LocalizedContentManager content, IAssetName assetName) { Grass[] grasses = ( from location in this.GetLocations() from grass in location.terrainFeatures.Values.OfType() - where this.IsSameAssetKey(grass.textureName(), key) + where assetName.IsEquivalentTo(grass.textureName()) select grass ) .ToArray(); if (grasses.Any()) { - Lazy texture = new Lazy(() => content.Load(key)); + Lazy texture = new Lazy(() => content.Load(assetName.Name)); foreach (Grass grass in grasses) grass.texture = texture; return true; @@ -932,11 +931,11 @@ namespace StardewModdingAPI.Metadata /// Reload the disposition data for matching NPCs. /// The content manager through which to reload the asset. - /// The asset key to reload. + /// The asset name to reload. /// Returns whether any NPCs were affected. - private bool ReloadNpcDispositions(LocalizedContentManager content, string key) + private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName) { - IDictionary data = content.Load>(key); + IDictionary data = content.Load>(assetName.Name); bool changed = false; foreach (NPC npc in this.GetCharacters()) { @@ -953,16 +952,16 @@ namespace StardewModdingAPI.Metadata /// 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) + private void ReloadNpcSprites(IEnumerable keys, IDictionary propagated) { // get NPCs - HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase); + IDictionary lookup = keys.ToDictionary(p => p.Name, 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 } + where key != null && lookup.ContainsKey(key) + select new { Npc = npc, AssetName = lookup[key] } ) .ToArray(); if (!characters.Any()) @@ -971,56 +970,56 @@ namespace StardewModdingAPI.Metadata // update sprite foreach (var target in characters) { - target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.Key); - propagated[target.Key] = true; + target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.Name); + propagated[target.AssetName] = 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) + private void ReloadNpcPortraits(IEnumerable keys, IDictionary propagated) { // get NPCs - HashSet lookup = new HashSet(keys, StringComparer.OrdinalIgnoreCase); + IDictionary lookup = keys.ToDictionary(p => p.Name, 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 } + where key != null && lookup.ContainsKey(key) + select new { Npc = npc, AssetName = lookup[key] } ) .ToList(); // special case: Gil is a private NPC field on the AdventureGui