From 25230d89fe6d05c03741240e083f7237eca302d6 Mon Sep 17 00:00:00 2001 From: blattersturm Date: Wed, 28 Oct 2020 11:32:08 +0100 Subject: [PATCH] Improvements for working with OneSync/Infinity. This changeset abstracts away the PlayerList instance into two separate implementations: one forwarding directly to the local player list, and one which will be used if the server informs us that Infinity is indeed enabled. --- vMenu/CommonFunctions.cs | 102 +++++++++++++----- vMenu/EventManager.cs | 13 ++- vMenu/FunctionsController.cs | 50 ++------- vMenu/MainMenu.cs | 59 +++++++++- vMenu/PlayerLists.cs | 204 +++++++++++++++++++++++++++++++++++ vMenu/menus/OnlinePlayers.cs | 53 +++++---- vMenu/vMenuClient.csproj | 3 +- vMenuServer/MainServer.cs | 90 +++++++++++++++- 8 files changed, 481 insertions(+), 93 deletions(-) create mode 100644 vMenu/PlayerLists.cs diff --git a/vMenu/CommonFunctions.cs b/vMenu/CommonFunctions.cs index ff3960f5..4c210a27 100644 --- a/vMenu/CommonFunctions.cs +++ b/vMenu/CommonFunctions.cs @@ -389,27 +389,63 @@ public static async void QuitGame() /// /// /// - public static async void TeleportToPlayer(int playerId, bool inVehicle = false) + public static async Task TeleportToPlayer(IPlayer player, bool inVehicle = false) { // If the player exists. - if (NetworkIsPlayerActive(playerId)) + if (player.IsActive || player is InfinityPlayer) { - int playerPed = GetPlayerPed(playerId); - if (Game.PlayerPed.Handle == playerPed) + Vector3 playerPos; + bool wasActive = true; + + if (player.IsActive) { - Notify.Error("Sorry, you can ~r~~h~not~h~ ~s~teleport to yourself!"); - return; - } + Ped playerPedObj = player.Character; + if (Game.PlayerPed == playerPedObj) + { + Notify.Error("Sorry, you can ~r~~h~not~h~ ~s~teleport to yourself!"); + return; + } - // Get the coords of the other player. - Vector3 playerPos = GetEntityCoords(playerPed, true); + // Get the coords of the other player. + playerPos = GetEntityCoords(playerPedObj.Handle, true); + } + else + { + playerPos = await MainMenu.RequestPlayerCoordinates(player.ServerId); + wasActive = false; + } // Then await the proper loading/teleporting. await TeleportToCoords(playerPos); + // Wait until the player has been created. + while (player.Character == null) + { + await Delay(0); + } + + var playerId = player.Handle; + var playerPed = player.Character.Handle; + // If the player should be teleported inside the other player's vehcile. if (inVehicle) { + // Wait until the target player vehicle has loaded, if they weren't active beforehand. + if (!wasActive) + { + var startWait = GetGameTimer(); + + while (!IsPedInAnyVehicle(playerPed, false)) + { + await Delay(0); + + if ((GetGameTimer() - startWait) > 1500) + { + break; + } + } + } + // Is the other player inside a vehicle? if (IsPedInAnyVehicle(playerPed, false)) { @@ -690,7 +726,7 @@ public static async void TeleportToWp() /// /// /// - public static async void KickPlayer(Player player, bool askUserForReason, string providedReason = "You have been kicked.") + public static async void KickPlayer(IPlayer player, bool askUserForReason, string providedReason = "You have been kicked.") { if (player != null) { @@ -728,7 +764,7 @@ public static async void KickPlayer(Player player, bool askUserForReason, string /// /// Player to ban. /// Ban forever or ban temporarily. - public static async void BanPlayer(Player player, bool forever) + public static async void BanPlayer(IPlayer player, bool forever) { string banReason = await GetUserInput(windowTitle: "Enter Ban Reason", defaultText: "Banned by staff.", maxInputLength: 200); if (!string.IsNullOrEmpty(banReason) && banReason.Length > 1) @@ -786,7 +822,7 @@ public static async void BanPlayer(Player player, bool forever) /// Kill player /// /// - public static void KillPlayer(Player player) => TriggerServerEvent("vMenu:KillPlayer", player.ServerId); + public static void KillPlayer(IPlayer player) => TriggerServerEvent("vMenu:KillPlayer", player.ServerId); /// /// Kill yourself. @@ -901,12 +937,12 @@ public static async void CommitSuicide() /// Summon player. /// /// - public static void SummonPlayer(Player player) => TriggerServerEvent("vMenu:SummonPlayer", player.ServerId); + public static void SummonPlayer(IPlayer player) => TriggerServerEvent("vMenu:SummonPlayer", player.ServerId); #endregion #region Spectate function private static int currentlySpectatingPlayer = -1; - public static async void SpectatePlayer(Player player, bool forceDisable = false) + public static async void SpectatePlayer(IPlayer player, bool forceDisable = false) { if (forceDisable) { @@ -914,6 +950,11 @@ public static async void SpectatePlayer(Player player, bool forceDisable = false } else { + if (!player.IsActive) + { + await TeleportToPlayer(player); + } + if (player.Handle == Game.Player.Handle) { if (NetworkIsInSpectatorMode()) @@ -934,12 +975,17 @@ public static async void SpectatePlayer(Player player, bool forceDisable = false { if (NetworkIsInSpectatorMode()) { - if (currentlySpectatingPlayer != player.Handle) + if (currentlySpectatingPlayer != player.Handle && player.Character != null) { DoScreenFadeOut(500); while (IsScreenFadingOut()) await Delay(0); - NetworkSetInSpectatorMode(false, 0); - NetworkSetInSpectatorMode(true, player.Character.Handle); + + if (player.Character != null) + { + NetworkSetInSpectatorMode(false, 0); + NetworkSetInSpectatorMode(true, player.Character.Handle); + } + DoScreenFadeIn(500); Notify.Success($"You are now spectating ~g~{GetSafePlayerName(player.Name)}~s~.", false, true); currentlySpectatingPlayer = player.Handle; @@ -956,13 +1002,21 @@ public static async void SpectatePlayer(Player player, bool forceDisable = false } else { - DoScreenFadeOut(500); - while (IsScreenFadingOut()) await Delay(0); - NetworkSetInSpectatorMode(false, 0); - NetworkSetInSpectatorMode(true, player.Character.Handle); - DoScreenFadeIn(500); - Notify.Success($"You are now spectating ~g~{GetSafePlayerName(player.Name)}~s~.", false, true); - currentlySpectatingPlayer = player.Handle; + if (player.Character != null) + { + DoScreenFadeOut(500); + while (IsScreenFadingOut()) await Delay(0); + + if (player.Character != null) + { + NetworkSetInSpectatorMode(false, 0); + NetworkSetInSpectatorMode(true, player.Character.Handle); + } + + DoScreenFadeIn(500); + Notify.Success($"You are now spectating ~g~{GetSafePlayerName(player.Name)}~s~.", false, true); + currentlySpectatingPlayer = player.Handle; + } } } } diff --git a/vMenu/EventManager.cs b/vMenu/EventManager.cs index 6c83ebd0..24e15fed 100644 --- a/vMenu/EventManager.cs +++ b/vMenu/EventManager.cs @@ -289,9 +289,18 @@ private void KillMe(string sourceName) /// Teleport to the specified player. /// /// - private void SummonPlayer(string targetPlayer) + private async void SummonPlayer(string targetPlayer) { - TeleportToPlayer(GetPlayerFromServerId(int.Parse(targetPlayer))); + // ensure the player list is requested in case of Infinity + MainMenu.PlayersList.RequestPlayerList(); + await MainMenu.PlayersList.WaitRequested(); + + var player = MainMenu.PlayersList.FirstOrDefault(a => a.ServerId == int.Parse(targetPlayer)); + + if (player != null) + { + _ = TeleportToPlayer(player); + } } /// diff --git a/vMenu/FunctionsController.cs b/vMenu/FunctionsController.cs index de8e0349..fb91ae84 100644 --- a/vMenu/FunctionsController.cs +++ b/vMenu/FunctionsController.cs @@ -24,7 +24,6 @@ class FunctionsController : BaseScript { private int LastVehicle = 0; private bool SwitchedVehicle = false; - private Dictionary playerList = new Dictionary(); private List deadPlayers = new List(); private float cameraRotationHeading = 0f; @@ -68,12 +67,6 @@ class FunctionsController : BaseScript /// public FunctionsController() { - // Load the initial playerlist. - foreach (Player p in Players) - { - playerList.Add(p.Handle, p.Name); - } - // Add all tick functions. Tick += GcTick; Tick += GeneralTasks; @@ -90,7 +83,6 @@ public FunctionsController() Tick += MiscSettings; Tick += MiscRecordingKeybinds; Tick += DeathNotifications; - Tick += JoinQuitNotifications; Tick += UpdateLocation; Tick += ManageCamera; Tick += PlayerBlipsControl; @@ -1118,51 +1110,27 @@ private async Task MiscRecordingKeybinds() } } } - #region Join / Quit notifications + #region Join / Quit notifications (via events) /// /// Runs join/quit notification checks. /// /// - private async Task JoinQuitNotifications() + [EventHandler("vMenu:PlayerJoinQuit")] + private void OnJoinQuitNotification(string playerName, string dropReason) { if (MainMenu.PermissionsSetupComplete && MainMenu.MiscSettingsMenu != null) { // Join/Quit notifications if (MainMenu.MiscSettingsMenu.JoinQuitNotifications && IsAllowed(Permission.MSJoinQuitNotifs)) { - PlayerList plist = Players; - Dictionary pl = new Dictionary(); - foreach (Player p in plist) + if (dropReason == null) { - pl.Add(p.Handle, p.Name); - } - await Delay(0); - // new player joined. - if (pl.Count > playerList.Count) - { - foreach (KeyValuePair player in pl) - { - if (!playerList.Contains(player)) - { - Notify.Custom($"~g~{GetSafePlayerName(player.Value)}~s~ joined the server."); - await Delay(0); - } - } + Notify.Custom($"~g~{GetSafePlayerName(playerName)}~s~ joined the server."); } - // player left. - else if (pl.Count < playerList.Count) + else { - foreach (KeyValuePair player in playerList) - { - if (!pl.Contains(player)) - { - Notify.Custom($"~r~{GetSafePlayerName(player.Value)}~s~ left the server."); - await Delay(0); - } - } + Notify.Custom($"~r~{GetSafePlayerName(playerName)}~s~ left the server. ~c~({GetSafePlayerName(dropReason)})"); } - playerList = pl; - await Delay(100); } } } @@ -2175,7 +2143,7 @@ private async Task PlayerBlipsControl() { bool enabled = MainMenu.MiscSettingsMenu.ShowPlayerBlips && IsAllowed(Permission.MSPlayerBlips); - foreach (Player p in MainMenu.PlayersList) + foreach (IPlayer p in MainMenu.PlayersList) { // continue only if this player is valid. if (p != null && NetworkIsPlayerActive(p.Handle) && p.Character != null && p.Character.Exists()) @@ -2188,7 +2156,7 @@ private async Task PlayerBlipsControl() // if blips are enabled and the player has permisisons to use them. if (enabled) { - if (p != Game.Player) + if (!p.IsLocal) { int ped = p.Character.Handle; int blip = GetBlipFromEntity(ped); diff --git a/vMenu/MainMenu.cs b/vMenu/MainMenu.cs index a85b6661..c90e91c5 100644 --- a/vMenu/MainMenu.cs +++ b/vMenu/MainMenu.cs @@ -48,7 +48,7 @@ public class MainMenu : BaseScript public static VoiceChat VoiceChatSettingsMenu { get; private set; } public static About AboutMenu { get; private set; } public static bool NoClipEnabled { get { return NoClip.IsNoclipActive(); } set { NoClip.SetNoclipActive(value); } } - public static PlayerList PlayersList; + public static IPlayerList PlayersList; public static bool DebugMode = GetResourceMetadata(GetCurrentResourceName(), "client_debug_mode", 0) == "true" ? true : false; public static bool EnableExperimentalFeatures = (GetResourceMetadata(GetCurrentResourceName(), "experimental_features_enabled", 0) ?? "0") == "1"; @@ -65,7 +65,7 @@ public class MainMenu : BaseScript /// public MainMenu() { - PlayersList = Players; + PlayersList = new NativePlayerList(Players); #region cleanup unused kvps int tmp_kvp_handle = StartFindKvp(""); @@ -297,6 +297,52 @@ public MainMenu() } } + #region Infinity bits + [EventHandler("vMenu:SetServerState")] + public void SetServerState(IDictionary data) + { + if (data.TryGetValue("IsInfinity", out var isInfinity)) + { + if (isInfinity is bool isInfinityBool) + { + if (isInfinityBool) + { + PlayersList = new InfinityPlayerList(Players); + } + } + } + } + + [EventHandler("vMenu:ReceivePlayerList")] + public void ReceivedPlayerList(IList players) + { + PlayersList?.ReceivedPlayerList(players); + } + + public static async Task RequestPlayerCoordinates(int serverId) + { + Vector3 coords = Vector3.Zero; + bool completed = false; + + // TODO: replace with client<->server RPC once implemented in CitizenFX! + Func CallbackFunction = (data) => + { + coords = data; + completed = true; + return true; + }; + + TriggerServerEvent("vMenu:GetPlayerCoords", serverId, CallbackFunction); + + while (!completed) + { + await Delay(0); + } + + return coords; + } + #endregion + #region Set Permissions function /// /// Set the permissions for this client. @@ -370,6 +416,9 @@ private async Task OnTick() // Request the permissions data from the server. TriggerServerEvent("vMenu:RequestPermissions"); + // Request server state from the server. + TriggerServerEvent("vMenu:RequestServerState"); + // Wait until the data is received and the player's name is loaded correctly. while (!ConfigOptionsSetupComplete || !PermissionsSetupComplete || Game.Player.Name == "**Invalid**" || Game.Player.Name == "** Invalid **") { @@ -540,11 +589,13 @@ private void CreateSubmenus() Label = "→→→" }; AddMenu(Menu, menu, button); - Menu.OnItemSelect += (sender, item, index) => + Menu.OnItemSelect += async (sender, item, index) => { if (item == button) { - OnlinePlayersMenu.UpdatePlayerlist(); + PlayersList.RequestPlayerList(); + + await OnlinePlayersMenu.UpdatePlayerlist(); menu.RefreshIndex(); } }; diff --git a/vMenu/PlayerLists.cs b/vMenu/PlayerLists.cs new file mode 100644 index 00000000..a2f1c11f --- /dev/null +++ b/vMenu/PlayerLists.cs @@ -0,0 +1,204 @@ +using CitizenFX.Core; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +using static CitizenFX.Core.Native.API; + +namespace vMenuClient +{ + public interface IPlayer + { + int Handle { get; } + int ServerId { get; } + Ped Character { get; } + bool IsLocal { get; } + bool IsActive { get; } + string Name { get; } + } + + public interface IPlayerList : IEnumerable + { + void RequestPlayerList(); + + void ReceivedPlayerList(IList players); + + Task WaitRequested(); + } + + public class NativePlayer : IPlayer + { + private readonly Player player; + + public NativePlayer(Player player) + { + this.player = player; + } + + public int Handle => player.Handle; + public int ServerId => player.ServerId; + public Ped Character => player.Character; + public bool IsLocal => player == Game.Player; + public bool IsActive => NetworkIsPlayerActive(player.Handle); + public string Name => player.Name; + } + + public class NativePlayerList : IPlayerList + { + private readonly PlayerList playerList; + + public NativePlayerList(PlayerList playerList) + { + this.playerList = playerList; + } + + public IEnumerator GetEnumerator() + { + foreach (var player in playerList) + { + yield return new NativePlayer(player); + } + } + + public void RequestPlayerList() + { + // we are a local-only player list + } + + public void ReceivedPlayerList(IList players) + { + + } + + public Task WaitRequested() + { + // we instantly complete, as we always have all players + return Task.FromResult(0); + } + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (var player in playerList) + { + yield return new NativePlayer(player); + } + } + } + + public class InfinityPlayer : IPlayer + { + public InfinityPlayer(int serverId, string name) + { + ServerId = serverId; + Name = name; + } + + public int Handle => GetPlayerFromServerId(ServerId); + + public int ServerId { get; } + + public Ped Character + { + get + { + if (Handle >= 0) + { + var ped = GetPlayerPed(Handle); + + if (ped > 0) + { + return new Ped(ped); + } + } + + return null; + } + } + + public bool IsLocal => ServerId == GetPlayerServerId(PlayerId()); + public bool IsActive => Handle != -1 && NetworkIsPlayerActive(Handle); + + public string Name { get; } + } + + public class InfinityPlayerList : IPlayerList + { + private readonly PlayerList playerList; + private readonly Dictionary remotePlayerList; + + private int updatingPlayerList; + + public InfinityPlayerList(PlayerList playerList) + { + this.playerList = playerList; + this.remotePlayerList = new Dictionary(); + } + + private IEnumerator GetEnumeratorInternal() + { + var nearPlayers = new HashSet(); + + foreach (var player in playerList) + { + yield return new NativePlayer(player); + nearPlayers.Add(player.ServerId); + } + + foreach (var player in remotePlayerList) + { + if (!nearPlayers.Contains(player.Value.ServerId)) + { + yield return player.Value; + } + } + } + + public IEnumerator GetEnumerator() + { + return GetEnumeratorInternal(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumeratorInternal(); + } + + public void RequestPlayerList() + { + updatingPlayerList++; + BaseScript.TriggerServerEvent("vMenu:RequestPlayerList"); + } + + public void ReceivedPlayerList(IList players) + { + remotePlayerList.Clear(); + + foreach (var playerPair in players) + { + if (playerPair is IDictionary playerDict) + { + if (playerDict.TryGetValue("n", out var nameObj) && playerDict.TryGetValue("s", out var serverIdObj)) + { + if (nameObj is string name) + { + var serverId = Convert.ToInt32(serverIdObj); + + remotePlayerList[serverId] = new InfinityPlayer(serverId, name); + } + } + } + } + + updatingPlayerList--; + } + + public async Task WaitRequested() + { + while (updatingPlayerList > 0) + { + await BaseScript.Delay(0); + } + } + } +} diff --git a/vMenu/menus/OnlinePlayers.cs b/vMenu/menus/OnlinePlayers.cs index 30e56dfc..d79a7649 100644 --- a/vMenu/menus/OnlinePlayers.cs +++ b/vMenu/menus/OnlinePlayers.cs @@ -21,7 +21,7 @@ public class OnlinePlayers private Menu menu; Menu playerMenu = new Menu("Online Players", "Player:"); - Player currentPlayer = new Player(Game.Player.Handle); + IPlayer currentPlayer = new NativePlayer(Game.Player); /// @@ -130,8 +130,8 @@ private void CreateMenu() // teleport (in vehicle) button else if (item == teleport || item == teleportVeh) { - if (Game.Player.Handle != currentPlayer.Handle) - TeleportToPlayer(currentPlayer.Handle, item == teleportVeh); // teleport to the player. optionally in the player's vehicle if that button was pressed. + if (!currentPlayer.IsLocal) + _ = TeleportToPlayer(currentPlayer, item == teleportVeh); // teleport to the player. optionally in the player's vehicle if that button was pressed. else Notify.Error("You can not teleport to yourself!"); } @@ -239,7 +239,7 @@ private void CreateMenu() if (ban.Label == "Are you sure?") { ban.Label = ""; - UpdatePlayerlist(); + _ = UpdatePlayerlist(); playerMenu.GoBack(); BanPlayer(currentPlayer, true); } @@ -253,9 +253,12 @@ private void CreateMenu() // handle button presses in the player list. menu.OnItemSelect += (sender, item, index) => { - if (MainMenu.PlayersList.ToList().Any(p => p.ServerId.ToString() == item.Label.Replace(" →→→", "").Replace("Server #", ""))) + var baseId = int.Parse(item.Label.Replace(" →→→", "").Replace("Server #", "")); + var player = MainMenu.PlayersList.FirstOrDefault(p => p.ServerId == baseId); + + if (player != null) { - currentPlayer = MainMenu.PlayersList.ToList().Find(p => p.ServerId.ToString() == item.Label.Replace(" →→→", "").Replace("Server #", "")); + currentPlayer = player; playerMenu.MenuSubtitle = $"~s~Player: ~y~{GetSafePlayerName(currentPlayer.Name)}"; playerMenu.CounterPreText = $"[Server ID: ~y~{currentPlayer.ServerId}~s~] "; } @@ -269,24 +272,34 @@ private void CreateMenu() /// /// Updates the player items. /// - public void UpdatePlayerlist() + public async Task UpdatePlayerlist() { - menu.ClearMenuItems(); - - foreach (Player p in MainMenu.PlayersList) + void UpdateStuff() { - MenuItem pItem = new MenuItem($"{GetSafePlayerName(p.Name)}", $"Click to view the options for this player. Server ID: {p.ServerId}. Local ID: {p.Handle}.") + menu.ClearMenuItems(); + + foreach (IPlayer p in MainMenu.PlayersList.OrderBy(a => a.Name)) { - Label = $"Server #{p.ServerId} →→→" - }; - menu.AddMenuItem(pItem); - MenuController.BindMenuItem(menu, playerMenu, pItem); + MenuItem pItem = new MenuItem($"{GetSafePlayerName(p.Name)}", $"Click to view the options for this player. Server ID: {p.ServerId}. Local ID: {p.Handle}.") + { + Label = $"Server #{p.ServerId} →→→" + }; + menu.AddMenuItem(pItem); + MenuController.BindMenuItem(menu, playerMenu, pItem); + } + + menu.RefreshIndex(); + //menu.UpdateScaleform(); + playerMenu.RefreshIndex(); + //playerMenu.UpdateScaleform(); } - menu.RefreshIndex(); - //menu.UpdateScaleform(); - playerMenu.RefreshIndex(); - //playerMenu.UpdateScaleform(); + // First, update *before* waiting - so we get all local players. + UpdateStuff(); + await MainMenu.PlayersList.WaitRequested(); + + // Update after waiting too so we have all remote players. + UpdateStuff(); } /// @@ -303,7 +316,7 @@ public Menu GetMenu() } else { - UpdatePlayerlist(); + _ = UpdatePlayerlist(); return menu; } } diff --git a/vMenu/vMenuClient.csproj b/vMenu/vMenuClient.csproj index 817ce2b9..47fc2b56 100644 --- a/vMenu/vMenuClient.csproj +++ b/vMenu/vMenuClient.csproj @@ -7,6 +7,7 @@ $(AssemblyName) ..\..\build\ CLIENT + 8.0 @@ -18,7 +19,7 @@ - + runtime diff --git a/vMenuServer/MainServer.cs b/vMenuServer/MainServer.cs index f54a3d3d..83385961 100644 --- a/vMenuServer/MainServer.cs +++ b/vMenuServer/MainServer.cs @@ -203,7 +203,7 @@ public MainServer() CallbackFunction(JsonConvert.SerializeObject(data)); })); EventHandlers.Add("vMenu:RequestPermissions", new Action(PermissionsManager.SetPermissionsForPlayer)); - + EventHandlers.Add("vMenu:RequestServerState", new Action(RequestServerStateFromPlayer)); // check addons file for errors string addons = LoadResourceFile(GetCurrentResourceName(), "config/addons.json") ?? "{}"; @@ -223,6 +223,8 @@ public MainServer() Debug.WriteLine("^3[vMenu] [WARNING] vMenu is set up to ignore permissions!\nIf you did this on purpose then you can ignore this warning.\nIf you did not set this on purpose, then you must have made a mistake while setting up vMenu.\nPlease read the vMenu documentation (^5https://docs.vespura.com/vmenu^3).\nMost likely you are not executing the permissions.cfg (correctly).^7"); } + Tick += PlayersFirstTick; + // Start the loops if (GetSettingsBool(Setting.vmenu_enable_weather_sync)) Tick += WeatherLoop; @@ -881,5 +883,91 @@ private void AddTeleportLocation([FromSource] Player source, string locationJson } #endregion + #region Infinity bits + private void RequestServerStateFromPlayer([FromSource] Player player) + { + player.TriggerEvent("vMenu:SetServerState", new + { + IsInfinity = GetConvar("onesync_enableInfinity", "false") == "true" + }); + } + + [EventHandler("vMenu:RequestPlayerList")] + private void RequestPlayerListFromPlayer([FromSource] Player player) + { + player.TriggerEvent("vMenu:ReceivePlayerList", Players.Select(p => new + { + n = p.Name, + s = int.Parse(p.Handle), + })); + } + + [EventHandler("vMenu:GetPlayerCoords")] + private void GetPlayerCoords([FromSource] Player source, int playerId, NetworkCallbackDelegate callback) + { + if (IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.Teleport") || IsPlayerAceAllowed(source.Handle, "vMenu.Everything") || + IsPlayerAceAllowed(source.Handle, "vMenu.OnlinePlayers.All")) + { + var coords = Players[playerId]?.Character?.Position ?? Vector3.Zero; + + _ = callback(coords); + + return; + } + + _ = callback(Vector3.Zero); + } + #endregion + + #region Player join/quit + private HashSet joinedPlayers = new HashSet(); + + private Task PlayersFirstTick() + { + Tick -= PlayersFirstTick; + + foreach (var player in Players) + { + joinedPlayers.Add(player.Handle); + } + + return Task.FromResult(0); + } + + [EventHandler("playerJoining")] + private void OnPlayerJoining([FromSource] Player sourcePlayer) + { + joinedPlayers.Add(sourcePlayer.Handle); + + foreach (var player in Players) + { + if (IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.JoinQuitNotifs") || + IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.All")) + { + player.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, null); + } + } + } + + [EventHandler("playerDropped")] + private void OnPlayerDropped([FromSource] Player sourcePlayer, string reason) + { + if (!joinedPlayers.Contains(sourcePlayer.Handle)) + { + return; + } + + joinedPlayers.Remove(sourcePlayer.Handle); + + foreach (var player in Players) + { + if (IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.JoinQuitNotifs") || + IsPlayerAceAllowed(player.Handle, "vMenu.MiscSettings.All")) + { + player.TriggerEvent("vMenu:PlayerJoinQuit", sourcePlayer.Name, reason); + } + } + } + #endregion } }