Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2017-10-14 11:44:02 -04:00
commit 7911831606
241 changed files with 5816 additions and 3720 deletions

247
.gitignore vendored
View File

@ -1,262 +1,25 @@
# SMAPI Specific Ignores
StardewModdingAPI/bin/
StardewModdingAPI/obj/
TrainerMod/bin/
TrainerMod/obj/
StardewInjector/bin/
StardewInjector/obj/
packages/
steamapps/
*.symlink
*.lnk
!*.exe
!*.dll
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
# user-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
# build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
# Visual Studio cache/options
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
# NuGet packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml

View File

@ -2,5 +2,5 @@ using System.Reflection;
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.15.4.0")]
[assembly: AssemblyFileVersion("1.15.4.0")]
[assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyFileVersion("2.0.0.0")]

45
docs/README.md Normal file
View File

@ -0,0 +1,45 @@
**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets
you play the game with mods. It's safely installed alongside the game's executable, and doesn't
change any of your game files. It serves six main purposes:
1. **Load mods into the game.**
_SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't
possible without SMAPI to load them.)_
2. **Provide APIs and events for mods.**
_SMAPI provides APIs and events which let mods interact with the game in ways they otherwise
couldn't._
3. **Rewrite mods for crossplatform compatibility.**
_SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows
without the mods needing to handle differences between the Linux/Mac and Windows versions of the
game._
4. **Rewrite mods to update them.**
_SMAPI detects when a mod accesses part of the game that changed in a game update which affects
many mods, and rewrites the mod so it's compatible._
5. **Intercept errors.**
_SMAPI intercepts errors that happen in the game, displays the error details in the console
window, and in most cases automatically recovers the game. This prevents mods from accidentally
crashing the game, and makes it possible to troubleshoot errors in the game itself that would
otherwise show a generic 'program has stopped working' type of message._
6. **Provide update checks.**
_SMAPI automatically checks for new versions of your installed mods, and notifies you when any
are available._
## Documentation
Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other
modders!
### For players
* [Modding guides](https://stardewvalleywiki.com/Modding:Index#For_players)
### For modders
* [Modding documentation](https://stardewvalleywiki.com/Modding:Index)
* [Mod build configuration](mod-build-config.md)
* [Release notes](release-notes.md)
### For SMAPI developers
* [Technical docs](technical-docs.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

175
docs/mod-build-config.md Normal file
View File

@ -0,0 +1,175 @@
The **mod build package** is an open-source NuGet package which automates the MSBuild configuration
for SMAPI mods.
The package...
* lets your code compile on any computer (Linux/Mac/Windows) without needing to change the assembly
references or game path.
* packages the mod into the game's `Mods` folder when you rebuild the code (configurable).
* configures Visual Studio so you can debug into the mod code when the game is running (_Windows
only_).
## Contents
* [Install](#install)
* [Configure](#configure)
* [Troubleshoot](#troubleshoot)
* [Release notes](#release-notes)
## Install
**When creating a new mod:**
1. Create an empty library project.
2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig).
3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod).
4. Compile on any platform.
**When migrating an existing mod:**
1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley,
`StardewModdingAPI`, and `xTile`.
2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig).
3. Compile on any platform.
## Configure
### Deploy files into the `Mods` folder
By default, your mod will be copied into the game's `Mods` folder (with a subfolder matching your
project name) when you rebuild the code. The package will automatically include your
`manifest.json`, any `i18n` files, and the build output.
To add custom files to the mod folder, just [add them to the build output](https://stackoverflow.com/a/10828462/262123).
(If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
You can change the mod's folder name by adding this above the first `</PropertyGroup>` in your
`.csproj`:
```xml
<ModFolderName>YourModName</ModFolderName>
```
If you don't want to deploy the mod automatically, you can add this:
```xml
<EnableModDeploy>False</EnableModDeploy>
```
### Create release zip
By default, a zip file will be created in the build output when you rebuild the code. This zip file
contains all the files needed to share your mod in the recommended format for uploading to Nexus
Mods or other sites.
You can change the zipped folder name (and zip name) by adding this above the first
`</PropertyGroup>` in your `.csproj`:
```xml
<ModFolderName>YourModName</ModFolderName>
```
You can change the folder path where the zip is created like this:
```xml
<ModZipPath>$(SolutionDir)\_releases</ModZipPath>
```
Finally, you can disable the zip creation with this:
```xml
<EnableModZip>False</EnableModZip>
```
Or only create it in release builds with this:
```xml
<EnableModZip Condition="$(Configuration) != 'Release'">False</EnableModZip>
```
### Game path
The package usually detects where your game is installed automatically. If it can't find your game
or you have multiple installs, you can specify the path yourself. There's two ways to do that:
* **Option 1: global game path (recommended).**
_This will apply to every project that uses the package._
1. Get the full folder path containing the Stardew Valley executable.
2. Create this file:
platform | path
--------- | ----
Linux/Mac | `~/stardewvalley.targets`
Windows | `%USERPROFILE%\stardewvalley.targets`
3. Save the file with this content:
```xml
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
</Project>
```
4. Replace `PATH_HERE` with your game path.
* **Option 2: path in the project file.**
_You'll need to do this for each project that uses the package._
1. Get the folder path containing the Stardew Valley `.exe` file.
2. Add this to your `.csproj` file under the `<Project` line:
```xml
<PropertyGroup>
<GamePath>PATH_HERE</GamePath>
</PropertyGroup>
```
3. Replace `PATH_HERE` with your custom game install path.
The configuration will check your custom path first, then fall back to the default paths (so it'll
still compile on a different computer).
## Troubleshoot
### "Failed to find the game install path"
That error means the package couldn't find your game. You can specify the game path yourself; see
_[Game path](#game-path)_ above.
## Release notes
### 2.0
* Added: mods are now copied into the `Mods` folder automatically (configurable).
* Added: release zips are now created automatically in your build output folder (configurable).
* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
* Added mod's version to release zip filename.
* Improved errors to simplify troubleshooting.
* Fixed release zip not having a mod folder.
* Fixed release zip failing if mod name contains characters that aren't valid in a filename.
### 1.7.1
* Fixed issue where i18n folders were flattened.
* The manifest/i18n files in the project now take precedence over those in the build output if both
are present.
### 1.7
* Added option to create release zips on build.
* Added reference to XNA's XACT library for audio-related mods.
### 1.6
* Added support for deploying mod files into `Mods` automatically.
* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI.
### 1.5
* Added support for setting a custom game path globally.
* Added default GOG path on Mac.
### 1.4
* Fixed detection of non-default game paths on 32-bit Windows.
* Removed support for SilVerPLuM (discontinued).
* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms
mods automatically).
### 1.3
* Added support for non-default game paths on Windows.
### 1.2
* Exclude game binaries from mod build output.
### 1.1
* Added support for overriding the target platform.
### 1.0
* Initial release.
* Added support for detecting the game path automatically.
* Added support for injecting XNA/MonoGame references automatically based on the OS.
* Added support for mod builders like SilVerPLuM.

View File

@ -1,31 +1,90 @@
# Release notes
## 2.0 (upcoming)
<!--See [log](https://github.com/Pathoschild/SMAPI/compare/1.10...2.0).-->
# Release notes
## 2.0
### Release highlights
* **Mod update checks**
SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the
mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with
update-check support for over 250 existing mods, and more will be added as modders enable the feature.
* **Mod stability warnings**
SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning
in the console.
* **Simpler console**
The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit
your terminal background color.
* **New features for modders**
SMAPI 2.0 adds several features to enable new kinds of mods (see
[API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)).
The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This let SMAPI mods do
anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g.
seasonal textures, NPC clothing that depend on the weather or location, etc).
The **input events** unify controller + keyboard + mouse input into one event and constant for easy handling, and add
metadata like the cursor position and grab tile to support click handling. They also let you prevent the game from
receiving input, to enable new scenarios like action highjacking and UI overlays.
The mod manifest has a few changes too:
* The **`UpdateKeys` field** lets you specify your Chucklefish, GitHub, or Nexus mod IDs. SMAPI will automatically
check for newer versions and notify the player.
* The **version field** is now a semantic string like `"1.0-alpha"`. (Mods which still use the version structure will
still work fine.)
* The **dependencies field** now lets you add optional dependencies which should be loaded first if available.
Finally, the `SDate` utility now has a `DayOfWeek` field for more convenient date calculations, and `ISemanticVersion`
now implements `IEquatable<ISemanticVersion>`.
* **Goodbye deprecated code**
SMAPI 2.0 removes all deprecated code to unshackle future development. That includes...
* removed all code marked obsolete;
* removed TrainerMod's `save` and `load` commands;
* removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest;
* removed support for multiple mods having the same `UniqueID` value;
* removed access to SMAPI internals through the reflection helper.
* **Command-line install**
For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments
(see [technical docs](technical-docs.md#command-line-arguments)).
### Change log
For players:
* The SMAPI console is now much simpler and easier to read.
* The SMAPI console now adjusts its colors when you have a light terminal background.
* SMAPI now alerts you when mods have new versions available.
* SMAPI now warns you about mods which may impact game stability or compatibility.
* The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color.
* Renamed installer folder to avoid confusion.
* Updated compatibility list.
* Fixed update check errors on Linux/Mac.
* Fixed collection-changed errors during startup for some players.
For mod developers:
* Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time.
<small>_This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._</small>
* Added new input events.
<small>_The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._</small>
* Added support for editing, injecting, and reloading XNB data loaded by the game at any time.
* Added support for automatic mod update checks.
* Added unified input events.
* Added support for suppressing input.
* Added support for optional dependencies.
* Added support for string versions (like `"1.0-alpha"`) in `manifest.json`.
* Added `IEquatable<ISemanticVersion>` to `ISemanticVersion`.
* Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`.
* Added day of week to `SDate` instances.
* Added `IEquatable<ISemanticVersion>` to `ISemanticVersion`.
* Updated Json.NET from 8.0.3 to 10.0.3.
* Removed the TrainerMod's `save` and `load` commands.
* Removed all deprecated code.
* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest.
* Removed support for mods with a non-unique `UniqueID` value in their manifest.
* Removed access to SMAPI internals through the reflection helper, to discourage fragile mods.
* Fixed `SDate.Now()` crashing when called during the new-game intro.
* Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro.
* Fixed SMAPI allowing map tilesheets with absolute or directory-climbing paths. These are now rejected even if the path exists, to avoid problems when players install the mod.
For power users:
* Added command-line arguments to the SMAPI installer so it can be scripted.
For SMAPI developers:
* Significantly refactored SMAPI to support changes in 2.0 and upcoming releases.
* Overhauled `StardewModdingAPI.config.json` format to support mod data like update keys.
* Removed SMAPI 1._x_ compatibility mode.
## 1.15.4
For players:
* Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader.

View File

@ -1,61 +1,19 @@
![](docs/imgs/SMAPI.png)
&larr; [README](README.md)
This file provides more technical documentation about SMAPI. If you only want to use or create
mods, this section isn't relevant to you; see the main README to use or create mods.
## Contents
* [What is SMAPI?](#what-is-smapi)
* **[For players](#for-players)**
* **[For mod developers](#for-mod-developers)**
* [For SMAPI developers](#for-smapi-developers)
* [Development](#development)
* [Compiling from source](#compiling-from-source)
* [Debugging a local build](#debugging-a-local-build)
* [Preparing a release](#preparing-a-release)
* [Advanced usage](#advanced-usage)
* [Customisation](#customisation)
* [Configuration file](#configuration-file)
* [Command-line arguments](#command-line-arguments)
* [Compile flags](#compile-flags)
## What is SMAPI?
**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/)
that lets you play the game with mods. It's safely installed alongside the game's executable, and
doesn't change any of your game files. It serves five main purposes:
1. **Load mods into the game.**
_SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't
possible without SMAPI to load them.)_
2. **Provide APIs and events for mods.**
_SMAPI provides low-level APIs and events which let mods interact with the game in ways they
otherwise couldn't._
3. **Rewrite mods for crossplatform compatibility.**
_SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows
without the mods needing to handle differences between the Linux/Mac and Windows versions of the
game._
4. **Rewrite mods to update them.**
_SMAPI detects when a mod accesses part of the game that changed in a recent update which
affects many mods, and rewrites the mod so it's compatible._
5. **Intercept errors.**
_SMAPI intercepts errors that happen in the game, displays the error details in the console
window, and in most cases automatically recovers the game. This prevents mods from accidentally
crashing the game, and makes it possible to troubleshoot errors in the game itself that would
otherwise show a generic 'program has stopped working' type of message._
## For players
* [Intro & FAQs](http://stardewvalleywiki.com/Modding:Player_FAQs)
* [Installing SMAPI](http://stardewvalleywiki.com/Modding:Installing_SMAPI)
* [Release notes](release-notes.md#release-notes)
* Need help? Come [chat on Discord](https://discord.gg/KCJHWhX) or [post in the support forums](http://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/).
_Please don't submit issues on GitHub for support questions._
## For mod developers
* [Modding documentation](http://stardewvalleywiki.com/Modding:Index)
* [Release notes](release-notes.md#release-notes)
* [Chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other modders
## For SMAPI developers
_This section is about compiling SMAPI itself from source. If you don't know what that means, this
section isn't relevant to you; see the previous sections to use or create mods._
## Development
### Compiling from source
Using an official SMAPI release is recommended for most users.
@ -84,13 +42,13 @@ on the wiki for the first-time setup.
build type | format | example
:--------- | :-------------------------------- | :------
dev build | `<version>-alpha.<timestamp>` | `1.0-alpha.20171230`
prerelease | `<version>-prerelease.<ID>` | `1.0-prerelease.2`
release | `<version>` | `1.0`
dev build | `<version>-alpha.<timestamp>` | `2.0-alpha.20171230`
prerelease | `<version>-prerelease.<ID>` | `2.0-prerelease.2`
release | `<version>` | `2.0`
2. In Windows:
1. Rebuild the solution in _Release_ mode.
2. Rename `bin/Packaged` to `SMAPI <version>` (e.g. `SMAPI 1.0`).
2. Rename `bin/Packaged` to `SMAPI <version>` (e.g. `SMAPI 2.0`).
2. Transfer the `SMAPI <version>` folder to Linux or Mac.
_This adds the installer executable and Windows files. We'll do the rest in Linux or Mac,
since we need to set Unix file permissions that Windows won't save._
@ -101,7 +59,7 @@ on the wiki for the first-time setup.
3. If you did everything right so far, you should have a folder like this:
```
SMAPI-1.x/
SMAPI-2.x/
install.exe
readme.txt
internal/
@ -139,7 +97,7 @@ on the wiki for the first-time setup.
* delete `internal/Windows/StardewModdingAPI.xml`.
7. Compress the two folders into `SMAPI <version>.zip` and `SMAPI <version> for developers.zip`.
## Advanced usage
## Customisation
### Configuration file
You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your
game folder.
@ -147,17 +105,11 @@ game folder.
Basic fields:
field | purpose
----- | -------
----------------- | -------
`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging).
`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background.
`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context.
Advanced fields (changing these isn't recommended and may destabilise your game):
field | purpose
----- | -------
`DisabledMods` | A list of mods to consider obsolete and not load.
`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. This can be used to force SMAPI to load an incompatible mod, though that isn't recommended.
`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file.
### Command-line arguments
The SMAPI installer recognises three command-line arguments:
@ -183,4 +135,3 @@ SMAPI uses a small number of conditional compilation constants, which you can se
flag | purpose
---- | -------
`SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`.
`SMAPI_1_x` | Sets legacy SMAPI 1._x_ mode, disables SMAPI 2.0 features, and enables deprecated code. This will be removed when SMAPI 2.0 is released.

View File

@ -0,0 +1,7 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")]
[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")]
[assembly: AssemblyProduct("StardewModdingAPI.AssemblyRewriters")]
[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")]

View File

@ -2,16 +2,16 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers
namespace StardewModdingAPI.AssemblyRewriters
{
/// <summary>Wraps <see cref="SpriteBatch"/> methods that are incompatible when converting compiled code between MonoGame and XNA.</summary>
public class SpriteBatchWrapper : SpriteBatch
/// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows.</summary>
public class SpriteBatchMethods : SpriteBatch
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public SpriteBatchWrapper(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { }
/****

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -30,44 +30,15 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Mdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Pdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\GlobalAssemblyInfo.cs">
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Finders\EventFinder.cs" />
<Compile Include="Finders\PropertyFinder.cs" />
<Compile Include="Finders\FieldFinder.cs" />
<Compile Include="Finders\MethodFinder.cs" />
<Compile Include="Finders\TypeFinder.cs" />
<Compile Include="IncompatibleInstructionException.cs" />
<Compile Include="RewriteHelper.cs" />
<Compile Include="IInstructionRewriter.cs" />
<Compile Include="Platform.cs" />
<Compile Include="PlatformAssemblyMap.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Rewriters\TypeReferenceRewriter.cs" />
<Compile Include="Rewriters\FieldReplaceRewriter.cs" />
<Compile Include="Rewriters\FieldToPropertyRewriter.cs" />
<Compile Include="Rewriters\MethodParentRewriter.cs" />
<Compile Include="Rewriters\Wrappers\SpriteBatchWrapper.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<Compile Include="SpriteBatchMethods.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\common.targets" />
<Import Project="..\..\build\common.targets" />
</Project>

View File

@ -0,0 +1,52 @@
namespace StardewModdingAPI.Common.Models
{
/// <summary>Generic metadata about a mod.</summary>
internal class ModInfoModel
{
/*********
** Accessors
*********/
/// <summary>The mod name.</summary>
public string Name { get; set; }
/// <summary>The mod's semantic version number.</summary>
public string Version { get; set; }
/// <summary>The mod's web URL.</summary>
public string Url { get; set; }
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
public string Error { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an empty instance.</summary>
public ModInfoModel()
{
// needed for JSON deserialising
}
/// <summary>Construct an instance.</summary>
/// <param name="name">The mod name.</param>
/// <param name="version">The mod's semantic version number.</param>
/// <param name="url">The mod's web URL.</param>
/// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
public ModInfoModel(string name, string version, string url, string error = null)
{
this.Name = name;
this.Version = version;
this.Url = url;
this.Error = error; // mainly initialised here for the JSON deserialiser
}
/// <summary>Construct an instance.</summary>
/// <param name="error">The error message indicating why the mod is invalid.</param>
public ModInfoModel(string error)
{
this.Error = error;
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
namespace StardewModdingAPI.Common.Models
{
/// <summary>Specifies mods whose update-check info to fetch.</summary>
internal class ModSearchModel
{
/*********
** Accessors
*********/
/// <summary>The namespaced mod keys to search.</summary>
public string[] ModKeys { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an empty instance.</summary>
public ModSearchModel()
{
// needed for JSON deserialising
}
/// <summary>Construct an instance.</summary>
/// <param name="modKeys">The namespaced mod keys to search.</param>
public ModSearchModel(IEnumerable<string> modKeys)
{
this.ModKeys = modKeys.ToArray();
}
}
}

View File

@ -0,0 +1,185 @@
using System;
using System.Text.RegularExpressions;
namespace StardewModdingAPI.Common
{
/// <summary>A low-level implementation of a semantic version with an optional release tag.</summary>
/// <remarks>The implementation is defined by Semantic Version 2.0 (http://semver.org/).</remarks>
internal class SemanticVersionImpl
{
/*********
** Accessors
*********/
/// <summary>The major version incremented for major API changes.</summary>
public int Major { get; }
/// <summary>The minor version incremented for backwards-compatible changes.</summary>
public int Minor { get; }
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
public int Patch { get; }
/// <summary>An optional prerelease tag.</summary>
public string Tag { get; }
/// <summary>A regular expression matching a semantic version string.</summary>
/// <remarks>
/// This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>,
/// with three important deviations intended to support Stardew Valley mod conventions:
/// - allows short-form "x.y" versions;
/// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3");
/// - doesn't allow '+build' suffixes.
/// </remarks>
internal static readonly Regex Regex = new Regex(@"^(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture);
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="major">The major version incremented for major API changes.</param>
/// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
/// <param name="patch">The patch version for backwards-compatible bug fixes.</param>
/// <param name="tag">An optional prerelease tag.</param>
public SemanticVersionImpl(int major, int minor, int patch, string tag = null)
{
this.Major = major;
this.Minor = minor;
this.Patch = patch;
this.Tag = this.GetNormalisedTag(tag);
}
/// <summary>Construct an instance.</summary>
/// <param name="version">The semantic version string.</param>
/// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception>
/// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception>
public SemanticVersionImpl(string version)
{
// parse
if (version == null)
throw new ArgumentNullException(nameof(version), "The input version string can't be null.");
var match = SemanticVersionImpl.Regex.Match(version.Trim());
if (!match.Success)
throw new FormatException($"The input '{version}' isn't a valid semantic version.");
// initialise
this.Major = int.Parse(match.Groups["major"].Value);
this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null;
}
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
/// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
public int CompareTo(SemanticVersionImpl other)
{
if (other == null)
throw new ArgumentNullException(nameof(other));
return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag);
}
/// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="otherMajor">The major version to compare with this instance.</param>
/// <param name="otherMinor">The minor version to compare with this instance.</param>
/// <param name="otherPatch">The patch version to compare with this instance.</param>
/// <param name="otherTag">The prerelease tag to compare with this instance.</param>
public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag)
{
const int same = 0;
const int curNewer = 1;
const int curOlder = -1;
// compare stable versions
if (this.Major != otherMajor)
return this.Major.CompareTo(otherMajor);
if (this.Minor != otherMinor)
return this.Minor.CompareTo(otherMinor);
if (this.Patch != otherPatch)
return this.Patch.CompareTo(otherPatch);
if (this.Tag == otherTag)
return same;
// stable supercedes pre-release
bool curIsStable = string.IsNullOrWhiteSpace(this.Tag);
bool otherIsStable = string.IsNullOrWhiteSpace(otherTag);
if (curIsStable)
return curNewer;
if (otherIsStable)
return curOlder;
// compare two pre-release tag values
string[] curParts = this.Tag.Split('.', '-');
string[] otherParts = otherTag.Split('.', '-');
for (int i = 0; i < curParts.Length; i++)
{
// longer prerelease tag supercedes if otherwise equal
if (otherParts.Length <= i)
return curNewer;
// compare if different
if (curParts[i] != otherParts[i])
{
// compare numerically if possible
{
if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum))
return curNum.CompareTo(otherNum);
}
// else compare lexically
return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase);
}
}
// fallback (this should never happen)
return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>Get a string representation of the version.</summary>
public override string ToString()
{
// version
string result = this.Patch != 0
? $"{this.Major}.{this.Minor}.{this.Patch}"
: $"{this.Major}.{this.Minor}";
// tag
string tag = this.Tag;
if (tag != null)
result += $"-{tag}";
return result;
}
/// <summary>Parse a version string without throwing an exception if it fails.</summary>
/// <param name="version">The version string.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
internal static bool TryParse(string version, out SemanticVersionImpl parsed)
{
try
{
parsed = new SemanticVersionImpl(version);
return true;
}
catch
{
parsed = null;
return false;
}
}
/*********
** Private methods
*********/
/// <summary>Get a normalised build tag.</summary>
/// <param name="tag">The tag to normalise.</param>
private string GetNormalisedTag(string tag)
{
tag = tag?.Trim();
if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation
return null;
return tag;
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>StardewModdingAPI.Common</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Models\ModSeachModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\ModInfoModel.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SemanticVersionImpl.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Models\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="StardewModdingAPI.Common.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -135,11 +135,6 @@ namespace StardewModdingApi.Installer
/// </remarks>
public void Run(string[] args)
{
#if SMAPI_1_x
bool installArg = false;
bool uninstallArg = false;
string gamePathArg = null;
#else
/****
** read command-line arguments
****/
@ -160,7 +155,6 @@ namespace StardewModdingApi.Installer
if (pathIndex >= 1 && args.Length >= pathIndex)
gamePathArg = args[pathIndex];
}
#endif
/****
** collect details

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -36,7 +36,7 @@
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\GlobalAssemblyInfo.cs">
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Enums\ScriptAction.cs" />
@ -51,6 +51,6 @@
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\common.targets" />
<Import Project="$(SolutionDir)\prepare-install-package.targets" />
<Import Project="..\..\build\common.targets" />
<Import Project="..\..\build\prepare-install-package.targets" />
</Project>

View File

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using StardewModdingAPI.ModBuildConfig.Framework;
namespace StardewModdingAPI.ModBuildConfig
{
/// <summary>A build task which deploys the mod files and prepares a release zip.</summary>
public class DeployModTask : Task
{
/*********
** Accessors
*********/
/// <summary>The name of the mod folder.</summary>
[Required]
public string ModFolderName { get; set; }
/// <summary>The absolute or relative path to the folder which should contain the generated zip file.</summary>
[Required]
public string ModZipPath { get; set; }
/// <summary>The folder containing the project files.</summary>
[Required]
public string ProjectDir { get; set; }
/// <summary>The folder containing the build output.</summary>
[Required]
public string TargetDir { get; set; }
/// <summary>The folder containing the game files.</summary>
[Required]
public string GameDir { get; set; }
/// <summary>Whether to enable copying the mod files into the game's Mods folder.</summary>
[Required]
public bool EnableModDeploy { get; set; }
/// <summary>Whether to enable the release zip.</summary>
[Required]
public bool EnableModZip { get; set; }
/*********
** Public methods
*********/
/// <summary>When overridden in a derived class, executes the task.</summary>
/// <returns>true if the task successfully executed; otherwise, false.</returns>
public override bool Execute()
{
if (!this.EnableModDeploy && !this.EnableModZip)
return true; // nothing to do
try
{
// get mod info
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir);
// deploy mod files
if (this.EnableModDeploy)
{
string outputPath = Path.Combine(this.GameDir, "Mods", this.EscapeInvalidFilenameCharacters(this.ModFolderName));
this.Log.LogMessage(MessageImportance.High, $"The mod build package is copying the mod files to {outputPath}...");
this.CreateModFolder(package.GetFiles(), outputPath);
}
// create release zip
if (this.EnableModZip)
{
this.Log.LogMessage(MessageImportance.High, $"The mod build package is generating a release zip at {this.ModZipPath} for {this.ModFolderName}...");
this.CreateReleaseZip(package.GetFiles(), this.ModFolderName, package.GetManifestVersion(), this.ModZipPath);
}
return true;
}
catch (UserErrorException ex)
{
this.Log.LogErrorFromException(ex);
return false;
}
catch (Exception ex)
{
this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}");
return false;
}
}
/*********
** Private methods
*********/
/// <summary>Copy the mod files into the game's mod folder.</summary>
/// <param name="files">The files to include.</param>
/// <param name="modFolderPath">The folder path to create with the mod files.</param>
private void CreateModFolder(IDictionary<string, FileInfo> files, string modFolderPath)
{
foreach (var entry in files)
{
string fromPath = entry.Value.FullName;
string toPath = Path.Combine(modFolderPath, entry.Key);
// ReSharper disable once AssignNullToNotNullAttribute -- not applicable in this context
Directory.CreateDirectory(Path.GetDirectoryName(toPath));
File.Copy(fromPath, toPath, overwrite: true);
}
}
/// <summary>Create a release zip in the recommended format for uploading to mod sites.</summary>
/// <param name="files">The files to include.</param>
/// <param name="modName">The name of the mod.</param>
/// <param name="modVersion">The mod version string.</param>
/// <param name="outputFolderPath">The absolute or relative path to the folder which should contain the generated zip file.</param>
private void CreateReleaseZip(IDictionary<string, FileInfo> files, string modName, string modVersion, string outputFolderPath)
{
// get names
string zipName = this.EscapeInvalidFilenameCharacters($"{modName} {modVersion}.zip");
string folderName = this.EscapeInvalidFilenameCharacters(modName);
string zipPath = Path.Combine(outputFolderPath, zipName);
// create zip file
Directory.CreateDirectory(outputFolderPath);
using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
{
foreach (var fileEntry in files)
{
string relativePath = fileEntry.Key;
FileInfo file = fileEntry.Value;
// get file info
string filePath = file.FullName;
string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
// add to zip
using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open())
fileStream.CopyTo(fileStreamInZip);
}
}
}
/// <summary>Get a copy of a filename with all invalid filename characters substituted.</summary>
/// <param name="name">The filename.</param>
private string EscapeInvalidFilenameCharacters(string name)
{
foreach (char invalidChar in Path.GetInvalidFileNameChars())
name = name.Replace(invalidChar, '.');
return name;
}
}
}

View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Script.Serialization;
using StardewModdingAPI.Common;
namespace StardewModdingAPI.ModBuildConfig.Framework
{
/// <summary>Manages the files that are part of a mod package.</summary>
internal class ModFileManager
{
/*********
** Properties
*********/
/// <summary>The name of the manifest file.</summary>
private readonly string ManifestFileName = "manifest.json";
/// <summary>The files that are part of the package.</summary>
private readonly IDictionary<string, FileInfo> Files;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="projectDir">The folder containing the project files.</param>
/// <param name="targetDir">The folder containing the build output.</param>
/// <exception cref="UserErrorException">The mod package isn't valid.</exception>
public ModFileManager(string projectDir, string targetDir)
{
this.Files = new Dictionary<string, FileInfo>(StringComparer.InvariantCultureIgnoreCase);
// validate paths
if (!Directory.Exists(projectDir))
throw new UserErrorException("Could not create mod package because the project folder wasn't found.");
if (!Directory.Exists(targetDir))
throw new UserErrorException("Could not create mod package because no build output was found.");
// project manifest
bool hasProjectManifest = false;
{
FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json"));
if (manifest.Exists)
{
this.Files[this.ManifestFileName] = manifest;
hasProjectManifest = true;
}
}
// project i18n files
bool hasProjectTranslations = false;
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
if (translationsFolder.Exists)
{
foreach (FileInfo file in translationsFolder.EnumerateFiles())
this.Files[Path.Combine("i18n", file.Name)] = file;
hasProjectTranslations = true;
}
// build output
DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
{
// get relative paths
string relativePath = file.FullName.Replace(buildFolder.FullName, "");
string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, "");
// prefer project manifest/i18n files
if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName))
continue;
if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n"))
continue;
// ignore release zips
if (this.EqualsInvariant(file.Extension, ".zip"))
continue;
// ignore Json.NET (bundled into SMAPI)
if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml"))
continue;
// add file
this.Files[relativePath] = file;
}
// check for missing manifest
if (!this.Files.ContainsKey(this.ManifestFileName))
throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output.");
// check for missing DLL
// ReSharper disable once SimplifyLinqExpression
if (!this.Files.Any(p => !p.Key.EndsWith(".dll")))
throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output.");
}
/// <summary>Get the files in the mod package.</summary>
public IDictionary<string, FileInfo> GetFiles()
{
return new Dictionary<string, FileInfo>(this.Files, StringComparer.InvariantCultureIgnoreCase);
}
/// <summary>Get a semantic version from the mod manifest.</summary>
/// <exception cref="UserErrorException">The manifest is missing or invalid.</exception>
public string GetManifestVersion()
{
// get manifest file
if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor
// read content
string json = File.ReadAllText(manifestFile.FullName);
if (string.IsNullOrWhiteSpace(json))
throw new UserErrorException("The mod's manifest must not be empty.");
// parse JSON
IDictionary<string, object> data;
try
{
data = this.Parse(json);
}
catch (Exception ex)
{
throw new UserErrorException($"The mod's manifest couldn't be parsed. It doesn't seem to be valid JSON.\n{ex}");
}
// get version field
object versionObj = data.ContainsKey("Version") ? data["Version"] : null;
if (versionObj == null)
throw new UserErrorException("The mod's manifest must have a version field.");
// get version string
if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x
{
int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0;
int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0;
int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0;
string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null;
return new SemanticVersionImpl(major, minor, patch, tag).ToString();
}
return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+
}
/*********
** Private methods
*********/
/// <summary>Get a case-insensitive dictionary matching the given JSON.</summary>
/// <param name="json">The JSON to parse.</param>
private IDictionary<string, object> Parse(string json)
{
IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict)
{
foreach (var field in dict.ToArray())
{
if (field.Value is IDictionary<string, object> value)
dict[field.Key] = MakeCaseInsensitive(value);
}
return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase);
}
IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
return MakeCaseInsensitive(data);
}
/// <summary>Get whether a string is equal to another case-insensitively.</summary>
/// <param name="str">The string value.</param>
/// <param name="other">The string to compare with.</param>
private bool EqualsInvariant(string str, string other)
{
return str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace StardewModdingAPI.ModBuildConfig.Framework
{
/// <summary>A user error whose message can be displayed to the user.</summary>
internal class UserErrorException : Exception
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="message">The error message.</param>
public UserErrorException(string message)
: base(message) { }
}
}

View File

@ -0,0 +1,9 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("StardewModdingAPI.ModBuildConfig")]
[assembly: AssemblyDescription("")]
[assembly: Guid("ea4f1e80-743f-4a1d-9757-ae66904a196a")]
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("2.0.1.0")]
[assembly: AssemblyFileVersion("2.0.1.0")]

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{EA4F1E80-743F-4A1D-9757-AE66904A196A}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
<AssemblyName>StardewModdingAPI.ModBuildConfig</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Build" />
<Reference Include="Microsoft.Build.Framework" />
<Reference Include="Microsoft.Build.Utilities.v4.0" />
<Reference Include="System" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<Compile Include="DeployModTask.cs" />
<Compile Include="Framework\UserErrorException.cs" />
<Compile Include="Framework\ModFileManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="assets\nuget-icon.pdn" />
<None Include="build\smapi.targets">
<SubType>Designer</SubType>
</None>
<None Include="package.nuspec">
<SubType>Designer</SubType>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="assets\nuget-icon.png" />
</ItemGroup>
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,144 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--*********************************************
** Import build tasks
**********************************************-->
<UsingTask TaskName="DeployModTask" AssemblyFile="StardewModdingAPI.ModBuildConfig.dll" />
<!--*********************************************
** Find the basic mod metadata
**********************************************-->
<!-- import developer's custom settings (if any) -->
<Import Condition="$(OS) != 'Windows_NT' AND Exists('$(HOME)\stardewvalley.targets')" Project="$(HOME)\stardewvalley.targets" />
<Import Condition="$(OS) == 'Windows_NT' AND Exists('$(USERPROFILE)\stardewvalley.targets')" Project="$(USERPROFILE)\stardewvalley.targets" />
<!-- set setting defaults -->
<PropertyGroup>
<!-- map legacy settings -->
<ModFolderName Condition="'$(ModFolderName)' == '' AND '$(DeployModFolderName)' != ''">$(DeployModFolderName)</ModFolderName>
<ModZipPath Condition="'$(ModZipPath)' == '' AND '$(DeployModZipTo)' != ''">$(DeployModZipTo)</ModZipPath>
<!-- set default settings -->
<ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName>
<ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath>
<EnableModDeploy Condition="'$(EnableModDeploy)' == ''">True</EnableModDeploy>
<EnableModZip Condition="'$(EnableModZip)' == ''">True</EnableModZip>
</PropertyGroup>
<!-- find platform + game path -->
<Choose>
<When Condition="$(OS) == 'Unix' OR $(OS) == 'OSX'">
<PropertyGroup>
<!-- Linux -->
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/GOG Games/Stardew Valley/game</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/.local/share/Steam/steamapps/common/Stardew Valley</GamePath>
<!-- Mac (may be 'Unix' or 'OSX') -->
<GamePath Condition="!Exists('$(GamePath)')">/Applications/Stardew Valley.app/Contents/MacOS</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS</GamePath>
</PropertyGroup>
</When>
<When Condition="$(OS) == 'Windows_NT'">
<PropertyGroup>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32))</GamePath>
<GamePath Condition="!Exists('$(GamePath)')">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32))</GamePath>
</PropertyGroup>
</When>
</Choose>
<!--*********************************************
** Inject the assembly references and debugging configuration
**********************************************-->
<Choose>
<When Condition="$(OS) == 'Windows_NT'">
<!-- references -->
<ItemGroup>
<Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>false</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>false</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>false</Private>
</Reference>
<Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86">
<Private>false</Private>
</Reference>
<Reference Include="Stardew Valley">
<HintPath>$(GamePath)\Stardew Valley.exe</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="StardewModdingAPI">
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86">
<HintPath>$(GamePath)\xTile.dll</HintPath>
<Private>false</Private>
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>
<!-- launch game for debugging -->
<PropertyGroup>
<StartAction>Program</StartAction>
<StartProgram>$(GamePath)\StardewModdingAPI.exe</StartProgram>
<StartWorkingDirectory>$(GamePath)</StartWorkingDirectory>
</PropertyGroup>
</When>
<Otherwise>
<!-- references -->
<ItemGroup>
<Reference Include="MonoGame.Framework">
<HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath>
<Private>false</Private>
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="StardewValley">
<HintPath>$(GamePath)\StardewValley.exe</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="StardewModdingAPI">
<HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="xTile">
<HintPath>$(GamePath)\xTile.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>
<!--*********************************************
** Deploy mod files & create release zip after build
**********************************************-->
<!-- if game path or OS is invalid, show one user-friendly error instead of a slew of reference errors -->
<Target Name="BeforeBuild">
<Error Condition="'$(OS)' != 'OSX' AND '$(OS)' != 'Unix' AND '$(OS)' != 'Windows_NT'" Text="The mod build package doesn't recognise OS type '$(OS)'." />
<Error Condition="!Exists('$(GamePath)')" Text="The mod build package can't find your game folder. You can specify where to find it; see details at https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#game-path." />
<Error Condition="'$(OS)' == 'Windows_NT' AND !Exists('$(GamePath)\Stardew Valley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the Stardew Valley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." />
<Error Condition="'$(OS)' != 'Windows_NT' AND !Exists('$(GamePath)\StardewValley.exe')" Text="The mod build package found a a game folder at $(GamePath), but it doesn't contain the StardewValley.exe file. If this folder is invalid, delete it and the package will autodetect another game install path." />
<Error Condition="!Exists('$(GamePath)\StardewModdingAPI.exe')" Text="The mod build package found a game folder at $(GamePath), but it doesn't contain SMAPI. You need to install SMAPI before building the mod." />
</Target>
<!-- deploy mod files & create release zip -->
<Target Name="AfterBuild">
<DeployModTask
ModFolderName="$(ModFolderName)"
ModZipPath="$(ModZipPath)"
EnableModDeploy="$(EnableModDeploy)"
EnableModZip="$(EnableModZip)"
ProjectDir="$(ProjectDir)"
TargetDir="$(TargetDir)"
GameDir="$(GamePath)"
/>
</Target>
</Project>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>Pathoschild.Stardew.ModBuildConfig</id>
<version>2.0.1</version>
<title>Build package for SMAPI mods</title>
<authors>Pathoschild</authors>
<owners>Pathoschild</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<licenseUrl>https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt</licenseUrl>
<projectUrl>https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme</projectUrl>
<iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl>
<description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods.</description>
<releaseNotes>
2.0:
- Added: mods are now copied into the `Mods` folder automatically (configurable).
- Added: release zips are now created automatically in your build output folder (configurable).
- Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
- Added mod's version to release zip filename.
- Improved errors to simplify troubleshooting.
- Fixed release zip not having a mod folder.
- Fixed release zip failing if mod name contains characters that aren't valid in a filename.
2.0.1:
- Fixed mod deploy failing to create subfolders if they don't already exist.
</releaseNotes>
</metadata>
<files>
<file src="build/smapi.targets" target="build/Pathoschild.Stardew.ModBuildConfig.targets" />
<file src="bin/StardewModdingAPI.ModBuildConfig.dll" target="build/StardewModdingAPI.ModBuildConfig.dll" />
</files>
</package>

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -30,7 +30,7 @@ namespace StardewModdingAPI.Tests.Core
Directory.CreateDirectory(rootFolder);
// act
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray();
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray();
// assert
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
@ -45,7 +45,7 @@ namespace StardewModdingAPI.Tests.Core
Directory.CreateDirectory(modFolder);
// act
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray();
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray();
IModMetadata mod = mods.FirstOrDefault();
// assert
@ -84,13 +84,13 @@ namespace StardewModdingAPI.Tests.Core
File.WriteAllText(filename, JsonConvert.SerializeObject(original));
// act
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray();
IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray();
IModMetadata mod = mods.FirstOrDefault();
// assert
Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest.");
Assert.IsNotNull(mod, "The loaded manifest shouldn't be null.");
Assert.AreEqual(null, mod.Compatibility, "The compatibility record should be null since we didn't provide one.");
Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one.");
Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match.");
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded.");
@ -119,7 +119,7 @@ namespace StardewModdingAPI.Tests.Core
[Test(Description = "Assert that validation doesn't fail if there are no mods installed.")]
public void ValidateManifests_NoMods_DoesNothing()
{
new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
}
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
@ -130,21 +130,25 @@ namespace StardewModdingAPI.Tests.Core
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
// act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
}
[Test(Description = "Assert that validation fails if the mod has 'assume broken' compatibility.")]
public void ValidateManifests_ModCompatibility_AssumeBroken_Fails()
[Test(Description = "Assert that validation fails if the mod has 'assume broken' status.")]
public void ValidateManifests_ModStatus_AssumeBroken_Fails()
{
// arrange
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" }});
this.SetupMetadataForValidation(mock, new ModDataRecord
{
Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) },
AlternativeUrl = "http://example.org"
});
// act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@ -159,7 +163,7 @@ namespace StardewModdingAPI.Tests.Core
this.SetupMetadataForValidation(mock);
// act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@ -173,13 +177,12 @@ namespace StardewModdingAPI.Tests.Core
this.SetupMetadataForValidation(mock);
// act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
}
#if !SMAPI_1_x
[Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")]
public void ValidateManifests_DuplicateUniqueID_Fails()
{
@ -191,13 +194,12 @@ namespace StardewModdingAPI.Tests.Core
this.SetupMetadataForValidation(mod);
// act
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the first mod with a unique ID.");
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the second mod with a unique ID.");
}
#endif
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
public void ValidateManifests_Valid_Passes()
@ -213,12 +215,12 @@ namespace StardewModdingAPI.Tests.Core
// arrange
Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mock.Setup(p => p.Compatibility).Returns(() => null);
mock.Setup(p => p.DataRecord).Returns(() => null);
mock.Setup(p => p.Manifest).Returns(manifest);
mock.Setup(p => p.DirectoryPath).Returns(modFolder);
// act
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"));
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary<string, string>());
// assert
// if Moq doesn't throw a method-not-setup exception, the validation didn't override the status.
@ -423,7 +425,6 @@ namespace StardewModdingAPI.Tests.Core
Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A.");
}
#if !SMAPI_1_x
[Test(Description = "Assert that optional dependencies are sorted correctly if present.")]
public void ProcessDependencies_IfOptional()
{
@ -455,7 +456,6 @@ namespace StardewModdingAPI.Tests.Core
Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input.");
Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod.");
}
#endif
/*********
@ -527,7 +527,7 @@ namespace StardewModdingAPI.Tests.Core
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false)
{
Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict);
mod.Setup(p => p.Compatibility).Returns(() => null);
mod.Setup(p => p.DataRecord).Returns(() => null);
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID);
mod.Setup(p => p.Manifest).Returns(manifest);
@ -543,14 +543,14 @@ namespace StardewModdingAPI.Tests.Core
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
/// <param name="mod">The mock mod metadata.</param>
/// <param name="compatibility">The compatibility record to set.</param>
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModCompatibility compatibility = null)
/// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecord modRecord = null)
{
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.Compatibility).Returns(() => null);
mod.Setup(p => p.DataRecord).Returns(() => null);
mod.Setup(p => p.Manifest).Returns(this.GetManifest());
mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath());
mod.Setup(p => p.Compatibility).Returns(compatibility);
mod.Setup(p => p.DataRecord).Returns(modRecord);
}
}
}

View File

@ -1,8 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModHelpers;
using StardewValley;

View File

@ -30,22 +30,23 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Castle.Core, Version=4.1.1.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll</HintPath>
<Reference Include="Castle.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc, processorArchitecture=MSIL">
<HintPath>..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll</HintPath>
</Reference>
<Reference Include="Moq, Version=4.7.99.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.7.99\lib\net45\Moq.dll</HintPath>
<Reference Include="Moq, Version=4.7.142.0, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\packages\Moq.4.7.142\lib\net45\Moq.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.7.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\GlobalAssemblyInfo.cs">
<Compile Include="..\..\build\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Utilities\SemanticVersionTests.cs" />
@ -59,11 +60,11 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StardewModdingAPI\StardewModdingAPI.csproj">
<ProjectReference Include="..\SMAPI\StardewModdingAPI.csproj">
<Project>{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}</Project>
<Name>StardewModdingAPI</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\common.targets" />
<Import Project="..\..\build\common.targets" />
</Project>

View File

@ -69,6 +69,8 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase(01, "Spring", 1)] // seasons are case-sensitive
[TestCase(01, "springs", 1)] // invalid season name
[TestCase(-1, "spring", 1)] // day < 0
[TestCase(0, "spring", 1)] // day zero
[TestCase(0, "spring", 2)] // day zero
[TestCase(29, "spring", 1)] // day > 28
[TestCase(01, "spring", -1)] // year < 1
[TestCase(01, "spring", 0)] // year < 1

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Castle.Core" version="4.2.1" targetFramework="net45" />
<package id="Moq" version="4.7.142" targetFramework="net45" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" />
<package id="NUnit" version="3.8.1" targetFramework="net45" />
</packages>

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Common.Models;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Controllers
{
/// <summary>Provides an API to perform mod update checks.</summary>
[Produces("application/json")]
[Route("api/{version:semanticVersion}/[controller]")]
internal class ModsController : Controller
{
/*********
** Properties
*********/
/// <summary>The mod repositories which provide mod metadata.</summary>
private readonly IDictionary<string, IModRepository> Repositories;
/// <summary>The cache in which to store mod metadata.</summary>
private readonly IMemoryCache Cache;
/// <summary>The number of minutes update checks should be cached before refetching them.</summary>
private readonly int CacheMinutes;
/// <summary>A regex which matches SMAPI-style semantic version.</summary>
private readonly string VersionRegex;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param>
public ModsController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider)
{
ModUpdateCheckConfig config = configProvider.Value;
this.Cache = cache;
this.CacheMinutes = config.CacheMinutes;
this.VersionRegex = config.SemanticVersionRegex;
string version = this.GetType().Assembly.GetName().Version.ToString(3);
this.Repositories =
new IModRepository[]
{
new ChucklefishRepository(
vendorKey: config.ChucklefishKey,
userAgent: string.Format(config.ChucklefishUserAgent, version),
baseUrl: config.ChucklefishBaseUrl,
modPageUrlFormat: config.ChucklefishModPageUrlFormat
),
new GitHubRepository(
vendorKey: config.GitHubKey,
baseUrl: config.GitHubBaseUrl,
releaseUrlFormat: config.GitHubReleaseUrlFormat,
userAgent: string.Format(config.GitHubUserAgent, version),
acceptHeader: config.GitHubAcceptHeader,
username: config.GitHubUsername,
password: config.GitHubPassword
),
new NexusRepository(
vendorKey: config.NexusKey,
userAgent: config.NexusUserAgent,
baseUrl: config.NexusBaseUrl,
modUrlFormat: config.NexusModUrlFormat
)
}
.ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
}
/// <summary>Fetch version metadata for the given mods.</summary>
/// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param>
[HttpGet]
public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys)
{
string[] modKeysArray = modKeys?.Split(',').ToArray();
if (modKeysArray == null || !modKeysArray.Any())
return new Dictionary<string, ModInfoModel>();
return await this.PostAsync(new ModSearchModel(modKeysArray));
}
/// <summary>Fetch version metadata for the given mods.</summary>
/// <param name="search">The mod search criteria.</param>
[HttpPost]
public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search)
{
// sort & filter keys
string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0])
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
// fetch mod info
IDictionary<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase);
foreach (string modKey in modKeys)
{
// parse mod key
if (!this.TryParseModKey(modKey, out string vendorKey, out string modID))
{
result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
continue;
}
// get matching repository
if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
{
result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
continue;
}
// fetch mod info
result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes);
ModInfoModel info = await repository.GetModInfoAsync(modID);
if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)))
info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'.");
return info;
});
}
return result;
}
/*********
** Private methods
*********/
/// <summary>Parse a namespaced mod ID.</summary>
/// <param name="raw">The raw mod ID to parse.</param>
/// <param name="vendorKey">The parsed vendor key.</param>
/// <param name="modID">The parsed mod ID.</param>
/// <returns>Returns whether the value could be parsed.</returns>
private bool TryParseModKey(string raw, out string vendorKey, out string modID)
{
// split parts
string[] parts = raw?.Split(':');
if (parts == null || parts.Length != 2)
{
vendorKey = null;
modID = null;
return false;
}
// parse
vendorKey = parts[0].Trim();
modID = parts[1].Trim();
return true;
}
}
}

View File

@ -0,0 +1,74 @@
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
/// <summary>The config settings for mod update checks.</summary>
public class ModUpdateCheckConfig
{
/*********
** Accessors
*********/
/****
** General
****/
/// <summary>The number of minutes update checks should be cached before refetching them.</summary>
public int CacheMinutes { get; set; }
/// <summary>A regex which matches SMAPI-style semantic version.</summary>
/// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks>
public string SemanticVersionRegex { get; set; }
/****
** Chucklefish mod site
****/
/// <summary>The repository key for the Chucklefish mod site.</summary>
public string ChucklefishKey { get; set; }
/// <summary>The user agent for the Chucklefish API client, where {0} is the SMAPI version.</summary>
public string ChucklefishUserAgent { get; set; }
/// <summary>The base URL for the Chucklefish mod site.</summary>
public string ChucklefishBaseUrl { get; set; }
/// <summary>The URL for a mod page on the Chucklefish mod site excluding the <see cref="GitHubBaseUrl"/>, where {0} is the mod ID.</summary>
public string ChucklefishModPageUrlFormat { get; set; }
/****
** GitHub
****/
/// <summary>The repository key for Nexus Mods.</summary>
public string GitHubKey { get; set; }
/// <summary>The user agent for the GitHub API client, where {0} is the SMAPI version.</summary>
public string GitHubUserAgent { get; set; }
/// <summary>The base URL for the GitHub API.</summary>
public string GitHubBaseUrl { get; set; }
/// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
public string GitHubReleaseUrlFormat { get; set; }
/// <summary>The Accept header value expected by the GitHub API.</summary>
public string GitHubAcceptHeader { get; set; }
/// <summary>The username with which to authenticate to the GitHub API (if any).</summary>
public string GitHubUsername { get; set; }
/// <summary>The password with which to authenticate to the GitHub API (if any).</summary>
public string GitHubPassword { get; set; }
/****
** Nexus Mods
****/
/// <summary>The repository key for Nexus Mods.</summary>
public string NexusKey { get; set; }
/// <summary>The user agent for the Nexus Mods API client.</summary>
public string NexusUserAgent { get; set; }
/// <summary>The base URL for the Nexus Mods API.</summary>
public string NexusBaseUrl { get; set; }
/// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
public string NexusModUrlFormat { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
namespace StardewModdingAPI.Web.Framework
{
/// <summary>Discovers controllers with support for non-public controllers.</summary>
internal class InternalControllerFeatureProvider : ControllerFeatureProvider
{
/*********
** Public methods
*********/
/// <summary>Determines if a given type is a controller.</summary>
/// <param name="type">The <see cref="T:System.Reflection.TypeInfo" /> candidate.</param>
/// <returns><code>true</code> if the type is a controller; otherwise <code>false</code>.</returns>
protected override bool IsController(TypeInfo type)
{
return
type.IsClass
&& !type.IsAbstract
&& (/*type.IsPublic &&*/ !type.ContainsGenericParameters)
&& (!type.IsDefined(typeof(NonControllerAttribute))
&& (type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || type.IsDefined(typeof(ControllerAttribute))));
}
}
}

View File

@ -0,0 +1,51 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
internal abstract class RepositoryBase : IModRepository
{
/*********
** Accessors
*********/
/// <summary>The unique key for this vendor.</summary>
public string VendorKey { get; }
/*********
** Public methods
*********/
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public abstract void Dispose();
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
public abstract Task<ModInfoModel> GetModInfoAsync(string id);
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
protected RepositoryBase(string vendorKey)
{
this.VendorKey = vendorKey;
}
/// <summary>Normalise a version string.</summary>
/// <param name="version">The version to normalise.</param>
protected string NormaliseVersion(string version)
{
if (string.IsNullOrWhiteSpace(version))
return null;
version = version.Trim();
if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix
version = version.Substring(1);
return version;
}
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.Net;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
/// <summary>An HTTP client for fetching mod metadata from the Chucklefish mod site.</summary>
internal class ChucklefishRepository : RepositoryBase
{
/*********
** Properties
*********/
/// <summary>The base URL for the Chucklefish mod site.</summary>
private readonly string BaseUrl;
/// <summary>The URL for a mod page excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ModPageUrlFormat;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="baseUrl">The base URL for the Chucklefish mod site.</param>
/// <param name="modPageUrlFormat">The URL for a mod page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat)
: base(vendorKey)
{
this.BaseUrl = baseUrl;
this.ModPageUrlFormat = modPageUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
}
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
if (!uint.TryParse(id, out uint _))
return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
// fetch info
try
{
// fetch HTML
string html;
try
{
html = await this.Client
.GetAsync(string.Format(this.ModPageUrlFormat, id))
.AsString();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new ModInfoModel("Found no mod with this ID.");
}
// parse HTML
var doc = new HtmlDocument();
doc.LoadHtml(html);
// extract mod info
string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString();
string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value;
if (name.StartsWith("[SMAPI] "))
name = name.Substring("[SMAPI] ".Length);
string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText;
// create model
return new ModInfoModel(name, this.NormaliseVersion(version), url);
}
catch (Exception ex)
{
return new ModInfoModel(ex.ToString());
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public override void Dispose()
{
this.Client.Dispose();
}
}
}

View File

@ -0,0 +1,97 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
/// <summary>An HTTP client for fetching mod metadata from GitHub project releases.</summary>
internal class GitHubRepository : RepositoryBase
{
/*********
** Properties
*********/
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ReleaseUrlFormat;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
/// <param name="releaseUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
/// <param name="userAgent">The user agent for the API client.</param>
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
: base(vendorKey)
{
this.ReleaseUrlFormat = releaseUrlFormat;
this.Client = new FluentClient(baseUrl)
.SetUserAgent(userAgent)
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
if (!string.IsNullOrWhiteSpace(username))
this.Client = this.Client.SetBasicAuthentication(username, password);
}
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
// fetch info
try
{
GitRelease release = await this.Client
.GetAsync(string.Format(this.ReleaseUrlFormat, id))
.As<GitRelease>();
return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases");
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
{
return new ModInfoModel("Found no mod with this ID.");
}
catch (Exception ex)
{
return new ModInfoModel(ex.ToString());
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public override void Dispose()
{
this.Client.Dispose();
}
/*********
** Private models
*********/
/// <summary>Metadata about a GitHub release tag.</summary>
private class GitRelease
{
/*********
** Accessors
*********/
/// <summary>The display name.</summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>The semantic version string.</summary>
[JsonProperty("tag_name")]
public string Tag { get; set; }
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
/// <summary>A repository which provides mod metadata.</summary>
internal interface IModRepository : IDisposable
{
/*********
** Accessors
*********/
/// <summary>The unique key for this vendor.</summary>
string VendorKey { get; }
/*********
** Public methods
*********/
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
Task<ModInfoModel> GetModInfoAsync(string id);
}
}

View File

@ -0,0 +1,89 @@
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Pathoschild.Http.Client;
using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
/// <summary>An HTTP client for fetching mod metadata from Nexus Mods.</summary>
internal class NexusRepository : RepositoryBase
{
/*********
** Properties
*********/
/// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ModUrlFormat;
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="vendorKey">The unique key for this vendor.</param>
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
/// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat)
: base(vendorKey)
{
this.ModUrlFormat = modUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
}
/// <summary>Get metadata about a mod in the repository.</summary>
/// <param name="id">The mod ID in this repository.</param>
public override async Task<ModInfoModel> GetModInfoAsync(string id)
{
// validate ID format
if (!uint.TryParse(id, out uint _))
return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
// fetch info
try
{
NexusResponseModel response = await this.Client
.GetAsync(string.Format(this.ModUrlFormat, id))
.As<NexusResponseModel>();
return response != null
? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url)
: new ModInfoModel("Found no mod with this ID.");
}
catch (Exception ex)
{
return new ModInfoModel(ex.ToString());
}
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public override void Dispose()
{
this.Client.Dispose();
}
/*********
** Private models
*********/
/// <summary>A mod metadata response from Nexus Mods.</summary>
private class NexusResponseModel
{
/*********
** Accessors
*********/
/// <summary>The mod name.</summary>
public string Name { get; set; }
/// <summary>The mod's semantic version number.</summary>
public string Version { get; set; }
/// <summary>The mod's web URL.</summary>
[JsonProperty("mod_page_uri")]
public string Url { get; set; }
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using Microsoft.AspNetCore.Rewrite;
namespace StardewModdingAPI.Web.Framework
{
/// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary>
/// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks>
internal class RewriteSubdomainRule : IRule
{
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
/// <param name="context">The rewrite context.</param>
public void ApplyRule(RewriteContext context)
{
context.Result = RuleResult.ContinueRules;
// get host parts
string host = context.HttpContext.Request.Host.Host;
string[] parts = host.Split('.');
// validate
if (parts.Length < 2)
return;
if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase))
return;
// prepend to path
context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}";
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Routing.Constraints;
using StardewModdingAPI.Common;
namespace StardewModdingAPI.Web.Framework
{
/// <summary>Constrains a route value to a valid semantic version.</summary>
internal class VersionConstraint : RegexRouteConstraint
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public VersionConstraint()
: base(SemanticVersionImpl.Regex) { }
}
}

26
src/SMAPI.Web/Program.cs Normal file
View File

@ -0,0 +1,26 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace StardewModdingAPI.Web
{
/// <summary>The main app entry point.</summary>
public class Program
{
/*********
** Public methods
*********/
/// <summary>The main app entry point.</summary>
/// <param name="args">The command-line arguments.</param>
public static void Main(string[] args)
{
// configure web server
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build()
.Run();
}
}
}

View File

@ -0,0 +1,4 @@
using System.Reflection;
[assembly: AssemblyTitle("StardewModdingAPI.Web")]
[assembly: AssemblyProduct("StardewModdingAPI.Web")]

View File

@ -0,0 +1,29 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59482/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Dewdrop": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:59483"
}
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.6.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
</ItemGroup>
<Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
</Project>

70
src/SMAPI.Web/Startup.cs Normal file
View File

@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.ConfigModels;
namespace StardewModdingAPI.Web
{
/// <summary>The web app startup configuration.</summary>
internal class Startup
{
/*********
** Accessors
*********/
/// <summary>The web app configuration.</summary>
public IConfigurationRoot Configuration { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="env">The hosting environment.</param>
public Startup(IHostingEnvironment env)
{
this.Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddEnvironmentVariables()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
/// <summary>The method called by the runtime to add services to the container.</summary>
/// <param name="services">The service injection container.</param>
public void ConfigureServices(IServiceCollection services)
{
services
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddMemoryCache()
.AddMvc()
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
.AddJsonOptions(options =>
{
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
}
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
/// <param name="app">The application builder.</param>
/// <param name="env">The hosting environment.</param>
/// <param name="loggerFactory">The logger factory.</param>
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app
.UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing
.UseMvc();
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,30 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Warning"
}
},
"ModUpdateCheck": {
"CacheMinutes": 60,
"SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$",
"ChucklefishKey": "Chucklefish",
"ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"ChucklefishBaseUrl": "https://community.playstarbound.com",
"ChucklefishModPageUrlFormat": "resources/{0}",
"GitHubKey": "GitHub",
"GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)",
"GitHubBaseUrl": "https://api.github.com",
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest",
"GitHubAcceptHeader": "application/vnd.github.v3+json",
"GitHubUsername": null, /* set via environment properties */
"GitHubPassword": null, /* set via environment properties */
"NexusKey": "Nexus",
"NexusUserAgent": "Nexus Client v0.63.15",
"NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
"NexusModUrlFormat": "mods/{0}"
}
}

View File

@ -1,37 +1,59 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26430.16
VisualStudioVersion = 15.0.26730.16
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "StardewModdingAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
..\.editorconfig = ..\.editorconfig
..\.gitattributes = ..\.gitattributes
..\.gitignore = ..\.gitignore
common.targets = common.targets
..\CONTRIBUTING.md = ..\CONTRIBUTING.md
GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs
..\LICENSE = ..\LICENSE
prepare-install-package.targets = prepare-install-package.targets
..\README.md = ..\README.md
..\release-notes.md = ..\release-notes.md
..\LICENSE.txt = ..\LICENSE.txt
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "StardewModdingAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}"
ProjectSection(ProjectDependencies) = postProject
{28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359}
{F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Common", "SMAPI.Common\StardewModdingAPI.Common.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}"
ProjectSection(SolutionItems) = preProject
..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md
..\docs\mod-build-config.md = ..\docs\mod-build-config.md
..\docs\README.md = ..\docs\README.md
..\docs\release-notes.md = ..\docs\release-notes.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}"
ProjectSection(SolutionItems) = preProject
..\build\common.targets = ..\build\common.targets
..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs
..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{EA4F1E80-743F-4A1D-9757-AE66904A196A}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13
SMAPI.Common\StardewModdingAPI.Common.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4
SMAPI.Common\StardewModdingAPI.Common.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
@ -91,8 +113,40 @@ Global
{36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.Build.0 = Release|x86
{36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86
{36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU
{A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.ActiveCfg = Debug|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.Build.0 = Debug|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.ActiveCfg = Release|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.Build.0 = Release|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86
{EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
{36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}
{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA}
{09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC}
EndGlobalSection
EndGlobal

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InheritdocConsiderUsage/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeArgumentsOfMethod/@EntryIndexedValue">HINT</s:String>

View File

@ -3,13 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.AssemblyRewriters.Finders;
using StardewModdingAPI.AssemblyRewriters.Rewriters;
using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
using StardewValley;
namespace StardewModdingAPI
@ -34,12 +29,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
public static ISemanticVersion ApiVersion { get; } =
#if SMAPI_1_x
new SemanticVersion(1, 15, 4);
#else
new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}");
#endif
public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0);
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");
@ -97,6 +87,14 @@ namespace StardewModdingAPI
Platform.Mono;
#endif
/// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API.</summary>
internal static readonly IDictionary<string, string> VendorModUrls = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
{
["Chucklefish"] = "https://community.playstarbound.com/resources/{0}",
["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}",
["GitHub"] = "https://github.com/{0}/releases"
};
/*********
** Internal methods
@ -147,79 +145,6 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
}
/// <summary>Get rewriters which detect or fix incompatible CIL instructions in mod assemblies.</summary>
internal static IEnumerable<IInstructionRewriter> GetRewriters()
{
return new IInstructionRewriter[]
{
/****
** Finders throw an exception when incompatible code is found.
****/
// changes in Stardew Valley 1.2 (with no rewriters)
new FieldFinder("StardewValley.Item", "set_Name"),
// APIs removed in SMAPI 1.9
new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"),
new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"),
new TypeFinder("StardewModdingAPI.Entities.SPlayer"),
new TypeFinder("StardewModdingAPI.Extensions"),
new TypeFinder("StardewModdingAPI.Inheritance.SGame"),
new TypeFinder("StardewModdingAPI.Inheritance.SObject"),
new TypeFinder("StardewModdingAPI.LogWriter"),
new TypeFinder("StardewModdingAPI.Manifest"),
new TypeFinder("StardewModdingAPI.Version"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"),
new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"),
// APIs removed in SMAPI 2.0
#if !SMAPI_1_x
new TypeFinder("StardewModdingAPI.Command"),
new TypeFinder("StardewModdingAPI.Config"),
new TypeFinder("StardewModdingAPI.Log"),
new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"),
new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"),
new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"),
new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"),
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"),
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"),
new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"),
new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"),
new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"),
new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"),
new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"),
new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"),
new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"),
new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"),
new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"),
#endif
/****
** Rewriters change CIL as needed to fix incompatible code
****/
// crossplatform
new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true),
// Stardew Valley 1.2
new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)),
new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)),
new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)),
new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)),
new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)),
new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)),
// SMAPI 1.9
new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange))
};
}
/*********
** Private methods

View File

@ -1,4 +1,3 @@
#if !SMAPI_1_x
using System;
using System.Linq;
using Microsoft.Xna.Framework;
@ -123,4 +122,3 @@ namespace StardewModdingAPI.Events
}
}
}
#endif

View File

@ -0,0 +1,96 @@
using System;
using StardewModdingAPI.Framework;
namespace StardewModdingAPI.Events
{
/// <summary>Events raised when the game changes state.</summary>
public static class GameEvents
{
/*********
** Events
*********/
/// <summary>Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after <see cref="Microsoft.Xna.Framework.Game.Initialize"/>.</summary>
internal static event EventHandler InitializeInternal;
/// <summary>Raised when the game updates its state (≈60 times per second).</summary>
public static event EventHandler UpdateTick;
/// <summary>Raised every other tick (≈30 times per second).</summary>
public static event EventHandler SecondUpdateTick;
/// <summary>Raised every fourth tick (≈15 times per second).</summary>
public static event EventHandler FourthUpdateTick;
/// <summary>Raised every eighth tick (≈8 times per second).</summary>
public static event EventHandler EighthUpdateTick;
/// <summary>Raised every 15th tick (≈4 times per second).</summary>
public static event EventHandler QuarterSecondTick;
/// <summary>Raised every 30th tick (≈twice per second).</summary>
public static event EventHandler HalfSecondTick;
/// <summary>Raised every 60th tick (≈once per second).</summary>
public static event EventHandler OneSecondTick;
/*********
** Internal methods
*********/
/// <summary>Raise an <see cref="InitializeInternal"/> event.</summary>
/// <param name="monitor">Encapsulates logging and monitoring.</param>
internal static void InvokeInitialize(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList());
}
/// <summary>Raise an <see cref="UpdateTick"/> event.</summary>
/// <param name="monitor">Encapsulates logging and monitoring.</param>
internal static void InvokeUpdateTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.UpdateTick)}", GameEvents.UpdateTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="SecondUpdateTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeSecondUpdateTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.SecondUpdateTick)}", GameEvents.SecondUpdateTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="FourthUpdateTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeFourthUpdateTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FourthUpdateTick)}", GameEvents.FourthUpdateTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="EighthUpdateTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeEighthUpdateTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.EighthUpdateTick)}", GameEvents.EighthUpdateTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="QuarterSecondTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeQuarterSecondTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.QuarterSecondTick)}", GameEvents.QuarterSecondTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="HalfSecondTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeHalfSecondTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.HalfSecondTick)}", GameEvents.HalfSecondTick?.GetInvocationList());
}
/// <summary>Raise a <see cref="OneSecondTick"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeOneSecondTick(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
}
}
}

View File

@ -1,4 +1,3 @@
#if !SMAPI_1_x
using System;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Utilities;
@ -42,4 +41,3 @@ namespace StardewModdingAPI.Events
}
}
}
#endif

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Framework;
using StardewValley;
namespace StardewModdingAPI.Events
{
/// <summary>Events raised when the player data changes.</summary>
public static class PlayerEvents
{
/*********
** Events
*********/
/// <summary>Raised after the player's inventory changes in any way (added or removed item, sorted, etc).</summary>
public static event EventHandler<EventArgsInventoryChanged> InventoryChanged;
/// <summary> Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed.</summary>
public static event EventHandler<EventArgsLevelUp> LeveledUp;
/*********
** Internal methods
*********/
/// <summary>Raise an <see cref="InventoryChanged"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="inventory">The player's inventory.</param>
/// <param name="changedItems">The inventory changes.</param>
internal static void InvokeInventoryChanged(IMonitor monitor, List<Item> inventory, IEnumerable<ItemStackChange> changedItems)
{
monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.InventoryChanged)}", PlayerEvents.InventoryChanged?.GetInvocationList(), null, new EventArgsInventoryChanged(inventory, changedItems.ToList()));
}
/// <summary>Rase a <see cref="LeveledUp"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="type">The player skill that leveled up.</param>
/// <param name="newLevel">The new skill level.</param>
internal static void InvokeLeveledUp(IMonitor monitor, EventArgsLevelUp.LevelType type, int newLevel)
{
monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LeveledUp)}", PlayerEvents.LeveledUp?.GetInvocationList(), null, new EventArgsLevelUp(type, newLevel));
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using StardewModdingAPI.Framework;
namespace StardewModdingAPI.Events
{
/// <summary>Events raised when the in-game date or time changes.</summary>
public static class TimeEvents
{
/*********
** Events
*********/
/// <summary>Raised after the game begins a new day, including when loading a save.</summary>
public static event EventHandler AfterDayStarted;
/// <summary>Raised after the in-game clock changes.</summary>
public static event EventHandler<EventArgsIntChanged> TimeOfDayChanged;
/*********
** Internal methods
*********/
/// <summary>Raise an <see cref="AfterDayStarted"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
internal static void InvokeAfterDayStarted(IMonitor monitor)
{
monitor.SafelyRaisePlainEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.AfterDayStarted)}", TimeEvents.AfterDayStarted?.GetInvocationList(), null, EventArgs.Empty);
}
/// <summary>Raise a <see cref="TimeOfDayChanged"/> event.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="priorTime">The previous time in military time format (e.g. 6:00pm is 1800).</param>
/// <param name="newTime">The current time in military time format (e.g. 6:10pm is 1810).</param>
internal static void InvokeTimeOfDayChanged(IMonitor monitor, int priorTime, int newTime)
{
monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime));
}
}
}

View File

@ -52,8 +52,7 @@ namespace StardewModdingAPI.Framework
public Command Get(string name)
{
name = this.GetNormalisedName(name);
Command command;
this.Commands.TryGetValue(name, out command);
this.Commands.TryGetValue(name, out Command command);
return command;
}
@ -92,8 +91,7 @@ namespace StardewModdingAPI.Framework
return false;
// get command
Command command;
if (this.Commands.TryGetValue(name, out command))
if (this.Commands.TryGetValue(name, out Command command))
{
command.Callback.Invoke(name, arguments);
return true;
@ -101,6 +99,7 @@ namespace StardewModdingAPI.Framework
return false;
}
/*********
** Private methods
*********/

Some files were not shown because too many files have changed in this diff Show More