Last active
January 21, 2021 04:40
-
-
Save spacechase0/ed486e9dd0d94b2b4c87a50ded8a3ef1 to your computer and use it in GitHub Desktop.
SDV/SMAPI custom net root
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Harmony; | |
using Netcode; | |
using SpaceShared; | |
using StardewModdingAPI; | |
using StardewModdingAPI.Events; | |
using StardewModdingAPI.Utilities; | |
using StardewValley; | |
using StardewValley.Network; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Runtime.CompilerServices; | |
using System.Text; | |
using System.Threading.Tasks; | |
namespace TestMod | |
{ | |
partial class Mod : StardewModdingAPI.Mod | |
{ | |
public static Mod instance; | |
public override void Entry( IModHelper helper ) | |
{ | |
instance = this; | |
Log.Monitor = Monitor; | |
FrameworkEntry(); | |
ModEntry(); | |
} | |
} | |
// Mod | |
public class MyNetModel : NetModel | |
{ | |
private readonly NetInt value = new NetInt( 0 ); | |
public int Value { get => value.Value; set => this.value.Value = value; } | |
} | |
public partial class Mod : StardewModdingAPI.Mod | |
{ | |
public PerScreen< Func< MyNetModel > > netObj = new PerScreen<Func< MyNetModel >>(); | |
public MyNetModel NetObj => netObj.Value(); | |
public void ModEntry() | |
{ | |
Helper.Events.GameLoop.SaveCreated += ( s, e ) => InitModel(); | |
Helper.Events.GameLoop.SaveLoaded += ( s, e ) => InitModel(); | |
Helper.ConsoleCommands.Add( "value", "Print or set net value", DoCommand ); | |
} | |
private void DoCommand( string cmd, string[] args ) | |
{ | |
Log.info( "meow:" + object.ReferenceEquals( NetObj, ModelRoots.Value[ "Mine" ].Value.Model.Value ) ); | |
if ( args.Length == 0 ) | |
Log.info( $"Value: {NetObj.Value}" ); | |
else | |
NetObj.Value = int.Parse( args[ 0 ] ); | |
} | |
private void InitModel() | |
{ | |
netObj.Value = AddNetModel< MyNetModel >( "Mine" ); | |
} | |
} | |
// Framework | |
internal class NetModelValues : INetObject<NetFields> | |
{ | |
public NetRef<NetModel> Model { get; } = new NetRef<NetModel>(); | |
public NetFields NetFields { get; } = new NetFields(); | |
public NetModelValues() { NetFields.AddField( Model ); } | |
} | |
internal class NetModelsRoot : NetRoot<NetModelValues> | |
{ | |
public NetModelsRoot() | |
{ | |
Value = new NetModelValues(); | |
} | |
} | |
public abstract class NetModel : INetObject< NetFields > | |
{ | |
public NetFields NetFields { get; } = new NetFields(); | |
public NetModel() | |
{ | |
var fields = this.GetType().GetFields( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance ); | |
foreach ( var field in fields ) | |
{ | |
if ( field.FieldType.GetInterfaces().Contains( typeof( INetSerializable ) ) ) | |
NetFields.AddField( ( INetSerializable ) field.GetValue( this ) ); | |
} | |
} | |
} | |
public class MessageContainer | |
{ | |
public string ID { get; set; } | |
public byte[] Data { get; set; } | |
public MessageContainer() { } | |
public MessageContainer( string id, byte[] data ) | |
{ | |
ID = id; | |
Data = data; | |
} | |
} | |
public partial class Mod : StardewModdingAPI.Mod | |
{ | |
internal PerScreen<Dictionary<string, NetModelsRoot>> ModelRoots = new PerScreen<Dictionary<string, NetModelsRoot>>( () => new Dictionary<string, NetModelsRoot>()); // This could be per-mod? | |
internal const string MSG_CONNECT = "Connect"; | |
internal const string MSG_DELTA = "Delta"; | |
internal const int DELTA_BROADCAST = 3; | |
// This would become part of the MP helper I guess? | |
// Has to be called during/after SaveCreated/SaveLoaded | |
// If you add one with the same ID as one existing (such as after connecting as a client, | |
// and the host value is received first) it returns the one you should use. | |
public Func<T> AddNetModel< T >( string ID ) where T : NetModel, new() | |
{ | |
if ( !ModelRoots.Value.ContainsKey( ID ) ) | |
{ | |
var model = new T(); | |
var root = new NetModelsRoot(); | |
root.Value.Model.Value = model; | |
ModelRoots.Value.Add( ID, root ); | |
if ( Game1.multiplayerMode != Game1.singlePlayer ) | |
{ | |
var mp = (Multiplayer) typeof( Game1 ).GetField( "multiplayer", BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null ); | |
Log.trace( $"Sending stuff (2, {ID}) to all" ); | |
var mods = new string[] { ModManifest.UniqueID }; | |
// writeObjectFullBytes uses CreateConnectionPacket | |
// From what I could tell in how NetRoot works, | |
// we really need to do this individually so we can pass the peer ID in | |
foreach ( var peer in Helper.Multiplayer.GetConnectedPlayers() ) | |
{ | |
Helper.Multiplayer.SendMessage( new MessageContainer( ID, mp.writeObjectFullBytes( root, peer.PlayerID ) ), MSG_CONNECT, mods ); | |
} | |
} | |
} | |
return () => ( T ) ModelRoots.Value[ ID ].Value.Model.Value; | |
} | |
public void FrameworkEntry() | |
{ | |
Helper.Events.Multiplayer.ModMessageReceived += OnModMessageReceived; | |
Helper.Events.Multiplayer.PeerConnected += OnPeerConnected; | |
Helper.Events.GameLoop.UpdateTicked += UpdateTicked; | |
Helper.Events.GameLoop.ReturnedToTitle += ( s, e ) => ModelRoots.Value.Clear(); | |
var harmony = HarmonyInstance.Create( ModManifest.UniqueID ); | |
harmony.PatchAll(); | |
} | |
private void OnModMessageReceived( object sender, ModMessageReceivedEventArgs e ) | |
{ | |
if ( Game1.multiplayerMode == Game1.singlePlayer ) | |
return; | |
var mp = (Multiplayer) typeof( Game1 ).GetField( "multiplayer", BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null ); | |
if ( e.Type == MSG_CONNECT && e.FromModID == ModManifest.UniqueID ) | |
{ | |
var msg = e.ReadAs< MessageContainer >(); | |
Log.trace( $"Got connection {msg.ID} from {e.FromPlayerID}" ); | |
var root = new NetModelsRoot(); | |
using var ms = new MemoryStream( msg.Data ); | |
using var br = new BinaryReader( ms ); | |
root.ReadConnectionPacket( br ); | |
root.Clock.InterpolationTicks = mp.defaultInterpolationTicks; | |
ModelRoots.Value.Add( msg.ID, root ); | |
} | |
else if ( e.Type == MSG_DELTA && e.FromModID == ModManifest.UniqueID ) | |
{ | |
var msg = e.ReadAs< MessageContainer >(); | |
Log.trace( $"Got delta {msg.ID} from {e.FromPlayerID}" ); | |
Log.info( "meow1:" + object.ReferenceEquals( NetObj, ModelRoots.Value[ "Mine" ].Value.Model.Value ) ); | |
using var ms = new MemoryStream( msg.Data ); | |
using var br = new BinaryReader( ms ); | |
mp.readObjectDelta( br, ModelRoots.Value[ msg.ID ] ); | |
Log.info( "meow2:" + object.ReferenceEquals( NetObj, ModelRoots.Value[ "Mine" ].Value.Model.Value ) ); | |
} | |
} | |
private void OnPeerConnected( object sender, PeerConnectedEventArgs e ) | |
{ | |
var mp = (Multiplayer) typeof( Game1 ).GetField( "multiplayer", BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null ); | |
if ( Game1.IsMasterGame ) | |
{ | |
Log.trace( $"Sending stuff (1) to {e.Peer.PlayerID}" ); | |
var mods = new string[] { ModManifest.UniqueID }; | |
var players = new long[] { e.Peer.PlayerID }; | |
// writeObjectFullBytes uses CreateConnectionPacket | |
foreach ( var rootEntry in ModelRoots.Value ) | |
{ | |
Helper.Multiplayer.SendMessage( new MessageContainer( rootEntry.Key, mp.writeObjectFullBytes( rootEntry.Value, e.Peer.PlayerID ) ), MSG_CONNECT, mods, players ); | |
} | |
} | |
} | |
private void UpdateTicked( object sender, UpdateTickedEventArgs e ) | |
{ | |
if ( ModelRoots.Value == null ) | |
return; | |
var mp = (Multiplayer) typeof( Game1 ).GetField( "multiplayer", BindingFlags.NonPublic | BindingFlags.Static ).GetValue( null ); | |
if ( Game1.multiplayerMode != Game1.singlePlayer ) | |
{ | |
if ( !mp.allowSyncDelay() /* || forceSync ? */ || Game1.ticks % DELTA_BROADCAST == 0 ) | |
{ | |
foreach ( var rootEntry in ModelRoots.Value ) | |
{ | |
if ( rootEntry.Value.Dirty ) | |
{ | |
Log.trace( "Syncing " + rootEntry.Key ); | |
var mods = new string[] { ModManifest.UniqueID }; | |
Helper.Multiplayer.SendMessage( new MessageContainer( rootEntry.Key, mp.writeObjectDeltaBytes( rootEntry.Value ) ), MSG_DELTA, mods ); | |
} | |
} | |
} | |
} | |
} | |
} | |
// This method is virtual and so could be managed by SMAPI without a Harmony patch | |
// Needs to be in here because of stuff in Multiplayer.updateRoot doing disconnect stuff | |
[HarmonyPatch( typeof( Multiplayer ), nameof( Multiplayer.updateRoots ) )] | |
public static class MultiplayerUpdateRootsHook | |
{ | |
public static void Postfix( Multiplayer __instance ) | |
{ | |
// Our stuff isn't setup yet | |
if ( Mod.instance.ModelRoots.Value == null ) | |
return; | |
// This updates the clock and tree for delta calculation I think? | |
// Doesn't actually send deltas | |
foreach ( var rootEntry in Mod.instance.ModelRoots.Value ) | |
{ | |
rootEntry.Value.Clock.InterpolationTicks = __instance.interpolationTicks(); | |
__instance.updateRoot( rootEntry.Value ); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment