Skip to content

Commit 4388665

Browse files
committed
Merge branch 'release-1.1.0' into prod
2 parents 92e8a09 + ed92c70 commit 4388665

File tree

5 files changed

+172
-37
lines changed

5 files changed

+172
-37
lines changed

SecretNoteFramework.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<RootNamespace>ichortower.SecretNoteFramework</RootNamespace>
4-
<Version>1.0.0</Version>
4+
<Version>1.1.0</Version>
55
<TargetFramework>net6.0</TargetFramework>
66
<Configuration>Release</Configuration>
77
<EnableHarmony>true</EnableHarmony>

docs/author-guide.md

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Stardew Valley.
99
* [Adding Notes](#adding-notes)
1010
* [Content Patcher example](#content-patcher-example)
1111
* [Image Notes](#image-notes)
12+
* [Combined Notes](#combined-notes)
1213
* [Custom Items](#custom-items)
1314
* [How Modded Notes Work](#how-modded-notes-work)
1415
* [Spawning](#spawning)
@@ -31,18 +32,19 @@ a few advanced things, like:
3132

3233
* specify complex eligibility conditions on a note-by-note basis with game
3334
state queries
34-
* determine which location context(s) your notes appear in, so you can (e.g.)
35-
restrict your mod's notes to spawn only in your mod's area
35+
* declare which specific locations (or location contexts) your notes appear
36+
in, so you can (e.g.) restrict your mod's notes to spawn only in your areas
3637
* use different (custom) items to represent different sets of notes, as
3738
desired
3839
* specify your own assets for note content and formatting, including image
3940
notes
41+
* create notes with an image as well as a small amount of text
4042
* set any number of trigger actions to be run when a note is first read
4143

4244
Let's begin!
4345

44-
(**Please note**: I recommend using i18n for text content in your mods. The
45-
examples in this guide omit its use, for clarity of purpose.)
46+
(**Please note**: although I recommend using i18n for text content in your
47+
mods, the examples in this guide omit its use, for clarity of purpose.)
4648

4749

4850
## Adding Notes
@@ -80,6 +82,13 @@ codes](https://stardewvalleywiki.com/Modding:Mail_data#Custom_mail_formatting)
8082
`[letterbg]` and `[textcolor]`, but it is preferred to use the other fields for
8183
that instead (the fields will supersede the in-band format codes).
8284

85+
Unlike vanilla notes, with Secret Note Framework you can include some text in
86+
image notes (see `NoteImageTextureIndex`, which is used to control whether a
87+
note displays an enclosed image). When `NoteImageTextureIndex` is >= 0, the
88+
`Contents` field will still be used, but is limited to two lines of text,
89+
displayed above and below the image. See [Combined Notes](#combined-notes) for
90+
more information.
91+
8392
*Default:* `""`
8493

8594
</td>
@@ -123,22 +132,46 @@ note has been seen by a player, which is useful for ordering your notes.
123132
</td>
124133
</tr>
125134

135+
<tr>
136+
<td><code>Location</code></td>
137+
<td>string</td>
138+
<td>
139+
140+
This string specifies one or more location names where the note is able to
141+
appear: except in the specified locations, the note will not spawn. This allows
142+
you to limit the areas where your note can be found even more narrowly than
143+
with `LocationContext` (but see that field for an alternative).
144+
145+
Specify any number of location names, separated by commas (e.g. `Town, Forest,
146+
{{ModId}}_MyMap`), to allow a note to spawn in any of them.
147+
148+
If this field is specified, it will supersede `LocationContext`; they cannot be
149+
combined.
150+
151+
*Default:* `null`
152+
153+
</td>
154+
</tr>
155+
126156
<tr>
127157
<td><code>LocationContext</code></td>
128158
<td>string</td>
129159
<td>
130160

131161
This string specifies one or more location contexts where the note is able to
132-
appear. Vanilla journal scraps spawn only in the `Island` context (i.e. on
133-
Ginger Island); secret notes spawn anywhere else. You can specify any value(s)
134-
here, including modded contexts, so for example if a mod adds a mountain area
135-
with a separate context, you can define notes which spawn only there.
162+
appear. In vanilla, journal scraps spawn only in the `Island` context (i.e. on
163+
Ginger Island), and secret notes spawn anywhere else. You can specify any
164+
value(s) here, including modded contexts, so for example if a mod adds a
165+
mountain area with a separate context, you can define notes which spawn only
166+
there.
136167

137168
Specify any number of context names, separated by commas (e.g. `Default,
138169
Desert`), to allow a note to spawn in any of them. Alternately, you can specify
139170
one context name preceded by a `!` to specify any location *except* that one:
140171
the default value is `!Island`, mimicking vanilla's secret notes.
141172

173+
If `Location` is specified, this field will be ignored.
174+
142175
*Default:* `!Island`
143176

144177
</td>
@@ -156,9 +189,9 @@ matching it will be read (see [Spawning](#spawning), under how notes work, for
156189
more details). This item's sprite will also be displayed in the collections
157190
page to represent this note, either grayed out (unread) or normal (read).
158191

159-
If this field is `null` (the default), the mod will use its default secret note
160-
object. For best results, you should leave it as default or specify a [custom
161-
item](#custom-items).
192+
If this field is `null` (the default), the framework will use its default
193+
secret note object. For best results, you should leave it as default or specify
194+
a [custom item](#custom-items).
162195

163196
*Default:* `null`
164197

@@ -214,6 +247,9 @@ any of the 10 acceptable vanilla color names:
214247
"NoteTextColor": "rgb(88, 34, 44)",
215248
```
216249

250+
Note that the text in mail and secret notes is rendered at 75% opacity, so the
251+
color you indicate here will blend slightly with the background texture.
252+
217253
*Default:* `null`
218254

219255
</td>
@@ -225,11 +261,12 @@ any of the 10 acceptable vanilla color names:
225261
<td>
226262

227263
A game asset path indicating the texture to use when loading an image for an
228-
[image note](#image-notes) (see that section for more details).
229-
Note images are 64x64 pixels and are read in order, left-to-right and
230-
top-to-bottom, just like other spritesheets; but be aware that there is a
231-
hardcoded offset for the image of the piece of tape holding the image inside
232-
the note (193/65, 14x21), so you should not use the index containing that.
264+
[image note](#image-notes) or [combined note](#combined-notes) (see those
265+
sections for more details). Note images are 64x64 pixels and are read in
266+
order, left-to-right and top-to-bottom, just like other spritesheets; but be
267+
aware that there is a hardcoded offset for the image of the piece of tape
268+
holding the image inside the note (193/65, 14x21), so you should not use the
269+
index containing that.
233270

234271
If `null`, the default secret notes image texture
235272
(`TileSheets/SecretNotesImages.png`) will be used.
@@ -247,8 +284,12 @@ If `null`, the default secret notes image texture
247284
An integer specifying the index in the `NoteImageTexture` to use for the note
248285
image. Unlike `NoteTextureIndex`, the default value here is `-1`, since the
249286
LetterViewerMenu itself uses this value to control rendering; as a result,
250-
**this value controls whether your note is an image note (>= 0) or a text note
251-
(-1)**.
287+
**this value controls whether your note is an image or combined note (>= 0) or
288+
a text note (-1)**.
289+
290+
To display just an image in your note, set this to a value >= 0 and also omit
291+
the `Contents` field; for a combined note, also include `Contents` (but mind
292+
the limit on length in this case).
252293

253294
*Default:* `-1`
254295

@@ -323,6 +364,9 @@ this offset in the texture will be displayed (this applies to both the hover
323364
tooltip and the inside of the letter). This image behaves exactly like the
324365
vanilla secret notes texture (`Data/SecretNotesImages`), as follows.
325366

367+
(Image notes can also include a small amount of text; see [Combined
368+
Notes](#combined-notes))
369+
326370
Each note image in the texture is 64x64 pixels, the same size as a character
327371
portrait. They are read left-to-right and top-to-bottom, like this:
328372

@@ -366,6 +410,41 @@ Here's how you might set up an image note via Content Patcher:
366410
}
367411
```
368412

413+
### Combined Notes
414+
415+
Combined notes are [image notes](#image-notes) which also include text. To
416+
define one, simply set `NoteImageTextureIndex` to a value 0 or greater, and
417+
also include a `Contents` field. When both fields are set, the letter will
418+
render with an enclosed image, as above, and the first two lines' worth of text
419+
from `Contents` will be drawn above and below the image.
420+
421+
**Note**: the text portion of combined notes **is not rendered in the note's
422+
hover tooltip** in the collections page. It is drawn only in the letter view
423+
itself.
424+
425+
You can draw only the leading line by keeping your `Contents` field short; if
426+
there isn't enough text for a second line, it won't be drawn. Likewise, if you
427+
want only the trailing line, you can write a short line and start it with a
428+
mail-formatted line break `^`, so that the first line is empty.
429+
430+
Here's how the previous image note example might look if it also included text:
431+
432+
```js
433+
{
434+
"Target": "Mods/ichortower.SecretNoteFramework/Notes",
435+
"Action": "EditData",
436+
"Entries": {
437+
"{{ModId}}_SecretNote_TreasureMap": {
438+
"Title": "Blackgull's Map",
439+
"Contents": "^Good luck findin' this one, matey!",
440+
"NoteImageTexture": "Mods/{{ModId}}/SecretNotesImages",
441+
"NoteImageTextureIndex": 3
442+
}
443+
}
444+
}
445+
```
446+
447+
369448
### Custom Items
370449

371450
Using custom items for your secret notes lets you add a little extra *je ne

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"Name": "Secret Note Framework",
33
"Author": "ichortower",
44
"Description": "A framework allowing mods to add their own Secret Notes.",
5-
"Version": "1.0.0",
5+
"Version": "1.1.0",
66
"UniqueID": "ichortower.SecretNoteFramework",
77
"EntryDll": "SecretNoteFramework.dll",
88
"MinimumApiVersion": "4.0.0",

src/Patches.cs

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using StardewModdingAPI;
55
using StardewModdingAPI.Events;
66
using StardewValley;
7+
using StardewValley.BellsAndWhistles;
78
using StardewValley.Extensions;
89
using StardewValley.Menus;
910
using StardewValley.Triggers;
@@ -24,17 +25,20 @@ public static void Apply()
2425
Harmony harmony = new(SNF.ModId);
2526

2627
PatchMethod(harmony, typeof(Object),
27-
nameof(Object.performUseAction),
28+
nameof(Object.performUseAction), null,
2829
nameof(Patches.Object_performUseAction_Postfix));
2930
PatchMethod(harmony, typeof(GameLocation),
30-
nameof(GameLocation.tryToCreateUnseenSecretNote),
31+
nameof(GameLocation.tryToCreateUnseenSecretNote), null,
3132
nameof(Patches.GameLocation_tryToCreateUnseenSecretNote_Postfix));
3233
PatchMethod(harmony, typeof(CollectionsPage),
33-
nameof(CollectionsPage.receiveLeftClick),
34+
nameof(CollectionsPage.receiveLeftClick), null,
3435
nameof(Patches.CollectionsPage_receiveLeftClick_Postfix));
3536
PatchMethod(harmony, typeof(CollectionsPage),
36-
nameof(CollectionsPage.performHoverAction),
37+
nameof(CollectionsPage.performHoverAction), null,
3738
nameof(Patches.CollectionsPage_performHoverAction_Transpiler));
39+
PatchMethod(harmony, typeof(IClickableMenu),
40+
nameof(IClickableMenu.draw), new[]{typeof(SpriteBatch)},
41+
nameof(Patches.IClickableMenu_draw_Postfix));
3842
ConstructorInfo collectionspage_ctor = typeof(CollectionsPage)
3943
.GetConstructor(new[]{typeof(int), typeof(int), typeof(int), typeof(int)});
4044
harmony.Patch(original: collectionspage_ctor,
@@ -43,7 +47,8 @@ public static void Apply()
4347
}
4448

4549
// only suitable for unambiguous method names
46-
private static void PatchMethod(Harmony harmony, Type t, string name, string patch)
50+
private static void PatchMethod(Harmony harmony, Type t, string name,
51+
Type[] argTypes, string patch)
4752
{
4853
string[] parts = patch.Split("_");
4954
string last = parts[parts.Length-1];
@@ -52,9 +57,18 @@ private static void PatchMethod(Harmony harmony, Type t, string name, string pat
5257
return;
5358
}
5459
try {
55-
MethodInfo m = t.GetMethod(name,
56-
BindingFlags.Public | BindingFlags.NonPublic |
57-
BindingFlags.Instance | BindingFlags.Static);
60+
MethodInfo m;
61+
if (argTypes is null) {
62+
m = t.GetMethod(name,
63+
BindingFlags.Public | BindingFlags.NonPublic |
64+
BindingFlags.Instance | BindingFlags.Static);
65+
}
66+
else {
67+
m = t.GetMethod(name,
68+
BindingFlags.Public | BindingFlags.NonPublic |
69+
BindingFlags.Instance | BindingFlags.Static,
70+
null, argTypes, null);
71+
}
5872
HarmonyMethod func = new(typeof(Patches), patch);
5973
if (last == "Prefix") {
6074
harmony.Patch(original: m, prefix: func);
@@ -140,18 +154,27 @@ public static void GameLocation_tryToCreateUnseenSecretNote_Postfix(
140154
if (__result != null && Game1.random.NextBool()) {
141155
return;
142156
}
143-
string context = __instance.GetLocationContextId().ToLower();
157+
string locContext = __instance.GetLocationContextId().ToLower();
158+
string locName = __instance.Name.ToLower();
159+
// eligible is recorded separately from unseen, because the ratio
160+
// determines the spawn chance. that's why the HasNote condition
161+
// isn't inside the first Where here
144162
var eligible = SecretModNotes.Data.Where((kvp) => {
145163
if (!SecretModNotes.AvailableNoteIds.Contains(kvp.Key)) {
146164
return false;
147165
}
148-
string lc = kvp.Value.LocationContext;
149-
if (lc.StartsWith("!")) {
150-
return context != lc.Substring(1).Trim().ToLower();
166+
string startValue = kvp.Value.Location;
167+
string checkValue = locName;
168+
if (String.IsNullOrEmpty(startValue)) {
169+
startValue = kvp.Value.LocationContext;
170+
if (startValue.StartsWith("!")) {
171+
return locContext != startValue.Substring(1).Trim().ToLower();
172+
}
173+
checkValue = locContext;
151174
}
152-
string[] locs = lc.Split(",")
175+
string[] validValues = startValue.Split(",")
153176
.Select(s => s.Trim().ToLower()).ToArray();
154-
return Array.IndexOf(locs, context) >= 0;
177+
return Array.IndexOf(validValues, checkValue) >= 0;
155178
}).ToList();
156179
var unseen = eligible.Where(kvp => !ModData.HasNote(who, kvp.Key)).ToList();
157180
int notesHeld = 0;
@@ -240,9 +263,12 @@ private static string makeHoverText(SecretModNoteData noteData)
240263
" " + noteData.NoteImageTextureIndex +
241264
" " + (noteData.Title ?? "???");
242265
}
243-
string parsedText = Game1.parseText(Utility.ParseGiftReveals(noteData.Contents)
266+
string subbedText = Dialogue.applyGenderSwitchBlocks(
267+
Game1.player.Gender, noteData.Contents);
268+
subbedText = Utility.ParseGiftReveals(subbedText)
244269
.TrimStart(' ', '^').Replace("^", Environment.NewLine)
245-
.Replace("@", Game1.player.Name), Game1.smallFont, 512);
270+
.Replace("@", Game1.player.Name);
271+
string parsedText = Game1.parseText(subbedText, Game1.smallFont, 512);
246272
string[] split = parsedText.Split(Environment.NewLine);
247273
int max = 15;
248274
if (split.Length > max) {
@@ -272,7 +298,7 @@ private static void resetNoteTexture(CollectionsPage instance)
272298
private static string applyHoverText(CollectionsPage instance, string text)
273299
{
274300
string[] split = ArgUtility.SplitBySpaceQuoteAware(text);
275-
if (!split[0].Equals("!image")) {
301+
if (!split[0].Equals("!image", StringComparison.OrdinalIgnoreCase)) {
276302
return text;
277303
}
278304
int index = -1;
@@ -338,6 +364,35 @@ public static IEnumerable<CodeInstruction>
338364
}
339365

340366

367+
public static void IClickableMenu_draw_Postfix(
368+
IClickableMenu __instance,
369+
SpriteBatch b)
370+
{
371+
if (__instance is LetterViewerMenu menu &&
372+
menu.secretNoteImage != -1 &&
373+
menu.mailMessage.Count > 0) {
374+
// TODO find a better way than "+8" to get this value.
375+
// using exactly the getHeight value doesn't work; probably
376+
// exactly the size or oboe, so the split fails to get text
377+
int linecap = SpriteText.getHeightOfString("Ag") + 8;
378+
List<string> lines = SpriteText.getStringBrokenIntoSectionsOfHeight(
379+
menu.mailMessage[0], menu.width - 64, linecap);
380+
SpriteText.drawStringHorizontallyCenteredAt(b, lines[0],
381+
menu.xPositionOnScreen + menu.width/2,
382+
menu.yPositionOnScreen + 32 + linecap,
383+
alpha: .75f, layerDepth: .867f,
384+
color: menu.getTextColor());
385+
if (lines.Count > 1) {
386+
SpriteText.drawStringHorizontallyCenteredAt(b, lines[1],
387+
menu.xPositionOnScreen + menu.width/2,
388+
menu.yPositionOnScreen + menu.height - 32 - linecap*2,
389+
alpha: .75f, layerDepth: .867f,
390+
color: menu.getTextColor());
391+
}
392+
}
393+
}
394+
395+
341396
public static void CollectionsPage_receiveLeftClick_Postfix(
342397
CollectionsPage __instance,
343398
int x, int y)

src/SecretModNotes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ internal class SecretModNoteData
122122
public string Contents = "";
123123
public string Title = null;
124124
public string Conditions = null;
125+
public string Location = null;
125126
public string LocationContext = "!Island";
126127
public string ObjectId = null;
127128
public string NoteTexture = null;

0 commit comments

Comments
 (0)