Skip to content

Commit

Permalink
Wrapped CVRPlayerEntity in UIPlayerObject.cs, switched from using CVR…
Browse files Browse the repository at this point in the history
…PlayerEntity to UIPlayerObject in PlayerList.cs, added Kick, vote kick, and friending to quick settings panel, added local user to playerlist, added misc tab first setting
ddakebono committed Sep 24, 2024
1 parent 6edf9e9 commit 13b7bee
Showing 11 changed files with 356 additions and 11 deletions.
20 changes: 20 additions & 0 deletions BTKUILib.cs
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ internal class BTKUILib : MelonMod
internal MelonPreferences_Entry<PlayerListStyleEnum> PlayerListStyle;

private MelonPreferences_Entry<bool> _displayPrefsTab;
private MelonPreferences_Entry<bool> _miscTabFirst;

private Thread _mainThread;
private Page _mlPrefsPage;
@@ -51,6 +52,8 @@ public override void OnInitializeMelon()
});

PlayerListStyle = MelonPreferences.CreateEntry("BTKUILib", "PlayerListStyleNew", PlayerListStyleEnum.TabBar, "PlayerList Button Style", "Sets where the playerlist button will appear");

_miscTabFirst = MelonPreferences.CreateEntry("BTKUILib", "MiscTabFirst", false, "Misc Tab Always First", "Makes sure the misc tab is always first in the tab row");

Patches.Initialize(HarmonyInstance);

@@ -59,6 +62,14 @@ public override void OnInitializeMelon()

ColourPicker.SetupColourPicker();
PlayerList.SetupPlayerList();

if (!_miscTabFirst.Value) return;

QuickMenuAPI.MiscTabPage = Page.GetOrCreatePage("Misc", "Misc", true, "MiscIcon");
QuickMenuAPI.MiscTabPage.Protected = true;
QuickMenuAPI.MiscTabPage.MenuTitle = "Misc";
QuickMenuAPI.MiscTabPage.MenuSubtitle = "Miscellaneous mod elements be found here!";
QuickMenuAPI.MiscTabPage.HideTab = true;
}

internal void GenerateSettingsPage()
@@ -81,6 +92,15 @@ internal void GenerateSettingsPage()
QuickMenuAPI.OpenMultiSelect(_playerListButtonStyle);
};

var miscTabFirst = mainCat.AddToggle("Misc Tab First", "Sets if the Misc Tab should be first in the tab list (requires restart)", _miscTabFirst.Value);
miscTabFirst.OnValueUpdated += b =>
{
_miscTabFirst.Value = b;
MelonPreferences.Save();

QuickMenuAPI.ShowNotice("Restart Required!", "To change the Misc tab first setting you must restart your game! This setting will be applied on the next startup!");
};

_playerListStyleNames = Enum.GetNames(typeof(PlayerListStyleEnum));

_playerListButtonStyle = new MultiSelection("PlayerList Button Position", _playerListStyleNames, (int)PlayerListStyle.Value);
9 changes: 9 additions & 0 deletions BTKUILib.csproj
Original file line number Diff line number Diff line change
@@ -122,6 +122,15 @@
<ItemGroup>
<Compile Remove="ExampleUI\ExampleUIMod.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="CohtmlUI\images\BTKUILib\UserAdd.png" />
<None Remove="CohtmlUI\images\BTKUILib\UserMinus.png" />
<EmbeddedResource Include="CohtmlUI\images\BTKUILib\UserMinus.png" />
<None Remove="CohtmlUI\images\BTKUILib\Exit-Icon.png" />
<EmbeddedResource Include="CohtmlUI\images\BTKUILib\ExitDoor.png" />
<None Remove="CohtmlUI\images\BTKUILib\ThumbsDown.png" />
<EmbeddedResource Include="CohtmlUI\images\BTKUILib\ThumbsDown.png" />
</ItemGroup>
<Target Name="CheckLessc">
<Exec Command="lessc --version" ConsoleToMSBuild="true" ContinueOnError="true" IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="LesscExitCode" />
Binary file added CohtmlUI/images/BTKUILib/ExitDoor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added CohtmlUI/images/BTKUILib/ThumbsDown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added CohtmlUI/images/BTKUILib/UserAdd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added CohtmlUI/images/BTKUILib/UserMinus.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions Patches.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
using System;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.Responses;
using ABI_RC.Core.Networking.IO.Social;
using ABI_RC.Systems.GameEventSystem;
using HarmonyLib;
using MelonLoader;
using System.Threading.Tasks;

