- What is ThemeManager?
- Quick! How do I use this?
- So, what is a Theme?
- What does the C# look like?
- Well, what does a
theme.json
look like? - Content Patcher Integration
- Theme Folder Structure
- Changelog
ThemeManager is an asset/data loader for Stardew Valley C# mods that:
- Discovers available themes from three sources:
- The current mod's
assets/themes/
folder. - Content Packs for the current mod that have a
theme.json
file. - Other mods that have a
my-mod.unique-id:theme
key in their manifest, pointing to a theme json file.
- The current mod's
- Selects an appropriate theme out of all available themes based on which mods are installed.
- Makes it easy to let users choose a theme with a config file.
- Makes it easy to reload themes at runtime for easier development.
- Passes assets through SMAPI's
AssetRequested
event (orIAssetLoader
for pre-3.14) so that Content Patcher packs (and other mods) can modify assets. - Exists as a single file that is easy to drop into a project with no other dependencies.
Also, ThemeManager is licensed under MIT-0. You can use this. You don't have to credit me. Please use it. Make life easier for people making content packs.
- Grab the
ThemeManager.cs
file (and optionallyColorConverter.cs
too) from this repository and drop it in your project. Everything is neat and self contained. In the future, if there's an update, just replace the file with the updated one. - If you're still using SMAPI version 3.13 or lower, then you'll want to open
ThemeManager.cs
and uncomment this line at the start of the file:// #define THEME_MANAGER_PRE_314
- Add a
ThemeManager
instance to your mod, probably inModEntry
but anywhere convenient will do. - Construct that
ThemeManager
and call its.Discover()
method, which both populates the list of available themes and also selects the best one to use. - Use
ThemeManager.Load<T>(path)
instead ofHelper.ModContent.Load<T>(path)
for theme-aware asset loading, as appropriate.
Optionally:
- Register a console command for changing the current theme. (Example Here)
- Register a theme picker in Generic Mod Config Menu. (Example Here)
- Listen to the
ThemeChanged
event and do anything you need to reload assets from your theme. (Example Here) - Subclass
BaseThemeData
and add extra colors and other values for themes to override, making themes more functional. (Example Here)
The included example mod does all of these, so check its ModEntry.cs
and see for yourself how simple it is.
Themes are a mix of custom data, which is loaded from a theme.json
file, as well as textures and potentially other resources loaded from assets/
. Mod authors can support as much or as little extra data as they want.
You're looking at the example project! If you don't want to check our ModEntry.cs file, here's the basics!
using Leclair.Stardew.ThemeManager;
namespace MyCoolMod {
// All theme data types extend BaseThemeData,
// which itself only contains a couple very
// basic properties to do with loading / enumeration
// and nothing to do with appearance.
class MyThemeData : BaseThemeData {
int PaddingTop { get; set; } = 8;
Color? TextColor { get; set; }
}
// Just your everyday, average config file.
class MyConfig {
string Theme { get; set; } = "automatic";
}
// The actual Mod entry point.
class ModEntry : Mod {
internal MyConfig Config;
// When declaring your ThemeManager, you provide
// your own theme data class (or use BaseThemeData).
internal ThemeManager<MyThemeData> ThemeManager;
// I find it useful to set up a property for
// quick access to the current theme.
internal MyThemeData Theme => ThemeManager.Theme;
public override void Entry(IModHelper helper) {
// Init stuff
Config = Helper.ReadConfig<MyConfig>();
// Theme Manager
ThemeManager = new(this, Config.Theme);
ThemeManager.ThemeChanged += OnThemeChanged;
ThemeManager.Discover();
}
private void OnThemeChanged(object sender, ThemeChangedEventArgs<MyThemeData> e) {
// Do stuff when the user changes theme, like
// reload textures.
}
private void SomewhereElse() {
// Instead of
Helper.ModContent.Load<Texture2D>("assets/blah.png");
// you do
ThemeManager.Load<Texture2D>("blah.png");
// And also stuff like
Color text = Theme.TextColor ?? Game1.textColor;
}
}
}
Glad you asked. For the above, hypothetical mod:
{
// This property is only used when loading a theme from your mod's
// assets/themes/ folder. Otherwise, it is ignored. This is for
// setting a default, human readable name for your theme.
"Name": "Flower",
// For displaying a list of themes in the Generic Mod Config Menu,
// you can provide localized names of your theme. Please note that
// this is optional. We're not using the i18n system because it's
// honestly complete overkill when these are the only translated
// strings provided by themes.
"LocalizedNames": {
"es": "La Flor"
},
// A list of unique IDs of mods that this theme is for compatibility
// with. If a mod in this list is loaded, this theme will be selected
// when the current theme is set to automatic.
"For": [
"SomeOtherRetextureMod.UniqueId"
],
// If you keep your assets for this theme in a different folder than
// "assets/", you can override that here. This can otherwise be
// left out from your theme.
"AssetPrefix": "assets",
// And... that's it. Everything above is optional, and there's
// nothing else in BaseThemeData. For the earlier C# example though,
// we also have a color. So...
"TextColor": "#222"
}
When Content Patcher is loaded, ThemeManager will redirect .Load<>()
requests through GameContent using its event handler for AssetRequested
.
Say, for example, you request a menu texture:
var texture = ThemeManager.Load<Texture2D>("Menu.png");
Without Content Patcher, we just load the file directly and return it. Simple. Efficient. But less flexible.
When Content Patcher is present, we instead pass that call to something like:
Helper.GameContent.Load<Texture2D>("Mods/MyName.MyCoolMod/Themes/SomeoneElse.TheirThemesName/Menu.png");
To break it down, that string is combined from:
- The literal string
Mods/
- Your mod's unique ID
- The literal string
/Themes/
- The current theme's unique ID
- The literal string
/
- The requested asset path.
Our event handler for AssetRequested
is in charge of intercepting that request later on, and actually loading the base resource. The important thing, however, is that you can then in Content Patcher do something like
{
"Format": "1.25.0",
"Changes": [
{
"Action": "EditImage",
"Target": "Mods/MyName.MyCoolMod/Themes/SomeoneElse.TheirThemesName/Menu.png",
"FromFile": "assets/MyButton.png",
"ToArea": { "X": 160, "Y": 80, "Width": 16, "Height" 16 }
}
]
}
This way, content packs don't need to replace your entire asset when they don't need to, potentially improving future compatibility.
As noted at the beginning, there are several ways that themes can be loaded.
📁 Mods/
📁 MyCoolMod/
🗎 MyCoolMod.dll
🗎 manifest.json
📁 assets/
📁 themes/
📁 SomeTheme/
🗎 theme.json
📁 assets/
🗎 example.png
📁 Mods/
📁 [MCM] My Cool Theme/
🗎 manifest.json
🗎 theme.json
📁 assets/
🗎 example.png
Including a theme in other mods is slightly more involved than just throwing down a theme.json
file. How would we know that that specific file is for us? To make things explicit, we check the manifest.json
of every mod for a matching theme key. For example:
{
// The Usual Suspects
"Name": "Some Other Cool Mod",
"Author": "A Super Cool Person",
"Version": "5.4.3",
"Description": "Totally rad stuff.",
"UniqueID": "SuperCoolPerson.OtherCoolMod",
"MinimumApiVersion": "3.7.3",
"ContentPackFor": {
"UniqueID": "Pathoschild.ContentPatcher"
},
// Our Theme!
"MyName.MyCoolMod:theme": "compat/MyCoolMod/theme.json"
}
ThemeManager looks for a key starting with your mod's unique ID that then ends with :theme
, and tries loading the theme JSON file it points to. If it succeeds, then it adds that theme using the folder the theme JSON file is in as the root folder of the theme. So, given the above, you'd end up with something like:
📁 Mods/
📁 SomeOtherCoolMod/
🗎 manifest.json
📁 compat/
📁 MyCoolMod/
🗎 theme.json
📁 assets/
🗎 example.png