Stardew Valley

File information

Last updated

Original upload

Created by

ncarigon

Uploaded by

ncarigon

Virus scan

Safe to use

About this mod

Allows creating custom bush blooming schedules to support whatever seasons, days, locations, and items you may want.

Requirements
Permissions and credits
Changelogs
Do you like running around areas clicking on colorful bushes and getting free items? Do you wish you could do that more often, and get things other than Salmonberries and Blackberries?

Me too!

This mod doesn't really do that, but it allows you (or someone else) to make that dream come alive with content packs.

This started as a way to get Juniper Berries to bloom in my other mod Copper Still and grew into a generic framework over time.

General features:
  • Create your own bush blooming schedules;
  • Shake off whatever item you choose;
  • Supports multiple schedules active at the same time;
  • Supports custom berry textures for each schedule;
  • Supports custom blooming chance;
  • Supports only blooming in specific locations, by inclusion or exclusion;
  • Supports only blooming in specific weather, by inclusion or exclusion (or destroying existing bloom);
  • Schedules can be as short as a single day, or as long as all year;
  • Supports only blooming in specific years;
  • Supports only blooming on specific tiles;
  • Allows other mods to check for custom blooming schedules;
  • Config options to toggle some features depending on your other mods.
  • Built-in support for Deluxe Grabber Redux (disabled until it gets updated for 1.6)
  • Built-in support for Almanac (disabled until it gets updated for 1.6)
  • Built-in support for Automate (Automate now natively supports custom shake off items)
  • *New in 1.2.0* Now supports content loaded and/or edited via Content Patcher (see example mods in zip)

Configuration options:
You can edit these configuration files in-game using the Generic Mod Config Menu interface, or directly in the config.json file.

Enable Default Blooming: (true|false) Enable the game's default spring and fall bush blooming schedules. Disabling them does not modify any other game logic, bundles, quests, or storytelling in the game. Do so at your own risk!

Use Spring Bush for Summer: (true|false) The default summer blooming bush does not have berries. This reuses the spring bush to accommodate blooming in summer. Disable if you are using another mod that adds a summer berry bush sprite.

Use Custom Winter Berry: (true|false) The default winter blooming bush does not exist. This uses a custom sprite to accommodate blooming in winter. Disable if you are using another mod that adds a winter blooming bush sprite.
                   
Support Deluxe Grabber Redux: (true|false) When using that mod, allows the grabber to harvest custom items from bushes.

Support Almanac: (true|false) When using that mod, allows the almanac to show custom gathering schedules. When enabled, it's highly recommended to disable that mod's option 'Page: Local Notices' > 'Show Gathering' in order to not show duplicates of the default gathering schedules.


Support Automate: (true|false) When using that mod, allows chests to harvest custom items from bushes.

Content pack details:
This mod includes an example directory that contains a manifest.json, content.json, and assets/berry.png. These can serve as a template for creating additional content packs. Below are some examples of what the content.json can contain.

You can also review my other mod as an example: Ore Berries

Example full schedule config:

[{
// Optional; Default is true if missing
"Enabled": true,

    // Optional: Default will be 'ModDirectoryName/StartSeason_ShakeOff'
    "Id": "[BBM] Bush Bloom Example/spring_0",

// Required; Value can be the item ID or Name, but must exist
"ShakeOff": 0,

// Required; Value must be 'spring', 'summer', 'fall', or 'winter'
"StartSeason": "spring",

// Required; Value must be from 1 to 28
"StartDay": 1,

// Optional; Default is StartSeason value if missing
"EndSeason": "winter",

// Optional; Default is StartDay value if missing
"EndDay": 28,

   // Optional; Default is first year
   "StartYear": 1,

   // Optional: Default is no year
   "EndYear": 2,

// Optional; Default is built-in texture if missing
"Texture": "assets\\berry.png", 

    // Optional; Default is 0.2 if missing; Values between 0.0 and 1.0 (inclusive)
    //           determine the overall probability. A value greater than 1.0 will
    //           guarantee that item will bloom, regardless of any existing bloom
    //           or other possible schedules unless they are also greater than 1.0.
"Chance": 0.2,

// Optional; Default is all locations if missing or empty
"Locations": [ "Forest", "Mountain" ],

// Optional: Default is no locations if missing or empty
"ExcludeLocations": [ "Town" ],

   // Optional; Default is all weather if missing or empty
   "Weather": [ "Rain", "GreenRain", "Sun", "Wind" ],

   // Optional: Default is no weather if missing or empty
   "ExcludeWeather": [ "Storm", "Festival", "Wedding" ],

   // Optional: Default is no weather if missing or empty
   "DestroyWeather": [ "Snow" ],

    // Optional: Restrict blooming to only bushes with the given tile locations.
    //           Most effective when combined with a specific "Locations" setting
    //           in order to bloom a *very* specific set of bushes.
    "Tiles": [
        { "X": 10, "Y": 20 },
        { "X": 23, "Y": 32 }
    ]
}]


