Restructuring of the files

This commit is contained in:
2025-02-25 12:58:41 +02:00
parent 8a25503432
commit b37fd411c6
154 changed files with 5 additions and 12 deletions

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
namespace Facepunch.Steamworks.Data
{
/// <summary>
/// Used as a base to create your client connection. This creates a socket
/// to a single connection.
///
/// You can override all the virtual functions to turn it into what you
/// want it to do.
/// </summary>
public struct Connection
{
public uint Id { get; set; }
public override string ToString() => Id.ToString();
public static implicit operator Connection( uint value ) => new Connection() { Id = value };
public static implicit operator uint( Connection value ) => value.Id;
/// <summary>
/// Accept an incoming connection that has been received on a listen socket.
/// </summary>
public Result Accept()
{
return SteamNetworkingSockets.Internal.AcceptConnection( this );
}
/// <summary>
/// Disconnects from the remote host and invalidates the connection handle. Any unread data on the connection is discarded..
/// reasonCode is defined and used by you.
/// </summary>
public bool Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" )
{
return SteamNetworkingSockets.Internal.CloseConnection( this, reasonCode, debugString, linger );
}
/// <summary>
/// Get/Set connection user data
/// </summary>
public long UserData
{
get => SteamNetworkingSockets.Internal.GetConnectionUserData( this );
set => SteamNetworkingSockets.Internal.SetConnectionUserData( this, value );
}
/// <summary>
/// A name for the connection, used mostly for debugging
/// </summary>
public string ConnectionName
{
get
{
if ( !SteamNetworkingSockets.Internal.GetConnectionName( this, out var strVal ) )
return "ERROR";
return strVal;
}
set => SteamNetworkingSockets.Internal.SetConnectionName( this, value );
}
/// <summary>
/// This is the best version to use.
/// </summary>
public Result SendMessage( IntPtr ptr, int size, SendType sendType = SendType.Reliable )
{
long messageNumber = 0;
return SteamNetworkingSockets.Internal.SendMessageToConnection( this, ptr, (uint) size, (int)sendType, ref messageNumber );
}
/// <summary>
/// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and
/// you're not creating a new one every frame (like using .ToArray())
/// </summary>
public unsafe Result SendMessage( byte[] data, SendType sendType = SendType.Reliable )
{
fixed ( byte* ptr = data )
{
return SendMessage( (IntPtr)ptr, data.Length, sendType );
}
}
/// <summary>
/// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and
/// you're not creating a new one every frame (like using .ToArray())
/// </summary>
public unsafe Result SendMessage( byte[] data, int offset, int length, SendType sendType = SendType.Reliable )
{
fixed ( byte* ptr = data )
{
return SendMessage( (IntPtr)ptr + offset, length, sendType );
}
}
/// <summary>
/// This creates a ton of garbage - so don't do anything with this beyond testing!
/// </summary>
public unsafe Result SendMessage( string str, SendType sendType = SendType.Reliable )
{
var bytes = System.Text.Encoding.UTF8.GetBytes( str );
return SendMessage( bytes, sendType );
}
/// <summary>
/// Flush any messages waiting on the Nagle timer and send them at the next transmission
/// opportunity (often that means right now).
/// </summary>
public Result Flush() => SteamNetworkingSockets.Internal.FlushMessagesOnConnection( this );
/// <summary>
/// Returns detailed connection stats in text format. Useful
/// for dumping to a log, etc.
/// </summary>
/// <returns>Plain text connection info</returns>
public string DetailedStatus()
{
if ( SteamNetworkingSockets.Internal.GetDetailedConnectionStatus( this, out var strVal ) != 0 )
return null;
return strVal;
}
}
}

View File

