Networking/LinkCableMode.cs
using Emulotl.Gba;
namespace Emulotl;
public sealed class LinkCableMode : INetMode
{
private const int HostSlot = 0;
private readonly NetworkManager _net;
public LinkCableSession Session { get; private set; }
public Action<LinkCableAvPacket> OnRemoteAv;
public Action<LinkCableStatePacket> OnRemoteState;
public bool ExternalAvDriver { get; set; }
private bool _active;
private int _linkedMask;
public LinkCableMode( NetworkManager net )
{
_net = net;
}
public void AttachSession( LinkCableSession session ) => Session = session;
public void DetachSession() => Session = null;
public void OnPeerJoined( Connection conn ) => ApplyRoster();
public void OnPeerLeft( int slot, Connection conn ) => ApplyRoster();
public void OnGameStarted() => BeginLinkedPlay();
public void Tick() => Pump();
public void OnHostLeft()
{
var emu = _net.Emulator;
if ( emu?.IsLinked == true )
emu.ResumeSoloAfterHostLost();
else
_net.ClearSessionAfterHostLost();
}
public void Exit()
{
_active = false;
_linkedMask = 0;
_net.Emulator?.EndLinkedMode();
}
public void BeginLinkedPlay()
{
var emu = _net.Emulator;
if ( emu == null )
return;
if ( _net.IsHost )
emu.BeginLinkedHost( _net.Roster.ActiveSlots() );
else if ( _net.IsClient )
emu.BeginLinkedClient();
}
private void ApplyRoster()
{
if ( !Networking.IsHost )
return;
var emu = _net.Emulator;
if ( emu == null )
return;
int mask = _net.Roster.ActiveMask();
if ( mask == _linkedMask )
return;
int count = CountBits( mask );
if ( !_active )
{
if ( count < 2 )
return;
_active = true;
_linkedMask = mask;
_net.SetState( SessionState.InGame );
_net.BroadcastBeginGame();
return;
}
if ( count < 2 )
{
_active = false;
_linkedMask = 0;
ReturnToLobby();
emu.ContinueSoloFromLinkedHost();
return;
}
int additions = mask & ~_linkedMask;
_linkedMask = mask;
emu.SyncLinkedHostSlots( _net.Roster.ActiveSlots() );
for ( int slot = 0; slot < _net.Roster.SlotCount; slot++ )
{
if ( (additions & (1 << slot)) == 0 )
continue;
var conn = _net.Roster.ConnectionAt( slot );
if ( conn is not null && conn != Connection.Local )
_net.SendJoinLinkedGame( conn );
}
}
private void ReturnToLobby()
{
_net.Roster.ReleaseRemoteSlots();
_net.CommitRoster();
_net.SetState( SessionState.Hosting );
}
private static int CountBits( int mask )
{
int n = 0;
while ( mask != 0 )
{
n += mask & 1;
mask >>= 1;
}
return n;
}
public void SendLocalInput( long frame, ushort keys )
{
if ( !_net.IsClient )
return;
var packet = new LinkCableInputPacket( frame, keys ).Serialize();
_net.SendRaw( HostSlot, packet );
}
public void SendLocalSave( byte[] saveData )
{
if ( !_net.IsClient || saveData == null || saveData.Length == 0 )
return;
var packet = new LinkCableSaveUploadPacket
{
PlayerId = _net.LocalPlayer?.Slot ?? -1,
SaveData = saveData,
}.Serialize();
_net.SendRaw( HostSlot, packet );
}
public void HostSendAv( int slot, LinkCableAvPacket packet )
{
if ( !_net.IsHost || packet is null )
return;
_net.SendRaw( slot, packet.Serialize() );
}
public void HostSendState( int slot, byte[] state )
{
if ( !_net.IsHost || state is null || state.Length == 0 )
return;
_net.SendRaw( slot, new LinkCableStatePacket { PlayerId = slot, State = state }.Serialize() );
}
private void Pump()
{
var session = Session;
if ( session is null || !_net.IsHost || ExternalAvDriver )
return;
var instances = session.Host.Instances;
for ( int i = 0; i < instances.Count; i++ )
{
var inst = instances[i];
if ( inst.Slot < 0 )
continue;
while ( session.TryDequeueAv( inst, out var packet ) )
_net.SendRaw( inst.Slot, packet.Serialize() );
}
}
public void OnPacket( int senderSlot, byte[] payload )
{
if ( payload.Length < 1 )
return;
switch ( (LinkCablePacketType)payload[0] )
{
case LinkCablePacketType.Input:
RouteInput( senderSlot, payload );
break;
case LinkCablePacketType.SaveUpload:
RouteSaveUpload( senderSlot, payload );
break;
case LinkCablePacketType.Av:
RouteAv( payload );
break;
case LinkCablePacketType.State:
RouteState( payload );
break;
}
}
private void RouteInput( int senderSlot, byte[] payload )
{
if ( !_net.IsHost || Session is null )
return;
if ( LinkCableInputPacket.TryParse( payload, payload.Length, out var packet ) )
Session.SetSlotInput( senderSlot, packet.Keys );
}
private void RouteSaveUpload( int senderSlot, byte[] payload )
{
if ( !_net.IsHost || Session is null )
return;
if ( LinkCableSaveUploadPacket.TryParse( payload, payload.Length, out var packet ) )
Session.LoadSlotSave( senderSlot, packet.SaveData );
}
private void RouteAv( byte[] payload )
{
if ( !_net.IsClient )
return;
if ( LinkCableAvPacket.TryParse( payload, payload.Length, out var packet ) )
{
try { OnRemoteAv?.Invoke( packet ); }
catch ( Exception e ) { Log.Warning( $"[Emulotl] OnRemoteAv error: {e.Message}" ); }
}
}
private void RouteState( byte[] payload )
{
if ( !_net.IsClient )
return;
if ( LinkCableStatePacket.TryParse( payload, payload.Length, out var packet ) )
{
try { OnRemoteState?.Invoke( packet ); }
catch ( Exception e ) { Log.Warning( $"[Emulotl] OnRemoteState error: {e.Message}" ); }
}
}
}