Example minimal default schedules:

[{
"ShakeOff": 296,
"StartSeason": "spring",
"StartDay": 15,
"EndDay": 18
}, {
"ShakeOff": 410,
"StartSeason": "fall",
"StartDay": 8,
"EndDay": 11
}]

Content Patcher can also be used to modify schedules or textures, such as:

{
"Format": "2.0.0",
"Changes": [
{
// This example will modify the existing default Fall blackberry
// schedule to guarantee blooming if the spirites are very happy
"Action": "EditData",
"Target": "NCarigon.BushBloomMod/Schedules",
"Fields": {
// Entry ID from BBM content packs will be formatted as:
//     ModDirectoryName/StartSeason_ShakeOff
// unless given specific Id within their configuration
"default/fall_410": {
"Chance": 1.0
}
},
"When": {
// Can use CP Tokens to dynamically modify schedules too.
// REF: https://github.com/Pathoschild/StardewMods/blob/develop/ContentPatcher/docs/author-guide/tokens.md
"Query: {{DailyLuck}} > 0.07": true // spirits are very happy today
}
},
{
// This example will change the default Fall blackberry blooming berry image
"Action": "EditImage",
"Target": "NCarigon.BushBloomMod/Textures/default/fall_410",
// Change the blooming to a different image
"FromFile": "assets/berry.png"
}
]
}

You can also create an entirely new schedule via Content Patcher, such as:

{
"Format": "2.0.0",
"Changes": [
{
"Action": "EditData",
"Target": "NCarigon.BushBloomMod/Schedules",
"Entries": {
// Id here *should* match the default of 'ModDirectoryName/StartSeason_ShakeOff'
// but can technically be anything you want. Can also overwrite existing
// schedules by using the same id.
"CPExampleBBM/spring_0": {
"ShakeOff": 0,
"StartSeason": "spring",
"StartDay": 1
}
}
},
{
"Action": "EditImage",
"Target": "NCarigon.BushBloomMod/Textures/CPExampleBBM/spring_0",
"FromFile": "assets/berry.png"
}
]
}

About Schedule ID Generation:
When no schedule ID is provided, the default behavior is to generate an ID from the fields, such as this:

ModDirectoryName/StartSeason_ShakeOff
In a situation where multiple schedules would generate the same ID, an incrementing suffix will be added for each past the first entry, such as:

ModDirectoryName/StartSeason_ShakeOff_1
This is particularly noteworthy when it comes to dynamically modifying schedules via Content Patcher, where you need to accurately know the ID to modify.

Integration details:
If you are making a different mod and want to integrate with this mods features, familiarize yourself with this: Modding:Modder Guide/APIs/Integrations

To incorporate this mod's logic into your own mod, create this interface in your project, including the specific method(s) you will actually need:

public interface IBushBloomModApi {
    /// <summary>
/// Returns an array of (item_id, first_day, last_day) for all possible active blooming
/// schedules on the given season and day, optionally within the given year and/or location.
/// </summary>
public (string, WorldDate, WorldDate)[] GetActiveSchedules(string season, int dayofMonth, int? year = null, GameLocation? location = null, Vector2? tile = null);

    /// <summary>
    /// Returns an array of (item_id, first_day, last_day) for all blooming schedules.
    /// </summary>
    public (string, WorldDate, WorldDate)[] GetAllSchedules();

    /// <summary>
    /// Clear and reparse all schedules.
    /// </summary>
    public void ReloadSchedules();

    /// <summary>
    /// Specifies whether BBM successfully parsed all schedules.
    /// </summary>
    public bool IsReady();

