This commit is contained in:
David Salz 2018-04-13 18:48:08 +02:00
commit 482dc27b8e
47 changed files with 23734 additions and 25138 deletions

View File

@ -51,5 +51,15 @@ public void PurchaseTime()
}
}
[TestMethod]
public void AppName()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
var name = client.App.GetName( 4000 );
Console.WriteLine( name );
}
}
}
}

View File

@ -50,16 +50,23 @@ public void Avatar()
{
Assert.IsTrue( client.IsValid );
var friend = client.Friends.All.First();
ulong id = (ulong)( 76561197960279927 + (new Random().Next() % 10000));
bool passed = false;
client.Friends.GetAvatar( Steamworks.Friends.AvatarSize.Medium, friend.Id, (avatar) =>
client.Friends.GetAvatar( Steamworks.Friends.AvatarSize.Medium, id, ( avatar) =>
{
Assert.AreEqual(avatar.Width, 64);
Assert.AreEqual(avatar.Height, 64);
Assert.AreEqual(avatar.Data.Length, avatar.Width * avatar.Height * 4);
if ( avatar == null )
{
Console.WriteLine( "No Avatar" );
}
else
{
Assert.AreEqual( avatar.Width, 64 );
Assert.AreEqual( avatar.Height, 64 );
Assert.AreEqual( avatar.Data.Length, avatar.Width * avatar.Height * 4 );
DrawImage(avatar);
DrawImage( avatar );
}
passed = true;
});
@ -105,6 +112,7 @@ public static void DrawImage( Image img )
var brightness = 1 - ((float)(p.r + p.g + p.b) / (255.0f * 3.0f));
var c = (int) ((grad.Length) * brightness);
if ( c > 3 ) c = 3;
str += grad[c];
}

View File

@ -15,6 +15,12 @@ public void InventoryDefinitions()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsNotNull( client.Inventory.Definitions );
Assert.AreNotEqual( 0, client.Inventory.Definitions.Length );
@ -36,6 +42,12 @@ public void InventoryDefinitionExchange()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsNotNull( client.Inventory.Definitions );
Assert.AreNotEqual( 0, client.Inventory.Definitions.Length );
@ -58,6 +70,12 @@ public void InventoryDefinitionIngredients()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsNotNull( client.Inventory.Definitions );
Assert.AreNotEqual( 0, client.Inventory.Definitions.Length );
@ -80,6 +98,12 @@ public void InventoryItemList()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
bool CallbackCalled = false;
// OnUpdate hsould be called when we receive a list of our items
@ -146,6 +170,12 @@ public void Deserialize()
{
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsTrue( client.IsValid );
Assert.IsNotNull(client.Inventory.Definitions);
Assert.AreNotEqual(0, client.Inventory.Definitions.Length);
@ -202,6 +232,12 @@ public void PurchaseItems()
{
using (var client = new Facepunch.Steamworks.Client(252490))
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsNotNull(client.Inventory.Definitions);
Assert.AreNotEqual(0, client.Inventory.Definitions.Length);
@ -235,6 +271,12 @@ public void ListPrices()
{
using (var client = new Facepunch.Steamworks.Client(252490))
{
while ( client.Inventory.Definitions == null )
{
client.Update();
System.Threading.Thread.Sleep( 10 );
}
Assert.IsNotNull(client.Inventory.Definitions);
Assert.AreNotEqual(0, client.Inventory.Definitions.Length);

View File

@ -171,6 +171,11 @@ public void AddScoresCallback()
Thread.Sleep(10);
client.Update();
if ( board.IsError )
{
throw new Exception( "Board is Error" );
}
if (time.Elapsed.TotalSeconds > 10)
{
throw new Exception("board.IsValid took too long");

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -12,6 +13,23 @@ namespace Facepunch.Steamworks.Test
[DeploymentItem( "steam_api64.dll" )]
public partial class ServerList
{
[TestMethod]
public void IpAddressConversions()
{
var ipstr = "185.38.150.40";
var ip = IPAddress.Parse( ipstr );
var ip_int = Facepunch.Steamworks.Utility.IpToInt32( ip );
var ip_back = Facepunch.Steamworks.Utility.Int32ToIp( ip_int );
Console.WriteLine( "ipstr: " + ipstr );
Console.WriteLine( "ip: " + ip );
Console.WriteLine( "ip int: " + ip_int );
Console.WriteLine( "ip_back: " + ip_back );
}
[TestMethod]
public void InternetList()
{
@ -27,7 +45,7 @@ public void InternetList()
for ( int i = 0; i < 1000; i++ )
{
client.Update();
System.Threading.Thread.Sleep( 5 );
System.Threading.Thread.Sleep( 100 );
foreach ( var s in query.Responded )
{
@ -388,9 +406,10 @@ public void Rules()
filter.Add( "gamedir", "rust" );
filter.Add( "secure", "1" );
filter.Add( "addr", "185.97.254.146" );
using ( var query = client.ServerList.Internet( filter ) )
{
for ( int i = 0; i < 1000; i++ )
{
GC.Collect();
@ -398,8 +417,8 @@ public void Rules()
GC.Collect();
System.Threading.Thread.Sleep( 10 );
if ( query.Responded.Count > 20 )
break;
// if ( query.Responded.Count > 20 )
// break;
if ( query.Finished )
break;
@ -407,20 +426,18 @@ public void Rules()
query.Dispose();
foreach ( var server in query.Responded.Take( 20 ) )
var servers = query.Responded.Take( 100 );
foreach ( var server in servers )
{
GC.Collect();
server.FetchRules();
GC.Collect();
int i = 0;
while ( !server.HasRules )
{
i++;
GC.Collect();
client.Update();
GC.Collect();
System.Threading.Thread.Sleep( 2 );
System.Threading.Thread.Sleep( 10 );
if ( i > 100 )
break;
@ -428,11 +445,20 @@ public void Rules()
if ( server.HasRules )
{
Console.WriteLine( "" );
Console.WriteLine( "" );
Console.WriteLine( server.Address );
Console.WriteLine( "" );
foreach ( var rule in server.Rules )
{
Console.WriteLine( rule.Key + " = " + rule.Value );
}
}
else
{
Console.WriteLine( "SERVER HAS NO RULES :(" );
}
}

View File

@ -25,6 +25,8 @@ public class BaseSteamworks : IDisposable
internal Interop.NativeInterface native;
private List<SteamNative.CallbackHandle> CallbackHandles = new List<SteamNative.CallbackHandle>();
private List<SteamNative.CallResult> CallResults = new List<SteamNative.CallResult>();
protected bool disposed = false;
protected BaseSteamworks( uint appId )
@ -38,14 +40,29 @@ protected BaseSteamworks( uint appId )
System.Environment.SetEnvironmentVariable("SteamGameId", AppId.ToString());
}
~BaseSteamworks()
{
Dispose();
}
public virtual void Dispose()
{
if ( disposed ) return;
Callbacks.Clear();
foreach ( var h in CallbackHandles )
{
h.Dispose();
}
CallbackHandles.Clear();
foreach ( var h in CallResults )
{
h.Dispose();
}
CallResults.Clear();
if ( Workshop != null )
{
Workshop.Dispose();
@ -72,6 +89,7 @@ public virtual void Dispose()
System.Environment.SetEnvironmentVariable("SteamAppId", null );
System.Environment.SetEnvironmentVariable("SteamGameId", null );
disposed = true;
}
protected void SetupCommonInterfaces()
@ -98,10 +116,18 @@ internal void RegisterCallbackHandle( SteamNative.CallbackHandle handle )
CallbackHandles.Add( handle );
}
internal void RegisterCallResult( SteamNative.CallResult handle )
{
CallResults.Add( handle );
}
internal void UnregisterCallResult( SteamNative.CallResult handle )
{
CallResults.Remove( handle );
}
public virtual void Update()
{
Inventory.Update();
Networking.Update();
RunUpdateCallbacks();
@ -114,6 +140,18 @@ public void RunUpdateCallbacks()
{
if ( OnUpdate != null )
OnUpdate();
for( int i=0; i < CallResults.Count; i++ )
{
CallResults[i].Try();
}
//
// The SourceServerQuery's happen in another thread, so we
// query them to see if they're finished, and if so post a callback
// in our main thread. This will all suck less once we have async.
//
Facepunch.Steamworks.SourceServerQuery.Cycle();
}
/// <summary>
@ -135,5 +173,47 @@ public void UpdateWhile( Func<bool> func )
#endif
}
}
/// <summary>
/// Debug function, called for every callback. Only really used to confirm that callbacks are working properly.
/// </summary>
public Action<object> OnAnyCallback;
Dictionary<Type, List<Action<object>>> Callbacks = new Dictionary<Type, List<Action<object>>>();
internal List<Action<object>> CallbackList( Type T )
{
List<Action<object>> list = null;
if ( !Callbacks.TryGetValue( T, out list ) )
{
list = new List<Action<object>>();
Callbacks[T] = list;
}
return list;
}
internal void OnCallback<T>( T data )
{
var list = CallbackList( typeof( T ) );
foreach ( var i in list )
{
i( data );
}
if ( OnAnyCallback != null )
{
OnAnyCallback.Invoke( data );
}
}
internal void RegisterCallback<T>( Action<T> func )
{
var list = CallbackList( typeof( T ) );
list.Add( ( o ) => func( (T) o ) );
}
}
}

View File

@ -84,6 +84,12 @@ public Client( uint appId ) : base( appId )
return;
}
//
// Register Callbacks
//
SteamNative.Callbacks.RegisterCallbacks( this );
//
// Setup interfaces that client and server both have
//
@ -115,20 +121,25 @@ public Client( uint appId ) : base( appId )
BetaName = native.apps.GetCurrentBetaName();
OwnerSteamId = native.apps.GetAppOwner();
var appInstallDir = native.apps.GetAppInstallDir(AppId);
if (!String.IsNullOrEmpty(appInstallDir) && Directory.Exists(appInstallDir))
InstallFolder = new DirectoryInfo(appInstallDir);
BuildId = native.apps.GetAppBuildId();
CurrentLanguage = native.apps.GetCurrentGameLanguage();
AvailableLanguages = native.apps.GetAvailableGameLanguages().Split( new[] {';'}, StringSplitOptions.RemoveEmptyEntries ); // TODO: Assumed colon separated
//
// Run update, first call does some initialization
//
Update();
}
~Client()
{
Dispose();
}
/// <summary>
/// Should be called at least once every frame
/// </summary>
@ -139,6 +150,7 @@ public override void Update()
RunCallbacks();
Voice.Update();
Friends.Cycle();
base.Update();
}
@ -156,6 +168,8 @@ public void RunCallbacks()
/// </summary>
public override void Dispose()
{
if ( disposed ) return;
if ( Voice != null )
{
Voice = null;

View File

@ -22,8 +22,8 @@ internal Achievements( Client c )
client = c;
All = new Achievement[0];
SteamNative.UserStatsReceived_t.RegisterCallback( c, UserStatsReceived );
SteamNative.UserStatsStored_t.RegisterCallback( c, UserStatsStored );
c.RegisterCallback<UserStatsReceived_t>( UserStatsReceived );
c.RegisterCallback<UserStatsStored_t>( UserStatsStored );
Refresh();
}
@ -100,9 +100,8 @@ public bool Reset( string identifier )
return client.native.userstats.ClearAchievement( identifier );
}
private void UserStatsReceived( UserStatsReceived_t stats, bool isError )
private void UserStatsReceived( UserStatsReceived_t stats )
{
if ( isError ) return;
if ( stats.GameID != client.AppId ) return;
Refresh();
@ -110,9 +109,8 @@ private void UserStatsReceived( UserStatsReceived_t stats, bool isError )
OnUpdated?.Invoke();
}
private void UserStatsStored( UserStatsStored_t stats, bool isError )
private void UserStatsStored( UserStatsStored_t stats )
{
if ( isError ) return;
if ( stats.GameID != client.AppId ) return;
Refresh();

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SteamNative;
namespace Facepunch.Steamworks
{
@ -12,6 +13,23 @@ public class App : IDisposable
internal App( Client c )
{
client = c;
client.RegisterCallback<SteamNative.DlcInstalled_t>( DlcInstalled );
}
public delegate void DlcInstalledDelegate( uint appid );
/// <summary>
/// Triggered after the current user gains ownership of DLC and that DLC is installed.
/// </summary>
public event DlcInstalledDelegate OnDlcInstalled;
private void DlcInstalled( DlcInstalled_t data )
{
if ( OnDlcInstalled != null )
{
OnDlcInstalled( data.AppID );
}
}
public void Dispose()
@ -72,5 +90,42 @@ public bool IsInstalled(uint appId)
{
return client.native.apps.BIsAppInstalled(appId);
}
/// <summary>
/// Returns true if specified app is installed.
/// </summary>
public bool IsDlcInstalled( uint appId )
{
return client.native.apps.BIsDlcInstalled( appId );
}
/// <summary>
/// Returns the appid's name
/// Returns error if the current app Id does not have permission to use this interface
/// </summary>
public string GetName( uint appId )
{
var str = client.native.applist.GetAppName( appId );
if ( str == null ) return "error";
return str;
}
/// <summary>
/// Returns the app's install folder
/// Returns error if the current app Id does not have permission to use this interface
/// </summary>
public string GetInstallFolder( uint appId )
{
return client.native.applist.GetAppInstallDir( appId );
}
/// <summary>
/// Returns the app's current build id
/// Returns 0 if the current app Id does not have permission to use this interface
/// </summary>
public int GetBuildId( uint appId )
{
return client.native.applist.GetAppBuildId( appId );
}
}
}

View File

@ -22,130 +22,6 @@ public Friends Friends
}
}
public class SteamFriend
{
/// <summary>
/// Steam Id
/// </summary>
public ulong Id { get; internal set; }
/// <summary>
/// Return true if blocked
/// </summary>
public bool IsBlocked => relationship == FriendRelationship.Blocked;
/// <summary>
/// Return true if is a friend. Returns false if blocked, request etc.
/// </summary>
public bool IsFriend => relationship == FriendRelationship.Friend;
/// <summary>
/// Their current display name
/// </summary>
public string Name;
/// <summary>
/// Returns true if this friend is online
/// </summary>
public bool IsOnline => personaState != PersonaState.Offline;
/// <summary>
/// Returns true if this friend is marked as away
/// </summary>
public bool IsAway => personaState == PersonaState.Away;
/// <summary>
/// Returns true if this friend is marked as busy
/// </summary>
public bool IsBusy => personaState == PersonaState.Busy;
/// <summary>
/// Returns true if this friend is marked as snoozing
/// </summary>
public bool IsSnoozing => personaState == PersonaState.Snooze;
/// <summary>
/// Returns true if this friend is online and playing this game
/// </summary>
public bool IsPlayingThisGame => CurrentAppId == Client.AppId;
/// <summary>
/// Returns true if this friend is online and playing this game
/// </summary>
public bool IsPlaying => CurrentAppId != 0;
/// <summary>
/// The AppId this guy is playing
/// </summary>
public ulong CurrentAppId { get; internal set; }
public uint ServerIp { get; internal set; }
public int ServerGamePort { get; internal set; }
public int ServerQueryPort { get; internal set; }
public ulong ServerLobbyId { get; internal set; }
internal Client Client { get; set; }
private PersonaState personaState;
private FriendRelationship relationship;
/// <summary>
/// Returns null if the value doesn't exist
/// </summary>
public string GetRichPresence( string key )
{
var val = Client.native.friends.GetFriendRichPresence( Id, key );
if ( string.IsNullOrEmpty( val ) ) return null;
return val;
}
/// <summary>
/// Update this friend, request the latest data from Steam's servers
/// </summary>
public void Refresh()
{
Name = Client.native.friends.GetFriendPersonaName( Id );
relationship = Client.native.friends.GetFriendRelationship( Id );
personaState = Client.native.friends.GetFriendPersonaState( Id );
CurrentAppId = 0;
ServerIp = 0;
ServerGamePort = 0;
ServerQueryPort = 0;
ServerLobbyId = 0;
var gameInfo = new SteamNative.FriendGameInfo_t();
if ( Client.native.friends.GetFriendGamePlayed( Id, ref gameInfo ) && gameInfo.GameID > 0 )
{
CurrentAppId = gameInfo.GameID;
ServerIp = gameInfo.GameIP;
ServerGamePort = gameInfo.GamePort;
ServerQueryPort = gameInfo.QueryPort;
ServerLobbyId = gameInfo.SteamIDLobby;
}
Client.native.friends.RequestFriendRichPresence( Id );
}
/// <summary>
/// This will return null if you don't have the target user's avatar in your cache.
/// Which usually happens for people not on your friends list.
/// </summary>
public Image GetAvatar( Friends.AvatarSize size )
{
return Client.Friends.GetCachedAvatar( size, Id );
}
/// <summary>
/// Invite this friend to the game that we are playing
/// </summary>
public bool InviteToGame(string Text)
{
return Client.native.friends.InviteUserToGame(Id, Text);
}
}
/// <summary>
/// Handles most interactions with people in Steam, not just friends as the name would suggest.
/// </summary>
@ -158,12 +34,82 @@ public bool InviteToGame(string Text)
public class Friends
{
internal Client client;
private byte[] buffer = new byte[1024 * 128];
internal Friends( Client c )
{
client = c;
SteamNative.PersonaStateChange_t.RegisterCallback( client, OnPersonaStateChange );
client.RegisterCallback<AvatarImageLoaded_t>( OnAvatarImageLoaded );
client.RegisterCallback<PersonaStateChange_t>( OnPersonaStateChange );
client.RegisterCallback<GameRichPresenceJoinRequested_t>( OnGameJoinRequested );
client.RegisterCallback<GameConnectedFriendChatMsg_t>( OnFriendChatMessage );
}
public delegate void ChatMessageDelegate( SteamFriend friend, string type, string message );
/// <summary>
/// Called when chat message has been received from a friend. You'll need to turn on
/// ListenForFriendsMessages to recieve this.
/// </summary>
public event ChatMessageDelegate OnChatMessage;
private unsafe void OnFriendChatMessage( GameConnectedFriendChatMsg_t data )
{
if ( OnChatMessage == null ) return;
var friend = Get( data.SteamIDUser );
var type = ChatEntryType.ChatMsg;
fixed ( byte* ptr = buffer )
{
var len = client.native.friends.GetFriendMessage( data.SteamIDUser, data.MessageID, (IntPtr)ptr, buffer.Length, out type );
if ( len == 0 && type == ChatEntryType.Invalid )
return;
var typeName = type.ToString();
var message = Encoding.UTF8.GetString( buffer, 0, len );
OnChatMessage( friend, typeName, message );
}
}
private bool _listenForFriendsMessages;
/// <summary>
/// Listens for Steam friends chat messages.
/// You can then show these chats inline in the game. For example with a Blizzard style chat message system or the chat system in Dota 2.
/// After enabling this you will receive callbacks when ever the user receives a chat message.
/// </summary>
public bool ListenForFriendsMessages
{
get
{
return _listenForFriendsMessages;
}
set
{
_listenForFriendsMessages = value;
client.native.friends.SetListenForFriendsMessages( value );
}
}
public delegate void JoinRequestedDelegate( SteamFriend friend, string connect );
//
// Called when a friend has invited you to their game (using InviteToGame)
//
public event JoinRequestedDelegate OnInvitedToGame;
private void OnGameJoinRequested( GameRichPresenceJoinRequested_t data )
{
if ( OnInvitedToGame != null )
{
OnInvitedToGame( Get( data.SteamIDFriend ), data.Connect );
}
}
/// <summary>
@ -292,21 +238,25 @@ public Image GetCachedAvatar( AvatarSize size, ulong steamid )
break;
}
if ( imageid == 1 ) return null; // Placeholder large
if ( imageid == 2 ) return null; // Placeholder medium
if ( imageid == 3 ) return null; // Placeholder small
var img = new Image()
{
Id = imageid
};
if (imageid != 0 && img.TryLoad(client.native.utils))
return img;
if ( !img.TryLoad( client.native.utils ) )
return null;
return null;
return img;
}
/// <summary>
/// Callback will be called when the avatar is ready. If we fail to get an
/// avatar, it'll be called with a null Image.
/// avatar, might be called with a null Image.
/// </summary>
public void GetAvatar( AvatarSize size, ulong steamid, Action<Image> callback )
{
@ -329,23 +279,27 @@ public void GetAvatar( AvatarSize size, ulong steamid, Action<Image> callback )
PersonaCallbacks.Add( new PersonaCallback
{
SteamId = steamid,
Callback = () =>
{
callback( GetCachedAvatar(size, steamid) );
}
Size = size,
Callback = callback,
Time = DateTime.Now
});
}
private class PersonaCallback
{
public ulong SteamId;
public Action Callback;
public AvatarSize Size;
public Action<Image> Callback;
public DateTime Time;
}
List<PersonaCallback> PersonaCallbacks = new List<PersonaCallback>();
public SteamFriend Get( ulong steamid )
{
var friend = All.Where( x => x.Id == steamid ).FirstOrDefault();
if ( friend != null ) return friend;
var f = new SteamFriend()
{
Id = steamid,
@ -357,9 +311,72 @@ public SteamFriend Get( ulong steamid )
return f;
}
private void OnPersonaStateChange( PersonaStateChange_t data, bool error )
internal void Cycle()
{
if ( PersonaCallbacks.Count == 0 ) return;
var timeOut = DateTime.Now.AddSeconds( -10 );
for ( int i = PersonaCallbacks.Count-1; i >= 0; i-- )
{
var cb = PersonaCallbacks[i];
// Timeout
if ( cb.Time < timeOut )
{
if ( cb.Callback != null )
{
cb.Callback( null );
}
PersonaCallbacks.Remove( cb );
continue;
}
}
}
private void OnPersonaStateChange( PersonaStateChange_t data )
{
// k_EPersonaChangeAvatar
if ( (data.ChangeFlags & 0x0040) == 0x0040 )
{
LoadAvatarForSteamId( data.SteamID );
}
//
// Find and refresh this friend's status
//
foreach ( var friend in All )
{
if ( friend.Id != data.SteamID ) continue;
friend.Refresh();
}
}
void LoadAvatarForSteamId( ulong Steamid )
{
for ( int i = PersonaCallbacks.Count - 1; i >= 0; i-- )
{
var cb = PersonaCallbacks[i];
if ( cb.SteamId != Steamid ) continue;
var image = GetCachedAvatar( cb.Size, cb.SteamId );
if ( image == null ) continue;
PersonaCallbacks.Remove( cb );
if ( cb.Callback != null )
{
cb.Callback( image );
}
}
}
private void OnAvatarImageLoaded( AvatarImageLoaded_t data )
{
LoadAvatarForSteamId( data.SteamID );
}
}

View File

@ -29,7 +29,7 @@ unsafe internal bool TryLoad( SteamNative.SteamUtils utils )
if ( utils.GetImageSize( Id, out width, out height ) == false )
{
IsError = true;
return true;
return false;
}
var buffer = new byte[ width * height * 4 ];
@ -39,7 +39,7 @@ unsafe internal bool TryLoad( SteamNative.SteamUtils utils )
if ( utils.GetImageRGBA( Id, (IntPtr) ptr, buffer.Length ) == false )
{
IsError = true;
return true;
return false;
}
}

View File

@ -107,6 +107,9 @@ private bool DeferOnCreated( Action onValid, FailureCallback onFailure = null )
internal void OnBoardCreated( LeaderboardFindResult_t result, bool error )
{
Console.WriteLine( $"result.LeaderboardFound: {result.LeaderboardFound}" );
Console.WriteLine( $"result.SteamLeaderboard: {result.SteamLeaderboard}" );
if ( error || ( result.LeaderboardFound == 0 ) )
{
IsError = true;
@ -234,7 +237,7 @@ public bool AttachRemoteFile( RemoteFile file, AttachRemoteFileCallback onSucces
}
} );
return handle.CallResultHandle != 0;
return handle.IsValid;
}
file.Share( () =>

View File

@ -0,0 +1,112 @@
using System.Collections.Generic;
namespace Facepunch.Steamworks
{
public partial class Lobby
{
/// <summary>
/// Class to hold global lobby data. This is stuff like maps/modes/etc. Data set here can be filtered by LobbyList.
/// </summary>
public class LobbyData
{
internal Client client;
internal ulong lobby;
internal Dictionary<string, string> data;
public LobbyData( Client c, ulong l )
{
client = c;
lobby = l;
data = new Dictionary<string, string>();
}
/// <summary>
/// Get the lobby value for the specific key
/// </summary>
/// <param name="k">The key to find</param>
/// <returns>The value at key</returns>
public string GetData( string k )
{
if ( data.ContainsKey( k ) )
{
return data[k];
}
return "ERROR: key not found";
}
/// <summary>
/// Get a list of all the data in the Lobby
/// </summary>
/// <returns>Dictionary of all the key/value pairs in the data</returns>
public Dictionary<string, string> GetAllData()
{
Dictionary<string, string> returnData = new Dictionary<string, string>();
foreach ( KeyValuePair<string, string> item in data )
{
returnData.Add( item.Key, item.Value );
}
return returnData;
}
/// <summary>
/// Set the value for specified Key. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
/// </summary>
/// <param name="k">The key to set the value for</param>
/// <param name="v">The value of the Key</param>
/// <returns>True if data successfully set</returns>
public bool SetData( string k, string v )
{
if ( data.ContainsKey( k ) )
{
if ( data[k] == v ) { return true; }
if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) )
{
data[k] = v;
return true;
}
}
else
{
if ( client.native.matchmaking.SetLobbyData( lobby, k, v ) )
{
data.Add( k, v );
return true;
}
}
return false;
}
/// <summary>
/// Remove the key from the LobbyData. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
/// </summary>
/// <param name="k">The key to remove</param>
/// <returns>True if Key successfully removed</returns>
public bool RemoveData( string k )
{
if ( data.ContainsKey( k ) )
{
if ( client.native.matchmaking.DeleteLobbyData( lobby, k ) )
{
data.Remove( k );
return true;
}
}
return false;
}
}
/*not implemented
//set the game server of the lobby
client.native.matchmaking.GetLobbyGameServer;
client.native.matchmaking.SetLobbyGameServer;
//used with game server stuff
SteamNative.LobbyGameCreated_t
*/
}
}

