This document explains how to use this mod to add your custom Secret Notes to Stardew Valley.
Secret Note Framework works for other mods by providing a data asset for them to edit. At this time, clients are expected to use Content Patcher or the C# API in order to perform their edits. It is probably possible to use SMAPI's content API as well, if you are writing a C# mod, but you would need to copy the interface definition over anyway, and at that point it's probably easier to just use Secret Note Framework's API.
I do not support content packs at this time, and that feature is unlikely to appear.
In addition to providing a way to add secret notes without fear of conflicts (or of running out of space on the collections page), this mod also lets you do a few advanced things, like:
- specify complex eligibility conditions on a note-by-note basis with game state queries
- declare which specific locations (or location contexts) your notes appear in, so you can (e.g.) restrict your mod's notes to spawn only in your areas
- use different (custom) items to represent different sets of notes, as desired
- specify your own assets for note content and formatting, including image notes
- create notes with an image as well as a small amount of text
- set any number of trigger actions to be run when a note is first read
Let's begin!
(Please note: although I recommend using i18n for text content in your mods, the examples in this guide omit its use, for clarity of purpose.)
The main goal of the framework is to add notes, so here's what that looks like. The asset to target with your edits is:
Mods/ichortower.SecretNoteFramework/Notes
The asset is a string->object dictionary. The string keys are note IDs, and
should be unique string
IDs,
like most 1.6-era data items. The model (object) has the following fields:
| Field | Type | Purpose |
|---|---|---|
Contents |
string |
The text content of your note. Obviously, it is required for text notes.
Formatting is the same as vanilla mail and secret notes: Unlike vanilla notes, with Secret Note Framework you can include some text in
image notes (see Default: |
Title |
string |
The note's title, which will be displayed on the hover tooltip in the
collections menu. Vanilla secret notes generate a title from their integer id
(e.g. "Secret Note #15"), but you can specify whatever you would like. If not
specified, the tooltip will display Default: |
Conditions |
string |
A game state query specifying the conditions for this note to be available to spawn. If null or empty, the note will be available without restriction, although the player will still need the Magnifying Glass in order to find secret notes in the first place. Conditions are evaluated at the start of each game day, so after fulfilling the conditions for a note to be available, the player will need to sleep a day in order for the note to be able to appear. This mod adds a query for checking whether a modded secret note has been seen by a player, which is useful for ordering your notes. Default: |
Location |
string |
This string specifies one or more location names where the note is able to
appear: except in the specified locations, the note will not spawn. This allows
you to limit the areas where your note can be found even more narrowly than
with Specify any number of location names, separated by commas (e.g. If this field is specified, it will supersede Default: |
LocationContext |
string |
This string specifies one or more location contexts where the note is able to
appear. In vanilla, journal scraps spawn only in the Specify any number of context names, separated by commas (e.g. If Default: |
ObjectId |
string |
A qualified or unqualified object ID from If this field is Default: |
NoteTexture |
string |
A game asset path indicating the note background texture to use when displaying
the note. This is equivalent to specifying an asset via the special mail format
code
Default: |
NoteTextureIndex |
integer |
An integer specifying the index in the Default: |
NoteTextColor |
string |
A string specifying what color to use to render the note's text, equivalent to
using the format code
... or, you can specify any RGB color you like by using the form Note that the text in mail and secret notes is rendered at 75% opacity, so the color you indicate here will blend slightly with the background texture. Default: |
NoteImageTexture |
string |
A game asset path indicating the texture to use when loading an image for an image note or combined note (see those sections for more details). Note images are 64x64 pixels and are read in order, left-to-right and top-to-bottom, just like other spritesheets; but be aware that there is a hardcoded offset for the image of the piece of tape holding the image inside the note (193/65, 14x21), so you should not use the index containing that. If Default: |
NoteImageTextureIndex |
integer |
An integer specifying the index in the To display just an image in your note, set this to a value >= 0 and also omit
the Default: |
ActionsOnFirstRead |
array(string) |
This array of strings specifies what trigger actions should be run when the player reads this note by using the item from inventory (i.e. when the note is added to the collection, versus when re-reading the note from the collections menu). The actions are run when the letter menu is closed. Note: if you use this mod's trigger action to mark this note as seen (adding it to a player's collection), these actions will not run. Likewise, if you mark a seen note as unseen and it is collected again, the actions will run again when the player uses the item and views the note. Default: |
Most of these fields are optional: you can create a fully-working text note by
specifying only Contents (although including a Title is nice to do), or an
image note with only NoteImageTextureIndex (although you ought to also
specify NoteImageTexture and use your own asset).
A Content Patcher patch to add a secret note to a mod might look like this:
{
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
"Action": "EditData",
"Entries": {
"{{ModId}}_SecretNote01": {
"Contents": "I sure hope nobody finds this! It's full of embarrassing secrets.",
"Title": "TOP SECRET DIARY",
"Conditions": "PLAYER_HEARTS Current {{YourNpc}} 4",
"ActionsOnFirstRead": [
"AddMail Current {{ModId}}_Mail_HowDareYouFindMyDiary tomorrow"
]
}
}
}This patch creates a note which is available only after reaching 4 hearts with
{{YourNpc}}. When it is found and read, it sends a mail to the current
player for the next day, presumably to scold them for reading the diary.
As usual for mod-provided APIs, to use this one you will have to copy its definitions into your own project. From API.cs you will need:
- the definitions for methods you will call
- the
INoteDatainterface definition
... which might look like this:
namespace ichortower.SNF
{
public interface ISnfApi
{
public INoteData CreateDataObject();
public bool RegisterSecretNote(string uniqueId, INoteData note);
public bool Reload(bool data = false, bool conditions = false);
}
public interface INoteData
{
public string Contents { get; set; }
public string Title { get; set; }
public string Conditions { get; set; }
public string Location { get; set; }
public string LocationContext { get; set; }
public string ObjectId { get; set; }
public string NoteTexture { get; set; }
public int NoteTextureIndex { get; set; }
public string NoteTextColor { get; set; }
public string NoteImageTexture { get; set; }
public int NoteImageTextureIndex { get; set; }
public List<string> ActionsOnFirstRead { get; set; }
}
}You must have a class instance that implements INoteData in order to set its
values and ultimately call RegisterSecretNote(): to do this, you can use
CreateDataObject() to get an instance of the one SNF uses internally, instead
of implementing your own subclass.
Looking back to the Content Patcher example above, here's how you could implement the same note using the C# API:
var snfApi = Helper.ModRegistry.GetApi<ISnfApi>("ichortower.SecretNoteFramework");
if (snfApi is null) {
// account for failure here, of course
}
INoteData obj = snfApi.CreateDataObject();
obj.Contents = "I sure hope nobody finds this! It's full of embarrassing secrets.";
obj.Title = "TOP SECRET DIARY";
obj.Conditions = $"PLAYER_HEARTS Current {MyNPC.InternalName} 4";
obj.ActionsOnFirstRead.Add($"AddMail Current {ModManifest.UniqueID}_Mail_HowDareYouFindMyDiary tomorrow");
snfApi.RegisterSecretNote($"{ModManifest.UniqueID}_SecretNote01", obj);Remember that you will have to do this work no earlier than GameLaunched. If
you need to register after the notes asset has already been requested and
loaded, you will need to invalidate the asset (or call the API's Reload(),
which will invalidate and request it) in order to see your notes.
You can create image secret notes (like the picture of Marnie, or the secret
dig locations) by specifying any value 0 or greater for
NoteImageTextureIndex in your note's data. When this value is >= 0, the
NoteImageTexture (or the vanilla texture, if unspecified) will be loaded, and
this offset in the texture will be displayed (this applies to both the hover
tooltip and the inside of the letter). This image behaves exactly like the
vanilla secret notes texture (Data/SecretNotesImages), as follows.
(Image notes can also include a small amount of text; see Combined Notes)
Each note image in the texture is 64x64 pixels, the same size as a character portrait. They are read left-to-right and top-to-bottom, like this:
0 1 2 3
4 5 6 7
8 9 10 11
etc.
The recommended minimum size for this image is 256x128 (four columns and two rows), because the LetterViewerMenu loads the texture for the piece of tape from this asset at the hardcoded offset (193, 65) and size (14, 21). In the layout above, this corresponds to index 7: if your image is wider, the affected index will change accordingly.
This means that you should not use that index for your notes, and should instead draw your tape image there (consult the vanilla asset for reference). Of course, if you don't want the tape piece to appear on your notes, you can leave that area blank or make your texture smaller.
Here's how you might set up an image note via Content Patcher:
{
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
"Action": "EditData",
"Entries": {
"{{ModId}}_SecretNote_TreasureMap": {
"Title": "Blackgull's Map",
"NoteImageTexture": "Mods/{{ModId}}/SecretNotesImages",
"NoteImageTextureIndex": 3
}
}
},
{
"Target": "Mods/{{ModId}}/SecretNotesImages",
"Action": "Load",
"FromFile": "assets/{{TargetWithoutPath}}.png"
}Combined notes are image notes which also include text. To
define one, simply set NoteImageTextureIndex to a value 0 or greater, and
also include a Contents field. When both fields are set, the letter will
render with an enclosed image, as above, and the first two lines' worth of text
from Contents will be drawn above and below the image.
Note: the text portion of combined notes is not rendered in the note's hover tooltip in the collections page. It is drawn only in the letter view itself.
You can draw only the leading line by keeping your Contents field short; if
there isn't enough text for a second line, it won't be drawn. Likewise, if you
want only the trailing line, you can write a short line and start it with a
mail-formatted line break ^, so that the first line is empty.
Here's how the previous image note example might look if it also included text:
{
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
"Action": "EditData",
"Entries": {
"{{ModId}}_SecretNote_TreasureMap": {
"Title": "Blackgull's Map",
"Contents": "^Good luck findin' this one, matey!",
"NoteImageTexture": "Mods/{{ModId}}/SecretNotesImages",
"NoteImageTextureIndex": 3
}
}
}Using custom items for your secret notes lets you add a little extra je ne
sais quoi to your mod, and it helps your notes stand out from the vanilla
notes as well as those added by other mods. You can use as many different
ObjectIds as you want, creating groups of notes with related meaning.
Adding an item to accompany your note is pretty simple. Here's a Content Patcher example adding a note which can be found after earning the Sous Chef achievement. When found, it gives the player a new cooking recipe:
{
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
"Action": "EditData",
"Entries": {
"{{ModId}}_Note_CookingSecrets": {
"Contents": "YOUR TEXT HERE: explain the top-secret cooking knowledge",
"Title": "Cooking Secrets",
"ObjectId": "(O){{ModId}}_Object_CookingSecrets",
"Conditions": "PLAYER_HAS_ACHIEVEMENT Current 16", // Sous Chef
"ActionsOnFirstRead": [
"MarkCookingRecipeKnown Current {{ModId}}_SecretFamilyRecipe"
]
}
}
},
{
"Target": "Data/Objects",
"Action": "EditData",
"Entries": {
"{{ModId}}_Object_CookingSecrets": {
"Name": "TornPageCookingSecrets",
"DisplayName": "[LocalizedText Strings\\Objects:{{ModId}}_Object_CookingSecrets_Name]",
"Description": "[LocalizedText Strings\\Objects:{{ModId}}_Object_CookingSecrets_Description]",
"Type": "asdf",
"Category": 0,
"Price": 1,
"Texture": "Mods/{{ModId}}/TornPageCookingSecrets",
"SpriteIndex": 0,
"Edibility": -300
}
}
},
{
"Target": "Strings/Objects",
"Action": "EditData",
"Entries": {
"{{ModId}}_Object_CookingSecrets_Name": "Torn Cookbook Page",
"{{ModId}}_Object_CookingSecrets_Description": "It's a page torn from an old cookbook. It's in bad shape, but still legible."
}
},
{
"Target": "Mods/{{ModId}}/TornPageCookingSecrets",
"Action": "Load",
"FromFile": "assets/{{TargetWithoutPath}}.png"
}I don't think your objects are required to be of "Type": "asdf" and
"Category": 0, but that's how the vanilla secret note items are and I
recommend copying them.
When adding notes, it is recommended to either omit ObjectId (and let the
framework use its own default object) or to use the ID of an object you are
adding to the game. When an object is connected to notes via ObjectId, this
framework automatically checks for the object id in its postfix patch to the
method Object.performUseAction, triggering the addition of the note to your
collection if it finds a match. This is why you should not use existing items:
they may already have code attached to them, and the note check may never be
run as a result.
Broadly, the notes added to the Mods/ichortower.SecretNoteFramework/Notes
asset behave in the same way that vanilla secret notes
do; this section explains what
that means in detail.
This mod adds a subsequent check for modded notes which is performed only after the base game has already attempted to spawn a note. If a vanilla note was spawned, there is a 50% chance that this mod's check will attempt to replace it, or else do nothing. If no vanilla note was spawned, the check proceeds normally but is less likely to succeed (the goal here is to avoid increasing the frequency of generated notes too much).
The check has the same chance as the vanilla notes, but taking into account
only notes which are available to spawn (based on their Conditions and
Location/LocationContext fields): a linear scale, from 80% if none have
been found to 12% if only one remains unseen. If not rolling to replace a
vanilla note, the starting chance is cut in half, so the range becomes 40% to
12%.
When a note is spawned, its ObjectId field is checked to generate the
inventory item. Like with vanilla secret notes, the note has not truly been
selected yet: that occurs only when the item is used, to read the note. On use,
the note item will randomly choose from unread notes that use its ID (i.e. the
set of notes that have this item for their ObjectId).
If no note is available to read when the item is used, it will disappear from inventory and display a message (in English, it's "The note crumbled to dust..."). This shouldn't happen unless the player cheated the note items into their inventory; or if they found a note and didn't use it for a while, and in the meantime all notes of its type became unavailable; or something of that nature.
Notes seen by each player are saved in the Farmer's modData, under the following key:
ichortower.SecretNoteFramework/NotesSeen
When opening the collections menu, this mod adds any notes the player has seen (drawn normally) and any notes that are eligible to spawn (grayed out), just like vanilla secret notes. Notes which are not eligible to spawn and have not been seen will not appear.
There is no specific limit to the number of notes that can be added. The code which adds the modded notes to the Collections menu accounts for pagination, so if you have a lot of modded notes, more pages will be added as needed, just like the Mail tab.
If you need to know whether a given note has been read, you can use the following game state query added by this mod:
ichortower.SecretNoteFramework_PLAYER_HAS_MOD_NOTE <player> <note_id>
Like most game state queries, the <player> argument can be any specified
player:
Any, All, Current, Host, Target, or a unique multiplayer ID.
This query is specific to notes added via this framework, since they are stored separately from the vanilla notes (in the farmer's modData, instead of in the dedicated secret notes field).
The expected use is to create note sequences, by making each later note require the previous one, or to gate a set of notes behind having first acquired a "key" note, or similar; but you can use it in any situation, like shop conditions, or whether a character attends your wedding, or anything else that strikes your fancy. Just remember that note conditions are only evaluated at the start of each day, so note chains will require multiple days to complete.
For example, two notes might look like this:
{
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
"Action": "EditData",
"Entries": {
"{{ModId}}_SecretNote_Part1": {
"Contents": "Part 1 of my debut mystery novel!",
"Title": "Mystery Part 1"
},
"{{ModId}}_SecretNote_Part2": {
"Contents": "And now, the thrilling conclusion!",
"Title": "Mystery Part 2",
"Conditions": "ichortower.SecretNoteFramework_PLAYER_HAS_MOD_NOTE Current {{ModId}}_SecretNote_Part1"
},
}
}With this setup, the first note is available as soon as the player has access to Secret Notes, but the second one is not; starting from the next day after finding the first one, the second becomes available.
This mod also adds a Content Patcher token:
{{ichortower.SecretNoteFramework/HasModNote}}
This token works just like {{HasFlag}}: it returns a comma-separated list of
all modded notes seen by the current player (at this time, no other specified
players are supported. I may add this in the future, if it's useful and
feasible). You can use it in the same way:
"When": {
"ichortower.SecretNoteFramework/HasModNote": "MyNoteId"
}
"When": {
"ichortower.SecretNoteFramework/HasModNote |contains=MyNoteId": true
}Be mindful of your patch's update rate, as usual, when relying on this token.
This mod adds a trigger action which you can use to mark notes as seen (or unseen) directly, without the player having to find the note or use the item.
ichortower.SecretNoteFramework_MarkModNoteSeen <player> <note id> [true/false]
<player> should be one of Current, Host, All, or a unique multiplayer
ID. The third argument is optional and may be set to false in order to mark a
note as unread instead of as read (removing it from the player's collection,
instead of adding it).
Like the GSQ and the CP token, this trigger works exclusively on modded notes and will not affect vanilla ones.
Important Note: when you use this trigger action to mark a note as read,
its actions listed under ActionsOnFirstRead will not be executed. If you
need to execute them, you should rely on the player to find and read the note,
or you should duplicate them in the context you are using to run this action.
Likewise, marking a note as unread will allow it to be collected again, which
will cause its ActionsOnFirstRead to execute an additional time.
This mod includes a SMAPI console command which is intended to help authors
iterate quickly when creating notes, much like Content Patcher's patch reload
and patch update. It is:
snf_reload <target>
Where <target> should be one of data, check, or full (or help, or
omit it, in order to see the usage notes directly in the console).
- data: cache-invalidate and reload the notes data asset.
- check: reevaluate the
Conditionsfields on all notes, rebuilding the list of notes eligible to spawn. - full: reload the notes data asset, then reevaluate note conditions (like running "data" followed by "check").