summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets2
-rw-r--r--docs/release-notes.md24
-rw-r--r--docs/technical/mod-package.md2
-rw-r--r--src/SMAPI.Installer/assets/unix-launcher.sh46
-rw-r--r--src/SMAPI.Installer/assets/windows-install.bat4
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs408
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs2
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs2
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/Manifest.cs10
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json9
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/i18n.json7
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/manifest.json4
-rw-r--r--src/SMAPI/Constants.cs43
-rw-r--r--src/SMAPI/Events/FurnitureListChangedEventArgs.cs42
-rw-r--r--src/SMAPI/Events/IWorldEvents.cs3
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs4
-rw-r--r--src/SMAPI/Framework/Events/ModWorldEvents.cs7
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs24
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs26
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs87
-rw-r--r--src/SMAPI/Framework/StateTracking/LocationTracker.cs7
-rw-r--r--src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs4
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs50
27 files changed, 583 insertions, 248 deletions
diff --git a/build/common.targets b/build/common.targets
index d538cc53..ce805402 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.10.1</Version>
+ <Version>3.11.0</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/docs/release-notes.md b/docs/release-notes.md
index caa90d19..d0aa2fbf 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -7,6 +7,30 @@
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
-->
+## 3.11.0
+Released 09 July 2021 for Stardew Valley 1.5.4 or later. See [release highlights](https://www.patreon.com/posts/53514295).
+
+* For players:
+ * Updated for Stardew Valley 1.4.5 multiplayer hotfix on Linux/macOS.
+ * Fixed installer error on Windows when running as administrator (thanks to LostLogic!).
+ * Fixed installer error on some Windows systems (thanks to eddyballs!).
+ * Fixed error if SMAPI fails to dispose on game exit.
+ * Fixed `player_add` and `list_items` console commands not including some shirts _(in Console Commands)_.
+
+* For mod authors:
+ * Added `World.FurnitureListChanged` event (thanks to DiscipleOfEris!).
+ * Added asset propagation for building/house paint masks.
+ * Added log message for troubleshooting if Windows software which often causes issues is installed (currently MSI Afterburner and RivaTuner).
+ * Improved validation for the manifest `Dependencies` field.
+ * Fixed validation for mods with invalid version `0.0.0`.
+ * Fixed _loaded with custom settings_ trace log added when using default settings.
+ * Fixed `Constants.SaveFolderName` and `Constants.CurrentSavePath` not set correctly in rare cases.
+
+* For the web UI and JSON validator:
+ * Updated the JSON validator/schema for Content Patcher 1.23.0.
+ * Fixed [JSON schema](technical/web.md#using-a-schema-file-directly) in Visual Studio Code warning about comments and trailing commas.
+ * Fixed JSON schema for `i18n` files requiring the wrong value for the `$schema` field.
+
## 3.10.1
Released 03 May 2021 for Stardew Valley 1.5.4 or later.
diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md
index 6d04722c..91da971b 100644
--- a/docs/technical/mod-package.md
+++ b/docs/technical/mod-package.md
@@ -324,7 +324,7 @@ To do that:
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
```
-3. Replace `PATH_HERE` with your game's folder path.
+3. Replace `PATH_HERE` with your game's folder path (don't add quotes).
The configuration will check your custom path first, then fall back to the default paths (so it'll
still compile on a different computer).
diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh
index a33c0d7f..d309f750 100644
--- a/src/SMAPI.Installer/assets/unix-launcher.sh
+++ b/src/SMAPI.Installer/assets/unix-launcher.sh
@@ -43,8 +43,34 @@ if [ "$UNAME" == "Darwin" ]; then
cp -p StardewValley.bin.osx StardewModdingAPI.bin.osx
fi
+ # Make sure we're running in Terminal (so the user can see errors/warnings/update alerts).
+ # Previously we would just use `open -a Terminal` to launch the .bin.osx file, but that
+ # doesn't let us set environment variables.
+ if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123
+ # sanity check to make sure we don't have an infinite loop of opening windows
+ SKIP_TERMINAL=false
+ for argument in "$@"; do
+ if [ "$argument" == "--no-reopen-terminal" ]; then
+ SKIP_TERMINAL=true
+ break
+ fi
+ done
+
+ # reopen in Terminal if needed
+ # https://stackoverflow.com/a/29511052/262123
+ if [ "$SKIP_TERMINAL" == "false" ]; then
+ echo "Reopening in the Terminal app..."
+ echo "\"$0\" $@ --no-reopen-terminal" > /tmp/open-smapi-terminal.sh
+ chmod +x /tmp/open-smapi-terminal.sh
+ cat /tmp/open-smapi-terminal.sh
+ open -W -a Terminal /tmp/open-smapi-terminal.sh
+ rm /tmp/open-smapi-terminal.sh
+ exit 0
+ fi
+ fi
+
# launch SMAPI
- open -a Terminal ./StardewModdingAPI.bin.osx "$@"
+ LC_ALL="C" ./StardewModdingAPI.bin.osx "$@"
else
# choose binary file to launch
LAUNCH_FILE=""
@@ -79,44 +105,44 @@ else
terminal|termite)
# consumes only one argument after -e
# options containing space characters are unsupported
- exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@"
+ exec $TERMINAL_NAME -e "env TERM=xterm LC_ALL=\"C\" $LAUNCH_FILE $@"
;;
xterm|konsole|alacritty)
# consumes all arguments after -e
- exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@"
+ exec $TERMINAL_NAME -e env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
;;
terminator|xfce4-terminal|mate-terminal)
# consumes all arguments after -x
- exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@"
+ exec $TERMINAL_NAME -x env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
;;
gnome-terminal)
# consumes all arguments after --
- exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
+ exec $TERMINAL_NAME -- env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
;;
kitty)
# consumes all trailing arguments
- exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@"
+ exec $TERMINAL_NAME env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
;;
*)
# If we don't know the terminal, just try to run it in the current shell.
# If THAT fails, launch with no output.
- env TERM=xterm $LAUNCH_FILE "$@"
+ env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
- exec $LAUNCH_FILE --no-terminal "$@"
+ exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
fi
esac
## terminal isn't executable; fallback to current shell or no terminal
else
echo "The '$TERMINAL_NAME' terminal isn't executable. SMAPI might be running in a sandbox or the system might be misconfigured? Falling back to current shell."
- env TERM=xterm $LAUNCH_FILE "$@"
+ env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
if [ $? -eq 127 ]; then
- exec $LAUNCH_FILE --no-terminal "$@"
+ exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
fi
fi
fi
diff --git a/src/SMAPI.Installer/assets/windows-install.bat b/src/SMAPI.Installer/assets/windows-install.bat
index 2cc54e80..2cd98554 100644
--- a/src/SMAPI.Installer/assets/windows-install.bat
+++ b/src/SMAPI.Installer/assets/windows-install.bat
@@ -1,8 +1,8 @@
@echo off
-echo %~dp0 | findstr /C:"%TEMP%" 1>nul
+echo "%~dp0" | findstr /C:"%TEMP%" 1>nul
if not errorlevel 1 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
pause
) else (
- start /WAIT /B ./internal/windows-install.exe
+ start /WAIT /B internal\windows-install.exe
)
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
index 34149209..0357fe6b 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
@@ -28,8 +28,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
** Public methods
*********/
/// <summary>Get all spawnable items.</summary>
+ /// <param name="itemTypes">The item types to fetch (or null for any type).</param>
+ /// <param name="includeVariants">Whether to include flavored variants like "Sunflower Honey".</param>
[SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
- public IEnumerable<SearchableItem> GetAll()
+ public IEnumerable<SearchableItem> GetAll(ItemType[] itemTypes = null, bool includeVariants = true)
{
//
//
@@ -41,222 +43,246 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
//
//
-
IEnumerable<SearchableItem> GetAllRaw()
{
+ HashSet<ItemType> types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
+ bool ShouldGet(ItemType type) => types == null || types.Contains(type);
+
// get tools
- for (int q = Tool.stone; q <= Tool.iridium; q++)
+ if (ShouldGet(ItemType.Tool))
{
- int quality = q;
-
- yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, _ => ToolFactory.getToolFromDescription(ToolFactory.axe, quality));
- yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, _ => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality));
- yield return this.TryCreate(ItemType.Tool, ToolFactory.pickAxe, _ => ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality));
- yield return this.TryCreate(ItemType.Tool, ToolFactory.wateringCan, _ => ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality));
- if (quality != Tool.iridium)
- yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, _ => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality));
+ for (int q = Tool.stone; q <= Tool.iridium; q++)
+ {
+ int quality = q;
+
+ yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, _ => ToolFactory.getToolFromDescription(ToolFactory.axe, quality));
+ yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, _ => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality));
+ yield return this.TryCreate(ItemType.Tool, ToolFactory.pickAxe, _ => ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality));
+ yield return this.TryCreate(ItemType.Tool, ToolFactory.wateringCan, _ => ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality));
+ if (quality != Tool.iridium)
+ yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, _ => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality));
+ }
+ yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset, _ => new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
+ yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 1, _ => new Shears());
+ yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 2, _ => new Pan());
+ yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 3, _ => new Wand());
}
- yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset, _ => new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
- yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 1, _ => new Shears());
- yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 2, _ => new Pan());
- yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 3, _ => new Wand());
// clothing
+ if (ShouldGet(ItemType.Clothing))
{
- // items
- HashSet<int> clothingIds = new HashSet<int>();
- foreach (int id in Game1.clothingInformation.Keys)
- {
- if (id < 0)
- continue; // placeholder data for character customization clothing below
-
- clothingIds.Add(id);
+ foreach (int id in this.GetShirtIds())
yield return this.TryCreate(ItemType.Clothing, id, p => new Clothing(p.ID));
- }
-
- // character customization shirts (some shirts in this range have no data, but game has special logic to handle them)
- for (int id = 1000; id <= 1111; id++)
- {
- if (!clothingIds.Contains(id))
- yield return this.TryCreate(ItemType.Clothing, id, p => new Clothing(p.ID));
- }
}
// wallpapers
- for (int id = 0; id < 112; id++)
- yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory });
+ if (ShouldGet(ItemType.Wallpaper))
+ {
+ for (int id = 0; id < 112; id++)
+ yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory });
+ }
// flooring
- for (int id = 0; id < 56; id++)
- yield return this.TryCreate(ItemType.Flooring, id, p => new Wallpaper(p.ID, isFloor: true) { Category = SObject.furnitureCategory });
+ if (ShouldGet(ItemType.Flooring))
+ {
+ for (int id = 0; id < 56; id++)
+ yield return this.TryCreate(ItemType.Flooring, id, p => new Wallpaper(p.ID, isFloor: true) { Category = SObject.furnitureCategory });
+ }
// equipment
- foreach (int id in this.TryLoad<int, string>("Data\\Boots").Keys)
- yield return this.TryCreate(ItemType.Boots, id, p => new Boots(p.ID));
- foreach (int id in this.TryLoad<int, string>("Data\\hats").Keys)
- yield return this.TryCreate(ItemType.Hat, id, p => new Hat(p.ID));
+ if (ShouldGet(ItemType.Boots))
+ {
+ foreach (int id in this.TryLoad<int, string>("Data\\Boots").Keys)
+ yield return this.TryCreate(ItemType.Boots, id, p => new Boots(p.ID));
+ }
+ if (ShouldGet(ItemType.Hat))
+ {
+ foreach (int id in this.TryLoad<int, string>("Data\\hats").Keys)
+ yield return this.TryCreate(ItemType.Hat, id, p => new Hat(p.ID));
+ }
// weapons
- foreach (int id in this.TryLoad<int, string>("Data\\weapons").Keys)
+ if (ShouldGet(ItemType.Weapon))
{
- yield return this.TryCreate(ItemType.Weapon, id, p => (p.ID >= 32 && p.ID <= 34)
- ? (Item)new Slingshot(p.ID)
- : new MeleeWeapon(p.ID)
- );
+ foreach (int id in this.TryLoad<int, string>("Data\\weapons").Keys)
+ {
+ yield return this.TryCreate(ItemType.Weapon, id, p => (p.ID >= 32 && p.ID <= 34)
+ ? (Item)new Slingshot(p.ID)
+ : new MeleeWeapon(p.ID)
+ );
+ }
}
// furniture
- foreach (int id in this.TryLoad<int, string>("Data\\Furniture").Keys)
- yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID));
+ if (ShouldGet(ItemType.Furniture))
+ {
+ foreach (int id in this.TryLoad<int, string>("Data\\Furniture").Keys)
+ yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID));
+ }
// craftables
- foreach (int id in Game1.bigCraftablesInformation.Keys)
- yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID));
+ if (ShouldGet(ItemType.BigCraftable))
+ {
+ foreach (int id in Game1.bigCraftablesInformation.Keys)
+ yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID));
+ }
// objects
- foreach (int id in Game1.objectInformation.Keys)
+ if (ShouldGet(ItemType.Object) || ShouldGet(ItemType.Ring))
{
- string[] fields = Game1.objectInformation[id]?.Split('/');
-
- // secret notes
- if (id == 79)
+ foreach (int id in Game1.objectInformation.Keys)
{
- foreach (int secretNoteId in this.TryLoad<int, string>("Data\\SecretNotes").Keys)
+ string[] fields = Game1.objectInformation[id]?.Split('/');
+
+ // secret notes
+ if (id == 79)
{
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ =>
+ if (ShouldGet(ItemType.Object))
{
- SObject note = new SObject(79, 1);
- note.name = $"{note.name} #{secretNoteId}";
- return note;
- });
+ foreach (int secretNoteId in this.TryLoad<int, string>("Data\\SecretNotes").Keys)
+ {
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ =>
+ {
+ SObject note = new SObject(79, 1);
+ note.name = $"{note.name} #{secretNoteId}";
+ return note;
+ });
+ }
+ }
}
- }
-
- // ring
- else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
- yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
- // item
- else
- {
- // spawn main item
- SObject item = null;
- yield return this.TryCreate(ItemType.Object, id, p =>
+ // ring
+ else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
{
- return item = (p.ID == 812 // roe
- ? new ColoredObject(p.ID, 1, Color.White)
- : new SObject(p.ID, 1)
- );
- });
- if (item == null)
- continue;
-
- // flavored items
- switch (item.Category)
+ if (ShouldGet(ItemType.Ring))
+ yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
+ }
+
+ // item
+ else if (ShouldGet(ItemType.Object))
{
- // fruit products
- case SObject.FruitsCategory:
- // wine
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
- {
- Name = $"{item.Name} Wine",
- Price = item.Price * 3,
- preserve = { SObject.PreserveType.Wine },
- preservedParentSheetIndex = { item.ParentSheetIndex }
- });
-
- // jelly
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + item.ParentSheetIndex, _ => new SObject(344, 1)
- {
- Name = $"{item.Name} Jelly",
- Price = 50 + item.Price * 2,
- preserve = { SObject.PreserveType.Jelly },
- preservedParentSheetIndex = { item.ParentSheetIndex }
- });
- break;
-
- // vegetable products
- case SObject.VegetableCategory:
- // juice
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + item.ParentSheetIndex, _ => new SObject(350, 1)
- {
- Name = $"{item.Name} Juice",
- Price = (int)(item.Price * 2.25d),
- preserve = { SObject.PreserveType.Juice },
- preservedParentSheetIndex = { item.ParentSheetIndex }
- });
-
- // pickled
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ => new SObject(342, 1)
- {
- Name = $"Pickled {item.Name}",
- Price = 50 + item.Price * 2,
- preserve = { SObject.PreserveType.Pickle },
- preservedParentSheetIndex = { item.ParentSheetIndex }
- });
- break;
-
- // flower honey
- case SObject.flowersCategory:
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ =>
- {
- SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
- {
- Name = $"{item.Name} Honey",
- preservedParentSheetIndex = { item.ParentSheetIndex }
- };
- honey.Price += item.Price * 2;
- return honey;
- });
- break;
-
- // roe and aged roe (derived from FishPond.GetFishProduce)
- case SObject.sellAtFishShopCategory when item.ParentSheetIndex == 812:
+ // spawn main item
+ SObject item = null;
+ yield return this.TryCreate(ItemType.Object, id, p =>
+ {
+ return item = (p.ID == 812 // roe
+ ? new ColoredObject(p.ID, 1, Color.White)
+ : new SObject(p.ID, 1)
+ );
+ });
+ if (item == null)
+ continue;
+
+ // flavored items
+ if (includeVariants)
+ {
+ switch (item.Category)
{
- this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
+ // fruit products
+ case SObject.FruitsCategory:
+ // wine
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
+ {
+ Name = $"{item.Name} Wine",
+ Price = item.Price * 3,
+ preserve = { SObject.PreserveType.Wine },
+ preservedParentSheetIndex = { item.ParentSheetIndex }
+ });
- foreach (var pair in Game1.objectInformation)
- {
- // get input
- SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
- var inputTags = input?.GetContextTags();
- if (inputTags?.Any() != true)
- continue;
-
- // check if roe-producing fish
- if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag))))
- continue;
-
- // yield roe
- SObject roe = null;
- Color color = this.GetRoeColor(input);
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ =>
+ // jelly
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + item.ParentSheetIndex, _ => new SObject(344, 1)
+ {
+ Name = $"{item.Name} Jelly",
+ Price = 50 + item.Price * 2,
+ preserve = { SObject.PreserveType.Jelly },
+ preservedParentSheetIndex = { item.ParentSheetIndex }
+ });
+ break;
+
+ // vegetable products
+ case SObject.VegetableCategory:
+ // juice
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + item.ParentSheetIndex, _ => new SObject(350, 1)
{
- roe = new ColoredObject(812, 1, color)
+ Name = $"{item.Name} Juice",
+ Price = (int)(item.Price * 2.25d),
+ preserve = { SObject.PreserveType.Juice },
+ preservedParentSheetIndex = { item.ParentSheetIndex }
+ });
+
+ // pickled
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ => new SObject(342, 1)
+ {
+ Name = $"Pickled {item.Name}",
+ Price = 50 + item.Price * 2,
+ preserve = { SObject.PreserveType.Pickle },
+ preservedParentSheetIndex = { item.ParentSheetIndex }
+ });
+ break;
+
+ // flower honey
+ case SObject.flowersCategory:
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ =>
+ {
+ SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
{
- name = $"{input.Name} Roe",
- preserve = { Value = SObject.PreserveType.Roe },
- preservedParentSheetIndex = { Value = input.ParentSheetIndex }
+ Name = $"{item.Name} Honey",
+ preservedParentSheetIndex = { item.ParentSheetIndex }
};
- roe.Price += input.Price / 2;
- return roe;
+ honey.Price += item.Price * 2;
+ return honey;
});
+ break;
- // aged roe
- if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
+ // roe and aged roe (derived from FishPond.GetFishProduce)
+ case SObject.sellAtFishShopCategory when item.ParentSheetIndex == 812:
{
- yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => new ColoredObject(447, 1, color)
+ this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
+
+ foreach (var pair in Game1.objectInformation)
{
- name = $"Aged {input.Name} Roe",
- Category = -27,
- preserve = { Value = SObject.PreserveType.AgedRoe },
- preservedParentSheetIndex = { Value = input.ParentSheetIndex },
- Price = roe.Price * 2
- });
+ // get input
+ SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
+ var inputTags = input?.GetContextTags();
+ if (inputTags?.Any() != true)
+ continue;
+
+ // check if roe-producing fish
+ if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag))))
+ continue;
+
+ // yield roe
+ SObject roe = null;
+ Color color = this.GetRoeColor(input);
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ =>
+ {
+ roe = new ColoredObject(812, 1, color)
+ {
+ name = $"{input.Name} Roe",
+ preserve = { Value = SObject.PreserveType.Roe },
+ preservedParentSheetIndex = { Value = input.ParentSheetIndex }
+ };
+ roe.Price += input.Price / 2;
+ return roe;
+ });
+
+ // aged roe
+ if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
+ {
+ yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => new ColoredObject(447, 1, color)
+ {
+ name = $"Aged {input.Name} Roe",
+ Category = -27,
+ preserve = { Value = SObject.PreserveType.AgedRoe },
+ preservedParentSheetIndex = { Value = input.ParentSheetIndex },
+