diff --git a/.gitignore b/.gitignore index b4b44f0..5bafc8c 100644 --- a/.gitignore +++ b/.gitignore @@ -55,11 +55,14 @@ Facepunch.Steamworks.Test/bin/Release/Facepunch.Steamworks.Test.dll Facepunch.Steamworks.Test/bin/Release/Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll *.user *.cache +*.idea +*.vscode TestResults obj Facepunch.Steamworks/bin/Debug/Facepunch.Steamworks.Api.dll Facepunch.Steamworks/bin/Debug/Facepunch.Steamworks.dll Facepunch.Steamworks/bin/Release/Facepunch.Steamworks.dll +Facepunch.Steamworks/bin *.opendb *.db Facepunch.Steamworks.dll @@ -69,4 +72,5 @@ mscorlib.dll *.nlp packages Generator/bin -*.XML \ No newline at end of file +*.XML +.vs \ No newline at end of file diff --git a/Facepunch.Steamworks.Test/Client/Lobby.cs b/Facepunch.Steamworks.Test/Client/Lobby.cs new file mode 100644 index 0000000..dd2cf04 --- /dev/null +++ b/Facepunch.Steamworks.Test/Client/Lobby.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Facepunch.Steamworks; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Facepunch.Steamworks.Test +{ + [TestClass] + [DeploymentItem("steam_api.dll")] + [DeploymentItem("steam_api64.dll")] + [DeploymentItem("steam_appid.txt")] + public class Lobby + { + [TestMethod] + public void CreateLobby() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + Console.WriteLine($"Owner: {client.Lobby.Owner}"); + Console.WriteLine($"Max Members: {client.Lobby.MaxMembers}"); + Console.WriteLine($"Num Members: {client.Lobby.NumMembers}"); + client.Lobby.Leave(); + }; + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 3) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + } + } + + [TestMethod] + public void GetCreatedLobbyData() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + foreach (KeyValuePair data in client.Lobby.CurrentLobbyData.GetAllData()) + { + if (data.Key == "appid") + { + Assert.IsTrue(data.Value == "252490"); + } + Console.WriteLine($"{data.Key} {data.Value}"); + } + client.Lobby.Leave(); + }; + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 3) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + } + } + + [TestMethod] + public void UpdateLobbyData() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + + client.Lobby.Name = "My Updated Lobby Name"; + client.Lobby.CurrentLobbyData.SetData("testkey", "testvalue"); + client.Lobby.LobbyType = Steamworks.Lobby.Type.Private; + client.Lobby.MaxMembers = 5; + client.Lobby.Joinable = false; + + foreach (KeyValuePair data in client.Lobby.CurrentLobbyData.GetAllData()) + { + if (data.Key == "appid") + { + Assert.IsTrue(data.Value == "252490"); + } + + if (data.Key == "testkey") + { + Assert.IsTrue(data.Value == "testvalue"); + } + + if (data.Key == "lobbytype") + { + Assert.IsTrue(data.Value == Steamworks.Lobby.Type.Private.ToString()); + } + + Console.WriteLine($"{data.Key} {data.Value}"); + } + + + + }; + + + client.Lobby.OnLobbyDataUpdated = () => + { + Console.WriteLine("lobby data updated"); + Console.WriteLine(client.Lobby.MaxMembers); + Console.WriteLine(client.Lobby.Joinable); + }; + + + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 3) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + client.Lobby.Leave(); + + } + } + + [TestMethod] + public void RefreshLobbyList() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + client.LobbyList.Refresh(); + }; + + client.LobbyList.OnLobbiesUpdated = () => + { + Console.WriteLine("lobbies updating"); + if (client.LobbyList.Finished) + { + Console.WriteLine("lobbies finished updating"); + Console.WriteLine($"found {client.LobbyList.Lobbies.Count} lobbies"); + + foreach (LobbyList.Lobby lobby in client.LobbyList.Lobbies) + { + Console.WriteLine($"Found Lobby: {lobby.Name}"); + } + + client.Lobby.Leave(); + + } + + }; + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 3) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + } + } + + [TestMethod] + public void RefreshLobbyListWithFilter() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + client.Lobby.CurrentLobbyData.SetData("testkey", "testvalue"); + }; + + client.Lobby.OnLobbyDataUpdated = () => + { + var filter = new LobbyList.Filter(); + filter.StringFilters.Add("testkey", "testvalue"); + client.LobbyList.Refresh(filter); + }; + + client.LobbyList.OnLobbiesUpdated = () => + { + Console.WriteLine("lobbies updating"); + if (client.LobbyList.Finished) + { + Console.WriteLine("lobbies finished updating"); + Console.WriteLine($"found {client.LobbyList.Lobbies.Count} lobbies"); + + foreach (LobbyList.Lobby lobby in client.LobbyList.Lobbies) + { + Console.WriteLine($"Found Lobby: {lobby.Name}"); + } + + } + + }; + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 5) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + client.Lobby.Leave(); + + } + } + + [TestMethod] + public void SendChatMessage() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + string testString = "Hello, World"; + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + client.Lobby.CurrentLobbyData.SetData("testkey", "testvalue"); + client.Lobby.SendChatMessage(testString); + }; + + client.Lobby.OnChatMessageRecieved = (steamID, bytes, length) => + { + string message = Encoding.UTF8.GetString(bytes, 0, length); + Console.WriteLine("message recieved"); + Assert.IsTrue(message == testString); + }; + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 5) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + client.Lobby.Leave(); + + } + } + + [TestMethod] + public void SetGetUserMetadata() + { + using (var client = new Facepunch.Steamworks.Client(252490)) + { + Assert.IsTrue(client.IsValid); + + client.Lobby.OnLobbyCreated = (success) => + { + Assert.IsTrue(success); + Assert.IsTrue(client.Lobby.IsValid); + Console.WriteLine("lobby created: " + client.Lobby.CurrentLobby); + client.Lobby.SetMemberData("testkey", "testvalue"); + }; + + client.Lobby.OnLobbyMemberDataUpdated = (steamID) => + { + string name = client.Friends.GetName(steamID); + Console.WriteLine(name + " updated data"); + Assert.IsTrue(client.Lobby.GetMemberData(steamID, "testkey") == "testvalue"); + Console.WriteLine("testkey is now: " + client.Lobby.GetMemberData(steamID, "testkey")); + }; + + client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); + + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < 5) + { + client.Update(); + System.Threading.Thread.Sleep(10); + } + + client.Lobby.Leave(); + + } + } + + } +} diff --git a/Facepunch.Steamworks.Test/Client/Serverlist.cs b/Facepunch.Steamworks.Test/Client/Serverlist.cs index 29614c8..2f13b33 100644 --- a/Facepunch.Steamworks.Test/Client/Serverlist.cs +++ b/Facepunch.Steamworks.Test/Client/Serverlist.cs @@ -47,7 +47,7 @@ public void InternetList() foreach ( var server in query.Responded.Take( 20 ) ) { - Console.WriteLine( "{0} {1}", server.AddressString, server.Name ); + Console.WriteLine( "{0} {1}", server.Address, server.Name ); } query.Dispose(); @@ -370,7 +370,9 @@ public void CustomList() foreach ( var s in query.Responded ) { - Console.WriteLine( "{0} - {1}", s.AddressString, s.Name ); + Console.WriteLine( "{0} - {1}", s.Address, s.Name ); + + Assert.IsTrue( servers.Contains( $"{s.Address}:{s.QueryPort}" ) ); } query.Dispose(); diff --git a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj index 891970f..ce77f05 100644 --- a/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj +++ b/Facepunch.Steamworks.Test/Facepunch.Steamworks.Test.csproj @@ -94,6 +94,7 @@ + @@ -107,12 +108,6 @@ - - - {dc2d9fa9-f005-468f-8581-85c79f4e0034} - Facepunch.Steamworks - - @@ -120,6 +115,12 @@ + + + {91962664-eb42-472a-94c8-c4ffeb44cc4b} + Facepunch.Steamworks + + diff --git a/Facepunch.Steamworks.Test/bin/Debug/Newtonsoft.Json.dll b/Facepunch.Steamworks.Test/bin/Debug/Newtonsoft.Json.dll new file mode 100644 index 0000000..5523f5c Binary files /dev/null and b/Facepunch.Steamworks.Test/bin/Debug/Newtonsoft.Json.dll differ diff --git a/Facepunch.Steamworks.Test/bin/Release/Newtonsoft.Json.dll b/Facepunch.Steamworks.Test/bin/Release/Newtonsoft.Json.dll new file mode 100644 index 0000000..5523f5c Binary files /dev/null and b/Facepunch.Steamworks.Test/bin/Release/Newtonsoft.Json.dll differ diff --git a/Facepunch.Steamworks.sln b/Facepunch.Steamworks.sln index 95fbbc6..7974e3c 100644 --- a/Facepunch.Steamworks.sln +++ b/Facepunch.Steamworks.sln @@ -1,11 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +VisualStudioVersion = 15.0.26730.12 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks.NetCore", "Facepunch.Steamworks\Facepunch.Steamworks.NetCore.csproj", "{91962664-EB42-472A-94C8-C4FFEB44CC4B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Facepunch.Steamworks", "Facepunch.Steamworks\Facepunch.Steamworks.csproj", "{DC2D9FA9-F005-468F-8581-85C79F4E0034}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Facepunch.Steamworks", "Facepunch.Steamworks\Facepunch.Steamworks.csproj", "{91962664-EB42-472A-94C8-C4FFEB44CC4B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Facepunch.Steamworks.Test", "Facepunch.Steamworks.Test\Facepunch.Steamworks.Test.csproj", "{3F6183AD-D966-44F2-A6EB-42E61E591B49}" EndProject @@ -21,10 +19,6 @@ Global {91962664-EB42-472A-94C8-C4FFEB44CC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {91962664-EB42-472A-94C8-C4FFEB44CC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {91962664-EB42-472A-94C8-C4FFEB44CC4B}.Release|Any CPU.Build.0 = Release|Any CPU - {DC2D9FA9-F005-468F-8581-85C79F4E0034}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DC2D9FA9-F005-468F-8581-85C79F4E0034}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC2D9FA9-F005-468F-8581-85C79F4E0034}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DC2D9FA9-F005-468F-8581-85C79F4E0034}.Release|Any CPU.Build.0 = Release|Any CPU {3F6183AD-D966-44F2-A6EB-42E61E591B49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3F6183AD-D966-44F2-A6EB-42E61E591B49}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F6183AD-D966-44F2-A6EB-42E61E591B49}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -37,4 +31,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {506FC2EC-38D1-45E2-BAE8-D61584162F7D} + EndGlobalSection EndGlobal diff --git a/Facepunch.Steamworks/BaseSteamworks.cs b/Facepunch.Steamworks/BaseSteamworks.cs index 435890c..bc26cda 100644 --- a/Facepunch.Steamworks/BaseSteamworks.cs +++ b/Facepunch.Steamworks/BaseSteamworks.cs @@ -111,7 +111,13 @@ public void UpdateWhile( Func func ) while ( func() ) { Update(); - System.Threading.Thread.Sleep( 1 ); + + const int waitPeriodMillis = 1; +#if NETCORE + System.Threading.Tasks.Task.Delay( waitPeriodMillis ).Wait(); +#else + System.Threading.Thread.Sleep( waitPeriodMillis ); +#endif } } } diff --git a/Facepunch.Steamworks/Client.cs b/Facepunch.Steamworks/Client.cs index ecf1bfa..32419bd 100644 --- a/Facepunch.Steamworks/Client.cs +++ b/Facepunch.Steamworks/Client.cs @@ -55,6 +55,7 @@ public partial class Client : BaseSteamworks public Voice Voice { get; private set; } public ServerList ServerList { get; private set; } + public LobbyList LobbyList { get; private set; } public App App { get; private set; } public Achievements Achievements { get; private set; } public Stats Stats { get; private set; } @@ -64,6 +65,11 @@ public partial class Client : BaseSteamworks public Client( uint appId ) { + if ( Instance != null ) + { + throw new System.Exception( "Only one Facepunch.Steamworks.Client can exist - dispose the old one before trying to create a new one." ); + } + Instance = this; native = new Interop.NativeInterface(); @@ -88,6 +94,7 @@ public Client( uint appId ) // Voice = new Voice( this ); ServerList = new ServerList( this ); + LobbyList = new LobbyList(this); App = new App( this ); Stats = new Stats( this ); Achievements = new Achievements( this ); @@ -159,6 +166,12 @@ public override void Dispose() ServerList = null; } + if (LobbyList != null) + { + LobbyList.Dispose(); + LobbyList = null; + } + if ( App != null ) { App.Dispose(); diff --git a/Facepunch.Steamworks/Client/Lobby.cs b/Facepunch.Steamworks/Client/Lobby.cs new file mode 100644 index 0000000..8f7e09e --- /dev/null +++ b/Facepunch.Steamworks/Client/Lobby.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.Generic; +using System.Text; +using SteamNative; + +namespace Facepunch.Steamworks +{ + public partial class Client : IDisposable + { + Lobby _lobby; + + public Lobby Lobby + { + get + { + if (_lobby == null) + _lobby = new Steamworks.Lobby(this); + return _lobby; + } + } + } + public 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, + 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) + { + 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); + } + + /// + /// The CSteamID of the lobby we're currently in. + /// + public ulong CurrentLobby { get; private set; } + + /// + /// The LobbyData of the CurrentLobby. Note this is the global data for the lobby. Use SetMemberData to set specific member data. + /// + public LobbyData CurrentLobbyData { get; private set; } + + /// + /// Returns true if this lobby is valid, ie, we've succesffuly created and/or joined a lobby. + /// + public bool IsValid => CurrentLobby != 0; + + /// + /// Join a Lobby through its LobbyID. OnLobbyJoined is called with the result of the Join attempt. + /// + /// CSteamID of lobby to join + public void Join(ulong lobbyID) + { + Leave(); + client.native.matchmaking.JoinLobby(lobbyID, OnLobbyJoinedAPI); + } + + void OnLobbyJoinedAPI(LobbyEnter_t callback, bool error) + { + if (error || (callback.EChatRoomEnterResponse != (uint)(SteamNative.ChatRoomEnterResponse.Success))) + { + if (OnLobbyJoined != null) { OnLobbyJoined(false); } + return; + } + + CurrentLobby = callback.SteamIDLobby; + UpdateLobbyData(); + if (OnLobbyJoined != null) { OnLobbyJoined(true); } + } + + /// + /// Called when a lobby has been attempted joined. Returns true if lobby was successfuly joined, false if not. + /// + public Action OnLobbyJoined; + + /// + /// Creates a lobby and returns the created lobby. You auto join the created lobby. The lobby is stored in Client.CurrentLobby if successful. + /// + /// The Lobby.Type of Lobby to be created + /// The maximum amount of people you want to be able to be in this lobby, including yourself + public void Create(Lobby.Type lobbyType, int maxMembers) + { + client.native.matchmaking.CreateLobby((SteamNative.LobbyType)lobbyType, maxMembers, OnLobbyCreatedAPI); + createdLobbyType = lobbyType; + } + + internal Type createdLobbyType; + + internal void OnLobbyCreatedAPI(LobbyCreated_t callback, bool error) + { + //from SpaceWarClient.cpp 793 + if (error || (callback.Result != Result.OK)) + { + if ( OnLobbyCreated != null) { OnLobbyCreated(false); } + return; + } + + //set owner specific properties + Owner = client.SteamId; + CurrentLobby = callback.SteamIDLobby; + CurrentLobbyData = new LobbyData(client, CurrentLobby); + Name = client.Username + "'s Lobby"; + CurrentLobbyData.SetData("appid", client.AppId.ToString()); + LobbyType = createdLobbyType; + CurrentLobbyData.SetData("lobbytype", LobbyType.ToString()); + Joinable = true; + if (OnLobbyCreated != null) { OnLobbyCreated(true); } + } + + /// + /// Callback for when lobby is created. Parameter resolves true when the Lobby was successfully created + /// + public Action OnLobbyCreated; + + /// + /// Class to hold global lobby data. This is stuff like maps/modes/etc. Data set here can be filtered by LobbyList. + /// + public class LobbyData + { + internal Client client; + internal ulong lobby; + internal Dictionary data; + + public LobbyData(Client c, ulong l) + { + client = c; + lobby = l; + data = new Dictionary(); + } + + /// + /// Get the lobby value for the specific key + /// + /// The key to find + /// The value at key + public string GetData(string k) + { + if (data.ContainsKey(k)) + { + return data[k]; + } + + return "ERROR: key not found"; + } + + /// + /// Get a list of all the data in the Lobby + /// + /// Dictionary of all the key/value pairs in the data + public Dictionary GetAllData() + { + Dictionary returnData = new Dictionary(); + foreach(KeyValuePair item in data) + { + returnData.Add(item.Key, item.Value); + } + return returnData; + } + + /// + /// Set the value for specified Key. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use. + /// + /// The key to set the value for + /// The value of the Key + /// True if data successfully set + 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; + } + + /// + /// Remove the key from the LobbyData. Note that the keys "joinable", "appid", "name", and "lobbytype" are reserved for internal library use. + /// + /// The key to remove + /// True if Key successfully removed + public bool RemoveData(string k) + { + if (data.ContainsKey(k)) + { + if (client.native.matchmaking.DeleteLobbyData(lobby, k)) + { + data.Remove(k); + return true; + } + } + + return false; + } + + } + + /// + /// Sets user data for the Lobby. Things like Character, Skin, Ready, etc. Can only set your own member data + /// + public void SetMemberData(string key, string value) + { + if(CurrentLobby == 0) { return; } + client.native.matchmaking.SetLobbyMemberData(CurrentLobby, key, value); + } + + /// + /// Get the per-user metadata from this lobby. Can get data from any user + /// + /// ulong SteamID of the user you want to get data from + /// String key of the type of data you want to get + /// + public string GetMemberData(ulong steamID, string 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) + { + if(error || (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 (OnLobbyMemberDataUpdated != null) { OnLobbyMemberDataUpdated(callback.SteamIDMember); } + } + } + + /// + /// Updates the LobbyData property to have the data for the current lobby, if any + /// + internal void UpdateLobbyData() + { + 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)) + { + CurrentLobbyData.SetData(key, value); + } + } + + if(OnLobbyDataUpdated != null) { OnLobbyDataUpdated(); } + } + + /// + /// Called when the lobby data itself has been updated. Called when someone has joined/left, Owner has updated data, etc. + /// + public Action OnLobbyDataUpdated; + + /// + /// Called when a member of the lobby has updated either their personal Lobby metadata or someone's global steam state has changed (like a display name). Parameter is the user who changed. + /// + public Action OnLobbyMemberDataUpdated; + + + public Type LobbyType + { + get + { + 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) + { + case "Private": + return Type.Private; + case "FriendsOnly": + return Type.FriendsOnly; + case "Invisible": + return Type.Invisible; + case "Public": + return Type.Public; + default: + return Type.Error; + } + } + set + { + if(!IsValid) { return; } + if(client.native.matchmaking.SetLobbyType(CurrentLobby, (SteamNative.LobbyType)value)) + { + CurrentLobbyData.SetData("lobbytype", value.ToString()); + } + } + } + + private unsafe void OnLobbyChatMessageRecievedAPI(LobbyChatMsg_t callback, bool error) + { + //from Client.Networking + if(error || callback.SteamIDLobby != CurrentLobby) { return; } + + byte[] ReceiveBuffer = new byte[1024]; + SteamNative.CSteamID steamid = 1; + ChatEntryType chatEntryType; //not used + int readData = 0; + fixed (byte* p = ReceiveBuffer) + { + readData = client.native.matchmaking.GetLobbyChatEntry(CurrentLobby, (int)callback.ChatID, out steamid, (IntPtr)p, ReceiveBuffer.Length, out chatEntryType); + while (ReceiveBuffer.Length < readData) + { + ReceiveBuffer = new byte[readData + 1024]; + readData = client.native.matchmaking.GetLobbyChatEntry(CurrentLobby, (int)callback.ChatID, out steamid, (IntPtr)p, ReceiveBuffer.Length, out chatEntryType); + } + + } + + if (OnChatMessageRecieved != null) { OnChatMessageRecieved(steamid, ReceiveBuffer, readData); } + + } + + /// + /// Callback to get chat messages. Use Encoding.UTF8.GetString to retrive the message. + /// + public Action OnChatMessageRecieved; + + /// + /// 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. + /// + /// True if message successfully sent + public unsafe bool SendChatMessage(string message) + { + 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); + } + } + + /// + /// Enums to catch the state of a user when their state has changed + /// + public enum MemberStateChange + { + Entered = ChatMemberStateChange.Entered, + Left = ChatMemberStateChange.Left, + Disconnected = ChatMemberStateChange.Disconnected, + Kicked = ChatMemberStateChange.Kicked, + Banned = ChatMemberStateChange.Banned, + } + + internal void OnLobbyStateUpdatedAPI(LobbyChatUpdate_t callback, bool error) + { + if (error || callback.SteamIDLobby != CurrentLobby) { return; } + MemberStateChange change = (MemberStateChange)callback.GfChatMemberStateChange; + ulong initiator = callback.SteamIDMakingChange; + ulong affected = callback.SteamIDUserChanged; + + if (OnLobbyStateChanged != null) { OnLobbyStateChanged(change, initiator, affected); } + } + + /// + /// Called when the state of the Lobby is somehow shifted. Usually when someone joins or leaves the lobby. + /// The first ulong is the SteamID of the user that initiated the change. + /// The second ulong is the person that was affected + /// + public Action OnLobbyStateChanged; + + /// + /// The name of the lobby as a property for easy getting/setting. Note that this is setting LobbyData, which you cannot do unless you are the Owner of the lobby + /// + public string Name + { + get + { + if (!IsValid) { return ""; } + return CurrentLobbyData.GetData("name"); + } + set + { + if (!IsValid) { return; } + CurrentLobbyData.SetData("name", value); + } + } + + /// + /// The Owner of the current lobby. Returns 0 if you are not in a valid lobby. + /// + public ulong Owner + { + get + { + if (IsValid) + { + return client.native.matchmaking.GetLobbyOwner(CurrentLobby); + } + return 0; + } + set + { + if (Owner == value) return; + client.native.matchmaking.SetLobbyOwner(CurrentLobby, value); + } + } + + /// + /// Is the Lobby joinable by other people? Defaults to true; + /// + public bool Joinable + { + get + { + if (!IsValid) { return false; } + string joinable = CurrentLobbyData.GetData("joinable"); + switch (joinable) + { + case "true": + return true; + case "false": + return false; + default: + return false; + } + } + set + { + if (!IsValid) { return; } + if (client.native.matchmaking.SetLobbyJoinable(CurrentLobby, value)) + { + CurrentLobbyData.SetData("joinable", value.ToString()); + } + } + } + + /// + /// How many people can be in the Lobby + /// + 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); + } + set + { + if (!IsValid) { return; } + client.native.matchmaking.SetLobbyMemberLimit(CurrentLobby, value); + } + } + + /// + /// How many people are currently in the Lobby + /// + public int NumMembers + { + get { return client.native.matchmaking.GetNumLobbyMembers(CurrentLobby);} + } + + /// + /// Leave the CurrentLobby. + /// + public void Leave() + { + if (CurrentLobby != 0) { client.native.matchmaking.LeaveLobby(CurrentLobby); } + CurrentLobby = 0; + CurrentLobbyData = null; + } + + public void Dispose() + { + client = null; + } + + /// + /// Get an array of all the CSteamIDs in the CurrentLobby. + /// Note that you must be in the Lobby you are trying to request the MemberIDs from. + /// Returns an empty array if you aren't in a lobby. + /// + /// Array of member SteamIDs + public ulong[] GetMemberIDs() + { + + ulong[] memIDs = new ulong[NumMembers]; + for (int i = 0; i < NumMembers; i++) + { + memIDs[i] = client.native.matchmaking.GetLobbyMemberByIndex(CurrentLobby, i); + } + return memIDs; + } + + /// + /// Check to see if a user is in your CurrentLobby + /// + /// SteamID of the user to check for + /// + public bool UserIsInCurrentLobby(ulong steamID) + { + if(CurrentLobby == 0) { return false; } + ulong[] mems = GetMemberIDs(); + for (int i = 0; i < mems.Length; i++) + { + if(mems[i] == steamID) + { + return true; + } + } + + return false; + } + + /// + /// Invites the specified user to the CurrentLobby the user is in. + /// + /// ulong ID of person to invite + public bool InviteUserToLobby(ulong friendID) + { + return client.native.matchmaking.InviteUserToLobby(CurrentLobby, friendID); + } + + internal void OnUserInvitedToLobbyAPI(LobbyInvite_t callback, bool error) + { + if (error || (callback.GameID != client.AppId)) { return; } + if (OnUserInvitedToLobby != null) { OnUserInvitedToLobby(callback.SteamIDLobby, callback.SteamIDUser); } + + } + + /// + /// 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 + /// + public Action OnUserInvitedToLobby; + + /// + /// Joins a lobby if a request was made to join the lobby through the friends list or an invite + /// + internal void OnLobbyJoinRequestedAPI(GameLobbyJoinRequested_t callback, bool error) + { + if (error) { return; } + Join(callback.SteamIDLobby); + } + + /// + /// Makes sure we send an update callback if a Lobby user updates their information + /// + internal void OnLobbyMemberPersonaChangeAPI(PersonaStateChange_t callback, bool error) + { + if (error || !UserIsInCurrentLobby(callback.SteamID)) { return; } + if (OnLobbyMemberDataUpdated != null) { OnLobbyMemberDataUpdated(callback.SteamID); } + } + + /*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 + */ + } +} diff --git a/Facepunch.Steamworks/Client/LobbyList.Lobby.cs b/Facepunch.Steamworks/Client/LobbyList.Lobby.cs new file mode 100644 index 0000000..e16dab2 --- /dev/null +++ b/Facepunch.Steamworks/Client/LobbyList.Lobby.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Facepunch.Steamworks +{ + public partial class LobbyList + { + public class Lobby + { + internal Client Client; + public string Name { get; private set; } + public ulong LobbyID { get; private set; } + public ulong Owner { get; private set; } + public int MemberLimit{ get; private set; } + public int NumMembers{ get; private set; } + + internal static Lobby FromSteam(Client client, ulong lobby) + { + return new Lobby() + { + Client = client, + LobbyID = lobby, + Name = client.native.matchmaking.GetLobbyData(lobby, "name"), + MemberLimit = client.native.matchmaking.GetLobbyMemberLimit(lobby), + Owner = client.native.matchmaking.GetLobbyOwner(lobby), + NumMembers = client.native.matchmaking.GetNumLobbyMembers(lobby) + }; + + } + } + } +} diff --git a/Facepunch.Steamworks/Client/LobbyList.cs b/Facepunch.Steamworks/Client/LobbyList.cs new file mode 100644 index 0000000..905695d --- /dev/null +++ b/Facepunch.Steamworks/Client/LobbyList.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Text; +using SteamNative; + +namespace Facepunch.Steamworks +{ + public partial class LobbyList : IDisposable + { + internal Client client; + + //The list of retrieved lobbies + public List Lobbies { get; private set; } + + //True when all the possible lobbies have had their data updated + //if the number of lobbies is now equal to the initial request number, we've found all lobbies + public bool Finished { get; private set; } + + //The number of possible lobbies we can get data from + internal List requests; + + internal LobbyList(Client client) + { + this.client = client; + Lobbies = new List(); + requests = new List(); + } + + /// + /// Refresh the List of Lobbies. If no filter is passed in, a default one is created that filters based on AppId ("appid"). + /// + /// + public void Refresh ( Filter filter = null) + { + //init out values + Lobbies.Clear(); + requests.Clear(); + Finished = false; + + if (filter == null) + { + filter = new Filter(); + filter.StringFilters.Add("appid", client.AppId.ToString()); + } + + client.native.matchmaking.AddRequestLobbyListDistanceFilter((SteamNative.LobbyDistanceFilter)filter.DistanceFilter); + + if (filter.SlotsAvailable != null) + { + client.native.matchmaking.AddRequestLobbyListFilterSlotsAvailable((int)filter.SlotsAvailable); + } + + if (filter.MaxResults != null) + { + client.native.matchmaking.AddRequestLobbyListResultCountFilter((int)filter.MaxResults); + } + + foreach (KeyValuePair fil in filter.StringFilters) + { + client.native.matchmaking.AddRequestLobbyListStringFilter(fil.Key, fil.Value, SteamNative.LobbyComparison.Equal); + } + foreach (KeyValuePair fil in filter.NearFilters) + { + client.native.matchmaking.AddRequestLobbyListNearValueFilter(fil.Key, fil.Value); + } + //foreach (KeyValuePair> fil in filter.NumericalFilters) + //{ + // client.native.matchmaking.AddRequestLobbyListNumericalFilter(fil.Key, fil.Value.Value, (SteamNative.LobbyComparison)fil.Value.Key); + //} + + + // this will never return lobbies that are full (via the actual api) + client.native.matchmaking.RequestLobbyList(OnLobbyList); + + } + + + void OnLobbyList(LobbyMatchList_t callback, bool error) + { + if (error) return; + + //how many lobbies matched + uint lobbiesMatching = callback.LobbiesMatching; + + // lobbies are returned in order of closeness to the user, so add them to the list in that order + for (int i = 0; i < lobbiesMatching; i++) + { + //add the lobby to the list of requests + ulong lobby = client.native.matchmaking.GetLobbyByIndex(i); + requests.Add(lobby); + + //cast to a LobbyList.Lobby + Lobby newLobby = Lobby.FromSteam(client, lobby); + if (newLobby.Name != "") + { + //if the lobby is valid add it to the valid return lobbies + Lobbies.Add(newLobby); + checkFinished(); + } + else + { + //else we need to get the info for the missing lobby + client.native.matchmaking.RequestLobbyData(lobby); + SteamNative.LobbyDataUpdate_t.RegisterCallback(client, OnLobbyDataUpdated); + } + + } + + checkFinished(); + + if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); } + } + + void checkFinished() + { + if (Lobbies.Count == requests.Count) + { + Finished = true; + return; + } + Finished = false; + } + + void OnLobbyDataUpdated(LobbyDataUpdate_t callback, bool error) + { + if (callback.Success == 1) //1 if success, 0 if failure + { + //find the lobby that has been updated + Lobby lobby = Lobbies.Find(x => x.LobbyID == callback.SteamIDLobby); + + //if this lobby isn't yet in the list of lobbies, we know that we should add it + if (lobby == null) + { + Lobbies.Add(lobby); + checkFinished(); + } + + //otherwise lobby data in general was updated and you should listen to see what changed + if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); } + } + + + } + + public Action OnLobbiesUpdated; + + public void Dispose() + { + client = null; + } + + public class Filter + { + // Filters that match actual metadata keys exactly + public Dictionary StringFilters = new Dictionary(); + // Filters that are of string key and int value for that key to be close to + public Dictionary NearFilters = new Dictionary(); + //Filters that are of string key and int value, with a comparison filter to say how we should relate to the value + //public Dictionary> NumericalFilters = new Dictionary>(); + public Distance DistanceFilter = Distance.Worldwide; + public int? SlotsAvailable { get; set; } + public int? MaxResults { get; set; } + + public enum Distance : int + { + Close = SteamNative.LobbyDistanceFilter.Close, + Default = SteamNative.LobbyDistanceFilter.Default, + Far = SteamNative.LobbyDistanceFilter.Far, + Worldwide = SteamNative.LobbyDistanceFilter.Worldwide + } + + public enum Comparison : int + { + EqualToOrLessThan = SteamNative.LobbyComparison.EqualToOrLessThan, + LessThan = SteamNative.LobbyComparison.LessThan, + Equal = SteamNative.LobbyComparison.Equal, + GreaterThan = SteamNative.LobbyComparison.GreaterThan, + EqualToOrGreaterThan = SteamNative.LobbyComparison.EqualToOrGreaterThan, + NotEqual = SteamNative.LobbyComparison.NotEqual + } + } + } +} diff --git a/Facepunch.Steamworks/Client/ServerList.Request.cs b/Facepunch.Steamworks/Client/ServerList.Request.cs index 503ef1f..f3da876 100644 --- a/Facepunch.Steamworks/Client/ServerList.Request.cs +++ b/Facepunch.Steamworks/Client/ServerList.Request.cs @@ -91,6 +91,8 @@ internal bool Update( SteamNative.SteamMatchmakingServers servers, Action OnServerResponded; + public Action OnFinished; /// /// A list of servers that responded. If you're only interested in servers that responded since you @@ -181,6 +183,8 @@ private void Update() { Finished = true; client.OnUpdate -= Update; + + OnFinished?.Invoke(); } } @@ -191,7 +195,10 @@ private void OnServer( SteamNative.gameserveritem_t info ) if ( Filter != null && !Filter.Test( info ) ) return; - Responded.Add( Server.FromSteam( client, info ) ); + var s = Server.FromSteam( client, info ); + Responded.Add( s ); + + OnServerResponded?.Invoke( s ); } else { diff --git a/Facepunch.Steamworks/Client/ServerList.Server.cs b/Facepunch.Steamworks/Client/ServerList.Server.cs index 473de8a..a750f2d 100644 --- a/Facepunch.Steamworks/Client/ServerList.Server.cs +++ b/Facepunch.Steamworks/Client/ServerList.Server.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Runtime.InteropServices; using System.Text; @@ -25,7 +26,7 @@ public class Server public int Version { get; set; } public string[] Tags { get; set; } public ulong SteamId { get; set; } - public uint Address { get; set; } + public IPAddress Address { get; set; } public int ConnectionPort { get; set; } public int QueryPort { get; set; } @@ -42,27 +43,13 @@ public bool Favourite internal Client Client; - public string AddressString - { - get - { - return string.Format( "{0}.{1}.{2}.{3}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul ); - } - } - public string ConnectionAddress - { - get - { - return string.Format( "{0}.{1}.{2}.{3}:{4}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul, ConnectionPort ); - } - } internal static Server FromSteam( Client client, SteamNative.gameserveritem_t item ) { return new Server() { Client = client, - Address = item.NetAdr.IP, + Address = Utility.Int32ToIp( item.NetAdr.IP ), ConnectionPort = item.NetAdr.ConnectionPort, QueryPort = item.NetAdr.QueryPort, Name = item.ServerName, @@ -133,7 +120,7 @@ internal void OnServerRulesReceiveFinished( bool Success ) /// public void AddToHistory() { - Client.native.matchmaking.AddFavoriteGame( AppId, Address, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Utility.Epoch.Current ); + Client.native.matchmaking.AddFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory, (uint)Utility.Epoch.Current ); Client.ServerList.UpdateFavouriteList(); } @@ -142,7 +129,7 @@ public void AddToHistory() /// public void RemoveFromHistory() { - Client.native.matchmaking.RemoveFavoriteGame( AppId, Address, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); + Client.native.matchmaking.RemoveFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagHistory ); Client.ServerList.UpdateFavouriteList(); } @@ -151,7 +138,7 @@ public void RemoveFromHistory() /// public void AddToFavourites() { - Client.native.matchmaking.AddFavoriteGame( AppId, Address, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Utility.Epoch.Current ); + Client.native.matchmaking.AddFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite, (uint)Utility.Epoch.Current ); Client.ServerList.UpdateFavouriteList(); } @@ -160,7 +147,7 @@ public void AddToFavourites() /// public void RemoveFromFavourites() { - Client.native.matchmaking.RemoveFavoriteGame( AppId, Address, (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); + Client.native.matchmaking.RemoveFavoriteGame( AppId, Utility.IpToInt32( Address ), (ushort)ConnectionPort, (ushort)QueryPort, k_unFavoriteFlagFavorite ); Client.ServerList.UpdateFavouriteList(); } } diff --git a/Facepunch.Steamworks/Client/ServerList.cs b/Facepunch.Steamworks/Client/ServerList.cs index 09f4149..407d98f 100644 --- a/Facepunch.Steamworks/Client/ServerList.cs +++ b/Facepunch.Steamworks/Client/ServerList.cs @@ -248,7 +248,7 @@ public Request Local( Filter filter = null ) internal bool IsFavourite( Server server ) { - ulong encoded = server.Address; + ulong encoded = Utility.IpToInt32( server.Address ); encoded = encoded << 32; encoded = encoded | (uint)server.ConnectionPort; @@ -257,7 +257,7 @@ internal bool IsFavourite( Server server ) internal bool IsHistory( Server server ) { - ulong encoded = server.Address; + ulong encoded = Utility.IpToInt32( server.Address ); encoded = encoded << 32; encoded = encoded | (uint)server.ConnectionPort; diff --git a/Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj b/Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj deleted file mode 100644 index 998525d..0000000 --- a/Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - netstandard1.6 - true - true - Facepunch.Steamworks - Facepunch.Steamworks - 1.6.0 - $(PackageTargetFallback);dnxcore50 - Facepunch.Steamworks - - False - - - - TRACE;DEBUG;NETSTANDARD1_6;NETCORE - 1701;1702;1705;618;1591 - - - - TRACE;RELEASE;NETSTANDARD1_6;NETCORE - 1701;1702;1705;618;1591 - - - - - - - diff --git a/Facepunch.Steamworks/Facepunch.Steamworks.csproj b/Facepunch.Steamworks/Facepunch.Steamworks.csproj index 4953776..58d73da 100644 --- a/Facepunch.Steamworks/Facepunch.Steamworks.csproj +++ b/Facepunch.Steamworks/Facepunch.Steamworks.csproj @@ -1,23 +1,40 @@  - net3.5 + + netstandard2.0;net45;net35;net40 true - true Facepunch.Steamworks - Facepunch.Steamworks - $(PackageTargetFallback);dnxcore50 - Facepunch.Steamworks + + true + false + - TRACE;DEBUG;NETSTANDARD1_6;NETCORE + TRACE;DEBUG 1701;1702;1705;618;1591 - TRACE;RELEASE;NETSTANDARD1_6;NETCORE + TRACE;RELEASE 1701;1702;1705;618;1591 + + $(DefineConstants);NETCORE + + + + + + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v3.5\Profile\Client + + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client + + diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs index 213d2e3..383f3d6 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.Editor.cs @@ -25,6 +25,7 @@ public class Editor public ItemType? Type { get; set; } public string Error { get; internal set; } = null; public string ChangeNote { get; set; } = ""; + public uint WorkshopUploadAppId { get; set; } public enum VisibilityType : int { @@ -113,8 +114,7 @@ private void StartCreatingItem() if ( !Type.HasValue ) throw new System.Exception( "Editor.Type must be set when creating a new item!" ); - System.Diagnostics.Debug.WriteLine( "StartCreatingItem()" ); - CreateItem = workshop.ugc.CreateItem( workshop.steamworks.AppId, (SteamNative.WorkshopFileType)(uint)Type, OnItemCreated ); + CreateItem = workshop.ugc.CreateItem( WorkshopUploadAppId, (SteamNative.WorkshopFileType)(uint)Type, OnItemCreated ); } private void OnItemCreated( SteamNative.CreateItemResult_t obj, bool Failed ) @@ -140,7 +140,7 @@ private void OnItemCreated( SteamNative.CreateItemResult_t obj, bool Failed ) private void PublishChanges() { - UpdateHandle = workshop.ugc.StartItemUpdate( workshop.steamworks.AppId, Id ); + UpdateHandle = workshop.ugc.StartItemUpdate(WorkshopUploadAppId, Id ); if ( Title != null ) workshop.ugc.SetItemTitle( UpdateHandle, Title ); diff --git a/Facepunch.Steamworks/Interfaces/Workshop.Query.cs b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs index 3b1dbd6..c31437a 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.Query.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.Query.cs @@ -267,7 +267,12 @@ public void Block() while ( IsRunning ) { - System.Threading.Thread.Sleep( 10 ); + const int waitPeriodMillis = 10; +#if NETCORE + System.Threading.Tasks.Task.Delay( waitPeriodMillis ).Wait(); +#else + System.Threading.Thread.Sleep( waitPeriodMillis ); +#endif workshop.steamworks.Update(); } } diff --git a/Facepunch.Steamworks/Interfaces/Workshop.cs b/Facepunch.Steamworks/Interfaces/Workshop.cs index e2423ca..cbb1a24 100644 --- a/Facepunch.Steamworks/Interfaces/Workshop.cs +++ b/Facepunch.Steamworks/Interfaces/Workshop.cs @@ -151,9 +151,20 @@ public Query CreateQuery() /// public Editor CreateItem( ItemType type ) { - return new Editor() { workshop = this, Type = type }; + return CreateItem(this.steamworks.AppId, type); } - + + /// + /// Create a new Editor object with the intention of creating a new item. + /// Your item won't actually be created until you call Publish() on the object. + /// Your item will be published to the provided appId. + /// + /// You need to add app publish permissions for cross app uploading to work. + public Editor CreateItem( uint workshopUploadAppId, ItemType type ) + { + return new Editor() { workshop = this, WorkshopUploadAppId = workshopUploadAppId, Type = type }; + } + /// /// Returns a class representing this ItemId. We don't query /// item name, description etc. We don't verify that item exists. diff --git a/Facepunch.Steamworks/Interop/Native.cs b/Facepunch.Steamworks/Interop/Native.cs index f79b74b..8ac3d77 100644 --- a/Facepunch.Steamworks/Interop/Native.cs +++ b/Facepunch.Steamworks/Interop/Native.cs @@ -39,22 +39,34 @@ internal bool InitClient( BaseSteamworks steamworks ) api = new SteamNative.SteamApi(); if ( !api.SteamAPI_Init() ) + { + Console.Error.WriteLine( "InitClient: SteamAPI_Init returned false" ); return false; + } hUser = api.SteamAPI_GetHSteamUser(); hPipe = api.SteamAPI_GetHSteamPipe(); if ( hPipe == 0 ) + { + Console.Error.WriteLine( "InitClient: hPipe == 0" ); return false; + } FillInterfaces( steamworks, hUser, hPipe ); if ( !user.IsValid ) + { + Console.Error.WriteLine( "InitClient: ISteamUser is null" ); return false; + } // Ensure that the user has logged into Steam. This will always return true if the game is launched // from Steam, but if Steam is at the login prompt when you run your game it will return false. if ( !user.BLoggedOn() ) + { + Console.Error.WriteLine( "InitClient: Not Logged On" ); return false; + } return true; } @@ -67,13 +79,17 @@ internal bool InitServer( BaseSteamworks steamworks, uint IpAddress /*uint32*/, if ( !api.SteamInternal_GameServer_Init( IpAddress, usPort, GamePort, QueryPort, eServerMode, pchVersionString ) ) { + Console.Error.WriteLine( "InitServer: GameServer_Init returned false" ); return false; } hUser = api.SteamGameServer_GetHSteamUser(); hPipe = api.SteamGameServer_GetHSteamPipe(); if ( hPipe == 0 ) + { + Console.Error.WriteLine( "InitServer: hPipe == 0" ); return false; + } FillInterfaces( steamworks, hPipe, hUser ); @@ -116,27 +132,6 @@ public void FillInterfaces( BaseSteamworks steamworks, int hpipe, int huser ) public void Dispose() { - if ( client != null ) - { - if ( hPipe != 0 ) - { - if ( hUser != 0 ) - { - client.ReleaseUser( hPipe, hUser ); - hUser = 0; - } - - client.BReleaseSteamPipe( hPipe ); - hPipe = 0; - } - - if ( !client.BShutdownIfAllPipesClosed() ) - Console.WriteLine( "BShutdownIfAllPipesClosed returned false" ); - - client.Dispose(); - client = null; - } - if ( user != null ) { user.Dispose(); @@ -186,12 +181,7 @@ public void Dispose() } if ( gameServer != null ) - { - // - // Calling this can cause the process to hang - // - //gameServer.LogOff(); - + { gameServer.Dispose(); gameServer = null; } @@ -232,6 +222,12 @@ public void Dispose() applist = null; } + if ( client != null ) + { + client.Dispose(); + client = null; + } + if ( api != null ) { if ( isServer ) @@ -239,6 +235,16 @@ public void Dispose() else api.SteamAPI_Shutdown(); + // + // The functions above destroy the pipeline handles + // and all of the classes. Trying to call a steam function + // at this point will result in a crash - because any + // pointers we stored are not invalid. + // + + hPipe = 0; + hUser = 0; + api.Dispose(); api = null; } diff --git a/Facepunch.Steamworks/Interop/ServerRules.cs b/Facepunch.Steamworks/Interop/ServerRules.cs index 78b8610..2cdb16a 100644 --- a/Facepunch.Steamworks/Interop/ServerRules.cs +++ b/Facepunch.Steamworks/Interop/ServerRules.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Runtime.InteropServices; using System.Text; @@ -20,7 +21,7 @@ class ServerRules : IDisposable // The server that called us private ServerList.Server Server; - public ServerRules( ServerList.Server server, uint address, int queryPort ) + public ServerRules( ServerList.Server server, IPAddress address, int queryPort ) { Server = server; @@ -32,7 +33,7 @@ public ServerRules( ServerList.Server server, uint address, int queryPort ) // // Ask Steam to get the server rules, respond to our fake vtable // - Server.Client.native.servers.ServerRules( address, (ushort)queryPort, GetPtr() ); + Server.Client.native.servers.ServerRules( Utility.IpToInt32( address ), (ushort)queryPort, GetPtr() ); } public void Dispose() diff --git a/Facepunch.Steamworks/Server.cs b/Facepunch.Steamworks/Server.cs index da274e0..01c21d4 100644 --- a/Facepunch.Steamworks/Server.cs +++ b/Facepunch.Steamworks/Server.cs @@ -26,6 +26,11 @@ public partial class Server : BaseSteamworks /// public Server( uint appId, ServerInit init ) { + if ( Instance != null ) + { + throw new System.Exception( "Only one Facepunch.Steamworks.Server can exist - dispose the old one before trying to create a new one." ); + } + Instance = this; native = new Interop.NativeInterface(); diff --git a/Facepunch.Steamworks/Utility.cs b/Facepunch.Steamworks/Utility.cs index 186af20..cb3547e 100644 --- a/Facepunch.Steamworks/Utility.cs +++ b/Facepunch.Steamworks/Utility.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; namespace Facepunch.Steamworks @@ -15,6 +16,17 @@ static internal uint SwapBytes( uint x ) ( ( x & 0xff000000 ) >> 24 ); } + + static internal uint IpToInt32( this IPAddress ipAddress ) + { + return BitConverter.ToUInt32( ipAddress.GetAddressBytes().Reverse().ToArray(), 0 ); + } + + static internal IPAddress Int32ToIp( uint ipAddress ) + { + return new IPAddress( BitConverter.GetBytes( ipAddress ).Reverse().ToArray() ); + } + static internal int NextPowerOf2( int x ) { var po2 = 1; diff --git a/Jenkinsfile b/Jenkinsfile index b7ac2fa..3095d5d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,22 +4,13 @@ node ( 'vs2017' ) checkout scm stage 'Restore' - bat 'nuget restore Facepunch.Steamworks.sln' + bat "dotnet restore Facepunch.Steamworks/Facepunch.Steamworks.csproj" stage 'Build Release' - bat "\"${tool 'MSBuild'}\" Facepunch.Steamworks/Facepunch.Steamworks.csproj /p:Configuration=Release /p:ProductVersion=1.0.0.${env.BUILD_NUMBER}" + bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.csproj --configuration Release" stage 'Build Debug' - bat "\"${tool 'MSBuild'}\" Facepunch.Steamworks/Facepunch.Steamworks.csproj /p:Configuration=Debug /p:ProductVersion=1.0.0.${env.BUILD_NUMBER}" - - stage 'Build Release NetCore' - bat "dotnet restore Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj" - - stage 'Build Release NetCore' - bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj --configuration Release" - - stage 'Build Debug NetCore' - bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.NetCore.csproj --configuration Debug" + bat "dotnet build Facepunch.Steamworks/Facepunch.Steamworks.csproj --configuration Debug" stage 'Archive' archiveArtifacts artifacts: 'Facepunch.Steamworks/bin/**/*' diff --git a/README.md b/README.md index ee7bc17..30d4f9e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You can view examples of everything in the Facepunch.Steamworks.Test project. ## Client -Compile and add the library to your project. To create a client you can do this. +Compile the Facepunch.Steamworks project and add the library to your Unity project. To create a client you can do this. ```csharp var client = new Facepunch.Steamworks.Client( 252490 ); @@ -68,6 +68,34 @@ var server = new Facepunch.Steamworks.Server( 252490, 0, 28015, true, "MyGame453 This will register a secure server for game 252490, any ip, port 28015. Again, more usage in the Facepunch.Steamworks.Test project. +## Lobby + +To create a Lobby do this. +```csharp +client.Lobby.Create(Steamworks.Lobby.Type.Public, 10); +``` + +Created lobbies are auto-joined, but if you want to find a friend's lobby, you'd call +```csharp +client.LobbyList.Refresh(); +//wait for the callback +client.LobbyList.OnLobbiesUpdated = () => +{ + if (client.LobbyList.Finished) + { + foreach (LobbyList.Lobby lobby in client.LobbyList.Lobbies) + { + Console.WriteLine($"Found Lobby: {lobby.Name}"); + } + } +}; +//join a lobby you found +client.Lobby.Join(LobbyList.Lobbies[0]); +``` + +Your can find more examples of Lobby functionality in the Lobby.cs file in the test project. Sending chat messages, assinging lobby data and member data, etc. + + # Unity Yeah this works under Unity. That's half the point of it.