Tutorial: Making Framework Mods
Introduction
Framework mods refers to C# mods that add new functionality to the game's code, and allows content mods to make use of them without having to make their own C# component. They usually work by reading custom data set in content mods and then apply their changes accordingly.
This page details the various patterns and examples for making frameworks, how to read custom data from a content mod in your own framework, particularly ones that make use of Stardew Valley 1.6's added fields specifically to support framework data. It assumes you're familiar with making C# mods; if not, follow the guide to get started.
CustomFields
1.6 added CustomFields
to many of the game's data models (crops, items, etc.). It is a dictionary of string keys to string values that's ignored by the vanilla game, but can be read by framework mods to detect whether to apply your framework's changes. This is the recommended method to store and read custom data set by content mods to frameworks due to its simplicity (the game will have already handled the loading/invalidation for you), though it does have the drawback of supporting strings only and requires deserialization to convert to a non-string type.
For example, this code snippet reads a custom field from building data and apply its logic accordingly:
var buildingData = building.GetData();
if (buildingData is not null &&
buildingData.CustomFields is not null &&
buildingData.CustomFields.TryGetValue("MyModID_CustomTexture", out string customTexture)) {
// do stuff with the customTexture field
}
A Content Patcher mod can then set this field in their custom buildings' CustomFields
dictionary, alongside the base game's building fields.
Custom assets
If your data model is too complex for a flat string dictionary provided by CustomFields
, you can define your own assets much like the vanilla game's own. This process is slightly more involved than reusing a vanilla model, but comes with greater flexibility.
Step 1 - Define your asset's data model type
First, consider what your new asset should look like. It can be JSON-serializable types, though the most commonly used type is a dictionary of strings IDs (matching a crop/item/building/NPC/etc.) to models containing custom data.
Important: If you have a list in your model, make sure it's either a list of strings, or a list of models with the Id
field (the latter is recommended); Content Patcher's targeted patching only works with these two types. If not either types then CP mods will have to patch by list index, which is not great for compatibility.
The example below is a (stripped down) view of Extra Animal Config's asset type, containing a list of commonly used types you may want in your own model:
class AnimalExtensionData {
public float MalePercentage = -1;
public string? FeedItemId = null;
public bool OutsideForager = false;
public List<string> ExtraHouses = [];
}
using AnimalExtensionDataAssetType = Dictionary<string, AnimalExtensionData>;
Step 2 - Load your custom asset
Next, use the AssetRequested event to Load
your new asset into the content pipeline with a name of your choosing.
This example code snippet loads the above asset into a new asset named selph.ExtraAnimalConfig/AnimalExtensionData
:
public void OnAssetRequested(object? sender, AssetRequestedEventArgs e) {
if (e.NameWithoutLocale.IsEquivalentTo("selph.ExtraAnimalConfig/AnimalExtensionData")) {
// Important: Make sure to initialize a new instance every time
e.LoadFrom(() => new Dictionary<string, AnimalExtensionData>(), AssetLoadPriority.Exclusive);
}
}
// Somewhere in your mod's Entry function...
helper.Events.Content.AssetRequested += OnAssetRequested;
You can then read this asset later in your mod code:
var animalExtensionDataAsset = Game1.content.Load<AnimalExtensionDataAssetType>("selph.ExtraAnimalConfig/AnimalExtensionData");
if (animalExtensionDataAsset.TryGetValue("Brown Cow", out var animalExtensionData) && animalExtensionData.OutsideForager) {
// animal has OutsideForager set, do stuff here
}
Step 3 (optional) - Handle caching and invalidation
Instead of calling Load
every time, you may instead want to cache the data somewhere, either due to the slight overhead of loading your asset, or if you want to apply some extra changes of your own and save that into a new type. In which case you need to handle the invalidation event which happens when another content mod adds its own data to your asset, causing it to be outdated and needing to be reloaded.
For example, you can define a class to handle your asset type as such:
public class AssetHandler {
private static Dictionary<string, AnimalExtensionDataAssetType>? privateData = null;
public static Dictionary<string, AnimalExtensionDataAssetType> data {
get {
if (privateData == null) {
privateData = Game1.content.Load<Dictionary<string, AnimalExtensionDataAssetType>>("selph.ExtraAnimalConfig/AnimalExtensionData");
// assuming monitor is the IMonitor instance from ModEntry
monitor.Log($"Loaded asset selph.ExtraAnimalConfig/AnimalExtensionData with {privateData.Count} entries.");
}
return privateData!;
}
}
This will handle loading (in conjunction with the AssetRequested
event above, and the rest of your code only needs to call AssetHandler.data
, but you'll also need to handle invalidation and reload privateData
accordingly.
// Put this in your AssetHandler class
public void OnAssetsInvalidated(object? sender, AssetsInvalidatedEventArgs e) {
foreach (var name in e.NamesWithoutLocale) {
if (name.IsEquivalentTo("selph.ExtraAnimalConfig/AnimalExtensionData")) {
// assuming monitor is the IMonitor instance from ModEntry
monitor.Log($"Asset selph.ExtraAnimalConfig/AnimalExtensionData invalidated, reloading.");
privateData = null;
}
}
}
// Next time .data is called, privateData should be updated with a fresh load as shown in the previous code snippet.
// Somewhere in your mod's Entry function...
helper.Events.Content.AssetsInvalidated += AssetHandler.OnAssetsInvalidated;
Now your asset is always updated even if a CP mod edits it later.
Custom content patch model
An older, but still supported method of storing framework data is to define a custom content pack model separate from CP and the game's asset model, similar to Alternative Textures or Fashion Sense. This is not recommended because you'll be missing out on CP's features like conditional patching, patching another mod's data and easy configuration, reducing compatibility (unless you opt to reimplement them in your own content pack model).
For instructions on setting up a custom content pack model, see this page.
Delegates
So far, this page described allowing content mods to provide custom data model for framework mods to parse. However, it's also possible to expose snippets of custom behaviour for content mods to call upon through registering special delegates defined by the base game.
Game State Query
A number of data models have a "Condition" field that accept a string known as a game state query. When required this string will be evaluated for a true or false value and change behaviour according to the result. For example, shop data uses "DAY_OF_WEEK Wednesday" to sell honey only on Wednesdays.
See this page for using game state queries from content mods.
Adding a Game State Query
public static bool MySpecialGSQ(string[] query, GameStateQueryContext context){
// Perform checks and return a boolean value here
// query contains the registered GSQ name, followed by any user provided arguments
// context has references to game state such as location, player.
// It also carries a instance of Random which should be used over Game1.random/Random.Shared within the GSQ, to respect any seeding done.
return true;
}
// during game launched
GameStateQuery.Register(
// GSQs are conventionally all caps snake case
$"{Manifest.UniqueID}_MY_SPECIAL_GSQ",
// Can use Delegate.CreateDelegate with MethodInfo instead, see vanilla's GameStateQuery() constructor.
MySpecialGSQ
);
Item Query
This is the primary mechanism for finding and producing items. It is used in shop data, machine outputs, and anywhere else that implements GenericSpawnItemData.
See this page for using item queries from content mods.
Adding an Item Query
public static IEnumerable<ItemQueryResult> MySpecialItemQuery(string key, string arguments, ItemQueryContext context, bool avoidRepeat, HashSet<string> avoidItemIds, Action<string, string> logError){
// Arguments must be split manually, with ItemQueryResolver.Helpers.SplitArguments
// Much like GSQ, you should use context.random instead of Game1.random/Random.Shared
return [new ItemQueryResult(item.getOne())];
}
// during game launched
ItemQueryResolver.Register(
// Item queries are conventionally all caps snake case
$"{Manifest.UniqueID}_MY_SPECIAL_ITEM_QUERY",
MySpecialItemQuery
);
One thing to note is that queries may have tokens that get resolved outside of the delegate scope, using the optional formatItemId argument to ItemQueryResolver.TryResolve. For example DROP_IN_ID only works with machine item queries and gets replaced by the ID of the item put into machine before the item query gets to run.
Actions
Actions are delegate methods that can be called from a content mod in certain situations. This includes:
- Dialog, with the $action command.
- Mail, with %action <action>%% command. (needs main wiki updates)
- Event, with action <action> command. (needs main wiki updates)
- Buff, with ActionsOnApply.
- Shop items, with ActionsOnPurchase.
- Museum completion, with RewardActions.
- Triggers, covered in the next section.
Adding an Action
public static bool MySpecialAction(string[] args, TriggerActionContext context, out string error){
// args
if (ActuallyWeCantDoOurThing()){
// will not mark the action as "complete"
return false;
}
DoOurThing();
// will mark the action as "complete"
return true;
}
// during game launched
TriggerActionManager.RegisterAction(
$"{Manifest.UniqueID}_MySpecialAction",
MySpecialAction
);
Triggers
Triggers are events that can be raised from C# to signal that an event has occurred to content mods. Through editing Data/TriggerActions, content mods can call upon an Action, from either base game or registered by a mod.
Adding a Trigger
// during game launched
TriggerActionManager.RegisterTrigger($"{Manifest.UniqueID}_MySpecialTrigger");
// Raise trigger at relevant place to call all actions
TriggerActionManager.Raise(
$"{Manifest.UniqueID}_MySpecialTrigger",
// these optional arguments will be provided to the action via TriggerActionContext
triggerArgs: null, // object[]
location: null, // GameLocation
player: null, // Farmer
targetItem: null, // Item
inputItem: null // Item
);
Map Properties and Tile Properties
Map files (tbin/tmx) can hold properties at the map level and per tile, see Modding:Maps for list of vanilla map properties & tile properties.
See this page for more info on adding properties to maps.
Accessing Map Properties
It is usually better to use Data/Locations CustomFields instead of map properties, unless you want to reuse a particular tmx/tbin map for many locations and have same property in all of them.
// get the property's string values
Game1.currentLocation.TryGetMapProperty($"{Manifest.UniqueID}_MySpecialMapProperty", out string propertyValue)
// get the property as a Vector2, this function has several other overloads for different types
Game1.currentLocation.TryGetMapPropertyAs($"{Manifest.UniqueID}_MySpecialMapProperty", out Vector2 propertyValue, required: true)
Accessing Tile Properties
Tile Properties are per tile and per layer, this example checks for a property in the Back layer.
var backLayer = location.map.RequireLayer("Back");
MapTile tile = backLayer.Tiles[x, y];
tile.Properties.ContainsKey($"{Manifest.UniqueID}_MySpecialTileProperty");
Tile Action and TouchAction
The "Action" and "TouchAction" tile properties trigger a handler, and they can be extended with custom handlers.
Adding a tile Action handler
Action handlers are invoked when player presses the "use" button.
public static bool MySpecialTileAction(GameLocation location, string[] args, Farmer farmer, Point point){
if (ActuallyWeCantDoOurThing()){
// continue to other interactions
return false;
}
DoOurThing();
// stop trying other interactions
return true;
}
// during game launched
GameLocation.RegisterTileAction(
$"{Manifest.UniqueID}_MySpecialTileAction",
MySpecialTileAction
);
Adding a tile TouchAction handler
TouchAction handlers are invoked when player enters(touches) a tile.
public static void MySpecialTileTouchAction(GameLocation location, string[] args, Farmer farmer, Vector2 position){
// touch actions don't have return value
}
// during game launched
GameLocation.RegisterTouchAction(
$"{Manifest.UniqueID}_MySpecialTileTouchAction",
MySpecialTileTouchAction
);