@@ -0,0 +1,45 @@
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
/// <summary>
/// Describe the state of a connection
/// </summary>
[StructLayout( LayoutKind.Sequential, Size = 696 )]
public struct ConnectionInfo
{
internal NetIdentity identity;
internal long userData;
internal Socket listenSocket;
internal NetAddress address;
internal ushort pad;
internal SteamNetworkingPOPID popRemote;
internal SteamNetworkingPOPID popRelay;
internal ConnectionState state;
internal int endReason;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst = 128 )]
internal string endDebug;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst = 128 )]
internal string connectionDescription;
/// <summary>
/// High level state of the connection
/// </summary>
public ConnectionState State => state;
/// <summary>
/// Remote address. Might be all 0's if we don't know it, or if this is N/A.
/// </summary>
public NetAddress Address => address;
/// <summary>
/// Who is on the other end? Depending on the connection type and phase of the connection, we might not know
/// </summary>
public NetIdentity Identity => identity;
/// <summary>
/// Basic cause of the connection termination or problem.
/// </summary>
public NetConnectionEnd EndReason => (NetConnectionEnd)endReason;
}
}

View File

@@ -0,0 +1,142 @@
using Facepunch.Steamworks.Data;
using System;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks
{
public class ConnectionManager
{
/// <summary>
/// An optional interface to use instead of deriving
/// </summary>
public IConnectionManager Interface { get; set; }
/// <summary>
/// The actual connection we're managing
/// </summary>
public Connection Connection;
/// <summary>
/// The last received ConnectionInfo
/// </summary>
public ConnectionInfo ConnectionInfo { get; internal set; }
public bool Connected = false;
public bool Connecting = true;
public string ConnectionName
{
get => Connection.ConnectionName;
set => Connection.ConnectionName = value;
}
public long UserData
{
get => Connection.UserData;
set => Connection.UserData = value;
}
public void Close() => Connection.Close();
public override string ToString() => Connection.ToString();
public virtual void OnConnectionChanged( ConnectionInfo info )
{
ConnectionInfo = info;
switch ( info.State )
{
case ConnectionState.Connecting:
OnConnecting( info );
break;
case ConnectionState.Connected:
OnConnected( info );
break;
case ConnectionState.ClosedByPeer:
case ConnectionState.ProblemDetectedLocally:
case ConnectionState.None:
OnDisconnected( info );
break;
}
}
/// <summary>
/// We're trying to connect!
/// </summary>
public virtual void OnConnecting( ConnectionInfo info )
{
Interface?.OnConnecting( info );
Connecting = true;
}
/// <summary>
/// Client is connected. They move from connecting to Connections
/// </summary>
public virtual void OnConnected( ConnectionInfo info )
{
Interface?.OnConnected( info );
Connected = true;
Connecting = false;
}
/// <summary>
/// The connection has been closed remotely or disconnected locally. Check data.State for details.
/// </summary>
public virtual void OnDisconnected( ConnectionInfo info )
{
Interface?.OnDisconnected( info );
Connected = false;
Connecting = false;
}
public void Receive( int bufferSize = 32 )
{
int processed = 0;
IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize );
try
{
processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, messageBuffer, bufferSize );
for ( int i = 0; i < processed; i++ )
{
ReceiveMessage( Marshal.ReadIntPtr( messageBuffer, i * IntPtr.Size ) );
}
}
finally
{
Marshal.FreeHGlobal( messageBuffer );
}
//
// Overwhelmed our buffer, keep going
//
if ( processed == bufferSize )
Receive( bufferSize );
}
internal unsafe void ReceiveMessage( IntPtr msgPtr )
{
var msg = Marshal.PtrToStructure<NetMsg>( msgPtr );
try
{
OnMessage( msg.DataPtr, msg.DataSize, msg.RecvTime, msg.MessageNumber, msg.Channel );
}
finally
{
//
// Releases the message
//
NetMsg.InternalRelease( (NetMsg*) msgPtr );
}
}
public virtual void OnMessage( IntPtr data, int size, long messageNum, long recvTime, int channel )
{
Interface?.OnMessage( data, size, messageNum, recvTime, channel );
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public interface IConnectionManager
{
/// <summary>
/// We started connecting to this guy
/// </summary>
void OnConnecting( ConnectionInfo info );
/// <summary>
/// Called when the connection is fully connected and can start being communicated with
/// </summary>
void OnConnected( ConnectionInfo info );
/// <summary>
/// We got disconnected
/// </summary>
void OnDisconnected( ConnectionInfo info );
/// <summary>
/// Received a message
/// </summary>
void OnMessage( IntPtr data, int size, long messageNum, long recvTime, int channel );
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
public interface ISocketManager
{
/// <summary>
/// Must call Accept or Close on the connection within a second or so
/// </summary>
void OnConnecting( Connection connection, ConnectionInfo info );
/// <summary>
/// Called when the connection is fully connected and can start being communicated with
/// </summary>
void OnConnected( Connection connection, ConnectionInfo info );
/// <summary>
/// Called when the connection leaves
/// </summary>
void OnDisconnected( Connection connection, ConnectionInfo info );
/// <summary>
/// Received a message from a connection
/// </summary>
void OnMessage( Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel );
}
}

View File

@@ -0,0 +1,156 @@
using System.Net;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Explicit, Size = 18, Pack = 1 )]
public partial struct NetAddress
{
[FieldOffset( 0 )]
internal IPV4 ip;
[FieldOffset( 16 )]
internal ushort port;
internal struct IPV4
{
internal ulong m_8zeros;
internal ushort m_0000;
internal ushort m_ffff;
internal byte ip0;
internal byte ip1;
internal byte ip2;
internal byte ip3;
}
/// <summary>
/// The Port. This is redundant documentation.
/// </summary>
public ushort Port => port;
/// <summary>
/// Any IP, specific port
/// </summary>
public static NetAddress AnyIp( ushort port )
{
var addr = Cleared;
addr.port = port;
return addr;
}
/// <summary>
/// Localhost IP, specific port
/// </summary>
public static NetAddress LocalHost( ushort port )
{
var local = Cleared;
InternalSetIPv6LocalHost( ref local, port );
return local;
}
/// <summary>
/// Specific IP, specific port
/// </summary>
public static NetAddress From( string addrStr, ushort port )
{
return From( IPAddress.Parse( addrStr ), port );
}
/// <summary>
/// Specific IP, specific port
/// </summary>
public static NetAddress From( IPAddress address, ushort port )
{
var addr = address.GetAddressBytes();
if ( address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork )
{
var local = Cleared;
InternalSetIPv4( ref local, Utility.IpToInt32( address ), port );
return local;
}
throw new System.NotImplementedException( "Oops - no IPV6 support yet?" );
}
/// <summary>
/// Set everything to zero
/// </summary>
public static NetAddress Cleared
{
get
{
NetAddress self = default;
InternalClear( ref self );
return self;
}
}
/// <summary>
/// Return true if the IP is ::0. (Doesn't check port.)
/// </summary>
public bool IsIPv6AllZeros
{
get
{
NetAddress self = this;
return InternalIsIPv6AllZeros( ref self );
}
}
/// <summary>
/// Return true if IP is mapped IPv4
/// </summary>
public bool IsIPv4
{
get
{
NetAddress self = this;
return InternalIsIPv4( ref self );
}
}
/// <summary>
/// Return true if this identity is localhost. (Either IPv6 ::1, or IPv4 127.0.0.1)
/// </summary>
public bool IsLocalHost
{
get
{
NetAddress self = this;
return InternalIsLocalHost( ref self );
}
}
/// <summary>
/// Get the Address section
/// </summary>
public IPAddress Address
{
get
{
if ( IsIPv4 )
{
NetAddress self = this;
var ip = InternalGetIPv4( ref self );
return Utility.Int32ToIp( ip );
}
if ( IsIPv6AllZeros )
{
return IPAddress.IPv6Loopback;
}
throw new System.NotImplementedException( "Oops - no IPV6 support yet?" );
}
}
public override string ToString()
{
var ptr = Helpers.TakeMemory();
var self = this;
InternalToString( ref self, ptr, Helpers.MemoryBufferSize, true );
return Helpers.MemoryToString( ptr );
}
}
}

View File

@@ -0,0 +1,9 @@
using Facepunch.Steamworks.Data;
using System;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[UnmanagedFunctionPointer( Platform.CC )]
delegate void NetDebugFunc( [In] NetDebugOutput nType, [In] IntPtr pszMsg );
}

View File

@@ -0,0 +1,10 @@
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks.Data
{
internal unsafe struct NetErrorMessage
{
public fixed char Value[1024];
}
}

View File

@@ -0,0 +1,126 @@
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Explicit, Size = 136, Pack = 1 )]
public partial struct NetIdentity
{
[FieldOffset( 0 )]
internal IdentityType type;
[FieldOffset( 4 )]
internal int size;
[FieldOffset( 8 )]
internal ulong steamid;
[FieldOffset( 8 )]
internal NetAddress netaddress;
/// <summary>
/// Return a NetIdentity that represents LocalHost
/// </summary>
public static NetIdentity LocalHost
{
get
{
NetIdentity id = default;
InternalSetLocalHost( ref id );
return id;
}
}
public bool IsSteamId => type == IdentityType.SteamID;
public bool IsIpAddress => type == IdentityType.IPAddress;
/// <summary>
/// Return true if this identity is localhost
/// </summary>
public bool IsLocalHost
{
get
{
NetIdentity id = default;
return InternalIsLocalHost( ref id );
}
}
/// <summary>
/// Convert to a SteamId
/// </summary>
/// <param name="value"></param>
public static implicit operator NetIdentity( SteamId value )
{
NetIdentity id = default;
InternalSetSteamID( ref id, value );
return id;
}
/// <summary>
/// Set the specified Address
/// </summary>
public static implicit operator NetIdentity( NetAddress address )
{
NetIdentity id = default;
InternalSetIPAddr( ref id, ref address );
return id;
}
/// <summary>
/// Automatically convert to a SteamId
/// </summary>
/// <param name="value"></param>
public static implicit operator SteamId( NetIdentity value )
{
return value.SteamId;
}
/// <summary>
/// Returns NULL if we're not a SteamId
/// </summary>
public SteamId SteamId
{
get
{
if ( type != IdentityType.SteamID ) return default;
var id = this;
return InternalGetSteamID( ref id );
}
}
/// <summary>
/// Returns NULL if we're not a NetAddress
/// </summary>
public NetAddress Address
{
get
{
if ( type != IdentityType.IPAddress ) return default;
var id = this;
var addrptr = InternalGetIPAddr( ref id );
return addrptr.ToType<NetAddress>();
}
}
/// <summary>
/// We override tostring to provide a sensible representation
/// </summary>
public override string ToString()
{
var id = this;
SteamNetworkingUtils.Internal.SteamNetworkingIdentity_ToString( ref id, out var str );
return str;
}
internal enum IdentityType
{
Invalid = 0,
IPAddress = 1,
GenericString = 2,
GenericBytes = 3,
SteamID = 16
}
}
}