namespace BTKUILib
{
internal class Patches
{
internal static UserDetailsResponse LocalUserDetails;

private static HarmonyLib.Harmony _modInstance;
private static bool _firstOnLoadComplete;

@@ -32,6 +37,11 @@ internal static void Initialize(HarmonyLib.Harmony modInstance)
}
});

CVRGameEventSystem.Instance.OnConnected.AddListener(msg =>
{
PlayerList.Instance.OnWorldJoin();
});

CVRGameEventSystem.Instance.OnConnectionRecovered.AddListener(s =>
{
if (PlayerList.Instance == null) return;
@@ -57,6 +67,24 @@ internal static void Initialize(HarmonyLib.Harmony modInstance)
BTKUILib.Log.Error(e);
}
});

CVRGameEventSystem.Authentication.OnLogin.AddListener(resp =>
{
Task.Run(async () =>
{
var profileRequest = (await ApiConnection.MakeRequest<UserDetailsResponse>(ApiConnection.ApiOperation.UserDetails, new
{
userID = resp.UserId
}, null, false)).Data;

LocalUserDetails = profileRequest;

}).ContinueWith(t =>
{
if (!t.IsFaulted) return;
BTKUILib.Log.Error("Unable to retrieve local user details! User entry in PlayerList will have no image!");
});
});

