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">
|
||||
<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>
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
// get tools
|
||||
for (int q = Tool.stone; q <= Tool.iridium; q++)
|
||||
{
|
||||
int quality = q;
|
||||
HashSet<ItemType> types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
|
||||
bool ShouldGet(ItemType type) => types == null || types.Contains(type);
|
||||
|
||||
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));
|
||||
// get tools
|
||||
if (ShouldGet(ItemType.Tool))
|
||||
{
|
||||
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;
|
||||
if (ShouldGet(ItemType.Ring))
|
||||
yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
|
||||
}
|
||||
|
||||
// flavored items
|
||||
switch (item.Category)
|
||||
// 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)
|
||||
// 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)
|
||||
{
|
||||
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:
|
||||
{
|
||||
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, _ =>
|
||||
// fruit products
|
||||
case SObject.FruitsCategory:
|
||||
// wine
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
|
||||
{
|
||||
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;
|
||||
Name = $"{item.Name} Wine",
|
||||
Price = item.Price * 3,
|
||||
preserve = { SObject.PreserveType.Wine },
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
});
|
||||
|
||||
// aged roe
|
||||
if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
|
||||
// jelly
|
||||
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",
|
||||
Category = -27,
|
||||
preserve = { Value = SObject.PreserveType.AgedRoe },
|
||||
preservedParentSheetIndex = { Value = input.ParentSheetIndex },
|
||||
Price = roe.Price * 2
|
||||
});
|
||||
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 = $"{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)
|
||||
: (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",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.10.1",
|
||||
"Version": "3.11.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.10.1"
|
||||
"MinimumApiVersion": "3.11.0"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Error Handler",
|
||||
"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.",
|
||||
"UniqueID": "SMAPI.ErrorHandler",
|
||||
"EntryDll": "ErrorHandler.dll",
|
||||
"MinimumApiVersion": "3.10.1"
|
||||
"MinimumApiVersion": "3.11.0"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.10.1",
|
||||
"Version": "3.11.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.10.1"
|
||||
"MinimumApiVersion": "3.11.0"
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
|
|||
{
|
||||
// update key
|
||||
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
|
||||
case ModDataFieldKey.StatusReasonPhrase:
|
||||
|
|
|
@ -69,7 +69,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
|
|||
public IEnumerable<string> GetUpdateKeys(Manifest manifest)
|
||||
{
|
||||
return
|
||||
(manifest.UpdateKeys ?? new string[0])
|
||||
manifest.UpdateKeys
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.ToArray();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using StardewModdingAPI.Toolkit.Serialization.Converters;
|
||||
|
||||
|
@ -70,5 +71,14 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
|
|||
this.UpdateKeys = new string[0];
|
||||
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",
|
||||
"description": "Content Patcher content file for mods",
|
||||
"@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme",
|
||||
"type": "object",
|
||||
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": true,
|
||||
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Format": {
|
||||
"title": "Format version",
|
||||
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
|
||||
"type": "string",
|
||||
"const": "1.22.0",
|
||||
"const": "1.23.0",
|
||||
"@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": {
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
"title": "SMAPI i18n file",
|
||||
"description": "A translation file for a SMAPI mod or content pack.",
|
||||
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation",
|
||||
"type": "object",
|
||||
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": true,
|
||||
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"title": "Schema",
|
||||
"description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.",
|
||||
"type": "string",
|
||||
"const": "https://smapi.io/schemas/manifest.json"
|
||||
"const": "https://smapi.io/schemas/i18n.json"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
"title": "SMAPI manifest",
|
||||
"description": "Manifest file for a SMAPI mod or content pack",
|
||||
"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest",
|
||||
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": true,
|
||||
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Name": {
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace StardewModdingAPI
|
|||
internal static int? LogScreenId { get; set; }
|
||||
|
||||
/// <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>
|
||||
|
@ -322,31 +322,42 @@ namespace StardewModdingAPI
|
|||
/// <summary>Get the name of the save folder, if any.</summary>
|
||||
private static string GetSaveFolderName()
|
||||
{
|
||||
// save not available
|
||||
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}";
|
||||
return Constants.GetSaveFolder()?.Name;
|
||||
}
|
||||
|
||||
/// <summary>Get the path to the current save folder, if any.</summary>
|
||||
private static string GetSaveFolderPathIfExists()
|
||||
{
|
||||
string folderName = Constants.GetSaveFolderName();
|
||||
if (folderName == null)
|
||||
DirectoryInfo saveFolder = Constants.GetSaveFolder();
|
||||
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;
|
||||
|
||||
string path = Path.Combine(Constants.SavesPath, folderName);
|
||||
return Directory.Exists(path)
|
||||
? path
|
||||
: null;
|
||||
// get basic info
|
||||
string rawSaveName = Game1.GetSaveGameName(set_value: false);
|
||||
ulong saveID = Context.LoadStage == LoadStage.SaveParsed
|
||||
? 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>
|
||||
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>
|
||||
public readonly ManagedEvent<TerrainFeatureListChangedEventArgs> TerrainFeatureListChanged;
|
||||
|
||||
/// <summary>Raised after furniture are added or removed in a location.</summary>
|
||||
public readonly ManagedEvent<FurnitureListChangedEventArgs> FurnitureListChanged;
|
||||
|
||||
/****
|
||||
** Specialized
|
||||
****/
|
||||
|
@ -238,6 +241,7 @@ namespace StardewModdingAPI.Framework.Events
|
|||
this.ObjectListChanged = ManageEventOf<ObjectListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged));
|
||||
this.ChestInventoryChanged = ManageEventOf<ChestInventoryChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.ChestInventoryChanged));
|
||||
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.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);
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
|
@ -195,7 +195,10 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
/// <inheritdoc />
|
||||
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);
|
||||
if (updateKey.LooksValid || !validOnly)
|
||||
|
@ -251,16 +254,19 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
{
|
||||
var ids = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// yield dependencies
|
||||
if (this.Manifest?.Dependencies != null)
|
||||
if (this.HasManifest())
|
||||
{
|
||||
foreach (var entry in this.Manifest?.Dependencies)
|
||||
ids[entry.UniqueID] = entry.IsRequired;
|
||||
}
|
||||
// yield dependencies
|
||||
foreach (IManifestDependency entry in this.Manifest.Dependencies)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry.UniqueID))
|
||||
ids[entry.UniqueID] = entry.IsRequired;
|
||||
}
|
||||
|
||||
// yield content pack parent
|
||||
if (this.Manifest?.ContentPackFor?.UniqueID != null)
|
||||
ids[this.Manifest.ContentPackFor.UniqueID] = true;
|
||||
// yield content pack parent
|
||||
if (!string.IsNullOrWhiteSpace(this.Manifest.ContentPackFor?.UniqueID))
|
||||
ids[this.Manifest.ContentPackFor.UniqueID] = true;
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
|
||||
// get update URLs
|
||||
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);
|
||||
if (url != null)
|
||||
|
@ -173,7 +173,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
|
||||
if (string.IsNullOrWhiteSpace(mod.Manifest.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));
|
||||
if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID))
|
||||
missingFields.Add(nameof(IManifest.UniqueID));
|
||||
|
@ -188,6 +188,28 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
// validate ID format
|
||||
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).");
|
||||
|
||||
// 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
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Models
|
|||
[nameof(VerboseLogging)] = false,
|
||||
[nameof(LogNetworkTraffic)] = false,
|
||||
[nameof(RewriteMods)] = true,
|
||||
[nameof(AggressiveMemoryOptimizations)] = true
|
||||
[nameof(AggressiveMemoryOptimizations)] = false
|
||||
};
|
||||
|
||||
/// <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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using Microsoft.Win32;
|
||||
#endif
|
||||
using Microsoft.Xna.Framework;
|
||||
#if SMAPI_FOR_XNA
|
||||
using System.Windows.Forms;
|
||||
|
@ -294,7 +297,14 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
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();
|
||||
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
|
||||
|
||||
// check for software likely to cause issues
|
||||
this.CheckForSoftwareConflicts();
|
||||
|
||||
// check for updates
|
||||
this.CheckForUpdatesAsync(mods);
|
||||
}
|
||||
|
@ -914,6 +927,10 @@ namespace StardewModdingAPI.Framework
|
|||
// terrain features changed
|
||||
if (locState.TerrainFeatures.IsChanged)
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="mods">The mods to include in the update check (if eligible).</param>
|
||||
private void CheckForUpdatesAsync(IModMetadata[] mods)
|
||||
|
@ -1593,19 +1659,16 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
// validate dependencies
|
||||
// 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))
|
||||
?.DisplayName ?? dependency.UniqueID;
|
||||
errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded.";
|
||||
failReason = ModFailReason.MissingDependencies;
|
||||
return false;
|
||||
}
|
||||
string dependencyName = mods
|
||||
.FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID))
|
||||
?.DisplayName ?? dependency.UniqueID;
|
||||
errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded.";
|
||||
failReason = ModFailReason.MissingDependencies;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,9 @@ namespace StardewModdingAPI.Framework.StateTracking
|
|||
/// <summary>Tracks added or removed terrain features.</summary>
|
||||
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>
|
||||
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.ObjectsWatcher = WatcherFactory.ForNetDictionary(location.netObjects);
|
||||
this.TerrainFeaturesWatcher = WatcherFactory.ForNetDictionary(location.terrainFeatures);
|
||||
this.FurnitureWatcher = WatcherFactory.ForNetCollection(location.furniture);
|
||||
|
||||
this.Watchers.AddRange(new IWatcher[]
|
||||
{
|
||||
|
@ -76,7 +80,8 @@ namespace StardewModdingAPI.Framework.StateTracking
|
|||
this.LargeTerrainFeaturesWatcher,
|
||||
this.NpcsWatcher,
|
||||
this.ObjectsWatcher,
|
||||
this.TerrainFeaturesWatcher
|
||||
this.TerrainFeaturesWatcher,
|
||||
this.FurnitureWatcher
|
||||
});
|
||||
|
||||
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>
|
||||
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>
|
||||
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.Objects.Update(watcher.ObjectsWatcher);
|
||||
this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher);
|
||||
this.Furniture.Update(watcher.FurnitureWatcher);
|
||||
|
||||
// chest inventories
|
||||
this.ChestItems.Clear();
|
||||
|
|
|
@ -233,6 +233,16 @@ namespace StardewModdingAPI.Metadata
|
|||
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
|
||||
****/
|
||||
|
@ -613,7 +623,7 @@ namespace StardewModdingAPI.Metadata
|
|||
return this.ReloadFarmAnimalSprites(content, key);
|
||||
|
||||
if (this.IsInFolder(key, "Buildings"))
|
||||
return this.ReloadBuildings(content, key);
|
||||
return this.ReloadBuildings(key);
|
||||
|
||||
if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
|
||||
return this.ReloadFenceTextures(key);
|
||||
|
@ -717,28 +727,39 @@ namespace StardewModdingAPI.Metadata
|
|||
}
|
||||
|
||||
/// <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>
|
||||
/// <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);
|
||||
if (isPaintMask)
|
||||
type = type.Substring(0, type.Length - paintMaskSuffix.Length);
|
||||
|
||||
// get buildings
|
||||
Building[] buildings = this.GetLocations(buildingInteriors: false)
|
||||
.OfType<BuildableGameLocation>()
|
||||
.SelectMany(p => p.buildings)
|
||||
.Where(p => p.buildingType.Value == type)
|
||||
.ToArray();
|
||||
|
||||
// reload buildings
|
||||
// remove from paint mask cache
|
||||
bool removedFromCache = this.RemoveFromPaintMaskCache(key);
|
||||
|
||||
// reload textures
|
||||
if (buildings.Any())
|
||||
{
|
||||
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
|
||||
foreach (Building building in buildings)
|
||||
building.texture = texture;
|
||||
building.resetTexture();
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
return removedFromCache;
|
||||
}
|
||||
|
||||
/// <summary>Reload map seat textures.</summary>
|
||||
|
@ -1295,5 +1316,18 @@ namespace StardewModdingAPI.Metadata
|
|||
// else just (re)load it from the main content manager
|
||||
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