View File

@ -13,35 +13,39 @@ public Lobby Lobby
{
get
{
if (_lobby == null)
_lobby = new Steamworks.Lobby(this);
if ( _lobby == null )
_lobby = new Steamworks.Lobby( this );
return _lobby;
}
}
}
public class Lobby : IDisposable
public partial class Lobby : IDisposable
{
//The type of lobby you are creating
public enum Type : int
{
Private = SteamNative.LobbyType.Private,
FriendsOnly = SteamNative.LobbyType.FriendsOnly,
Public = SteamNative.LobbyType.Public,
Public = SteamNative.LobbyType.Public,
Invisible = SteamNative.LobbyType.Invisible,
Error //happens if you try to get this when you aren't in a valid lobby
}
internal Client client;
public Lobby(Client c)
public Lobby( Client c )
{
client = c;
SteamNative.LobbyDataUpdate_t.RegisterCallback(client, OnLobbyDataUpdatedAPI);
SteamNative.LobbyChatMsg_t.RegisterCallback(client, OnLobbyChatMessageRecievedAPI);
SteamNative.LobbyChatUpdate_t.RegisterCallback(client, OnLobbyStateUpdatedAPI);
SteamNative.GameLobbyJoinRequested_t.RegisterCallback(client, OnLobbyJoinRequestedAPI);
SteamNative.LobbyInvite_t.RegisterCallback(client, OnUserInvitedToLobbyAPI);
SteamNative.PersonaStateChange_t.RegisterCallback(client, OnLobbyMemberPersonaChangeAPI);
// For backwards compatibility
OnLobbyJoinRequested = Join;
client.RegisterCallback<SteamNative.LobbyDataUpdate_t>( OnLobbyDataUpdatedAPI );
client.RegisterCallback<SteamNative.LobbyChatMsg_t>( OnLobbyChatMessageRecievedAPI );
client.RegisterCallback<SteamNative.LobbyChatUpdate_t>( OnLobbyStateUpdatedAPI );
client.RegisterCallback<SteamNative.GameLobbyJoinRequested_t>( OnLobbyJoinRequestedAPI );
client.RegisterCallback<SteamNative.LobbyInvite_t>( OnUserInvitedToLobbyAPI );
client.RegisterCallback<SteamNative.PersonaStateChange_t>( OnLobbyMemberPersonaChangeAPI );
}
/// <summary>
@ -63,23 +67,23 @@ public Lobby(Client c)
/// Join a Lobby through its LobbyID. OnLobbyJoined is called with the result of the Join attempt.
/// </summary>
/// <param name="lobbyID">CSteamID of lobby to join</param>
public void Join(ulong lobbyID)
public void Join( ulong lobbyID )
{
Leave();
client.native.matchmaking.JoinLobby(lobbyID, OnLobbyJoinedAPI);
client.native.matchmaking.JoinLobby( lobbyID, OnLobbyJoinedAPI );
}
void OnLobbyJoinedAPI(LobbyEnter_t callback, bool error)
void OnLobbyJoinedAPI( LobbyEnter_t callback, bool error )
{
if (error || (callback.EChatRoomEnterResponse != (uint)(SteamNative.ChatRoomEnterResponse.Success)))
if ( error || (callback.EChatRoomEnterResponse != (uint)(SteamNative.ChatRoomEnterResponse.Success)) )
{
if (OnLobbyJoined != null) { OnLobbyJoined(false); }
if ( OnLobbyJoined != null ) { OnLobbyJoined( false ); }
return;
}
CurrentLobby = callback.SteamIDLobby;
UpdateLobbyData();
if (OnLobbyJoined != null) { OnLobbyJoined(true); }
if ( OnLobbyJoined != null ) { OnLobbyJoined( true ); }
}
/// <summary>
@ -88,37 +92,37 @@ void OnLobbyJoinedAPI(LobbyEnter_t callback, bool error)
public Action<bool> OnLobbyJoined;
/// <summary>
/// Creates a lobby and returns the created lobby. You auto join the created lobby. The lobby is stored in Client.CurrentLobby if successful.
/// Creates a lobby and returns the created lobby. You auto join the created lobby. The lobby is stored in Client.Lobby.CurrentLobby if successful.
/// </summary>
/// <param name="lobbyType">The Lobby.Type of Lobby to be created</param>
/// <param name="maxMembers">The maximum amount of people you want to be able to be in this lobby, including yourself</param>
public void Create(Lobby.Type lobbyType, int maxMembers)
public void Create( Lobby.Type lobbyType, int maxMembers )
{
client.native.matchmaking.CreateLobby((SteamNative.LobbyType)lobbyType, maxMembers, OnLobbyCreatedAPI);
client.native.matchmaking.CreateLobby( (SteamNative.LobbyType)lobbyType, maxMembers, OnLobbyCreatedAPI );
createdLobbyType = lobbyType;
}
internal Type createdLobbyType;
internal void OnLobbyCreatedAPI(LobbyCreated_t callback, bool error)
internal void OnLobbyCreatedAPI( LobbyCreated_t callback, bool error )
{
//from SpaceWarClient.cpp 793
if (error || (callback.Result != Result.OK))
if ( error || (callback.Result != Result.OK) )
{
if ( OnLobbyCreated != null) { OnLobbyCreated(false); }
if ( OnLobbyCreated != null ) { OnLobbyCreated( false ); }
return;
}
//set owner specific properties
Owner = client.SteamId;
CurrentLobby = callback.SteamIDLobby;
CurrentLobbyData = new LobbyData(client, CurrentLobby);
CurrentLobbyData = new LobbyData( client, CurrentLobby );
Name = client.Username + "'s Lobby";
CurrentLobbyData.SetData("appid", client.AppId.ToString());
CurrentLobbyData.SetData( "appid", client.AppId.ToString() );
LobbyType = createdLobbyType;
CurrentLobbyData.SetData("lobbytype", LobbyType.ToString());
CurrentLobbyData.SetData( "lobbytype", LobbyType.ToString() );
Joinable = true;
if (OnLobbyCreated != null) { OnLobbyCreated(true); }
if ( OnLobbyCreated != null ) { OnLobbyCreated( true ); }
}
/// <summary>
@ -126,108 +130,13 @@ internal void OnLobbyCreatedAPI(LobbyCreated_t callback, bool error)
/// </summary>
public Action<bool> OnLobbyCreated;
/// <summary>
/// Class to hold global lobby data. This is stuff like maps/modes/etc. Data set here can be filtered by LobbyList.
/// </summary>
public class LobbyData
{
internal Client client;
internal ulong lobby;
internal Dictionary<string, string> data;
public LobbyData(Client c, ulong l)
{
client = c;
lobby = l;
data = new Dictionary<string, string>();
}
/// <summary>
/// Get the lobby value for the specific key
/// </summary>
/// <param name="k">The key to find</param>
/// <returns>The value at key</returns>
public string GetData(string k)
{
if (data.ContainsKey(k))
{
return data[k];
}
return "ERROR: key not found";
}
/// <summary>
/// Get a list of all the data in the Lobby
/// </summary>
/// <returns>Dictionary of all the key/value pairs in the data</returns>
public Dictionary<string,string> GetAllData()
{
Dictionary<string, string> returnData = new Dictionary<string, string>();
foreach(KeyValuePair<string, string> item in data)
{
returnData.Add(item.Key, item.Value);
}
return returnData;
}
/// <summary>
/// Set the value for specified Key. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
/// </summary>
/// <param name="k">The key to set the value for</param>
/// <param name="v">The value of the Key</param>
/// <returns>True if data successfully set</returns>
public bool SetData(string k, string v)
{
if (data.ContainsKey(k))
{
if (data[k] == v) { return true; }
if (client.native.matchmaking.SetLobbyData(lobby, k, v))
{
data[k] = v;
return true;
}
}
else
{
if (client.native.matchmaking.SetLobbyData(lobby, k, v))
{
data.Add(k, v);
return true;
}
}
return false;
}
/// <summary>
/// Remove the key from the LobbyData. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use.
/// </summary>
/// <param name="k">The key to remove</param>
/// <returns>True if Key successfully removed</returns>
public bool RemoveData(string k)
{
if (data.ContainsKey(k))
{
if (client.native.matchmaking.DeleteLobbyData(lobby, k))
{
data.Remove(k);
return true;
}
}
return false;
}
}
/// <summary>
/// Sets user data for the Lobby. Things like Character, Skin, Ready, etc. Can only set your own member data
/// </summary>
public void SetMemberData(string key, string value)
public void SetMemberData( string key, string value )
{
if(CurrentLobby == 0) { return; }
client.native.matchmaking.SetLobbyMemberData(CurrentLobby, key, value);
if ( CurrentLobby == 0 ) { return; }
client.native.matchmaking.SetLobbyMemberData( CurrentLobby, key, value );
}
/// <summary>
@ -236,23 +145,24 @@ public void SetMemberData(string key, string value)
/// <param name="steamID">ulong SteamID of the user you want to get data from</param>
/// <param name="key">String key of the type of data you want to get</param>
/// <returns></returns>
public string GetMemberData(ulong steamID, string key)
public string GetMemberData( ulong steamID, string key )
{
if (CurrentLobby == 0) { return "ERROR: NOT IN ANY LOBBY"; }
return client.native.matchmaking.GetLobbyMemberData(CurrentLobby, steamID, key);
if ( CurrentLobby == 0 ) { return "ERROR: NOT IN ANY LOBBY"; }
return client.native.matchmaking.GetLobbyMemberData( CurrentLobby, steamID, key );
}
internal void OnLobbyDataUpdatedAPI(LobbyDataUpdate_t callback, bool error)
internal void OnLobbyDataUpdatedAPI( LobbyDataUpdate_t callback )
{
if(error || (callback.SteamIDLobby != CurrentLobby)) { return; }
if(callback.SteamIDLobby == CurrentLobby) //actual lobby data was updated by owner
if ( callback.SteamIDLobby != CurrentLobby ) return;
if ( callback.SteamIDLobby == CurrentLobby ) //actual lobby data was updated by owner
{
UpdateLobbyData();
}
if(UserIsInCurrentLobby(callback.SteamIDMember)) //some member of this lobby updated their information
if ( UserIsInCurrentLobby( callback.SteamIDMember ) ) //some member of this lobby updated their information
{
if (OnLobbyMemberDataUpdated != null) { OnLobbyMemberDataUpdated(callback.SteamIDMember); }
if ( OnLobbyMemberDataUpdated != null ) { OnLobbyMemberDataUpdated( callback.SteamIDMember ); }
}
}
@ -261,17 +171,17 @@ internal void OnLobbyDataUpdatedAPI(LobbyDataUpdate_t callback, bool error)
/// </summary>
internal void UpdateLobbyData()
{
int dataCount = client.native.matchmaking.GetLobbyDataCount(CurrentLobby);
CurrentLobbyData = new LobbyData(client, CurrentLobby);
for (int i = 0; i < dataCount; i++)
int dataCount = client.native.matchmaking.GetLobbyDataCount( CurrentLobby );
CurrentLobbyData = new LobbyData( client, CurrentLobby );
for ( int i = 0; i < dataCount; i++ )
{
if (client.native.matchmaking.GetLobbyDataByIndex(CurrentLobby, i, out string key, out string value))
if ( client.native.matchmaking.GetLobbyDataByIndex( CurrentLobby, i, out string key, out string value ) )
{
CurrentLobbyData.SetData(key, value);
CurrentLobbyData.SetData( key, value );
}
}
if(OnLobbyDataUpdated != null) { OnLobbyDataUpdated(); }
if ( OnLobbyDataUpdated != null ) { OnLobbyDataUpdated(); }
}
/// <summary>
@ -289,12 +199,12 @@ public Type LobbyType
{
get
{
if (!IsValid) { return Type.Error; } //if we're currently in a valid server
if ( !IsValid ) { return Type.Error; } //if we're currently in a valid server
//we know that we've set the lobby type via the lobbydata in the creation function
//ps this is important because steam doesn't have an easy way to get lobby type (why idk)
string lobbyType = CurrentLobbyData.GetData("lobbytype");
switch (lobbyType)
string lobbyType = CurrentLobbyData.GetData( "lobbytype" );
switch ( lobbyType )
{
case "Private":
return Type.Private;
@ -310,32 +220,37 @@ public Type LobbyType
}
set
{
if(!IsValid) { return; }
if(client.native.matchmaking.SetLobbyType(CurrentLobby, (SteamNative.LobbyType)value))
if ( !IsValid ) { return; }
if ( client.native.matchmaking.SetLobbyType( CurrentLobby, (SteamNative.LobbyType)value ) )
{
CurrentLobbyData.SetData("lobbytype", value.ToString());
CurrentLobbyData.SetData( "lobbytype", value.ToString() );
}
}
}
private static byte[] chatMessageData = new byte[1024 * 4];
private unsafe void OnLobbyChatMessageRecievedAPI(LobbyChatMsg_t callback, bool error)
private unsafe void OnLobbyChatMessageRecievedAPI( LobbyChatMsg_t callback )
{
//from Client.Networking
if(error || callback.SteamIDLobby != CurrentLobby)
if ( callback.SteamIDLobby != CurrentLobby )
return;
SteamNative.CSteamID steamid = 1;
ChatEntryType chatEntryType; // "If set then this will just always return k_EChatEntryTypeChatMsg. This can usually just be set to NULL."
int readData = 0;
fixed (byte* p = chatMessageData)
fixed ( byte* p = chatMessageData )
{
readData = client.native.matchmaking.GetLobbyChatEntry(CurrentLobby, (int)callback.ChatID, out steamid, (IntPtr)p, chatMessageData.Length, out chatEntryType);
readData = client.native.matchmaking.GetLobbyChatEntry( CurrentLobby, (int)callback.ChatID, out steamid, (IntPtr)p, chatMessageData.Length, out chatEntryType );
}
OnChatMessageRecieved?.Invoke(steamid, chatMessageData, readData);
OnChatStringRecieved?.Invoke(steamid, Encoding.UTF8.GetString(chatMessageData));
OnChatMessageRecieved?.Invoke( steamid, chatMessageData, readData );
if ( readData > 0 )
{
OnChatStringRecieved?.Invoke( steamid, Encoding.UTF8.GetString( chatMessageData, 0, readData ) );
}
}
/// <summary>
@ -352,14 +267,14 @@ private unsafe void OnLobbyChatMessageRecievedAPI(LobbyChatMsg_t callback, bool
/// Broadcasts a chat message to the all the users in the lobby users in the lobby (including the local user) will receive a LobbyChatMsg_t callback.
/// </summary>
/// <returns>True if message successfully sent</returns>
public unsafe bool SendChatMessage(string message)
public unsafe bool SendChatMessage( string message )
{
var data = Encoding.UTF8.GetBytes(message);
fixed (byte* p = data)
var data = Encoding.UTF8.GetBytes( message );
fixed ( byte* p = data )
{
// pvMsgBody can be binary or text data, up to 4k
// if pvMsgBody is text, cubMsgBody should be strlen( text ) + 1, to include the null terminator
return client.native.matchmaking.SendLobbyChatMsg(CurrentLobby, (IntPtr)p, data.Length);
return client.native.matchmaking.SendLobbyChatMsg( CurrentLobby, (IntPtr)p, data.Length );
}
}
@ -375,16 +290,16 @@ public enum MemberStateChange
Banned = ChatMemberStateChange.Banned,
}
internal void OnLobbyStateUpdatedAPI(LobbyChatUpdate_t callback, bool error)
internal void OnLobbyStateUpdatedAPI( LobbyChatUpdate_t callback )
{
if (error || callback.SteamIDLobby != CurrentLobby)
if ( callback.SteamIDLobby != CurrentLobby )
return;
MemberStateChange change = (MemberStateChange)callback.GfChatMemberStateChange;
ulong initiator = callback.SteamIDMakingChange;
ulong affected = callback.SteamIDUserChanged;
OnLobbyStateChanged?.Invoke(change, initiator, affected);
OnLobbyStateChanged?.Invoke( change, initiator, affected );
}
/// <summary>
@ -401,13 +316,24 @@ public string Name
{
get
{
if (!IsValid) { return ""; }
return CurrentLobbyData.GetData("name");
if ( !IsValid ) { return ""; }
return CurrentLobbyData.GetData( "name" );
}
set
{
if (!IsValid) { return; }
CurrentLobbyData.SetData("name", value);
if ( !IsValid ) { return; }
CurrentLobbyData.SetData( "name", value );
}
}
/// <summary>
/// returns true if we're the current owner
/// </summary>
public bool IsOwner
{
get
{
return Owner == client.SteamId;
}
}
@ -418,16 +344,16 @@ public ulong Owner
{
get
{
if (IsValid)
if ( IsValid )
{
return client.native.matchmaking.GetLobbyOwner(CurrentLobby);
return client.native.matchmaking.GetLobbyOwner( CurrentLobby );
}
return 0;
}
set
{
if (Owner == value) return;
client.native.matchmaking.SetLobbyOwner(CurrentLobby, value);
if ( Owner == value ) return;
client.native.matchmaking.SetLobbyOwner( CurrentLobby, value );
}
}
@ -438,9 +364,9 @@ public bool Joinable
{
get
{
if (!IsValid) { return false; }
string joinable = CurrentLobbyData.GetData("joinable");
switch (joinable)
if ( !IsValid ) { return false; }
string joinable = CurrentLobbyData.GetData( "joinable" );
switch ( joinable )
{
case "true":
return true;
@ -452,10 +378,10 @@ public bool Joinable
}
set
{
if (!IsValid) { return; }
if (client.native.matchmaking.SetLobbyJoinable(CurrentLobby, value))
if ( !IsValid ) { return; }
if ( client.native.matchmaking.SetLobbyJoinable( CurrentLobby, value ) )
{
CurrentLobbyData.SetData("joinable", value.ToString());
CurrentLobbyData.SetData( "joinable", value.ToString() );
}
}
}
@ -467,13 +393,13 @@ public int MaxMembers
{
get
{
if (!IsValid) { return 0; } //0 is default, but value is inited when lobby is created.
return client.native.matchmaking.GetLobbyMemberLimit(CurrentLobby);
if ( !IsValid ) { return 0; } //0 is default, but value is inited when lobby is created.
return client.native.matchmaking.GetLobbyMemberLimit( CurrentLobby );
}
set
{
if (!IsValid) { return; }
client.native.matchmaking.SetLobbyMemberLimit(CurrentLobby, value);
if ( !IsValid ) { return; }
client.native.matchmaking.SetLobbyMemberLimit( CurrentLobby, value );
}
}
@ -482,7 +408,7 @@ public int MaxMembers
/// </summary>
public int NumMembers
{
get { return client.native.matchmaking.GetNumLobbyMembers(CurrentLobby);}
get { return client.native.matchmaking.GetNumLobbyMembers( CurrentLobby ); }
}
/// <summary>
@ -490,9 +416,9 @@ public int NumMembers
/// </summary>
public void Leave()
{
if (CurrentLobby != 0)
if ( CurrentLobby != 0 )
{
client.native.matchmaking.LeaveLobby(CurrentLobby);
client.native.matchmaking.LeaveLobby( CurrentLobby );
}
CurrentLobby = 0;
@ -513,9 +439,9 @@ public void Dispose()
public ulong[] GetMemberIDs()
{
ulong[] memIDs = new ulong[NumMembers];
for (int i = 0; i < NumMembers; i++)
for ( int i = 0; i < NumMembers; i++ )
{
memIDs[i] = client.native.matchmaking.GetLobbyMemberByIndex(CurrentLobby, i);
memIDs[i] = client.native.matchmaking.GetLobbyMemberByIndex( CurrentLobby, i );
}
return memIDs;
}
@ -525,14 +451,14 @@ public ulong[] GetMemberIDs()
/// </summary>
/// <param name="steamID">SteamID of the user to check for</param>
/// <returns></returns>
public bool UserIsInCurrentLobby(ulong steamID)
public bool UserIsInCurrentLobby( ulong steamID )
{
if ( CurrentLobby == 0 )
return false;
ulong[] mems = GetMemberIDs();
for (int i = 0; i < mems.Length; i++)
for ( int i = 0; i < mems.Length; i++ )
{
if ( mems[i] == steamID )
return true;
@ -545,39 +471,119 @@ public bool UserIsInCurrentLobby(ulong steamID)
/// Invites the specified user to the CurrentLobby the user is in.
/// </summary>
/// <param name="friendID">ulong ID of person to invite</param>
public bool InviteUserToLobby(ulong friendID)
public bool InviteUserToLobby( ulong friendID )
{
return client.native.matchmaking.InviteUserToLobby(CurrentLobby, friendID);
return client.native.matchmaking.InviteUserToLobby( CurrentLobby, friendID );
}
internal void OnUserInvitedToLobbyAPI(LobbyInvite_t callback, bool error)
internal void OnUserInvitedToLobbyAPI( LobbyInvite_t callback )
{
if (error || (callback.GameID != client.AppId)) { return; }
if (OnUserInvitedToLobby != null) { OnUserInvitedToLobby(callback.SteamIDLobby, callback.SteamIDUser); }
if ( callback.GameID != client.AppId ) return;
if ( OnUserInvitedToLobby != null ) { OnUserInvitedToLobby( callback.SteamIDLobby, callback.SteamIDUser ); }
}
/// <summary>
/// Activates the steam overlay to invite friends to the CurrentLobby the user is in.
/// </summary>
public void OpenFriendInviteOverlay()
{
client.native.friends.ActivateGameOverlayInviteDialog(CurrentLobby);
}
/// <summary>
/// Called when a user invites the current user to a lobby. The first parameter is the lobby the user was invited to, the second is the CSteamID of the person who invited this user
/// </summary>
public Action<ulong, ulong> OnUserInvitedToLobby;
/// <summary>
/// Called when a user accepts an invitation to a lobby while the game is running. The parameter is a lobby id.
/// </summary>
public Action<ulong> OnLobbyJoinRequested;
/// <summary>
/// Joins a lobby if a request was made to join the lobby through the friends list or an invite
/// </summary>
internal void OnLobbyJoinRequestedAPI(GameLobbyJoinRequested_t callback, bool error)
internal void OnLobbyJoinRequestedAPI( GameLobbyJoinRequested_t callback )
{
if (error) { return; }
Join(callback.SteamIDLobby);
if (OnLobbyJoinRequested != null) { OnLobbyJoinRequested(callback.SteamIDLobby); }
}
/// <summary>
/// Makes sure we send an update callback if a Lobby user updates their information
/// </summary>
internal void OnLobbyMemberPersonaChangeAPI(PersonaStateChange_t callback, bool error)
internal void OnLobbyMemberPersonaChangeAPI( PersonaStateChange_t callback )
{
if (error || !UserIsInCurrentLobby(callback.SteamID)) { return; }
if (OnLobbyMemberDataUpdated != null) { OnLobbyMemberDataUpdated(callback.SteamID); }
if ( !UserIsInCurrentLobby( callback.SteamID ) ) return;
if ( OnLobbyMemberDataUpdated != null ) { OnLobbyMemberDataUpdated( callback.SteamID ); }
}
/// <summary>
/// Sets the game server associated with the lobby.
/// This can only be set by the owner of the lobby.
/// Either the IP/Port or the Steam ID of the game server must be valid, depending on how you want the clients to be able to connect.
/// </summary>
public bool SetGameServer( System.Net.IPAddress ip, int port, ulong serverSteamId = 0 )
{
if ( !IsValid || !IsOwner ) return false;
var ipint = System.Net.IPAddress.NetworkToHostOrder( ip.Address );
client.native.matchmaking.SetLobbyGameServer( CurrentLobby, (uint)ipint, (ushort)port, serverSteamId );
return true;
}
/// <summary>
/// Gets the details of a game server set in a lobby.
/// </summary>
public System.Net.IPAddress GameServerIp
{
get
{
uint ip;
ushort port;
CSteamID steamid;
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) || ip == 0 )
return null;
return new System.Net.IPAddress( System.Net.IPAddress.HostToNetworkOrder( ip ) );
}
}
/// <summary>
/// Gets the details of a game server set in a lobby.
/// </summary>
public int GameServerPort
{
get
{
uint ip;
ushort port;
CSteamID steamid;
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) )
return 0;
return (int)port;
}
}
/// <summary>
/// Gets the details of a game server set in a lobby.
/// </summary>
public ulong GameServerSteamId
{
get
{
uint ip;
ushort port;
CSteamID steamid;
if ( !client.native.matchmaking.GetLobbyGameServer( CurrentLobby, out ip, out port, out steamid ) )
return 0;
return steamid;
}
}
/*not implemented

View File

@ -41,6 +41,8 @@ public void Refresh ( Filter filter = null)
{
filter = new Filter();
filter.StringFilters.Add("appid", client.AppId.ToString());
client.native.matchmaking.RequestLobbyList(OnLobbyList);
return;
}
client.native.matchmaking.AddRequestLobbyListDistanceFilter((SteamNative.LobbyDistanceFilter)filter.DistanceFilter);
@ -101,7 +103,7 @@ void OnLobbyList(LobbyMatchList_t callback, bool error)
{
//else we need to get the info for the missing lobby
client.native.matchmaking.RequestLobbyData(lobby);
SteamNative.LobbyDataUpdate_t.RegisterCallback(client, OnLobbyDataUpdated);
client.RegisterCallback<SteamNative.LobbyDataUpdate_t>( OnLobbyDataUpdated );
}
}
@ -121,7 +123,7 @@ void checkFinished()
Finished = false;
}
void OnLobbyDataUpdated(LobbyDataUpdate_t callback, bool error)
void OnLobbyDataUpdated(LobbyDataUpdate_t callback)
{
if (callback.Success == 1) //1 if success, 0 if failure
{

View File

@ -23,10 +23,10 @@ internal MicroTransactions( Client c )
{
client = c;
SteamNative.MicroTxnAuthorizationResponse_t.RegisterCallback( client, onMicroTxnAuthorizationResponse );
client.RegisterCallback<SteamNative.MicroTxnAuthorizationResponse_t>( onMicroTxnAuthorizationResponse );
}
private void onMicroTxnAuthorizationResponse( MicroTxnAuthorizationResponse_t arg1, bool arg2 )
private void onMicroTxnAuthorizationResponse( MicroTxnAuthorizationResponse_t arg1 )
{
if ( OnAuthorizationResponse != null )
{

View File

@ -86,7 +86,7 @@ internal static Server FromSteam( Client client, SteamNative.gameserveritem_t it
/// </summary>
public bool HasRules { get { return Rules != null && Rules.Count > 0; } }
internal Interop.ServerRules RulesRequest;
internal SourceServerQuery RulesRequest;
/// <summary>
/// Populates Rules for this server
@ -96,18 +96,23 @@ public void FetchRules()
if ( RulesRequest != null )
return;
Rules = new Dictionary<string, string>();
RulesRequest = new Interop.ServerRules( this, Address, QueryPort );
Rules = null;
RulesRequest = new SourceServerQuery( this, Address, QueryPort );
}
internal void OnServerRulesReceiveFinished( bool Success )
internal void OnServerRulesReceiveFinished( Dictionary<string, string> rules, bool Success )
{
RulesRequest.Dispose();
RulesRequest = null;
if ( Success )
{
Rules = rules;
}
if ( OnReceivedRules != null )
{
OnReceivedRules( Success );
}
}
internal const uint k_unFavoriteFlagNone = 0x00;

View File

@ -1,513 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
#if !NET_CORE
internal class SourceServerQuery :IDisposable
{
public class PlayersResponse
{
public short player_count;
public List<Player> players = new List<Player>();
public class Player
{
public String name { get; set; }
public int score { get; set; }
public float playtime { get; set; }
}
}
private IPEndPoint endPoint;
private Socket socket;
private UdpClient client;
// send & receive timeouts
private int send_timeout = 2500;
private int receive_timeout = 2500;
// raw response returned from the server
private byte[] raw_data;
private int offset = 0;
// constants
private readonly byte[] FFFFFFFF = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
public SourceServerQuery( String ip, int port )
{
this.endPoint = new IPEndPoint( IPAddress.Parse( ip ), port );
}
/// <summary>
/// Get a list of currently in-game clients on the specified gameserver.
/// <b>Please note:</b> the playtime is stored as a float in <i>seconds</i>, you might want to convert it.
///
/// See https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER for more Information
/// </summary>
/// <returns>A PLayersResponse Object containing the name, score and playtime of each player</returns>
public PlayersResponse GetPlayerList()
{
// open socket if not already open
this.GetSocket();
// we don't need the header, so set pointer to where the payload begins
this.offset = 5;
try
{
PlayersResponse pr = new PlayersResponse();
// since A2S_PLAYER requests require a valid challenge, get it first
byte[] challenge = this.GetChallenge(0x55, true);
byte[] request = new byte[challenge.Length + this.FFFFFFFF.Length + 1];
Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
request[this.FFFFFFFF.Length] = 0x55;
Array.Copy( challenge, 0, request, this.FFFFFFFF.Length + 1, challenge.Length );
this.socket.Send( request );
// MODIFIED BY ZACKBOE
// Increased byte size from 1024 in order to receive more player data
// Previously returned a socket exception at >~ 51 players.
this.raw_data = new byte[2048];
// END MODIFICATION
this.socket.Receive( this.raw_data );
byte player_count = this.ReadByte();
// fill up the list of players
for ( int i = 0; i < player_count; i++ )
{
this.ReadByte();
PlayersResponse.Player p = new PlayersResponse.Player();
p.name = this.ReadString();
p.score = this.ReadInt32();
p.playtime = this.ReadFloat();
pr.players.Add( p );
}
pr.player_count = player_count;
return pr;
}
catch ( SocketException )
{
return null;
}
}
/// <summary>
/// Get a list of all publically available CVars ("rules") from the server.
/// <b>Note:</b> Due to a bug in the Source Engine, it might happen that some CVars/values are cut off.
///
/// Example: mp_idlemaxtime = [nothing]
/// Only Valve can fix that.
/// </summary>
/// <returns>A RulesResponse Object containing a Name-Value pair of each CVar</returns>
public Dictionary<string, string> GetRules()
{
// open udpclient if not already open
this.GetClient();
try
{
var d = new Dictionary<string, string>();
// similar to A2S_PLAYER requests, A2S_RULES require a valid challenge
byte[] challenge = this.GetChallenge(0x56, false);
byte[] request = new byte[challenge.Length + this.FFFFFFFF.Length + 1];
Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
request[this.FFFFFFFF.Length] = 0x56;
Array.Copy( challenge, 0, request, this.FFFFFFFF.Length + 1, challenge.Length );
this.client.Send( request, request.Length );
//
// Since A2S_RULES responses might be split up into several packages/compressed, we have to do a special handling of them
//
int bytesRead;
// this will keep our assembled message
byte[] buffer = new byte[4096];
// send first request
this.raw_data = this.client.Receive( ref this.endPoint );
bytesRead = this.raw_data.Length;
// reset pointer
this.offset = 0;
int is_split = this.ReadInt32();
int requestid = this.ReadInt32();
this.offset = 4;
// response is split up into several packets
if ( this.PacketIsSplit( is_split ) )
{
bool isCompressed = false;
byte[] splitData;
int packetCount, packetNumber, requestId;
int packetsReceived = 1;
int packetChecksum = 0;
int packetSplit = 0;
short splitSize;
int uncompressedSize = 0;
List<byte[]> splitPackets = new List<byte[]>();
do
{
// unique request id
requestId = this.ReverseBytes( this.ReadInt32() );
isCompressed = this.PacketIsCompressed( requestId );
packetCount = this.ReadByte();
packetNumber = this.ReadByte() + 1;
// so we know how big our byte arrays have to be
splitSize = this.ReadInt16();
splitSize -= 4; // fix
if ( packetsReceived == 1 )
{
for ( int i = 0; i < packetCount; i++ )
{
splitPackets.Add( new byte[] { } );
}
}
// if the packets are compressed, get some data to decompress them
if ( isCompressed )
{
uncompressedSize = ReverseBytes( this.ReadInt32() );
packetChecksum = ReverseBytes( this.ReadInt32() );
}
// ommit header in first packet
if ( packetNumber == 1 ) this.ReadInt32();
splitData = new byte[splitSize];
splitPackets[packetNumber - 1] = this.ReadBytes();
// fixes a case where the returned package might still contain a character after the last \0 terminator (truncated name => value)
// please note: this therefore also removes the value of said variable, but atleast the program won't crash
if ( splitPackets[packetNumber - 1].Length - 1 > 0 && splitPackets[packetNumber - 1][splitPackets[packetNumber - 1].Length - 1] != 0x00 )
{
splitPackets[packetNumber - 1][splitPackets[packetNumber - 1].Length - 1] = 0x00;
}
// reset pointer again, so we can copy over the contents
this.offset = 0;
if ( packetsReceived < packetCount )
{
this.raw_data = this.client.Receive( ref this.endPoint );
bytesRead = this.raw_data.Length;
// continue with the next packets
packetSplit = this.ReadInt32();
packetsReceived++;
}
else
{
// all packets received
bytesRead = 0;
}
}
while ( packetsReceived <= packetCount && bytesRead > 0 && packetSplit == -2 );
// decompress
if ( isCompressed )
{
buffer = ReassemblePacket( splitPackets, true, uncompressedSize, packetChecksum );
}
else
{
buffer = ReassemblePacket( splitPackets, false, 0, 0 );
}
}
else
{
buffer = this.raw_data;
}
// move our final result over to handle it
this.raw_data = buffer;
// omitting header
this.offset += 1;
var count = this.ReadInt16();
for ( int i = 0; i < count; i++ )
{
var k = this.ReadString();
var v = this.ReadString();
if ( !d.ContainsKey( k ) )
d.Add( k, v );
}
return d;
}
catch ( SocketException e )
{
Console.WriteLine( e );
return null;
}
}
/// <summary>
/// Close all currently open socket/UdpClient connections
/// </summary>
public void Dispose()
{
if ( this.socket != null ) this.socket.Close();
if ( this.client != null ) this.client.Close();
}
/// <summary>
/// Open up a new Socket-based connection to a server, if not already open.
/// </summary>
private void GetSocket()
{
if ( this.socket == null )
{
this.socket = new Socket(
AddressFamily.InterNetwork,
SocketType.Dgram,
ProtocolType.Udp );
this.socket.SendTimeout = this.send_timeout;
this.socket.ReceiveTimeout = this.receive_timeout;
this.socket.Connect( this.endPoint );
}
}
/// <summary>
/// Create a new UdpClient connection to a server (mostly used for multi-packet answers)
/// </summary>
private void GetClient()
{
if ( this.client == null )
{
this.client = new UdpClient();
this.client.Connect( this.endPoint );
this.client.DontFragment = true;
this.client.Client.SendTimeout = this.send_timeout;
this.client.Client.ReceiveTimeout = this.receive_timeout;
}
}
/// <summary>
/// Reassmble a multi-packet response.
/// </summary>
/// <param name="splitPackets">The packets to assemble</param>
/// <param name="isCompressed">true: packets are compressed; false: not</param>
/// <param name="uncompressedSize">The size of the message after decompression (for comparison)</param>
/// <param name="packetChecksum">Validation of the result</param>
/// <returns>A byte-array containing all packets assembled together/decompressed.</returns>
private byte[] ReassemblePacket( List<byte[]> splitPackets, bool isCompressed, int uncompressedSize, int packetChecksum )
{
byte[] packetData, tmpData;
packetData = new byte[0];
foreach ( byte[] splitPacket in splitPackets )
{
if ( splitPacket == null )
{
throw new Exception();
}
tmpData = packetData;
packetData = new byte[tmpData.Length + splitPacket.Length];
MemoryStream memStream = new MemoryStream(packetData);
memStream.Write( tmpData, 0, tmpData.Length );
memStream.Write( splitPacket, 0, splitPacket.Length );
}
if ( isCompressed )
{
throw new System.NotImplementedException();
}
return packetData;
}
/// <summary>
/// Invert the Byte-order Mark of an value, used for compatibility between Little Large BOM
/// </summary>
/// <param name="value">The value to invert</param>
/// <returns>BOM-inversed value (if needed), otherwise the original value</returns>
private int ReverseBytes( int value )
{
byte[] bytes = BitConverter.GetBytes(value);
if ( BitConverter.IsLittleEndian )
{
Array.Reverse( bytes );
}
return BitConverter.ToInt32( bytes, 0 );
}
/// <summary>
/// Determine whetever or not a message is compressed.
/// Simply detects if the most significant bit is 1.
/// </summary>
/// <param name="value">The value to check</param>
/// <returns>true, if message is compressed, false otherwise</returns>
private bool PacketIsCompressed( int value )
{
return ( value & 0x8000 ) != 0;
}
/// <summary>
/// Determine whetever or not a message is split up.
/// </summary>
/// <param name="paket">The value to check</param>
/// <returns>true, if message is split up, false otherwise</returns>
private bool PacketIsSplit( int paket )
{
return ( paket == -2 );
}
/// <summary>
/// Request the 4-byte challenge id from the server, required for A2S_RULES and A2S_PLAYER.
/// </summary>
/// <param name="type">The type of message to request the challenge for (see constants)</param>
/// <param name="socket">Request method to use (performance reasons)</param>
/// <returns>A Byte Array (4-bytes) containing the challenge</returns>
private Byte[] GetChallenge( byte type, bool socket = true )
{
byte[] request = new byte[this.FFFFFFFF.Length + this.FFFFFFFF.Length + 1];
Array.Copy( this.FFFFFFFF, 0, request, 0, this.FFFFFFFF.Length );
request[FFFFFFFF.Length] = type;
Array.Copy( this.FFFFFFFF, 0, request, this.FFFFFFFF.Length + 1, this.FFFFFFFF.Length );
byte[] raw_response = new byte[24];
byte[] challenge = new byte[4];
// using sockets
if ( socket )
{
this.socket.Send( request );
this.socket.Receive( raw_response );
}
else
{
this.client.Send( request, request.Length );
raw_response = this.client.Receive( ref this.endPoint );
}
Array.Copy( raw_response, 5, challenge, 0, 4 ); // change this valve modifies the protocol!
return challenge;
}
/// <summary>
/// Read a single byte value from our raw data.
/// </summary>
/// <returns>A single Byte at the next Offset Address</returns>
private Byte ReadByte()
{
byte[] b = new byte[1];
Array.Copy( this.raw_data, this.offset, b, 0, 1 );
this.offset++;
return b[0];
}
/// <summary>
/// Read all remaining Bytes from our raw data.
/// Used for multi-packet responses.
/// </summary>
/// <returns>All remaining data</returns>
private Byte[] ReadBytes()
{
int size = (this.raw_data.Length - this.offset - 4);
if ( size < 1 ) return new Byte[] { };
byte[] b = new byte[size];
Array.Copy( this.raw_data, this.offset, b, 0, this.raw_data.Length - this.offset - 4 );
this.offset += ( this.raw_data.Length - this.offset - 4 );
return b;
}
/// <summary>
/// Read a 32-Bit Integer value from the next offset address.
/// </summary>
/// <returns>The Int32 Value found at the offset address</returns>
private Int32 ReadInt32()
{
byte[] b = new byte[4];
Array.Copy( this.raw_data, this.offset, b, 0, 4 );
this.offset += 4;
return BitConverter.ToInt32( b, 0 );
}
/// <summary>
/// Read a 16-Bit Integer (also called "short") value from the next offset address.
/// </summary>
/// <returns>The Int16 Value found at the offset address</returns>
private Int16 ReadInt16()
{
byte[] b = new byte[2];
Array.Copy( this.raw_data, this.offset, b, 0, 2 );
this.offset += 2;
return BitConverter.ToInt16( b, 0 );
}
/// <summary>
/// Read a Float value from the next offset address.
/// </summary>
/// <returns>The Float Value found at the offset address</returns>
private float ReadFloat()
{
byte[] b = new byte[4];
Array.Copy( this.raw_data, this.offset, b, 0, 4 );
this.offset += 4;
return BitConverter.ToSingle( b, 0 );
}
/// <summary>
/// Read a String until its end starting from the next offset address.
/// Reading stops once the method detects a 0x00 Character at the next position (\0 terminator)
/// </summary>
/// <returns>The String read</returns>
private String ReadString()
{
byte[] cache = new byte[1] { 0x01 };
String output = "";
while ( cache[0] != 0x00 )
{
if ( this.offset == this.raw_data.Length ) break; // fixes Valve's inability to code a proper query protocol
Array.Copy( this.raw_data, this.offset, cache, 0, 1 );
this.offset++;
if ( cache[0] != 0x00)
output += Encoding.UTF8.GetString( cache );
}
return output;
}
}
#endif

View File

@ -0,0 +1,136 @@
using SteamNative;
namespace Facepunch.Steamworks
{
public class SteamFriend
{
/// <summary>
/// Steam Id
/// </summary>
public ulong Id { get; internal set; }
/// <summary>
/// Return true if blocked
/// </summary>
public bool IsBlocked => relationship == FriendRelationship.Blocked;
/// <summary>
/// Return true if is a friend. Returns false if blocked, request etc.
/// </summary>
public bool IsFriend => relationship == FriendRelationship.Friend;
/// <summary>
/// Their current display name
/// </summary>
public string Name;
/// <summary>
/// Returns true if this friend is online
/// </summary>
public bool IsOnline => personaState != PersonaState.Offline;
/// <summary>
/// Returns true if this friend is marked as away
/// </summary>
public bool IsAway => personaState == PersonaState.Away;
/// <summary>
/// Returns true if this friend is marked as busy
/// </summary>
public bool IsBusy => personaState == PersonaState.Busy;
/// <summary>
/// Returns true if this friend is marked as snoozing
/// </summary>
public bool IsSnoozing => personaState == PersonaState.Snooze;
/// <summary>
/// Returns true if this friend is online and playing this game
/// </summary>
public bool IsPlayingThisGame => CurrentAppId == Client.AppId;
/// <summary>
/// Returns true if this friend is online and playing this game
/// </summary>
public bool IsPlaying => CurrentAppId != 0;
/// <summary>
/// The AppId this guy is playing
/// </summary>
public ulong CurrentAppId { get; internal set; }
public uint ServerIp { get; internal set; }
public int ServerGamePort { get; internal set; }
public int ServerQueryPort { get; internal set; }
public ulong ServerLobbyId { get; internal set; }
internal Client Client { get; set; }
private PersonaState personaState;
private FriendRelationship relationship;
/// <summary>
/// Returns null if the value doesn't exist
/// </summary>
public string GetRichPresence( string key )
{
var val = Client.native.friends.GetFriendRichPresence( Id, key );
if ( string.IsNullOrEmpty( val ) ) return null;
return val;
}
/// <summary>
/// Update this friend, request the latest data from Steam's servers
/// </summary>
public void Refresh()
{
Name = Client.native.friends.GetFriendPersonaName( Id );
relationship = Client.native.friends.GetFriendRelationship( Id );
personaState = Client.native.friends.GetFriendPersonaState( Id );
CurrentAppId = 0;
ServerIp = 0;
ServerGamePort = 0;
ServerQueryPort = 0;
ServerLobbyId = 0;
var gameInfo = new SteamNative.FriendGameInfo_t();
if ( Client.native.friends.GetFriendGamePlayed( Id, ref gameInfo ) && gameInfo.GameID > 0 )
{
CurrentAppId = gameInfo.GameID;
ServerIp = gameInfo.GameIP;
ServerGamePort = gameInfo.GamePort;
ServerQueryPort = gameInfo.QueryPort;
ServerLobbyId = gameInfo.SteamIDLobby;
}
Client.native.friends.RequestFriendRichPresence( Id );
}
/// <summary>
/// This will return null if you don't have the target user's avatar in your cache.
/// Which usually happens for people not on your friends list.
/// </summary>
public Image GetAvatar( Friends.AvatarSize size )
{
return Client.Friends.GetCachedAvatar( size, Id );
}
/// <summary>
/// Invite this friend to the game that we are playing
/// </summary>
public bool InviteToGame(string Text)
{
return Client.native.friends.InviteUserToGame(Id, Text);
}
/// <summary>
/// Sends a message to a Steam friend. Returns true if success
/// </summary>
public bool SendMessage( string message )
{
return Client.native.friends.ReplyToFriendMessage( Id, message );
}
}
}

View File

@ -18,11 +18,6 @@ public static void ForUnity( string platform )
//
if ( platform == "WindowsEditor" || platform == "WindowsPlayer" )
{
//
// 32bit windows unity uses a stdcall
//
if ( IntPtr.Size == 4 ) UseThisCall = false;
ForcePlatform( OperatingSystem.Windows, IntPtr.Size == 4 ? Architecture.x86 : Architecture.x64 );
}
@ -36,9 +31,14 @@ public static void ForUnity( string platform )
ForcePlatform( OperatingSystem.Linux, IntPtr.Size == 4 ? Architecture.x86 : Architecture.x64 );
}
IsUnity = true;
UseThisCall = true;
Console.WriteLine( "Facepunch.Steamworks Unity: " + platform );
Console.WriteLine( "Facepunch.Steamworks Os: " + SteamNative.Platform.Os );
Console.WriteLine( "Facepunch.Steamworks Arch: " + SteamNative.Platform.Arch );
}
/// <summary>
@ -50,7 +50,13 @@ public static void ForUnity( string platform )
/// for releasing his shit open source under the MIT license so we can all learn and iterate.
///
/// </summary>
public static bool UseThisCall { get; set; } = true;
public static bool UseThisCall { get; set; } = SteamNative.Platform.Os == OperatingSystem.Windows;
/// <summary>
/// Should be true if we're using Unity
/// </summary>
internal static bool IsUnity { get; set; }
/// <summary>

View File

@ -8,6 +8,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
@ -26,7 +27,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="*AssemblyInfo.cs"/>
<Compile Remove="*AssemblyInfo.cs" />
</ItemGroup>
<PropertyGroup>
@ -35,6 +36,18 @@
<PropertyGroup>
<FrameworkPathOverride Condition="'$(TargetFramework)' == 'net40'">C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client</FrameworkPathOverride>
<Authors>Garry Newman</Authors>
<PackageId>Facepunch.Steamworks</PackageId>
<PackageProjectUrl>https://github.com/Facepunch/Facepunch.Steamworks</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/Facepunch/Facepunch.Steamworks/blob/master/LICENSE</PackageLicenseUrl>
<PackageIconUrl>https://avatars2.githubusercontent.com/u/3371040</PackageIconUrl>
<PackageTags>facepunch;steam;unity;steamworks;valve</PackageTags>
<PackageVersion>0.7.5</PackageVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.0|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
</Project>

View File

@ -11,8 +11,16 @@ public partial class Inventory
/// <summary>
/// An item in your inventory.
/// </summary>
public struct Item : IEquatable<Item>
public class Item : IEquatable<Item>
{
internal Item( Inventory Inventory, ulong Id, int Quantity, int DefinitionId )
{
this.Inventory = Inventory;
this.Id = Id;
this.Quantity = Quantity;
this.DefinitionId = DefinitionId;
}
public struct Amount
{
public Item Item;
@ -24,12 +32,26 @@ public struct Amount
public int DefinitionId;
internal Inventory Inventory;
public Dictionary<string, string> Properties { get; internal set; }
private Definition _cachedDefinition;
/// <summary>
/// Careful, this might not be available. Especially on a game server.
/// </summary>
public Definition Definition;
public Definition Definition
{
get
{
if ( _cachedDefinition != null )
return _cachedDefinition;
_cachedDefinition = Inventory.FindDefinition( DefinitionId );
return _cachedDefinition;
}
}
public bool TradeLocked;
@ -64,41 +86,68 @@ public override int GetHashCode()
return !c1.Equals( c2 );
}
/// <summary>
/// Consumes items from a user's inventory. If the quantity of the given item goes to zero, it is permanently removed.
/// Once an item is removed it cannot be recovered.This is not for the faint of heart - if your game implements item removal at all,
/// a high-friction UI confirmation process is highly recommended.ConsumeItem can be restricted to certain item definitions or fully
/// blocked via the Steamworks website to minimize support/abuse issues such as the classic "my brother borrowed my laptop and deleted all of my rare items".
/// </summary>
public Result Consume( int amount = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !Inventory.inventory.ConsumeItem( ref resultHandle, Id, (uint)amount ) )
return null;
return new Result( Inventory, resultHandle, true );
}
/// <summary>
/// Split stack into two items
/// </summary>
public Result SplitStack( int quantity = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !Inventory.inventory.TransferItemQuantity( ref resultHandle, Id, (uint)quantity, ulong.MaxValue ) )
return null;
return new Result( Inventory, resultHandle, true );
}
SteamNative.SteamInventoryUpdateHandle_t updateHandle;
private void UpdatingProperties()
{
if (updateHandle != 0) return;
updateHandle = Definition.inventory.inventory.StartUpdateProperties();
updateHandle = Inventory.inventory.StartUpdateProperties();
}
public bool SetProperty( string name, string value )
{
UpdatingProperties();
Properties[name] = value.ToString();
return Definition.inventory.inventory.SetProperty(updateHandle, Id, name, value);
return Inventory.inventory.SetProperty(updateHandle, Id, name, value);
}
public bool SetProperty(string name, bool value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Definition.inventory.inventory.SetProperty0(updateHandle, Id, name, value);
return Inventory.inventory.SetProperty0(updateHandle, Id, name, value);
}
public bool SetProperty(string name, long value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Definition.inventory.inventory.SetProperty1(updateHandle, Id, name, value);
return Inventory.inventory.SetProperty1(updateHandle, Id, name, value);
}
public bool SetProperty(string name, float value)
{
UpdatingProperties();
Properties[name] = value.ToString();
return Definition.inventory.inventory.SetProperty2(updateHandle, Id, name, value);
return Inventory.inventory.SetProperty2(updateHandle, Id, name, value);
}
/// <summary>
@ -113,12 +162,12 @@ public bool SubmitProperties()
{
SteamNative.SteamInventoryResult_t result = -1;
if (!Definition.inventory.inventory.SubmitUpdateProperties(updateHandle, ref result))
if (!Inventory.inventory.SubmitUpdateProperties(updateHandle, ref result))
{
return false;
}
Definition.inventory.inventory.DestroyResult(result);
Inventory.inventory.DestroyResult(result);
return true;
}

View File

@ -14,7 +14,7 @@ public class Result : IDisposable
internal static Dictionary< int, Result > Pending;
internal Inventory inventory;
private SteamNative.SteamInventoryResult_t Handle { get; set; }
private SteamNative.SteamInventoryResult_t Handle { get; set; } = -1;
/// <summary>
/// Called when result is successfully returned
@ -139,9 +139,9 @@ internal void Fill()
}
}
internal void OnSteamResult( SteamInventoryResultReady_t data, bool error )
internal void OnSteamResult( SteamInventoryResultReady_t data )
{
var success = data.Result == SteamNative.Result.OK && !error;
var success = data.Result == SteamNative.Result.OK;
if ( success )
{
@ -169,8 +169,12 @@ internal unsafe byte[] Serialize()
public void Dispose()
{
inventory.inventory.DestroyResult( Handle );
Handle = -1;
if ( Handle != -1 && inventory != null )
{
inventory.inventory.DestroyResult( Handle );
Handle = -1;
}
inventory = null;
}
}
@ -190,15 +194,8 @@ internal Item ItemFrom( SteamInventoryResult_t handle, SteamItemDetails_t detail
}
}
var item = new Item()
{
Quantity = detail.Quantity,
Id = detail.ItemId,
DefinitionId = detail.Definition,
TradeLocked = ((int)detail.Flags & (int)SteamNative.SteamItemFlags.NoTrade) != 0,
Definition = FindDefinition(detail.Definition),
Properties = props
};
var item = new Item( this, detail.ItemId, detail.Quantity, detail.Definition );
item.Properties = props;
return item;
}

View File

@ -37,25 +37,28 @@ public partial class Inventory : IDisposable
internal SteamNative.SteamInventory inventory;
private Stopwatch fetchRetryTimer;
private bool IsServer { get; set; }
public event Action OnDefinitionsUpdated;
internal Inventory( BaseSteamworks steamworks, SteamNative.SteamInventory c, bool server )
{
IsServer = server;
inventory = c;
steamworks.RegisterCallback<SteamNative.SteamInventoryDefinitionUpdate_t>( onDefinitionsUpdated );
Result.Pending = new Dictionary<int, Result>();
inventory.LoadItemDefinitions();
FetchItemDefinitions();
LoadDefinitions();
UpdatePrices();
if ( !server )
{
SteamNative.SteamInventoryResultReady_t.RegisterCallback( steamworks, onResultReady );
SteamNative.SteamInventoryFullUpdate_t.RegisterCallback( steamworks, onFullUpdate );
steamworks.RegisterCallback<SteamNative.SteamInventoryResultReady_t>( onResultReady );
steamworks.RegisterCallback<SteamNative.SteamInventoryFullUpdate_t>( onFullUpdate );
//
// Get a list of our items immediately
@ -64,13 +67,41 @@ internal Inventory( BaseSteamworks steamworks, SteamNative.SteamInventory c, boo
}
}
/// <summary>
/// Should get called when the definitions get updated from Steam.
/// </summary>
private void onDefinitionsUpdated( SteamInventoryDefinitionUpdate_t obj )
{
LoadDefinitions();
UpdatePrices();
if ( OnDefinitionsUpdated != null )
{
OnDefinitionsUpdated.Invoke();
}
}
private bool LoadDefinitions()
{
var ids = inventory.GetItemDefinitionIDs();
if ( ids == null )
return false;
Definitions = ids.Select( x => CreateDefinition( x ) ).ToArray();
foreach ( var def in Definitions )
{
def.Link( Definitions );
}
return true;
}
/// <summary>
/// We've received a FULL update
/// </summary>
private void onFullUpdate( SteamInventoryFullUpdate_t data, bool error )
private void onFullUpdate( SteamInventoryFullUpdate_t data )
{
if ( error ) return;
var result = new Result( this, data.Handle, false );
result.Fill();
@ -80,15 +111,15 @@ private void onFullUpdate( SteamInventoryFullUpdate_t data, bool error )
/// <summary>
/// A generic result has returned.
/// </summary>
private void onResultReady( SteamInventoryResultReady_t data, bool error )
private void onResultReady( SteamInventoryResultReady_t data )
{
if ( Result.Pending.ContainsKey( data.Handle ) )
{
var result = Result.Pending[data.Handle];
result.OnSteamResult( data, error );
result.OnSteamResult( data );
if ( !error && data.Result == SteamNative.Result.OK )
if ( data.Result == SteamNative.Result.OK )
{
onResult( result, false );
}
@ -200,22 +231,12 @@ public Definition CreateDefinition( int id )
return new Definition( this, id );
}
internal void FetchItemDefinitions()
/// <summary>
/// Fetch item definitions in case new ones have been added since we've initialized
/// </summary>
public void FetchItemDefinitions()
{
//
// Make sure item definitions are loaded, because we're going to be using them.
//
var ids = inventory.GetItemDefinitionIDs();
if ( ids == null )
return;
Definitions = ids.Select( x => CreateDefinition( x ) ).ToArray();
foreach ( var def in Definitions )
{
def.Link( Definitions );
}
inventory.LoadItemDefinitions();
}
/// <summary>
@ -223,25 +244,7 @@ internal void FetchItemDefinitions()
/// </summary>
public void Update()
{
if ( Definitions == null )
{
//
// Don't try every frame, just try every 10 seconds.
//
{
if ( fetchRetryTimer != null && fetchRetryTimer.Elapsed.TotalSeconds < 10.0f )
return;
if ( fetchRetryTimer == null )
fetchRetryTimer = Stopwatch.StartNew();
fetchRetryTimer.Reset();
fetchRetryTimer.Start();
}
FetchItemDefinitions();
inventory.LoadItemDefinitions();
}
}
/// <summary>
@ -257,7 +260,10 @@ public IEnumerable<Definition> DefinitionsWithPrices
{
get
{
for( int i=0; i< Definitions.Length; i++ )
if ( Definitions == null )
yield break;
for ( int i=0; i< Definitions.Length; i++ )
{
if (Definitions[i].LocalPrice > 0)
yield return Definitions[i];
@ -283,14 +289,19 @@ public static float PriceCategoryToFloat( string price )
}
/// <summary>
/// You really need me to explain what this does?
/// Use your brains.
/// We might be better off using a dictionary for this, once there's 1000+ definitions
/// </summary>
public Definition FindDefinition( int DefinitionId )
{
if ( Definitions == null ) return null;
return Definitions.FirstOrDefault( x => x.Id == DefinitionId );
for( int i=0; i< Definitions.Length; i++ )
{
if ( Definitions[i].Id == DefinitionId )
return Definitions[i];
}
return null;
}
public unsafe Result Deserialize( byte[] data, int dataLength = -1 )
@ -362,11 +373,7 @@ public Result CraftItem( Item.Amount[] list, Definition target )
/// </summary>
public Result SplitStack( Item item, int quantity = 1 )
{
SteamNative.SteamInventoryResult_t resultHandle = -1;
if ( !inventory.TransferItemQuantity( ref resultHandle, item.Id, (uint)quantity, ulong.MaxValue ) )
return null;
return new Result( this, resultHandle, true );
return item.SplitStack( quantity );
}
/// <summary>

View File

@ -26,8 +26,8 @@ internal Networking( BaseSteamworks steamworks, SteamNative.SteamNetworking netw
{
this.networking = networking;
SteamNative.P2PSessionRequest_t.RegisterCallback( steamworks, onP2PConnectionRequest );
SteamNative.P2PSessionConnectFail_t.RegisterCallback( steamworks, onP2PConnectionFailed );
steamworks.RegisterCallback<SteamNative.P2PSessionRequest_t>( onP2PConnectionRequest );
steamworks.RegisterCallback<SteamNative.P2PSessionConnectFail_t>( onP2PConnectionFailed );
}
public void Dispose()
@ -80,7 +80,7 @@ public void SetListenChannel( int ChannelId, bool Listen )
}
}
private void onP2PConnectionRequest( SteamNative.P2PSessionRequest_t o, bool b )
private void onP2PConnectionRequest( SteamNative.P2PSessionRequest_t o )
{
if ( OnIncomingConnection != null )
{
@ -116,7 +116,7 @@ public enum SessionError : byte
Max = 5
};
private void onP2PConnectionFailed( SteamNative.P2PSessionConnectFail_t o, bool b )
private void onP2PConnectionFailed( SteamNative.P2PSessionConnectFail_t o )
{
if ( OnConnectionFailed != null )
{
@ -186,5 +186,14 @@ private unsafe bool ReadP2PPacket( int channel )
return true;
}
}
/// <summary>
/// This should be called when you're done communicating with a user, as this will free up all of the resources allocated for the connection under-the-hood.
/// If the remote user tries to send data to you again, a new onP2PConnectionRequest callback will be posted.
/// </summary>
public bool CloseSession( ulong steamId )
{
return networking.CloseP2PSessionWithUser( steamId );
}
}
}

View File

@ -48,19 +48,20 @@ internal static Item From( SteamNative.SteamUGCDetails_t details, Workshop works
return item;
}
public void Download( bool highPriority = true )
public bool Download( bool highPriority = true )
{
if ( Installed ) return;
if ( Downloading ) return;
if ( Installed ) return true;
if ( Downloading ) return true;
if ( !workshop.ugc.DownloadItem( Id, highPriority ) )
{
Console.WriteLine( "Download Failed" );
return;
return false;
}
workshop.OnFileDownloaded += OnFileDownloaded;
workshop.OnItemInstalled += OnItemInstalled;
return true;
}
public void Subscribe()

View File

@ -46,8 +46,8 @@ internal Workshop( BaseSteamworks steamworks, SteamNative.SteamUGC ugc, SteamNat
this.steamworks = steamworks;
this.remoteStorage = remoteStorage;
SteamNative.DownloadItemResult_t.RegisterCallback( steamworks, onDownloadResult );
SteamNative.ItemInstalled_t.RegisterCallback( steamworks, onItemInstalled );
steamworks.RegisterCallback<SteamNative.DownloadItemResult_t>( onDownloadResult );
steamworks.RegisterCallback<SteamNative.ItemInstalled_t>( onItemInstalled );
}
/// <summary>
@ -64,13 +64,13 @@ public void Dispose()
OnItemInstalled = null;
}
private void onItemInstalled( SteamNative.ItemInstalled_t obj, bool failed )
private void onItemInstalled( SteamNative.ItemInstalled_t obj )
{
if ( OnItemInstalled != null && obj.AppID == Client.Instance.AppId )
OnItemInstalled( obj.PublishedFileId );
}
private void onDownloadResult( SteamNative.DownloadItemResult_t obj, bool failed )
private void onDownloadResult( SteamNative.DownloadItemResult_t obj )
{
if ( OnFileDownloaded != null && obj.AppID == Client.Instance.AppId )
OnFileDownloaded( obj.PublishedFileId, (Callbacks.Result) obj.Result );

View File

@ -1,171 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
namespace Facepunch.Steamworks.Interop
{
class ServerRules : IDisposable
{
// Pins and pointers for the created vtable
private GCHandle vTablePin;
private IntPtr vTablePtr;
// Pins for the functions
private GCHandle RulesRespondPin;
private GCHandle FailedRespondPin;
private GCHandle CompletePin;
// The server that called us
private ServerList.Server Server;
public ServerRules( ServerList.Server server, IPAddress address, int queryPort )
{
Server = server;
//
// Create a fake VTable to pass to c++
//
InstallVTable();
//
// Ask Steam to get the server rules, respond to our fake vtable
//
Server.Client.native.servers.ServerRules( Utility.IpToInt32( address ), (ushort)queryPort, GetPtr() );
}
public void Dispose()
{
if ( vTablePtr != IntPtr.Zero )
{
Marshal.FreeHGlobal( vTablePtr );
vTablePtr = IntPtr.Zero;
}
if ( vTablePin.IsAllocated )
vTablePin.Free();
if ( RulesRespondPin.IsAllocated )
RulesRespondPin.Free();
if ( FailedRespondPin.IsAllocated )
FailedRespondPin.Free();
if ( CompletePin.IsAllocated )
CompletePin.Free();
}
void InstallVTable()
{
//
// Depending on platform, we either use ThisCall or stdcall.
// This is a bit of a fuckabout but you need to define Client.UseThisCall
//
if ( Config.UseThisCall )
{
ThisVTable.InternalRulesResponded da = ( _, k, v ) => InternalOnRulesResponded( k, v );
ThisVTable.InternalRulesFailedToRespond db = ( _ ) => InternalOnRulesFailedToRespond();
ThisVTable.InternalRulesRefreshComplete dc = ( _ ) => InternalOnRulesRefreshComplete();
RulesRespondPin = GCHandle.Alloc( da );
FailedRespondPin = GCHandle.Alloc( db );
CompletePin = GCHandle.Alloc( dc );
var t = new ThisVTable()
{
m_VTRulesResponded = da,
m_VTRulesFailedToRespond = db,
m_VTRulesRefreshComplete = dc,
};
vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( ThisVTable ) ) );
Marshal.StructureToPtr( t, vTablePtr, false );
vTablePin = GCHandle.Alloc( vTablePtr, GCHandleType.Pinned );
}
else
{
StdVTable.InternalRulesResponded da = InternalOnRulesResponded;
StdVTable.InternalRulesFailedToRespond db = InternalOnRulesFailedToRespond;
StdVTable.InternalRulesRefreshComplete dc = InternalOnRulesRefreshComplete;
RulesRespondPin = GCHandle.Alloc( da );
FailedRespondPin = GCHandle.Alloc( db );
CompletePin = GCHandle.Alloc( dc );
var t = new StdVTable()
{
m_VTRulesResponded = da,
m_VTRulesFailedToRespond = db,
m_VTRulesRefreshComplete = dc
};
vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( StdVTable ) ) );
Marshal.StructureToPtr( t, vTablePtr, false );
vTablePin = GCHandle.Alloc( vTablePtr, GCHandleType.Pinned );
}
}
private void InternalOnRulesResponded( string k, string v )
{
Server.Rules.Add( k, v );
}
private void InternalOnRulesFailedToRespond()
{
Server.OnServerRulesReceiveFinished( false );
}
private void InternalOnRulesRefreshComplete()
{
Server.OnServerRulesReceiveFinished( true );
}
[StructLayout( LayoutKind.Sequential )]
private class StdVTable
{
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesResponded m_VTRulesResponded;
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesFailedToRespond m_VTRulesFailedToRespond;
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesRefreshComplete m_VTRulesRefreshComplete;
[UnmanagedFunctionPointer( CallingConvention.StdCall )]
public delegate void InternalRulesResponded( string pchRule, string pchValue );
[UnmanagedFunctionPointer( CallingConvention.StdCall )]
public delegate void InternalRulesFailedToRespond();
[UnmanagedFunctionPointer( CallingConvention.StdCall )]
public delegate void InternalRulesRefreshComplete();
}
[StructLayout( LayoutKind.Sequential )]
private class ThisVTable
{
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesResponded m_VTRulesResponded;
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesFailedToRespond m_VTRulesFailedToRespond;
[MarshalAs(UnmanagedType.FunctionPtr)]
public InternalRulesRefreshComplete m_VTRulesRefreshComplete;
[UnmanagedFunctionPointer( CallingConvention.ThisCall )]
public delegate void InternalRulesResponded( IntPtr thisptr, string pchRule, string pchValue );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )]
public delegate void InternalRulesFailedToRespond( IntPtr thisptr );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )]
public delegate void InternalRulesRefreshComplete( IntPtr thisptr );
}
public System.IntPtr GetPtr()
{
return vTablePin.AddrOfPinnedObject();
}
};
}

View File

@ -33,13 +33,15 @@ public Server( uint appId, ServerInit init) : base( appId )
Instance = this;
native = new Interop.NativeInterface();
uint ipaddress = 0; // Any Port
if ( init.SteamPort == 0 ) init.RandomSteamPort();
if ( init.IpAddress != null ) ipaddress = Utility.IpToInt32( init.IpAddress );
//
// Get other interfaces
//
if ( !native.InitServer( this, init.IpAddress, init.SteamPort, init.GamePort, init.QueryPort, init.Secure ? 3 : 2, init.VersionString ) )
if ( !native.InitServer( this, ipaddress, init.SteamPort, init.GamePort, init.QueryPort, init.Secure ? 3 : 2, init.VersionString ) )
{
native.Dispose();
native = null;
@ -47,15 +49,18 @@ public Server( uint appId, ServerInit init) : base( appId )
return;
}
//
// Register Callbacks
//
SteamNative.Callbacks.RegisterCallbacks( this );
//
// Setup interfaces that client and server both have
//
SetupCommonInterfaces();
//
// Cache common, unchanging info
//
AppId = appId;
//
// Initial settings
@ -82,6 +87,11 @@ public Server( uint appId, ServerInit init) : base( appId )
Update();
}
~Server()
{
Dispose();
}
/// <summary>
/// Should be called at least once every frame
/// </summary>
@ -255,6 +265,8 @@ public void UpdatePlayer( ulong steamid, string name, int score )
/// </summary>
public override void Dispose()
{
if ( disposed ) return;
if ( Query != null )
{
Query = null;
@ -290,7 +302,7 @@ public System.Net.IPAddress PublicIp
var ip = native.gameServer.GetPublicIP();
if ( ip == 0 ) return null;
return new System.Net.IPAddress( Utility.SwapBytes( ip ) );
return Utility.Int32ToIp( ip );
}
}

View File

@ -35,10 +35,10 @@ internal ServerAuth( Server s )
{
server = s;
SteamNative.ValidateAuthTicketResponse_t.RegisterCallback( server, OnAuthTicketValidate );
server.RegisterCallback<SteamNative.ValidateAuthTicketResponse_t>( OnAuthTicketValidate );
}
void OnAuthTicketValidate( SteamNative.ValidateAuthTicketResponse_t data, bool b )
void OnAuthTicketValidate( SteamNative.ValidateAuthTicketResponse_t data )
{
if ( OnAuthChange != null )
OnAuthChange( data.SteamID, data.OwnerSteamID, (Status) data.AuthSessionResponse );

View File

@ -13,7 +13,7 @@ namespace Facepunch.Steamworks
/// </summary>
public class ServerInit
{
public uint IpAddress = 0;
public IPAddress IpAddress;
public ushort SteamPort;
public ushort GamePort = 27015;
public ushort QueryPort = 27016;

View File

@ -113,40 +113,34 @@ public float GetFloat( ulong steamid, string name, float defaultValue = 0 )
}
/// <summary>
/// Resets the unlock state of an achievement for the given user. Useful mainly for testing purposes.
/// If the "set by" field says "Official GS", only a registerd server IP may do this.
/// You should have called Refresh for this userid - which downloads the stats from the backend.
/// If you didn't call it this will always return false.
/// Unlocks the specified achievement for the specified user. Must have called Refresh on a steamid first.
/// Remember to use Commit after use.
/// </summary>
public bool ClearUserAchievement(ulong steamid, string name)
public bool SetAchievement( ulong steamid, string name )
{
return server.native.gameServerStats.ClearUserAchievement(steamid, name);
return server.native.gameServerStats.SetUserAchievement( steamid, name );
}
/// <summary>
/// Awards an achievement for the given user. The achievement must have been set up in the steam backand.
/// If the "set by" field says "Official GS", only a registerd server IP may do this.
/// You should have called Refresh for this userid - which downloads the stats from the backend.
/// If you didn't call it this will always return false.
/// Resets the unlock status of an achievement for the specified user. Must have called Refresh on a steamid first.
/// Remember to use Commit after use.
/// </summary>
public bool SetUserAchievement(ulong steamid, string name)
public bool ClearAchievement( ulong steamid, string name )
{
return server.native.gameServerStats.SetUserAchievement(steamid, name);
return server.native.gameServerStats.ClearUserAchievement( steamid, name );
}
/// <summary>
/// Returns true if the given user has the given achievement unlocked. The achievement must have been set up in the steam backand.
/// You should have called Refresh for this userid - which downloads the stats from the backend.
/// If you didn't call it this will always return false.
/// Return true if available, exists and unlocked
/// </summary>
public bool GetUserAchievement(ulong steamid, string name)
public bool GetAchievement( ulong steamid, string name )
{
bool result = false;
if(!server.native.gameServerStats.GetUserAchievement(steamid, name, ref result))
bool achieved = false;
if ( !server.native.gameServerStats.GetUserAchievement( steamid, name, ref achieved ) )
return false;
return result;
return achieved;
}
}
}

View File

@ -20,29 +20,50 @@ internal enum Flags : byte
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public class VTable
{
public IntPtr ResultA;
public IntPtr ResultB;
public IntPtr GetSize;
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void ResultD( IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void ResultWithInfoD( IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate int GetSizeD();
public ResultD ResultA;
public ResultWithInfoD ResultB;
public GetSizeD GetSize;
}
//
// All possible functions
//
internal class ThisCall
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public class VTableWin
{
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void Result( IntPtr thisptr, IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void ResultWithInfo( IntPtr thisptr, IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate int GetSize( IntPtr thisptr );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void ResultD( IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void ResultWithInfoD( IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate int GetSizeD();
public ResultWithInfoD ResultB;
public ResultD ResultA;
public GetSizeD GetSize;
}
internal class StdCall
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public class VTableThis
{
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void Result( IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate void ResultWithInfo( IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.StdCall )] public delegate int GetSize();
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void ResultD( IntPtr thisptr, IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void ResultWithInfoD( IntPtr thisptr, IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate int GetSizeD( IntPtr thisptr );
public ResultD ResultA;
public ResultWithInfoD ResultB;
public GetSizeD GetSize;
}
[StructLayout( LayoutKind.Sequential, Pack = 1 )]
public class VTableWinThis
{
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void ResultD( IntPtr thisptr, IntPtr pvParam );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate void ResultWithInfoD( IntPtr thisptr, IntPtr pvParam, bool bIOFailure, SteamNative.SteamAPICall_t hSteamAPICall );
[UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate int GetSizeD( IntPtr thisptr );
public ResultWithInfoD ResultB;
public ResultD ResultA;
public GetSizeD GetSize;
}
};
//
@ -50,21 +71,23 @@ internal class StdCall
//
internal class CallbackHandle : IDisposable
{
internal BaseSteamworks steamworks;
internal SteamAPICall_t CallResultHandle;
internal bool CallResult;
internal BaseSteamworks Steamworks;
// Get Rid
internal GCHandle FuncA;
internal GCHandle FuncB;
internal GCHandle FuncC;
internal IntPtr vTablePtr;
internal GCHandle PinnedCallback;
internal CallbackHandle( Facepunch.Steamworks.BaseSteamworks steamworks )
{
Steamworks = steamworks;
}
public void Dispose()
{
if ( CallResult )
UnregisterCallResult();
else
UnregisterCallback();
UnregisterCallback();
if ( FuncA.IsAllocated )
FuncA.Free();
@ -90,19 +113,86 @@ private void UnregisterCallback()
if ( !PinnedCallback.IsAllocated )
return;
steamworks.native.api.SteamAPI_UnregisterCallback( PinnedCallback.AddrOfPinnedObject() );
Steamworks.native.api.SteamAPI_UnregisterCallback( PinnedCallback.AddrOfPinnedObject() );
}
private void UnregisterCallResult()
public virtual bool IsValid { get { return true; } }
}
internal abstract class CallResult : CallbackHandle
{
internal SteamAPICall_t Call;
public override bool IsValid { get { return Call > 0; } }
internal CallResult( Facepunch.Steamworks.BaseSteamworks steamworks, SteamAPICall_t call ) : base( steamworks )
{
if ( CallResultHandle == 0 )
Call = call;
}
internal void Try()
{
bool failed = false;
if ( !Steamworks.native.utils.IsAPICallCompleted( Call, ref failed ))
return;
if ( !PinnedCallback.IsAllocated )
return;
Steamworks.UnregisterCallResult( this );
steamworks.native.api.SteamAPI_UnregisterCallResult( PinnedCallback.AddrOfPinnedObject(), CallResultHandle );
RunCallback();
}
internal abstract void RunCallback();
}
internal class CallResult<T> : CallResult
{
private static byte[] resultBuffer = new byte[1024 * 16];
internal delegate T ConvertFromPointer( IntPtr p );
Action<T, bool> CallbackFunction;
ConvertFromPointer ConvertFromPointerFunction;
internal int ResultSize = -1;
internal int CallbackId = 0;
internal CallResult( Facepunch.Steamworks.BaseSteamworks steamworks, SteamAPICall_t call, Action<T, bool> callbackFunction, ConvertFromPointer fromPointer, int resultSize, int callbackId ) : base( steamworks, call )
{
ResultSize = resultSize;
CallbackId = callbackId;
CallbackFunction = callbackFunction;
ConvertFromPointerFunction = fromPointer;
Steamworks.RegisterCallResult( this );
}
public override string ToString()
{
return $"CallResult( {typeof(T).Name}, {CallbackId}, {ResultSize}b )";
}
unsafe internal override void RunCallback()
{
bool failed = false;
fixed ( byte* ptr = resultBuffer )
{
if ( !Steamworks.native.utils.GetAPICallResult( Call, (IntPtr)ptr, resultBuffer.Length, CallbackId, ref failed ) || failed )
{
CallbackFunction( default(T), true );
return;
}
var val = ConvertFromPointerFunction( (IntPtr)ptr );
CallbackFunction( val, false );
}
}
}
internal class MonoPInvokeCallbackAttribute : Attribute
{
public MonoPInvokeCallbackAttribute() { }
}
}

View File

@ -41,6 +41,7 @@ internal static class CallbackIdentifiers
public const int ClientRemoteClientManager = 3300;
public const int ClientUGC = 3400;
public const int SteamStreamClient = 3500;
public const int ClientProductBuilder = 3600;
public const int ClientShortcuts = 3700;
public const int ClientRemoteControlManager = 3800;
public const int SteamAppList = 3900;
@ -54,6 +55,7 @@ internal static class CallbackIdentifiers
public const int ClientInventory = 4700;
public const int ClientBluetoothManager = 4800;
public const int ClientSharedConnection = 4900;
public const int SteamParentalSettings = 5000;
public const int ClientShader = 5100;
}
internal static class Defines

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
namespace Facepunch.Steamworks
{
internal static class Utility
public static partial class Utility
{
static internal uint SwapBytes( uint x )
static internal uint Swap( uint x )
{
return ( ( x & 0x000000ff ) << 24 ) +
( ( x & 0x0000ff00 ) << 8 ) +
( ( x & 0x00ff0000 ) >> 8 ) +
( ( x & 0xff000000 ) >> 24 );
return ((x & 0x000000ff) << 24) +
((x & 0x0000ff00) << 8) +
((x & 0x00ff0000) >> 8) +
((x & 0xff000000) >> 24);
}
static internal uint IpToInt32( this IPAddress ipAddress )
static public uint IpToInt32( this IPAddress ipAddress )
{
return BitConverter.ToUInt32( ipAddress.GetAddressBytes().Reverse().ToArray(), 0 );
return Swap( (uint) ipAddress.Address );
}
static internal IPAddress Int32ToIp( uint ipAddress )
static public IPAddress Int32ToIp( uint ipAddress )
{
return new IPAddress( BitConverter.GetBytes( ipAddress ).Reverse().ToArray() );
return new IPAddress( Swap( ipAddress ) );
}
static internal class Epoch
@ -77,8 +77,23 @@ internal static string FormatPrice(string currency, ulong price)
default: return $"{decimaled}{currency}";
}
}
public static string ReadNullTerminatedUTF8String( this BinaryReader br, byte[] buffer = null )
{
if ( buffer == null )
buffer = new byte[1024];
byte chr;
int i = 0;
while ( (chr = br.ReadByte()) != 0 && i < buffer.Length )
{
buffer[i] = chr;
i++;
}
return Encoding.UTF8.GetString( buffer, 0, i );
}
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Facepunch.Steamworks
{
internal class SourceServerQuery : IDisposable
{
public static List<SourceServerQuery> Current = new List<SourceServerQuery>();
public static void Cycle()
{
if ( Current.Count == 0 )
return;
for( int i = Current.Count; i>0; i-- )
{
Current[i-1].Update();
}
}
private static readonly byte[] A2S_SERVERQUERY_GETCHALLENGE = { 0x55, 0xFF, 0xFF, 0xFF, 0xFF };
private static readonly byte A2S_PLAYER = 0x55;
private static readonly byte A2S_RULES = 0x56;
public volatile bool IsRunning;
public volatile bool IsSuccessful;
private ServerList.Server Server;
private UdpClient udpClient;
private IPEndPoint endPoint;
private System.Threading.Thread thread;
private byte[] _challengeBytes;
private Dictionary<string, string> rules = new Dictionary<string, string>();
public SourceServerQuery( ServerList.Server server, IPAddress address, int queryPort )
{
Server = server;
endPoint = new IPEndPoint( address, queryPort );
Current.Add( this );
IsRunning = true;
IsSuccessful = false;
thread = new System.Threading.Thread( ThreadedStart );
thread.Start();
}
void Update()
{
if ( !IsRunning )
{
Current.Remove( this );
Server.OnServerRulesReceiveFinished( rules, IsSuccessful );
}
}
private void ThreadedStart( object obj )
{
try
{
using ( udpClient = new UdpClient() )
{
udpClient.Client.SendTimeout = 3000;
udpClient.Client.ReceiveTimeout = 3000;
udpClient.Connect( endPoint );
GetRules();
IsSuccessful = true;
}
}
catch ( System.Exception )
{
IsSuccessful = false;
}
udpClient = null;
IsRunning = false;
}
void GetRules()
{
GetChallengeData();
_challengeBytes[0] = A2S_RULES;
Send( _challengeBytes );
var ruleData = Receive();
using ( var br = new BinaryReader( new MemoryStream( ruleData ) ) )
{
if ( br.ReadByte() != 0x45 )
throw new Exception( "Invalid data received in response to A2S_RULES request" );
var numRules = br.ReadUInt16();
for ( int index = 0; index < numRules; index++ )
{
rules.Add( br.ReadNullTerminatedUTF8String( readBuffer ), br.ReadNullTerminatedUTF8String( readBuffer ) );
}
}
}
byte[] readBuffer = new byte[1024 * 4];
private byte[] Receive()
{
byte[][] packets = null;
byte packetNumber = 0, packetCount = 1;
do
{
var result = udpClient.Receive( ref endPoint );
using ( var br = new BinaryReader( new MemoryStream( result ) ) )
{
var header = br.ReadInt32();
if ( header == -1 )
{
var unsplitdata = new byte[result.Length - br.BaseStream.Position];
Buffer.BlockCopy( result, (int)br.BaseStream.Position, unsplitdata, 0, unsplitdata.Length );
return unsplitdata;
}
else if ( header == -2 )
{
int requestId = br.ReadInt32();
packetNumber = br.ReadByte();
packetCount = br.ReadByte();
int splitSize = br.ReadInt32();
}
else
{
throw new System.Exception( "Invalid Header" );
}
if ( packets == null ) packets = new byte[packetCount][];
var data = new byte[result.Length - br.BaseStream.Position];
Buffer.BlockCopy( result, (int)br.BaseStream.Position, data, 0, data.Length );
packets[packetNumber] = data;
}
}
while ( packets.Any( p => p == null ) );
var combinedData = Combine( packets );
return combinedData;
}
private void GetChallengeData()
{
if ( _challengeBytes != null ) return;
Send( A2S_SERVERQUERY_GETCHALLENGE );
var challengeData = Receive();
if ( challengeData[0] != 0x41 )
throw new Exception( "Invalid Challenge" );
_challengeBytes = challengeData;
}
byte[] sendBuffer = new byte[1024];
private void Send( byte[] message )
{
sendBuffer[0] = 0xFF;
sendBuffer[1] = 0xFF;
sendBuffer[2] = 0xFF;
sendBuffer[3] = 0xFF;
Buffer.BlockCopy( message, 0, sendBuffer, 4, message.Length );
udpClient.Send( sendBuffer, message.Length + 4 );
}
private byte[] Combine( byte[][] arrays )
{
var rv = new byte[arrays.Sum( a => a.Length )];
int offset = 0;
foreach ( byte[] array in arrays )
{
Buffer.BlockCopy( array, 0, rv, offset, array.Length );
offset += array.Length;
}
return rv;
}
public void Dispose()
{
if ( thread != null && thread.IsAlive )
{
thread.Abort();
}
thread = null;
}
};
}

View File

@ -29,12 +29,12 @@ internal void ExtendDefinition( SteamApiDefinition def )
//
def.CallbackIds = new Dictionary<string, int>();
{
var r = new Regex( @"enum { (k_i(?:.+)) = ([0-9]+) };" );
var r = new Regex( @"enum { (k_[i|I](?:.+)) = ([0-9]+) };" );
var ma = r.Matches( Content );
foreach ( Match m in ma )
{
def.CallbackIds.Add( m.Groups[1].Value.Replace( "k_i", "" ).Replace( "Callbacks", "" ), int.Parse( m.Groups[2].Value ) );
def.CallbackIds.Add( m.Groups[1].Value.Substring( 3 ).Replace( "Callbacks", "" ), int.Parse( m.Groups[2].Value ) );
}
}
@ -62,7 +62,7 @@ internal void ExtendDefinition( SteamApiDefinition def )
num = "0";
}
kName = kName.Replace( "k_i", "CallbackIdentifiers." ).Replace( "Callbacks", "" );
kName = "CallbackIdentifiers." + kName.Substring( 3 ).Replace( "Callbacks", "" );
t.CallbackId = $"{kName} + {num}";
}
@ -77,7 +77,8 @@ internal void ExtendDefinition( SteamApiDefinition def )
var kName = m.Groups[1].Value;
var num = m.Groups[2].Value;
kName = kName.Replace( "k_i", "CallbackIdentifiers." ).Replace( "Callbacks", "" );
//kName = kName.Replace( "k_i", "CallbackIdentifiers." ).Replace( "Callbacks", "" );
kName = "CallbackIdentifiers." + kName.Substring( 3 ).Replace( "Callbacks", "" );
t.CallbackId = $"{kName} + {num}";
}
@ -119,6 +120,25 @@ internal void ExtendDefinition( SteamApiDefinition def )
}
}
//
// Find missing structs
//
{
var r = new Regex( @"struct ([a-zA-Z]+_t)" );
var ma = r.Matches( Content );
foreach ( Match m in ma )
{
var s = def.structs.SingleOrDefault( x => x.Name == m.Groups[1].Value );
if ( s == null )
{
Console.WriteLine( "Missing Struct: " + m.Groups[1].Value );
}
}
//Console.ReadKey();
}
}
}
}

View File

@ -9,10 +9,12 @@ namespace Generator
public partial class CodeWriter
{
bool LargePack;
bool X86;
private void PlatformClass( string type, string libraryName, bool LargePack )
{
this.LargePack = LargePack;
X86 = type.EndsWith( "32" );
StartBlock( $"internal static partial class Platform" );
{
@ -187,7 +189,10 @@ private void InteropClassMethod( string library, string classname, SteamApiDefin
if ( argstring != "" ) argstring = $" {argstring} ";
Write( $"[DllImportAttribute( \"{library}\" )] " );
if ( X86 )
Write( $"[DllImport( \"{library}\", CallingConvention = CallingConvention.Cdecl )] " );
else
Write( $"[DllImport( \"{library}\" )] " );
if ( ret.Return() == "bool" ) WriteLine( "[return: MarshalAs(UnmanagedType.U1)]" );

View File

@ -37,6 +37,8 @@ public class TypeDef
void Structs()
{
var callbackList = new List<SteamApiDefinition.StructDef>();
foreach ( var c in def.structs )
{
if ( SkipStructs.Contains( c.Name ) )
@ -47,7 +49,7 @@ void Structs()
int defaultPack = 8;
if ( c.Fields.Any( x => x.Type.Contains( "class CSteamID" ) ) && !ForceLargePackStructs.Contains( c.Name ) )
if ( c.Fields.Any( x => x.Type.Contains( "class CSteamID" ) ) && !ForceLargePackStructs.Contains( c.Name ) )
defaultPack = 4;
//
@ -58,7 +60,7 @@ void Structs()
{
if ( !string.IsNullOrEmpty( c.CallbackId ) )
{
WriteLine( "public const int CallbackId = " + c.CallbackId + ";" );
WriteLine( "internal const int CallbackId = " + c.CallbackId + ";" );
}
//
@ -70,7 +72,7 @@ void Structs()
WriteLine( "//" );
WriteLine( "// Read this struct from a pointer, usually from Native. It will automatically do the awesome stuff." );
WriteLine( "//" );
StartBlock( $"public static {c.Name} FromPointer( IntPtr p )" );
StartBlock( $"internal static {c.Name} FromPointer( IntPtr p )" );
{
WriteLine( $"if ( Platform.PackSmall ) return (PackSmall) Marshal.PtrToStructure( p, typeof(PackSmall) );" );
@ -78,6 +80,18 @@ void Structs()
}
EndBlock();
WriteLine();
WriteLine( "//" );
WriteLine( "// Get the size of the structure we're going to be using." );
WriteLine( "//" );
StartBlock( $"internal static int StructSize()" );
{
WriteLine( $"if ( Platform.PackSmall ) return System.Runtime.InteropServices.Marshal.SizeOf( typeof(PackSmall) );" );
WriteLine( $"return System.Runtime.InteropServices.Marshal.SizeOf( typeof({c.Name}) );" );
}
EndBlock();
if ( defaultPack == 8 )
defaultPack = 4;
@ -121,12 +135,24 @@ void Structs()
if ( !string.IsNullOrEmpty( c.CallbackId ) )
{
Callback( c );
callbackList.Add( c );
}
}
EndBlock();
WriteLine();
}
StartBlock( $"internal static class Callbacks" );
StartBlock( $"internal static void RegisterCallbacks( Facepunch.Steamworks.BaseSteamworks steamworks )" );
{
foreach ( var c in callbackList )
{
WriteLine( $"{c.Name}.Register( steamworks );" );
}
}
EndBlock();
EndBlock();
}
private void StructFields( SteamApiDefinition.StructDef.StructFields[] fields )
@ -199,20 +225,19 @@ private void StructFields( SteamApiDefinition.StructDef.StructFields[] fields )
WriteLine($"[MarshalAs(UnmanagedType.ByValArray, SizeConst = {num}, ArraySubType = UnmanagedType.U4)]");
}
WriteLine( $"public {t} {CleanMemberName( m.Name )}; // {m.Name} {m.Type}" );
WriteLine( $"internal {t} {CleanMemberName( m.Name )}; // {m.Name} {m.Type}" );
}
}
private void Callback( SteamApiDefinition.StructDef c )
{
{
WriteLine();
StartBlock( $"public static void RegisterCallback( Facepunch.Steamworks.BaseSteamworks steamworks, Action<{c.Name}, bool> CallbackFunction )" );
StartBlock( $"internal static void Register( Facepunch.Steamworks.BaseSteamworks steamworks )" );
{
WriteLine( $"var handle = new CallbackHandle();" );
WriteLine( $"handle.steamworks = steamworks;" );
WriteLine( $"var handle = new CallbackHandle( steamworks );" );
WriteLine( $"" );
CallbackCallresultShared( c, false );
CallbackCall( c );
WriteLine( "" );
WriteLine( "//" );
@ -224,36 +249,57 @@ private void Callback( SteamApiDefinition.StructDef c )
WriteLine( "steamworks.RegisterCallbackHandle( handle );" );
}
EndBlock();
WriteLine();
WriteLine( "[MonoPInvokeCallback]" );
WriteLine( "internal static void OnResultThis( IntPtr self, IntPtr param ){ OnResult( param ); }" );
WriteLine( "[MonoPInvokeCallback]" );
WriteLine( "internal static void OnResultWithInfoThis( IntPtr self, IntPtr param, bool failure, SteamNative.SteamAPICall_t call ){ OnResultWithInfo( param, failure, call ); }" );
WriteLine( "[MonoPInvokeCallback]" );
WriteLine( "internal static int OnGetSizeThis( IntPtr self ){ return OnGetSize(); }" );
WriteLine( "[MonoPInvokeCallback]" );
WriteLine( "internal static int OnGetSize(){ return StructSize(); }" );
WriteLine();
WriteLine( "[MonoPInvokeCallback]" );
StartBlock( "internal static void OnResult( IntPtr param )" );
{
WriteLine( $"OnResultWithInfo( param, false, 0 );" );
}
EndBlock();
WriteLine();
WriteLine( "[MonoPInvokeCallback]" );
StartBlock( "internal static void OnResultWithInfo( IntPtr param, bool failure, SteamNative.SteamAPICall_t call )" );
{
WriteLine( $"if ( failure ) return;" );
WriteLine();
WriteLine( "var value = FromPointer( param );" );
WriteLine();
WriteLine( "if ( Facepunch.Steamworks.Client.Instance != null )" );
WriteLine( $" Facepunch.Steamworks.Client.Instance.OnCallback<{c.Name}>( value );" );
WriteLine();
WriteLine( "if ( Facepunch.Steamworks.Server.Instance != null )" );
WriteLine( $" Facepunch.Steamworks.Server.Instance.OnCallback<{c.Name}>( value );" );
}
EndBlock();
}
private void CallResult( SteamApiDefinition.StructDef c )
{
WriteLine();
StartBlock( $"public static CallbackHandle CallResult( Facepunch.Steamworks.BaseSteamworks steamworks, SteamAPICall_t call, Action<{c.Name}, bool> CallbackFunction )" );
StartBlock( $"internal static CallResult<{c.Name}> CallResult( Facepunch.Steamworks.BaseSteamworks steamworks, SteamAPICall_t call, Action<{c.Name}, bool> CallbackFunction )" );
{
WriteLine( $"var handle = new CallbackHandle();" );
WriteLine( $"handle.steamworks = steamworks;" );
WriteLine( $"handle.CallResultHandle = call;" );
WriteLine( $"handle.CallResult = true;" );
WriteLine( $"" );
CallbackCallresultShared( c, true );
WriteLine( "" );
WriteLine( "//" );
WriteLine( "// Register the callback with Steam" );
WriteLine( "//" );
WriteLine( $"steamworks.native.api.SteamAPI_RegisterCallResult( handle.PinnedCallback.AddrOfPinnedObject(), call );" );
WriteLine();
WriteLine( "return handle;" );
WriteLine( $"return new CallResult<{c.Name}>( steamworks, call, CallbackFunction, FromPointer, StructSize(), CallbackId );" );
}
EndBlock();
}
private void CallbackCallresultShared( SteamApiDefinition.StructDef c, bool Result )
private void CallbackCall( SteamApiDefinition.StructDef c )
{
WriteLine( "//" );
WriteLine( "// Create the functions we need for the vtable" );
@ -261,11 +307,11 @@ private void CallbackCallresultShared( SteamApiDefinition.StructDef c, bool Resu
StartBlock( "if ( Facepunch.Steamworks.Config.UseThisCall )" );
{
CallresultFunctions( c, Result, "ThisCall", "_" );
CallFunctions( c, "ThisCall", "_" );
}
Else();
{
CallresultFunctions( c, Result, "StdCall", "" );
CallFunctions( c, "StdCall", "" );
}
EndBlock();
@ -285,73 +331,51 @@ private void CallbackCallresultShared( SteamApiDefinition.StructDef c, bool Resu
WriteLine( $"handle.PinnedCallback = GCHandle.Alloc( cb, GCHandleType.Pinned );" );
}
private void CallresultFunctions( SteamApiDefinition.StructDef c, bool Result, string ThisCall, string ThisArg )
private void CallFunctions( SteamApiDefinition.StructDef c, string ThisCall, string ThisArg )
{
var ThisArgC = ThisArg.Length > 0 ? $"{ThisArg}, " : "";
if ( Result )
{
WriteLine( $"Callback.{ThisCall}.Result funcA = ( {ThisArgC}p ) => {{ handle.Dispose(); CallbackFunction( FromPointer( p ), false ); }};" );
StartBlock( $"Callback.{ThisCall}.ResultWithInfo funcB = ( {ThisArgC}p, bIOFailure, hSteamAPICall ) => " );
{
WriteLine( "if ( hSteamAPICall != call ) return;" );
WriteLine();
WriteLine( "handle.CallResultHandle = 0;" );
WriteLine( "handle.Dispose();" );
WriteLine();
WriteLine( "CallbackFunction( FromPointer( p ), bIOFailure );" );
}
EndBlock( ";" );
}
else
{
WriteLine( $"Callback.{ThisCall}.Result funcA = ( {ThisArgC}p ) => {{ CallbackFunction( FromPointer( p ), false ); }};" );
WriteLine( $"Callback.{ThisCall}.ResultWithInfo funcB = ( {ThisArgC}p, bIOFailure, hSteamAPICall ) => {{ CallbackFunction( FromPointer( p ), bIOFailure ); }};" );
}
WriteLine( $"Callback.{ThisCall}.GetSize funcC = ( {ThisArg} ) => Marshal.SizeOf( typeof( {c.Name} ) );" );
WriteLine();
WriteLine( "//" );
WriteLine( "// If this platform is PackSmall, use PackSmall versions of everything instead" );
WriteLine( "//" );
StartBlock( "if ( Platform.PackSmall )" );
{
WriteLine( $"funcC = ( {ThisArg} ) => Marshal.SizeOf( typeof( PackSmall ) );" );
}
EndBlock();
WriteLine( "" );
WriteLine( "//" );
WriteLine( "// Allocate a handle to each function, so they don't get disposed" );
WriteLine( "//" );
WriteLine( "handle.FuncA = GCHandle.Alloc( funcA );" );
WriteLine( "handle.FuncB = GCHandle.Alloc( funcB );" );
WriteLine( "handle.FuncC = GCHandle.Alloc( funcC );" );
WriteLine();
var This = ThisArg.Length > 0 ? "This" : "";
WriteLine( "//" );
WriteLine( "// Create the VTable by manually allocating the memory and copying across" );
WriteLine( "//" );
WriteLine( "handle.vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( Callback.VTable ) ) );" );
StartBlock( "var vTable = new Callback.VTable()" );
{
WriteLine( "ResultA = Marshal.GetFunctionPointerForDelegate( funcA )," );
WriteLine( "ResultB = Marshal.GetFunctionPointerForDelegate( funcB )," );
WriteLine( "GetSize = Marshal.GetFunctionPointerForDelegate( funcC )," );
}
EndBlock( ";" );
WriteLine( "//" );
WriteLine( "// The order of these functions are swapped on Windows" );
WriteLine( "//" );
StartBlock( "if ( Platform.IsWindows )" );
{
WriteLine( "vTable.ResultA = Marshal.GetFunctionPointerForDelegate( funcB );" );
WriteLine( "vTable.ResultB = Marshal.GetFunctionPointerForDelegate( funcA );" );
WriteLine( $"handle.vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( Callback.VTableWin{This} ) ) );" );
StartBlock( $"var vTable = new Callback.VTableWin{This}" );
{
WriteLine( $"ResultA = OnResult{This}," );
WriteLine( $"ResultB = OnResultWithInfo{This}," );
WriteLine( $"GetSize = OnGetSize{This}," );
}
EndBlock( ";" );
WriteLine( "handle.FuncA = GCHandle.Alloc( vTable.ResultA );" );
WriteLine( "handle.FuncB = GCHandle.Alloc( vTable.ResultB );" );
WriteLine( "handle.FuncC = GCHandle.Alloc( vTable.GetSize );" );
WriteLine( "Marshal.StructureToPtr( vTable, handle.vTablePtr, false );" );
}
Else();
{
WriteLine( $"handle.vTablePtr = Marshal.AllocHGlobal( Marshal.SizeOf( typeof( Callback.VTable{This} ) ) );" );
StartBlock( $"var vTable = new Callback.VTable{This}" );
{
WriteLine( $"ResultA = OnResult{This}," );
WriteLine( $"ResultB = OnResultWithInfo{This}," );
WriteLine( $"GetSize = OnGetSize{This}," );
}
EndBlock( ";" );
WriteLine( "handle.FuncA = GCHandle.Alloc( vTable.ResultA );" );
WriteLine( "handle.FuncB = GCHandle.Alloc( vTable.ResultB );" );
WriteLine( "handle.FuncC = GCHandle.Alloc( vTable.GetSize );" );
WriteLine( "Marshal.StructureToPtr( vTable, handle.vTablePtr, false );" );
}
EndBlock();
WriteLine( "Marshal.StructureToPtr( vTable, handle.vTablePtr, false );" );
}
}
}

View File

@ -32,6 +32,11 @@ private static void AddMissing( SteamApiDefinition output )
output.structs.AddRange( missing.structs );
output.methods.AddRange( missing.methods );
foreach ( var s in output.structs )
{
if ( s.Fields == null ) s.Fields = new SteamApiDefinition.StructDef.StructFields[0];
}
}
}
}

View File

@ -1,229 +1,281 @@
{
structs:
[
{
struct: "ItemInstalled_t",
fields:
[
{
fieldname: "m_unAppID",
fieldtype: "AppId_t"
},
{
fieldname: "m_nPublishedFileId",
fieldtype: "PublishedFileId_t"
}
]
}
],
methods:
[
{
classname: "SteamApi",
methodname: "SteamAPI_Init",
returntype: "bool",
NeedsSelfPointer: false
},
{
classname: "SteamApi",
methodname: "SteamAPI_RunCallbacks",
returntype: "void",
NeedsSelfPointer: false
},
{
classname: "SteamApi",
methodname: "SteamGameServer_RunCallbacks",
returntype: "void",
NeedsSelfPointer: false
},
{
classname: "SteamApi",
methodname: "SteamAPI_RegisterCallback",
returntype: "void",
NeedsSelfPointer: false,
params:
[
{
paramname: "pCallback",
paramtype: "void *"
},
{
paramname: "callback",
paramtype: "int"
},
]
},
{
classname: "SteamApi",
methodname: "SteamAPI_UnregisterCallback",
returntype: "void",
NeedsSelfPointer: false,
params:
[
{
paramname: "pCallback",
paramtype: "void *"
}
]
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_RegisterCallResult",
returntype: "void",
params:
[
{
paramname: "pCallback",
paramtype: "void *"
},
{
paramname: "callback",
paramtype: "SteamAPICall_t"
}
],
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_UnregisterCallResult",
returntype: "void",
params:
[
{
paramname: "pCallback",
paramtype: "void *"
},
{
paramname: "callback",
paramtype: "SteamAPICall_t"
}
]
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamInternal_GameServer_Init",
returntype: "bool",
params:
[
{
paramname: "unIP",
paramtype: "uint32"
},
{
paramname: "usPort",
paramtype: "uint16"
},
{
paramname: "usGamePort",
paramtype: "uint16"
},
{
paramname: "usQueryPort",
paramtype: "uint16"
},
{
paramname: "eServerMode",
paramtype: "int"
},
{
paramname: "pchVersionString",
paramtype: "const char *"
}
],
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_Shutdown",
returntype: "void",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamGameServer_Shutdown",
returntype: "void",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_GetHSteamUser",
returntype: "HSteamUser",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_GetHSteamPipe",
returntype: "HSteamPipe",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamGameServer_GetHSteamUser",
returntype: "HSteamUser",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamGameServer_GetHSteamPipe",
returntype: "HSteamPipe",
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamInternal_CreateInterface",
returntype: "void *",
params:
[
{
paramname: "version",
paramtype: "const char *"
}
],
},
{
NeedsSelfPointer: false,
classname: "SteamApi",
methodname: "SteamAPI_RestartAppIfNecessary",
returntype: "bool",
params:
[
{
paramname: "unOwnAppID",
paramtype: "uint32"
}
],
},
]
"structs":
[
{
"struct": "ItemInstalled_t",
"fields":
[
{
"fieldname": "m_unAppID",
"fieldtype": "AppId_t"
},
{
"fieldname": "m_nPublishedFileId",
"fieldtype": "PublishedFileId_t"
}
]
},
{
"struct": "SteamInventoryDefinitionUpdate_t"
},
{
"struct": "SteamParentalSettingsChanged_t"
},
{
"struct": "SteamServersConnected_t"
},
{
"struct": "NewLaunchQueryParameters_t"
},
{
"struct": "GCMessageAvailable_t",
"fields":
[
{
"fieldname": "m_nMessageSize",
"fieldtype": "uint32"
}
]
},
{
"struct": "GCMessageFailed_t"
},
{
"struct": "ScreenshotRequested_t"
},
{
"struct": "LicensesUpdated_t"
},
{
"struct": "SteamShutdown_t"
},
{
"struct": "IPCountry_t"
},
{
"struct": "IPCFailure_t",
"fields":
[
{
"fieldname": "m_eFailureType",
"fieldtype": "uint8"
}
]
}
],
"methods":
[
{
"classname": "SteamApi",
"methodname": "SteamAPI_Init",
"returntype": "bool",
"NeedsSelfPointer": false
},
{
"classname": "SteamApi",
"methodname": "SteamAPI_RunCallbacks",
"returntype": "void",
"NeedsSelfPointer": false
},
{
"classname": "SteamApi",
"methodname": "SteamGameServer_RunCallbacks",
"returntype": "void",
"NeedsSelfPointer": false
},
{
"classname": "SteamApi",
"methodname": "SteamAPI_RegisterCallback",
"returntype": "void",
"NeedsSelfPointer": false,
"params":
[
{
"paramname": "pCallback",
"paramtype": "void *"
},
{
"paramname": "callback",
"paramtype": "int"
}
]
},
{
"classname": "SteamApi",
"methodname": "SteamAPI_UnregisterCallback",
"returntype": "void",
"NeedsSelfPointer": false,
"params":
[
{
"paramname": "pCallback",
"paramtype": "void *"
}
]
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_RegisterCallResult",
"returntype": "void",
"params":
[
{
"paramname": "pCallback",
"paramtype": "void *"
},
{
"paramname": "callback",
"paramtype": "SteamAPICall_t"
}
]
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_UnregisterCallResult",
"returntype": "void",
"params":
[
{
"paramname": "pCallback",
"paramtype": "void *"
},
{
"paramname": "callback",
"paramtype": "SteamAPICall_t"
}
]
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamInternal_GameServer_Init",
"returntype": "bool",
"params":
[
{
"paramname": "unIP",
"paramtype": "uint32"
},
{
"paramname": "usPort",
"paramtype": "uint16"
},
{
"paramname": "usGamePort",
"paramtype": "uint16"
},
{
"paramname": "usQueryPort",
"paramtype": "uint16"
},
{
"paramname": "eServerMode",
"paramtype": "int"
},
{
"paramname": "pchVersionString",
"paramtype": "const char *"
}
]
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_Shutdown",
"returntype": "void"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamGameServer_Shutdown",
"returntype": "void"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_GetHSteamUser",
"returntype": "HSteamUser"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_GetHSteamPipe",
"returntype": "HSteamPipe"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamGameServer_GetHSteamUser",
"returntype": "HSteamUser"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamGameServer_GetHSteamPipe",
"returntype": "HSteamPipe"
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamInternal_CreateInterface",
"returntype": "void *",
"params":
[
{
"paramname": "version",
"paramtype": "const char *"
}
]
},
{
"NeedsSelfPointer": false,
"classname": "SteamApi",
"methodname": "SteamAPI_RestartAppIfNecessary",
"returntype": "bool",
"params":
[
{
"paramname": "unOwnAppID",
"paramtype": "uint32"
}
]
}
]
}