Minor fix
This commit is contained in:
parent
c0ef6a5b53
commit
0e859cfbcb
|
@ -28,46 +28,34 @@ ldnull
|
||||||
callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D)
|
callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D)
|
||||||
|
|
||||||
|
|
||||||
Optional Section
|
|
||||||
|
|
||||||
Fix back button£º
|
Optional
|
||||||
|
|
||||||
|
GamePad Input
|
||||||
Modify class StardewValley.Game1, modify method updateAndroidMenus(), modify Instructions at beginning:
|
Modify class StardewValley.Game1, modify method updateAndroidMenus(), modify Instructions at beginning:
|
||||||
ldsfld StardewValley.InputState StardewValley.Game1::input
|
ldsfld StardewValley.InputState StardewValley.Game1::input
|
||||||
callvirt Microsoft.Xna.Framework.Input.GamePadState StardewValley.InputState::GetGamePadState()
|
callvirt Microsoft.Xna.Framework.Input.GamePadState StardewValley.InputState::GetGamePadState()
|
||||||
|
|
||||||
|
|
||||||
|
Json Asset£º
|
||||||
|
checkForAction Prefix
|
||||||
|
|
||||||
Json Asset Support£º
|
0 ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
||||||
|
5 ldarg.0
|
||||||
|
6 callvirt System.Boolean StardewValley.ModHooks::OnObject_checkForAction(StardewValley.Object)
|
||||||
|
11 brtrue.s -> (6) ldarg.2
|
||||||
|
13 ldc.i4.0
|
||||||
|
14 ret
|
||||||
|
|
||||||
Modify class StardewValley.Object, modify method checkForAction,insert instructions at beginning:
|
isIndexOkForBasicShippedCategory Postfix
|
||||||
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
0 ldarg.0
|
||||||
ldarg.0
|
1 ldc.i4 434
|
||||||
callvirt System.Boolean StardewValley.ModHooks::OnObject_checkForAction(StardewValley.Object)
|
6 bne.un.s -> (5) ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
||||||
brtrue.s -> (6) ldarg.2
|
8 ldc.i4.0
|
||||||
ldc.i4.0
|
9 ret
|
||||||
ret
|
10 ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
||||||
|
15 ldarg.0
|
||||||
modify method isIndexOkForBasicShippedCategory,replace instructions:
|
16 ldloca.s -> (0) (System.Boolean)
|
||||||
ldarg.0
|
18 callvirt System.Void StardewValley.ModHooks::OnObject_isIndexOkForBasicShippedCategory(System.Int32,System.Boolean&)
|
||||||
ldc.i4 434
|
23 ldloc.0
|
||||||
bne.un.s -> (5) ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
24 ret
|
||||||
ldc.i4.0
|
|
||||||
ret
|
|
||||||
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
|
||||||
ldarg.0
|
|
||||||
ldloca.s -> (0) (System.Boolean)
|
|
||||||
callvirt System.Void StardewValley.ModHooks::OnObject_isIndexOkForBasicShippedCategory(System.Int32,System.Boolean&)
|
|
||||||
ldloc.0
|
|
||||||
ret
|
|
||||||
|
|
||||||
modify method canBePlacedHere insert instructions at beginning:
|
|
||||||
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
|
|
||||||
ldarg.0
|
|
||||||
ldarg.1
|
|
||||||
ldarg.2
|
|
||||||
ldloca.s -> (1) (System.Boolean)
|
|
||||||
callvirt System.Boolean StardewValley.ModHooks::OnObject_canBePlacedHere(StardewValley.Object,StardewValley.GameLocation,Microsoft.Xna.Framework.Vector2,System.Boolean&)
|
|
||||||
brtrue.s -> (9) ldarg.1
|
|
||||||
ldloc.1
|
|
||||||
ret
|
|
|
@ -544,6 +544,7 @@
|
||||||
<Compile Include="SMAPI\Framework\ModHelpers\InputHelper.cs" />
|
<Compile Include="SMAPI\Framework\ModHelpers\InputHelper.cs" />
|
||||||
<Compile Include="SMAPI\Framework\ModHelpers\ModHelper.cs" />
|
<Compile Include="SMAPI\Framework\ModHelpers\ModHelper.cs" />
|
||||||
<Compile Include="SMAPI\Framework\ModHelpers\ModRegistryHelper.cs" />
|
<Compile Include="SMAPI\Framework\ModHelpers\ModRegistryHelper.cs" />
|
||||||
|
<Compile Include="SMAPI\Framework\ModHelpers\MultiplayerHelper.cs" />
|
||||||
<Compile Include="SMAPI\Framework\ModHelpers\ReflectionHelper.cs" />
|
<Compile Include="SMAPI\Framework\ModHelpers\ReflectionHelper.cs" />
|
||||||
<Compile Include="SMAPI\Framework\ModHelpers\TranslationHelper.cs" />
|
<Compile Include="SMAPI\Framework\ModHelpers\TranslationHelper.cs" />
|
||||||
<Compile Include="SMAPI\Framework\ModLoading\AssemblyDefinitionResolver.cs" />
|
<Compile Include="SMAPI\Framework\ModLoading\AssemblyDefinitionResolver.cs" />
|
||||||
|
@ -600,6 +601,7 @@
|
||||||
<Compile Include="SMAPI\Framework\SGameConstructorHack.cs" />
|
<Compile Include="SMAPI\Framework\SGameConstructorHack.cs" />
|
||||||
<Compile Include="SMAPI\Framework\Singleton.cs" />
|
<Compile Include="SMAPI\Framework\Singleton.cs" />
|
||||||
<Compile Include="SMAPI\Framework\SModHooks.cs" />
|
<Compile Include="SMAPI\Framework\SModHooks.cs" />
|
||||||
|
<Compile Include="SMAPI\Framework\SMultiplayer.cs" />
|
||||||
<Compile Include="SMAPI\Framework\StateTracking\Comparers\EquatableComparer.cs" />
|
<Compile Include="SMAPI\Framework\StateTracking\Comparers\EquatableComparer.cs" />
|
||||||
<Compile Include="SMAPI\Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
|
<Compile Include="SMAPI\Framework\StateTracking\Comparers\GenericEqualsComparer.cs" />
|
||||||
<Compile Include="SMAPI\Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" />
|
<Compile Include="SMAPI\Framework\StateTracking\Comparers\ObjectReferenceComparer.cs" />
|
||||||
|
@ -641,6 +643,7 @@
|
||||||
<Compile Include="SMAPI\IModLinked.cs" />
|
<Compile Include="SMAPI\IModLinked.cs" />
|
||||||
<Compile Include="SMAPI\IModRegistry.cs" />
|
<Compile Include="SMAPI\IModRegistry.cs" />
|
||||||
<Compile Include="SMAPI\IMonitor.cs" />
|
<Compile Include="SMAPI\IMonitor.cs" />
|
||||||
|
<Compile Include="SMAPI\IMultiplayerHelper.cs" />
|
||||||
<Compile Include="SMAPI\IMultiplayerPeer.cs" />
|
<Compile Include="SMAPI\IMultiplayerPeer.cs" />
|
||||||
<Compile Include="SMAPI\IMultiplayerPeerMod.cs" />
|
<Compile Include="SMAPI\IMultiplayerPeerMod.cs" />
|
||||||
<Compile Include="SMAPI\Internal\ConsoleWriting\ColorfulConsoleWriter.cs" />
|
<Compile Include="SMAPI\Internal\ConsoleWriting\ColorfulConsoleWriter.cs" />
|
||||||
|
|
|
@ -4,6 +4,7 @@ using StardewValley;
|
||||||
using StardewModdingAPI.Framework;
|
using StardewModdingAPI.Framework;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace SMDroid
|
namespace SMDroid
|
||||||
{
|
{
|
||||||
|
@ -19,7 +20,7 @@ namespace SMDroid
|
||||||
|
|
||||||
public ModEntry()
|
public ModEntry()
|
||||||
{
|
{
|
||||||
this.core = new SCore("/sdcard/SMDroid/Mods", false);
|
this.core = new SCore(Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid/Mods"), false);
|
||||||
}
|
}
|
||||||
public override LocalizedContentManager OnGame1_CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
|
public override LocalizedContentManager OnGame1_CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
|
||||||
{
|
{
|
||||||
|
@ -30,7 +31,6 @@ namespace SMDroid
|
||||||
this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, SGame.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset);
|
this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, SGame.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset);
|
||||||
this.NextContentManagerIsMain = true;
|
this.NextContentManagerIsMain = true;
|
||||||
this.core.RunInteractively(this.ContentCore);
|
this.core.RunInteractively(this.ContentCore);
|
||||||
SGame.printLog("ROOT Directory:" + rootDirectory);
|
|
||||||
return this.ContentCore.CreateGameContentManager("Game1._temporaryContent");
|
return this.ContentCore.CreateGameContentManager("Game1._temporaryContent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,16 +32,16 @@ namespace StardewModdingAPI
|
||||||
public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
|
public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
|
||||||
|
|
||||||
/// <summary>The path to the game folder.</summary>
|
/// <summary>The path to the game folder.</summary>
|
||||||
public static string ExecutionPath { get; } = "/sdcard/SMDroid";
|
public static string ExecutionPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path , "SMDroid");
|
||||||
|
|
||||||
/// <summary>The directory path containing Stardew Valley's app data.</summary>
|
/// <summary>The directory path containing Stardew Valley's app data.</summary>
|
||||||
public static string DataPath { get; } = Path.Combine("/sdcard", "SMDroid");
|
public static string DataPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid");
|
||||||
|
|
||||||
/// <summary>The directory path in which error logs should be stored.</summary>
|
/// <summary>The directory path in which error logs should be stored.</summary>
|
||||||
public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs");
|
public static string LogDir { get; } = Path.Combine(Constants.DataPath, "ErrorLogs");
|
||||||
|
|
||||||
/// <summary>The directory path where all saves are stored.</summary>
|
/// <summary>The directory path where all saves are stored.</summary>
|
||||||
public static string SavesPath { get; } = Path.Combine("/sdcard", "StardewValley");
|
public static string SavesPath { get; } = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "StardewValley");
|
||||||
|
|
||||||
/// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary>
|
/// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary>
|
||||||
public static string SaveFolderName => Constants.GetSaveFolderName();
|
public static string SaveFolderName => Constants.GetSaveFolderName();
|
||||||
|
@ -56,7 +56,7 @@ namespace StardewModdingAPI
|
||||||
internal const string HomePageUrl = "https://smapi.io";
|
internal const string HomePageUrl = "https://smapi.io";
|
||||||
|
|
||||||
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
|
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
|
||||||
internal static readonly string InternalFilesPath = "/sdcard/SMDroid/smapi-internal";
|
internal static readonly string InternalFilesPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.Path, "SMDroid/smapi-internal");
|
||||||
|
|
||||||
/// <summary>The file path for the SMAPI configuration file.</summary>
|
/// <summary>The file path for the SMAPI configuration file.</summary>
|
||||||
internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json");
|
internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json");
|
||||||
|
|
|
@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework.Input
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// get new states
|
// get new states
|
||||||
GamePadState realController = GamePad.GetState(PlayerIndex.One);
|
GamePadState realController = GamePad.GetState(Game1.playerOneIndex);
|
||||||
KeyboardState realKeyboard = Keyboard.GetState();
|
KeyboardState realKeyboard = Keyboard.GetState();
|
||||||
MouseState realMouse = Mouse.GetState();
|
MouseState realMouse = Mouse.GetState();
|
||||||
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
|
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
public ICommandHelper ConsoleCommands { get; }
|
public ICommandHelper ConsoleCommands { get; }
|
||||||
|
|
||||||
/// <summary>Provides multiplayer utilities.</summary>
|
/// <summary>Provides multiplayer utilities.</summary>
|
||||||
//public IMultiplayerHelper Multiplayer { get; }
|
public IMultiplayerHelper Multiplayer { get; }
|
||||||
|
|
||||||
/// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary>
|
/// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary>
|
||||||
public ITranslationHelper Translation { get; }
|
public ITranslationHelper Translation { get; }
|
||||||
|
@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
|
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
|
||||||
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
|
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
|
||||||
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
|
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
|
||||||
public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, ITranslationHelper translationHelper)
|
public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper)
|
||||||
: base(modID)
|
: base(modID)
|
||||||
{
|
{
|
||||||
// validate directory
|
// validate directory
|
||||||
|
@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry));
|
this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry));
|
||||||
this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper));
|
this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper));
|
||||||
this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper));
|
this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper));
|
||||||
//this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
|
this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
|
||||||
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
|
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
|
||||||
this.Events = events;
|
this.Events = events;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using StardewModdingAPI.Framework.Networking;
|
||||||
|
using StardewValley;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
|
{
|
||||||
|
/// <summary>Provides multiplayer utilities.</summary>
|
||||||
|
internal class MultiplayerHelper : BaseHelper, IMultiplayerHelper
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>SMAPI's core multiplayer utility.</summary>
|
||||||
|
private readonly SMultiplayer Multiplayer;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="modID">The unique ID of the relevant mod.</param>
|
||||||
|
/// <param name="multiplayer">SMAPI's core multiplayer utility.</param>
|
||||||
|
public MultiplayerHelper(string modID, SMultiplayer multiplayer)
|
||||||
|
: base(modID)
|
||||||
|
{
|
||||||
|
this.Multiplayer = multiplayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a new multiplayer ID.</summary>
|
||||||
|
public long GetNewID()
|
||||||
|
{
|
||||||
|
return this.Multiplayer.getNewID();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the locations which are being actively synced from the host.</summary>
|
||||||
|
public IEnumerable<GameLocation> GetActiveLocations()
|
||||||
|
{
|
||||||
|
return this.Multiplayer.activeLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get a connected player.</summary>
|
||||||
|
/// <param name="id">The player's unique ID.</param>
|
||||||
|
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
|
||||||
|
public IMultiplayerPeer GetConnectedPlayer(long id)
|
||||||
|
{
|
||||||
|
return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer)
|
||||||
|
? peer
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get all connected players.</summary>
|
||||||
|
public IEnumerable<IMultiplayerPeer> GetConnectedPlayers()
|
||||||
|
{
|
||||||
|
return this.Multiplayer.Peers.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Send a message to mods installed by connected players.</summary>
|
||||||
|
/// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
|
||||||
|
/// <param name="message">The data to send over the network.</param>
|
||||||
|
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
|
||||||
|
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
|
||||||
|
/// <param name="playerIDs">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>
|
||||||
|
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
|
||||||
|
public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null)
|
||||||
|
{
|
||||||
|
this.Multiplayer.BroadcastModMessage(
|
||||||
|
message: message,
|
||||||
|
messageType: messageType,
|
||||||
|
fromModID: this.ModID,
|
||||||
|
toModIDs: modIDs,
|
||||||
|
toPlayerIDs: playerIDs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -981,7 +981,7 @@ namespace StardewModdingAPI.Framework
|
||||||
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
|
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
|
||||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
|
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
|
||||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
|
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
|
||||||
//IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
|
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
|
||||||
|
|
||||||
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
|
IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
|
||||||
{
|
{
|
||||||
|
@ -991,7 +991,7 @@ namespace StardewModdingAPI.Framework
|
||||||
return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
|
return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
// init mod
|
// init mod
|
||||||
|
|
|
@ -72,6 +72,7 @@ namespace StardewModdingAPI.Framework
|
||||||
internal bool OnObjectCanBePlacedHere(SObject instance, GameLocation location, Vector2 tile, ref bool result)
|
internal bool OnObjectCanBePlacedHere(SObject instance, GameLocation location, Vector2 tile, ref bool result)
|
||||||
{
|
{
|
||||||
ObjectCanBePlacedHereEventArgs args = new ObjectCanBePlacedHereEventArgs(instance, location, tile, result);
|
ObjectCanBePlacedHereEventArgs args = new ObjectCanBePlacedHereEventArgs(instance, location, tile, result);
|
||||||
|
args.__result = result;
|
||||||
bool run =this.Events.ObjectCanBePlacedHere.RaiseForChainRun(args);
|
bool run =this.Events.ObjectCanBePlacedHere.RaiseForChainRun(args);
|
||||||
result = args.__result;
|
result = args.__result;
|
||||||
return run;
|
return run;
|
||||||
|
@ -80,6 +81,7 @@ namespace StardewModdingAPI.Framework
|
||||||
internal void OnObjectIsIndexOkForBasicShippedCategory(int index, ref bool result)
|
internal void OnObjectIsIndexOkForBasicShippedCategory(int index, ref bool result)
|
||||||
{
|
{
|
||||||
ObjectIsIndexOkForBasicShippedCategoryEventArgs args = new ObjectIsIndexOkForBasicShippedCategoryEventArgs(index, result);
|
ObjectIsIndexOkForBasicShippedCategoryEventArgs args = new ObjectIsIndexOkForBasicShippedCategoryEventArgs(index, result);
|
||||||
|
args.__result = result;
|
||||||
this.Events.ObjectIsIndexOkForBasicShippedCategory.RaiseForChainRun(args);
|
this.Events.ObjectIsIndexOkForBasicShippedCategory.RaiseForChainRun(args);
|
||||||
result = args.__result;
|
result = args.__result;
|
||||||
}
|
}
|
||||||
|
@ -147,8 +149,8 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>Manages input visible to the game.</summary>
|
/// <summary>Manages input visible to the game.</summary>
|
||||||
public SInputState Input => (SInputState)this.Reflection.GetField<InputState>(typeof(Game1), "input").GetValue();
|
public SInputState Input => (SInputState)this.Reflection.GetField<InputState>(typeof(Game1), "input").GetValue();
|
||||||
|
|
||||||
///// <summary>The game's core multiplayer utility.</summary>
|
/// <summary>The game's core multiplayer utility.</summary>
|
||||||
//public SMultiplayer Multiplayer => (SMultiplayer)this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").GetValue();
|
public SMultiplayer Multiplayer => (SMultiplayer)this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").GetValue();
|
||||||
|
|
||||||
/// <summary>A list of queued commands to execute.</summary>
|
/// <summary>A list of queued commands to execute.</summary>
|
||||||
/// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks>
|
/// <remarks>This property must be threadsafe, since it's accessed from a separate console input thread.</remarks>
|
||||||
|
@ -198,7 +200,7 @@ namespace StardewModdingAPI.Framework
|
||||||
this.OnGameInitialised = onGameInitialised;
|
this.OnGameInitialised = onGameInitialised;
|
||||||
this.OnGameExiting = onGameExiting;
|
this.OnGameExiting = onGameExiting;
|
||||||
this.Reflection.GetField<InputState>(typeof(Game1), "input").SetValue(new SInputState());
|
this.Reflection.GetField<InputState>(typeof(Game1), "input").SetValue(new SInputState());
|
||||||
//this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").SetValue(new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived));
|
this.Reflection.GetField<Multiplayer>(typeof(Game1), "multiplayer").SetValue(new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived));
|
||||||
//Game1.hooks = new SModHooks(this.OnNewDayAfterFade);
|
//Game1.hooks = new SModHooks(this.OnNewDayAfterFade);
|
||||||
|
|
||||||
// init observables
|
// init observables
|
||||||
|
@ -500,7 +502,7 @@ namespace StardewModdingAPI.Framework
|
||||||
/*********
|
/*********
|
||||||
** Locale changed events
|
** Locale changed events
|
||||||
*********/
|
*********/
|
||||||
if (this.Watchers.LocaleWatcher.IsChanged)
|
if (this.Watchers.LocaleWatcher.IsChanged || SGame.TicksElapsed == 0)
|
||||||
{
|
{
|
||||||
this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace);
|
this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace);
|
||||||
this.OnLocaleChanged();
|
this.OnLocaleChanged();
|
||||||
|
@ -1605,6 +1607,7 @@ namespace StardewModdingAPI.Framework
|
||||||
string s = Game1.content.LoadString(@"Strings\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
|
string s = Game1.content.LoadString(@"Strings\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
|
||||||
SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 0x60, 0x20, "", 1f, -1, 0.088f);
|
SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 0x60, 0x20, "", 1f, -1, 0.088f);
|
||||||
}
|
}
|
||||||
|
events.Rendered.RaiseEmpty();
|
||||||
_spriteBatchEnd.Invoke();
|
_spriteBatchEnd.Invoke();
|
||||||
drawOverlays.Invoke(Game1.spriteBatch);
|
drawOverlays.Invoke(Game1.spriteBatch);
|
||||||
renderScreenBuffer.Invoke(BlendState.Opaque);
|
renderScreenBuffer.Invoke(BlendState.Opaque);
|
||||||
|
@ -1615,12 +1618,12 @@ namespace StardewModdingAPI.Framework
|
||||||
SpriteBatchBegin.Invoke(1f);
|
SpriteBatchBegin.Invoke(1f);
|
||||||
events.RenderingHud.RaiseEmpty();
|
events.RenderingHud.RaiseEmpty();
|
||||||
DrawHUD.Invoke();
|
DrawHUD.Invoke();
|
||||||
events.RenderedHud.RaiseEmpty();
|
|
||||||
if (((Game1.currentLocation != null) && !(Game1.activeClickableMenu is GameMenu)) && !(Game1.activeClickableMenu is QuestLog))
|
if (((Game1.currentLocation != null) && !(Game1.activeClickableMenu is GameMenu)) && !(Game1.activeClickableMenu is QuestLog))
|
||||||
{
|
{
|
||||||
Game1.currentLocation.drawAboveAlwaysFrontLayerText(Game1.spriteBatch);
|
Game1.currentLocation.drawAboveAlwaysFrontLayerText(Game1.spriteBatch);
|
||||||
}
|
}
|
||||||
DrawAfterMap.Invoke();
|
DrawAfterMap.Invoke();
|
||||||
|
events.RenderedHud.RaiseEmpty();
|
||||||
_spriteBatchEnd.Invoke();
|
_spriteBatchEnd.Invoke();
|
||||||
if (Game1.tutorialManager != null)
|
if (Game1.tutorialManager != null)
|
||||||
{
|
{
|
||||||
|
@ -1658,7 +1661,6 @@ namespace StardewModdingAPI.Framework
|
||||||
_spriteBatchEnd.Invoke();
|
_spriteBatchEnd.Invoke();
|
||||||
}
|
}
|
||||||
DrawTutorialUI.Invoke();
|
DrawTutorialUI.Invoke();
|
||||||
events.Rendered.RaiseEmpty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,515 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
//using Galaxy.Api;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using StardewModdingAPI.Events;
|
||||||
|
using StardewModdingAPI.Framework.Events;
|
||||||
|
using StardewModdingAPI.Framework.Networking;
|
||||||
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
|
using StardewModdingAPI.Toolkit.Serialisation;
|
||||||
|
using StardewValley;
|
||||||
|
using StardewValley.Network;
|
||||||
|
using StardewValley.SDKs;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework
|
||||||
|
{
|
||||||
|
/// <summary>SMAPI's implementation of the game's core multiplayer logic.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// SMAPI syncs mod context to all players through the host as such:
|
||||||
|
/// 1. Farmhand sends ModContext + PlayerIntro.
|
||||||
|
/// 2. If host receives ModContext: it stores the context, replies with known contexts, and forwards it to other farmhands.
|
||||||
|
/// 3. If host receives PlayerIntro before ModContext: it stores a 'vanilla player' context, and forwards it to other farmhands.
|
||||||
|
/// 4. If farmhand receives ModContext: it stores it.
|
||||||
|
/// 5. If farmhand receives ServerIntro without a preceding ModContext: it stores a 'vanilla host' context.
|
||||||
|
/// 6. If farmhand receives PlayerIntro without a preceding ModContext AND it's not the host peer: it stores a 'vanilla player' context.
|
||||||
|
///
|
||||||
|
/// Once a farmhand/server stored a context, messages can be sent to that player through the SMAPI APIs.
|
||||||
|
/// </remarks>
|
||||||
|
internal class SMultiplayer : Multiplayer
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>Encapsulates monitoring and logging.</summary>
|
||||||
|
private readonly IMonitor Monitor;
|
||||||
|
|
||||||
|
/// <summary>Tracks the installed mods.</summary>
|
||||||
|
private readonly ModRegistry ModRegistry;
|
||||||
|
|
||||||
|
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
|
||||||
|
private readonly JsonHelper JsonHelper;
|
||||||
|
|
||||||
|
/// <summary>Simplifies access to private code.</summary>
|
||||||
|
private readonly Reflector Reflection;
|
||||||
|
|
||||||
|
/// <summary>Manages SMAPI events.</summary>
|
||||||
|
private readonly EventManager EventManager;
|
||||||
|
|
||||||
|
/// <summary>A callback to invoke when a mod message is received.</summary>
|
||||||
|
private readonly Action<ModMessageModel> OnModMessageReceived;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Accessors
|
||||||
|
*********/
|
||||||
|
/// <summary>The metadata for each connected peer.</summary>
|
||||||
|
public IDictionary<long, MultiplayerPeer> Peers { get; } = new Dictionary<long, MultiplayerPeer>();
|
||||||
|
|
||||||
|
/// <summary>The metadata for the host player, if the current player is a farmhand.</summary>
|
||||||
|
public MultiplayerPeer HostPeer;
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
||||||
|
/// <param name="eventManager">Manages SMAPI events.</param>
|
||||||
|
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
|
||||||
|
/// <param name="modRegistry">Tracks the installed mods.</param>
|
||||||
|
/// <param name="reflection">Simplifies access to private code.</param>
|
||||||
|
/// <param name="onModMessageReceived">A callback to invoke when a mod message is received.</param>
|
||||||
|
public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action<ModMessageModel> onModMessageReceived)
|
||||||
|
{
|
||||||
|
this.Monitor = monitor;
|
||||||
|
this.EventManager = eventManager;
|
||||||
|
this.JsonHelper = jsonHelper;
|
||||||
|
this.ModRegistry = modRegistry;
|
||||||
|
this.Reflection = reflection;
|
||||||
|
this.OnModMessageReceived = onModMessageReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Perform cleanup needed when a multiplayer session ends.</summary>
|
||||||
|
public void CleanupOnMultiplayerExit()
|
||||||
|
{
|
||||||
|
this.Peers.Clear();
|
||||||
|
this.HostPeer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initialise a client before the game connects to a remote server.</summary>
|
||||||
|
/// <param name="client">The client to initialise.</param>
|
||||||
|
public override Client InitClient(Client client)
|
||||||
|
{
|
||||||
|
switch (client)
|
||||||
|
{
|
||||||
|
//case LidgrenClient _:
|
||||||
|
// {
|
||||||
|
// string address = this.Reflection.GetField<string>(client, "address").GetValue();
|
||||||
|
// return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
|
||||||
|
// }
|
||||||
|
|
||||||
|
//case GalaxyNetClient _:
|
||||||
|
// {
|
||||||
|
// GalaxyID address = this.Reflection.GetField<GalaxyID>(client, "lobbyId").GetValue();
|
||||||
|
// return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
|
||||||
|
// }
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}", LogLevel.Trace);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initialise a server before the game connects to an incoming player.</summary>
|
||||||
|
/// <param name="server">The server to initialise.</param>
|
||||||
|
public override Server InitServer(Server server)
|
||||||
|
{
|
||||||
|
switch (server)
|
||||||
|
{
|
||||||
|
//case LidgrenServer _:
|
||||||
|
// {
|
||||||
|
// IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
|
||||||
|
// return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage);
|
||||||
|
// }
|
||||||
|
|
||||||
|
//case GalaxyNetServer _:
|
||||||
|
// {
|
||||||
|
// IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
|
||||||
|
// return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage);
|
||||||
|
// }
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}", LogLevel.Trace);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A callback raised when sending a message as a farmhand.</summary>
|
||||||
|
/// <param name="message">The message being sent.</param>
|
||||||
|
/// <param name="sendMessage">Send an arbitrary message through the client.</param>
|
||||||
|
/// <param name="resume">Resume sending the underlying message.</param>
|
||||||
|
protected void OnClientSendingMessage(OutgoingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
|
||||||
|
{
|
||||||
|
if (this.Monitor.IsVerbose)
|
||||||
|
this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
|
||||||
|
|
||||||
|
switch (message.MessageType)
|
||||||
|
{
|
||||||
|
// sync mod context (step 1)
|
||||||
|
case (byte)MessageType.PlayerIntroduction:
|
||||||
|
sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// run default logic
|
||||||
|
default:
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Process an incoming network message as the host player.</summary>
|
||||||
|
/// <param name="message">The message to process.</param>
|
||||||
|
/// <param name="sendMessage">A method which sends the given message to the client.</param>
|
||||||
|
/// <param name="resume">Process the message using the game's default logic.</param>
|
||||||
|
public void OnServerProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
|
||||||
|
{
|
||||||
|
if (this.Monitor.IsVerbose)
|
||||||
|
this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
|
||||||
|
|
||||||
|
switch (message.MessageType)
|
||||||
|
{
|
||||||
|
// sync mod context (step 2)
|
||||||
|
case (byte)MessageType.ModContext:
|
||||||
|
{
|
||||||
|
// parse message
|
||||||
|
RemoteContextModel model = this.ReadContext(message.Reader);
|
||||||
|
this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
||||||
|
|
||||||
|
// store peer
|
||||||
|
MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false);
|
||||||
|
if (this.Peers.ContainsKey(message.FarmerID))
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info);
|
||||||
|
this.Peers.Remove(message.FarmerID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.AddPeer(newPeer, canBeHost: false, raiseEvent: false);
|
||||||
|
|
||||||
|
// reply with own context
|
||||||
|
this.Monitor.VerboseLog(" Replying with host context...");
|
||||||
|
newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
|
||||||
|
|
||||||
|
// reply with other players' context
|
||||||
|
foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
|
||||||
|
{
|
||||||
|
this.Monitor.VerboseLog($" Replying with context for player {otherPeer.PlayerID}...");
|
||||||
|
newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// forward to other peers
|
||||||
|
if (this.Peers.Count > 1)
|
||||||
|
{
|
||||||
|
object[] fields = this.GetContextSyncMessageFields(newPeer);
|
||||||
|
foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
|
||||||
|
{
|
||||||
|
this.Monitor.VerboseLog($" Forwarding context to player {otherPeer.PlayerID}...");
|
||||||
|
otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// raise event
|
||||||
|
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// handle player intro
|
||||||
|
case (byte)MessageType.PlayerIntroduction:
|
||||||
|
// store peer if new
|
||||||
|
if (!this.Peers.ContainsKey(message.FarmerID))
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace);
|
||||||
|
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false);
|
||||||
|
this.AddPeer(peer, canBeHost: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// handle mod message
|
||||||
|
case (byte)MessageType.ModMessage:
|
||||||
|
this.ReceiveModMessage(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Process an incoming network message as a farmhand.</summary>
|
||||||
|
/// <param name="message">The message to process.</param>
|
||||||
|
/// <param name="sendMessage">Send an arbitrary message through the client.</param>
|
||||||
|
/// <param name="resume">Resume processing the message using the game's default logic.</param>
|
||||||
|
/// <returns>Returns whether the message was handled.</returns>
|
||||||
|
public void OnClientProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
|
||||||
|
{
|
||||||
|
if (this.Monitor.IsVerbose)
|
||||||
|
this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);
|
||||||
|
|
||||||
|
switch (message.MessageType)
|
||||||
|
{
|
||||||
|
// mod context sync (step 4)
|
||||||
|
case (byte)MessageType.ModContext:
|
||||||
|
{
|
||||||
|
// parse message
|
||||||
|
RemoteContextModel model = this.ReadContext(message.Reader);
|
||||||
|
this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);
|
||||||
|
|
||||||
|
// store peer
|
||||||
|
MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null);
|
||||||
|
if (peer.IsHost && this.HostPeer != null)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.AddPeer(peer, canBeHost: true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// handle server intro
|
||||||
|
case (byte)MessageType.ServerIntroduction:
|
||||||
|
{
|
||||||
|
// store peer
|
||||||
|
if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null)
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace);
|
||||||
|
this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false);
|
||||||
|
}
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle player intro
|
||||||
|
case (byte)MessageType.PlayerIntroduction:
|
||||||
|
{
|
||||||
|
// store peer
|
||||||
|
if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
|
||||||
|
{
|
||||||
|
peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null);
|
||||||
|
this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace);
|
||||||
|
this.AddPeer(peer, canBeHost: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle mod message
|
||||||
|
case (byte)MessageType.ModMessage:
|
||||||
|
this.ReceiveModMessage(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
resume();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Remove players who are disconnecting.</summary>
|
||||||
|
protected override void removeDisconnectedFarmers()
|
||||||
|
{
|
||||||
|
//foreach (long playerID in this.disconnectingFarmers)
|
||||||
|
//{
|
||||||
|
// if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
|
||||||
|
// {
|
||||||
|
// this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace);
|
||||||
|
// this.Peers.Remove(playerID);
|
||||||
|
// this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer));
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
base.removeDisconnectedFarmers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Broadcast a mod message to matching players.</summary>
|
||||||
|
/// <param name="message">The data to send over the network.</param>
|
||||||
|
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
|
||||||
|
/// <param name="fromModID">The unique ID of the mod sending the message.</param>
|
||||||
|
/// <param name="toModIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
|
||||||
|
/// <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
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter player IDs
|
||||||
|
HashSet<long> playerIDs = null;
|
||||||
|
if (toPlayerIDs != null && toPlayerIDs.Any())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get data to send
|
||||||
|
ModMessageModel model = new ModMessageModel(
|
||||||
|
fromPlayerID: Game1.player.UniqueMultiplayerID,
|
||||||
|
fromModID: fromModID,
|
||||||
|
toModIDs: toModIDs,
|
||||||
|
toPlayerIDs: playerIDs?.ToArray(),
|
||||||
|
type: messageType,
|
||||||
|
data: JToken.FromObject(message)
|
||||||
|
);
|
||||||
|
string data = JsonConvert.SerializeObject(model, Formatting.None);
|
||||||
|
|
||||||
|
// log message
|
||||||
|
if (this.Monitor.IsVerbose)
|
||||||
|
this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);
|
||||||
|
|
||||||
|
// send message
|
||||||
|
if (Context.IsMainPlayer)
|
||||||
|
{
|
||||||
|
foreach (MultiplayerPeer peer in this.Peers.Values)
|
||||||
|
{
|
||||||
|
if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
|
||||||
|
{
|
||||||
|
model.ToPlayerIDs = new[] { peer.PlayerID };
|
||||||
|
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.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Save a received peer.</summary>
|
||||||
|
/// <param name="peer">The peer to add.</param>
|
||||||
|
/// <param name="canBeHost">Whether to track the peer as the host if applicable.</param>
|
||||||
|
/// <param name="raiseEvent">Whether to raise the <see cref="Events.EventManager.PeerContextReceived"/> event.</param>
|
||||||
|
private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true)
|
||||||
|
{
|
||||||
|
// store
|
||||||
|
this.Peers[peer.PlayerID] = peer;
|
||||||
|
if (canBeHost && peer.IsHost)
|
||||||
|
this.HostPeer = peer;
|
||||||
|
|
||||||
|
// raise event
|
||||||
|
if (raiseEvent)
|
||||||
|
this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Read the metadata context for a player.</summary>
|
||||||
|
/// <param name="reader">The stream reader.</param>
|
||||||
|
private RemoteContextModel ReadContext(BinaryReader reader)
|
||||||
|
{
|
||||||
|
string data = reader.ReadString();
|
||||||
|
RemoteContextModel model = this.JsonHelper.Deserialise<RemoteContextModel>(data);
|
||||||
|
return model.ApiVersion != null
|
||||||
|
? model
|
||||||
|
: null; // no data available for unmodded players
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Receive a mod message sent from another player's mods.</summary>
|
||||||
|
/// <param name="message">The raw message to parse.</param>
|
||||||
|
private void ReceiveModMessage(IncomingMessage message)
|
||||||
|
{
|
||||||
|
// parse message
|
||||||
|
string json = message.Reader.ReadString();
|
||||||
|
ModMessageModel model = this.JsonHelper.Deserialise<ModMessageModel>(json);
|
||||||
|
HashSet<long> playerIDs = new HashSet<long>(model.ToPlayerIDs ?? this.GetKnownPlayerIDs());
|
||||||
|
if (this.Monitor.IsVerbose)
|
||||||
|
this.Monitor.Log($"Received message: {json}.", LogLevel.Trace);
|
||||||
|
|
||||||
|
// notify local mods
|
||||||
|
if (playerIDs.Contains(Game1.player.UniqueMultiplayerID))
|
||||||
|
this.OnModMessageReceived(model);
|
||||||
|
|
||||||
|
// forward to other players
|
||||||
|
if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID))
|
||||||
|
{
|
||||||
|
ModMessageModel newModel = new ModMessageModel(model);
|
||||||
|
foreach (long playerID in playerIDs)
|
||||||
|
{
|
||||||
|
if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
|
||||||
|
{
|
||||||
|
newModel.ToPlayerIDs = new[] { peer.PlayerID };
|
||||||
|
this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}.");
|
||||||
|
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get all connected player IDs, including the current player.</summary>
|
||||||
|
private IEnumerable<long> GetKnownPlayerIDs()
|
||||||
|
{
|
||||||
|
yield return Game1.player.UniqueMultiplayerID;
|
||||||
|
foreach (long peerID in this.Peers.Keys)
|
||||||
|
yield return peerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the fields to include in a context sync message sent to other players.</summary>
|
||||||
|
private object[] GetContextSyncMessageFields()
|
||||||
|
{
|
||||||
|
RemoteContextModel model = new RemoteContextModel
|
||||||
|
{
|
||||||
|
IsHost = Context.IsWorldReady && Context.IsMainPlayer,
|
||||||
|
Platform = Constants.TargetPlatform,
|
||||||
|
ApiVersion = Constants.ApiVersion,
|
||||||
|
GameVersion = Constants.GameVersion,
|
||||||
|
Mods = this.ModRegistry
|
||||||
|
.GetAll()
|
||||||
|
.Select(mod => new RemoteContextModModel
|
||||||
|
{
|
||||||
|
ID = mod.Manifest.UniqueID,
|
||||||
|
Name = mod.Manifest.Name,
|
||||||
|
Version = mod.Manifest.Version
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
return new object[] { this.JsonHelper.Serialise(model, Formatting.None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the fields to include in a context sync message sent to other players.</summary>
|
||||||
|
/// <param name="peer">The peer whose data to represent.</param>
|
||||||
|
private object[] GetContextSyncMessageFields(IMultiplayerPeer peer)
|
||||||
|
{
|
||||||
|
if (!peer.HasSmapi)
|
||||||
|
return new object[] { "{}" };
|
||||||
|
|
||||||
|
RemoteContextModel model = new RemoteContextModel
|
||||||
|
{
|
||||||
|
IsHost = peer.IsHost,
|
||||||
|
Platform = peer.Platform.Value,
|
||||||
|
ApiVersion = peer.ApiVersion,
|
||||||
|
GameVersion = peer.GameVersion,
|
||||||
|
Mods = peer.Mods
|
||||||
|
.Select(mod => new RemoteContextModModel
|
||||||
|
{
|
||||||
|
ID = mod.ID,
|
||||||
|
Name = mod.Name,
|
||||||
|
Version = mod.Version
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
return new object[] { this.JsonHelper.Serialise(model, Formatting.None) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,8 +35,8 @@ namespace StardewModdingAPI
|
||||||
/// <summary>Metadata about loaded mods.</summary>
|
/// <summary>Metadata about loaded mods.</summary>
|
||||||
IModRegistry ModRegistry { get; }
|
IModRegistry ModRegistry { get; }
|
||||||
|
|
||||||
///// <summary>Provides multiplayer utilities.</summary>
|
/// <summary>Provides multiplayer utilities.</summary>
|
||||||
//IMultiplayerHelper Multiplayer { get; }
|
IMultiplayerHelper Multiplayer { get; }
|
||||||
|
|
||||||
/// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary>
|
/// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary>
|
||||||
ITranslationHelper Translation { get; }
|
ITranslationHelper Translation { get; }
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using StardewValley;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI
|
||||||
|
{
|
||||||
|
/// <summary>Provides multiplayer utilities.</summary>
|
||||||
|
public interface IMultiplayerHelper : IModLinked
|
||||||
|
{
|
||||||
|
/// <summary>Get a new multiplayer ID.</summary>
|
||||||
|
long GetNewID();
|
||||||
|
|
||||||
|
/// <summary>Get the locations which are being actively synced from the host.</summary>
|
||||||
|
IEnumerable<GameLocation> GetActiveLocations();
|
||||||
|
|
||||||
|
/// <summary>Get a connected player.</summary>
|
||||||
|
/// <param name="id">The player's unique ID.</param>
|
||||||
|
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
|
||||||
|
IMultiplayerPeer GetConnectedPlayer(long id);
|
||||||
|
|
||||||
|
/// <summary>Get all connected players.</summary>
|
||||||
|
IEnumerable<IMultiplayerPeer> GetConnectedPlayers();
|
||||||
|
|
||||||
|
/// <summary>Send a message to mods installed by connected players.</summary>
|
||||||
|
/// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
|
||||||
|
/// <param name="message">The data to send over the network.</param>
|
||||||
|
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
|
||||||
|
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
|
||||||
|
/// <param name="playerIDs">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>
|
||||||
|
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
|
||||||
|
void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue