summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs23
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs8
-rw-r--r--src/SMAPI.Tests/Utilities/SDateTests.cs53
-rw-r--r--src/SMAPI.Web/Framework/LogParser/PastebinClient.cs41
-rw-r--r--src/SMAPI.Web/Program.cs8
-rw-r--r--src/SMAPI.Web/Startup.cs7
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml93
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/log-parser.css45
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/log-parser.js70
-rw-r--r--src/SMAPI/App.config9
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Events/EventArgsInput.cs19
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs31
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs12
-rw-r--r--src/SMAPI/Framework/Models/ManifestDependency.cs1
-rw-r--r--src/SMAPI/Framework/SContentManager.cs52
-rw-r--r--src/SMAPI/IContentHelper.cs6
-rw-r--r--src/SMAPI/Program.cs10
-rw-r--r--src/SMAPI/StardewModdingAPI.config.json3
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj3
-rw-r--r--src/SMAPI/Utilities/SDate.cs58
21 files changed, 339 insertions, 215 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index b5c2735b..83f353ae 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -296,7 +296,7 @@ namespace StardewModdingApi.Installer
{
// copy SMAPI files to game dir
this.PrintDebug("Adding SMAPI files...");
- foreach (FileInfo sourceFile in packageDir.EnumerateFiles())
+ foreach (FileInfo sourceFile in packageDir.EnumerateFiles().Where(this.ShouldCopyFile))
{
string targetPath = Path.Combine(installDir.FullName, sourceFile.Name);
this.InteractivelyDelete(targetPath);
@@ -338,7 +338,7 @@ namespace StardewModdingApi.Installer
targetDir.Create();
// copy files
- foreach (FileInfo sourceFile in sourceDir.EnumerateFiles())
+ foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
}
}
@@ -684,7 +684,7 @@ namespace StardewModdingApi.Installer
this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported).");
// move mods if no conflicts (else warn)
- foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos())
+ foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile))
{
// get type
bool isDir = entry is DirectoryInfo;
@@ -718,7 +718,7 @@ namespace StardewModdingApi.Installer
else
{
this.PrintDebug(" Deleted empty directory.");
- modDir.Delete();
+ modDir.Delete(recursive: true);
}
}
@@ -741,11 +741,22 @@ namespace StardewModdingApi.Installer
Directory.CreateDirectory(newPath);
DirectoryInfo directory = (DirectoryInfo)entry;
- foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos())
+ foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile))
this.Move(child, Path.Combine(newPath, child.Name));
- directory.Delete();
+ directory.Delete(recursive: true);
}
}
+
+ /// <summary>Get whether a file should be copied when moving a folder.</summary>
+ /// <param name="file">The file info.</param>
+ private bool ShouldCopyFile(FileSystemInfo file)
+ {
+ // ignore Mac symlink
+ if (file is FileInfo && file.Name == "mcs")
+ return false;
+
+ return true;
+ }
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
index 81167747..14a519fb 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
using StardewValley;
@@ -49,10 +49,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
return;
}
- // apply count & quality
+ // apply count
match.Item.Stack = count;
+
+ // apply quality
if (match.Item is Object obj)
obj.quality = quality;
+ else if (match.Item is Tool tool)
+ tool.UpgradeLevel = quality;
// add to inventory
Game1.player.addItemByMenuIfNecessary(match.Item);
diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs
index 86a0d3d0..b89d8857 100644
--- a/src/SMAPI.Tests/Utilities/SDateTests.cs
+++ b/src/SMAPI.Tests/Utilities/SDateTests.cs
@@ -82,6 +82,59 @@ namespace StardewModdingAPI.Tests.Utilities
}
/****
+ ** DayOfWeek
+ ****/
+ [Test(Description = "Assert the day of week.")]
+ [TestCase("01 spring Y1", ExpectedResult = System.DayOfWeek.Monday)]
+ [TestCase("02 spring Y2", ExpectedResult = System.DayOfWeek.Tuesday)]
+ [TestCase("03 spring Y3", ExpectedResult = System.DayOfWeek.Wednesday)]
+ [TestCase("04 spring Y4", ExpectedResult = System.DayOfWeek.Thursday)]
+ [TestCase("05 spring Y5", ExpectedResult = System.DayOfWeek.Friday)]
+ [TestCase("06 spring Y6", ExpectedResult = System.DayOfWeek.Saturday)]
+ [TestCase("07 spring Y7", ExpectedResult = System.DayOfWeek.Sunday)]
+ [TestCase("08 summer Y8", ExpectedResult = System.DayOfWeek.Monday)]
+ [TestCase("09 summer Y9", ExpectedResult = System.DayOfWeek.Tuesday)]
+ [TestCase("10 summer Y10", ExpectedResult = System.DayOfWeek.Wednesday)]
+ [TestCase("11 summer Y11", ExpectedResult = System.DayOfWeek.Thursday)]
+ [TestCase("12 summer Y12", ExpectedResult = System.DayOfWeek.Friday)]
+ [TestCase("13 summer Y13", ExpectedResult = System.DayOfWeek.Saturday)]
+ [TestCase("14 summer Y14", ExpectedResult = System.DayOfWeek.Sunday)]
+ [TestCase("15 fall Y15", ExpectedResult = System.DayOfWeek.Monday)]
+ [TestCase("16 fall Y16", ExpectedResult = System.DayOfWeek.Tuesday)]
+ [TestCase("17 fall Y17", ExpectedResult = System.DayOfWeek.Wednesday)]
+ [TestCase("18 fall Y18", ExpectedResult = System.DayOfWeek.Thursday)]
+ [TestCase("19 fall Y19", ExpectedResult = System.DayOfWeek.Friday)]
+ [TestCase("20 fall Y20", ExpectedResult = System.DayOfWeek.Saturday)]
+ [TestCase("21 fall Y21", ExpectedResult = System.DayOfWeek.Sunday)]
+ [TestCase("22 winter Y22", ExpectedResult = System.DayOfWeek.Monday)]
+ [TestCase("23 winter Y23", ExpectedResult = System.DayOfWeek.Tuesday)]
+ [TestCase("24 winter Y24", ExpectedResult = System.DayOfWeek.Wednesday)]
+ [TestCase("25 winter Y25", ExpectedResult = System.DayOfWeek.Thursday)]
+ [TestCase("26 winter Y26", ExpectedResult = System.DayOfWeek.Friday)]
+ [TestCase("27 winter Y27", ExpectedResult = System.DayOfWeek.Saturday)]
+ [TestCase("28 winter Y28" + "", ExpectedResult = System.DayOfWeek.Sunday)]
+ public DayOfWeek DayOfWeek(string dateStr)
+ {
+ // act
+ return this.GetDate(dateStr).DayOfWeek;
+ }
+
+ /****
+ ** DaysSinceStart
+ ****/
+ [Test(Description = "Assert the number of days since 01 spring Y1 (inclusive).")]
+ [TestCase("01 spring Y1", ExpectedResult = 1)]
+ [TestCase("02 spring Y1", ExpectedResult = 2)]
+ [TestCase("28 spring Y1", ExpectedResult = 28)]
+ [TestCase("01 summer Y1", ExpectedResult = 29)]
+ [TestCase("01 summer Y2", ExpectedResult = 141)]
+ public int DaysSinceStart(string dateStr)
+ {
+ // act
+ return this.GetDate(dateStr).DaysSinceStart;
+ }
+
+ /****
** ToString
****/
[Test(Description = "Assert that ToString returns the expected string.")]
diff --git a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs
index 738330d3..1cfaed17 100644
--- a/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs
+++ b/src/SMAPI.Web/Framework/LogParser/PastebinClient.cs
@@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Threading.Tasks;
+using System.Web;
using Pathoschild.Http.Client;
namespace StardewModdingAPI.Web.Framework.LogParser
@@ -67,6 +69,8 @@ namespace StardewModdingAPI.Web.Framework.LogParser
}
}
+ /// <summary>Save a paste to Pastebin.</summary>
+ /// <param name="content">The paste content.</param>
public async Task<SavePasteResponse> PostAsync(string content)
{
try
@@ -77,18 +81,18 @@ namespace StardewModdingAPI.Web.Framework.LogParser
// post to API
string response = await this.Client
- .PostAsync("api/api_post.php")
- .WithBodyContent(new FormUrlEncodedContent(new Dictionary<string, string>
- {
- ["api_option"] = "paste",
- ["api_user_key"] = this.UserKey,
- ["api_dev_key"] = this.DevKey,
- ["api_paste_private"] = "1", // unlisted
- ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}",
- ["api_paste_expire_date"] = "1W", // one week
- ["api_paste_code"] = content
- }))
- .AsString();
+ .PostAsync("api/api_post.php")
+ .WithBodyContent(this.GetFormUrlEncodedContent(new Dictionary<string, string>
+ {
+ ["api_option"] = "paste",
+ ["api_user_key"] = this.UserKey,
+ ["api_dev_key"] = this.DevKey,
+ ["api_paste_private"] = "1", // unlisted
+ ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}",
+ ["api_paste_expire_date"] = "N", // never expire
+ ["api_paste_code"] = content
+ }))
+ .AsString();
// handle Pastebin errors
if (string.IsNullOrWhiteSpace(response))
@@ -113,5 +117,18 @@ namespace StardewModdingAPI.Web.Framework.LogParser
{
this.Client.Dispose();
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Build an HTTP content body with form-url-encoded content.</summary>
+ /// <param name="data">The content to encode.</param>
+ /// <remarks>This bypasses an issue where <see cref="FormUrlEncodedContent"/> restricts the body length to the maximum size of a URL, which isn't applicable here.</remarks>
+ private HttpContent GetFormUrlEncodedContent(IDictionary<string, string> data)
+ {
+ string body = string.Join("&", from arg in data select $"{HttpUtility.UrlEncode(arg.Key)}={HttpUtility.UrlEncode(arg.Value)}");
+ return new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
+ }
}
}
diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs
index eeecb791..5856fc98 100644
--- a/src/SMAPI.Web/Program.cs
+++ b/src/SMAPI.Web/Program.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace StardewModdingAPI.Web
@@ -14,10 +14,8 @@ namespace StardewModdingAPI.Web
public static void Main(string[] args)
{
// configure web server
- new WebHostBuilder()
- .UseKestrel()
- .UseContentRoot(Directory.GetCurrentDirectory())
- .UseIISIntegration()
+ WebHost
+ .CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build()
.Run();
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 0ea9f7ee..16952124 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
- .Add(new BeanstalkEnvPropsConfigProvider()) //.AddEnvironmentVariables()
+ .Add(new BeanstalkEnvPropsConfigProvider())
.Build();
}
@@ -63,6 +63,10 @@ namespace StardewModdingAPI.Web
{
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
+
+ if (env.IsDevelopment())
+ app.UseDeveloperExceptionPage();
+
app
.UseCors(policy => policy
.AllowAnyHeader()
@@ -76,6 +80,7 @@ namespace StardewModdingAPI.Web
shouldRewrite: req =>
req.Host.Host != "localhost"
&& !req.Path.StartsWithSegments("/api")
+ && !req.Host.Host.StartsWith("api.")
))
// convert subdomain.smapi.io => smapi.io/subdomain for routing
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 49688d78..b7724c69 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -3,9 +3,9 @@
}
@model StardewModdingAPI.Web.ViewModels.LogParserModel
@section Head {
- <link rel="stylesheet" href="~/Content/css/log-parser.css" />
+ <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20171202" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
- <script src="~/Content/js/log-parser.js"></script>
+ <script src="~/Content/js/log-parser.js?r=20171202"></script>
<style type="text/css" id="modflags"></style>
<script>
$(function() {
@@ -23,49 +23,48 @@
@if (Model.PasteID != null)
{
<h2>Parsed log</h2>
- <ul id="tabs">
- <li>TRACE</li>
- <li>DEBUG</li>
- <li class="active">INFO</li>
- <li class="active">ALERT</li>
- <li class="active">WARN</li>
- <li class="active">ERROR</li>
- <li class="notice">Click tabs to toggle message visibility</li>
- </ul>
}
-<div id="output" class="trace debug"></div>
-<script class="template" id="template-body" type="text/html">
- <div class="always">
- <table id="gameinfo">
- <caption>Game info:</caption>
- <tr>
- <td>SMAPI Version</td>
- <td>{0}</td>
- </tr>
- <tr>
- <td>Game Version</td>
- <td>{1}</td>
- </tr>
- <tr>
- <td>Platform</td>
- <td>{2}</td>
- </tr>
- <tr>
- <td>Mods path</td>
- <td>{4}</td>
- </tr>
- <tr>
- <td>Log started</td>
- <td>{3}</td>
- </tr>
- </table>
- <br />
- <table id="modslist">
- <caption>Installed Mods: <span id="modlink-r" class="notice btn">Remove all mod filters</span><span class="notice txt"><i>Click any mod to filter</i></span> <span id="modlink-a" class="notice btn txt">Select all</span></caption>
- </table>
- </div>
- <table id="log"></table>
-</script>
+<div id="output" class="trace debug">
+ @if (Model.PasteID != null)
+ {
+ <div id="log-data" style="display: none;">
+ <div class="always">
+ <table id="gameinfo">
+ <caption>Game info:</caption>
+ <tr>
+ <td>SMAPI Version</td>
+ <td id="api-version"></td>
+ </tr>
+ <tr>
+ <td>Game Version</td>
+ <td id="game-version"></td>
+ </tr>
+ <tr>
+ <td>Platform</td>
+ <td id="platform"></td>
+ </tr>
+ <tr>
+ <td>Mods path</td>
+ <td id="mods-path"></td>
+ </tr>
+ <tr>
+ <td>Log started</td>
+ <td id="log-started"></td>
+ </tr>
+ </table>
+ <br/>
+ <table id="modslist">
+ <caption>Installed Mods: <span id="modlink-r" class="notice btn">Remove all mod filters</span><span class="notice txt"><i>Click any mod to filter</i></span> <span id="modlink-a" class="notice btn txt">Select all</span></caption>
+ </table>
+ <div id="filters">
+ Filter messages: <span>TRACE</span> | <span>DEBUG</span> | <span class="active">INFO</span> | <span class="active">ALERT</span> | <span class="active">WARN</span> | <span class="active">ERROR</span>
+ </div>
+ </div>
+ <table id="log"></table>
+ </div>
+ }
+ <div id="error" class="color-red"></div>
+</div>
<script class="template" id="template-css" type="text/html">
#output.modfilter:not(.mod-{0}) .mod-{0} { display:none; } #output.modfilter.mod-{0} #modslist tr { background:#ffeeee; } #output.modfilter.mod-{0} #modslist tr#modlink-{0} { background:#eeffee; }
</script>
@@ -95,15 +94,15 @@
<h1>Upload log file</h1>
<div class="frame">
<ol>
- <li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log</a>.</li>
+ <li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li>
<li>Drag the file onto the textbox below (or paste the text in).</li>
<li>Click <em>Parse</em>.</li>
<li>Share the URL of the new page.</li>
</ol>
<textarea id="input" placeholder="Paste or drag the log here"></textarea>
<div class="buttons">
- <input type="button" id="submit" value="Parse"/>
- <input type="button" id="cancel" value="Cancel"/>
+ <input type="button" id="submit" value="Parse" />
+ <input type="button" id="cancel" value="Cancel" />
</div>
</div>
</div>
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
index 975e9c2e..9f07f9e8 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -124,67 +124,38 @@ input[type="button"] {
color: green;
}
-#tabs {
- border-bottom: 0;
- display: block;
- margin: 0;
+#filters {
+ margin: 1em 0 0 0;
padding: 0;
- background: linear-gradient(to bottom, rgba(255, 255, 255, 1) 0%, rgba(210, 235, 249, 1) 100%);
}
-#tabs li {
- margin: 5px 1px 0 0;
- height: 25px;
+#filters span {
+ padding: 3px 1em;
display: inline-block;
- width: 75px;
border: 1px solid #000000;
- border-bottom: 0;
- border-radius: 5px 5px 0 0;
- text-align: center;
+ border-radius: 3px;
font-family: monospace;
- font-size: 18px;
cursor: pointer;
font-weight: bold;
color: #000;
- text-shadow: 0px 0px 2px #fff;
border-color: #880000;
background-color: #fcc;
- box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 0, .2);
}
-#tabs li:hover {
+#filters span:hover {
background-color: #fee;
}
-#tabs li:first-child {
- margin-left: 5px;
-}
-
-#tabs li.active {
+#filters span.active {
background: #cfc;
border-color: #008800;
}
-#tabs li.active:hover {
+#filters span.active:hover {
background: #efe;
}
-#tabs li.notice {
- color: #000000;
- background: transparent;
- border: 0;
- padding-top: 1px;
- font-size: 13px;
- font-weight: normal;
- width: auto;
- margin-left: 5px;
- cursor: default;
- box-shadow: none;
- font-style: italic;
-}
-
#output {
- border-top: 1px solid #888;
padding: 10px;
overflow: auto;
font-family: monospace;
diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
index 8e30ae12..6cce1ce9 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -8,6 +8,7 @@ smapi.logParser = function(sectionUrl, pasteID) {
var stage,
flags = $("#modflags"),
output = $("#output"),
+ error = $("#error"),
filters = 0,
memory = "",
versionInfo,
@@ -15,7 +16,6 @@ smapi.logParser = function(sectionUrl, pasteID) {
modMap,
modErrors,
logInfo,
- templateBody = $("#template-body").text(),
templateModentry = $("#template-modentry").text(),
templateCss = $("#template-css").text(),
templateLogentry = $("#template-logentry").text(),
@@ -27,16 +27,24 @@ smapi.logParser = function(sectionUrl, pasteID) {
regexDate = /\[\d{2}:\d{2}:\d{2} TRACE SMAPI\] Log started at (.*?) UTC/g,
regexPath = /\[\d{2}:\d{2}:\d{2} DEBUG SMAPI\] Mods go here: (.*?)(?:\n|$)/g;
- $("#tabs li:not(.notice)").on("click", function(evt) {
+ $("#filters span").on("click", function(evt) {
var t = $(evt.currentTarget);
t.toggleClass("active");
- $("#output").toggleClass(t.text().toLowerCase());
+ output.toggleClass(t.text().toLowerCase());
});
$("#upload-button").on("click", function() {
memory = $("#input").val() || "";
$("#input").val("");
$("#popup-upload").fadeIn();
});
+
+ var closeUploadPopUp = function() {
+ $("#popup-upload").fadeOut(400, function() {
+ $("#input").val(memory);
+ memory = "";
+ });
+ };
+
$("#popup-upload").on({
'dragover dragenter': function(e) {
e.preventDefault();
@@ -58,6 +66,10 @@ smapi.logParser = function(sectionUrl, pasteID) {
}, this, file, $("#input"));
reader.readAsText(file);
}
+ },
+ 'click': function(e) {
+ if (e.target.id === "popup-upload")
+ closeUploadPopUp();
}
});
@@ -77,12 +89,12 @@ smapi.logParser = function(sectionUrl, pasteID) {
})
.fail(function(xhr, textStatus) {
$("#uploader").fadeOut();
- $("#output").html('<div id="log" class="color-red"><h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + textStatus + ': ' + xhr.responseText + "<hr /><pre>" + $("#input").val() + "</pre></div>");
+ error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + textStatus + ': ' + xhr.responseText + "<hr /><pre>" + $("#input").val() + "</pre>");
})
.then(function(data) {
$("#uploader").fadeOut();
if (!data.success)
- $("#output").html('<div id="log" class="color-red"><h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + data.error + "<hr />" + $("#input").val() + "</div>");
+ error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + data.error + "<hr /><pre>" + $("#input").val() + "</pre>");
else
location.href = (sectionUrl.replace(/\/$/, "") + "/" + data.id);
});
@@ -91,15 +103,28 @@ smapi.logParser = function(sectionUrl, pasteID) {
$("#uploader").fadeOut();
}
});
- $("#cancel").on("click", function() {
- $("#popup-upload").fadeOut(400, function() {
- $("#input").val(memory);
- memory = "";
- });
+
+ $(document).on("keydown", function(e) {
+ if (e.which == 27) {
+ if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") == 1) {
+ closeUploadPopUp();
+ }
+
+ $("#popup-raw").fadeOut(400);
+ }
});
+ $("#cancel").on("click", closeUploadPopUp);
+
$("#closeraw").on("click", function() {
$("#popup-raw").fadeOut(400);
});
+
+ $("#popup-raw").on("click", function(e) {
+ if (e.target.id === "popup-raw") {
+ $("#popup-raw").fadeOut(400);
+ }
+ });
+
if (pasteID) {
getData(pasteID);
}
@@ -176,7 +201,13 @@ smapi.logParser = function(sectionUrl, pasteID) {
];
stage = "parseData.parseInfo";
var date = dataDate ? new Date(dataDate[1] + "Z") : null;
- versionInfo = [dataInfo[1], dataInfo[2], dataInfo[3], date ? date.getFullYear() + "-" + ("0" + date.getMonth().toString()).substr(-2) + "-" + ("0" + date.getDay().toString()).substr(-2) + " at " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + " " + date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ")[2] : "No timestamp found", dataPath[1]];
+ versionInfo = {
+ apiVersion: dataInfo[1],
+ gameVersion: dataInfo[2],
+ platform: dataInfo[3],
+ logDate: date ? date.getFullYear() + "-" + ("0" + date.getMonth().toString()).substr(-2) + "-" + ("0" + date.getDay().toString()).substr(-2) + " at " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + " " + date.toLocaleTimeString("en-us", { timeZoneName: "short" }).split(" ")[2] : "No timestamp found",
+ modsPath: dataPath[1]
+ };
stage = "parseData.parseMods";
while ((match = regexMod.exec(dataMods))) {
modErrors[match[1]] = 0;
@@ -196,7 +227,13 @@ smapi.logParser = function(sectionUrl, pasteID) {
function renderData() {
stage = "renderData.pre";
- output.html(prepare(templateBody, versionInfo));
+
+ output.find("#api-version").text(versionInfo.apiVersion);
+ output.find("#game-version").text(versionInfo.gameVersion);
+ output.find("#platform").text(versionInfo.platform);
+ output.find("#log-started").text(versionInfo.logDate);
+ output.find("#mods-path").text(versionInfo.modsPath);
+
var modslist = $("#modslist"), log = $("#log"), modCache = [], y = 0;
for (; y < modInfo.length; y++) {
var errors = modErrors[modInfo[y][0]],
@@ -233,6 +270,8 @@ smapi.logParser = function(sectionUrl, pasteID) {
log.append(logCache.join(""));
$("#modlink-r").on("click", removeFilter);
$("#modlink-a").on("click", selectAll);
+
+ $("#log-data").show();
}
function prepare(str, arr) {
@@ -245,11 +284,8 @@ smapi.logParser = function(sectionUrl, pasteID) {
function loadData() {
try {
stage = "loadData.Pre";
- var start = performance.now();
parseData();
renderData();
- var end = performance.now();
- $(".always").prepend("<div>Log processed in: " + (Math.round((end - start) * 100) / 100) + ' ms (<a id="viewraw" href="#">View raw</a>)</div><br />');
$("#viewraw").on("click", function() {
$("#dataraw").val($("#input").val());
$("#popup-raw").fadeIn();
@@ -257,7 +293,7 @@ smapi.logParser = function(sectionUrl, pasteID) {
stage = "loadData.Post";
}
catch (err) {
- $("#output").html('<div id="log" class="color-red"><h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: ' + stage + "</p>" + err + '<hr /><pre id="rawlog"></pre></div>');
+ error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: ' + stage + "</p>" + err + '<hr /><pre id="rawlog"></pre>');
$("#rawlog").text($("#input").val());
}
}
@@ -269,7 +305,7 @@ smapi.logParser = function(sectionUrl, pasteID) {
$("#input").val(data.content);
loadData();
} else {
- $("#output").html('<div id="log" class="color-red"><h1>Fetching the log failed!</h1><p>' + data.error + '</p><pre id="rawlog"></pre></div>');
+ error.html('<h1>Fetching the log failed!</h1><p>' + data.error + '</p><pre id="rawlog"></pre>');
$("#rawlog").text($("#input").val());
}
$("#uploader").fadeOut();
diff --git a/src/SMAPI/App.config b/src/SMAPI/App.config
deleted file mode 100644
index 27cdf0f7..00000000
--- a/src/SMAPI/App.config
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<configuration>
- <startup>
- <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
- </startup>
- <runtime>
- <loadFromRemoteSources enabled="true"/>
- </runtime>
-</configuration>
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index a2dbdd98..41b79272 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -29,7 +29,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 1, 0);
+ public static ISemanticVersion ApiVersion { get; } = new SemanticVersion("2.2");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30");
diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs
index ff904675..54ce9b53 100644
--- a/src/SMAPI/Events/EventArgsInput.cs
+++ b/src/SMAPI/Events/EventArgsInput.cs
@@ -56,11 +56,11 @@ namespace StardewModdingAPI.Events
public void SuppressButton(SButton button)
{
// keyboard
- if (this.Button.TryGetKeyboard(out Keys key))
+ if (button.TryGetKeyboard(out Keys key))
Game1.oldKBState = new KeyboardState(Game1.oldKBState.GetPressedKeys().Union(new[] { key }).ToArray());
// controller
- else if (this.Button.TryGetController(out Buttons controllerButton))
+ else if (button.TryGetController(out Buttons controllerButton))
{
var newState = GamePad.GetState(PlayerIndex.One);
var thumbsticks = Game1.oldPadState.ThumbSticks;
@@ -127,6 +127,21 @@ namespace StardewModdingAPI.Events
Game1.oldPadState = new GamePadState(thumbsticks, triggers, buttons, dpad);
}
+
+ // mouse
+ else if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2)
+ {
+ Game1.oldMouseState = new MouseState(
+ x: Game1.oldMouseState.X,
+ y: Game1.oldMouseState.Y,
+ scrollWheel: Game1.oldMouseState.ScrollWheelValue,
+ leftButton: button == SButton.MouseLeft ? ButtonState.Pressed : Game1.oldMouseState.LeftButton,
+ middleButton: button == SButton.MouseMiddle ? ButtonState.Pressed : Game1.oldMouseState.MiddleButton,
+ rightButton: button == SButton.MouseRight ? ButtonState.Pressed : Game1.oldMouseState.RightButton,
+ xButton1: button == SButton.MouseX1 ? ButtonState.Pressed : Game1.oldMouseState.XButton1,
+ xButton2: button == SButton.MouseX2 ? ButtonState.Pressed : Game1.oldMouseState.XButton2
+ );
+ }
}
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index be9594ee..4a1d3853 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Content;
@@ -26,9 +27,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The absolute path to the mod folder.</summary>
private readonly string ModFolderPath;
- /// <summary>The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName").</summary>
- private readonly string ModFolderPathFromContent;
-
/// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName;
@@ -73,7 +71,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentManager = contentManager;
this.ModFolderPath = modFolderPath;
this.ModName = modName;
- this.ModFolderPathFromContent = this.ContentManager.GetRelativePath(modFolderPath);
this.Monitor = monitor;
}
@@ -102,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw GetContentError($"there's no matching file at path '{file.FullName}'.");
// get asset path
- string assetName = this.GetModAssetPath(key, file.FullName);
+ string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName);
// try cache
if (this.ContentManager.IsLoaded(assetName))
@@ -138,6 +135,14 @@ namespace StardewModdingAPI.Framework.ModHelpers
}
}
+ /// <summary>Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like <see cref="string.StartsWith(string)"/> on generated asset names, and isn't necessary when passing asset names into other content helper methods.</summary>
+ /// <param name="assetName">The asset key.</param>
+ [Pure]
+ public string NormaliseAssetName(string assetName)
+ {
+ return this.ContentManager.NormaliseAssetName(assetName);
+ }
+
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
@@ -151,7 +156,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
case ContentSource.ModFolder:
FileInfo file = this.GetModFile(key);
- return this.ContentManager.NormaliseAssetName(this.GetModAssetPath(key, file.FullName));
+ return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName));
default:
throw new NotSupportedException($"Unknown content source '{source}'.");
@@ -356,19 +361,5 @@ namespace StardewModdingAPI.Framework.ModHelpers
// get file
return new FileInfo(path);
}
-
- /// <summary>Get the asset path which loads a mod folder through a content manager.</summary>
- /// <param name="localPath">The file path relative to the mod's folder.</param>
- /// <param name="absolutePath">The absolute file path.</param>
- private string GetModAssetPath(string localPath, string absolutePath)
- {
-#if SMAPI_FOR_WINDOWS
- // XNA doesn't allow absolute asset paths, so get a path relative to the content folder
- return Path.Combine(this.ModFolderPathFromContent, localPath);
-#else
- // MonoGame is weird about relative paths on Mac, but allows absolute paths
- return absolutePath;
-#endif
- }
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index d0ef1b08..9802d9e9 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -142,6 +142,18 @@ namespace StardewModdingAPI.Framework.ModLoading
continue;
}
+ // validate DLL value
+ if (string.IsNullOrWhiteSpace(mod.Manifest.EntryDll))
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, "its manifest has no EntryDLL field.");
+ continue;
+ }
+ if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any())
+ {
+ mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
+ continue;
+ }
+
// validate DLL path
string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
if (!File.Exists(assemblyPath))
diff --git a/src/SMAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs
index 5646b335..97f0775a 100644
--- a/src/SMAPI/Framework/Models/ManifestDependency.cs
+++ b/src/SMAPI/Framework/Models/ManifestDependency.cs
@@ -15,6 +15,7 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether the dependency must be installed to use the mod.</summary>
public bool IsRequired { get; set; }
+
/*********
** Public methods
*********/
diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs
index a755a6df..524b2d17 100644
--- a/src/SMAPI/Framework/SContentManager.cs
+++ b/src/SMAPI/Framework/SContentManager.cs
@@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Cache = new ContentCache(this, reflection, SContentManager.PossiblePathSeparators, SContentManager.PreferredPathSeparator);
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
- this.ModContentPrefix = this.GetRelativePath(Constants.ModPath);
+ this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data
this.CoreAssets = new CoreAssets(this.NormaliseAssetName);
@@ -140,19 +140,17 @@ namespace StardewModdingAPI.Framework
throw new ArgumentException("The asset key or local path contains invalid characters.");
}
- /// <summary>Get a directory path relative to the content root.</summary>
- /// <param name="targetPath">The target file path.</param>
- public string GetRelativePath(string targetPath)
+ /// <summary>Convert an absolute file path into a appropriate asset name.</summary>
+ /// <param name="absolutePath">The absolute path to the file.</param>
+ public string GetAssetNameFromFilePath(string absolutePath)
{
- // convert to URIs
- Uri from = new Uri(this.FullRootDirectory + "/");
- Uri to = new Uri(targetPath + "/");
- if (from.Scheme != to.Scheme)
- throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'.");
-
- // get relative path
- return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
- .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
+#if SMAPI_FOR_WINDOWS
+ // XNA doesn't allow absolute asset paths, so get a path relative to the content folder
+ return this.GetRelativePath(absolutePath);
+#else
+ // MonoGame is weird about relative paths on Mac, but allows absolute paths
+ return absolutePath;
+#endif
}
/****
@@ -395,6 +393,21 @@ namespace StardewModdingAPI.Framework
/****
** Asset name/key handling
****/
+ /// <summary>Get a directory or file path relative to the content root.</summary>
+ /// <param name="targetPath">The target file path.</param>
+ private string GetRelativePath(string targetPath)
+ {
+ // convert to URIs
+ Uri from = new Uri(this.FullRootDirectory + "/");
+ Uri to = new Uri(targetPath + "/");
+ if (from.Scheme != to.Scheme)
+ throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{this.FullRootDirectory}'.");
+
+ // get relative path
+ return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())
+ .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform
+ }
+
/// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary>
/// <param name="reflection">Simplifies access to private game code.</param>
private IDictionary<string, LanguageCode> GetKeyLocales(Reflector reflection)
@@ -551,19 +564,6 @@ namespace StardewModdingAPI.Framework
return file;
}
- /// <summary>Get a file from the game's content folder.</summary>
- /// <param name="key">The asset key.</param>
- private FileInfo GetContentFolderFile(string key)
- {
- // get file path
- string path = Path.Combine(this.FullRootDirectory, key);
- if (!path.EndsWith(".xnb"))
- path += ".xnb";
-
- // get file
- return new FileInfo(path);
- }
-
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
/// <param name="info">The basic asset metadata.</param>
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs
index e3362502..1b87183d 100644
--- a/src/SMAPI/IContentHelper.cs
+++ b/src/SMAPI/IContentHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.Contracts;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
@@ -37,6 +38,11 @@ namespace StardewModdingAPI
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
T Load<T>(string key, ContentSource source = ContentSource.ModFolder);
+ /// <summary>Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like <see cref="string.StartsWith(string)"/> on generated asset names, and isn't necessary when passing asset names into other content helper methods.</summary>
+ /// <param name="assetName">The asset key.</param>
+ [Pure]
+ string NormaliseAssetName(string assetName);
+
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
/// <param name="source">Where to search for a matching content asset.</param>
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index b742467b..3ba35e43 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -653,11 +653,8 @@ namespace StardewModdingAPI
{
// get basic info
IManifest manifest = metadata.Manifest;
- string assemblyPath = metadata.Manifest?.EntryDll != null
- ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll)
- : null;
- this.Monitor.Log(assemblyPath != null
- ? $"Loading {metadata.DisplayName} from {assemblyPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}..."
+ this.Monitor.Log(metadata.Manifest?.EntryDll != null
+ ? $"Loading {metadata.DisplayName} from {metadata.DirectoryPath.Replace(Constants.ModPath, "").TrimStart(Path.DirectorySeparatorChar)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll}..." // don't use Path.Combine here, since EntryDLL might not be valid
: $"Loading {metadata.DisplayName}...", LogLevel.Trace);
// validate status
@@ -669,6 +666,9 @@ namespace StardewModdingAPI
}
// preprocess & load mod assembly
+ string assemblyPath = metadata.Manifest?.EntryDll != null
+ ? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll)
+ : null;
Assembly modAssembly;
try
{
diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json
index fa3bdcc9..6718806e 100644
--- a/src/SMAPI/StardewModdingAPI.config.json
+++ b/src/SMAPI/StardewModdingAPI.config.json
@@ -509,7 +509,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha
"ID": "Platonymous.CFAutomate",
"Compatibility": {
"~1.0.1": { "Status": "AssumeBroken" } // no longer compatible with Automate
- }
+ },
+ "AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991"
},
{
// Custom Farm Types
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 605292b2..380ed733 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -234,9 +234,6 @@
<Compile Include="Framework\CursorPosition.cs" />
</ItemGroup>
<ItemGroup>
- <None Include="App.config">
- <SubType>Designer</SubType>
- </None>
<None Include="packages.config">
<SubType>Designer</SubType>
</None>
diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs
index 2630731a..e589e9a4 100644
--- a/src/SMAPI/Utilities/SDate.cs
+++ b/src/SMAPI/Utilities/SDate.cs
@@ -35,6 +35,9 @@ namespace StardewModdingAPI.Utilities
/// <summary>The day of week.</summary>
public DayOfWeek DayOfWeek { get; }
+ /// <summary>The number of days since the game began (starting at 1 for the first day of spring in Y1).</summary>
+ public int DaysSinceStart { get; }
+
/*********
** Public methods
@@ -67,7 +70,7 @@ namespace StardewModdingAPI.Utilities
public SDate AddDays(int offset)
{
// get new hash code
- int hashCode = this.GetHashCode() + offset;
+ int hashCode = this.DaysSinceStart + offset;
if (hashCode < 1)
throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1.");
@@ -115,12 +118,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get a hash code which uniquely identifies a date.</summary>
public override int GetHashCode()
{
- // return the number of days since 01 spring Y1 (inclusively)
- int yearIndex = this.Year - 1;
- return
- yearIndex * this.DaysInSeason * this.SeasonsInYear
- + this.GetSeasonIndex() * this.DaysInSeason
- + this.Day;
+ return this.DaysSinceStart;
}
/****
@@ -132,7 +130,7 @@ namespace StardewModdingAPI.Utilities
/// <returns>The equality of the dates</returns>
public static bool operator ==(SDate date, SDate other)
{
- return date?.GetHashCode() == other?.GetHashCode();
+ return date?.DaysSinceStart == other?.DaysSinceStart;
}
/// <summary>Get whether one date is not equal to another.</summary>
@@ -140,7 +138,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="other">The other date to compare.</param>
public static bool operator !=(SDate date, SDate other)
{
- return date?.GetHashCode() != other?.GetHashCode();
+ return date?.DaysSinceStart != other?.DaysSinceStart;
}
/// <summary>Get whether one date is more than another.</summary>
@@ -148,7 +146,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="other">The other date to compare.</param>
public static bool operator >(SDate date, SDate other)
{
- return date?.GetHashCode() > other?.GetHashCode();
+ return date?.DaysSinceStart > other?.DaysSinceStart;
}
/// <summary>Get whether one date is more than or equal to another.</summary>
@@ -156,7 +154,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="other">The other date to compare.</param>
public static bool operator >=(SDate date, SDate other)
{
- return date?.GetHashCode() >= other?.GetHashCode();
+ return date?.DaysSinceStart >= other?.DaysSinceStart;
}
/// <summary>Get whether one date is less than or equal to another.</summary>
@@ -164,7 +162,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="other">The other date to compare.</param>
public static bool operator <=(SDate date, SDate other)
{
- return date?.GetHashCode() <= other?.GetHashCode();
+ return date?.DaysSinceStart <= other?.DaysSinceStart;
}
/// <summary>Get whether one date is less than another.</summary>
@@ -172,7 +170,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="other">The other date to compare.</param>
public static bool operator <(SDate date, SDate other)
{
- return date?.GetHashCode() < other?.GetHashCode();
+ return date?.DaysSinceStart < other?.DaysSinceStart;
}
@@ -203,7 +201,9 @@ namespace StardewModdingAPI.Utilities
this.Day = day;
this.Season = season;
this.Year = year;
- this.DayOfWeek = this.GetDayOfWeek();
+ this.DayOfWeek = this.GetDayOfWeek(day);
+ this.DaysSinceStart = this.GetDaysSinceStart(day, season, year);
+
}
/// <summary>Get whether a date represents 0 spring Y1, which is the date during the in-game intro.</summary>
@@ -215,10 +215,11 @@ namespace StardewModdingAPI.Utilities
return day == 0 && season == "spring" && year == 1;
}
- /// <summary>Get the day of week for the current date.</summary>
- private DayOfWeek GetDayOfWeek()
+ /// <summary>Get the day of week for a given date.</summary>
+ /// <param name="day">The day of month.</param>
+ private DayOfWeek GetDayOfWeek(int day)
{
- switch (this.Day % 7)
+ switch (day % 7)
{
case 0:
return DayOfWeek.Sunday;
@@ -239,13 +240,28 @@ namespace StardewModdingAPI.Utilities
}
}
- /// <summary>Get the current season index.</summary>
+ /// <summary>Get the number of days since the game began (starting at 1 for the first day of spring in Y1).</summary>
+ /// <param name="day">The day of month.</param>
+ /// <param name="season">The season name.</param>
+ /// <param name="year">The year.</param>
+ private int GetDaysSinceStart(int day, string season, int year)
+ {
+ // return the number of days since 01 spring Y1 (inclusively)
+ int yearIndex = year - 1;
+ return
+ yearIndex * this.DaysInSeason * this.SeasonsInYear
+ + this.GetSeasonIndex(season) * this.DaysInSeason
+ + day;
+ }
+
+ /// <summary>Get a season index.</summary>
+ /// <param name="season">The season name.</param>
/// <exception cref="InvalidOperationException">The current season wasn't recognised.</exception>
- private int GetSeasonIndex()
+ private int GetSeasonIndex(string season)
{
- int index = Array.IndexOf(this.Seasons, this.Season);
+ int index = Array.IndexOf(this.Seasons, season);
if (index == -1)
- throw new InvalidOperationException($"The current season '{this.Season}' wasn't recognised.");
+ throw new InvalidOperationException($"The season '{season}' wasn't recognised.");
return index;
}
}