Can't read unpacked map file '..\Mods\....tbin' directly from the underlying content manager
Pathoschild opened this issue ยท 4 comments
Farm Expansion fails with this error:
[00:46:52 ERROR Farm Expansion] This mod failed in the SaveEvents.AfterLoad event. Technical details:
StardewModdingAPI.Framework.Exceptions.SContentLoadException: Failed loading content asset '..\Mods\FarmExpansion 130\assets\FarmExpansion.tbin': can't read unpacked map file '..\Mods\FarmExpansion 130\assets\FarmExpansion.tbin' directly from the underlying content manager. It must be loaded through the mod's StardewModdingAPI.IModHelper.Content helper.
at StardewModdingAPI.Framework.SContentManager.Load[T](String assetName, LanguageCode language) in C:\source\_Stardew\SMAPI\src\SMAPI\Framework\SContentManager.cs:line 150
at StardewModdingAPI.Framework.SContentManager.Load[T](String assetName) in C:\source\_Stardew\SMAPI\src\SMAPI\Framework\SContentManager.cs:line 111
at StardewValley.GameLocation.loadMap(String mapPath) in C:\Users\gitlab-runner\gitlab-runner\builds\5c0f9387\0\chucklefish\stardewvalley\Farmer\Farmer\Locations\GameLocation.cs:line 612
at StardewValley.GameLocation.reloadMap() in C:\Users\gitlab-runner\gitlab-runner\builds\5c0f9387\0\chucklefish\stardewvalley\Farmer\Farmer\Locations\GameLocation.cs:line 666
at StardewValley.GameLocation..ctor(String mapPath, String name) in C:\Users\gitlab-runner\gitlab-runner\builds\5c0f9387\0\chucklefish\stardewvalley\Farmer\Farmer\Locations\GameLocation.cs:line 583
at StardewValley.Locations.BuildableGameLocation..ctor(String mapPath, String name) in C:\Users\gitlab-runner\gitlab-runner\builds\5c0f9387\0\chucklefish\stardewvalley\Farmer\Farmer\Locations\BuildableGameLocation.cs:line 24
at StardewValley.Farm..ctor(String mapPath, String name) in C:\Users\gitlab-runner\gitlab-runner\builds\5c0f9387\0\chucklefish\stardewvalley\Farmer\Farmer\Locations\Farm.cs:line 55
at FarmExpansion.FarmExpansion..ctor(String mapAssetKey, String name, FEFramework framework)
at FarmExpansion.Framework.FEFramework.SaveEvents_AfterLoad(Object sender, EventArgs e)
at StardewModdingAPI.Framework.Events.ManagedEvent.Raise() in C:\source\_Stardew\SMAPI\src\SMAPI\Framework\Events\ManagedEvent.cs:line 110
See full log.
I spent a bit of time today attempting to get Farm Expansion to work with the latest beta version of SMAPI, and stumbled across this issue. I did some digging, and I thought I would share the results of my analysis. I am fairly new to modding/open source in general, and have never looked at this codebase before today, so probably take the following with a grain of salt - I just wanted to write out my findings.
-
The StardewValley.Farm constructor changed at some point from taking a Map object to taking a map path string as its first argument.
-
#488 overhauled SMAPI's content caching system, making it entirely decentralized. From what I can tell, this means that a cache for a mod's content manager will be entirely separate from a cache for the game's content manager.
-
because of 1), the game has to load the map itself - it cannot be pre-loaded by the mod. Even if the mod loads the map through its content manager before calling the Farm constructor and that manager caches it, because of 2) the game will not find it in the cache, causing it to go through SContentManager.Load, get a cache miss, and proceed to the throw condition for .tbin files.
While I was looking through #488, I saw @Entoarox's suggestion of a centralized cache that is then copied into per-manager caches - I think that might work here.
Thanks for the help! I'm working on an approach which keeps the decentralised cache (so we don't have an exponential number of cloned assets), but allows cloned asset transfers between content managers. This approach also eliminates the ambiguity between Content
and mod assets.
The essential change is that each mod asset now has a namespaced key handled by SMAPI (instead of a file path handled by the XNA content pipeline). When a content manager receives a namespaced key, it checks whether it owns the namespace. If so, it loads the asset from its root folder. If not, it requests the asset through the content coordinator and caches its own copy. (The content coordinator is an existing class which handles any central content logic, like content manager creation or cache invalidation.)
For example, here's how that works in practice:
- Farm Expansion requests
assets\map.tbin
from its mod folder. - The Farm Expansion content manager caches
SMAPI\advize.farmexpansion\assets\map.tbin
locally. - The game later requests
SMAPI\advize.farmexpansion\assets\map.tbin
fromGame1.content
. Game1.content
detects the namespaced key, realises that it doesn't have a cached copy and doesn't own the namespace, and requests it from the content coordinator.- The content coordinator requests it from the Farm Expansion content manager, which returns the cached copy (or reloads it if needed, e.g. due to cache invalidation).
- The content coordinator clones the asset (if needed), and returns it to
Game1.content
. Game1.content
cachesSMAPI\advize.farmexpansion\assets\map.tbin
and returns it to the game.
If the game requests it again, Game1.content
can immediately return its own cached copy. Any asset changes will only affect the content manager it was taken from, and cache invalidation is easy since all content managers use the same asset key.
This still needs more polishing and testing before it's ready to commit, but I think it combines the benefits of a decentralised cache (e.g. changes aren't leaked between content managers) and a centralised one (e.g. less memory overhead).
That sounds like an excellent solution - thanks for taking the time to write out that explanation!