Merge branch 'develop' into stable
This commit is contained in:
commit
8f96a97f07
|
@ -1,7 +1,7 @@
|
||||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--set general build properties -->
|
<!--set general build properties -->
|
||||||
<Version>3.10.1</Version>
|
<Version>3.11.0</Version>
|
||||||
<Product>SMAPI</Product>
|
<Product>SMAPI</Product>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||||
|
|
|
@ -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).
|
* 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
|
## 3.10.1
|
||||||
Released 03 May 2021 for Stardew Valley 1.5.4 or later.
|
Released 03 May 2021 for Stardew Valley 1.5.4 or later.
|
||||||
|
|
||||||
|
|
|
@ -324,7 +324,7 @@ To do that:
|
||||||
<GamePath>PATH_HERE</GamePath>
|
<GamePath>PATH_HERE</GamePath>
|
||||||
</PropertyGroup>
|
</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
|
The configuration will check your custom path first, then fall back to the default paths (so it'll
|
||||||
still compile on a different computer).
|
still compile on a different computer).
|
||||||
|
|
|
@ -43,8 +43,34 @@ if [ "$UNAME" == "Darwin" ]; then
|
||||||
cp -p StardewValley.bin.osx StardewModdingAPI.bin.osx
|
cp -p StardewValley.bin.osx StardewModdingAPI.bin.osx
|
||||||
fi
|
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
|
# launch SMAPI
|
||||||
open -a Terminal ./StardewModdingAPI.bin.osx "$@"
|
LC_ALL="C" ./StardewModdingAPI.bin.osx "$@"
|
||||||
else
|
else
|
||||||
# choose binary file to launch
|
# choose binary file to launch
|
||||||
LAUNCH_FILE=""
|
LAUNCH_FILE=""
|
||||||
|
@ -79,44 +105,44 @@ else
|
||||||
terminal|termite)
|
terminal|termite)
|
||||||
# consumes only one argument after -e
|
# consumes only one argument after -e
|
||||||
# options containing space characters are unsupported
|
# 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)
|
xterm|konsole|alacritty)
|
||||||
# consumes all arguments after -e
|
# 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)
|
terminator|xfce4-terminal|mate-terminal)
|
||||||
# consumes all arguments after -x
|
# 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)
|
gnome-terminal)
|
||||||
# consumes all arguments after --
|
# consumes all arguments after --
|
||||||
exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
|
exec $TERMINAL_NAME -- env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
kitty)
|
kitty)
|
||||||
# consumes all trailing arguments
|
# 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 we don't know the terminal, just try to run it in the current shell.
|
||||||
# If THAT fails, launch with no output.
|
# If THAT fails, launch with no output.
|
||||||
env TERM=xterm $LAUNCH_FILE "$@"
|
env TERM=xterm LC_ALL="C" $LAUNCH_FILE "$@"
|
||||||
if [ $? -eq 127 ]; then
|
if [ $? -eq 127 ]; then
|
||||||
exec $LAUNCH_FILE --no-terminal "$@"
|
exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
|
||||||
fi
|
fi
|
||||||
esac
|
esac
|
||||||
|
|
||||||
## terminal isn't executable; fallback to current shell or no terminal
|
## terminal isn't executable; fallback to current shell or no terminal
|
||||||
else
|
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."
|
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
|
if [ $? -eq 127 ]; then
|
||||||
exec $LAUNCH_FILE --no-terminal "$@"
|
exec LC_ALL="C" $LAUNCH_FILE --no-terminal "$@"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
@echo off
|
@echo off
|
||||||
echo %~dp0 | findstr /C:"%TEMP%" 1>nul
|
echo "%~dp0" | findstr /C:"%TEMP%" 1>nul
|
||||||
if not errorlevel 1 (
|
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.
|
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
|
||||||
pause
|
pause
|
||||||
) else (
|
) else (
|
||||||
start /WAIT /B ./internal/windows-install.exe
|
start /WAIT /B internal\windows-install.exe
|
||||||
)
|
)
|
||||||
|
|
|
@ -28,8 +28,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Get all spawnable items.</summary>
|
/// <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.")]
|
[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()
|
IEnumerable<SearchableItem> GetAllRaw()
|
||||||
{
|
{
|
||||||
// get tools
|
HashSet<ItemType> types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
|
||||||
for (int q = Tool.stone; q <= Tool.iridium; q++)
|
bool ShouldGet(ItemType type) => types == null || types.Contains(type);
|
||||||
{
|
|
||||||
int quality = q;
|
|
||||||
|
|
||||||
yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, _ => ToolFactory.getToolFromDescription(ToolFactory.axe, quality));
|
// get tools
|
||||||
yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, _ => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality));
|
if (ShouldGet(ItemType.Tool))
|
||||||
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));
|
for (int q = Tool.stone; q <= Tool.iridium; q++)
|
||||||
if (quality != Tool.iridium)
|
{
|
||||||
yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, _ => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality));
|
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
|
// clothing
|
||||||
|
if (ShouldGet(ItemType.Clothing))
|
||||||
{
|
{
|
||||||
// items
|
foreach (int id in this.GetShirtIds())
|
||||||
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);
|
|
||||||
yield return this.TryCreate(ItemType.Clothing, id, p => new Clothing(p.ID));
|
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
|
// wallpapers
|
||||||
for (int id = 0; id < 112; id++)
|
if (ShouldGet(ItemType.Wallpaper))
|
||||||
yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory });
|
{
|
||||||
|
for (int id = 0; id < 112; id++)
|
||||||
|
yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory });
|
||||||
|
}
|
||||||
|
|
||||||
// flooring
|
// flooring
|
||||||
for (int id = 0; id < 56; id++)
|
if (ShouldGet(ItemType.Flooring))
|
||||||
yield return this.TryCreate(ItemType.Flooring, id, p => new Wallpaper(p.ID, isFloor: true) { Category = SObject.furnitureCategory });
|
{
|
||||||
|
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
|
// equipment
|
||||||
foreach (int id in this.TryLoad<int, string>("Data\\Boots").Keys)
|
if (ShouldGet(ItemType.Boots))
|
||||||
yield return this.TryCreate(ItemType.Boots, id, p => new Boots(p.ID));
|
{
|
||||||
foreach (int id in this.TryLoad<int, string>("Data\\hats").Keys)
|
foreach (int id in this.TryLoad<int, string>("Data\\Boots").Keys)
|
||||||
yield return this.TryCreate(ItemType.Hat, id, p => new Hat(p.ID));
|
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
|
// 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)
|
foreach (int id in this.TryLoad<int, string>("Data\\weapons").Keys)
|
||||||
? (Item)new Slingshot(p.ID)
|
{
|
||||||
: new MeleeWeapon(p.ID)
|
yield return this.TryCreate(ItemType.Weapon, id, p => (p.ID >= 32 && p.ID <= 34)
|
||||||
);
|
? (Item)new Slingshot(p.ID)
|
||||||
|
: new MeleeWeapon(p.ID)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// furniture
|
// furniture
|
||||||
foreach (int id in this.TryLoad<int, string>("Data\\Furniture").Keys)
|
if (ShouldGet(ItemType.Furniture))
|
||||||
yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID));
|
{
|
||||||
|
foreach (int id in this.TryLoad<int, string>("Data\\Furniture").Keys)
|
||||||
|
yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID));
|
||||||
|
}
|
||||||
|
|
||||||
// craftables
|
// craftables
|
||||||
foreach (int id in Game1.bigCraftablesInformation.Keys)
|
if (ShouldGet(ItemType.BigCraftable))
|
||||||
yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID));
|
{
|
||||||
|
foreach (int id in Game1.bigCraftablesInformation.Keys)
|
||||||
|
yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID));
|
||||||
|
}
|
||||||
|
|
||||||
// objects
|
// objects
|
||||||
foreach (int id in Game1.objectInformation.Keys)
|
if (ShouldGet(ItemType.Object) || ShouldGet(ItemType.Ring))
|
||||||
{
|
{
|
||||||
string[] fields = Game1.objectInformation[id]?.Split('/');
|
foreach (int id in Game1.objectInformation.Keys)
|
||||||
|
|
||||||
// secret notes
|
|
||||||
if (id == 79)
|
|
||||||
{
|
{
|
||||||
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);
|
foreach (int secretNoteId in this.TryLoad<int, string>("Data\\SecretNotes").Keys)
|
||||||
note.name = $"{note.name} #{secretNoteId}";
|
{
|
||||||
return note;
|
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ =>
|
||||||
});
|
{
|
||||||
|
SObject note = new SObject(79, 1);
|
||||||
|
note.name = $"{note.name} #{secretNoteId}";
|
||||||
|
return note;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ring
|
// ring
|
||||||
else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable 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 =>
|
|
||||||
{
|
{
|
||||||
return item = (p.ID == 812 // roe
|
if (ShouldGet(ItemType.Ring))
|
||||||
? new ColoredObject(p.ID, 1, Color.White)
|
yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
|
||||||
: new SObject(p.ID, 1)
|
}
|
||||||
);
|
|
||||||
});
|
|
||||||
if (item == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// flavored items
|
// item
|
||||||
switch (item.Category)
|
else if (ShouldGet(ItemType.Object))
|
||||||
{
|
{
|
||||||
// fruit products
|
// spawn main item
|
||||||
case SObject.FruitsCategory:
|
SObject item = null;
|
||||||
// wine
|
yield return this.TryCreate(ItemType.Object, id, p =>
|
||||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
Name = $"{item.Name} Wine",
|
// fruit products
|
||||||
Price = item.Price * 3,
|
case SObject.FruitsCategory:
|
||||||
preserve = { SObject.PreserveType.Wine },
|
// wine
|
||||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
|
||||||
});
|
|
||||||
|
|
||||||
// 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:
|
|
||||||
{
|
|
||||||
this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
|
|
||||||
|
|
||||||
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, _ =>
|
|
||||||
{
|
{
|
||||||
roe = new ColoredObject(812, 1, color)
|
Name = $"{item.Name} Wine",
|
||||||
{
|
Price = item.Price * 3,
|
||||||
name = $"{input.Name} Roe",
|
preserve = { SObject.PreserveType.Wine },
|
||||||
preserve = { Value = SObject.PreserveType.Roe },
|
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||||
preservedParentSheetIndex = { Value = input.ParentSheetIndex }
|
|
||||||
};
|
|
||||||
roe.Price += input.Price / 2;
|
|
||||||
return roe;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// aged roe
|
// jelly
|
||||||
if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
|
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + item.ParentSheetIndex, _ => new SObject(344, 1)
|
||||||
{
|
{
|
||||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => new ColoredObject(447, 1, color)
|
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 = $"Aged {input.Name} Roe",
|
Name = $"{item.Name} Honey",
|
||||||
Category = -27,
|
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||||
preserve = { Value = SObject.PreserveType.AgedRoe },
|
};
|
||||||
preservedParentSheetIndex = { Value = input.ParentSheetIndex },
|
honey.Price += item.Price * 2;
|
||||||
Price = roe.Price * 2
|
return honey;
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
// roe and aged roe (derived from FishPond.GetFishProduce)
|
||||||
|
case SObject.sellAtFishShopCategory when item.ParentSheetIndex == 812:
|
||||||
|
{
|
||||||
|
this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
|
||||||
|
|
||||||
|
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, _ =>
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
Price = roe.Price * 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,5 +359,43 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
||||||
? new Color(61, 55, 42)
|
? new Color(61, 55, 42)
|
||||||
: (TailoringMenu.GetDyeColor(fish) ?? Color.Orange);
|
: (TailoringMenu.GetDyeColor(fish) ?? Color.Orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get valid shirt IDs.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Shirts have a possible range of 1000–1999, but not all of those IDs are valid. There are two sets of IDs:
|
||||||
|
///
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>
|
||||||
|
/// Shirts which exist in <see cref="Game1.clothingInformation"/>.
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// Shirts with a dynamic ID and no entry in <see cref="Game1.clothingInformation"/>. These automatically
|
||||||
|
/// use the generic shirt entry with ID <c>-1</c> and are mapped to a calculated position in the
|
||||||
|
/// <c>Characters/Farmer/shirts</c> spritesheet. There's no constant we can use, but some known valid
|
||||||
|
/// ranges are 1000–1111 (used in <see cref="Farmer.changeShirt"/> for the customization screen and
|
||||||
|
/// 1000–1127 (used in <see cref="Utility.getShopStock"/> and <see cref="GameLocation.sandyShopStock"/>).
|
||||||
|
/// Based on the spritesheet, the max valid ID is 1299.
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
private IEnumerable<int> GetShirtIds()
|
||||||
|
{
|
||||||
|
// defined shirt items
|
||||||
|
foreach (int id in Game1.clothingInformation.Keys)
|
||||||
|
{
|
||||||
|
if (id < 0)
|
||||||
|
continue; // placeholder data for character customization clothing below
|
||||||
|
|
||||||
|
yield return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamic shirts
|
||||||
|
HashSet<int> clothingIds = new HashSet<int>(Game1.clothingInformation.Keys);
|
||||||
|
for (int id = 1000; id <= 1299; id++)
|
||||||
|
{
|
||||||
|
if (!clothingIds.Contains(id))
|
||||||
|
yield return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Console Commands",
|
"Name": "Console Commands",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.10.1",
|
"Version": "3.11.0",
|
||||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||||
"UniqueID": "SMAPI.ConsoleCommands",
|
"UniqueID": "SMAPI.ConsoleCommands",
|
||||||
"EntryDll": "ConsoleCommands.dll",
|
"EntryDll": "ConsoleCommands.dll",
|
||||||
"MinimumApiVersion": "3.10.1"
|
"MinimumApiVersion": "3.11.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Error Handler",
|
"Name": "Error Handler",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.10.1",
|
"Version": "3.11.0",
|
||||||
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||||
"UniqueID": "SMAPI.ErrorHandler",
|
"UniqueID": "SMAPI.ErrorHandler",
|
||||||
"EntryDll": "ErrorHandler.dll",
|
"EntryDll": "ErrorHandler.dll",
|
||||||
"MinimumApiVersion": "3.10.1"
|
"MinimumApiVersion": "3.11.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Save Backup",
|
"Name": "Save Backup",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.10.1",
|
"Version": "3.11.0",
|
||||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||||
"UniqueID": "SMAPI.SaveBackup",
|
"UniqueID": "SMAPI.SaveBackup",
|
||||||
"EntryDll": "SaveBackup.dll",
|
"EntryDll": "SaveBackup.dll",
|
||||||
"MinimumApiVersion": "3.10.1"
|
"MinimumApiVersion": "3.11.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
|
||||||
{
|
{
|
||||||
// update key
|
// update key
|
||||||
case ModDataFieldKey.UpdateKey:
|
case ModDataFieldKey.UpdateKey:
|
||||||
return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p));
|
return manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p));
|
||||||
|
|
||||||
// non-manifest fields
|
// non-manifest fields
|
||||||
case ModDataFieldKey.StatusReasonPhrase:
|
case ModDataFieldKey.StatusReasonPhrase:
|
||||||
|
|
|
@ -69,7 +69,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
||||||
public IEnumerable<string> GetUpdateKeys(Manifest manifest)
|
public IEnumerable<string> GetUpdateKeys(Manifest manifest)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
(manifest.UpdateKeys ?? new string[0])
|
manifest.UpdateKeys
|
||||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using StardewModdingAPI.Toolkit.Serialization.Converters;
|
using StardewModdingAPI.Toolkit.Serialization.Converters;
|
||||||
|
|
||||||
|
@ -70,5 +71,14 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
|
||||||
this.UpdateKeys = new string[0];
|
this.UpdateKeys = new string[0];
|
||||||
this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor };
|
this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Normalize the model after it's deserialized.</summary>
|
||||||
|
/// <param name="context">The deserialization context.</param>
|
||||||
|
[OnDeserialized]
|
||||||
|
public void OnDeserialized(StreamingContext context)
|
||||||
|
{
|
||||||
|
this.Dependencies ??= new IManifestDependency[0];
|
||||||
|
this.UpdateKeys ??= new string[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,19 @@
|
||||||
"title": "Content Patcher content pack",
|
"title": "Content Patcher content pack",
|
||||||
"description": "Content Patcher content file for mods",
|
"description": "Content Patcher content file for mods",
|
||||||
"@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme",
|
"@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme",
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
|
"allowComments": true,
|
||||||
|
"allowTrailingCommas": true,
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Format": {
|
"Format": {
|
||||||
"title": "Format version",
|
"title": "Format version",
|
||||||
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
|
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "1.22.0",
|
"const": "1.23.0",
|
||||||
"@errorMessages": {
|
"@errorMessages": {
|
||||||
"const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.22.0'."
|
"const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.23.0'."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConfigSchema": {
|
"ConfigSchema": {
|
||||||
|
|
|
@ -4,14 +4,17 @@
|
||||||
"title": "SMAPI i18n file",
|
"title": "SMAPI i18n file",
|
||||||
"description": "A translation file for a SMAPI mod or content pack.",
|
"description": "A translation file for a SMAPI mod or content pack.",
|
||||||
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation",
|
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation",
|
||||||
"type": "object",
|
|
||||||
|
|
||||||
|
"allowComments": true,
|
||||||
|
"allowTrailingCommas": true,
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"$schema": {
|
"$schema": {
|
||||||
"title": "Schema",
|
"title": "Schema",
|
||||||
"description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.",
|
"description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "https://smapi.io/schemas/manifest.json"
|
"const": "https://smapi.io/schemas/i18n.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
"title": "SMAPI manifest",
|
"title": "SMAPI manifest",
|
||||||
"description": "Manifest file for a SMAPI mod or content pack",
|
"description": "Manifest file for a SMAPI mod or content pack",
|
||||||
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest",
|
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest",
|
||||||
|
|
||||||
|
"allowComments": true,
|
||||||
|
"allowTrailingCommas": true,
|
||||||
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Name": {
|
"Name": {
|
||||||
|
|
|
@ -61,7 +61,7 @@ namespace StardewModdingAPI
|
||||||
internal static int? LogScreenId { get; set; }
|
internal static int? LogScreenId { get; set; }
|
||||||
|
|
||||||
/// <summary>SMAPI's current raw semantic version.</summary>
|
/// <summary>SMAPI's current raw semantic version.</summary>
|
||||||
internal static string RawApiVersion = "3.10.1";
|
internal static string RawApiVersion = "3.11.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||||
|
@ -322,31 +322,42 @@ namespace StardewModdingAPI
|
||||||
/// <summary>Get the name of the save folder, if any.</summary>
|
/// <summary>Get the name of the save folder, if any.</summary>
|
||||||
private static string GetSaveFolderName()
|
private static string GetSaveFolderName()
|
||||||
{
|
{
|
||||||
// save not available
|
return Constants.GetSaveFolder()?.Name;
|
||||||
if (Context.LoadStage == LoadStage.None)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// get basic info
|
|
||||||
string saveName = Game1.GetSaveGameName(set_value: false);
|
|
||||||
ulong saveID = Context.LoadStage == LoadStage.SaveParsed
|
|
||||||
? SaveGame.loaded.uniqueIDForThisGame
|
|
||||||
: Game1.uniqueIDForThisGame;
|
|
||||||
|
|
||||||
// build folder name
|
|
||||||
return $"{new string(saveName.Where(char.IsLetterOrDigit).ToArray())}_{saveID}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get the path to the current save folder, if any.</summary>
|
/// <summary>Get the path to the current save folder, if any.</summary>
|
||||||
private static string GetSaveFolderPathIfExists()
|
private static string GetSaveFolderPathIfExists()
|
||||||
{
|
{
|
||||||
string folderName = Constants.GetSaveFolderName();
|
DirectoryInfo saveFolder = Constants.GetSaveFolder();
|
||||||
if (folderName == null)
|
return saveFolder?.Exists == true
|
||||||
|
? saveFolder.FullName
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the current save folder, if any.</summary>
|
||||||
|
private static DirectoryInfo GetSaveFolder()
|
||||||
|
{
|
||||||
|
// save not available
|
||||||
|
if (Context.LoadStage == LoadStage.None)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
string path = Path.Combine(Constants.SavesPath, folderName);
|
// get basic info
|
||||||
return Directory.Exists(path)
|
string rawSaveName = Game1.GetSaveGameName(set_value: false);
|
||||||
? path
|
ulong saveID = Context.LoadStage == LoadStage.SaveParsed
|
||||||
: null;
|
? SaveGame.loaded.uniqueIDForThisGame
|
||||||
|
: Game1.uniqueIDForThisGame;
|
||||||
|
|
||||||
|
// get best match (accounting for rare case where folder name isn't sanitized)
|
||||||
|
DirectoryInfo folder = null;
|
||||||
|
foreach (string saveName in new[] { rawSaveName, new string(rawSaveName.Where(char.IsLetterOrDigit).ToArray()) })
|
||||||
|
{
|
||||||
|
folder = new DirectoryInfo(Path.Combine(Constants.SavesPath, $"{saveName}_{saveID}"));
|
||||||
|
if (folder.Exists)
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if save doesn't exist yet, return the default one we expect to be created
|
||||||
|
return folder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using StardewValley;
|
||||||
|
using StardewValley.Objects;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Events
|
||||||
|
{
|
||||||
|
/// <summary>Event arguments for a <see cref="IWorldEvents.FurnitureListChanged"/> event.</summary>
|
||||||
|
public class FurnitureListChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The location which changed.</summary>
|
||||||
|
public GameLocation Location { get; }
|
||||||
|
|
||||||
|
/// <summary>The furniture added to the location.</summary>
|
||||||
|
public IEnumerable<Furniture> Added { get; }
|
||||||
|
|
||||||
|
/// <summary>The furniture removed from the location.</summary>
|
||||||
|
public IEnumerable<Furniture> Removed { get; }
|
||||||
|
|
||||||
|
/// <summary>Whether this is the location containing the local player.</summary>
|
||||||
|
public bool IsCurrentLocation => object.ReferenceEquals(this.Location, Game1.player?.currentLocation);
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="location">The location which changed.</param>
|
||||||
|
/// <param name="added">The furniture added to the location.</param>
|
||||||
|
/// <param name="removed">The furniture removed from the location.</param>
|
||||||
|
internal FurnitureListChangedEventArgs(GameLocation location, IEnumerable<Furniture> added, IEnumerable<Furniture> removed)
|
||||||
|
{
|
||||||
|
this.Location = location;
|
||||||
|
this.Added = added.ToArray();
|
||||||
|
this.Removed = removed.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,5 +28,8 @@ namespace StardewModdingAPI.Events
|
||||||
|
|
||||||
/// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
|
/// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
|
||||||
event EventHandler<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
|
event EventHandler<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
|
||||||
|
|
||||||
|
/// <summary>Raised after furniture are added or removed in a location.</summary>
|
||||||
|
event EventHandler<FurnitureListChangedEventArgs> FurnitureListChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,9 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
/// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
|
/// <summary>Raised after terrain features (like floors and trees) are added or removed in a location.</summary>
|
||||||
public readonly ManagedEvent<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
|
public readonly ManagedEvent<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
|
||||||
|
|
||||||
|
/// <summary>Raised after furniture are added or removed in a location.</summary>
|
||||||
|
public readonly ManagedEvent<FurnitureListChangedEventArgs> FurnitureListChanged;
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Specialized
|
** Specialized
|
||||||
****/
|
****/
|
||||||
|
@ -238,6 +241,7 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
this.ObjectListChanged = ManageEventOf<ObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged));
|
this.ObjectListChanged = ManageEventOf<ObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged));
|
||||||
this.ChestInventoryChanged = ManageEventOf<ChestInventoryChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ChestInventoryChanged));
|
this.ChestInventoryChanged = ManageEventOf<ChestInventoryChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ChestInventoryChanged));
|
||||||
this.TerrainFeatureListChanged = ManageEventOf<TerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged));
|
this.TerrainFeatureListChanged = ManageEventOf<TerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged));
|
||||||
|
this.FurnitureListChanged = ManageEventOf<FurnitureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.FurnitureListChanged));
|
||||||
|
|
||||||
this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged));
|
this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged));
|
||||||
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true);
|
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true);
|
||||||
|
|
|
@ -65,6 +65,13 @@ namespace StardewModdingAPI.Framework.Events
|
||||||
remove => this.EventManager.TerrainFeatureListChanged.Remove(value);
|
remove => this.EventManager.TerrainFeatureListChanged.Remove(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Raised after furniture are added or removed in a location.</summary>
|
||||||
|
public event EventHandler<FurnitureListChangedEventArgs> FurnitureListChanged
|
||||||
|
{
|
||||||
|
add => this.EventManager.FurnitureListChanged.Add(value, this.Mod);
|
||||||
|
remove => this.EventManager.FurnitureListChanged.Remove(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
|
|
@ -195,7 +195,10 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false)
|
public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false)
|
||||||
{
|
{
|
||||||
foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0])
|
if (!this.HasManifest())
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (string rawKey in this.Manifest.UpdateKeys)
|
||||||
{
|
{
|
||||||
UpdateKey updateKey = UpdateKey.Parse(rawKey);
|
UpdateKey updateKey = UpdateKey.Parse(rawKey);
|
||||||
if (updateKey.LooksValid || !validOnly)
|
if (updateKey.LooksValid || !validOnly)
|
||||||
|
@ -251,16 +254,19 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
{
|
{
|
||||||
var ids = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
var ids = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// yield dependencies
|
if (this.HasManifest())
|
||||||
if (this.Manifest?.Dependencies != null)
|
|
||||||
{
|
{
|
||||||
foreach (var entry in this.Manifest?.Dependencies)
|
// yield dependencies
|
||||||
ids[entry.UniqueID] = entry.IsRequired;
|
foreach (IManifestDependency entry in this.Manifest.Dependencies)
|
||||||
}
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(entry.UniqueID))
|
||||||
|
ids[entry.UniqueID] = entry.IsRequired;
|
||||||
|
}
|
||||||
|
|
||||||
// yield content pack parent
|
// yield content pack parent
|
||||||
if (this.Manifest?.ContentPackFor?.UniqueID != null)
|
if (!string.IsNullOrWhiteSpace(this.Manifest.ContentPackFor?.UniqueID))
|
||||||
ids[this.Manifest.ContentPackFor.UniqueID] = true;
|
ids[this.Manifest.ContentPackFor.UniqueID] = true;
|
||||||
|
}
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
|
|
||||||
// get update URLs
|
// get update URLs
|
||||||
List<string> updateUrls = new List<string>();
|
List<string> updateUrls = new List<string>();
|
||||||
foreach (string key in mod.Manifest.UpdateKeys ?? new string[0])
|
foreach (string key in mod.Manifest.UpdateKeys)
|
||||||
{
|
{
|
||||||
string url = getUpdateUrl(key);
|
string url = getUpdateUrl(key);
|
||||||
if (url != null)
|
if (url != null)
|
||||||
|
@ -173,7 +173,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
|
if (string.IsNullOrWhiteSpace(mod.Manifest.Name))
|
||||||
missingFields.Add(nameof(IManifest.Name));
|
missingFields.Add(nameof(IManifest.Name));
|
||||||
if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0")
|
if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0.0")
|
||||||
missingFields.Add(nameof(IManifest.Version));
|
missingFields.Add(nameof(IManifest.Version));
|
||||||
if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
|
if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
|
||||||
missingFields.Add(nameof(IManifest.UniqueID));
|
missingFields.Add(nameof(IManifest.UniqueID));
|
||||||
|
@ -188,6 +188,28 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
// validate ID format
|
// validate ID format
|
||||||
if (!PathUtilities.IsSlug(mod.Manifest.UniqueID))
|
if (!PathUtilities.IsSlug(mod.Manifest.UniqueID))
|
||||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
|
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
|
||||||
|
|
||||||
|
// validate dependencies
|
||||||
|
foreach (var dependency in mod.Manifest.Dependencies)
|
||||||
|
{
|
||||||
|
// null dependency
|
||||||
|
if (dependency == null)
|
||||||
|
{
|
||||||
|
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a null entry under {nameof(IManifest.Dependencies)}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// missing ID
|
||||||
|
if (string.IsNullOrWhiteSpace(dependency.UniqueID))
|
||||||
|
{
|
||||||
|
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with no {nameof(IManifestDependency.UniqueID)} field.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid ID
|
||||||
|
if (!PathUtilities.IsSlug(dependency.UniqueID))
|
||||||
|
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has a {nameof(IManifest.Dependencies)} entry with an invalid {nameof(IManifestDependency.UniqueID)} field (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate IDs are unique
|
// validate IDs are unique
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Models
|
||||||
[nameof(VerboseLogging)] = false,
|
[nameof(VerboseLogging)] = false,
|
||||||
[nameof(LogNetworkTraffic)] = false,
|
[nameof(LogNetworkTraffic)] = false,
|
||||||
[nameof(RewriteMods)] = true,
|
[nameof(RewriteMods)] = true,
|
||||||
[nameof(AggressiveMemoryOptimizations)] = true
|
[nameof(AggressiveMemoryOptimizations)] = false
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
|
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
|
||||||
|
|
|
@ -11,6 +11,9 @@ using System.Security;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
#if SMAPI_FOR_WINDOWS
|
||||||
|
using Microsoft.Win32;
|
||||||
|
#endif
|
||||||
using Microsoft.Xna.Framework;
|
using Microsoft.Xna.Framework;
|
||||||
#if SMAPI_FOR_XNA
|
#if SMAPI_FOR_XNA
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
@ -294,7 +297,14 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
this.Dispose();
|
try
|
||||||
|
{
|
||||||
|
this.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"The game ended, but SMAPI wasn't able to dispose correctly. Technical details: {ex}", LogLevel.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,6 +386,9 @@ namespace StardewModdingAPI.Framework
|
||||||
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
|
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
|
||||||
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
|
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
|
||||||
|
|
||||||
|
// check for software likely to cause issues
|
||||||
|
this.CheckForSoftwareConflicts();
|
||||||
|
|
||||||
// check for updates
|
// check for updates
|
||||||
this.CheckForUpdatesAsync(mods);
|
this.CheckForUpdatesAsync(mods);
|
||||||
}
|
}
|
||||||
|
@ -914,6 +927,10 @@ namespace StardewModdingAPI.Framework
|
||||||
// terrain features changed
|
// terrain features changed
|
||||||
if (locState.TerrainFeatures.IsChanged)
|
if (locState.TerrainFeatures.IsChanged)
|
||||||
events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed));
|
events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed));
|
||||||
|
|
||||||
|
// furniture changed
|
||||||
|
if (locState.Furniture.IsChanged)
|
||||||
|
events.FurnitureListChanged.Raise(new FurnitureListChangedEventArgs(location, locState.Furniture.Added, locState.Furniture.Removed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1247,6 +1264,55 @@ namespace StardewModdingAPI.Framework
|
||||||
this.LogManager.SetConsoleTitle(consoleTitle);
|
this.LogManager.SetConsoleTitle(consoleTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Log a warning if software known to cause issues is installed.</summary>
|
||||||
|
private void CheckForSoftwareConflicts()
|
||||||
|
{
|
||||||
|
#if SMAPI_FOR_WINDOWS
|
||||||
|
this.Monitor.Log("Checking for known software conflicts...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] registryKeys = { @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" };
|
||||||
|
|
||||||
|
string[] installedNames = registryKeys
|
||||||
|
.SelectMany(registryKey =>
|
||||||
|
{
|
||||||
|
using RegistryKey key = Registry.LocalMachine.OpenSubKey(registryKey);
|
||||||
|
if (key == null)
|
||||||
|
return new string[0];
|
||||||
|
|
||||||
|
return key
|
||||||
|
.GetSubKeyNames()
|
||||||
|
.Select(subkeyName =>
|
||||||
|
{
|
||||||
|
using RegistryKey subkey = key.OpenSubKey(subkeyName);
|
||||||
|
string displayName = (string)subkey?.GetValue("DisplayName");
|
||||||
|
string displayVersion = (string)subkey?.GetValue("DisplayVersion");
|
||||||
|
|
||||||
|
if (displayName != null && displayVersion != null && displayName.EndsWith($" {displayVersion}"))
|
||||||
|
displayName = displayName.Substring(0, displayName.Length - displayVersion.Length - 1);
|
||||||
|
|
||||||
|
return displayName;
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
})
|
||||||
|
.Where(name => name != null && (name.Contains("MSI Afterburner") || name.Contains("RivaTuner")))
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(name => name)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (installedNames.Any())
|
||||||
|
this.Monitor.Log($" Found {string.Join(" and ", installedNames)} installed, which can conflict with SMAPI. If you experience errors or crashes, try disabling that software or adding an exception for SMAPI / Stardew Valley.");
|
||||||
|
else
|
||||||
|
this.Monitor.Log(" None found!");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Failed when checking for conflicting software. Technical details:\n{ex}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary>
|
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary>
|
||||||
/// <param name="mods">The mods to include in the update check (if eligible).</param>
|
/// <param name="mods">The mods to include in the update check (if eligible).</param>
|
||||||
private void CheckForUpdatesAsync(IModMetadata[] mods)
|
private void CheckForUpdatesAsync(IModMetadata[] mods)
|
||||||
|
@ -1593,19 +1659,16 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// validate dependencies
|
// validate dependencies
|
||||||
// Although dependencies are validated before mods are loaded, a dependency may have failed to load.
|
// Although dependencies are validated before mods are loaded, a dependency may have failed to load.
|
||||||
if (mod.Manifest.Dependencies?.Any() == true)
|
foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired))
|
||||||
{
|
{
|
||||||
foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired))
|
if (this.ModRegistry.Get(dependency.UniqueID) == null)
|
||||||
{
|
{
|
||||||
if (this.ModRegistry.Get(dependency.UniqueID) == null)
|
string dependencyName = mods
|
||||||
{
|
.FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID))
|
||||||
string dependencyName = mods
|
?.DisplayName ?? dependency.UniqueID;
|
||||||
.FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID))
|
errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded.";
|
||||||
?.DisplayName ?? dependency.UniqueID;
|
failReason = ModFailReason.MissingDependencies;
|
||||||
errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded.";
|
return false;
|
||||||
failReason = ModFailReason.MissingDependencies;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,9 @@ namespace StardewModdingAPI.Framework.StateTracking
|
||||||
/// <summary>Tracks added or removed terrain features.</summary>
|
/// <summary>Tracks added or removed terrain features.</summary>
|
||||||
public IDictionaryWatcher<Vector2, TerrainFeature> TerrainFeaturesWatcher { get; }
|
public IDictionaryWatcher<Vector2, TerrainFeature> TerrainFeaturesWatcher { get; }
|
||||||
|
|
||||||
|
/// <summary>Tracks added or removed furniture.</summary>
|
||||||
|
public ICollectionWatcher<Furniture> FurnitureWatcher { get; }
|
||||||
|
|
||||||
/// <summary>Tracks items added or removed to chests.</summary>
|
/// <summary>Tracks items added or removed to chests.</summary>
|
||||||
public IDictionary<Vector2, ChestTracker> ChestWatchers { get; } = new Dictionary<Vector2, ChestTracker>();
|
public IDictionary<Vector2, ChestTracker> ChestWatchers { get; } = new Dictionary<Vector2, ChestTracker>();
|
||||||
|
|
||||||
|
@ -68,6 +71,7 @@ namespace StardewModdingAPI.Framework.StateTracking
|
||||||
this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters);
|
this.NpcsWatcher = WatcherFactory.ForNetCollection(location.characters);
|
||||||
this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects);
|
this.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects);
|
||||||
this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures);
|
this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures);
|
||||||
|
this.FurnitureWatcher = WatcherFactory.ForNetCollection(location.furniture);
|
||||||
|
|
||||||
this.Watchers.AddRange(new IWatcher[]
|
this.Watchers.AddRange(new IWatcher[]
|
||||||
{
|
{
|
||||||
|
@ -76,7 +80,8 @@ namespace StardewModdingAPI.Framework.StateTracking
|
||||||
this.LargeTerrainFeaturesWatcher,
|
this.LargeTerrainFeaturesWatcher,
|
||||||
this.NpcsWatcher,
|
this.NpcsWatcher,
|
||||||
this.ObjectsWatcher,
|
this.ObjectsWatcher,
|
||||||
this.TerrainFeaturesWatcher
|
this.TerrainFeaturesWatcher,
|
||||||
|
this.FurnitureWatcher
|
||||||
});
|
});
|
||||||
|
|
||||||
this.UpdateChestWatcherList(added: location.Objects.Pairs, removed: new KeyValuePair<Vector2, SObject>[0]);
|
this.UpdateChestWatcherList(added: location.Objects.Pairs, removed: new KeyValuePair<Vector2, SObject>[0]);
|
||||||
|
|
|
@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
|
||||||
/// <summary>Tracks added or removed terrain features.</summary>
|
/// <summary>Tracks added or removed terrain features.</summary>
|
||||||
public SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>> TerrainFeatures { get; } = new SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>>();
|
public SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>> TerrainFeatures { get; } = new SnapshotListDiff<KeyValuePair<Vector2, TerrainFeature>>();
|
||||||
|
|
||||||
|
/// <summary>Tracks added or removed furniture.</summary>
|
||||||
|
public SnapshotListDiff<Furniture> Furniture { get; } = new SnapshotListDiff<Furniture>();
|
||||||
|
|
||||||
/// <summary>Tracks changed chest inventories.</summary>
|
/// <summary>Tracks changed chest inventories.</summary>
|
||||||
public IDictionary<Chest, SnapshotItemListDiff> ChestItems { get; } = new Dictionary<Chest, SnapshotItemListDiff>();
|
public IDictionary<Chest, SnapshotItemListDiff> ChestItems { get; } = new Dictionary<Chest, SnapshotItemListDiff>();
|
||||||
|
|
||||||
|
@ -59,6 +62,7 @@ namespace StardewModdingAPI.Framework.StateTracking.Snapshots
|
||||||
this.Npcs.Update(watcher.NpcsWatcher);
|
this.Npcs.Update(watcher.NpcsWatcher);
|
||||||
this.Objects.Update(watcher.ObjectsWatcher);
|
this.Objects.Update(watcher.ObjectsWatcher);
|
||||||
this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher);
|
this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher);
|
||||||
|
this.Furniture.Update(watcher.FurnitureWatcher);
|
||||||
|
|
||||||
// chest inventories
|
// chest inventories
|
||||||
this.ChestItems.Clear();
|
this.ChestItems.Clear();
|
||||||
|
|
|
@ -233,6 +233,16 @@ namespace StardewModdingAPI.Metadata
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "buildings\\houses_paintmask": // Farm
|
||||||
|
{
|
||||||
|
bool removedFromCache = this.RemoveFromPaintMaskCache(key);
|
||||||
|
|
||||||
|
Farm farm = Game1.getFarm();
|
||||||
|
farm?.ApplyHousePaint();
|
||||||
|
|
||||||
|
return removedFromCache || farm != null;
|
||||||
|
}
|
||||||
|
|
||||||
/****
|
/****
|
||||||
** Content\Characters\Farmer
|
** Content\Characters\Farmer
|
||||||
****/
|
****/
|
||||||
|
@ -613,7 +623,7 @@ namespace StardewModdingAPI.Metadata
|
||||||
return this.ReloadFarmAnimalSprites(content, key);
|
return this.ReloadFarmAnimalSprites(content, key);
|
||||||
|
|
||||||
if (this.IsInFolder(key, "Buildings"))
|
if (this.IsInFolder(key, "Buildings"))
|
||||||
return this.ReloadBuildings(content, key);
|
return this.ReloadBuildings(key);
|
||||||
|
|
||||||
if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
|
if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
|
||||||
return this.ReloadFenceTextures(key);
|
return this.ReloadFenceTextures(key);
|
||||||
|
@ -717,28 +727,39 @@ namespace StardewModdingAPI.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reload building textures.</summary>
|
/// <summary>Reload building textures.</summary>
|
||||||
/// <param name="content">The content manager through which to reload the asset.</param>
|
|
||||||
/// <param name="key">The asset key to reload.</param>
|
/// <param name="key">The asset key to reload.</param>
|
||||||
/// <returns>Returns whether any textures were reloaded.</returns>
|
/// <returns>Returns whether any textures were reloaded.</returns>
|
||||||
private bool ReloadBuildings(LocalizedContentManager content, string key)
|
private bool ReloadBuildings(string key)
|
||||||
{
|
{
|
||||||
// get buildings
|
// get paint mask info
|
||||||
|
const string paintMaskSuffix = "_PaintMask";
|
||||||
|
bool isPaintMask = key.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// get building type
|
||||||
string type = Path.GetFileName(key);
|
string type = Path.GetFileName(key);
|
||||||
|
if (isPaintMask)
|
||||||
|
type = type.Substring(0, type.Length - paintMaskSuffix.Length);
|
||||||
|
|
||||||
|
// get buildings
|
||||||
Building[] buildings = this.GetLocations(buildingInteriors: false)
|
Building[] buildings = this.GetLocations(buildingInteriors: false)
|
||||||
.OfType<BuildableGameLocation>()
|
.OfType<BuildableGameLocation>()
|
||||||
.SelectMany(p => p.buildings)
|
.SelectMany(p => p.buildings)
|
||||||
.Where(p => p.buildingType.Value == type)
|
.Where(p => p.buildingType.Value == type)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// reload buildings
|
// remove from paint mask cache
|
||||||
|
bool removedFromCache = this.RemoveFromPaintMaskCache(key);
|
||||||
|
|
||||||
|
// reload textures
|
||||||
if (buildings.Any())
|
if (buildings.Any())
|
||||||
{
|
{
|
||||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
|
||||||
foreach (Building building in buildings)
|
foreach (Building building in buildings)
|
||||||
building.texture = texture;
|
building.resetTexture();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return removedFromCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Reload map seat textures.</summary>
|
/// <summary>Reload map seat textures.</summary>
|
||||||
|
@ -1295,5 +1316,18 @@ namespace StardewModdingAPI.Metadata
|
||||||
// else just (re)load it from the main content manager
|
// else just (re)load it from the main content manager
|
||||||
return this.MainContentManager.Load<Texture2D>(key);
|
return this.MainContentManager.Load<Texture2D>(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Remove a case-insensitive key from the paint mask cache.</summary>
|
||||||
|
/// <param name="key">The paint mask asset key.</param>
|
||||||
|
private bool RemoveFromPaintMaskCache(string key)
|
||||||
|
{
|
||||||
|
// make cache case-insensitive
|
||||||
|
// This is needed for cache invalidation since mods may specify keys with a different capitalization
|
||||||
|
if (!object.ReferenceEquals(BuildingPainter.paintMaskLookup.Comparer, StringComparer.OrdinalIgnoreCase))
|
||||||
|
BuildingPainter.paintMaskLookup = new Dictionary<string, List<List<int>>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// remove key from cache
|
||||||
|
return BuildingPainter.paintMaskLookup.Remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue