Merge branch 'develop' of https://github.com/Pathoschild/SMAPI.git into android
This commit is contained in:
commit
1648ec1cba
|
@ -1,6 +1,26 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
## Upcoming release
|
||||
* For players:
|
||||
* Updated translations. Thanks to xCarloC (added Italian)!
|
||||
|
||||
* For the Save Backup mod:
|
||||
* Fixed warning on MacOS when you have no saves yet.
|
||||
* Reduced log messages.
|
||||
|
||||
* For modders:
|
||||
* Added support for self-broadcasts through the multiplayer API. (Mods can now send messages to the current machine. That enables simple integrations between mods without needing an API, and lets mods notify a host mod without needing different code depending on whether the current player is the host or a farmhand.)
|
||||
* Added `helper.Input.GetStatus` method to get the low-level status of a button.
|
||||
* Eliminated unneeded network messages when broadcasting to a peer who can't handle the message (e.g. because they don't have SMAPI or don't have the target mod).
|
||||
* Fixed marriage dialogue cleared when propagating dialogue changes.
|
||||
|
||||
* For the web UI:
|
||||
* Updated the JSON validator and Content Patcher schema for `.tmx` support.
|
||||
|
||||
* For SMAPI/tool developers:
|
||||
* The SMAPI log now prefixes the OS name with `Android` on Android.
|
||||
|
||||
## 3.2
|
||||
Released 01 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
|
||||
|
|
|
@ -66,29 +66,37 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
|
||||
DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel));
|
||||
if (targetFile.Exists || fallbackDir.Exists)
|
||||
{
|
||||
this.Monitor.Log("Already backed up today.");
|
||||
return;
|
||||
}
|
||||
|
||||
// copy saves to fallback directory (ignore non-save files/folders)
|
||||
this.Monitor.Log($"Backing up saves to {fallbackDir.FullName}...", LogLevel.Trace);
|
||||
DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath);
|
||||
this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false);
|
||||
if (!this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false))
|
||||
{
|
||||
this.Monitor.Log("No saves found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// compress backup if possible
|
||||
this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace);
|
||||
if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError))
|
||||
{
|
||||
if (Constants.TargetPlatform != GamePlatform.Android) // expected to fail on Android
|
||||
this.Monitor.Log($"Couldn't compress backup, leaving it uncompressed.\n{compressError}", LogLevel.Trace);
|
||||
this.Monitor.Log(Constants.TargetPlatform != GamePlatform.Android
|
||||
? $"Backed up to {fallbackDir.FullName}." // expected to fail on Android
|
||||
: $"Backed up to {fallbackDir.FullName}. Couldn't compress backup:\n{compressError}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Monitor.Log($"Backed up to {targetFile.FullName}.");
|
||||
fallbackDir.Delete(recursive: true);
|
||||
|
||||
this.Monitor.Log("Backup done!", LogLevel.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't back up save files (see log file for details).", LogLevel.Warn);
|
||||
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
|
||||
this.Monitor.Log("Couldn't back up saves (see log file for details).", LogLevel.Warn);
|
||||
this.Monitor.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +116,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
{
|
||||
try
|
||||
{
|
||||
this.Monitor.Log($"Deleting {entry.Name}...", LogLevel.Trace);
|
||||
this.Monitor.Log($"Deleting {entry.Name}...");
|
||||
if (entry is DirectoryInfo folder)
|
||||
folder.Delete(recursive: true);
|
||||
else
|
||||
|
@ -123,7 +131,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log("Couldn't remove old backups (see log file for details).", LogLevel.Warn);
|
||||
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
|
||||
this.Monitor.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,29 +207,33 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
/// <param name="copyRoot">Whether to copy the root folder itself, or <c>false</c> to only copy its contents.</param>
|
||||
/// <param name="filter">A filter which matches the files or directories to copy, or <c>null</c> to copy everything.</param>
|
||||
/// <remarks>Derived from the SMAPI installer code.</remarks>
|
||||
private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
|
||||
/// <returns>Returns whether any files were copied.</returns>
|
||||
private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
|
||||
{
|
||||
if (!targetFolder.Exists)
|
||||
targetFolder.Create();
|
||||
if (!source.Exists || filter?.Invoke(source) == false)
|
||||
return false;
|
||||
|
||||
if (filter?.Invoke(source) == false)
|
||||
return;
|
||||
bool anyCopied = false;
|
||||
|
||||
switch (source)
|
||||
{
|
||||
case FileInfo sourceFile:
|
||||
targetFolder.Create();
|
||||
sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name));
|
||||
anyCopied = true;
|
||||
break;
|
||||
|
||||
case DirectoryInfo sourceDir:
|
||||
DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder;
|
||||
foreach (var entry in sourceDir.EnumerateFileSystemInfos())
|
||||
this.RecursiveCopy(entry, targetSubfolder, filter);
|
||||
anyCopied = this.RecursiveCopy(entry, targetSubfolder, filter) || anyCopied;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'.");
|
||||
}
|
||||
|
||||
return anyCopied;
|
||||
}
|
||||
|
||||
/// <summary>A copy filter which matches save folders.</summary>
|
||||
|
|
|
@ -53,7 +53,19 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
|||
}
|
||||
catch { }
|
||||
#endif
|
||||
return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion;
|
||||
|
||||
string name = Environment.OSVersion.ToString();
|
||||
switch (platform)
|
||||
{
|
||||
case Platform.Android:
|
||||
name = $"Android {name}";
|
||||
break;
|
||||
|
||||
case Platform.Mac:
|
||||
name = $"MacOS {name}";
|
||||
break;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>Get the name of the Stardew Valley executable.</summary>
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
},
|
||||
"FromFile": {
|
||||
"title": "Source file",
|
||||
"description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalization doesn't matter.",
|
||||
"description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin or .tmx (map), or .xnb file. This field supports tokens and capitalization doesn't matter.",
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
|
@ -151,12 +151,12 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"pattern": "\\.(json|png|tbin|xnb) *$"
|
||||
"pattern": "\\.(json|png|tbin|tmx|xnb) *$"
|
||||
}
|
||||
],
|
||||
"@errorMessages": {
|
||||
"allOf:indexes: 0": "Invalid value; must not contain directory climbing (like '../').",
|
||||
"allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, or .xnb."
|
||||
"allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, .tmx, or .xnb."
|
||||
}
|
||||
},
|
||||
"FromArea": {
|
||||
|
@ -325,7 +325,7 @@
|
|||
"then": {
|
||||
"properties": {
|
||||
"FromFile": {
|
||||
"description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder."
|
||||
"description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin, .tmx, or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder."
|
||||
},
|
||||
"FromArea": {
|
||||
"description": "The part of the source map to copy. Defaults to the whole source map."
|
||||
|
|
|
@ -169,6 +169,13 @@ namespace StardewModdingAPI.Framework.Input
|
|||
return buttons.Any(button => this.IsDown(button.ToSButton()));
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public InputStatus GetStatus(SButton button)
|
||||
{
|
||||
return this.GetStatus(this.ActiveButtons, button);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
|
|
|
@ -50,5 +50,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
|||
{
|
||||
this.InputState.SuppressButtons.Add(button);
|
||||
}
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public InputStatus GetStatus(SButton button)
|
||||
{
|
||||
return this.InputState.GetStatus(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Networking
|
|||
/****
|
||||
** Destination
|
||||
****/
|
||||
/// <summary>The players who should receive the message, or <c>null</c> for all players.</summary>
|
||||
/// <summary>The players who should receive the message.</summary>
|
||||
public long[] ToPlayerIDs { get; set; }
|
||||
|
||||
/// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>
|
||||
|
|
|
@ -338,30 +338,49 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
|
||||
public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
|
||||
{
|
||||
// validate
|
||||
// validate input
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
if (string.IsNullOrWhiteSpace(messageType))
|
||||
throw new ArgumentNullException(nameof(messageType));
|
||||
if (string.IsNullOrWhiteSpace(fromModID))
|
||||
throw new ArgumentNullException(nameof(fromModID));
|
||||
if (!this.Peers.Any())
|
||||
|
||||
// get target players
|
||||
long curPlayerId = Game1.player.UniqueMultiplayerID;
|
||||
bool sendToSelf = false;
|
||||
List<MultiplayerPeer> sendToPeers = new List<MultiplayerPeer>();
|
||||
if (toPlayerIDs == null)
|
||||
{
|
||||
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
|
||||
return;
|
||||
sendToSelf = true;
|
||||
sendToPeers.AddRange(this.Peers.Values);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (long id in toPlayerIDs.Distinct())
|
||||
{
|
||||
if (id == curPlayerId)
|
||||
sendToSelf = true;
|
||||
else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi)
|
||||
sendToPeers.Add(peer);
|
||||
}
|
||||
}
|
||||
|
||||
// filter player IDs
|
||||
HashSet<long> playerIDs = null;
|
||||
if (toPlayerIDs != null && toPlayerIDs.Any())
|
||||
// filter by mod ID
|
||||
if (toModIDs != null)
|
||||
{
|
||||
playerIDs = new HashSet<long>(toPlayerIDs);
|
||||
playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id));
|
||||
if (!playerIDs.Any())
|
||||
{
|
||||
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected.");
|
||||
return;
|
||||
}
|
||||
HashSet<string> sendToMods = new HashSet<string>(toModIDs, StringComparer.InvariantCultureIgnoreCase);
|
||||
if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null))
|
||||
sendToSelf = false;
|
||||
|
||||
sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID)));
|
||||
}
|
||||
|
||||
// validate recipients
|
||||
if (!sendToSelf && !sendToPeers.Any())
|
||||
{
|
||||
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs can receive this message.");
|
||||
return;
|
||||
}
|
||||
|
||||
// get data to send
|
||||
|
@ -369,33 +388,44 @@ namespace StardewModdingAPI.Framework
|
|||
fromPlayerID: Game1.player.UniqueMultiplayerID,
|
||||
fromModID: fromModID,
|
||||
toModIDs: toModIDs,
|
||||
toPlayerIDs: playerIDs?.ToArray(),
|
||||
toPlayerIDs: sendToPeers.Select(p => p.PlayerID).ToArray(),
|
||||
type: messageType,
|
||||
data: JToken.FromObject(message)
|
||||
);
|
||||
string data = JsonConvert.SerializeObject(model, Formatting.None);
|
||||
|
||||
// log message
|
||||
if (this.LogNetworkTraffic)
|
||||
this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);
|
||||
|
||||
// send message
|
||||
if (Context.IsMainPlayer)
|
||||
// send self-message
|
||||
if (sendToSelf)
|
||||
{
|
||||
foreach (MultiplayerPeer peer in this.Peers.Values)
|
||||
if (this.LogNetworkTraffic)
|
||||
this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}.", LogLevel.Trace);
|
||||
|
||||
this.OnModMessageReceived(model);
|
||||
}
|
||||
|
||||
// send message to peers
|
||||
if (sendToPeers.Any())
|
||||
{
|
||||
if (Context.IsMainPlayer)
|
||||
{
|
||||
if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
|
||||
foreach (MultiplayerPeer peer in sendToPeers)
|
||||
{
|
||||
model.ToPlayerIDs = new[] { peer.PlayerID };
|
||||
if (this.LogNetworkTraffic)
|
||||
this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}.", LogLevel.Trace);
|
||||
|
||||
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.HostPeer != null && this.HostPeer.HasSmapi)
|
||||
this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
|
||||
else
|
||||
this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
|
||||
else if (this.HostPeer?.HasSmapi == true)
|
||||
{
|
||||
if (this.LogNetworkTraffic)
|
||||
this.Monitor.Log($"Broadcasting '{messageType}' message to host {this.HostPeer.PlayerID}: {data}.", LogLevel.Trace);
|
||||
|
||||
this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
|
||||
}
|
||||
else
|
||||
this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -17,5 +17,9 @@ namespace StardewModdingAPI
|
|||
/// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary>
|
||||
/// <param name="button">The button to suppress.</param>
|
||||
void Suppress(SButton button);
|
||||
|
||||
/// <summary>Get the status of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
InputStatus GetStatus(SButton button);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace StardewModdingAPI.Framework.Input
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>The input status for a button during an update frame.</summary>
|
||||
internal enum InputStatus
|
||||
public enum InputStatus
|
||||
{
|
||||
/// <summary>The button was neither pressed, held, nor released.</summary>
|
||||
None,
|
|
@ -887,10 +887,17 @@ namespace StardewModdingAPI.Metadata
|
|||
return false;
|
||||
|
||||
// update dialogue
|
||||
// Note that marriage dialogue isn't reloaded after reset, but it doesn't need to be
|
||||
// propagated anyway since marriage dialogue keys can't be added/removed and the field
|
||||
// doesn't store the text itself.
|
||||
foreach (NPC villager in villagers)
|
||||
{
|
||||
MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray();
|
||||
|
||||
villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
|
||||
villager.resetCurrentDialogue();
|
||||
|
||||
villager.currentMarriageDialogue.Set(marriageDialogue);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)."
|
||||
}
|
Loading…
Reference in New Issue