diff --git a/MyceliumNetworkingForCW/MyceliumNetwork.cs b/MyceliumNetworkingForCW/MyceliumNetwork.cs index 1fcf720..92328be 100644 --- a/MyceliumNetworkingForCW/MyceliumNetwork.cs +++ b/MyceliumNetworkingForCW/MyceliumNetwork.cs @@ -43,47 +43,74 @@ public static class MyceliumNetwork /// /// The current lobby /// - public static CSteamID Lobby; + public static CSteamID Lobby { get; private set; } + + /// + /// Are we in a lobby? + /// + public static bool InLobby { get; private set; } /// /// Called when a lobby is created by the local player /// - public static Action LobbyCreated; + public static event Action LobbyCreated; /// /// Called when a lobby is entered by the local player /// - public static Action LobbyEntered; + public static event Action LobbyEntered; /// /// Called when a lobby is left by the local player /// - public static Action LobbyLeft; + public static event Action LobbyLeft; /// /// Called when lobby creation has failed on the local player /// - public static Action LobbyCreationFailed; + public static event Action LobbyCreationFailed; /// /// Called a player enters the lobby /// - public static Action PlayerEntered; + public static event Action PlayerEntered; /// /// Called when a player leaves the lobby /// - public static Action PlayerLeft; + public static event Action PlayerLeft; + + /// + /// Called when a player's data is updated, or when a player is promoted to host. + /// If the new player data value is the same as the previous, this will not be called. + /// Provides the CSteamID of the player who's data was changed, and a list of the keys of player data that were changed. + /// + public static event Action> PlayerDataUpdated; + + /// + /// Called when the lobby's data is updated, when a lobby is created, joined, or when the lobby owner changes. + /// If the new lobby data value is the same as the previous, this will not be called. + /// Provides a list of the keys of lobby data that were changed. + /// + public static event Action> LobbyDataUpdated; + + static List lobbyDataKeys = new List(); + static List playerDataKeys = new List(); + + static Dictionary> lastPlayerData = new Dictionary>(); + static Dictionary lastLobbyData = new Dictionary(); static Callback _c2; static Callback _c3; static Callback _c4; + static Callback _c5; public static void Initialize() { _c2 = Callback.Create(OnLobbyEnter); _c3 = Callback.Create(OnLobbyCreated); _c4 = Callback.Create(OnLobbyChatUpdate); + _c5 = Callback.Create(OnLobbyDataUpdate); } static void OnLobbyEnter(LobbyEnter_t param) @@ -91,6 +118,7 @@ static void OnLobbyEnter(LobbyEnter_t param) RugLogger.Log($"Entering lobby {param.m_ulSteamIDLobby}"); Lobby = new CSteamID(param.m_ulSteamIDLobby); + InLobby = true; RefreshPlayerList(); @@ -128,6 +156,14 @@ static void OnLobbyChatUpdate(LobbyChatUpdate_t param) PlayerEntered?.Invoke(steamID); break; case EChatMemberStateChange.k_EChatMemberStateChangeLeft: + if(steamID == SteamUser.GetSteamID()) + { + lastLobbyData.Clear(); + lastPlayerData.Clear(); + InLobby = false; + LobbyLeft?.Invoke(); + } + break; case EChatMemberStateChange.k_EChatMemberStateChangeDisconnected: case EChatMemberStateChange.k_EChatMemberStateChangeKicked: case EChatMemberStateChange.k_EChatMemberStateChangeBanned: @@ -213,6 +249,240 @@ public static void RPC(uint modId, string methodName, ReliableType reliable, par } #endregion + #region LobbyData + public static void RegisterLobbyDataKey(string key) + { + if(lobbyDataKeys.Contains(key)) + { + RugLogger.LogError($"Lobby data key {key} is already defined"); + } + else + { + lobbyDataKeys.Add(key); + } + } + + public static void RegisterPlayerDataKey(string key) + { + if(playerDataKeys.Contains(key)) + { + RugLogger.LogError($"Player data key {key} is already defined"); + } + else + { + playerDataKeys.Add(key); + } + } + + static void OnLobbyDataUpdate(LobbyDataUpdate_t param) + { + // OnLobbyDataUpdate is also triggered by a RequestLobbyData call, so we have to check + if(!InLobby) + return; + + if(param.m_ulSteamIDLobby != Lobby.m_SteamID) + return; + + if(param.m_ulSteamIDLobby == param.m_ulSteamIDMember) // Lobby data update + { + List changedKeys = new List(); + + for(int i = 0; i < lobbyDataKeys.Count; i++) + { + string key = lobbyDataKeys[i]; + string data = SteamMatchmaking.GetLobbyData(Lobby, key); + + if(lastLobbyData.ContainsKey(key)) + { + if(!lastLobbyData[key].Equals(data)) + { + changedKeys.Add(key); + } + } + else + { + changedKeys.Add(key); + } + + lastLobbyData[key] = data; + } + + // sometimes nothing changes + if(changedKeys.Count > 0) + { + LobbyDataUpdated?.Invoke(changedKeys); + } + } + else // Player data update + { + var player = new CSteamID(param.m_ulSteamIDMember); + + if(!lastPlayerData.ContainsKey(player)) + { + lastPlayerData[player] = new Dictionary(); + } + + List changedKeys = new List(); + + for(int i = 0; i < playerDataKeys.Count; i++) + { + string key = playerDataKeys[i]; + string data = SteamMatchmaking.GetLobbyMemberData(Lobby, player, key); + + if(lastPlayerData[player].ContainsKey(key)) + { + if(!lastPlayerData[player][key].Equals(data)) + { + changedKeys.Add(key); + } + } + else + { + changedKeys.Add(key); + } + + lastPlayerData[player][key] = data; + } + + // sometimes nothing changes + if(changedKeys.Count > 0) + { + PlayerDataUpdated?.Invoke(player, changedKeys); + } + } + } + + /// + /// Assign a value to a lobby data key. Syncs for all players. Invokes LobbyDataUpdated. Can only be called by the lobby host. + /// + /// The key to set + /// The value to assign the key + public static void SetLobbyData(string key, object value) + { + if(!InLobby) + { + RugLogger.LogError("Cannot set lobby data when not in lobby."); + return; + } + + if(!SteamMatchmaking.SetLobbyData(Lobby, key, value.ToString())) + { + RugLogger.LogError("Error setting lobby data."); + } + } + + /// + /// Check if a lobby data key is defined. Can be called by any player. + /// + /// + /// True if the key is defined + public static bool HasLobbyData(string key) + { + if(!InLobby) + { + RugLogger.LogError("Cannot get lobby data when not in lobby."); + return false; + } + + string value = SteamMatchmaking.GetLobbyData(Lobby, key.ToString()); + + return !string.IsNullOrEmpty(value); + } + + /// + /// Get the value of a lobby data key for a specific lobby. Can be called by any player. + /// + /// The type of the data (ex. int, float, bool) + /// The key to get the value of + /// The value from the key + public static T GetLobbyData(string key) + { + if(!InLobby) + { + RugLogger.LogError("Cannot get lobby data when not in lobby."); + return default(T); + } + + string value = SteamMatchmaking.GetLobbyData(Lobby, key.ToString()); + + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch(Exception ex) + { + Debug.LogError($"Could not parse lobby data [{key}, {value}] as {typeof(T).Name}: {ex.Message}"); + } + + return default(T); + } + + /// + /// Assign a value to a player data key. Syncs for all players. Invokes PlayerDataUpdated. Can be called by any player to set their own data. + /// + /// The key to set + /// The value to assign the key + public static void SetPlayerData(string key, object value) + { + if(!InLobby) + { + RugLogger.LogError("Cannot set player data when not in lobby."); + return; + } + + SteamMatchmaking.SetLobbyMemberData(Lobby, key.ToString(), value.ToString()); + } + + /// + /// Check if a player data key is defined. Can be called by any player, on any player. + /// + /// + /// True if the key is defined + public static bool HasPlayerData(CSteamID player, string key) + { + if(!InLobby) + { + RugLogger.LogError("Cannot get player data when not in lobby."); + return false; + } + + string value = SteamMatchmaking.GetLobbyMemberData(MyceliumNetwork.Lobby, player, key.ToString()); + + return !string.IsNullOrEmpty(value); + } + + /// + /// Get the data associated with a key for a player. Can be called by any player, on any player. + /// Note that player data takes a few hundred miliseconds to load in before it can be accessed when a player first joins. + /// + /// The value from the key + public static T GetPlayerData(CSteamID player, string key) + { + if(!InLobby) + { + RugLogger.LogError("Cannot get player data when not in lobby."); + return default(T); + } + + string value = SteamMatchmaking.GetLobbyMemberData(Lobby, player, key.ToString()); + + // If this key has been set + if(!string.IsNullOrEmpty(value)) + { + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch(Exception ex) + { + Debug.LogError($"Could not parse [{key}, {value}] as {typeof(T).Name}: {ex.Message}"); + } + } + + return default(T); + } + #endregion + /// /// Send a byte array over the network to a specific player /// diff --git a/MyceliumNetworkingForCW/MyceliumNetworkingForCW.csproj b/MyceliumNetworkingForCW/MyceliumNetworkingForCW.csproj index 78589c2..5a2b7d8 100644 --- a/MyceliumNetworkingForCW/MyceliumNetworkingForCW.csproj +++ b/MyceliumNetworkingForCW/MyceliumNetworkingForCW.csproj @@ -38,11 +38,6 @@ latest - - - enable - - diff --git a/MyceliumNetworkingForCW/ts-assets/CHANGELOG.md b/MyceliumNetworkingForCW/ts-assets/CHANGELOG.md index dee32c4..359ff23 100644 --- a/MyceliumNetworkingForCW/ts-assets/CHANGELOG.md +++ b/MyceliumNetworkingForCW/ts-assets/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +- The LobbyLeft callback should now properly fire // NOT TESTED +- Added LobbyData and PlayerData functionality, which allows you to define synced variables associated with the lobby (perfect for config syncing) or individual players +- General code cleanup + ## [1.0.6] - Automated Thunderstore release