View File

@@ -0,0 +1,31 @@
using Facepunch.Steamworks.Data;
using System;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Explicit, Pack = Platform.StructPlatformPackSize )]
internal struct NetKeyValue
{
[FieldOffset(0)]
internal NetConfig Value; // m_eValue ESteamNetworkingConfigValue
[FieldOffset( 4 )]
internal NetConfigType DataType; // m_eDataType ESteamNetworkingConfigDataType
[FieldOffset( 8 )]
internal long Int64Value; // m_int64 int64_t
[FieldOffset( 8 )]
internal int Int32Value; // m_val_int32 int32_t
[FieldOffset( 8 )]
internal float FloatValue; // m_val_float float
[FieldOffset( 8 )]
internal IntPtr PointerValue; // m_val_functionPtr void *
// TODO - support strings, maybe
}
}

View File

@@ -0,0 +1,21 @@
using Facepunch.Steamworks.Data;
using System;
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Sequential )]
internal partial struct NetMsg
{
internal IntPtr DataPtr;
internal int DataSize;
internal Connection Connection;
internal NetIdentity Identity;
internal long ConnectionUserData;
internal long RecvTime;
internal long MessageNumber;
internal IntPtr FreeDataPtr;
internal IntPtr ReleasePtr;
internal int Channel;
}
}

