Skip to content

Instantly share code, notes, and snippets.

@spacechase0
Last active January 21, 2021 04:40
Show Gist options
  • Save spacechase0/ed486e9dd0d94b2b4c87a50ded8a3ef1 to your computer and use it in GitHub Desktop.
Save spacechase0/ed486e9dd0d94b2b4c87a50ded8a3ef1 to your computer and use it in GitHub Desktop.
SDV/SMAPI custom net root
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