summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-03 14:31:27 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-03 14:31:27 -0500
commit04c6733adae9ce568aefb5d9dee6101097e994c5 (patch)
treec93f0650f6f79a95016c29526f8af437ad91a815 /src/SMAPI/Framework
parent48bb1581a6adeabfefbdd774011796e09a07aae2 (diff)
parent2b3f0506a16622b25a702aae250e10005287c4f4 (diff)
downloadSMAPI-04c6733adae9ce568aefb5d9dee6101097e994c5.tar.gz
SMAPI-04c6733adae9ce568aefb5d9dee6101097e994c5.tar.bz2
SMAPI-04c6733adae9ce568aefb5d9dee6101097e994c5.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs23
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs84
-rw-r--r--src/SMAPI/Framework/CursorPosition.cs21
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs5
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs18
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs20
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs36
-rw-r--r--src/SMAPI/Framework/SCore.cs4
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs15
9 files changed, 195 insertions, 31 deletions
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index f9027972..3d5bb29d 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -54,6 +54,9 @@ namespace StardewModdingAPI.Framework
/// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks>
private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim();
+ /// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary>
+ private readonly LocalizedContentManager VanillaContentManager;
+
/*********
** Accessors
@@ -95,6 +98,7 @@ namespace StardewModdingAPI.Framework
this.ContentManagers.Add(
this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset)
);
+ this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection);
}
@@ -150,6 +154,8 @@ namespace StardewModdingAPI.Framework
{
foreach (IContentManager contentManager in this.ContentManagers)
contentManager.OnLocaleChanged();
+
+ this.VanillaContentManager.Unload();
});
}
@@ -287,6 +293,23 @@ namespace StardewModdingAPI.Framework
});
}
+ /// <summary>Get a vanilla asset without interception.</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param>
+ public bool TryLoadVanillaAsset<T>(string assetName, out T asset)
+ {
+ try
+ {
+ asset = this.VanillaContentManager.Load<T>(assetName);
+ return true;
+ }
+ catch
+ {
+ asset = default;
+ return false;
+ }
+ }
+
/// <summary>Dispose held resources.</summary>
public void Dispose()
{
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index ad8f2ef1..424d6ff3 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
using xTile;
+using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -308,15 +309,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
return null;
}
- // validate asset
- if (data == null)
- {
- mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error);
- return null;
- }
-
// return matched asset
- return new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
+ return this.TryValidateLoadedAsset(info, data, mod)
+ ? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName)
+ : null;
}
/// <summary>Apply any <see cref="Editors"/> to a loaded asset.</summary>
@@ -386,5 +382,77 @@ namespace StardewModdingAPI.Framework.ContentManagers
// return result
return asset;
}
+
+ /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary>
+ /// <typeparam name="T">The asset type.</typeparam>
+ /// <param name="info">The basic asset metadata.</param>
+ /// <param name="data">The loaded asset data.</param>
+ /// <param name="mod">The mod which loaded the asset.</param>
+ private bool TryValidateLoadedAsset<T>(IAssetInfo info, T data, IModMetadata mod)
+ {
+ // can't load a null asset
+ if (data == null)
+ {
+ mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': mod incorrectly set asset to a null value.", LogLevel.Error);
+ return false;
+ }
+
+ // when replacing a map, the vanilla tilesheets must have the same order and IDs
+ if (data is Map loadedMap && this.Coordinator.TryLoadVanillaAsset(info.AssetName, out Map vanillaMap))
+ {
+ for (int i = 0; i < vanillaMap.TileSheets.Count; i++)
+ {
+ // check for match
+ TileSheet vanillaSheet = vanillaMap.TileSheets[i];
+ bool found = this.TryFindTilesheet(loadedMap, vanillaSheet.Id, out int loadedIndex, out TileSheet loadedSheet);
+ if (found && loadedIndex == i)
+ continue;
+
+ // handle mismatch
+ {
+ // only show warning if not farm map
+ // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting.
+ bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining");
+
+
+ string reason = found
+ ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaMap.TileSheets.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {i}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets."
+ : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes.";
+
+ SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval);
+ if (isFarmMap)
+ {
+ mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': {reason}", LogLevel.Error);
+ return false;
+ }
+ mod.LogAsMod($"SMAPI detected a potential issue with asset replacement for '{info.AssetName}' map: {reason}", LogLevel.Warn);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>Find a map tilesheet by ID.</summary>
+ /// <param name="map">The map whose tilesheets to search.</param>
+ /// <param name="id">The tilesheet ID to match.</param>
+ /// <param name="index">The matched tilesheet index, if any.</param>
+ /// <param name="tilesheet">The matched tilesheet, if any.</param>
+ private bool TryFindTilesheet(Map map, string id, out int index, out TileSheet tilesheet)
+ {
+ for (int i = 0; i < map.TileSheets.Count; i++)
+ {
+ if (map.TileSheets[i].Id == id)
+ {
+ index = i;
+ tilesheet = map.TileSheets[i];
+ return true;
+ }
+ }
+
+ index = -1;
+ tilesheet = null;
+ return false;
+ }
}
}
diff --git a/src/SMAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs
index 80d89994..107481e7 100644
--- a/src/SMAPI/Framework/CursorPosition.cs
+++ b/src/SMAPI/Framework/CursorPosition.cs
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
+using StardewValley;
namespace StardewModdingAPI.Framework
{
@@ -25,8 +26,8 @@ namespace StardewModdingAPI.Framework
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- /// <param name="absolutePixels">The pixel position relative to the top-left corner of the in-game map, adjusted for pixel zoom.</param>
- /// <param name="screenPixels">The pixel position relative to the top-left corner of the visible screen, adjusted for pixel zoom.</param>
+ /// <param name="absolutePixels">The pixel position relative to the top-left corner of the in-game map, adjusted for zoom but not UI scaling.</param>
+ /// <param name="screenPixels">The pixel position relative to the top-left corner of the visible screen, adjusted for zoom but not UI scaling.</param>
/// <param name="tile">The tile position relative to the top-left corner of the map.</param>
/// <param name="grabTile">The tile position that the game considers under the cursor for purposes of clicking actions.</param>
public CursorPosition(Vector2 absolutePixels, Vector2 screenPixels, Vector2 tile, Vector2 grabTile)
@@ -42,5 +43,21 @@ namespace StardewModdingAPI.Framework
{
return other != null && this.AbsolutePixels == other.AbsolutePixels;
}
+
+ /// <inheritdoc />
+ public Vector2 GetScaledAbsolutePixels()
+ {
+ return Game1.uiMode
+ ? Utility.ModifyCoordinatesForUIScale(this.AbsolutePixels)
+ : this.AbsolutePixels;
+ }
+
+ /// <inheritdoc />
+ public Vector2 GetScaledScreenPixels()
+ {
+ return Game1.uiMode
+ ? Utility.ModifyCoordinatesForUIScale(this.ScreenPixels)
+ : this.ScreenPixels;
+ }
}
}
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index c22b5718..fc1b434b 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -63,6 +63,11 @@ namespace StardewModdingAPI.Framework
this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, Environment.StackTrace));
}
+ /// <summary>A placeholder method used to track deprecated code for which a separate warning will be shown.</summary>
+ /// <param name="version">The SMAPI version which deprecated it.</param>
+ /// <param name="severity">How deprecated the code is.</param>
+ public void PlaceholderWarn(string version, DeprecationLevel severity) { }
+
/// <summary>Print any queued messages.</summary>
public void PrintQueued()
{
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index 23670202..a8d1f371 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -63,18 +63,16 @@ namespace StardewModdingAPI.Framework.Input
base.Update();
// update SMAPI extended data
+ // note: Stardew Valley is *not* in UI mode when this code runs
try
{
- float scale = Game1.options.uiScale;
+ float zoomMultiplier = (1f / Game1.options.zoomLevel);
// get real values
var controller = new GamePadStateBuilder(base.GetGamePadState());
var keyboard = new KeyboardStateBuilder(base.GetKeyboardState());
var mouse = new MouseStateBuilder(base.GetMouseState());
- Vector2 cursorAbsolutePos = new Vector2(
- x: (mouse.X / scale) + Game1.uiViewport.X,
- y: (mouse.Y / scale) + Game1.uiViewport.Y
- );
+ Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y);
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
HashSet<SButton> reallyDown = new HashSet<SButton>(this.GetPressedButtons(keyboard, mouse, controller));
@@ -109,7 +107,7 @@ namespace StardewModdingAPI.Framework.Input
if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
{
this.LastPlayerTile = playerTilePos;
- this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, scale);
+ this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier);
}
}
catch (InvalidOperationException)
@@ -202,11 +200,11 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>Get the current cursor position.</summary>
/// <param name="mouseState">The current mouse state.</param>
/// <param name="absolutePixels">The absolute pixel position relative to the map, adjusted for pixel zoom.</param>
- /// <param name="scale">The UI scale applied to pixel coordinates.</param>
- private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels, float scale)
+ /// <param name="zoomMultiplier">The multiplier applied to pixel coordinates to adjust them for pixel zoom.</param>
+ private CursorPosition GetCursorPosition(MouseState mouseState, Vector2 absolutePixels, float zoomMultiplier)
{
- Vector2 screenPixels = new Vector2(mouseState.X / scale, mouseState.Y / scale);
- Vector2 tile = new Vector2((int)((Game1.uiViewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.uiViewport.Y + screenPixels.Y) / Game1.tileSize));
+ Vector2 screenPixels = new Vector2(mouseState.X * zoomMultiplier, mouseState.Y * zoomMultiplier);
+ Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize));
Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton
? tile
: Game1.player.GetGrabTile();
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index ee013a85..e504218b 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -46,7 +46,7 @@ namespace StardewModdingAPI.Framework.Logging
search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
replacement:
#if SMAPI_FOR_WINDOWS
- "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).",
+ "Oops! Steam achievements won't work because Steam isn't loaded. See 'Launch SMAPI through Steam or GOG Galaxy' in the install guide for more info: https://smapi.io/install.",
#else
"Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.",
#endif
@@ -425,9 +425,11 @@ namespace StardewModdingAPI.Framework.Logging
this.Monitor.Log($" ({mod.ErrorDetails})");
}
- // find skipped dependencies
- IModMetadata[] skippedDependencies;
+ // group mods
+ List<IModMetadata> skippedDependencies = new List<IModMetadata>();
+ List<IModMetadata> otherSkippedMods = new List<IModMetadata>();
{
+ // track broken dependencies
HashSet<string> skippedDependencyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string> skippedModIds = new HashSet<string>(from mod in skippedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
foreach (IModMetadata mod in skippedMods)
@@ -435,7 +437,15 @@ namespace StardewModdingAPI.Framework.Logging
foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds()))
skippedDependencyIds.Add(requiredId);
}
- skippedDependencies = skippedMods.Where(p => p.HasID() && skippedDependencyIds.Contains(p.Manifest.UniqueID)).ToArray();
+
+ // collect mod groups
+ foreach (IModMetadata mod in skippedMods)
+ {
+ if (mod.HasID() && skippedDependencyIds.Contains(mod.Manifest.UniqueID))
+ skippedDependencies.Add(mod);
+ else
+ otherSkippedMods.Add(mod);
+ }
}
// log skipped mods
@@ -451,7 +461,7 @@ namespace StardewModdingAPI.Framework.Logging
this.Monitor.Newline();
}
- foreach (IModMetadata mod in skippedMods.OrderBy(p => p.DisplayName))
+ foreach (IModMetadata mod in otherSkippedMods.OrderBy(p => p.DisplayName))
LogSkippedMod(mod);
this.Monitor.Newline();
}
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 08df7b76..af7d90f6 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -72,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading
switch (mod.DataRecord?.Status)
{
case ModStatus.Obsolete:
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}");
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}", this.GetTechnicalReasonForStatusOverride(mod));
continue;
case ModStatus.AssumeBroken:
@@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading
error += $"version newer than {mod.DataRecord.StatusUpperVersion}";
error += " at " + string.Join(" or ", updateUrls);
- mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error);
+ mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error, this.GetTechnicalReasonForStatusOverride(mod));
}
continue;
}
@@ -409,6 +409,38 @@ namespace StardewModdingAPI.Framework.ModLoading
yield return new ModDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, FindMod(manifest.ContentPackFor.UniqueID), isRequired: true);
}
+ /// <summary>Get a technical message indicating why a mod's compatibility status was overridden, if applicable.</summary>
+ /// <param name="mod">The mod metadata.</param>
+ private string GetTechnicalReasonForStatusOverride(IModMetadata mod)
+ {
+ // get compatibility list record
+ var data = mod.DataRecord;
+ if (data == null)
+ return null;
+
+ // get status label
+ string statusLabel = data.Status switch
+ {
+ ModStatus.AssumeBroken => "'assume broken'",
+ ModStatus.AssumeCompatible => "'assume compatible'",
+ ModStatus.Obsolete => "obsolete",
+ _ => data.Status.ToString()
+ };
+
+ // get reason
+ string[] reasons = new[] { mod.DataRecord.StatusReasonPhrase, mod.DataRecord.StatusReasonDetails }
+ .Where(p => !string.IsNullOrWhiteSpace(p))
+ .ToArray();
+
+ // build message
+ return
+ $"marked {statusLabel} in SMAPI's internal compatibility list for "
+ + (mod.DataRecord.StatusUpperVersion != null ? $"versions up to {mod.DataRecord.StatusUpperVersion}" : "all versions")
+ + ": "
+ + (reasons.Any() ? string.Join(": ", reasons) : "no reason given")
+ + ".";
+ }
+
/*********
** Private models
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index e05213f0..5dc33828 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1128,7 +1128,7 @@ namespace StardewModdingAPI.Framework
modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender
// raise events
- this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID));
+ this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID));
}
/// <summary>Constructor a content manager to read game content files.</summary>
@@ -1532,7 +1532,7 @@ namespace StardewModdingAPI.Framework
// validate status
if (mod.Status == ModMetadataStatus.Failed)
{
- this.Monitor.Log($" Failed: {mod.Error}");
+ this.Monitor.Log($" Failed: {mod.ErrorDetails ?? mod.Error}");
failReason = mod.FailReason;
errorReasonPhrase = mod.Error;
return false;
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index f3b5e9b9..2f89fce9 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -10,6 +10,7 @@ using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialization;
+using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Network;
using StardewValley.SDKs;
@@ -54,15 +55,25 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether to log network traffic.</summary>
private readonly bool LogNetworkTraffic;
+ /// <summary>The backing field for <see cref="Peers"/>.</summary>
+ private readonly PerScreen<IDictionary<long, MultiplayerPeer>> PeersImpl = new(() => new Dictionary<long, MultiplayerPeer>());
+
+ /// <summary>The backing field for <see cref="HostPeer"/>.</summary>
+ private readonly PerScreen<MultiplayerPeer> HostPeerImpl = new();
+
/*********
** Accessors
*********/
/// <summary>The metadata for each connected peer.</summary>
- public IDictionary<long, MultiplayerPeer> Peers { get; } = new Dictionary<long, MultiplayerPeer>();
+ public IDictionary<long, MultiplayerPeer> Peers => this.PeersImpl.Value;
/// <summary>The metadata for the host player, if the current player is a farmhand.</summary>
- public MultiplayerPeer HostPeer;
+ public MultiplayerPeer HostPeer
+ {
+ get => this.HostPeerImpl.Value;
+ private set => this.HostPeerImpl.Value = value;
+ }
/*********