View File

@@ -0,0 +1,67 @@
using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
/// <summary>
///
/// Object that describes a "location" on the Internet with sufficient
/// detail that we can reasonably estimate an upper bound on the ping between
/// the two hosts, even if a direct route between the hosts is not possible,
/// and the connection must be routed through the Steam Datagram Relay network.
/// This does not contain any information that identifies the host. Indeed,
/// if two hosts are in the same building or otherwise have nearly identical
/// networking characteristics, then it's valid to use the same location
/// object for both of them.
///
/// NOTE: This object should only be used in the same process! Do not serialize it,
/// send it over the wire, or persist it in a file or database! If you need
/// to do that, convert it to a string representation using the methods in
/// ISteamNetworkingUtils().
///
/// </summary>
[StructLayout( LayoutKind.Explicit, Size = 512 )]
public struct NetPingLocation
{
public static NetPingLocation? TryParseFromString( string str )
{
var result = default( NetPingLocation );
if ( !SteamNetworkingUtils.Internal.ParsePingLocationString( str, ref result ) )
return null;
return result;
}
public override string ToString()
{
SteamNetworkingUtils.Internal.ConvertPingLocationToString( ref this, out var strVal );
return strVal;
}
/// Estimate the round-trip latency between two arbitrary locations, in
/// milliseconds. This is a conservative estimate, based on routing through
/// the relay network. For most basic relayed connections, this ping time
/// will be pretty accurate, since it will be based on the route likely to
/// be actually used.
///
/// If a direct IP route is used (perhaps via NAT traversal), then the route
/// will be different, and the ping time might be better. Or it might actually
/// be a bit worse! Standard IP routing is frequently suboptimal!
///
/// But even in this case, the estimate obtained using this method is a
/// reasonable upper bound on the ping time. (Also it has the advantage
/// of returning immediately and not sending any packets.)
///
/// In a few cases we might not able to estimate the route. In this case
/// a negative value is returned. k_nSteamNetworkingPing_Failed means
/// the reason was because of some networking difficulty. (Failure to
/// ping, etc) k_nSteamNetworkingPing_Unknown is returned if we cannot
/// currently answer the question for some other reason.
///
/// Do you need to be able to do this from a backend/matchmaking server?
/// You are looking for the "ticketgen" library.
public int EstimatePingTo( NetPingLocation target )
{
return SteamNetworkingUtils.Internal.EstimatePingTimeBetweenTwoLocations( ref this, ref target );
}
}
}

View File

@@ -0,0 +1,29 @@