CVRGameEventSystem.World.OnLoad.AddListener((message) =>
{
102 changes: 96 additions & 6 deletions PlayerList.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Networking.IO.Social;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using BTKUILib.UIObjects;
@@ -14,7 +17,7 @@ internal class PlayerList
internal static PlayerList Instance;

internal Page PlayerSelectPage;
internal CVRPlayerEntity SelectedPlayer;
internal UIPlayerObject SelectedPlayer;
internal Page InternalPlayerListPage;

private Category _internalPlayerListCategory;
@@ -27,7 +30,9 @@ internal class PlayerList
private MultiSelection _avatarBlockMode, _propBlockMode;
private CVRSelfModerationEntryUi _moderationEntry;
private bool _playerSelectMode;
private Action<CVRPlayerEntity> _playerSelectCallback;
private Action<UIPlayerObject> _playerSelectCallback;
private UIPlayerObject _localUserObject;
private Button _friendButton;

internal static void SetupPlayerList()
{
@@ -37,7 +42,7 @@ internal static void SetupPlayerList()
Instance.SetupPlayerListInstance();
}

internal void OpenPlayerActionPage(CVRPlayerEntity player)
internal void OpenPlayerActionPage(UIPlayerObject player)
{
if (_playerSelectMode)
{
@@ -59,17 +64,22 @@ internal void OpenPlayerActionPage(CVRPlayerEntity player)
QuickMenuAPI.OnPlayerSelected?.Invoke(player.Username, player.Uuid);
QuickMenuAPI.OnPlayerEntitySelected?.Invoke(player);

_moderationEntry = MetaPort.Instance.SelfModerationManager.GetPlayerSelfModerationProfile(player.Uuid, player.AvatarId);
_internalSelectCategory.Hidden = player.IsLocalUser;

_moderationEntry = MetaPort.Instance.SelfModerationManager.GetPlayerSelfModerationProfile(player.Uuid, player.AvatarID);
_muteUser.ToggleValue = _moderationEntry.mute;
_playerVolume.SetSliderValue(_moderationEntry.voiceVolume*100f);
_propBlockMode.SetSelectedOptionWithoutAction(_moderationEntry.userPropVisibility);
_avatarBlockMode.SetSelectedOptionWithoutAction(_moderationEntry.userAvatarVisibility);
_friendButton.ButtonText = Friends.FriendsWith(player.Uuid) ? "Unfriend" : "Add Friend";
_friendButton.ButtonTooltip = Friends.FriendsWith(player.Uuid) ? "Unfriend this user" : "Send a friend request to this user";
_friendButton.ButtonIcon = Friends.FriendsWith(player.Uuid) ? "UserMinus" : "UserAdd";

QuickMenuAPI.PlayerSelectPage.PageDisplayName = player.Username;
QuickMenuAPI.PlayerSelectPage.OpenPage();
}

internal void OpenPlayerPicker(string title, Action<CVRPlayerEntity> callback)
internal void OpenPlayerPicker(string title, Action<UIPlayerObject> callback)
{
InternalPlayerListPage.PageDisplayName = title;
foreach (var button in _userButtons.Values)
@@ -86,12 +96,19 @@ internal void ResetListAfterConnRecovery()
_internalPlayerListCategory.ClearChildren();
_userButtons.Clear();

AddLocalUser();

foreach (var player in CVRPlayerManager.Instance.NetworkPlayers)
{
UserJoin(player);
}
}

internal void OnWorldJoin()
{
AddLocalUser();
}

private void SetupPlayerListInstance()
{
//Attach to events
@@ -107,6 +124,8 @@ private void SetupPlayerListInstance()

InternalPlayerListPage.OnPageClosed += OnPageClosed;

_localUserObject = new UIPlayerObject(null);

//Setup playerlist action page
PlayerSelectPage = new Page("btkUI-PlayerSelectPage");
_internalSelectCategory = PlayerSelectPage.AddCategory("Quick Settings", "BTKUILib");
@@ -124,10 +143,57 @@ private void SetupPlayerListInstance()
reloadAvatar.OnPress += ReloadAvatar;
var blockUser = _internalSelectCategory.AddButton("Block User", "BlockUser", "Blocks the selected user");
blockUser.OnPress += BlockUser;
_friendButton = _internalSelectCategory.AddButton("Add Friend", "UserAdd", "Sends a friend request to this user!");
_friendButton.OnPress += FriendButtonPress;
var kickUser = _internalSelectCategory.AddButton("Kick User", "ExitDoor", "Kick this user from the instance (Only instance owner/moderator)");
kickUser.OnPress += KickUser;
var voteKick = _internalSelectCategory.AddButton("Vote Kick", "ThumbsDown", "Start a vote kick against this user!");
voteKick.OnPress += VoteKick;
_playerVolume = _internalSelectCategory.AddSlider("Player Voice Volume", "Adjust this players voice volume", 100, 0, 200);
_playerVolume.OnValueUpdated += AdjustVoiceVolume;
}

private void VoteKick()
{
QuickMenuAPI.ShowConfirm("Start Vote Kick?", $"Are you sure you want to start a vote kick against {SelectedPlayer.Username}? They won't be able to rejoin for an hour!", () =>
{
ViewManager.Instance.StartVoteKick(SelectedPlayer.Uuid);
});
}

private void KickUser()
{
QuickMenuAPI.ShowConfirm("Kick User?", $"Are you sure you want to kick from the instance {SelectedPlayer.Username}? They won't be able to rejoin for an hour!", () =>
{
UIUtils.KickUser(SelectedPlayer.Uuid);
});
}

private void FriendButtonPress()
{
if (Friends.FriendsWith(SelectedPlayer.Uuid))
{
//Remove friend
QuickMenuAPI.ShowConfirm("Remove Friend?", $"Are you sure you want to unfriend {SelectedPlayer.Username}?", () =>
{
ApiConnection.SendWebSocketRequest(RequestType.UnFriend, new
{
id = SelectedPlayer.Uuid
});
});
}
else
{
QuickMenuAPI.ShowConfirm("Send Friend Request?", $"Are you sure you want to send a friend request to {SelectedPlayer.Username}?", () =>
{
ApiConnection.SendWebSocketRequest(RequestType.FriendRequestSend, new
{
id = SelectedPlayer.Uuid
});
});
}
}

private void OnPageClosed()
{
if (!_playerSelectMode) return;
@@ -227,6 +293,8 @@ private void OnWorldLeave()
_userButtons.Clear();
if(!_playerSelectMode)
InternalPlayerListPage.PageDisplayName = "Playerlist | 0 Players in World";

AddLocalUser();
}

private void UserLeave(CVRPlayerEntity player)
@@ -242,18 +310,40 @@ private void UserLeave(CVRPlayerEntity player)

private void UserJoin(CVRPlayerEntity player)
{
if(_userButtons.Count == 0)
AddLocalUser();

if (_userButtons.ContainsKey(player.Uuid)) return;

var playerObject = new UIPlayerObject(player);

var newUserBtn = _internalPlayerListCategory.AddButton(player.Username, player.ApiProfileImageUrl, $"Opens the player options for {player.Username}!", ButtonStyle.FullSizeImage);
newUserBtn.OnPress += () =>
{
//User select
OpenPlayerActionPage(player);
OpenPlayerActionPage(playerObject);
};

_userButtons.Add(player.Uuid, newUserBtn);

if(!_playerSelectMode)
InternalPlayerListPage.PageDisplayName = $"Playerlist | {_userButtons.Count} Players in World";
}

private void AddLocalUser()
{
if (_userButtons.ContainsKey(_localUserObject.Uuid)) return;

var newUserBtn = _internalPlayerListCategory.AddButton(_localUserObject.Username, _localUserObject.PlayerIconURL, $"Opens the player options for {_localUserObject.Username}!", ButtonStyle.FullSizeImage);
newUserBtn.OnPress += () =>
{
//User select
OpenPlayerActionPage(_localUserObject);
};

_userButtons.Add(_localUserObject.Uuid, newUserBtn);

if(!_playerSelectMode)
InternalPlayerListPage.PageDisplayName = $"Playerlist | {_userButtons.Count} Players in World";
}
}
19 changes: 14 additions & 5 deletions QuickMenuAPI.cs
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ public static class QuickMenuAPI
/// <summary>
/// Called when a user is selected in the playerlist, passes that players CVRPlayerEntity
/// </summary>
public static Action<CVRPlayerEntity> OnPlayerEntitySelected;
public static Action<UIPlayerObject> OnPlayerEntitySelected;

/// <summary>
/// Called when a page change occurs, passes the new target page and the previous page
@@ -78,7 +78,7 @@ public static class QuickMenuAPI
/// <summary>
/// Last selected player's CVRPlayerEntity from the PlayerList page
/// </summary>
public static CVRPlayerEntity SelectedPlayerEntity => PlayerList.Instance.SelectedPlayer;
public static UIPlayerObject SelectedPlayerEntity => PlayerList.Instance.SelectedPlayer;

/// <summary>
/// Player select page for setting up functions that should be used in the context of a user
@@ -97,7 +97,6 @@ public static Page MiscTabPage
{
get
{
//Create the page as needed
if (_miscTabPage == null)
{
_miscTabPage = Page.GetOrCreatePage("Misc", "Misc", true, "MiscIcon");
@@ -106,8 +105,16 @@ public static Page MiscTabPage
_miscTabPage.MenuSubtitle = "Miscellaneous mod elements be found here!";
}

//Create the page as needed
if (_miscTabPage.HideTab)
{
_miscTabPage.HideTab = false;
}

return _miscTabPage;
}

internal set => _miscTabPage = value;
}

//Internal actions for utility functions
@@ -358,15 +365,17 @@ public static void OpenPlayerListByUserID(string userId)

if (playerEntity == null) return;

PlayerList.Instance.OpenPlayerActionPage(playerEntity);
var playerObject = new UIPlayerObject(playerEntity);

PlayerList.Instance.OpenPlayerActionPage(playerObject);
}

/// <summary>
/// Opens the playerlist in player selection mode
/// </summary>
/// <param name="title">Title for the PlayerList while in player selection mode</param>
/// <param name="callback">Callback to be fired when a player is selected</param>
public static void OpenPlayerSelector(string title, Action<CVRPlayerEntity> callback)
public static void OpenPlayerSelector(string title, Action<UIPlayerObject> callback)
{
PlayerList.Instance.OpenPlayerPicker(title, callback);
}
158 changes: 158 additions & 0 deletions UIPlayerObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using UnityEngine;

namespace BTKUILib;

/// <summary>
/// Wrapper object for CVRPlayerEntity, used to make handling local user information a bit easier
/// </summary>
public class UIPlayerObject
{
private readonly CVRPlayerEntity _playerEntity;
private readonly bool _isRemotePlayer;

internal UIPlayerObject(CVRPlayerEntity playerEntity)
{
_playerEntity = playerEntity;

if (ReferenceEquals(playerEntity, null)) return;

_isRemotePlayer = true;
}

/// <summary>
/// Returns full CVRPlayerEntity for remote users, null for local
/// </summary>
public CVRPlayerEntity CVRPlayer => _isRemotePlayer ? _playerEntity : null;

/// <summary>
/// returns the avatar object
/// </summary>
public GameObject AvatarObject
{
get
{
if (!_isRemotePlayer)
return PlayerSetup.Instance._avatar;

return _playerEntity.PuppetMaster == null ? null : _playerEntity.PuppetMaster.avatarObject;
}
}

/// <summary>
/// Returns the UUID for this user
/// </summary>
public string Uuid
{
get
{
if (!_isRemotePlayer)
return MetaPort.Instance.ownerId;
return ReferenceEquals(_playerEntity, null) ? null : _playerEntity.Uuid;
}
}

/// <summary>
/// Returns the Username of this user
/// </summary>
public string Username
{
get
{
if (!_isRemotePlayer)
return UIUtils.GetSelfUsername();
return ReferenceEquals(_playerEntity, null) ? null : _playerEntity.Username;
}
}

/// <summary>
/// Returns the private animator from the PuppetMaster
/// </summary>
public Animator AvatarAnimator
{
get
{
if (!_isRemotePlayer)
return PlayerSetup.Instance._animator;
return ReferenceEquals(_playerEntity, null) ? null : UIUtils.GetAvatarAnimator(_playerEntity.PuppetMaster);
}
}

/// <summary>
/// Returns the player's root gameobject
/// </summary>
public GameObject PlayerGameObject
{
get
{
if (!_isRemotePlayer)
return PlayerSetup.Instance.gameObject;
return ReferenceEquals(_playerEntity, null) ? null : _playerEntity.PlayerObject;
}
}

/// <summary>
/// Returns the player position, remote users require some weirdness
/// </summary>
public Vector3 PlayerPosition
{
get
{
if (!_isRemotePlayer)
return PlayerSetup.Instance.GetPlayerPosition();
// remote players avatar root is stuck at their playspace center, game bug :)
return ReferenceEquals(_playerEntity, null)
? Vector3.zero
: _playerEntity.PuppetMaster.GetViewWorldPosition() with
{
y = _playerEntity.PuppetMaster.transform.position.y
};
}
}

/// <summary>
/// Returns the AvatarID of this user
/// </summary>
public string AvatarID
{
get
{
if (!_isRemotePlayer)
return MetaPort.Instance.currentAvatarGuid;
return ReferenceEquals(_playerEntity, null) ? null : _playerEntity.AvatarId;
}
}

/// <summary>
/// Returns the player ImageURL from the API, if local user is null API didn't give us the user details
/// </summary>
public string PlayerIconURL
{
get
{
if (!_isRemotePlayer)
return Patches.LocalUserDetails?.ImageUrl;
return ReferenceEquals(_playerEntity, null) ? "" : _playerEntity.ApiProfileImageUrl;
}
}

/// <summary>
/// Returns true if this UIPlayerObject is the local user
/// </summary>
public bool IsLocalUser => !_isRemotePlayer;

/// <inheritdoc />
public override string ToString()
{
return $"UIPlayerObject - [Uuid: {Uuid}, Username: {Username}]";
}

/// <inheritdoc />
public override bool Equals(object obj)
{
if(obj is UIPlayerObject playerObject)
return Uuid == playerObject.Uuid;
return false;
}
}
31 changes: 31 additions & 0 deletions UIUtils.cs
Original file line number Diff line number Diff line change
@@ -3,13 +3,16 @@
using System.Text;
using System.Text.RegularExpressions;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Core.UI;
using cohtml.Net;
using MelonLoader;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEngine;

namespace BTKUILib
{
@@ -20,6 +23,9 @@ public static class UIUtils
{
private static MD5 _hasher = MD5.Create();
private static FieldInfo _internalCohtmlView = typeof(CohtmlControlledViewWrapper).GetField("_view", BindingFlags.Instance | BindingFlags.NonPublic);
private static PropertyInfo _selfUsername = typeof(MetaPort).Assembly.GetType("ABI_RC.Core.Networking.AuthManager").GetProperty("Username", BindingFlags.Static | BindingFlags.Public);
private static FieldInfo _animatorGetter = typeof(PuppetMaster).GetField("_animator", BindingFlags.Instance | BindingFlags.NonPublic);
private static MethodInfo _kickUserMethod = typeof(MetaPort).Assembly.GetType("ABI_RC.Core.Networking.Guardian.GuardianExtendedControls").GetMethod("KickUser", BindingFlags.Static | BindingFlags.NonPublic);
private static View _internalViewCache;

/// <summary>
@@ -56,6 +62,31 @@ public static Stream GetIconStream(string iconName)
string assemblyName = melon.MelonAssembly.Assembly.GetName().Name;
return melon.MelonAssembly.Assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}");
}

/// <summary>
/// Gets the private Animator from PuppetMaster
/// </summary>
/// <param name="pm">Target puppet master</param>
/// <returns>Private avatar animator</returns>
public static Animator GetAvatarAnimator(PuppetMaster pm)
{
if (pm == null) return null;
return (Animator)_animatorGetter.GetValue(pm);
}

/// <summary>
/// Gets the username of the local user
/// </summary>
/// <returns>Local users username</returns>
public static string GetSelfUsername()
{
return (string)_selfUsername.GetValue(null);
}

internal static void KickUser(string uuid)
{
_kickUserMethod.Invoke(null, [uuid]);
}

internal static string CreateMD5(string input)
{

0 comments on commit 13b7bee

Please sign in to comment.