    /// <summary>
    /// Performs the general operations of the Bush.shake() function without all the player, debris,
    /// and UI logic. Namely, this will return an item ID if the bush is in bloom and mark the bush
    /// as no longer blooming. You must create the item and handle any operations needed for it.
    /// </summary>
    public string FakeShake(Bush bush);
}


Then call for the API in your Entry method:

var bbm = helper.ModRegistry.GetApi<IBushBloomModApi>("NCarigon.BushBloomMod");
You will need to update your bush checking logic to include calling the API.

For example, if your original method is:

private void Harvest(Bush bush) {
var season = (bush.overrideSeason.Value == -1) ? Game1.GetSeasonForLocation(bush.currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value);
if (bush.size.Value == 1 && bush.inBloom(season, Game1.dayOfMonth)) {
var shakeOff = -1;
if (season == "spring") shakeOff = 296; //Salmonberry
else if (season == "fall") shakeOff = 410; //Blackberry;
if (shakeOff != -1) {
bush.tileSheetOffset.Value = 0;
Game1.player.addItemToInventory(new SObject(shakeOff, 1));
}
}
}


***NEW*** Version 1.6 of Stardew Valley greatly simplifies this:

private void Harvest(Bush bush) {
    if (bbm is not null) {
        var shakeOff = bbm.FakeShake(bush);
        if (shakeOff != null) {
            Game1.player.addItemToInventory(new SObject(shakeOff, 1));
        }
    } else {
        var season = (bush.overrideSeason.Value == -1)
            ? Game1.GetSeasonForLocation(bush.currentLocation)
            :Utility.getSeasonNameFromNumber(bush.overrideSeason.Value);
    if (bush.size.Value == 1 && bush.inBloom(season, Game1.dayOfMonth)) {
    var shakeOff = -1;
    if (season == "spring") shakeOff = 296; //Salmonberry
    else if (season == "fall") shakeOff = 410; //Blackberry;
    if (shakeOff != -1) {
    bush.tileSheetOffset.Value = 0;
    Game1.player.addItemToInventory(new SObject(shakeOff, 1));
    }
    }
    }
}


For another example, if you want to enumerate the blooming items for a given date, your original code might look like this:

private IEnumerable<(Item, WorldDate, WorldDate)> GetBloomSchedules(Bush bush, WorldDate date) {
if (bush.inBloom(date.Season, date.DayOfMonth)) {
int id = -1;
if (date.SeasonIndex == 0) id = 296; // Salmonberry
else if (date.SeasonIndex == 2) id = 410; // Blackberry
if (id != -1) {
if (date.DayOfMonth == 1 || !bush.inBloom(date.Season, date.DayOfMonth - 1)) {
int last = date.DayOfMonth;
for (int d = date.DayOfMonth + 1; d <= 28; d++) {
if (!bush.inBloom(date.Season, d))
break;
last = d;
}
yield return (new SObject(id, 1), new WorldDate(date.Year, date.Season, date.DayOfMonth), new WorldDate(date.Year, date.Season, last));
}
}
}
}


And the integrated code would look like this:

private IEnumerable<(Item, WorldDate, WorldDate)> GetBloomSchedules(Bush bush, WorldDate date) {
if (bbm is not null) {
foreach (var sched in bbm.GetActiveSchedules(date.Season, date.DayOfMonth)) {
yield return (new SObject(sched.Item1, 1), sched.Item2, sched.Item3);
}
} else if (bush.inBloom(date.Season, date.DayOfMonth)) {
int id = -1;
if (date.SeasonIndex == 0) id = 296; // Salmonberry
else if (date.SeasonIndex == 2) id = 410; // Blackberry
if (id != -1) {
if (date.DayOfMonth == 1 || !bush.inBloom(date.Season, date.DayOfMonth - 1)) {
int last = date.DayOfMonth;
for (int d = date.DayOfMonth + 1; d <= 28; d++) {
if (!bush.inBloom(date.Season, d))
break;
last = d;
}
yield return (new SObject(id, 1), new WorldDate(date.Year, date.Season, date.DayOfMonth), new WorldDate(date.Year, date.Season, last));
}
}
}
}


Source: ncarigon/StardewValleyMods (github.com)

My mods:
Copper Still
Garden Pot - Automate
Passable Crops
Bush Bloom Mod
Ore Berries
Tree Shake Mod
Garden Pot Options