using System.Runtime.InteropServices;
namespace Facepunch.Steamworks.Data
{
[StructLayout( LayoutKind.Sequential )]
public struct Socket
{
internal uint Id;
public override string ToString() => Id.ToString();
public static implicit operator Socket( uint value ) => new Socket() { Id = value };
public static implicit operator uint( Socket value ) => value.Id;
/// <summary>
/// Destroy a listen socket. All the connections that were accepting on the listen
/// socket are closed ungracefully.
/// </summary>
public bool Close()
{
return SteamNetworkingSockets.Internal.CloseListenSocket( Id );
}
public SocketManager Manager
{
get => SteamNetworkingSockets.GetSocketManager( Id );
set => SteamNetworkingSockets.SetSocketManager( Id, value );
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Facepunch.Steamworks.Data;
namespace Facepunch.Steamworks
{
/// <summary>
/// Used as a base to create your networking server. This creates a socket
/// and listens/communicates with multiple queries.
///
/// You can override all the virtual functions to turn it into what you
/// want it to do.
/// </summary>
public partial class SocketManager
{
public ISocketManager Interface { get; set; }
public List<Connection> Connecting = new List<Connection>();
public List<Connection> Connected = new List<Connection>();
public Socket Socket { get; internal set; }
public override string ToString() => Socket.ToString();
internal HSteamNetPollGroup pollGroup;
internal void Initialize()
{
pollGroup = SteamNetworkingSockets.Internal.CreatePollGroup();
}
public bool Close()
{
if ( SteamNetworkingSockets.Internal.IsValid )
{
SteamNetworkingSockets.Internal.DestroyPollGroup( pollGroup );
Socket.Close();
}
pollGroup = 0;
Socket = 0;
return true;
}
public virtual void OnConnectionChanged( Connection connection, ConnectionInfo info )
{
switch ( info.State )
{
case ConnectionState.Connecting:
if ( !Connecting.Contains( connection ) )
{
Connecting.Add( connection );
OnConnecting( connection, info );
}
break;
case ConnectionState.Connected:
if ( !Connected.Contains( connection ) )
{
Connecting.Remove( connection );
Connected.Add( connection );
OnConnected( connection, info );
}
break;
case ConnectionState.ClosedByPeer:
case ConnectionState.ProblemDetectedLocally:
case ConnectionState.None:
if ( Connecting.Contains( connection ) || Connected.Contains( connection ) )
{
OnDisconnected( connection, info );
}
break;
}
}
/// <summary>
/// Default behaviour is to accept every connection
/// </summary>
public virtual void OnConnecting( Connection connection, ConnectionInfo info )
{
if ( Interface != null )
{
Interface.OnConnecting( connection, info );
return;
}
else
{
connection.Accept();
}
}
/// <summary>
/// Client is connected. They move from connecting to Connections
/// </summary>
public virtual void OnConnected( Connection connection, ConnectionInfo info )
{
SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, pollGroup );
Interface?.OnConnected( connection, info );
}
/// <summary>
/// The connection has been closed remotely or disconnected locally. Check data.State for details.
/// </summary>
public virtual void OnDisconnected( Connection connection, ConnectionInfo info )
{
SteamNetworkingSockets.Internal.SetConnectionPollGroup( connection, 0 );
connection.Close();
Connecting.Remove( connection );
Connected.Remove( connection );
Interface?.OnDisconnected( connection, info );
}
public void Receive( int bufferSize = 32 )
{
int processed = 0;
IntPtr messageBuffer = Marshal.AllocHGlobal( IntPtr.Size * bufferSize );
try
{
processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnPollGroup( pollGroup, messageBuffer, bufferSize );
for ( int i = 0; i < processed; i++ )
{
ReceiveMessage( Marshal.ReadIntPtr( messageBuffer, i * IntPtr.Size ) );
}
}
finally
{
Marshal.FreeHGlobal( messageBuffer );
}
//
// Overwhelmed our buffer, keep going
//
if ( processed == bufferSize )
Receive( bufferSize );
}
internal unsafe void ReceiveMessage( IntPtr msgPtr )
{
var msg = Marshal.PtrToStructure<NetMsg>( msgPtr );
try
{
OnMessage( msg.Connection, msg.Identity, msg.DataPtr, msg.DataSize, msg.RecvTime, msg.MessageNumber, msg.Channel );
}
finally
{
//
// Releases the message
//
NetMsg.InternalRelease( (NetMsg*) msgPtr );
}
}
public virtual void OnMessage( Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel )
{
Interface?.OnMessage( connection, identity, data, size, messageNum, recvTime, channel );
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Data
{
struct SteamDatagramRelayAuthTicket
{
// Not implemented, not used
};
}