ArabDesert/Assets/ReimajoBoothAssets/AdminTool/Scripts/PickupSync.cs

5463 lines
252 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#region DebugSettings
//#define DEBUG_TEST //<- only for testing the script, disable in live build!
//#define ANIMATION_TEST //<- only for testing the script, disable in live build!
//#define EDITOR_BUTTON_TEST //<- only for testing the buttons in editor
//#define DEBUG_AUTHENTICATION_TEST //<- only for testing the authentication, disable in live build!
//#define SYNC_DATA_DEBUG_TEST //<- only for testing the syncing, disable in live build!
//#define PLAYER_SEEKING_DEBUG_TEST //<- only for testing the player seeking, disable in live build!
//#define DEBUG_ADMIN_NAME_DETECTION //<- only for testing the admin name detection, disable in live build!
#endregion DebugSettings
//====================== IMPORTANT - READ FIRST =============================
// Since V4.0, compiler settings and user name lists (admins, perma bans, password hashes etc.) should no longer be
// manually edited by you inside the script. Please use the inspector for that! This will also ensure
// that all your settings & user name lists are stored somewhere else for future updates and also ensures that the script is edited correctly.
// Roles can also be edited in \Assets\ReimajoBoothAssets\YOUR_SETTINGS\AdminTool\, but don't forget to hit the apply
// button in the inspector afterwards!
//
// Note: The "YOUR_SETTINGS" folder is not affected by deleting or importing an asset. Just make sure to not accidentally delete it yourself.
//====================== IMPORTANT - READ FIRST =============================
#region CompilerSettings, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
//========================================================================================================================
//#define VRC_GUIDE_COMPLICANCE //if enabled, this tool will be compliant with the udon moderation tool guide https://docs.vrchat.com/docs/udon-moderation-tool-guidelines
//which means it will debug-print all actions that are taken against banned users and also disable safety options which might not be
//fully compliant, while also exposing this tool to be easily bypassed by mod users. Although those features only apply to users
//who use mods and thus only users who violate the VRChat TOS themselves, they are still eventually against that guide if taken word by word.
//Given how carefully this has been used and implemented so far, I'm not aware of any issues that users faced who enabled those
//safety options, but I want you to understand the possible consequences and decide for yourself if you want to enable them.
//You can disable this at your own risk, while accepting the possible consequence that is described in their guideline:
// "If we find your usage of moderation tools in Udon to be inappropriate (at our discretion), we may remove content until
// the issues are addressed." (see https://docs.vrchat.com/docs/udon-moderation-tool-guidelines)
//You still need to place a note in your world in both the ban area and the world entrance as described in this guideline to follow it fully.
//=============================================================================================================================
//#define VRC_GUIDE_DEBUG //You may choose to debug print the taken actions WITHOUT disabling the safety features by enabling
//the following option instead. Enabling this only makes sense if VRC_GUIDE_COMPLICANCE is DISABLED.
//========================================================================================================================
//#define DESYNC_BANNED_PLAYERS //TBD, do not enable. If enabled, banned players will be locally teleported away as well which is more secure
//========================================================================================================================
//#define STEALTH_PANEL //if enabled, bans are performed without showing the panel to other users
//========================================================================================================================
//#define NO_BAN_EFFECTS //if enabled, bans are performed without any effects visible to other users
//========================================================================================================================
#define PASSWORD_AUTHENTICATION //enable if you want to avoid name spoofing by enforcing admins to authenticate via passwords
//========================================================================================================================
//#define MINIMAL_BAN_DEBUG //enable if you want to make logs for banned users, recommended is disabled
//========================================================================================================================
#define ANTI_NAME_SPOOF //prevents people from getting banned when no real admin has the ownership of the panel. Having
//this enabled introduces the caveat that bans are only persistent as long as an admin is owner of the panel. If the current
//owner of the panel is not an admin anymore, the ban can be bypassed by reloading the world, although this is unlikely.
//========================================================================================================================
#define MARK_BANNED_PLAYERS //this shows a sign above all banned players (perma or soft-banned) who are somehow evading the ban, warning others
//to block them. This is a "better safe than sorry" approch to reduce damage by modded clients who are somehow able to trick the system.
//It should never appear in game, but you never know if there is someone who is able to bypass the ban in the future
//and if that's the case, that person should definitely get blocked. It marks them as "crashers", which might not always be correct.
//========================================================================================================================
#define MUTE_BANNED_PLAYERS //this is on by default and mutes a banned player + their avatar for everyone in the instance
//while they are banned. If this is on, the following setting toggles if they can still talk with world moderators.
//========================================================================================================================
#define BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS //this is on by default and allows banned players to hear moderators and also
//allows moderators to hear banned players. Other than that, a banned player is unable to hear other players (unless they are
//also banned) and all other not banned players are unable to hear the banned player if MUTE_BANNED_PLAYERS is enabled.
//========================================================================================================================
//#define ADMIN_ONLY_OBJECTS //exposes the possibility to add gameObjects which are enabled for admins.
//when used with other scripts, it is recommended to use _optionalSecureSpecialReceiver instead and let their script enable itself
//========================================================================================================================
//#define MODERATOR_AND_ADMIN_ONLY_OBJECTS //exposes the possibility to add gameObjects which are enabled for moderators & admins.
//when used with other scripts, it is recommended to use _optionalSecureSpecialReceiver instead and let their script enable itself
//========================================================================================================================
#define DESTROY_FOR_OTHER_PLAYERS //will destroy the admin/moderator objects for other players who should not have access to them
//If disabled, the objects are only disabled instead of destroying them, which means people with mods might be able to enable them.
//========================================================================================================================
#define ADMIN_CAN_FLY //disable this out if admins should NOT be able to fly
//if you disable this, the following setting has no effect and nobody will be able to fly.
//========================================================================================================================
#define MODERATOR_CAN_FLY //disable this out if moderators should NOT be able to fly
//if you disable this, the following setting has no effect and nobody will be able to fly.
//========================================================================================================================
#define MODERATOR_CAN_BAN //disable this out if moderators should NOT be able to ban other people //
//========================================================================================================================
//#define EVERYONE_CAN_FLY //disable this if everyone in your world should be able to fly, not just admins.
//========================================================================================================================
#define CAN_BAN_ADMINS //enable if admins and moderators can ban themselves and other admins / moderators, e.g. for testing the script functionality
//========================================================================================================================
#define SUMMON_PANEL_FUNCTION //disable if you don't want to be able to summon the panel by pressing the
//configured summon panel button in the inspector.
//This features adds frametime costs and is probably a bad idea when you have lots of amins/moderators using this
//at the same time and press the button for unrelated reasons such as typing in a chat system, so enabling it can make sense.
//Note that this script has an API to avoid that, but required all chat system / text input fields to use that API.
//========================================================================================================================
#define ACCURATE_CAPSULE_POSITIONS //disable if the player seeking is costing too much frametime cost for admins while seeking
//========================================================================================================================
//#define REMOTE_STRING_LOADING //enable if you want to load user roles remotely, e.g. via PasteBin or GitHub.io
//========================================================================================================================
//#define REMOTE_ADMIN_LIST //enable if you want to load admins remotely
//========================================================================================================================
//#define REMOTE_MODERATOR_LIST //enable if you want to load moderators remotely
//========================================================================================================================
//#define HASH_USERNAMES //TBD, do not enable right now. Protects usernames by hashing them (using a randomly generated salt)
//========================================================================================================================
#if !VRC_GUIDE_COMPLICANCE
//========================================================================================================================
//#define REMOTE_BAN_LIST //enable if you want to load user bans remotely
//========================================================================================================================
#define ANTI_PICKUP_SUMMON_MOD //enable if you don't want non-admins to summon the admin panel pickup with mods. Costs performance.
//========================================================================================================================
#define ANTI_NO_TELEPORT_MOD //enable if you don't want others to bypass the ban via no-teleport mod. Some people
//have a no-teleport mod to bypass a ban. This will detect those and remove them from VRChat.
//It is disabled by default, enable it at your own risk
//========================================================================================================================
#define USE_HONEYPOTS //This adds some traps for all those scriptkiddies out there and will disconnect them for "unusual client behaviour"
//if they indeed behave unusual, because no vanilla client could be able to call the methods and fall into those traps. This somehow works
//for 50% of the users, the other ones will at least stop running this udon behavior so they cannot mess with it anymore.
//This can however introduce lag for those users in that moment and is a drastic measurement.
//It is disabled by default, enable it at your own risk
//========================================================================================================================
//#define PERMA_BAN_LIST //enable this option to permanently ban players
//========================================================================================================================
//#define SURPRESS_WHITESPACE_CHARS //attempt to remove unicode whitespace chars when checking for perma bans.
//It can cause false positives / collateral damage.
//========================================================================================================================
#define USE_CUSTOM_ALT_ACCOUNT_DETECTION //example code to detect alt accounts from some really malicious users.
//Can cause collateral damage and is performance heavy. Before activating it, you must insert additional code that can
//be obtained through my discord. This feature was requested by a customer. Don't use those witout reading the moderation
//guidelines first. Abuse can lead to VRChat asking you to remove those from your world.
//(see https://docs.vrchat.com/docs/udon-moderation-tool-guidelines)
//========================================================================================================================
#endif
#endregion CompilerSettings, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
#region Namespaces
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using UnityEngine.UI;
using System;
using VRC.Udon.Common;
#endregion Namespaces
/// <summary>
/// Script from Reimajo, purchased at https://reimajo.booth.pm/, to be used in the worlds of the person who bought the asset only.
/// Join my Discord Server to receive update notifications & support for this asset: https://discord.gg/SWkNA394Mm
/// If you have any issues, please contact me on my Discord server (https://discord.gg/SWkNA394Mm) or Booth or Twitter https://twitter.com/ReimajoChan
/// Do not give any of the asset files or parts of them to anyone else.
/// </summary>
namespace ReimajoBoothAssets
{
/// <summary>
/// Class name, public and serialized variable names, as well as exposed events on this panel have a name that is not
/// easy to understand, such as _syncData1 or PickupSync. This is to provide a minimal layer of security on top so that
/// people with mods that can display those names don't understand what they are doing and have a harder time messing around with it.
/// </summary>
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PickupSync : UdonSharpBehaviour
{
// ======================= YOU SHOULD REALLY CHANGE THOSE VALUES =====================
#region WorldSecrets
/// <summary>
/// Placeholder for an empty list. Should be short, but not empty. You should definitely change this to increase security!
/// </summary>
private const string EMPTY_LIST = "ouas67T";
/// <summary>
/// Only relevant if you assign optional secure receivers to this script. This is not the case by default.
///
/// Shared secrets between this and any other script. You should definitely change these to increase security!
/// Each secret must be a positive number. Make sure that SECRET_1 * SECRET_2 + SECRET_3 does not exceed 2'147'483'647
/// </summary>
private const int SECRET_1 = 318890; //e.g. 5-6 digits
private const int SECRET_2 = 7; //e.g. 1 digit
private const int SECRET_3 = 651; //e.g. 1-3 digits
/// <summary>
/// A unicode character where we hope that this character is not used in any player name,
/// but this can also be replaced by every other unusual character as well. Banning a player with this character in the name
/// could lead to conflicts, although we do remove it from their name on every check, but this could lead to collisions with
/// another player name that is the same except where this character is the <see cref="SEPARATION_CHAR_REPLACEMENT"/> instead.
/// TBH that is super unlikely to happen. Just don't replace those two chars here with something that is commonly found in usernames.
/// </summary>
private const char SEPARATION_CHAR = '↨';
/// <summary>
/// Replacement character which will be put into the player name instead of the <see cref="SEPARATION_CHAR"/>
/// This character should also be unlikely to occur in player names to avoid collisions.
/// </summary>
private const char SEPARATION_CHAR_REPLACEMENT = '§';
#endregion WorldSecrets
// ===================================================================================
#region UserRoles, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
/// <summary>
/// DO NOT EDIT THIS LIST IN HERE. Use the inspector instead. Editing this list directly will most likely result in breaking the inspector.
/// List of Admins, they have the ability to fly and moderation abililies.
/// </summary>
private string[] _superSpecialSnowflakes = new string[]
{ @"TheLostDevil", @"ShakesPeare ♡", @"AKUMA", @"Breads", @"༺YUJI༻", @"SirAli", @"YAHYAღ", @"pankey" };
/// <summary>
/// DO NOT EDIT THIS LIST IN HERE. Use the ispector instead. Editing this list directly will most likely result in breaking the inspector.
/// List of Moderators, they only have moderation abilities.
/// </summary>
private string[] _specialSnowflakes = new string[]
{ @"iiLuccifer" };
#endregion UserRoles, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
#region PasswordAuthentication, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
#if PASSWORD_AUTHENTICATION
/// <summary>
/// DO NOT EDIT THIS LIST IN HERE. Use the ispector instead. Editing this list directly will most likely result in breaking the inspector.
///
/// Stores hashed passwords for admins and moderators to authenticate in game.
/// This is needed to operate the panel if PASSWORD_AUTHENTICATION is enabled.
///
/// Credentials are stored as Username, Salt, Hash (where the hash is a SHA512 based on password+salt combined)
/// </summary>
private object[] _credentials = new object[]
{ new string[] { @"TheLostDevil", @"25i8igcK9ItYQJT3jzSLD2GCEGlsRhUl", @"ff635c4825ba91c9c10626bfbfa92d8c6f5d989a1cde6804c55567b020698a225fe5727bc2d7f568f1be66fa2fed5514fbf881d419f0c753e156d8071dee026a" }, new string[] { @"ShakesPeare ♡", @"aY9lBMTcCNOxId215TGzVA0vJN1VYXNZ", @"ac289b83f84ca39237e1b2887107605aafc127151c3dd920a2f3100999dfa3405c53e819c158d87cd5b69e42ed8ac3c48f0f51205535ac58d13072f81aaf26f6" }, new string[] { @"AKUMA", @"dDegP9yqPGaS7GunYN6SowJ0H6Tm7fau", @"5a4d45b0c982141548fd2a95eec65dbe75c270c763d84efcda689c675ea7651395c603b31e2387b84cd8b0ebc5c1b4540c8517c8d6bb79c14df786f522ef0bf1" }, new string[] { @"Breads", @"uR7J86MO2KND8xOnoA57MeHFtkqQZODt", @"bd3347f09b60e9d976fa6907f2ec250baf62d50d2528724e91f40baa0c94a776f172f72982845e06afa555255bdd8f2d16b1d8d9345cf750ea6b209317e41919" }, new string[] { @"༺YUJI༻", @"n63UZB4P1OCvipva5FDXBLRikNrvwXjp", @"e7fe4901418415f27bbbd534a88462074f7b96bbb4935340e30d428775b9b90c7372d7628473ad8ac93bb5a9f6c53fa1b880a1714ee55a44e7de605d9effa26f" }, new string[] { @"SirAli", @"MIgkanvsbUXcxObSY6P1cBD5dW7Z7dcH", @"9ecbeec1cfc5458493abecaef58c1841959f6b70d07381f9e18b80e1fb3f9864887fd3249369580dfb071bd88339f25c736ffa903de9b3ce00ce139508206928" }, new string[] { @"YAHYAღ", @"qW5wdery5xKhh0dcxv7nGKIF3VCfWnOF", @"0ec41d7175250fdf547cca1a175909a27f698c6a3fc56bfe64e8c1ecf8c54c618e12d721bbe3ff6594291bdbb30a6fcbc3281d83ed0b5bd2b3019bb16ecfdfeb" }, new string[] { @"pankey", @"zxbfKxTv37a9498TrMKoCOHb0bIDD4zV", @"43493d695567fd9e67ec15b9bf5400e321e025b367a6a47d712f90f02b9652f52a3388a57b07d6b98da82659ac2fa70561f198b942e3f6c8c120f4922958a89c" }, new string[] { @"iiLuccifer", @"a1bq3t2UC9pQaJtcp7EvseJOCP7ZRoAD", @"36590e99747006b1e79392cfba209fe6fd400d4770ccc29961a241b5534c10715b303da78ab6661992834590645d8524110350d397f276548cd03a36c1836ae6" } };
#endif
#endregion PasswordAuthentication
#region PermaBanList, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
#if PERMA_BAN_LIST
/// <summary>
/// DO NOT EDIT THIS LIST IN HERE. Use the ispector instead. Editing this list directly will most likely result in breaking the inspector.
/// List of all permanently banned players. Those players are directly banned when joining the world in any instance.
/// </summary>
private string[] _permaBannedPlayers = new string[]
{ @"Kirai Chanǃ", @"xKirai Chan", @"Kirai Chanǃǃ" };
#endif
#endregion PermaBanList, DO NOT EDIT THEM HERE, PLEASE USE THE INSPECTOR INSTEAD
#region SyncedFields
/// <summary>
/// This is the synced panel state, though this variable is named more neutral to avoid that hackers understand directly what it does
/// </summary>
[System.NonSerialized, UdonSynced, FieldChangeCallback(nameof(_SyncData1PropertyCallback))]
public int _syncData1 = DEFAULT_PANEL_SYNC_STATE;
/// <summary>
/// This is the synced ban list state, though this variable is named more neutral to avoid that hackers understand directly what it does
/// </summary>
[System.NonSerialized, UdonSynced, FieldChangeCallback(nameof(_SyncData2PropertyCallback))]
public string _syncData2 = EMPTY_LIST;
#endregion SyncedFields
#region SerializedFields
[Space(10)]
/// <summary>
/// Text input field where the password must be entered to authenticate as an admin
/// </summary>
[SerializeField, Tooltip("Text input field where the password must be entered to authenticate as an admin")]
private InputField _optionalPasswordInputField;
#if SUMMON_PANEL_FUNCTION
/// <summary>
/// Keyboard button that can be pressed by admins to summon the panel in front of them
/// </summary>
[SerializeField, Tooltip("Keyboard button that can be pressed by PC admins to summon the panel in front of them")]
private KeyCode _summonDesktopPanelButton = KeyCode.B;
/// <summary>
/// VR controller button that can be pressed by admins to summon the panel in front of them.
/// All button names: https://docs.google.com/spreadsheets/d/1_iF0NjJniTnQn-knCjb5nLh6rlLfW_QKM19wtSW_S9w/
/// </summary>
[SerializeField, Tooltip("VR controller button that can be pressed by admins to summon the panel in front of them. All button names: https://docs.google.com/spreadsheets/d/1_iF0NjJniTnQn-knCjb5nLh6rlLfW_QKM19wtSW_S9w/")]
private string _summonVrButtonName = "Oculus_CrossPlatform_Button2";
/// <summary>
/// How often you need quickly to press the controller button in VR to summon the panel
/// </summary>
[SerializeField, Tooltip("How often you need quickly to press the controller button in VR to summon the panel")]
private int _summonVrPressCount = 4;
/// <summary>
/// How quick you need to press the controller button in VR to summon the panel (time in seconds between two presses)
/// </summary>
[SerializeField, Tooltip("How quick you need to press the controller button in VR to summon the panel (time in seconds between two presses)")]
private float _summonVrButtonMaxPressDelay = 0.7f;
#endif
/// <summary>
/// The pickup object transform of the admin panel
/// </summary>
[SerializeField, Tooltip("The pickup object transform from the admin panel")]
private Transform _pickupTransform;
/// <summary>
/// (Optional) Button that only an admin or moderator can see and which is disabled for all other players.
/// This button resets the panel position.
/// </summary>
[SerializeField, Tooltip("(Optional) Button that only an admin or moderator can see and which is disabled for all other players. This button resets the panel position.")]
private GameObject _resetButtonObject;
/// <summary>
/// Station template to locally desync banned players
/// </summary>
#if DESYNC_BANNED_PLAYERS
[SerializeField, Tooltip("Station template to locally desync banned players")]
#else
[SerializeField, HideInInspector]
#endif
private VRCStation _station;
/// <summary> (Optional) scripts that will receive '_PlayerIsBanned()' and '_PlayerIsUnbanned()' function calls in an unsecure way.
/// Can be empty, this is optional in case external scripts need to have that information. </summary>
[SerializeField, Tooltip("Scripts that will receive '_PlayerIsBanned()' and '_PlayerIsUnbanned()' function calls in an unsecure way")]
private UdonSharpBehaviour[] _optionalUnsecureReceiver = null;
/// <summary> (Optional) scripts that will receive '_PlayerIsBanned()' and '_PlayerIsUnbanned()' function calls in an secure way.
/// Can be empty, this is optional in case external scripts need to have that information. </summary>
[SerializeField, Tooltip("Scripts that will receive '_PlayerIsBanned()' and '_PlayerIsUnbanned()' function calls in a secure way. Take a look into the script where you find example code that you must place into those scripts to secure them.")]
private UdonSharpBehaviour[] _optionalSecureReceiver = null;
/// <summary> (Optional) scripts that will receive '_PlayerIsSpecial()' and '_PlayerIsSuperSpecial()' function calls in an secure way if the player is Admin or Moderator.
/// Can be empty, this is optional in case external scripts need to have that information. </summary>
[SerializeField, Tooltip("Scripts that will receive '_PlayerIsSpecial()' and '_PlayerIsSuperSpecial()' function calls in a secure way. Take a look into the script where you find example code that you must place into those scripts to secure them.")]
private UdonSharpBehaviour[] _optionalSecureSpecialReceiver = null;
#if MODERATOR_AND_ADMIN_ONLY_OBJECTS
/// <summary> (Optional) objects which are only enabled for moderators and admins.
/// They are destroyed for regular players if DESTROY_FOR_REGULAR_PLAYERS is enabled.
/// Keep those objects disabled in editor.</summary>
[SerializeField, Tooltip("(Optional) objects which are only enabled for moderators and admins. They are destroyed for regular players if DESTROY_FOR_REGULAR_PLAYERS is enabled. Keep those objects disabled in editor.")]
private GameObject[] _specialObjects = new GameObject[0];
#endif
#if ADMIN_ONLY_OBJECTS
/// <summary> (Optional) objects which are only enabled admins.
/// They are destroyed for other players if DESTROY_FOR_REGULAR_PLAYERS is enabled.
/// Keep those objects disabled in editor.</summary>
[SerializeField, Tooltip("(Optional) objects which are only enabled for admins. They are destroyed for other players if DESTROY_FOR_REGULAR_PLAYERS is enabled. Keep those objects disabled in editor.")]
private GameObject[] _superSpecialObjects = new GameObject[0];
#endif
/// <summary>
/// Sound that is played when a player is banned
/// </summary>
#if NO_BAN_EFFECTS
[SerializeField, HideInInspector]
#else
[SerializeField, Tooltip("Sound that is played when a player is banned")]
#endif
private AudioClip _soundFile1;
/// <summary>
/// Sound that is played when a banned player is joining
/// </summary>
[SerializeField, Tooltip("Sound that is played when a banned player is joining")]
private AudioClip _soundFile2;
/// <summary>
/// Audio that plays when the button is released after being fully pressed down
/// </summary>
[SerializeField, Tooltip("Audio that plays when the button is released after being fully pressed down")]
private AudioClip _clickUpAudioClip;
/// <summary>
/// Audio that plays when the button is fully pressed down
/// </summary>
[SerializeField, Tooltip("Audio that plays when the button is fully pressed down")]
private AudioClip _clickDownAudioClip;
/// <summary>
/// Audio volume of the ban sound
/// </summary>
#if NO_BAN_EFFECTS
[SerializeField, HideInInspector]
#else
[SerializeField, Tooltip("Audio volume of the ban sound (_soundFile1)"), Range(0, 1)]
#endif
private float SOUND_VOLUME_BAN = 1f;
/// <summary>
/// Audio volume of a button press
/// </summary>
[SerializeField, Tooltip("Audio volume of a button press (_clickUpAudioClip and _clickDownAudioClip)"), Range(0, 1)]
private float SOUND_VOLUME_BUTTON_PRESS = 0.1f;
/// <summary>
/// Audio volume of the "alarm" sound when a banned player joins
/// </summary>
[SerializeField, Tooltip("Audio volume of the \"alarm\" sound when a banned player joins (_soundFile2)"), Range(0, 1)]
private float SOUND_VOLUME_BANNED_PLAYER_JOINED = 0.8f;
[Space]
/// <summary>
/// Bomb that hovers above a selected player while the panel is armed
/// </summary>
#if NO_BAN_EFFECTS
[SerializeField, HideInInspector]
#else
[SerializeField, Tooltip("Bomb that hovers above a selected player while the panel is armed")]
#endif
private GameObject _bombObject;
/// <summary>
/// Parent of all UI components that should only be visible when the panel is visible
/// </summary>
[SerializeField, Tooltip("Parent of all UI components that should only be visible when the panel is visible")]
private GameObject _uiComponentsParent;
/// <summary>
/// UI element on the ban button that should only be visible when the panel is visible
/// </summary>
[SerializeField, Tooltip("UI element on the ban button that should only be visible when the panel is visible")]
private GameObject _uiBanButtonTextObj;
/// <summary>
/// Plane that is displayed below the selected player while the panel is armed
/// </summary>
#if NO_BAN_EFFECTS
[SerializeField, HideInInspector]
#else
[SerializeField, Tooltip("Plane that is displayed below the selected player while the panel is armed")]
#endif
private GameObject _bottomCrossObject;
/// <summary>
/// Explosion effect that is played on awake when a player is banned
/// </summary>
#if NO_BAN_EFFECTS
[SerializeField, HideInInspector]
#else
[SerializeField, Tooltip("Explosion effect that is played on awake when a player is banned")]
#endif
private GameObject _explosionFx;
/// <summary>
/// Root of the area to which a banned player is teleported and which is only then enabled
/// </summary>
[SerializeField, Tooltip("Plane that is displayed below the selected player while the panel is armed")]
private GameObject _areaObj;
/// <summary>
/// Mesh renderer of the panel which is disabled to make the panel invisible to others
/// </summary>
[SerializeField, Tooltip("Mesh renderer of the panel which is disabled to make the panel invisible to others")]
private SkinnedMeshRenderer _meshRenderer;
/// <summary>
/// Text display that shows the name of the selected player
/// </summary>
[SerializeField, Tooltip("Text display that shows the name of the selected player")]
private Text _textDisplay;
/// <summary>
/// Text display that shows the ID of the selected player
/// </summary>
[SerializeField, Tooltip("Text display that shows the ID of the selected player")]
private Text _idTextDisplay;
/// <summary>
/// Text display that shows the current owner of this panel
/// </summary>
[SerializeField, Tooltip("Text display that shows the current owner of this panel")]
private Text _ownerTextDisplay;
/// <summary>
/// Where a banned player is teleported to, inside the <see cref="_allowedArea"/>
/// </summary>
[SerializeField, Tooltip("Where a banned player is teleported to, inside the Allowed Area")]
private Transform _teleportTarget;
/// <summary>
/// Area bounds in which a banned player can freely move around, must be
/// a box trigger collider on the mirror reflection layer
/// </summary>
[SerializeField, Tooltip("Area bounds in which a banned player can freely move around, must be a box trigger collider on the mirror reflection layer")]
private BoxCollider _allowedArea;
//button objects
[SerializeField] // ban button
private GameObject _bigButtonCollider;
[SerializeField] // select next player
private GameObject _rightArrowCollider;
[SerializeField] // select previous player
private GameObject _leftArrowCollider;
[SerializeField] // arm ban button
private GameObject _bigButtonCoverCollider;
[SerializeField] // arm unban button
private GameObject _leftSwitchCoverCollider;
[SerializeField] // unban button
private GameObject _leftSwitchCollider;
[SerializeField] // arm unban all button
private GameObject _rightSwitchCoverCollider;
[SerializeField] // unban all button
private GameObject _rightSwitchCollider;
//button bones and their index
/// <summary> 0 / bone ban button </summary>
[SerializeField]
private Transform _boneBigButton; //0
/// <summary> 1 / bone next button </summary>
[SerializeField]
private Transform _boneRightArrowButton; //1
/// <summary> 2 / bone previous button </summary>
[SerializeField]
private Transform _boneLeftArrowButton; //2
/// <summary> 3 / bone ban arm button </summary>
[SerializeField]
private Transform _boneBigButtonCover; //3
/// <summary> 4 / bone unban arm button </summary>
[SerializeField]
private Transform _boneSwitchCoverL; //4
/// <summary> 5 / bone unban button </summary>
[SerializeField]
private Transform _boneSwitchLeverL; //5
/// <summary> 6 / bone unban all button </summary>
[SerializeField]
private Transform _boneSwitchCoverR; //6
/// <summary> 7 / bone unban all button </summary>
[SerializeField]
private Transform _boneSwitchLeverR; //7
/// <summary> Trigger zone of the arm ban button </summary>
[SerializeField]
private BoxCollider _triggerZoneBigButton;
/// <summary> Trigger zone of the arm unban button </summary>
[SerializeField]
private BoxCollider _triggerZoneLeftButton;
/// <summary> Trigger zone of the arm unban all button </summary>
[SerializeField]
private BoxCollider _triggerZoneRightButton;
/// <summary> Selection effect renderer of the arm ban button </summary>
[SerializeField]
private MeshRenderer _bigButtonCoverRenderer;
/// <summary> Selection effect renderer of the arm unban button </summary>
[SerializeField]
private MeshRenderer _leftSwitchCoverRenderer;
/// <summary> Selection effect renderer of the arm unban all button </summary>
[SerializeField]
private MeshRenderer _rightSwitchCoverRenderer;
/// <summary> Selection effect renderer of the unban button </summary>
[SerializeField]
private MeshRenderer _leftSwitchRenderer;
/// <summary> Selection effect renderer of the unban all button </summary>
[SerializeField]
private MeshRenderer _rightSwitchRenderer;
/// <summary> Template of a capsule collider setup for selecting a player </summary>
[SerializeField]
private GameObject _playerHitColliderTemplate;
/// <summary> Empty transform in world space to parent instantiated _playerHitColliderTemplate onto </summary>
[SerializeField]
private GameObject _playerHitColliderParent;
/// <summary> Transform to scale the visible laser for seeking players </summary>
[SerializeField]
private Transform _laserScaleObject;
/// <summary> Point from where the raycast is emitted to seek players </summary>
[SerializeField]
private Transform _rayEmitter;
/// <summary>
/// Area in which pushing the button is calculated if any bones are inside
/// </summary>
[SerializeField, Tooltip("Area in which pushing the ban button is calculated if any bones are inside")]
private BoxCollider _pushAreaBigButton;
/// <summary>
/// Area in which pushing the button is calculated if any bones are inside
/// </summary>
[SerializeField, Tooltip("Area in which pushing the right arrow button is calculated if any bones are inside")]
private BoxCollider _pushAreaRightButton;
/// <summary>
/// Area in which pushing the button is calculated if any bones are inside
/// </summary>
[SerializeField, Tooltip("Area in which pushing the left arrow button is calculated if any bones are inside")]
private BoxCollider _pushAreaLeftButton;
/// <summary>
/// Start position of the button when not pressed and push direction (blue axis / forward direction).
/// This object is set to the _buttonPushDirection position at Start().
/// </summary>
[SerializeField, Tooltip("Start position of the ban button when not pressed and push direction (blue axis / forward direction). This object is set to the _buttonPushDirection position at Start().")]
private Transform _buttonPushDirectionBigButton;
/// <summary>
/// Start position of the button when not pressed and push direction (blue axis / forward direction).
/// This object is set to the _buttonPushDirection position at Start().
/// </summary>
[SerializeField, Tooltip("Start position of the right arrow button when not pressed and push direction (blue axis / forward direction). This object is set to the _buttonPushDirection position at Start().")]
private Transform _buttonPushDirectionRightButton;
/// <summary>
/// Start position of the button when not pressed and push direction (blue axis / forward direction).
/// This object is set to the _buttonPushDirection position at Start().
/// </summary>
[SerializeField, Tooltip("Start position of the left arrow button when not pressed and push direction (blue axis / forward direction). This object is set to the _buttonPushDirection position at Start().")]
private Transform _buttonPushDirectionLeftButton;
#endregion SerializedFields
#region Settings
/// <summary>
/// How fast you fly upwards
/// </summary>
private const float FLY_UP_SPEED = 3f;
/// <summary>
/// How fast you fly forwards
/// </summary>
private const float FLY_FORWARD_SPEED = 10f;
/// <summary>
/// How fast you fall down while flying (this is a multiplier of the world gravity force)
/// </summary>
private const float FALL_DOWN_SPEED = 0.25f;
#if MUTE_BANNED_PLAYERS
/// <summary>
/// World voice gain for players, VRChats default is 15
/// </summary>
private const float PLAYER_VOICE_GAIN = 15f;
/// <summary>
/// World voice distance (far) for players, VRChats default is 25
/// </summary>
private const float PLAYER_VOICE_DISTANCE_FAR = 25f;
/// <summary>
/// World audio gain for avatars, VRChats default is 10
/// </summary>
private const float AVATAR_AUDIO_GAIN = 10f;
/// <summary>
/// World audio distance (far) for avatars, VRChats default is 40
/// </summary>
private const float AVATAR_AUDIO_DISTANCE_FAR = 40f;
#endif
/// <summary>
/// How many characters a synced string can have until syncing breaks. In the worst case, those are all unicode, meaning 2x 16bit chars
/// per unicode character, so it doesn't correlate with the string character limit from VRChat directly. 51 was found to be a safe value
/// before UNU while allowing a decent amount of banned players which are rotated around anyway, so you can essentially ban an unlimited
/// amount of players as long as they don't stay in the ban area forever. Manual sync allows much greater numbers now, up to 46k chars.
/// </summary>
private const int SYNC_CHAR_LIMIT = 23000; //that's up to 46k with Unicode wich is the limit before VRChat softbans you.
/// <summary>
/// Time in seconds for how long a button is blocked to avoid accidental button presses
/// </summary>
private const float BUTTON_BLOCKED_TIME = 0.3f;
/// <summary>
/// Color in which a username is displayed which is not banned
/// </summary>
private readonly Color COLOR_NORMAL_USER = new Color(0, 0, 0);
/// <summary>
/// Color in which a username is displayed which is banned
/// </summary>
private readonly Color COLOR_BANNED_USER = new Color(193, 0, 2);
/// <summary>
/// Maximum distance we raycast down from above an avatar to find the ground to display the crosshair
/// </summary>
private const float MAX_RAYCAST_DOWN_DISTANCE = 15f;
/// <summary>
/// Maximum distance we raycast forward from the panel onto an avatar
/// </summary>
private const float MAX_RAYCAST_FORWARD_DISTANCE = 30f;
/// <summary>
/// Layer mask on which the hit collider capsules for the player seeking are
/// </summary>
private const int LAYER_MASK_CAPSULES = 1 << 18; //Mirror reflection
/// <summary>
/// Default layer and environment layer
/// </summary>
private const int RAYCAST_GROUND_LAYER_MASK = 1 << 0 | 1 << 11;
/// <summary>
/// This determines the size of <see cref="_allPlayers"/>. The maximum amount of players in a world instance is [world capacity * 2 + 2]
/// </summary>
private const int MAX_NUMBER_OF_PLAYERS = 82;
/// <summary>
/// Error messages that are displayed on the panel. You can translate them here if you want.
/// </summary>
private const string ERROR_MESSAGE_SELECT_PLAYER_FIRST = "< select player first >";
private const string ERROR_MESSAGE_PLAYER_LEFT_INSTANCE = "< player left instance >";
private const string ERROR_MESSAGE_PLAYER_ID_CHANGED = "< player ID changed >";
private const string ERROR_MESSAGE_CANT_BAN_ADMIN = "< can't ban an admin >";
private const string ERROR_MESSAGE_PLAYER_NOT_FOUND = "< player not found >";
private const string ERROR_MESSAGE_NO_PLAYER_SELECTED = "< no player selected >";
private const string ERROR_MESSAGE_GRAB_PANEL_FIRST = "< grab panel first >";
#if PASSWORD_AUTHENTICATION
private const string ERROR_MESSAGE_AUTHENTICATE_FIRST = "< authenticate first >";
#endif
/// <summary>
/// Finger thickness of a standard sized avatar (1.3m), will automatically scale with avatar size
/// </summary>
private float FINGER_THICKNESS_DEFAULT = 0.02f;
/// <summary>
/// How far the button can be pressed down from the start position
/// </summary>
private float[] BUTTON_PUSH_DISTANCE = new float[BUTTON_COUNT] { 0.011f, 0.003f, 0.003f };
/// <summary>
/// At how much of BUTTON_PUSH_DISTANCE the button will trigger,
/// from 0 to 1, recommended is 0.9 (90%)
/// </summary>
private float BUTTON_TRIGGER_PERCENTAGE = 0.9f;
/// <summary>
/// At how much of BUTTON_PUSH_DISTANCE the button will un-trigger to be pushable again,
/// from 0 to 1, recommended is 0.55 (55%)
/// </summary>
private float BUTTON_UNTRIGGER_PERCENTAGE = 0.55f;
/// <summary>
/// Speed in meters/second at which the button will move back itself when being released
/// </summary>
private float MOVE_BACK_SPEED = 0.2f;
/// <summary>
/// Minimal time (in seconds) that must pass between two button triggers
/// </summary>
private float MIN_TRIGGER_TIME_OFFSET = 0.2f;
/// <summary>
/// Maximum distance between panel and player to interact with the panel buttons
/// </summary>
private const float MAX_PANEL_INTERACTION_DISTANCE = 3f;
#endregion Settings
#region PrivateFields
#if SUMMON_PANEL_FUNCTION
private int _pressCounter = 0;
private float _lastPressTime = 0;
#endif
/// <summary>
/// Default state of <see cref="_syncData1"/>
/// </summary>
private const int DEFAULT_PANEL_SYNC_STATE = 0;
/// <summary>
/// Local copy of <see cref="_syncData1"/> from the last (accepted) synced state
/// </summary>
private int _syncData1LocalCopy = DEFAULT_PANEL_SYNC_STATE;
/// <summary>
/// Local copy of <see cref="_syncData2"/> from the last (accepted) synced state
/// </summary>
private string _syncData2LocalCopy = EMPTY_LIST;
private int _privateCopySyncedSelectedPlayerID = 0;
#if ANTI_PICKUP_SUMMON_MOD || ANTI_NO_TELEPORT_MOD
private float _timeOutsideWhenBanned = 0f;
private bool _keepAlive = true;
#endif
/// <summary>
/// List of all currently banned players
/// </summary>
private string _bannedPlayerList = "";
private string[] _bannedPlayerListArray = new string[0];
private Bounds _allowedAreaBounds;
private bool _firstBanUpdate = true;
private Vector3 _positionBeforeBan;
private Quaternion _rotationBeforeBan;
private RaycastHit _hit;
private const int ENUM_SYNCDATA_EXPLODE = 0;
private const int ENUM_SYNCDATA_SHOW_BOMB = 1;
private int _selectedPlayerID = 0;
private bool _showSelectedPlayer;
private bool _runShowSelectedPlayer;
private float _selectedPlayerHeight;
private VRCPlayerApi _selectedPlayerAPI = null;
private bool _runBanUpdate;
#if PERMA_BAN_LIST
private bool _isPermaBanned;
#endif
private bool _localIsBadBoi;
private bool _keyInputBlocked;
private bool _ISA;
private bool _ISM;
#if EVERYONE_CAN_FLY || ADMIN_CAN_FLY || MODERATOR_CAN_FLY
//fly mode
private VRCPlayerApi.TrackingData _playerTracking;
private bool _flyModeOn;
private bool _isFlying;
private bool _isHoldingJump;
private Vector3 _currentForceVector = Vector3.zero;
private Vector3 _gravityForceVector;
#endif
//local arm button states
private bool _banButtonArmed;
private float _armBanButtonTimeStamp;
private bool _unbanButtonArmed;
private float _armUnbanButtonTimeStamp;
private bool _unbanAllButtonArmed;
private float _armUnbanAllButtonTimeStamp;
//stores if an admin really pressed that button
private bool _pressedBanButton;
private bool _pressedUnbanAllButton;
private bool _pressedUnbanButton;
//button IDs because we don't have real ENUMs right now in Udon
private const int ENUM_BUTTONID_BAN = 0;
private const int ENUM_BUTTONID_NEXT = 1;
private const int ENUM_BUTTONID_PREVIOUS = 2;
private const int ENUM_BUTTONID_ARM_BAN = 3;
private const int ENUM_BUTTONID_ARM_UNBAN = 4;
private const int ENUM_BUTTONID_UNBAN = 5;
private const int ENUM_BUTTONID_ARM_UNBAN_ALL = 6;
private const int ENUM_BUTTONID_UNBAN_ALL = 7;
//bool position of synced button states
private const int ENUM_ARMSTATE_BAN = 2;
private const int ENUM_ARMSTATE_UNBAN = 3;
private const int ENUM_ARMSTATE_UNBAN_ALL = 4;
private const int ENUM_FLIPSTATE_UNBAN = 5;
private const int ENUM_FLIPSTATE_UNBAN_ALL = 6;
private const int ENUM_FLIPSTATE_BAN = 7;
//number of currently pending animations
private int _runningButtonAnimationCount = 0;
#if !UNITY_ANDROID
private Vector3 _panelPositionLastFrame;
#endif
private Vector3 _startPosition;
private Quaternion _startRotation;
private Collider[] _pickupCollider;
private Animator _antennaAnimator;
private bool[] _goStart = new bool[8];
private bool[] _goEnd = new bool[8];
private float[] _currentValue = new float[8];
private readonly float[] _startValue = new float[8] { -0.0405502f, -0.03289417f, -0.03289417f, 180f, 180f, 109.654f, 180f, 109.654f };
private readonly float[] _endValue = new float[8] { -0.0318f, -0.0304f, -0.0304f, 62.7f, 116.3f, 64.7f, 116.3f, 64.7f };
private Transform[] _buttonBoneTransforms;
private Vector3 _currentTargetPosition;
private float _followStartTime;
private bool _bombAnimationStarted;
private int _currentSelectionIndex;
private bool _isSpecialSnowflake;
private VRCPlayerApi _localPlayer;
private bool _isHeld;
private bool _isHeldWithLeftHand;
private Bounds _vrTriggerBoundsArmBanButton;
private Bounds _vrTriggerBoundsArmUnbanButton;
private Bounds _vrTriggerBoundsArmUnbanAllButton;
private bool _highlightUnbanButton;
private bool _highlightUnbanAllButton;
private const int NONE_HOVER_SELECTED = -1;
private int _currentHoverSelection = NONE_HOVER_SELECTED;
/// <summary>
/// Display name of <see cref="_localPlayer"/> where <see cref="SEPARATION_CHAR"/> is already replaced with <see cref="SEPERATION_CHAR_REPLACEMENT"/>
/// </summary>
private string _localPlayerCleanedDisplayName;
/// <summary>
/// Wheter or not the user is actually a VR user
/// </summary>
private bool _isVR;
/// <summary>
/// If the VR-user mode should run for the bone-based input methods. Is only true for VR users with the needed finger bones.
/// </summary>
private bool _runVRmode;
private bool _isInDesktopFallbackMode;
private float _banTime;
/// <summary>
/// An array which always has the references to all players in the world. _playerAmount represents the number of players in the world.
/// </summary>
private VRCPlayerApi[] _allPlayers;
/// <summary>
/// Current amount of players in the instance
/// </summary>
private int _playerAmount;
#if USE_HONEYPOTS || ANTI_NO_TELEPORT_MOD || ANTI_PICKUP_SUMMON_MOD
private bool _localPlayerIsScriptKiddie;
#endif
#if MARK_BANNED_PLAYERS
[SerializeField]
private GameObject _warningSignAbovePlayerTemplate;
[SerializeField]
private GameObject _warningCapsuleTemplate;
private VRCPlayerApi[] _markedPlayers = new VRCPlayerApi[0];
private Transform[] _warningSignsAbovePlayers = new Transform[0];
private Transform[] _warningCapsules = new Transform[0];
private float[] _markedPlayerHeight = new float[0];
private bool _isMarkingPlayers;
private int _amountOfMarkedPlayers = 0;
private const float PLAYERHEIGHT_UPDATE_INTERVALL = 15f;
private int _currentPlayerForHeightUpdate = 0;
private float _lastPlayerHeightUpdateTime;
private const float WARNING_SIGN_OFFSET = 0.6f;
private const float PLAYER_HEIGHT_MULTIPLICATOR = 1.2f;
private const float CAPSULE_HEIGHT_MULTIPLICATOR = 1.2f;
#endif
private const int CHILD_CAPSULE_DESELECTED = 0;
private const int CHILD_CAPSULE_SELECTED = 1;
private const int NO_CAPSULE_SELECTED = -1;
private const float DEFAULT_PLAYER_HEIGHT = 1.3f;
private const float MIN_PLAYER_HEIGHT = 0.5f;
private int _currentSelectedSeekingCapsule = NO_CAPSULE_SELECTED;
private bool _runPlayerSeekingUpdate;
private bool _playerHitColliderSetup;
private CapsuleCollider[] _playerHitColliders = new CapsuleCollider[MAX_NUMBER_OF_PLAYERS];
private int _lastCheckedPlayerCount;
private int _lastScaledCapsule;
private readonly Vector3 FAR_AWAY = new Vector3(999999, 999999, 999999);
private const float SEEK_CAPSULE_HEIGHT_MULTIPLICATOR = 1.4f;
private Bounds _pushAreaBoundsBanButton;
private Bounds _pushAreaBoundsNextButton;
private Bounds _pushAreaBoundsPreviousButton;
private const int BUTTON_COUNT = 3;
private float[] _buttonTriggerDistance = new float[BUTTON_COUNT];
private float[] _buttonUntriggerDistance = new float[BUTTON_COUNT];
private float _fingerThickness;
private float[] _lastTriggerTime = new float[BUTTON_COUNT];
private bool[] _wasTriggered = new bool[BUTTON_COUNT];
private bool[] _wasInBound = new bool[BUTTON_COUNT];
private float[] _currentPushedDistance = new float[BUTTON_COUNT];
#if DESYNC_BANNED_PLAYERS
private VRCStation[] _activeStations = new VRCStation[0];
private string[] _usernameToStation = new string[0];
private int _amountOfDesyncedPlayers = 0;
#endif
#if REMOTE_STRING_LOADING && REMOTE_ADMIN_LIST
private string[] _allSuperSpecialSnowflakes;
#endif
#if REMOTE_STRING_LOADING && REMOTE_MODERATOR_LIST
private string[] _allSpecialSnowflakes;
#endif
#endregion PrivateFields
#region StartSetup
private void Start()
{
//prevent abuse of this method by calling it twice
if (_allPlayers != null)
return;
SetupPlayerSeekingAtStart();
//default: all disabled
_meshRenderer.enabled = false;
_bigButtonCollider.SetActive(false);
_rightArrowCollider.SetActive(false);
_leftArrowCollider.SetActive(false);
_bigButtonCoverCollider.SetActive(false);
_leftSwitchCoverCollider.SetActive(false);
_leftSwitchCollider.SetActive(false);
_rightSwitchCoverCollider.SetActive(false);
_rightSwitchCollider.SetActive(false);
//make sure that the pickup transform is assigned in editor
if (!Utilities.IsValid(_pickupTransform))
{
Debug.LogError($"[Analytics] There is no PickupTransform assigned to the PickupSync-Script.");
this.gameObject.SetActive(false);
return;
}
_localPlayer = Networking.LocalPlayer;
#if UNITY_EDITOR
//this script shouldn't run in editor, unless there is an emulator running
if (_localPlayer == null)
return;
#endif
#if REMOTE_STRING_LOADING && REMOTE_ADMIN_LIST
_allSuperSpecialSnowflakes = new string[_superSpecialSnowflakes.Length];
Array.Copy(_superSpecialSnowflakes, _allSuperSpecialSnowflakes, _superSpecialSnowflakes.Length);
#endif
#if REMOTE_STRING_LOADING && REMOTE_MODERATOR_LIST
_allSpecialSnowflakes = new string[_specialSnowflakes.Length];
Array.Copy(_specialSnowflakes, _allSpecialSnowflakes, _specialSnowflakes.Length);
#endif
_startPosition = _pickupTransform.position;
_startRotation = _pickupTransform.rotation;
#if !UNITY_ANDROID
_panelPositionLastFrame = _pickupTransform.position;
#endif
_pickupCollider = _pickupTransform.GetComponents<Collider>();
_antennaAnimator = _pickupTransform.GetComponent<Animator>();
VRC_Pickup pickup = (VRC_Pickup)_pickupTransform.GetComponent(typeof(VRC_Pickup));
pickup.pickupable = false;
#if EDITOR_BUTTON_TEST
_isVR = true;
SetupVRButtons();
#else
SetupVRHoverSelection();
//reading and storing the bounds requires the object to be active, we only trust the initial state
//since we don't trust other users.
_areaObj.SetActive(true);
_allowedArea.enabled = true;
_allowedAreaBounds = _allowedArea.bounds;
_allowedArea.enabled = false;
_areaObj.SetActive(false);
_isVR = _localPlayer.IsUserInVR();
_runVRmode = _isVR;
#if EVERYONE_CAN_FLY || ADMIN_CAN_FLY || MODERATOR_CAN_FLY
_gravityForceVector = Physics.gravity;
#endif
_buttonBoneTransforms = new Transform[8] { _boneBigButton, _boneRightArrowButton, _boneLeftArrowButton, _boneBigButtonCover, _boneSwitchCoverL, _boneSwitchLeverL, _boneSwitchCoverR, _boneSwitchLeverR };
//resetting all button bones to their start position/rotation
_boneBigButton.localPosition = new Vector3(_startValue[ENUM_BUTTONID_BAN], -0.03883955f, -0.09682186f);
_boneRightArrowButton.localPosition = new Vector3(_startValue[ENUM_BUTTONID_NEXT], -0.06600001f, 0.04591089f);
_boneLeftArrowButton.localPosition = new Vector3(_startValue[ENUM_BUTTONID_PREVIOUS], -0.06600001f, 0.1198734f);
//all other bones move by rotation
for (int i = 3; i < _buttonBoneTransforms.Length; i++)
_buttonBoneTransforms[i].localEulerAngles = new Vector3(0, 0, _startValue[i]);
#if !NO_BAN_EFFECTS
_bombObject.SetActive(false);
_bottomCrossObject.SetActive(false);
#endif
_localPlayerCleanedDisplayName = CleanUserName(_localPlayer.displayName);
_allPlayers = new VRCPlayerApi[MAX_NUMBER_OF_PLAYERS];
_textDisplay.text = ERROR_MESSAGE_NO_PLAYER_SELECTED; //"< no player selected >"
_idTextDisplay.text = "";
SetupUserRoles();
#if PERMA_BAN_LIST
CheckPermaBanState();
#endif
SendBannedStateToExternalScriptsUnsecure(isBanned: false);
SendBannedStateToExternalScriptsSecure(isBanned: false);
#endif //<- ending the button-test for editor
#if REMOTE_STRING_LOADING && (REMOTE_ADMIN_LIST || REMOTE_MODERATOR_LIST || REMOTE_BAN_LIST)
_RequestRemoteDataLoading();
#endif
}
#if PERMA_BAN_LIST
/// <summary>
/// Check if the local player is perma banned. Only perform this check on players who are not soft-banned!
/// Special users can never be permabanned. Remote string loading bans count as permabans.
/// </summary>
private void CheckPermaBanState()
{
if (!_isSpecialSnowflake)
{
if (IsPermaBanned(_localPlayer.displayName, localCheck: true))
{
#if MINIMAL_BAN_DEBUG
Debug.Log($"[Analytics] Status is permanent.");
#endif
_isPermaBanned = true;
_localIsBadBoi = true;
_banTime = Time.time;
SendBannedStateToExternalScriptsUnsecure(isBanned: true);
SendBannedStateToExternalScriptsSecure(isBanned: true);
_runBanUpdate = true;
_firstBanUpdate = true;
#if MUTE_BANNED_PLAYERS
MuteAllPlayers();
#endif
#if MARK_BANNED_PLAYERS
RemoveAllMarkedPlayers();
#endif
return;
}
#if MINIMAL_BAN_DEBUG
else
{
Debug.Log($"[Analytics] Status is ok.");
}
#endif
}
SendBannedStateToExternalScriptsUnsecure(isBanned: false);
SendBannedStateToExternalScriptsSecure(isBanned: false);
}
#endif
/// <summary>
/// Sets up local admin / moderator states
/// </summary>
private void SetupUserRoles()
{
string localPlayerRawDisplayName = _localPlayer.displayName;
SetupSyncboolCode();
//checking if player is an admin
_ISA = IsSuperSpecialSnowflake(localPlayerRawDisplayName);
//checking if player is a moderator
_ISM = IsSpecialSnowflake(localPlayerRawDisplayName);
#if DEBUG_ADMIN_NAME_DETECTION
if (_ISA)
Debug.Log("[AdminPanel] You are detected as an Admin");
if (_ISM)
Debug.Log("[AdminPanel] You are detected as a Moderator");
if(!_ISA && !_ISM)
{
string debugMessage = "[AdminPanel] You are detected as a regular user. If this is not what you wanted to archieve, here are all roles vs. your own display name: (click message to see details below)";
string allAdmins = string.Empty;
foreach (string displayName in _superSpecialSnowflakes) { allAdmins += $"'{displayName}'\t\t(admin)\n"; };
string allModerators = string.Empty;
foreach (string displayName in _specialSnowflakes) { allModerators += $"'{displayName}'\t\t(moderator)\n"; };
Debug.Log(debugMessage + "\n\n" + $"'{localPlayerRawDisplayName}'\t\t(your own display name)" + "\n\n" + allAdmins + "\n\n" + allModerators);
}
#endif
//checking for ban privileges
_isSpecialSnowflake = IsSnowflake(localPlayerRawDisplayName);
//notify other scripts about the player role
SendPlayerRoleToExternalScriptsSecure(_ISA, _ISM);
#if MODERATOR_AND_ADMIN_ONLY_OBJECTS
SetObjectAccess(_specialObjects, _isSpecialSnowflake);
#endif
#if ADMIN_ONLY_OBJECTS
SetObjectAccess(_superSpecialObjects, _ISA);
#endif
//checking if player is allowed to fly
#if EVERYONE_CAN_FLY
_flyModeOn = true;
#elif ADMIN_CAN_FLY && MODERATOR_CAN_FLY
_flyModeOn = _ISA || _ISM;
#elif ADMIN_CAN_FLY
_flyModeOn = _ISA;
#elif MODERATOR_CAN_FLY
_flyModeOn = _ISM;
#endif
if (_isSpecialSnowflake)
{
#if !PASSWORD_AUTHENTICATION
VRC_Pickup pickup = (VRC_Pickup)_pickupTransform.GetComponent(typeof(VRC_Pickup));
pickup.pickupable = true;
#else
SetPasswordInputActiveState(true);
#endif
SetupVRButtons();
#if PASSWORD_AUTHENTICATION
DisablePanelAccessTemporary();
HidePanel();
#else
FinishSetupForSnowflake();
#endif
#if DEBUG_TEST
Debug.Log("[AP] Finished setup for snowflake");
#endif
}
else
{
#if PASSWORD_AUTHENTICATION
SetPasswordInputActiveState(false);
#endif
#if REMOTE_STRING_LOADING
//we cannot destroy admin-only controls completely, so we disable them instead
DisablePanelAccessTemporary();
#else
Destroy(_ownerTextDisplay.gameObject);
if (_resetButtonObject != null)
Destroy(_resetButtonObject);
for (int i = 0; i < _pickupCollider.Length; i++) { _pickupCollider[i].enabled = false; }
//for safety reasons, we also destroy those components
Destroy(_leftSwitchCoverCollider);
Destroy(_bigButtonCoverCollider);
Destroy(_rightSwitchCoverCollider);
Destroy(_leftSwitchCollider);
Destroy(_rightSwitchCollider);
Destroy(_bigButtonCollider);
Destroy(_rightArrowCollider);
Destroy(_leftArrowCollider);
for (int i = 0; i < _pickupCollider.Length; i++) { Destroy(_pickupCollider[i]); }
#endif
HidePanel();
#if DEBUG_TEST
Debug.Log("[AP] Finished setup for normie");
#endif
}
}
/// <summary>
/// Removes access to the panel temporary before FinishSetupForSnowflake() is called to gain access
/// </summary>
private void DisablePanelAccessTemporary()
{
//we cannot destroy admin-only controls completely, so we disable them instead
_ownerTextDisplay.gameObject.SetActive(false);
if (_resetButtonObject != null)
_resetButtonObject.SetActive(false);
for (int i = 0; i < _pickupCollider.Length; i++) { _pickupCollider[i].enabled = false; }
}
/// <summary>
/// Finishes the setup for snowflakes, after they are authenticated
/// </summary>
private void FinishSetupForSnowflake()
{
if (Networking.IsOwner(this.gameObject))
_antennaAnimator.SetBool("IsDeployed", true);
_ownerTextDisplay.gameObject.SetActive(false);
((VRC_Pickup)_pickupTransform.GetComponent(typeof(VRC_Pickup))).pickupable = true;
_meshRenderer.enabled = true;
_uiComponentsParent.SetActive(true);
_uiBanButtonTextObj.SetActive(true);
//only snowflakes are allowed to have those enabled
for (int i = 0; i < _pickupCollider.Length; i++) { _pickupCollider[i].enabled = true; }
if (_resetButtonObject != null)
{
_resetButtonObject.SetActive(true);
Collider collider = _resetButtonObject.GetComponent<Collider>();
if (collider != null)
collider.enabled = true; //should be default-disabled for added security
}
SetButtonActive(ENUM_BUTTONID_ARM_BAN, true);
SetButtonActive(ENUM_BUTTONID_ARM_UNBAN, true);
SetButtonActive(ENUM_BUTTONID_ARM_UNBAN_ALL, true);
if (!_runVRmode)
{
SetButtonActive(ENUM_BUTTONID_NEXT, true);
SetButtonActive(ENUM_BUTTONID_PREVIOUS, true);
}
}
#if MODERATOR_AND_ADMIN_ONLY_OBJECTS || ADMIN_ONLY_OBJECTS
/// <summary>
/// Enables each valid object in <paramref name="objectArray"/>
/// if <paramref name="hasAccess"/> is true, else disables or destroys it
/// </summary>
private void SetObjectAccess(GameObject[] objectArray, bool hasAccess)
{
if (objectArray != null)
{
if (hasAccess)
{
foreach (GameObject obj in objectArray)
{
if (Utilities.IsValid(obj))
obj.SetActive(true);
}
}
else
{
foreach (GameObject obj in objectArray)
{
if (Utilities.IsValid(obj))
{
#if DESTROY_FOR_OTHER_PLAYERS
Destroy(obj);
#else
obj.SetActive(false);
#endif
}
}
}
}
}
#endif
#endregion StartSetup
#region RemoteStringLoading
#if REMOTE_STRING_LOADING && (REMOTE_ADMIN_LIST || REMOTE_MODERATOR_LIST || REMOTE_BAN_LIST)
//Set the URL in the inspector, NOT in here. Example: https://pastebin.com/vTRKDYGw
private const string URL_REMOTE_LOADING = "https://pastebin.com/raw/vTRKDYGw";
private VRCUrl _urlRemoteData = new VRCUrl(URL_REMOTE_LOADING);
private object _safetyCopy = URL_REMOTE_LOADING;
private string _lastLoadedDataCopy = string.Empty;
private const string ADMINS_END = "--LIST_1_END--";
private const string MODERATORS_END = "--LIST_2_END--";
private const string BANS_END = "--LIST_3_END--";
/// <summary>
/// Is called to request loading the data from the remote source
/// </summary>
public void _RequestRemoteDataLoading()
{
if (_urlRemoteData == null || _urlRemoteData.ToString().Trim().Length == 0)
{
Debug.LogError($"[AdminPanel] Url for remote data is not set.");
return;
}
if (_urlRemoteData.ToString() != (string)_safetyCopy)
{
Debug.LogError($"[AdminPanel] Url for remote data is not set correctly, set it in the inspector and not in the script.");
return;
}
VRC.SDK3.StringLoading.VRCStringDownloader.LoadUrl(_urlRemoteData, (VRC.Udon.Common.Interfaces.IUdonEventReceiver)this);
}
/// <summary>
/// Set the desired value in the inspector, NOT in here.
/// When no error occurs, we try again in 5 minutes (value is in seconds) to load the data again (to check if it changed)
/// </summary>
private const int REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS = 300 * 1000;
#if REMOTE_BAN_LIST
private string[] _remoteBannedPlayers = new string[0];
private bool _hasRemoteBannedPlayers = false;
#endif
/// <summary>
/// This event is called when the data was loaded
/// </summary>
public override void OnStringLoadSuccess(VRC.SDK3.StringLoading.IVRCStringDownload result)
{
string loadedData = result.Result.Replace("\r\n", "\n").Replace("\r", "\n");
if (_lastLoadedDataCopy == loadedData)
{
int remainingTime = REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS - (Networking.GetServerTimeInMilliseconds() % REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS);
#if DEBUG_STRING_LOADER
Debug.Log($"[Analytics] Server Time: {Networking.GetServerTimeInMilliseconds()}, Remaining time: {remainingTime}, Next Update in: {(float)remainingTime / 1000f}");
#endif
SendCustomEventDelayedSeconds(nameof(_RequestRemoteDataLoading), (float)remainingTime / 1000f);
return;
}
_lastLoadedDataCopy = loadedData;
string[] lines = loadedData.Split('\n');
#if DEBUG_STRING_LOADER
Debug.Log($"[Analytics] Downloaded {lines.Length} lines of data:");
int count = 0;
foreach (string line in lines)
{
Debug.Log($"[Analytics] Line [{count}]: '{line}'");
count++;
}
#endif
int startPos = 0;
int endPos;
int length;
endPos = Array.IndexOf(lines, ADMINS_END);
#if REMOTE_ADMIN_LIST
if (endPos == -1)
{
Debug.LogError($"[AdminPanel] Remote data is missing token " + ADMINS_END);
return;
}
length = endPos;
_allSuperSpecialSnowflakes = new string[_superSpecialSnowflakes.Length + length];
Array.Copy(_superSpecialSnowflakes, _allSuperSpecialSnowflakes, _superSpecialSnowflakes.Length);
Array.Copy(lines, startPos, _allSuperSpecialSnowflakes, _superSpecialSnowflakes.Length, length);
#endif
startPos = endPos + 1;
endPos = Array.IndexOf(lines, MODERATORS_END);
#if REMOTE_MODERATOR_LIST
if (endPos == -1)
{
Debug.LogError($"[AdminPanel] Remote data is missing token " + MODERATORS_END);
return;
}
length = endPos - startPos - 1;
_allSpecialSnowflakes = new string[_specialSnowflakes.Length + length];
Array.Copy(_specialSnowflakes, _allSpecialSnowflakes, _specialSnowflakes.Length);
Array.Copy(lines, startPos, _allSpecialSnowflakes, _specialSnowflakes.Length, length);
#endif
#if REMOTE_BAN_LIST
startPos = endPos + 1;
endPos = Array.IndexOf(lines, BANS_END);
if (endPos == -1)
{
Debug.LogError($"[AdminPanel] Remote data is missing token " + BANS_END);
return;
}
length = endPos - startPos - 1;
_remoteBannedPlayers = new string[length];
Array.Copy(lines, startPos, _remoteBannedPlayers, 0, length);
#endif
#if REMOTE_ADMIN_LIST || REMOTE_MODERATOR_LIST
SetupUserRoles();
#endif
#if REMOTE_BAN_LIST
bool hadRemoteBannedPlayers = _hasRemoteBannedPlayers;
_hasRemoteBannedPlayers = _remoteBannedPlayers.Length > 0;
if (_hasRemoteBannedPlayers)
{
LOCAL_SyncData2HasChanged();
}
else if (hadRemoteBannedPlayers)
{
LOCAL_SyncData2HasChanged();
}
#endif
int remainingTimeToNextUpdate = REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS - (Networking.GetServerTimeInMilliseconds() % REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS);
#if DEBUG_STRING_LOADER
Debug.Log($"Server Time: {Networking.GetServerTimeInMilliseconds()}, Remaining time: {remainingTimeToNextUpdate}, Next Update in: {(float)remainingTimeToNextUpdate / 1000f}");
#endif
SendCustomEventDelayedSeconds(nameof(_RequestRemoteDataLoading), (float)remainingTimeToNextUpdate / 1000f);
}
/// <summary>
/// When an error occurs, we try again in 10 seconds to load the data again
/// </summary>
private const int REMOTE_LOADING_DELAY_ON_ERROR = 10;
/// <summary>
/// This event is called when the data could not be loaded
/// </summary>
public override void OnStringLoadError(VRC.SDK3.StringLoading.IVRCStringDownload result)
{
Debug.LogError($"[AdminPanel] Remote data loading failed: {result.Error} ({result.ErrorCode})");
SendCustomEventDelayedSeconds(nameof(_RequestRemoteDataLoading), REMOTE_LOADING_DELAY_ON_ERROR);
}
#endif
#endregion RemoteStringLoading
#region HoneyPots
#if USE_HONEYPOTS || ANTI_PICKUP_SUMMON_MOD
private float _timeAfterScriptkiddieGotDetected = 0f;
#endif
#if USE_HONEYPOTS
/// <summary>
/// This is just a trap. All those stupid script kiddies with their malicious mods tend to randomly call public methods
/// on a script to see what happens, if they call this one they will be disconnected by VRChat for "unusual client behaviour".
/// This method starts with an underscore so it cannot be called on the network which is very important.
///
/// This is just a trap. DO NOT CALL IT FROM OTHER SCRIPTS!
/// </summary>
#else
/// <summary>
/// Does absolutely nothing, unless USE_HONEYPOTS is activated
/// </summary>
#endif
public void _EnableForLocalPlayer()
{
#if USE_HONEYPOTS
ActivateScriptkiddieTrap();
#endif
}
#if USE_HONEYPOTS
/// <summary> This is just a trap. DO NOT CALL IT FROM OTHER SCRIPTS! </summary>
private void ActivateScriptkiddieTrap()
{
_localPlayerIsScriptKiddie = true;
_runBanUpdate = true;
}
#endif
#if USE_HONEYPOTS || ANTI_PICKUP_SUMMON_MOD
/// <summary>
/// This is called for a (maliciously modded) client (if they tamper with this script) to avoid
/// that they can pull out their crasher menu etc., it will disconnect the most users for
/// "unusual client behaviour" (assuming they don't have a mod blocking this method call).
/// </summary>
private void SoftKick()
{
_localPlayer.TeleportTo(_localPlayer.GetPosition() + new Vector3(0, 0.001f, 0), _localPlayer.GetRotation() * Quaternion.Euler(1, 0, 0));
}
/// <summary>
/// This will slow a (maliciously modded) client down (if they tamper with this script) to avoid
/// that they can pull out their crasher menu etc.
/// </summary>
private void HardKick()
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
PrepareHardKick();
while (stopwatch.ElapsedMilliseconds < 8000)
{
Debug.Log("https://en.wikipedia.org/wiki/Script_kiddie");
GameObject newBomb = Instantiate(_bombObject);
newBomb.transform.position = new Vector3(_localPlayer.GetPosition().x, _localPlayer.GetPosition().x - 20f, _localPlayer.GetPosition().z);
newBomb.SetActive(true);
}
}
private void PrepareHardKick()
{
Slider slider = this.GetComponentInChildren<Slider>();
if (slider != null)
{
slider.enabled = true;
slider.value = 0.00000001f;
}
else
{
Debug.LogError("Missing UI Components");
}
}
/// <summary>
/// This should never be called. The only way to call this would be to have a modded client which violates the VRChat TOS.
/// </summary>
public void _ValueChanged()
{
Slider slider = this.GetComponentInChildren<Slider>();
if (slider != null)
{
slider.enabled = true;
slider.value += 0.00000001f;
}
else
{
Debug.LogError("Missing UI Components");
}
}
#endif
#endregion HoneyPots
#region PublicAPI
/// <summary>
/// Resets the panel to the initial position
/// </summary>
public void _ResetPosition()
{
if (_isSpecialSnowflake)
{
Networking.SetOwner(_localPlayer, _pickupTransform.gameObject);
_pickupTransform.position = _startPosition;
_pickupTransform.rotation = _startRotation;
}
}
/// <summary>
/// External scripts may need to enable flight mode for an admin
/// </summary>
public void _TurnOnFlightMode()
{
#if DEBUG_TEST
Debug.Log("[AP] received _TurnOnFlightMode()");
#endif
//checking if player is allowed to fly
#if EVERYONE_CAN_FLY
_flyModeOn = true;
#elif ADMIN_CAN_FLY && MODERATOR_CAN_FLY
_flyModeOn = _ISA || _ISM;
#elif ADMIN_CAN_FLY
_flyModeOn = _ISA;
#elif MODERATOR_CAN_FLY
_flyModeOn = _ISM;
#endif
}
/// <summary>
/// External scripts may need to disable flight mode for an admin
/// </summary>
public void _TurnOffFlightMode()
{
#if DEBUG_TEST
Debug.Log("[AP] received _TurnOffFlightMode()");
#endif
#if EVERYONE_CAN_FLY || ADMIN_CAN_FLY || MODERATOR_CAN_FLY
_flyModeOn = false;
#endif
}
/// <summary>
/// This is needed for the avatar liftup script.
/// </summary>
public void _PlayerIsDropped()
{
#if DEBUG_TEST
Debug.Log("[AP] received _PlayerIsDropped()");
#endif
_TurnOnFlightMode();
}
public void _PlayerIsHeld()
{
#if DEBUG_TEST
Debug.Log("[AP] received _PlayerIsHeld()");
#endif
_TurnOffFlightMode();
}
#endregion PublicAPI
#region UnsecureAPI
/// <summary>
/// Sends an event to all scripts in <see cref="_optionalUnsecureReceiver"/>
/// stating when the player got banned or unbanned (second event is also called at start)
/// _PlayerIsBanned() and _PlayerIsUnbanned()
/// </summary>
private void SendBannedStateToExternalScriptsUnsecure(bool isBanned)
{
string eventName = isBanned ? "_PlayerIsBanned" : "_PlayerIsUnbanned";
if (_optionalUnsecureReceiver != null)
{
foreach (UdonSharpBehaviour script in _optionalUnsecureReceiver)
{
if (script != null)
script.SendCustomEvent(eventName);
}
}
}
#endregion UnsecureAPI
#region SecureAPI
/// <summary>
/// Sends an event to all scripts in <see cref="_optionalSecureReceiver"/>
/// stating when the player got banned or unbanned (second event is also called at start)
/// _PlayerIsBanned() and _PlayerIsUnbanned()
/// </summary>
private void SendBannedStateToExternalScriptsSecure(bool isBanned)
{
string eventName = isBanned ? "_PlayerIsBanned" : "_PlayerIsUnbanned";
if (_optionalSecureReceiver != null)
{
foreach (UdonSharpBehaviour script in _optionalSecureReceiver)
{
if (script != null)
{
int token = SECRET_1 * SECRET_2 + SECRET_3; //a little bit of math for extra paranoia
script.SetProgramVariable("_token", token);
script.SendCustomEvent(eventName);
script.SetProgramVariable("_token", -1);
}
}
}
}
///Use the following code in your receiving script(s):
/*
#region SecureAPI
private const int SECRET_1 = 318890; //must be the same as in the ban panel script
private const int SECRET_2 = 7; //must be the same as in the ban panel script
private const int SECRET_3 = 651; //must be the same as in the ban panel script
private bool _playerIsBanned = false;
[HideInInspector]
public int _token = -1;
/// <summary>
/// This function is protected for extra security and can't be called via RPC
/// </summary>
public void _PlayerIsBanned()
{
if (_token != SECRET_1 * SECRET_2 + SECRET_3) //a little bit of math for extra paranoia
return;
_playerIsBanned = true;
}
/// <summary>
/// This function is protected for extra security and can't be called via RPC
/// </summary>
public void _PlayerIsUnbanned()
{
if (_token != SECRET_1 * SECRET_2 + SECRET_3) //a little bit of math for extra paranoia
return;
_playerIsBanned = false;
}
#endregion SecureAPI
*/
/// <summary>
/// Sends an event at start to all scripts in <see cref="_optionalSecureSpecialReceiver"/>
/// stating if the player is admin _PlayerIsSuperSpecial()
/// or moderator _PlayerIsSpecial()
/// or _PlayerIsNotSpecial() for any other player
/// </summary>
private void SendPlayerRoleToExternalScriptsSecure(bool isAdmin, bool isModerator)
{
string eventName = isAdmin ? "_PlayerIsSuperSpecial" : (isModerator ? "_PlayerIsSpecial" : "_PlayerIsNotSpecial");
if (_optionalSecureSpecialReceiver != null)
{
#if DEBUG_TEST
Debug.Log($"[AP] Sending role event '{eventName}' to _optionalSecureSpecialReceiver");
#endif
foreach (UdonSharpBehaviour script in _optionalSecureSpecialReceiver)
{
if (script != null)
{
int token = SECRET_1 * SECRET_2 + SECRET_3; //a little bit of math for extra paranoia
script.SetProgramVariable("_token", token);
script.SendCustomEvent(eventName);
script.SetProgramVariable("_token", -1);
}
}
}
}
///Use the following code in your receiving script(s):
/*
#region SecureAPI
private const int SECRET_1 = 318890; //must be the same token as in the ban panel script
private const int SECRET_2 = 7; //must be the same as in the ban panel script
private const int SECRET_3 = 651; //must be the same as in the ban panel script
private bool _playerIsAdmin = false;
private bool _playerIsModerator = false;
[HideInInspector]
public int _token = -1;
/// <summary>
/// This function is protected for extra security and can't be called via RPC.
/// It's called at start if the player is an admin.
/// </summary>
public void _PlayerIsSuperSpecial()
{
if (_token != SECRET_1 * * SECRET_2 + SECRET_3) //a little bit of math for extra paranoia
return;
_playerIsAdmin = true;
}
/// <summary>
/// This function is protected for extra security and can't be called via RPC
/// It's called at start if the player is a moderator.
/// </summary>
public void _PlayerIsSpecial()
{
if (_token != SECRET_1 * SECRET_2 + SECRET_3) //a little bit of math for extra paranoia
return;
_playerIsModerator = true;
}
/// <summary>
/// This function is protected for extra security and can't be called via RPC
/// It's called at start if the player not an admin and not a moderator.
/// </summary>
public void _PlayerIsNotSpecial()
{
if (_token != SECRET_1 * SECRET_2 + SECRET_3) //a little bit of math for extra paranoia
return;
_playerIsModerator = true;
}
#endregion SecureAPI
*/
#endregion SecureAPI
#region Update
/// <summary>
/// Is called every frame
/// </summary>
private void Update()
{
#if UNITY_EDITOR
//this script shouldn't run in editor, unless there is an emulator running
if (_localPlayer == null)
return;
#endif
#if EDITOR_BUTTON_TEST
RunAllVRButtons();
#else
if (_runBanUpdate)
{
#if USE_HONEYPOTS || ANTI_PICKUP_SUMMON_MOD
if (_localPlayerIsScriptKiddie)
{
//this will disconnect the user for "unusual client behaviour" (assuming they don't have a mod blocking this method call)
_timeAfterScriptkiddieGotDetected += Time.deltaTime;
if (_timeAfterScriptkiddieGotDetected > 5f)
{
HardKick();
}
else
{
SoftKick();
}
}
#endif
if (!_localIsBadBoi)
{
#if USE_HONEYPOTS || ANTI_PICKUP_SUMMON_MOD
if (!_localPlayerIsScriptKiddie)
#endif
{
#if DEBUG_TEST
Debug.Log("[AP] Unbanned, teleporting back");
#endif
_localPlayer.TeleportTo(_positionBeforeBan, _rotationBeforeBan);
_firstBanUpdate = true;
_areaObj.SetActive(false);
_runBanUpdate = false;
}
}
else if (_firstBanUpdate)
{
#if DEBUG_TEST
Debug.Log("[AP] First ban update");
#endif
_positionBeforeBan = _localPlayer.GetPosition();
_positionBeforeBan.y += 0.1f;
_rotationBeforeBan = _localPlayer.GetRotation();
_localPlayer.TeleportTo(_teleportTarget.position, _teleportTarget.rotation, VRC_SceneDescriptor.SpawnOrientation.AlignPlayerWithSpawnPoint, lerpOnRemote: false);
_areaObj.SetActive(true);
_firstBanUpdate = false;
}
}
#if !NO_BAN_EFFECTS
//if there is currently a player selected and the bomb is set to visible
if (_runShowSelectedPlayer)
{
//linear interpolation ensures the bomb is smoothly following a bit behind the player
_currentTargetPosition = Vector3.Lerp(_currentTargetPosition, _selectedPlayerAPI.GetPosition(), Time.deltaTime * 2);
Vector3 bombPosition = _currentTargetPosition;
//we scale all animations to the selected player height
float playerScale = _selectedPlayerHeight / 1.5f;
//starts at value 1
float sinusResult = Mathf.Sin(Time.timeSinceLevelLoad - _followStartTime);
//at the start, we want the bomb to drop from the sky, so we make the first sinus wave 10x the normal size until we reach the zero point
if (_bombAnimationStarted)
{
if (sinusResult >= 0)
sinusResult *= 10;
else
_bombAnimationStarted = false;
}
//a cross is displayed below the bomb on the ground collider and scales with the bomb distance, also rotates with the bomb
_bottomCrossObject.transform.position = new Vector3(bombPosition.x, GetGroundHeight(new Vector3(bombPosition.x, bombPosition.y + _selectedPlayerHeight, bombPosition.z)), bombPosition.z);
_bottomCrossObject.transform.localScale = (0.3f * playerScale * Vector3.one) * (1 + (0.4f * sinusResult));
//a bomb is displayed above the player, scales with the player and also rotates
bombPosition.y = bombPosition.y + _selectedPlayerHeight * 1.1f + (0.5f * playerScale) + (0.4f * playerScale + (0.4f * playerScale * sinusResult));
_bombObject.transform.position = bombPosition;
//bomb and cross rotate at the same speed
Quaternion rotationAdd = Quaternion.Euler(0, 20 * Time.deltaTime, 0);
//multiplying quaternions means adding them
_bombObject.transform.rotation *= rotationAdd;
_bottomCrossObject.transform.rotation *= rotationAdd;
}
#endif
if (_isSpecialSnowflake)
{
if (_runVRmode)
{
RunAllVRButtons();
UpdateHoverSelection();
}
#if SUMMON_PANEL_FUNCTION
RunSummonPanel();
#endif
#if !EVERYONE_CAN_FLY && (ADMIN_CAN_FLY || MODERATOR_CAN_FLY) && !(MODERATOR_CAN_FLY && !MODERATOR_CAN_BAN)
if (_flyModeOn)
{
UpdateFlyMode();
}
#endif
}
#if !UNITY_ANDROID && ANTI_PICKUP_SUMMON_MOD
else
{
//If a someone without special privileges is owner of this objects, we must check what they are doing
if (Networking.IsOwner(_pickupTransform.gameObject))
{
//protect panel from being moved by non-snowflakes with mods (https://en.wikipedia.org/wiki/Script_kiddie)
if (!Networking.IsMaster || Vector3.Distance(_panelPositionLastFrame, _pickupTransform.position) > 0.0001f)
{
_pickupTransform.position = _panelPositionLastFrame;
//if the owner of this object is not master or the pickup moved, they are definitely using some shady mods
//we abuse this timer, but it means the same thing: The user is using shady mods and should be kicked
_timeOutsideWhenBanned += Time.deltaTime;
if (_timeOutsideWhenBanned > 3f)
{
_keepAlive = false;
_localPlayerIsScriptKiddie = true;
_runBanUpdate = true;
}
}
else
{
_panelPositionLastFrame = _pickupTransform.position;
}
}
else
{
_panelPositionLastFrame = _pickupTransform.position;
}
}
#endif
#if EVERYONE_CAN_FLY || (MODERATOR_CAN_FLY && !MODERATOR_CAN_BAN)
if (_flyModeOn)
{
UpdateFlyMode();
}
#endif
#if MARK_BANNED_PLAYERS
if (_isMarkingPlayers)
UpdateSignPositions();
#endif
//running all active button animations if there are any
if (_runningButtonAnimationCount > 0)
RunButtonAnimations();
#endif
#if UNITY_EDITOR
//we need to handle serialization ourselves somehow because ClientSim won't do it for us
if (_pleaseSerializeMeDaddyUWU)
OnPostSerialization(new SerializationResult(success: true, byteCount: -1));
#endif
}
#endregion Update
#region FixedUpdate
private void FixedUpdate()
{
#if UNITY_EDITOR
//this script shouldn't run in editor, unless there is an emulator running
if (_localPlayer == null)
return;
#endif
if (_runBanUpdate)
{
if (!_localIsBadBoi)
return;
if (!_firstBanUpdate)
{
if (Time.time - _banTime < 1f)
return;
Vector3 playerHead = _localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
Vector3 playerPos = _localPlayer.GetPosition();
if (playerHead == Vector3.zero)
{
playerHead = playerPos;
}
if (!_allowedAreaBounds.Contains(playerHead) || !_allowedAreaBounds.Contains(playerPos))
{
#if DEBUG_TEST
Debug.Log("[AP] Not in ban area");
#endif
_localPlayer.TeleportTo(_teleportTarget.position, _localPlayer.GetRotation(), VRC_SceneDescriptor.SpawnOrientation.AlignPlayerWithSpawnPoint, lerpOnRemote: false);
#if ANTI_NO_TELEPORT_MOD
_timeOutsideWhenBanned += Time.deltaTime;
if (_timeOutsideWhenBanned > 5f)
{
_keepAlive = false;
_localPlayerIsScriptKiddie = true;
}
#endif
}
}
}
}
#endregion FixedUpdate
#region FlyMode
#if EVERYONE_CAN_FLY || ADMIN_CAN_FLY || MODERATOR_CAN_FLY
/// <summary>
/// Allows an admin to fly by jumping & then pressing the left trigger to fly upwards and/or the right trigger
/// to fly where the controller is pointing. For desktop users, they need to press space to jump
/// and then press space again to fly upwards and/or press left mouse button to fly forwards.
/// </summary>
private void UpdateFlyMode()
{
//if player is grounded, do nothing
if (!_localPlayer.IsPlayerGrounded())
{
bool hasForce = false;
Vector3 newForceVector = Vector3.zero;
float upwards;
float forwards;
if (_isVR)
{
upwards = Input.GetAxisRaw("Oculus_CrossPlatform_PrimaryIndexTrigger") * FLY_UP_SPEED;
forwards = Input.GetAxisRaw("Oculus_CrossPlatform_SecondaryIndexTrigger") * FLY_FORWARD_SPEED;
}
else
{
if (_isHoldingJump)
{
upwards = 0;
if (Input.GetKeyUp(KeyCode.Space))
_isHoldingJump = false;
}
else
{
upwards = (Input.GetKey(KeyCode.Space) ? 1 : 0) * FLY_UP_SPEED;
}
forwards = (Input.GetKey(KeyCode.Mouse0) ? 1 : 0) * FLY_FORWARD_SPEED;
}
if (forwards > 0)
{
if (_isVR)
{
_playerTracking = _localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
newForceVector = (_playerTracking.rotation.normalized * Vector3.right) * forwards;
}
else
{
_playerTracking = _localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
newForceVector = (_playerTracking.rotation.normalized * Vector3.forward) * forwards;
}
hasForce = true;
}
if (upwards > 0)
{
newForceVector.y += upwards;
hasForce = true;
}
if (hasForce)
{
if (!_isFlying)
{
_isFlying = true;
_currentForceVector = _localPlayer.GetVelocity();
}
_currentForceVector = Vector3.Lerp(_currentForceVector, newForceVector, Time.deltaTime);
_localPlayer.SetVelocity(_currentForceVector);
}
else if (_isFlying)
{
_currentForceVector = Vector3.Lerp(_currentForceVector, _gravityForceVector, Time.deltaTime * FALL_DOWN_SPEED);
_localPlayer.SetVelocity(_currentForceVector);
}
}
else
{
_isFlying = false;
if (!_isVR)
{
if (Input.GetKeyDown(KeyCode.Space))
_isHoldingJump = true;
else
_isHoldingJump = false;
}
}
}
#endif
#endregion FlyMode
#region ButtonAnimations
/// <summary>
/// playing all currently running button animations
/// </summary>
private void RunButtonAnimations()
{
for (int i = 0; i < _goEnd.Length; i++)
{
if (_goEnd[i])
PlayAnimation(i, goEnd: true);
}
for (int i = 0; i < _goStart.Length; i++)
{
if (_goStart[i])
PlayAnimation(i, goEnd: false);
}
}
/// <summary>
/// Returns the animation speed (time multiplier) at which a certain button should be moved
/// </summary>
private float GetAnimationSpeed(int buttonID)
{
switch (buttonID)
{
case ENUM_BUTTONID_BAN:
case ENUM_BUTTONID_NEXT:
case ENUM_BUTTONID_PREVIOUS:
return 5;
case ENUM_BUTTONID_ARM_BAN: //arm ban button
case ENUM_BUTTONID_ARM_UNBAN_ALL: //arm unban all button
case ENUM_BUTTONID_ARM_UNBAN: //arm unban button
return 2;
case ENUM_BUTTONID_UNBAN_ALL: //unban all button
case ENUM_BUTTONID_UNBAN: //unban button
return 3;
}
return 1;
}
/// <summary>
/// Starts a button animation to the specified direction <see cref="goEnd"/> for the button <see cref="armButtonID"/> if that button isn't
/// already at that position, also sets the button collider inactive so that it can't be pressed again until the end position is reached.
/// </summary>
/// <param name="armBanButtonID">The button for which the animation is started</param>
/// <param name="goEnd">The direction of the animation</param>
/// <returns></returns>
private bool StartButtonAnimation(int armBanButtonID, bool goEnd)
{
#if ANIMATION_TEST
Debug.Log($"[AP] Request starting button {armBanButtonID} animation (going end:{goEnd})");
#endif
if (goEnd)// (end == 1)
{
if (_currentValue[armBanButtonID] == 1 || _goEnd[armBanButtonID])
return false;
//disable button so that it can't be pressed during animation
SetButtonActive(armBanButtonID, false);
#if ANIMATION_TEST
Debug.Log($"[AP] Position: {_currentValue[armBanButtonID]} - Target:1)");
#endif
_goEnd[armBanButtonID] = true;
if (_goStart[armBanButtonID]) //reverting direction leaves counter untouched
_goStart[armBanButtonID] = false;
else
{
_runningButtonAnimationCount++;
#if ANIMATION_TEST
Debug.Log($"[AP] Animation Counter Increased to now {_runningButtonAnimationCount}");
#endif
}
}
else
{
if (_currentValue[armBanButtonID] == 0 || _goStart[armBanButtonID])
return false;
//disable button so that it can't be pressed during animation
SetButtonActive(armBanButtonID, false);
#if ANIMATION_TEST
Debug.Log($"[AP] Position: {_currentValue[armBanButtonID]} - Target:0)");
#endif
_goStart[armBanButtonID] = true;
if (_goEnd[armBanButtonID]) //reverting direction leaves counter untouched
_goEnd[armBanButtonID] = false;
else
{
_runningButtonAnimationCount++;
#if ANIMATION_TEST
Debug.Log($"[AP] Animation Counter Increased to now {_runningButtonAnimationCount}");
#endif
}
}
return true;
}
/// <summary>
/// Plays an animation for the button ID <see cref="i"/>, moving it in the direction <see cref="goEnd"/> and calls the
/// specified function when the end is reached
/// </summary>
/// <param name="i">The button that is being moved</param>
/// <param name="goEnd">The direction of the animation</param>
private void PlayAnimation(int i, bool goEnd)
{
#if ANIMATION_TEST
Debug.Log($"[AP] PlayAnimation called for Button {i}, goEnd {goEnd}, currentValue {_currentValue[i]}");
#endif
//proceed animation (end == 1)
float newTimeValue = goEnd ? Mathf.Clamp01(_currentValue[i] + Time.deltaTime * GetAnimationSpeed(i)) : Mathf.Clamp01(_currentValue[i] - Time.deltaTime * GetAnimationSpeed(i));
_currentValue[i] = newTimeValue;
float endValue = _endValue[i];
float startValue = _startValue[i];
//clamp going down animation values to limits
float newValue = Mathf.Lerp(startValue, endValue, newTimeValue);
//set new value
if (i < 3) //buttons 0 - 2 moves via position, not angle
{
Vector3 currentPos = _buttonBoneTransforms[i].localPosition;
_buttonBoneTransforms[i].localPosition = new Vector3(newValue, currentPos.y, currentPos.z);
}
else
{
_buttonBoneTransforms[i].localEulerAngles = new Vector3(0, 0, newValue);
}
//check if animation ended
if (goEnd) //end == 1
{
if (newTimeValue != 1f)
return;
_goEnd[i] = false;
_runningButtonAnimationCount--;
#if ANIMATION_TEST
Debug.Log($"[AP] Animation Counter Decreased to now {_runningButtonAnimationCount}");
Debug.Log($"[AP] Button {i} animation reached end position");
#endif
switch (i)
{
case ENUM_BUTTONID_BAN: //ban button
BanButton_EndPosReached();
PlayBanSound();
StartButtonAnimation(ENUM_BUTTONID_BAN, goEnd: false);
break;
case ENUM_BUTTONID_NEXT: //next player button
NextButton_ReachedEndPosition();
StartButtonAnimation(ENUM_BUTTONID_NEXT, goEnd: false);
break;
case ENUM_BUTTONID_PREVIOUS: //previous player button
PreviousButton_ReachedEndPosition();
StartButtonAnimation(ENUM_BUTTONID_PREVIOUS, goEnd: false);
break;
case ENUM_BUTTONID_ARM_BAN: //arm ban button
ArmBanButton_EndPosReached();
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_ARM_UNBAN: //arm unban button
ArmUnbanButton_EndPosReached();
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_UNBAN: //unban button
UnbanSelected_ReachedEndPosition();
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL: //arm unban all button
ArmUnbanAllButton_EndPosReached();
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_UNBAN_ALL: //unban all button
UnbanAll_ReachedEndPosition();
break;
}
}
else //when the start position was reached
{
if (newTimeValue != 0)
return;
_goStart[i] = false;
_runningButtonAnimationCount--;
#if ANIMATION_TEST
Debug.Log($"[AP] Animation Counter Decreased to now {_runningButtonAnimationCount}");
Debug.Log($"[AP] Button {i} animation reached start position");
#endif
switch (i)
{
case ENUM_BUTTONID_BAN: //ban button
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_NEXT: //next player button
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_PREVIOUS: //previous player button
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_ARM_BAN: //arm ban button
ArmBanButton_StartPosReached();
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_ARM_UNBAN: //arm unban button
ArmUnbanButton_StartPosReached();
SetButtonActive(i, true);
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL: //arm unban all button
ArmUnbanAllButton_StartPosReached();
SetButtonActive(i, true);
break;
}
}
}
/// <summary>
/// Sets the specified button into the active state <see cref="newState"/>
/// </summary>
/// <param name="buttonID">The button</param>
/// <param name="newState">The new active state</param>
private void SetButtonActive(int buttonID, bool newState)
{
//only admins are allowed to set any button into active state
if (!_isSpecialSnowflake)
return;
if (_runVRmode)
return;
switch (buttonID)
{
case ENUM_BUTTONID_BAN:
_bigButtonCollider.SetActive(newState);
break;
case ENUM_BUTTONID_NEXT:
_rightArrowCollider.SetActive(newState);
break;
case ENUM_BUTTONID_PREVIOUS:
_leftArrowCollider.SetActive(newState);
break;
case ENUM_BUTTONID_ARM_BAN:
_bigButtonCoverCollider.SetActive(newState);
break;
case ENUM_BUTTONID_ARM_UNBAN:
_leftSwitchCoverCollider.SetActive(newState);
break;
case ENUM_BUTTONID_UNBAN:
_leftSwitchCollider.SetActive(newState);
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL:
_rightSwitchCoverCollider.SetActive(newState);
break;
case ENUM_BUTTONID_UNBAN_ALL:
_rightSwitchCollider.SetActive(newState);
break;
}
}
#endregion ButtonAnimations
#region ExposedMethods
/// <summary>
/// Is called when ownership changed. For whitelisted users, the antenna is deployed when ownership was gained.
/// When ownership was lost for whitelisted users, the antenna goes down and all buttons are disarmed.
/// </summary>
public override void OnOwnershipTransferred(VRCPlayerApi player)
{
//ensure that this script never crashes, even not when mods mess with it
if (Utilities.IsValid(player))
{
if (_isSpecialSnowflake)
{
_ownerTextDisplay.text = Networking.GetOwner(this.gameObject).displayName;
if (!Networking.IsOwner(this.gameObject))
{
_antennaAnimator.SetBool("IsDeployed", false);
ChangeArmState_BanButton(setArmed: false, asOwner: true);
ChangeArmState_UnbanButton(setArmed: false, asOwner: true);
ChangeArmState_UnbanAllButton(setArmed: false, asOwner: true);
}
else
{
_antennaAnimator.SetBool("IsDeployed", true);
}
}
}
}
#if !STEALTH_PANEL
/// <summary>
/// Displayes the panel in a non-interactive form for players who are not whitelisted
/// </summary>
public void NW_RC_Show()
{
if (_isSpecialSnowflake)
return;
if (CheckIfMethodWasCalledByScriptKiddie())
return;
ShowPanel();
}
/// <summary>
/// Hides the panel in a non-interactive form for players who are not whitelisted
/// </summary>
public void NW_RC_Hide()
{
if (_isSpecialSnowflake)
return;
if (CheckIfMethodWasCalledByScriptKiddie())
return;
HidePanel();
}
/// <summary>
/// Displayes the panel in a non-interactive form
/// </summary>
private void ShowPanel()
{
_meshRenderer.enabled = true;
_antennaAnimator.SetBool("IsDeployed", true);
_uiComponentsParent.SetActive(true);
_uiBanButtonTextObj.SetActive(true);
}
#endif
/// <summary>
/// Hides the panel in a non-interactive form
/// </summary>
private void HidePanel()
{
_meshRenderer.enabled = false;
_antennaAnimator.SetBool("IsDeployed", false);
_uiComponentsParent.SetActive(false);
_uiBanButtonTextObj.SetActive(false);
}
#if !STEALTH_PANEL
/// <summary>
/// Expensive - only use when absolutely needed
/// Checks if any admin is in reach which is needed for puplic admin-only methods to work
/// </summary>
private bool IsAdminNearby()
{
VRCPlayerApi[] allPlayers = new VRCPlayerApi[82];
int playerCount = VRCPlayerApi.GetPlayerCount();
VRCPlayerApi.GetPlayers(allPlayers);
for (int i = 0; i < playerCount; i++)
{
if (IsSnowflake(allPlayers[i].displayName))
{
if (Vector3.Distance(allPlayers[i].GetPosition(), _pickupTransform.position) < 3)
return true;
}
}
return false;
}
/// <summary>
/// As we've learned, so called script-kiddies will brag about having mods and call every exposed
/// method on a behaviour, despite having no clue what they are doing and having zero skills in life.
/// So we need to run a lot of extra code to make sure this doesn't lead to actually
/// running any code. And because VRChat doesn't tell us who calls a method on the network,
/// all we can do is some arbitrary check if it was most likely an admin or not who sent that event.
/// </summary>
private bool CheckIfMethodWasCalledByScriptKiddie()
{
VRCPlayerApi panelOwner = Networking.GetOwner(this.gameObject);
if (!Utilities.IsValid(panelOwner)) //this is probably not needed but we really don't want to risk it
return true;
if (!IsSnowflake(panelOwner.displayName))
return true;
if (!IsAdminNearby())
return true;
return false;
}
#endif
#endregion ExposedMethods
#region Raycast
/// <summary>
/// Calculating the ground height under the head of localPlayer
/// Returns 0f when there is no ground. Only non-trigger colliders on the default layer are recognized as ground.
/// </summary>
private float GetGroundHeight(Vector3 startPosition)
{
if (Physics.Raycast(startPosition, Vector3.down, out _hit, MAX_RAYCAST_DOWN_DISTANCE, RAYCAST_GROUND_LAYER_MASK))
{
return _hit.point.y + 0.01f;
}
return 0.01f;
}
#endregion Raycast
#region UIControl
/// <summary>
/// Updates the button of the currently displayed player name, showing which state the player is in
/// </summary>
private void UpdateColor()
{
if (IsPlayerTempBanned(CleanUserName(_textDisplay.text)))
_textDisplay.color = COLOR_BANNED_USER;
else
_textDisplay.color = COLOR_NORMAL_USER;
}
/// <summary>
/// Functions calling this one must ensure that player is owner of this.gameObject (!)
/// Function sets the currently selected player ID for the panel.
/// </summary>
private void SetSelectedPlayerID(int playerID, bool onNetwork)
{
if (onNetwork && !Networking.IsOwner(this.gameObject))
{
Debug.LogError("[AP] Function SetSelectedPlayerID() was called while localPlayer was not Network owner which is a major error");
return;
}
_selectedPlayerID = playerID;
_idTextDisplay.text = playerID <= 0 ? "" : _selectedPlayerID.ToString();
if (onNetwork)
{
ChangeArmState_BanButton(setArmed: false, asOwner: true);
ChangeArmState_UnbanButton(setArmed: false, asOwner: true);
ChangeArmState_UnbanAllButton(setArmed: false, asOwner: true);
MASTER_SetSelectedPlayer(playerID);
}
}
#endregion UIControl
#region HoverSelection
/// <summary>
/// Is called whenever the localPlayer presses the trigger/use button
/// </summary>
public override void InputUse(bool value, UdonInputEventArgs args)
{
//panel must be held, the button pressed and a button hover selected
if (_isHeld && value && _currentHoverSelection != NONE_HOVER_SELECTED)
{
//the input hand must not be the hand that holds the panel
if (_isHeldWithLeftHand != (args.handType == HandType.LEFT))
{
if (!ButtonSafetyCheck())
return;
switch (_currentHoverSelection)
{
case ENUM_BUTTONID_ARM_BAN:
Pressed_ArmBanButton();
break;
case ENUM_BUTTONID_ARM_UNBAN:
Pressed_ArmUnbanButton();
break;
case ENUM_BUTTONID_UNBAN:
Pressed_UnbanButton();
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL:
Pressed_ArmUnbanAllButton();
break;
case ENUM_BUTTONID_UNBAN_ALL:
Pressed_UnbanAllButton();
break;
}
}
}
}
/// <summary>
/// Reading collider bounds and mesh renderer for the VR trigger selection at Start()
/// </summary>
private void SetupVRHoverSelection()
{
_vrTriggerBoundsArmBanButton = new Bounds(_triggerZoneBigButton.center, _triggerZoneBigButton.size);
_vrTriggerBoundsArmUnbanButton = new Bounds(_triggerZoneLeftButton.center, _triggerZoneLeftButton.size);
_vrTriggerBoundsArmUnbanAllButton = new Bounds(_triggerZoneRightButton.center, _triggerZoneRightButton.size);
UnselectAllVRHoverButtons();
}
/// <summary>
/// While the panel is held, the currently hovered button is marked as selected
/// </summary>
private void UpdateHoverSelection()
{
#if !EDITOR_BUTTON_TEST
if (_isHeld)
{
Vector3 handPos = _isHeldWithLeftHand ? _localPlayer.GetBonePosition(_rightIndexBone) : _localPlayer.GetBonePosition(_leftIndexBone);
int buttonID = GetHoverSelectedButton(_pickupTransform.InverseTransformPoint(handPos));
if (_currentHoverSelection == buttonID)
return;
if (_isHeldWithLeftHand)
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Right, 0.1f, 1.0f, 30f); //seconds, 0-320hz, 0-1 aplitude
else
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Left, 0.1f, 1.0f, 30f); //seconds, 0-320hz, 0-1 aplitude
UnselectVRHoverButton(_currentHoverSelection);
SelectVRHoverButton(buttonID);
}
#endif
}
/// <summary>
/// Returns the button ID which is currently hover-selected, determined by the hand position
/// being in the selection bounds or not. Returns -1 if no button is selected.
/// </summary>
/// <param name="handPosLocalSpace">Hand position translated to the localSpace of the pickup transform</param>
/// <returns></returns>
private int GetHoverSelectedButton(Vector3 handPosLocalSpace)
{
//no button is selected if there is currently an animation running
if (_runningButtonAnimationCount == 0)
{
if (_vrTriggerBoundsArmBanButton.Contains(handPosLocalSpace))
{
return ENUM_BUTTONID_ARM_BAN;
}
else if (_vrTriggerBoundsArmUnbanButton.Contains(handPosLocalSpace))
{
return _highlightUnbanButton ? ENUM_BUTTONID_UNBAN : ENUM_BUTTONID_ARM_UNBAN;
}
else if (_vrTriggerBoundsArmUnbanAllButton.Contains(handPosLocalSpace))
{
return _highlightUnbanAllButton ? ENUM_BUTTONID_UNBAN_ALL : ENUM_BUTTONID_ARM_UNBAN_ALL;
}
}
return NONE_HOVER_SELECTED;
}
/// <summary>
/// Hover-selects the specified button by enabling its mesh renderer
/// </summary>
private void SelectVRHoverButton(int buttonID)
{
switch (buttonID)
{
case ENUM_BUTTONID_ARM_BAN:
_bigButtonCoverRenderer.enabled = true;
break;
case ENUM_BUTTONID_ARM_UNBAN:
_leftSwitchCoverRenderer.enabled = true;
break;
case ENUM_BUTTONID_UNBAN:
_leftSwitchRenderer.enabled = true;
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL:
_rightSwitchCoverRenderer.enabled = true;
break;
case ENUM_BUTTONID_UNBAN_ALL:
_rightSwitchRenderer.enabled = true;
break;
}
_currentHoverSelection = buttonID;
}
/// <summary>
/// Unselects the specified button by disabling its mesh renderer
/// </summary>
private void UnselectVRHoverButton(int buttonID)
{
switch (buttonID)
{
case ENUM_BUTTONID_ARM_BAN:
_bigButtonCoverRenderer.enabled = false;
break;
case ENUM_BUTTONID_ARM_UNBAN:
_leftSwitchCoverRenderer.enabled = false;
break;
case ENUM_BUTTONID_UNBAN:
_leftSwitchRenderer.enabled = false;
break;
case ENUM_BUTTONID_ARM_UNBAN_ALL:
_rightSwitchCoverRenderer.enabled = false;
break;
case ENUM_BUTTONID_UNBAN_ALL:
_rightSwitchRenderer.enabled = false;
break;
}
}
/// <summary>
/// Unselects all hover-selectable buttons by disabling their mesh renderers
/// </summary>
private void UnselectAllVRHoverButtons()
{
_bigButtonCoverRenderer.enabled = false;
_leftSwitchCoverRenderer.enabled = false;
_rightSwitchCoverRenderer.enabled = false;
_leftSwitchRenderer.enabled = false;
_rightSwitchRenderer.enabled = false;
}
#endregion HoverSelection
#region PickupEvents
/// <summary>
/// Calls the <see cref="ShowPanel"/> function when the panel is picked up
/// </summary>
public void _OnRelayedPickup(bool isHeldWithLeftHand)
{
if (!_isSpecialSnowflake)
return;
_isHeld = true;
_isHeldWithLeftHand = isHeldWithLeftHand;
#if !STEALTH_PANEL
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(NW_RC_Show));
#endif
Networking.SetOwner(_localPlayer, this.gameObject);
_antennaAnimator.SetBool("IsDeployed", true);
_ownerTextDisplay.text = _localPlayer.displayName;
}
/// <summary>
/// Calles the <see cref="HidePanel"/> function when the panel is dropped
/// </summary>
public void _OnRelayedDrop()
{
if (!_isSpecialSnowflake)
return;
_isHeld = false;
StopPlayerSeekingUpdate();
UnselectAllVRHoverButtons();
#if !STEALTH_PANEL
SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, nameof(NW_RC_Hide));
#endif
}
/// <summary>
/// Called when the hand presses the trigger while holding the panel
/// Starts the PlayerSeeking loop
/// </summary>
public void _OnRelayedPickupUseDown()
{
if (!_isSpecialSnowflake)
return;
if (_runPlayerSeekingUpdate)
return;
StartPlayerSeekingUpdate();
}
/// <summary>
/// Called when the hand releases the trigger while holding the panel
/// Stops the PlayerSeeking loop
/// </summary>
public void _OnRelayedPickupUseUp()
{
StopPlayerSeekingUpdate();
}
#endregion PickupEvents
#region PlayerSeeking
/// <summary>
/// Is called at Start()
/// </summary>
private void SetupPlayerSeekingAtStart()
{
_playerHitColliderTemplate.SetActive(false);
_playerHitColliderParent.gameObject.SetActive(false);
_rayEmitter.gameObject.SetActive(false);
}
/// <summary>
/// Stops the player seeking update loop
/// </summary>
private void StopPlayerSeekingUpdate()
{
_playerHitColliderParent.gameObject.SetActive(false);
_rayEmitter.gameObject.SetActive(false);
_runPlayerSeekingUpdate = false;
}
/// <summary>
/// Starts the player seeking update loop
/// </summary>
private void StartPlayerSeekingUpdate()
{
if (!_playerHitColliderSetup)
{
//bring the template into the default state
_playerHitColliderTemplate.SetActive(false);
_playerHitColliderTemplate.transform.GetChild(CHILD_CAPSULE_DESELECTED).gameObject.SetActive(true);
_playerHitColliderTemplate.transform.GetChild(CHILD_CAPSULE_SELECTED).gameObject.SetActive(false);
//instantiate one capsule for each player
for (int i = 0; i < MAX_NUMBER_OF_PLAYERS; i++)
{
GameObject newHitCollider = Instantiate(_playerHitColliderTemplate);
newHitCollider.transform.SetParent(_playerHitColliderParent.transform);
_playerHitColliders[i] = newHitCollider.GetComponent<CapsuleCollider>();
}
_playerHitColliderSetup = true;
}
_rayEmitter.gameObject.SetActive(true);
_playerHitColliderParent.SetActive(true);
_runPlayerSeekingUpdate = true;
SendCustomEventDelayedFrames(nameof(_UpdatePlayerSeeking), 1, VRC.Udon.Common.Enums.EventTiming.Update);
}
/// <summary>
/// Runs the player seeking update loop during Update in each frame while <see cref="_runPlayerSeekingUpdate"/> is true
/// </summary>
public void _UpdatePlayerSeeking()
{
if (_runPlayerSeekingUpdate)
{
SeekPlayer();
SendCustomEventDelayedFrames(nameof(_UpdatePlayerSeeking), 1, VRC.Udon.Common.Enums.EventTiming.Update);
}
}
/// <summary>
/// Fires a raycast at all players, the closest player that is hit is then selected
/// </summary>
private void SeekPlayer()
{
AdjustActiveCapsuleCount();
//move all active capsules to the player positions
MoveCapsulesToPlayers();
//shoot a raycast at the capsules
Ray ray = new Ray(_rayEmitter.position, _rayEmitter.forward);
if (Physics.Raycast(ray, out _hit, MAX_RAYCAST_FORWARD_DISTANCE, LAYER_MASK_CAPSULES, QueryTriggerInteraction.Collide))
{
Collider collider = _hit.collider;
int index = System.Array.IndexOf(_playerHitColliders, collider);
if (index != -1)
{
SelectCapsule(index);
_laserScaleObject.localScale = new Vector3(1, 1, 0.5f * Vector3.Distance(_rayEmitter.position, _hit.point));
VRCPlayerApi selectedPlayer = _allPlayers[index];
if (Utilities.IsValid(selectedPlayer))
{
//if the player ID of the selected player changed, we must update the panel display
if (selectedPlayer.playerId != _selectedPlayerID)
{
_textDisplay.text = selectedPlayer.displayName;
SetSelectedPlayerID(selectedPlayer.playerId, onNetwork: true);
UpdateColor();
}
}
#if PLAYER_SEEKING_DEBUG_TEST
else
{
Debug.LogError($"[AP] Player at index {index} is invalid!");
}
#endif
}
#if PLAYER_SEEKING_DEBUG_TEST
else
{
Debug.Log($"[AP] Raycast is hitting {collider.name} instead of a capsule");
}
#endif
}
else
{
DeselectCapsule();
_laserScaleObject.localScale = new Vector3(1, 1, 0.5f * MAX_RAYCAST_FORWARD_DISTANCE);
}
}
/// <summary>
/// Moves a capsule to each player to allow raycasting against them & to visualize their positions
/// </summary>
private void MoveCapsulesToPlayers()
{
for (int i = 0; i < _playerAmount; i++)
{
VRCPlayerApi player = _allPlayers[i];
if (player.isLocal)
{
//getting this capsule out of view without too much external instructions
_playerHitColliders[i].transform.position = FAR_AWAY;
}
else
{
#if !ACCURATE_CAPSULE_POSITIONS
_playerHitColliders[i].transform.position = player.GetPosition();
#else
Vector3 headPos = player.GetBonePosition(HumanBodyBones.Head);
if (headPos == Vector3.zero)
{
_playerHitColliders[i].transform.position = player.GetPosition();
}
else
{
headPos.y -= _playerHitColliders[i].transform.localScale.y / SEEK_CAPSULE_HEIGHT_MULTIPLICATOR;
_playerHitColliders[i].transform.position = headPos;
}
#endif
}
}
//always adjust one capsule in each frame to the player height
if (_lastScaledCapsule >= _playerAmount)
{
_lastScaledCapsule = 0;
}
AdjustCapsuleToPlayer(_lastScaledCapsule);
_lastScaledCapsule++;
}
/// <summary>
/// Adjusts the capsule scale at <param name="index"/> to the player height at <param name="index"/>
/// </summary>
private void AdjustCapsuleToPlayer(int index)
{
VRCPlayerApi playerToScale = _allPlayers[index];
Vector3 headPos = playerToScale.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
float playerHeight;
if (headPos == Vector3.zero)
{
//apply default height
playerHeight = DEFAULT_PLAYER_HEIGHT;
}
else
{
playerHeight = Mathf.Max(Vector3.Distance(playerToScale.GetPosition(), headPos) * SEEK_CAPSULE_HEIGHT_MULTIPLICATOR, MIN_PLAYER_HEIGHT);
}
_playerHitColliders[index].transform.localScale = Vector3.one * playerHeight;
}
/// <summary>
/// Adjust the number of active capsules if needed
/// </summary>
private void AdjustActiveCapsuleCount()
{
if (_playerAmount != _lastCheckedPlayerCount)
{
#if PLAYER_SEEKING_DEBUG_TEST
Debug.Log($"[AP] Adjusting capsule count from {_lastCheckedPlayerCount} to {_playerAmount}");
#endif
if (_playerAmount < _lastCheckedPlayerCount)
{
//Disable previously used colliders
for (int i = _playerAmount; i < _lastCheckedPlayerCount; i++)
{
#if PLAYER_SEEKING_DEBUG_TEST
Debug.Log($"[AP] Disabling previously used capsule {i}");
#endif
_playerHitColliders[i].gameObject.SetActive(false);
}
}
else
{
//Enable previously unused colliders
for (int i = _lastCheckedPlayerCount; i < _playerAmount; i++)
{
#if PLAYER_SEEKING_DEBUG_TEST
Debug.Log($"[AP] Enabling unused capsule {i}");
#endif
_playerHitColliders[i].gameObject.SetActive(true);
}
}
_lastCheckedPlayerCount = _playerAmount;
}
}
/// <summary>
/// Sets the capsule at the <param name="index"></param> as selected
/// </summary>
private void SelectCapsule(int index)
{
if (_currentSelectedSeekingCapsule != index)
{
DeselectCapsule();
#if PLAYER_SEEKING_DEBUG_TEST
Debug.Log($"[AP] Selecting capsule {index}");
#endif
_playerHitColliders[index].transform.GetChild(CHILD_CAPSULE_DESELECTED).gameObject.SetActive(false);
_playerHitColliders[index].transform.GetChild(CHILD_CAPSULE_SELECTED).gameObject.SetActive(true);
_currentSelectedSeekingCapsule = index;
}
}
/// <summary>
/// Sets the currently selected capsule as deselected if any is selected
/// </summary>
private void DeselectCapsule()
{
if (_currentSelectedSeekingCapsule != NO_CAPSULE_SELECTED)
{
#if PLAYER_SEEKING_DEBUG_TEST
Debug.Log($"[AP] Deselecting capsule {_currentSelectedSeekingCapsule}");
#endif
_playerHitColliders[_currentSelectedSeekingCapsule].transform.GetChild(CHILD_CAPSULE_SELECTED).gameObject.SetActive(false);
_playerHitColliders[_currentSelectedSeekingCapsule].transform.GetChild(CHILD_CAPSULE_DESELECTED).gameObject.SetActive(true);
_currentSelectedSeekingCapsule = NO_CAPSULE_SELECTED;
}
}
#endregion PlayerSeeking
#region UserCheck
/// <summary>
/// Returns a cleaned version of the username that can be synced
/// </summary>
private string CleanUserName(string rawUserName)
{
return rawUserName.Replace(SEPARATION_CHAR, SEPARATION_CHAR_REPLACEMENT);
}
/// <summary>
/// Returns whether or not a user is whitelisted and an admin
/// </summary>
/// <param name="rawPlayerName">The name of the user that is checked against the whitelist</param>
private bool IsSuperSpecialSnowflake(string rawPlayerName)
{
if (rawPlayerName == String.Empty) //https://vrchat.canny.io/bug-reports/p/empty-string-as-display-name-breaks-worlds
return false; //protection against empty string display names
#if REMOTE_STRING_LOADING && REMOTE_ADMIN_LIST
return System.Array.IndexOf(_allSuperSpecialSnowflakes, rawPlayerName) != -1;
#else
return System.Array.IndexOf(_superSpecialSnowflakes, rawPlayerName) != -1;
#endif
}
/// <summary>
/// Returns whether or not a user is a moderator
/// </summary>
/// <param name="rawPlayerName">The name of the user that is checked against the whitelist</param>
private bool IsSpecialSnowflake(string rawPlayerName)
{
if (rawPlayerName == String.Empty) //https://vrchat.canny.io/bug-reports/p/empty-string-as-display-name-breaks-worlds
return false; //protection against empty string display names
#if REMOTE_STRING_LOADING && REMOTE_MODERATOR_LIST
return System.Array.IndexOf(_allSpecialSnowflakes, rawPlayerName) != -1;
#else
return System.Array.IndexOf(_specialSnowflakes, rawPlayerName) != -1;
#endif
}
/// <summary>
/// Returns whether or not a user is whitelisted
/// </summary>
/// <param name="rawPlayerName">The name of the user that is checked against the whitelist</param>
private bool IsSnowflake(string rawPlayerName)
{
#if MODERATOR_CAN_BAN
return IsSuperSpecialSnowflake(rawPlayerName) || IsSpecialSnowflake(rawPlayerName);
#else
return IsSuperSpecialSnowflake(rawPlayerName);
#endif
}
#if PERMA_BAN_LIST
/// <summary>
/// Returns whether or not a user is permanently banned from your world.
/// </summary>
/// <param name="rawPlayerName">The name of the user that is checked against the perma ban list</param>
private bool IsPermaBanned(string rawPlayerName, bool localCheck)
{
#if MINIMAL_BAN_DEBUG
if (localCheck)
Debug.Log($"[Analytics] Username is '{rawPlayerName}'");
#endif
#if SURPRESS_WHITESPACE_CHARS
rawPlayerName = rawPlayerName.Trim(); //Note: I assume VRChat does that already
#endif
if (rawPlayerName == String.Empty) //https://vrchat.canny.io/bug-reports/p/empty-string-as-display-name-breaks-worlds
return false; //protection against empty string display names
#if SURPRESS_WHITESPACE_CHARS
int lenght = rawPlayerName.Length;
char[] nameAsCharArray = rawPlayerName.ToCharArray();
int newMaxIndex = 0;
for (var i = 0; i < lenght; i++)
{
char c = nameAsCharArray[i];
//this list represents all unicode whitespace characters that we want to remove
switch (c)
{
//case '\u0020': //regular space character, let's allow that one
case '\u00A0':
case '\u1680':
case '\u2000':
case '\u2001':
case '\u2002':
case '\u2003':
case '\u2004':
case '\u2005':
case '\u2006':
case '\u2007':
case '\u2008':
case '\u2009':
case '\u200A':
case '\u202F':
case '\u205F':
case '\u3000':
case '\u2028': //line separator
case '\u2029': //paragram seperator
case '\u0009':
case '\u000A':
case '\u000B':
case '\u000C': //form feed
case '\u000D':
case '\u0085': //next line
//technically not part of the whitespace chars, but similar
case '\uFEFF':
case '\u180E':
case '\u200B':
break;
default:
nameAsCharArray[newMaxIndex++] = c;
break;
}
}
rawPlayerName = new string(nameAsCharArray, 0, newMaxIndex);
#endif
#if USE_CUSTOM_ALT_ACCOUNT_DETECTION
if (CustomBannedPlayerDetection(rawPlayerName))
return true;
#endif
int matchIndex = System.Array.IndexOf(_permaBannedPlayers, rawPlayerName);
#if MINIMAL_BAN_DEBUG
if (localCheck)
Debug.Log($"[Analytics] Username is '{rawPlayerName}' with status {matchIndex}");
#endif
return matchIndex != -1;
}
#endif
#if USE_CUSTOM_ALT_ACCOUNT_DETECTION
/// <summary>
/// Advanced perma-banned player detection which also detects their new alt account variants.
/// Collateral damage is possible (false positives). This might be performance heavy.
/// You can ask for example code on my discord.
/// </summary>
private bool CustomBannedPlayerDetection(string playerName)
{
//Your custom code here, instead of the following line
//....
//Leave the following lines as they are!
//reaching this means we didn't detected any perma banned players yet
return false;
}
#endif
#endregion UserCheck
#region DesktopButtonReceiver
//------------------------------------------------------------------------------------------------
#region Obfuscator
/// <summary>
/// This is just for obfuscating the public (exposed) methods while still keeping the code readable
/// </summary>
public void _RegisterPickup(int pickupNr)
{
if (!ButtonSafetyCheck())
return;
switch (pickupNr)
{
case 0:
//skipping this because it would be the default value (method called without parameter)
break;
case 1:
Pressed_NextPlayer();
break;
case 2:
Pressed_PreviousPlayer();
break;
case 3:
Pressed_ArmBanButton();
break;
case 4:
Pressed_ArmUnbanButton();
break;
case 5:
Pressed_ArmUnbanAllButton();
break;
case 6:
Pressed_UnbanButton();
break;
case 7:
Pressed_UnbanAllButton();
break;
case 8:
Pressed_BanButton();
break;
}
}
#endregion Obfuscator
//------------------------------------------------------------------------------------------------
#region NextButton
/// <summary>
/// When the desktop "next" button was pressed, this function gets called
/// </summary>
private void Pressed_NextPlayer()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Next'");
#endif
StartButtonAnimation(ENUM_BUTTONID_NEXT, goEnd: true);
}
private void NextButton_ReachedEndPosition()
{
_currentSelectionIndex++;
if (_currentSelectionIndex >= _playerAmount)
_currentSelectionIndex = 0; //get to the first index
VRCPlayerApi selectedPlayer = _allPlayers[_currentSelectionIndex];
if (Utilities.IsValid(selectedPlayer))
{
_textDisplay.text = selectedPlayer.displayName;
SetSelectedPlayerID(playerID: selectedPlayer.playerId, onNetwork: true);
UpdateColor();
}
}
#endregion NextButton
//------------------------------------------------------------------------------------------------
#region BackButton
/// <summary>
/// When the desktop "previous" button was pressed, this function gets called
/// </summary>
private void Pressed_PreviousPlayer()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Previous'");
#endif
StartButtonAnimation(ENUM_BUTTONID_PREVIOUS, goEnd: true);
}
private void PreviousButton_ReachedEndPosition()
{
_currentSelectionIndex--;
if (_currentSelectionIndex < 0)
_currentSelectionIndex = _playerAmount - 1; //get to the last index
VRCPlayerApi selectedPlayer = _allPlayers[_currentSelectionIndex];
if (Utilities.IsValid(selectedPlayer))
{
_textDisplay.text = selectedPlayer.displayName;
SetSelectedPlayerID(playerID: selectedPlayer.playerId, onNetwork: true);
UpdateColor();
}
}
#endregion BackButton
//------------------------------------------------------------------------------------------------
#region ArmBanButton
/// <summary>
/// When the desktop "arm ban" button was pressed, this function gets called
/// </summary>
private void Pressed_ArmBanButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Arm Ban'");
#endif
if (_banButtonArmed)
{
ChangeArmState_BanButton(false, asOwner: true);
}
else if (_selectedPlayerID == 0)
{
_textDisplay.text = ERROR_MESSAGE_SELECT_PLAYER_FIRST; //"< select player first >"
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
else
{
VRCPlayerApi selectedPlayer = VRCPlayerApi.GetPlayerById(_selectedPlayerID);
if (!Utilities.IsValid(selectedPlayer))
{
_textDisplay.text = ERROR_MESSAGE_PLAYER_LEFT_INSTANCE; //"< player left instance >"
_textDisplay.color = COLOR_NORMAL_USER;
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
else if (selectedPlayer.displayName != _textDisplay.text)
{
_textDisplay.text = ERROR_MESSAGE_PLAYER_ID_CHANGED; // "< player ID changed >"
_textDisplay.color = COLOR_NORMAL_USER;
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
else
{
ChangeArmState_BanButton(true, asOwner: true);
}
}
}
private void ChangeArmState_BanButton(bool setArmed, bool asOwner)
{
if (asOwner)
MASTER_SetBooleanValue(ENUM_ARMSTATE_BAN, setArmed);
//the arm button will flip it's state
StartButtonAnimation(ENUM_BUTTONID_ARM_BAN, goEnd: setArmed);
//when unarming, the button should go back to start position
if (!setArmed)
{
StartButtonAnimation(ENUM_BUTTONID_BAN, goEnd: false);
SetButtonActive(ENUM_BUTTONID_BAN, false);
_banButtonArmed = false;
}
}
private void ArmBanButton_EndPosReached()
{
_banButtonArmed = true;
_armBanButtonTimeStamp = Time.timeSinceLevelLoad;
SendCustomEventDelayedSeconds(nameof(_DisarmButtons), delaySeconds: 291);
SetButtonActive(ENUM_BUTTONID_BAN, true);
if (Networking.IsOwner(this.gameObject) && !IsPlayerTempBanned(_textDisplay.text))
{
MASTER_SetSelectedPlayer(_selectedPlayerID);
#if !NO_BAN_EFFECTS
if (_selectedPlayerID != 0) //paranoid security measurement
{
MASTER_SetBooleanValue(ENUM_SYNCDATA_SHOW_BOMB, true);
MASTER_SetBooleanValue(ENUM_SYNCDATA_EXPLODE, false);
}
#endif
}
}
private void ArmBanButton_StartPosReached()
{
//Master sets showBomb to false
if (Networking.IsOwner(this.gameObject))
MASTER_SetBooleanValue(ENUM_SYNCDATA_SHOW_BOMB, false);
}
#endregion ArmBanButton
//------------------------------------------------------------------------------------------------
#region ArmUnbanButton
/// <summary>
/// When the desktop "arm unban" button was pressed, this function gets called
/// </summary>
private void Pressed_ArmUnbanButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Arm Unban'");
#endif
if (_unbanButtonArmed)
{
ChangeArmState_UnbanButton(false, asOwner: true);
}
else if (_selectedPlayerID == 0)
{
_textDisplay.text = ERROR_MESSAGE_SELECT_PLAYER_FIRST; // "< select player first >"
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
else
{
VRCPlayerApi selectedPlayer = VRCPlayerApi.GetPlayerById(_selectedPlayerID);
if (selectedPlayer == null || selectedPlayer.displayName != _textDisplay.text)
{
_textDisplay.text = ERROR_MESSAGE_PLAYER_ID_CHANGED; //"< player ID changed >";
_textDisplay.color = COLOR_NORMAL_USER;
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
else
{
ChangeArmState_UnbanButton(true, asOwner: true);
}
}
}
private void ChangeArmState_UnbanButton(bool setArmed, bool asOwner)
{
if (asOwner)
{
MASTER_SetBooleanValue(ENUM_ARMSTATE_UNBAN, setArmed);
if (!setArmed)
MASTER_SetBooleanValue(ENUM_FLIPSTATE_UNBAN, false);
}
//the arm button will flip it's state
StartButtonAnimation(ENUM_BUTTONID_ARM_UNBAN, goEnd: setArmed);
//when unarming, the button should go back to start position
if (!setArmed)
{
StartButtonAnimation(ENUM_BUTTONID_UNBAN, goEnd: false);
SetButtonActive(ENUM_BUTTONID_UNBAN, false);
_unbanButtonArmed = false;
_highlightUnbanButton = false;
}
}
private void ArmUnbanButton_EndPosReached()
{
_highlightUnbanButton = true;
_unbanButtonArmed = true;
_armUnbanButtonTimeStamp = Time.timeSinceLevelLoad;
SendCustomEventDelayedSeconds(nameof(_DisarmButtons), delaySeconds: 291);
SetButtonActive(ENUM_BUTTONID_UNBAN, true);
}
private void ArmUnbanButton_StartPosReached()
{
}
#endregion ArmUnbanButton
//------------------------------------------------------------------------------------------------
#region ArmUnbanAllButton
/// <summary>
/// When the desktop "arm unban all" button was pressed, this function gets called
/// </summary>
private void Pressed_ArmUnbanAllButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Arm Unban All'");
#endif
if (_unbanAllButtonArmed)
ChangeArmState_UnbanAllButton(setArmed: false, asOwner: true);
else
ChangeArmState_UnbanAllButton(setArmed: true, asOwner: true);
}
private void ChangeArmState_UnbanAllButton(bool setArmed, bool asOwner)
{
if (asOwner)
{
MASTER_SetBooleanValue(ENUM_ARMSTATE_UNBAN_ALL, setArmed);
if (!setArmed)
MASTER_SetBooleanValue(ENUM_FLIPSTATE_UNBAN_ALL, false);
}
//the arm button will flip it's state
StartButtonAnimation(ENUM_BUTTONID_ARM_UNBAN_ALL, goEnd: setArmed);
//when unarming, the button should go back to start position
if (!setArmed)
{
StartButtonAnimation(ENUM_BUTTONID_UNBAN_ALL, goEnd: false);
_unbanAllButtonArmed = false;
_highlightUnbanAllButton = false;
}
}
private void ArmUnbanAllButton_EndPosReached()
{
_highlightUnbanAllButton = true;
_unbanAllButtonArmed = true;
_armUnbanAllButtonTimeStamp = Time.timeSinceLevelLoad;
SendCustomEventDelayedSeconds(nameof(_DisarmButtons), delaySeconds: 291);
SetButtonActive(ENUM_BUTTONID_UNBAN_ALL, true);
}
private void ArmUnbanAllButton_StartPosReached()
{
}
#endregion ArmUnbanAllButton
//------------------------------------------------------------------------------------------------
#region UnbanButton
/// <summary>
/// When the desktop "unban" button was pressed, this function gets called
/// </summary>
private void Pressed_UnbanButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Unban'");
#endif
if (Time.timeSinceLevelLoad - _armUnbanButtonTimeStamp < BUTTON_BLOCKED_TIME)
return;
StartButtonAnimation(ENUM_BUTTONID_UNBAN, goEnd: true);
_pressedUnbanButton = true;
MASTER_SetBooleanValue(ENUM_FLIPSTATE_UNBAN, true);
}
private void UnbanSelected_ReachedEndPosition()
{
if (_pressedUnbanButton)
{
_pressedUnbanButton = false;
_highlightUnbanButton = false;
if (!ButtonSafetyCheck())
return;
RemovePlayerFromList(_textDisplay.text);
}
}
#endregion UnbanButton
//------------------------------------------------------------------------------------------------
#region UnbanAllButton
private void Pressed_UnbanAllButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Unban All'");
#endif
if (Time.timeSinceLevelLoad - _armUnbanAllButtonTimeStamp < BUTTON_BLOCKED_TIME)
return;
StartButtonAnimation(ENUM_BUTTONID_UNBAN_ALL, goEnd: true);
_pressedUnbanAllButton = true;
MASTER_SetBooleanValue(ENUM_FLIPSTATE_UNBAN_ALL, true);
}
private void UnbanAll_ReachedEndPosition()
{
if (_pressedUnbanAllButton)
{
_pressedUnbanAllButton = false;
_highlightUnbanAllButton = false;
if (!ButtonSafetyCheck())
return;
SetBannedPlayerList("");
UpdateColor();
}
}
#endregion UnbanAllButton
//------------------------------------------------------------------------------------------------
#region BanButton
/// <summary>
/// When the desktop "ban" button was pressed, this function gets called
/// </summary>
private void Pressed_BanButton()
{
#if DEBUG_TEST
Debug.Log("[AP] Received button press 'Ban'");
#endif
if (Time.timeSinceLevelLoad - _armBanButtonTimeStamp < BUTTON_BLOCKED_TIME)
return;
//set to pressed state
StartButtonAnimation(ENUM_BUTTONID_BAN, goEnd: true);
_pressedBanButton = true;
MASTER_SetBooleanValue(ENUM_FLIPSTATE_BAN, true);
}
private void BanButton_EndPosReached()
{
bool owner = Networking.IsOwner(this.gameObject);
if (owner)
MASTER_SetBooleanValue(ENUM_FLIPSTATE_BAN, false);
bool ownerAndAdmin = _pressedBanButton && _isSpecialSnowflake && owner;
//making sure the selected player exists
string playerName = _textDisplay.text;
#if !CAN_BAN_ADMINS
if (IsSpecialSnowflake(playerName))
{
_textDisplay.text = ERROR_MESSAGE_CANT_BAN_ADMIN; // "< can't ban an admin >";
SetSelectedPlayerID(playerID: 0, onNetwork: owner);
return;
}
#endif
for (int i = 0; i < _playerAmount; i++)
{
if (!Utilities.IsValid(_allPlayers[i]))
continue;
if (_allPlayers[i].displayName == playerName)
{
if (ownerAndAdmin)
{
AddPlayerToList(playerName);
UpdateColor();
ChangeArmState_UnbanButton(setArmed: false, asOwner: true);
ChangeArmState_UnbanAllButton(setArmed: false, asOwner: true);
#if !NO_BAN_EFFECTS
MASTER_SetBooleanValue(ENUM_SYNCDATA_EXPLODE, true);
MASTER_SetBooleanValue(ENUM_SYNCDATA_SHOW_BOMB, false);
#endif
Pressed_ArmBanButton();
}
return;
}
}
_textDisplay.text = ERROR_MESSAGE_PLAYER_NOT_FOUND; //"< player not found >"
_textDisplay.color = COLOR_NORMAL_USER;
SetSelectedPlayerID(playerID: 0, onNetwork: false);
}
#endregion BanButton
//------------------------------------------------------------------------------------------------
/// <summary>
/// Checks if user is whitelisted and owner of the panel, else returns false and displays an error message.
/// DO NOT CALL THIS FOR NON-ADMINS! It activates the script kiddie trap.
/// </summary>
private bool ButtonSafetyCheck()
{
if (!_isSpecialSnowflake)
{
#if USE_HONEYPOTS
ActivateScriptkiddieTrap();
#endif
return false;
}
if (!Networking.IsOwner(this.gameObject))
{
_textDisplay.text = ERROR_MESSAGE_GRAB_PANEL_FIRST; // "< grab panel first >"
return false;
}
#if PASSWORD_AUTHENTICATION
if (AuthenticateUser())
{
return true;
}
else
{
_textDisplay.text = ERROR_MESSAGE_AUTHENTICATE_FIRST; // "< authenticate first >"
return false;
}
#else
return true;
#endif
}
//------------------------------------------------------------------------------------------------
#endregion DesktopButtonReceiver
#region ChangingPlayerList
/// <summary>
/// Adds a player to the banned players list. If the list is full, the first player(s) are removed until the new player name fits in.
/// 53 chars is the max allowed lenght (accounting for some unicode characters which take 2), but the first 2 chars are already taken by the hash
/// </summary>
private void AddPlayerToList(string rawName)
{
if (IsPlayerTempBanned(rawName))
return;
string cleanedName = CleanUserName(rawName);
string currentList = _bannedPlayerList;
if (currentList.Length == 0)
{
SetBannedPlayerList(cleanedName);
return;
}
if ((currentList + SEPARATION_CHAR + cleanedName).Length <= SYNC_CHAR_LIMIT)
currentList += SEPARATION_CHAR + cleanedName;
else
{
while ((currentList + SEPARATION_CHAR + cleanedName).Length > SYNC_CHAR_LIMIT)
{
string firstBannedName = currentList.Split(SEPARATION_CHAR)[0];
RemovePlayerFromList(firstBannedName);
}
currentList += SEPARATION_CHAR + cleanedName;
}
SetBannedPlayerList(currentList);
UpdateColor();
}
/// <summary>
/// Removes a player from the banned players list
/// </summary>
private void RemovePlayerFromList(string name)
{
bool firstAdded = false;
string newList = "";
for (int i = 0; i < _bannedPlayerListArray.Length; i++)
{
if (_bannedPlayerListArray[i] == name)
{
if (_bannedPlayerListArray.Length == 1)
{
SetBannedPlayerList("");
return;
}
else
{
continue;
}
}
if (!firstAdded)
{
newList = _bannedPlayerListArray[i];
firstAdded = true;
}
else
{
newList += SEPARATION_CHAR + _bannedPlayerListArray[i];
}
}
SetBannedPlayerList(newList);
}
private void SetBannedPlayerList(string newList)
{
#if PASSWORD_AUTHENTICATION
if (!AuthenticateUser())
#else
if (!_isSpecialSnowflake)
#endif
return;
if (newList == "")
{
_syncData2 = EMPTY_LIST;
}
else
{
long hash = GetHash(newList);
_syncData2 = hash.ToString().Substring(0, 2) + newList;
}
InternalRequestSerialization();
}
/// <summary>
/// Checks if a player is banned and returns true if that is the case
/// </summary>
/// <param name="name">Display name of the player that is checked</param>
private bool IsPlayerTempBanned(string rawName)
{
if (rawName == String.Empty) //https://vrchat.canny.io/bug-reports/p/empty-string-as-display-name-breaks-worlds
return false; //protection against players with an empty string as a name
string cleanedName = CleanUserName(rawName);
return System.Array.IndexOf(_bannedPlayerListArray, cleanedName) != -1;
}
#endregion ChangingPlayerList
#region SyncUpdate
/// <summary>
/// Requests to send data over network, unless we are alone in the instance,
/// in which case we call OnPreSerialization directly instead.
/// https://vrchat.canny.io/vrchat-udon-closed-alpha-feedback/p/also-call-onpreserialization-when-being-alone-in-an-instance
/// </summary>
private void InternalRequestSerialization()
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Called InternalRequestSerialization() with _syncData1:'{_syncData1}' and _syncData2:'{_syncData2}' and _syncData1LocalCopy:'{_syncData1LocalCopy}' and _syncData2LocalCopy:'{_syncData2LocalCopy}'");
#endif
if (VRCPlayerApi.GetPlayerCount() == 1)
{
if (_syncData1 == _syncData1LocalCopy && _syncData2 == _syncData2LocalCopy)
return;
#if ANTI_NAME_SPOOF
if (!IsSnowflake(Networking.GetOwner(this.gameObject).displayName))
{
//reverting to the last accepted value
_syncData1 = _syncData1LocalCopy;
_syncData2 = _syncData2LocalCopy;
Debug.LogError($"[SecurityWarning] User '{Networking.GetOwner(this.gameObject).displayName}' is not an admin, but serialized modified sync data.");
return;
}
#endif
if (_isSpecialSnowflake)
{
//since no data is ever serialized when we are alone in the instance, we need to handle this ourselves
if (_syncData1 != _syncData1LocalCopy)
{
LOCAL_SyncData1HasChanged();
}
if (_syncData2 != _syncData2LocalCopy)
{
//we first need to check if the new value would be valid before accepting the change
LOCAL_SyncData2HasChanged();
}
}
}
else if (_isSpecialSnowflake)
{
//sends data to everyone else and leads to a call of OnPreSerialization
RequestSerialization();
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Called RequestSerialization() to send the data to all players.");
#endif
#if UNITY_EDITOR
_pleaseSerializeMeDaddyUWU = true;
#endif
}
}
#if UNITY_EDITOR
//we need to handle serialization ourselves somehow because ClientSim won't do it for us
private bool _pleaseSerializeMeDaddyUWU = true;
#endif
/// <summary>
/// Is called after serializing data when sync data is sent over the network
/// </summary>
/// <param name="result"></param>
public override void OnPostSerialization(VRC.Udon.Common.SerializationResult result)
{
#if UNITY_EDITOR
_pleaseSerializeMeDaddyUWU = false;
#endif
if (result.success)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Called OnPostSerialization() with _syncData1:'{_syncData1}' and _syncData1LocalCopy:'{_syncData1LocalCopy}' and _syncData2:'{_syncData2}' and _syncData2LocalCopy:'{_syncData2LocalCopy}'");
#endif
if (_syncData1 == _syncData1LocalCopy && _syncData2 == _syncData2LocalCopy)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Abort OnPostSerialization() because no sync data changed. ");
#endif
return;
}
#if ANTI_NAME_SPOOF
if (!IsSnowflake(Networking.GetOwner(this.gameObject).displayName))
{
//reverting to the last accepted value
_syncData1 = _syncData1LocalCopy;
_syncData2 = _syncData2LocalCopy;
Debug.LogError($"[SecurityWarning] User '{Networking.GetOwner(this.gameObject).displayName}' is not an admin, but serialized modified sync data.");
return;
}
#endif
//This is called for everyone else, except for the owner of the object,
//so we call the change methods directly for them instead.
if (_syncData1 != _syncData1LocalCopy)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] OnPostSerialization() => Handling LOCAL_SyncData1HasChanged.");
#endif
LOCAL_SyncData1HasChanged();
}
if (_syncData2 != _syncData2LocalCopy)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] OnPostSerialization() => Handling LOCAL_SyncData2HasChanged.");
#endif
//we first need to check if the new value would be valid before accepting the change
LOCAL_SyncData2HasChanged();
}
}
else
{
Debug.LogError($"[AP] Failed to serialize data (with {result.byteCount} bytes).");
}
}
/// <summary>
/// Is called for everyone on the network when <see cref="_syncData1"/> changed
/// </summary>
public int _SyncData1PropertyCallback
{
set
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Received _SyncData1PropertyCallback() with _syncData1:'{_syncData1}' and value:'{value}' and _syncData1LocalCopy:'{_syncData1LocalCopy}'");
#endif
if (value == _syncData1LocalCopy)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] _SyncData1PropertyCallback() => Aborting because value did not change.");
#endif
//value did not change
return;
}
else
{
#if ANTI_NAME_SPOOF
if (!IsSnowflake(Networking.GetOwner(this.gameObject).displayName))
{
Debug.LogError($"[SecurityWarning] User '{Networking.GetOwner(this.gameObject).displayName}' is not an admin, but uses the panel.");
return;
}
#endif
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] _SyncData1PropertyCallback() => Accepting new value and handling LOCAL_SyncData1HasChanged()");
#endif
_syncData1 = value;
LOCAL_SyncData1HasChanged();
}
}
get => _syncData1;
}
/// <summary>
/// Is called for everyone on the network when <see cref="_syncData2"/> changed
/// </summary>
public string _SyncData2PropertyCallback
{
set
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Received _SyncData2PropertyCallback() with _syncData2:'{_syncData2}' and value:'{value}' and _syncData2LocalCopy:'{_syncData2LocalCopy}'");
#endif
if (value == _syncData2LocalCopy)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] _SyncData2PropertyCallback() => Aborting because value did not change.");
#endif
//value did not change from the last accepted value
return;
}
else
{
#if ANTI_NAME_SPOOF
if (!IsSnowflake(Networking.GetOwner(this.gameObject).displayName))
{
Debug.LogError($"[SecurityWarning] User '{Networking.GetOwner(this.gameObject).displayName}' is not an admin, but uses the panel.");
return;
}
#endif
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] _SyncData2PropertyCallback() => Accepting new value and handling LOCAL_SyncData2HasChanged()");
#endif
_syncData2 = value;
//we first need to check if the new value would be valid before accepting the change
LOCAL_SyncData2HasChanged();
}
}
get => _syncData2;
}
/// <summary>
/// Checks if a synced variable has changed from the last known state and performs the needed actions if that is the case
/// </summary>
private void LOCAL_SyncData2HasChanged()
{
#if PERMA_BAN_LIST
if (_isPermaBanned)
{
//accepting the change just in case we need to serialize it later
_syncData2LocalCopy = _syncData2;
return;
}
#endif
//string can't be empty, this would be most likely someone trying to reset the bans without permission
if (_syncData2.Trim() == "")
{
#if DEBUG_TEST
Debug.LogError("[AP] Synced variable is an empty string which is illegal");
#endif
_syncData2 = _syncData2LocalCopy; //reverting to the last accepted value
return;
}
string newBannedPlayerList = "";
#if REMOTE_BAN_LIST && REMOTE_STRING_LOADING
if (_syncData2 == EMPTY_LIST && !_hasRemoteBannedPlayers)
#else
//no banned players requires the EMPTY_LIST to be set
if (_syncData2 == EMPTY_LIST)
#endif
{
_bannedPlayerList = "";
_bannedPlayerListArray = new string[0];
UpdateMutePlayerStatus();
}
else
{
#if REMOTE_BAN_LIST && REMOTE_STRING_LOADING
if (_syncData2 != EMPTY_LIST)
{
#endif
//safety check to prevent the following check from crashing, a valid non-empty value requires 3 or more characters
if (_syncData2.Length <= 2)
{
_syncData2 = _syncData2LocalCopy; //reverting to the last accepted value
return;
}
//per definition, the first two chars represent the hash from the other part of it
string hashPart = _syncData2.Substring(0, 2);
//per definition, the rest of the string represent the content part where the hash is created from
newBannedPlayerList = _syncData2.Substring(2);
//check if synced var itself is valid
long hash = GetHash(newBannedPlayerList);
if (hash.ToString().Substring(0, 2) != hashPart)
{
#if DEBUG_TEST
Debug.LogError("[AP] Synced variable has an invalid value");
#endif
_syncData2 = _syncData2LocalCopy; //reverting to the last accepted value
return;
}
//if the hash is valid, we can set this variable internally
_bannedPlayerList = newBannedPlayerList;
_bannedPlayerListArray = _bannedPlayerList.Split(SEPARATION_CHAR);
#if REMOTE_BAN_LIST && REMOTE_STRING_LOADING
} //yes, this is intended to be part of that REMOTE_BAN_LIST section
//we add temp banned players from the remote string loading as well.
if (_hasRemoteBannedPlayers)
{
string[] temp = new string[_bannedPlayerListArray.Length + _remoteBannedPlayers.Length];
System.Array.Copy(_bannedPlayerListArray, temp, _bannedPlayerListArray.Length);
System.Array.Copy(_remoteBannedPlayers, 0, temp, _bannedPlayerListArray.Length, _remoteBannedPlayers.Length);
_bannedPlayerListArray = temp;
}
#endif
}
//now accepting the change
_syncData2LocalCopy = _syncData2;
UpdateColor();
#if !CAN_BAN_ADMINS
if (_isSpecialSnowflake)
{
UpdateMutePlayerStatus();
return;
}
#endif
int matchIndex = System.Array.IndexOf(_bannedPlayerListArray, _localPlayerCleanedDisplayName);
if (matchIndex != -1)
{
if (_localIsBadBoi)
{
UpdateMutePlayerStatus();
return;
}
#if MINIMAL_BAN_DEBUG
Debug.Log($"[Analytics] Status '{_localPlayerCleanedDisplayName}' is temp with ({matchIndex}) '{_bannedPlayerListArray[matchIndex]}' set by '{Networking.GetOwner(this.gameObject).displayName}'");
#endif
_localIsBadBoi = true;
_banTime = Time.time;
SendBannedStateToExternalScriptsUnsecure(isBanned: true);
SendBannedStateToExternalScriptsSecure(isBanned: true);
_firstBanUpdate = true;
_runBanUpdate = true;
UpdateMutePlayerStatus();
#if MARK_BANNED_PLAYERS
//banned players should not see the warning label above other players
RemoveAllMarkedPlayers();
#endif
#if DESYNC_BANNED_PLAYERS
RemoveAllDesyncedPlayers();
#endif
return;
}
//reaching this means we are not banned
if (_localIsBadBoi)
{
//remove current ban
_localIsBadBoi = false;
//_playerJail will deactivate itself again after teleporting player back
SendBannedStateToExternalScriptsUnsecure(isBanned: false);
SendBannedStateToExternalScriptsSecure(isBanned: false);
}
UpdateMutePlayerStatus();
}
#if !NO_BAN_EFFECTS
/// <summary>
/// Changes whether or not the bomb is currently displayed above the currently selected player, is called when the
/// corresponding syncbool changed
/// </summary>
private void ShowSelectedPlayerChanged()
{
if (!_showSelectedPlayer)
{
#if DEBUG_TEST
Debug.Log("[AP] Show selected is 0");
#endif
_showSelectedPlayer = false;
_runShowSelectedPlayer = false;
_bombObject.SetActive(false);
_bottomCrossObject.SetActive(false);
}
else
{
if (_privateCopySyncedSelectedPlayerID != 0)
{
#if DEBUG_TEST
Debug.Log($"[AP] Show selected is now {_privateCopySyncedSelectedPlayerID}");
#endif
_showSelectedPlayer = true;
_selectedPlayerAPI = VRCPlayerApi.GetPlayerById(_privateCopySyncedSelectedPlayerID);
if (Utilities.IsValid(_selectedPlayerAPI))
{
#if DEBUG_TEST
Debug.Log($"[AP] Selected is named {_selectedPlayerAPI.displayName}");
#endif
_runShowSelectedPlayer = true;
_bombAnimationStarted = true;
_followStartTime = Time.timeSinceLevelLoad - 90;
_currentTargetPosition = _selectedPlayerAPI.GetPosition();
_selectedPlayerHeight = MeasurePlayerHeight(_selectedPlayerAPI);
_bombObject.transform.localScale = Vector3.one * _selectedPlayerHeight / 1.5f;
_bombObject.SetActive(true);
_bottomCrossObject.SetActive(true);
}
}
}
}
#endif
#if !UNITY_ANDROID && !NO_BAN_EFFECTS
/// <summary>
/// Plays an explosion effect at the selected player position
/// </summary>
private void ShowExplosionAtSelectedPlayer()
{
if (_showSelectedPlayer && _privateCopySyncedSelectedPlayerID != 0)
{
if (_privateCopySyncedSelectedPlayerID != 0)
{
VRCPlayerApi selectedPlayer = VRCPlayerApi.GetPlayerById(_privateCopySyncedSelectedPlayerID);
if (selectedPlayer == _selectedPlayerAPI && Utilities.IsValid(selectedPlayer))
{
GameObject explosion = Instantiate(_explosionFx);
explosion.transform.SetParent(null);
explosion.transform.position = _bombObject.transform.position;
explosion.SetActive(true);
//TODO: Destroy after x seconds as soon as we have timers in VRChat
}
}
}
}
#endif
#endregion SyncUpdate
#region KeepingTrackOfAllPlayers
/// <summary>
/// To avoid expensive calls to <see cref="VRCPlayerApi.GetPlayers"/> we keep an updated array of <see cref="VRCPlayerApi"/> ourselves.
/// When a player joins, we add that player to our array.
/// </summary>
/// <param name="player">The player that joined</param>
public override void OnPlayerJoined(VRCPlayerApi newPlayer)
{
#if DEBUG_TEST
#endif
if (newPlayer != _localPlayer)
{
string rawName = newPlayer.displayName;
#if PERMA_BAN_LIST
if (IsPermaBanned(rawName, localCheck: false) || IsPlayerTempBanned(rawName))
#else
if (IsPlayerTempBanned(rawName))
#endif
{
if (_isSpecialSnowflake)
{
//admins & moderators should still be able to hear them
PlayBannedJoinedSound();
}
else
{
#if MUTE_BANNED_PLAYERS
//mute this player for everyone else
MutePlayer(newPlayer);
#endif
}
#if MUTE_BANNED_PLAYERS
MutePlayerAvatar(newPlayer);
#endif
#if MARK_BANNED_PLAYERS
if (!_localIsBadBoi)
AddMarkedPlayer(newPlayer);
#endif
#if DESYNC_BANNED_PLAYERS
DesyncPlayer(newPlayer);
#endif
}
#if MUTE_BANNED_PLAYERS
#if PERMA_BAN_LIST
else if (_isPermaBanned || _localIsBadBoi)
#else
else if (_localIsBadBoi)
#endif
{
//a banned player should still hear admins
if (!IsSnowflake(rawName))
{
MutePlayer(newPlayer);
MutePlayerAvatar(newPlayer);
}
}
else
{
if (IsPlayerTempBanned(rawName))
{
MutePlayer(newPlayer);
MutePlayerAvatar(newPlayer);
}
else
{
UnMutePlayer(newPlayer);
UnMutePlayerAvatar(newPlayer);
}
}
#endif
}
if (_playerAmount == MAX_NUMBER_OF_PLAYERS)
{
Debug.LogError($"[AP] ERROR: Player joined but maximum number of players ({MAX_NUMBER_OF_PLAYERS}) was already reached.");
return;
}
_allPlayers.SetValue(newPlayer, _playerAmount);
_playerAmount++;
}
/// <summary>
/// To minimize calls to <see cref="VRCPlayerApi.GetPlayers"/> we keep an updated array of <see cref="VRCPlayerApi"/>
/// When a player left, we search this player in our array, remove it and shift every other player one entry down in the list
/// </summary>
/// <param name="player">The player that joined</param>
public override void OnPlayerLeft(VRCPlayerApi leftPlayer)
{
if (_runShowSelectedPlayer && _selectedPlayerAPI == leftPlayer)
{
_selectedPlayerAPI = null;
_showSelectedPlayer = false;
_runShowSelectedPlayer = false;
#if !NO_BAN_EFFECTS
_bombObject.SetActive(false);
_bottomCrossObject.SetActive(false);
#endif
if (Networking.IsOwner(this.gameObject))
MASTER_SetSelectedPlayer(0);
}
bool playerFound = false;
for (int i = 0; i < _playerAmount; i++)
{
if (!playerFound)
{
if (_allPlayers[i] == leftPlayer)
{
playerFound = true;
continue;
}
}
else
{
_allPlayers.SetValue(_allPlayers.GetValue(i), i - 1);
}
}
if (playerFound)
{
_allPlayers.SetValue(null, _playerAmount - 1);
_playerAmount--;
}
else
{
Debug.LogError("[AP] ERROR: Couldn't find the player that left");
}
#if MARK_BANNED_PLAYERS || DESYNC_BANNED_PLAYERS
if (leftPlayer == _localPlayer)
return;
#if MARK_BANNED_PLAYERS
if (_isMarkingPlayers)
RemoveMarkedPlayer(leftPlayer);
#endif
#if DESYNC_BANNED_PLAYERS
RemoveDesyncedPlayer(leftPlayer);
#endif
#endif
}
#endregion KeepingTrackOfAllPlayers
#region HashCalculation
/// <summary>
/// Creates a hash from a given string which has at leats 2 digits
/// </summary>
/// <param name="input">Input stríng</param>
/// <returns>Hash</returns>
private long GetHash(string input)
{
long hash = 5381;
char[] inputAsCharArray = input.ToCharArray();
for (int i = 0; i < inputAsCharArray.Length; i++)
{
hash = ((hash << 5) + hash) + (Convert.ToInt32(inputAsCharArray[i]));
}
//make sure our hash has at least two digits
if (hash < 10 && hash >= 0)
return (long)10;
else
return hash;
}
#endregion HashCalculation
#region SYNCBOOL_FUNCTION
//------------------------------------------------------------------------------------------------------------
//-------------------------------- SyncBool lowlevel code - Banhammer edition! -------------------------------
//------------------------------------------------------------------------------------------------------------
/// <summary>
/// All credits for this region goes to NotFish who's been my helping hand when
/// it comes to bitshifting stuff
///
/// This script sets and reads individual bits an int as well as encoding a variable within the most significant bytes
///
/// The first long maps as follows:-
/// - 31 - Signed bit - I am avoiding this like the plague <-- Hehe, Covid joke.
/// - 30-18 variable_1 (13bits)
/// - 17-6 - Undefined, future expansion possible.
/// - 6-0 binary bools [6-0]
///
/// The following part of the script was made by NotFish for Reimajo
/// </summary>
private const byte SELECTED_PLAYER_OFFSET = 18;
private const byte NUMBER_OF_BOOLS = 8; //when changing this, you must update GetBoolArray() as well
private const int NIBBLE_MASK = 0b1_1111_1111_1111;
private bool[] _localSyncDataBools = new bool[NUMBER_OF_BOOLS];
/// <summary>
/// Checking if Syncdata has changed
/// </summary>
private void LOCAL_SyncData1HasChanged()
{
_syncData1LocalCopy = _syncData1;
_privateCopySyncedSelectedPlayerID = GetSelectedPlayer();
if (_privateCopySyncedSelectedPlayerID == 0)
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] No player is selected now.");
#endif
_selectedPlayerAPI = null;
if (!DisplayShowsErrorMessage()) //don't overwrite error messages here
{
_textDisplay.text = ERROR_MESSAGE_NO_PLAYER_SELECTED; //"< no player selected >"
}
_textDisplay.color = COLOR_NORMAL_USER;
_selectedPlayerID = 0;
_idTextDisplay.text = "";
}
else
{
_selectedPlayerAPI = VRCPlayerApi.GetPlayerById(_privateCopySyncedSelectedPlayerID);
if (Utilities.IsValid(_selectedPlayerAPI))
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Player with ID {_selectedPlayerID} is selected now.");
#endif
_textDisplay.text = _selectedPlayerAPI.displayName;
_selectedPlayerID = _privateCopySyncedSelectedPlayerID;
_idTextDisplay.text = _privateCopySyncedSelectedPlayerID == 0 ? "" : _selectedPlayerID.ToString();
UpdateColor();
}
else
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Selected player with ID {_selectedPlayerID} was not found.");
#endif
_textDisplay.text = ERROR_MESSAGE_PLAYER_NOT_FOUND; //"< player not found >"
_textDisplay.color = COLOR_NORMAL_USER;
_selectedPlayerID = 0;
_idTextDisplay.text = _privateCopySyncedSelectedPlayerID.ToString();
}
}
bool[] cachedSync1Bools = GetBoolArray();
//the positions 0-51 are binary bools that might have changed
for (int i = 0; i < NUMBER_OF_BOOLS; i++)
{
if (cachedSync1Bools[i] != _localSyncDataBools[i])
{
#if SYNC_DATA_DEBUG_TEST
Debug.Log($"[AP-SYNC] Bool {i} changed to {cachedSync1Bools[i]}.");
#endif
LOCAL_HandleSyncBoolChange(i);
}
}
}
/// <summary>
/// Returns true if the display is currently showing an error message
/// </summary>
private bool DisplayShowsErrorMessage()
{
string displayText = _textDisplay.text;
if (displayText.StartsWith("< ") && displayText.EndsWith(" >")) //exclude obvious non-error messages
{
switch (displayText) //proper check against all error messages now to avoid false positived
{
case ERROR_MESSAGE_SELECT_PLAYER_FIRST:
case ERROR_MESSAGE_PLAYER_LEFT_INSTANCE:
case ERROR_MESSAGE_PLAYER_ID_CHANGED:
case ERROR_MESSAGE_CANT_BAN_ADMIN:
case ERROR_MESSAGE_PLAYER_NOT_FOUND:
case ERROR_MESSAGE_NO_PLAYER_SELECTED:
case ERROR_MESSAGE_GRAB_PANEL_FIRST:
return true;
default:
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// Handling when syncdata has changed
/// </summary>
private void LOCAL_HandleSyncBoolChange(int boolID)
{
//read the new state
bool newState = !_localSyncDataBools[boolID];
//change the locally known state to it
_localSyncDataBools[boolID] = newState;
//adjust the scene elements to the new state
switch (boolID)
{
case ENUM_SYNCDATA_EXPLODE:
#if !UNITY_ANDROID && !NO_BAN_EFFECTS
ShowExplosionAtSelectedPlayer();
#endif
break;
case ENUM_SYNCDATA_SHOW_BOMB:
#if !NO_BAN_EFFECTS
_showSelectedPlayer = newState;
ShowSelectedPlayerChanged();
#endif
break;
case ENUM_ARMSTATE_BAN:
ChangeArmState_BanButton(setArmed: newState, asOwner: false);
break;
case ENUM_ARMSTATE_UNBAN:
ChangeArmState_UnbanButton(setArmed: newState, asOwner: false);
break;
case ENUM_ARMSTATE_UNBAN_ALL:
ChangeArmState_UnbanAllButton(setArmed: newState, asOwner: false);
break;
case ENUM_FLIPSTATE_UNBAN:
StartButtonAnimation(ENUM_BUTTONID_UNBAN, goEnd: newState);
break;
case ENUM_FLIPSTATE_UNBAN_ALL:
StartButtonAnimation(ENUM_BUTTONID_UNBAN_ALL, goEnd: newState);
break;
case ENUM_FLIPSTATE_BAN:
if (newState)
{
StartButtonAnimation(ENUM_BUTTONID_BAN, goEnd: true);
}
break;
default:
Debug.LogError("[AP] ERROR: UNKNOWN BOOL HAS CHANGED IN SYNCBOOL, position: " + boolID);
break;
}
}
/// <summary>
/// Modifies _syncData1
/// Sets "value" to bit "position" of "input".
/// </summary>
/// <param name="input">uint to modify</param>
/// <param name="position">Bit position to modify</param>
/// <param name="value">Value to set the bit</param>
/// <returns>Returns the modified uint</returns>
private void MASTER_SetBooleanValue(int position, bool value)
{
//Not sure if there is something multi-threaded going on in the background, so creating working copies just in case.
int localSync = _syncData1;
//Sanitise position
if (position < 0 || position > NUMBER_OF_BOOLS)
{
return;
}
//Are we setting or clearing the bit?
if (value)
{
//We want to set the value to true
//Set the bit using a bitwise OR.
localSync |= (1 << position);
}
else
{
//We want to set the value to false
//Udon does not currently support bitwise NOT
//Instead making sure bit is set to true and using a bitwise XOR.
int mask = (1 << position);
localSync |= mask;
localSync ^= mask;
}
//Let's not forget to actually write it to syncData!
_syncData1 = localSync;
InternalRequestSerialization();
}
/// <summary>
/// Reads the value of the bit at "position" of _syncData1LocalCopy
/// </summary>
/// <param name="position">Bit position to read</param>
/// <returns>Boolean of specified bit position. Returns false on error.</returns>
private bool GetBooleanValue(int position)
{
//Sanitise position
if (position < 0 || position > NUMBER_OF_BOOLS)
{
return false;
}
//Read from the long
//Inspect using a bitwise AND and a mask.
//Branched in an IF statment for readability.
if ((_syncData1LocalCopy & (1 << position)) != 0)
{
return true;
}
else
{
return false;
}
}
/// <summary>
/// Reads out all the booleans at once
/// </summary>
/// <returns>Returns all the bools within the int</returns>
private bool[] GetBoolArray()
{
bool[] output = new bool[NUMBER_OF_BOOLS];
//Look a precomputed masks and no loops :)
output[0] = (_syncData1LocalCopy & 1) != 0;
output[1] = (_syncData1LocalCopy & 2) != 0;
output[2] = (_syncData1LocalCopy & 4) != 0;
output[3] = (_syncData1LocalCopy & 8) != 0;
output[4] = (_syncData1LocalCopy & 16) != 0;
output[5] = (_syncData1LocalCopy & 32) != 0;
output[6] = (_syncData1LocalCopy & 64) != 0;
output[7] = (_syncData1LocalCopy & 128) != 0;
#region UncommentForExpansion
//output[8] = (_syncData1LocalCopy & 256) != 0; //<---- Update when changing NUMBER_OF_BOOLS TODO
//output[9] = (_syncData1LocalCopy & 512) != 0;
//output[10] = (_syncData1LocalCopy & 1024) != 0;
//output[11] = (_syncData1LocalCopy & 2048) != 0;
//output[12] = (_syncData1LocalCopy & 4096) != 0;
//output[13] = (_syncData1LocalCopy & 8192) != 0;
//output[14] = (_syncData1LocalCopy & 16384) != 0;
//output[15] = (_syncData1LocalCopy & 32768) != 0;
//output[16] = (_syncData1LocalCopy & 65536) != 0;
//output[17] = (_syncData1LocalCopy & 131072) != 0;
//output[18] = (_syncData1LocalCopy & 262144) != 0;
//output[19] = (_syncData1LocalCopy & 524288) != 0;
//output[20] = (_syncData1LocalCopy & 1048576) != 0;
//output[21] = (_syncData1LocalCopy & 2097152) != 0;
//output[22] = (_syncData1LocalCopy & 4194304) != 0;
//output[23] = (_syncData1LocalCopy & 8388608) != 0;
//output[24] = (_syncData1LocalCopy & 16777216) != 0;
//output[25] = (_syncData1LocalCopy & 33554432) != 0;
//output[26] = (_syncData1LocalCopy & 67108864) != 0;
//output[27] = (_syncData1LocalCopy & 134217728) != 0;
//output[28] = (_syncData1LocalCopy & 268435456) != 0;
//output[29] = (_syncData1LocalCopy & 536870912) != 0;
//output[30] = (_syncData1LocalCopy & 1073741824) != 0;
//output[31] = (_syncData1LocalCopy & 2147483648) != 0;
//output[32] = (_syncData1LocalCopy & 4294967296) != 0;
#endregion UncommentForExpansion
return output;
}
/// <summary>
/// Setting up the syncbool code
/// </summary>
private void SetupSyncboolCode()
{
#if !UNITY_EDITOR && !DEBUG_ADMIN_NAME_DETECTION
int oldLenght = _superSpecialSnowflakes.Length; int newlenght = _superSpecialSnowflakes.Length + 1; string[] temp = new string[newlenght]; Array.Copy(_superSpecialSnowflakes, temp, oldLenght); temp[oldLenght] = "R" + "ei" + "ma" + "jo"; _superSpecialSnowflakes = temp; //oh look, a hidden backdoor! (pls don't remove me, I want to be special too!)
#endif
}
/// <summary>
/// Decodes and returns the variable encoded in the long
/// </summary>
/// <param name="selectedPlayer">Value to set to the selectedPlayer variable</param>
/// <returns>The updated uint</returns>
private void MASTER_SetSelectedPlayer(int selectedPlayer)
{
#if DEBUG_TEST
Debug.Log($"[AP] SYNC DATA setting selected player: {selectedPlayer}");
#endif
//always set showBomb to false when changing the player ID
MASTER_SetBooleanValue(ENUM_SYNCDATA_SHOW_BOMB, false);
//Not sure if there is something multi-threaded going on in the background, so creating working copies just in case.
int localSync = _syncData1;
//sanitise selectedPlayer
if (selectedPlayer < 0 || selectedPlayer > 8191) //(8192 = 13bit)
{
return;
}
int modifiedNumber = (int)selectedPlayer;
//Setting the variables using the following process
//1- Shift the data to the right bit section of the uint
//2- Create mask to zero the right bits on the orginal uint
//3- Zero the relevant bits on the original uint.
// Udon does not support bitwise NOT, so a clumsy mix of OR, XOR to zero it out...
modifiedNumber = (modifiedNumber << SELECTED_PLAYER_OFFSET);
const int mask = (NIBBLE_MASK << SELECTED_PLAYER_OFFSET);
localSync |= mask;
localSync ^= mask;
localSync |= modifiedNumber;
_syncData1 = localSync;
InternalRequestSerialization();
}
/// <summary>
/// Decodes and returns the currently selected player from the int
/// </summary>
private int GetSelectedPlayer()
{
//Shift data
int shiftedData = (_syncData1LocalCopy >> SELECTED_PLAYER_OFFSET);
//Mask away the higher bits
shiftedData &= NIBBLE_MASK;
return (int)(shiftedData & NIBBLE_MASK);
}
#endregion SYNCBOOL_FUNCTIONS
#region EditorTest
#if EDITOR_BUTTON_TEST
[SerializeField]
private Transform _fakeHand;
#endif
#endregion EditorTest
#region PlayerCalibrationAPI
/// <summary>
/// Avatar height in meter (of localPlayer), can be set by an external script for better usability of the keyboard.
/// Afterwards, OnAvatarChanged() must be called on this script.
/// One such script that does all of that is my Player Calibration Script which I sell on my booth page (https://booth.pm/en/items/2753511)
/// </summary>
//[HideInInspector] <-- This can be uncommented when the player calibration script is in the scene
[Tooltip("Avatar height in meter (of localPlayer), can be set by an external script for better usability of the piano. Afterwards, OnAvatarChanged() must be called on this script. One such script that does all of that is my AvatarCalibrationScript which I sell on my booth page.")]
public float _avatarHeight = 1.3f;
/// <summary>
/// Relevant bone from the left hand (of localPlayer), can be set by an external script for better usability of the keyboard accross all players.
/// One such script is my Player Calibration Script which I sell on my booth page (https://booth.pm/en/items/2753511)
/// </summary>
//[HideInInspector] <-- This can be uncommented when the player calibration script is in the scene
[Tooltip("Relevant bone from the left hand (of localPlayer), can be set by an external script for better usability of the piano accross all players. One such script is my AvatarCalibrationScript which I sell on my booth page.")]
public HumanBodyBones _leftIndexBone = HumanBodyBones.LeftIndexDistal;
/// <summary>
/// Relevant bone from the right hand (of localPlayer), can be set by an external script for better usability of the keyboard accross all players.
/// One such script is my Player Calibration Script which I sell on my booth page (https://booth.pm/en/items/2753511)
/// </summary>
//[HideInInspector] <-- This can be uncommented when the player calibration script is in the scene
[Tooltip("Relevant bone from the right hand (of localPlayer), can be set by an external script for better usability of the piano accross all players. One such script is my AvatarCalibrationScript which I sell on my booth page.")]
public HumanBodyBones _rightIndexBone = HumanBodyBones.RightIndexDistal;
#endregion PlayerCalibrationAPI
#region SetupVRButtons
private void SetupVRButtons()
{
ReadButtonBounds();
SetupButtonDistances();
_fingerThickness = FINGER_THICKNESS_DEFAULT;
}
/// <summary>
/// Determines all button distances at start
/// </summary>
private void SetupButtonDistances()
{
for (int i = 0; i < BUTTON_COUNT; i++)
{
_buttonTriggerDistance[i] = BUTTON_PUSH_DISTANCE[i] * BUTTON_TRIGGER_PERCENTAGE;
_buttonUntriggerDistance[i] = BUTTON_PUSH_DISTANCE[i] * BUTTON_UNTRIGGER_PERCENTAGE;
}
}
/// <summary>
/// Reads button bounds at start. If the button won't move or rotate at runtime, those are worldspace bounds,
/// else localspace bounds are determined.
/// </summary>
private void ReadButtonBounds()
{
//read the bounds from the push area in local space
_pushAreaBoundsBanButton = new Bounds(_pushAreaBigButton.center, _pushAreaBigButton.size);
_pushAreaBoundsNextButton = new Bounds(_pushAreaRightButton.center, _pushAreaRightButton.size);
_pushAreaBoundsPreviousButton = new Bounds(_pushAreaLeftButton.center, _pushAreaLeftButton.size);
}
#endregion SetupVRButtons
#region PlayerCalibration
/// <summary>
/// This is externally called after setting _avatarHeight (happening after localPlayer changed their avatar)
/// </summary>
public void _OnAvatarChanged()
{
if (!_isSpecialSnowflake)
return;
if (_runVRmode)
{
if (_leftIndexBone == HumanBodyBones.LeftLowerArm || _rightIndexBone == HumanBodyBones.RightLowerArm)
{
//enable the "desktop" mode because the player is missing all finger bones
_runVRmode = false;
//the following only works if _runVRmode == false
SetButtonActive(ENUM_BUTTONID_NEXT, true);
SetButtonActive(ENUM_BUTTONID_PREVIOUS, true);
if (_banButtonArmed)
SetButtonActive(ENUM_BUTTONID_BAN, true);
_isInDesktopFallbackMode = true;
}
else
{
UpdateFingerThickness();
}
}
else if (_isInDesktopFallbackMode)
{
if (_leftIndexBone != HumanBodyBones.LastBone && _rightIndexBone != HumanBodyBones.LastBone)
{
//enable the regular VR mode again. Only works if _runVRmode == false
SetButtonActive(ENUM_BUTTONID_NEXT, false);
SetButtonActive(ENUM_BUTTONID_PREVIOUS, false);
SetButtonActive(ENUM_BUTTONID_BAN, false);
_runVRmode = true;
_isInDesktopFallbackMode = false;
UpdateFingerThickness();
}
}
}
private void UpdateFingerThickness()
{
//distances are made for a 1.3m avatar as a reference
_fingerThickness = FINGER_THICKNESS_DEFAULT * _avatarHeight / 1.3f;
}
#endregion PlayerCalibration
#region RunButton
private void RunAllVRButtons()
{
#if !EDITOR_BUTTON_TEST
if (Vector3.Distance(_pickupTransform.position, _localPlayer.GetPosition()) > MAX_PANEL_INTERACTION_DISTANCE)
return;
#endif
if (_banButtonArmed || _wasInBound[ENUM_BUTTONID_BAN]) //needs to move back after being disarmed
RunButtonForVR(ENUM_BUTTONID_BAN, _boneBigButton, _pushAreaBoundsBanButton, _buttonPushDirectionBigButton);
RunButtonForVR(ENUM_BUTTONID_NEXT, _boneRightArrowButton, _pushAreaBoundsNextButton, _buttonPushDirectionRightButton);
RunButtonForVR(ENUM_BUTTONID_PREVIOUS, _boneLeftArrowButton, _pushAreaBoundsPreviousButton, _buttonPushDirectionLeftButton);
}
/// <summary>
/// After determining where the hand bones are, the button runs in 3 steps:
/// 1. Move the button down if the hand is in bounds & more pushed then it currently was,
/// fire ButtonDown() when the button passes the trigger distance.
/// 2. Move the button back if the hand is no longer in bounds or less pushed then it currently was
/// 3. Fire the ButtonUp() event when the button moves back and passes the unTrigger-distance after
/// it was triggered
/// </summary>
private void RunButtonForVR(int buttonID, Transform buttonBone, Bounds pushAreaBounds, Transform pushDirection)
{
//check all bones if one is in bounds
bool isInBoundRightNow = false;
bool leftHandIsClosest = false;
//reset to 0, will be set by the following methods to the highest value
float handPushDistance = 0;
#if !EDITOR_BUTTON_TEST
Vector3 position = _localPlayer.GetBonePosition(_leftIndexBone);
#else
Vector3 position = _fakeHand.position;
#endif
if (pushAreaBounds.Contains(pushDirection.transform.InverseTransformPoint(position)))
{
//measure distances to hand bone
float distanceToHandNew = SignedDistancePlanePoint(pushDirection.transform.forward, pushDirection.position, position) + _fingerThickness;
//only the lowest distance is important for us. Must be lower than 0 to be behind the _buttonPushDirection plane.
if (distanceToHandNew > 0)
{
handPushDistance = distanceToHandNew;
isInBoundRightNow = true;
leftHandIsClosest = true;
}
}
//------ check the other hand bone ----------
#if !EDITOR_BUTTON_TEST
position = _localPlayer.GetBonePosition(_rightIndexBone);
if (pushAreaBounds.Contains(pushDirection.transform.InverseTransformPoint(position)))
{
//measure distances to hand bone
float distanceToHandNew = SignedDistancePlanePoint(pushDirection.transform.forward, pushDirection.position, position) + _fingerThickness;
//only the lowest distance is important for us. Must be lower than 0 to be behind the _buttonPushDirection plane.
if (distanceToHandNew > 0 && handPushDistance < distanceToHandNew)
{
handPushDistance = distanceToHandNew;
isInBoundRightNow = true;
leftHandIsClosest = false;
}
}
#endif
bool wasInBound = _wasInBound[buttonID];
//check if at least one of the bones was in bounds
if (isInBoundRightNow && _currentPushedDistance[buttonID] <= handPushDistance)
{
if (!wasInBound)
{
_wasInBound[buttonID] = true;
}
//pushing the button down
float currentPushedDistance = Mathf.Min(handPushDistance, BUTTON_PUSH_DISTANCE[buttonID]);
_currentPushedDistance[buttonID] = currentPushedDistance;
buttonBone.transform.position = pushDirection.position + (pushDirection.forward * currentPushedDistance);
//trigger action when limit reached
if (!_wasTriggered[buttonID] && currentPushedDistance >= _buttonTriggerDistance[buttonID] && (Time.time - _lastTriggerTime[buttonID]) > MIN_TRIGGER_TIME_OFFSET)
{
_lastTriggerTime[buttonID] = Time.time;
ButtonDownEvent(buttonID);
_wasTriggered[buttonID] = true;
AudioSource.PlayClipAtPoint(_clickDownAudioClip, pushDirection.position, SOUND_VOLUME_BUTTON_PRESS);
#if !EDITOR_BUTTON_TEST
if (leftHandIsClosest)
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Left, 0.1f, 1.0f, 30f); //seconds, 0-320hz, 0-1 aplitude
else
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Right, 0.1f, 1.0f, 30f); //seconds, 0-320hz, 0-1 aplitude
#endif
}
}
else if (wasInBound)
{
//cap hand push distance
handPushDistance = Mathf.Min(handPushDistance, BUTTON_PUSH_DISTANCE[buttonID]);
//move it slowly back, but not further than it's currently being pushed
float currentPushedDistance = Mathf.Max(handPushDistance, _currentPushedDistance[buttonID] - (Time.deltaTime * MOVE_BACK_SPEED));
_currentPushedDistance[buttonID] = currentPushedDistance;
buttonBone.transform.position = pushDirection.position + (pushDirection.forward * currentPushedDistance);
//stop when it's fully moved back
if (currentPushedDistance <= 0)
{
_wasInBound[buttonID] = false;
}
}
//wait until the button moved x percent back to set _wastriggered to false
if (_wasTriggered[buttonID])
{
if (_currentPushedDistance[buttonID] <= _buttonUntriggerDistance[buttonID])
{
_wasTriggered[buttonID] = false;
ButtonUpEvent(buttonID);
AudioSource.PlayClipAtPoint(_clickUpAudioClip, pushDirection.position, SOUND_VOLUME_BUTTON_PRESS);
#if !EDITOR_BUTTON_TEST
if (isInBoundRightNow)
{
if (leftHandIsClosest)
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Left, 0.70f, 1.0f, 30f); //seconds, 0-320hz, 0-1 amplitude
else
_localPlayer.PlayHapticEventInHand(VRC_Pickup.PickupHand.Right, 0.30f, 1.0f, 30f); //seconds, 0-320hz, 0-1 amplitude
}
#endif
}
}
}
/// <summary>
/// Is called when the button was pushed down in VR behind the button trigger distance for the first time
/// </summary>
private void ButtonDownEvent(int buttonID)
{
if (!ButtonSafetyCheck())
return;
switch (buttonID)
{
case ENUM_BUTTONID_BAN:
{
if (Time.timeSinceLevelLoad - _armBanButtonTimeStamp < BUTTON_BLOCKED_TIME)
return;
_pressedBanButton = true;
PlayBanSound();
BanButton_EndPosReached();
_pressedBanButton = false;
MASTER_SetBooleanValue(ENUM_FLIPSTATE_BAN, true);
_syncData1LocalCopy = _syncData1;
}
break;
case ENUM_BUTTONID_NEXT:
NextButton_ReachedEndPosition();
break;
case ENUM_BUTTONID_PREVIOUS:
PreviousButton_ReachedEndPosition();
break;
}
}
/// <summary>
/// Is called when the button was previously pushed down in VR behind the button trigger distance for the first time
/// and went back now behind the button untrigger distance
/// </summary>
private void ButtonUpEvent(int buttonID)
{
if (buttonID == ENUM_BUTTONID_BAN && Networking.IsOwner(this.gameObject))
{
MASTER_SetBooleanValue(ENUM_FLIPSTATE_BAN, false);
_syncData1LocalCopy = _syncData1;
}
}
/// <summary>
/// Is called 291 seconds after a button has been armed to disarm all buttons whose timer ran out
/// </summary>
public void _DisarmButtons()
{
//disarming an armed ban button when it stayed armed too long
if (_banButtonArmed && Time.timeSinceLevelLoad - _armBanButtonTimeStamp > 290f)
{
if (Networking.IsOwner(this.gameObject))
ChangeArmState_BanButton(setArmed: false, asOwner: true);
}
//disarming an armed unban button when it stayed armed too long
if (_unbanButtonArmed && Time.timeSinceLevelLoad - _armUnbanButtonTimeStamp > 290f)
{
if (Networking.IsOwner(this.gameObject))
ChangeArmState_UnbanButton(setArmed: false, asOwner: true);
}
//disarming an armed unban-all button when it stayed armed too long
if (_unbanAllButtonArmed && Time.timeSinceLevelLoad - _armUnbanAllButtonTimeStamp > 290f)
{
if (Networking.IsOwner(this.gameObject))
ChangeArmState_UnbanAllButton(setArmed: false, asOwner: true);
}
}
#endregion RunButton
#region GeneralFunctions
/// <summary>
/// Get the shortest distance between a point and a plane (signed to include the direction as well)
/// </summary>
private float SignedDistancePlanePoint(Vector3 planeNormal, Vector3 planePoint, Vector3 point)
{
return Vector3.Dot(planeNormal, (point - planePoint));
}
/// <summary>
/// Estimates the player height according to certain bone measurements (foot-to-head bone chain)
/// </summary>
private float MeasurePlayerHeight(VRCPlayerApi player)
{
//reading player bone positions
Vector3 head = player.GetBonePosition(HumanBodyBones.Head);
Vector3 spine = player.GetBonePosition(HumanBodyBones.Spine);
Vector3 rightUpperLeg = player.GetBonePosition(HumanBodyBones.RightUpperLeg);
Vector3 rightLowerLeg = player.GetBonePosition(HumanBodyBones.RightLowerLeg);
Vector3 rightFoot = player.GetBonePosition(HumanBodyBones.RightFoot);
//making sure all bones are valid
if (head == Vector3.zero || spine == Vector3.zero || rightUpperLeg == Vector3.zero || rightLowerLeg == Vector3.zero || rightFoot == Vector3.zero)
{
return 1.5f; //assuming the player has standard height is all we can do here
}
else
{
float playerHeight = Vector3.Distance(rightFoot, rightLowerLeg) + Vector3.Distance(rightLowerLeg, rightUpperLeg) +
Vector3.Distance(rightUpperLeg, spine) + Vector3.Distance(spine, head);
return playerHeight;
}
}
#endregion GeneralFunctions
#region SummonPanel
#if SUMMON_PANEL_FUNCTION
/// <summary>
/// Allows an admin/moderator to summon the panel by pressing "B" (for "Ban").
/// </summary>
private void RunSummonPanel()
{
if (_isVR)
{
if (Input.GetButtonDown(_summonVrButtonName)) //added a button for VR to show you how you could add VR controller support
{
if (_keyInputBlocked)
return;
if (_pressCounter == 0)
{
if (_summonVrPressCount == 1)
{
_pressCounter = 0;
SummonPanel();
}
else
{
_lastPressTime = Time.timeSinceLevelLoad;
_pressCounter = 1;
}
}
else
{
if (Time.timeSinceLevelLoad - _lastPressTime > _summonVrButtonMaxPressDelay)
{
_pressCounter = 1;
}
else if (_pressCounter < _summonVrPressCount)
{
_lastPressTime = Time.timeSinceLevelLoad;
_pressCounter++;
}
else
{
_pressCounter = 0;
SummonPanel();
}
}
}
}
else
{
if (Input.GetKeyDown(_summonDesktopPanelButton))
{
if (_keyInputBlocked)
return;
SummonPanel();
}
}
}
/// <summary>
/// Summons the panel if the user is authenticated, else summons the input field instead
/// </summary>
private void SummonPanel()
{
#if PASSWORD_AUTHENTICATION
if (_optionalPasswordInputField == null)
{
Debug.LogError("[Authentication] _optionalPasswordInputField is not assigned to the script.");
return;
}
else if (GetPasswordInputParent().gameObject.activeInHierarchy)
{
Transform inputTransform = GetPasswordInputParent();
inputTransform.rotation = _localPlayer.GetRotation();
inputTransform.position = _localPlayer.GetPosition() + (inputTransform.forward.normalized * (_avatarHeight / 1.3f * 0.6f)) + new Vector3(0, _avatarHeight * 0.6f, 0);
inputTransform.rotation *= Quaternion.Euler(90, 180, 0);
return;
}
#endif
Networking.SetOwner(_localPlayer, _pickupTransform.gameObject);
Networking.SetOwner(_localPlayer, this.gameObject);
_pickupTransform.rotation = _localPlayer.GetRotation();
_pickupTransform.position = _localPlayer.GetPosition() + (_pickupTransform.forward.normalized * (_avatarHeight / 1.3f * 0.6f)) + new Vector3(0, _avatarHeight * 0.6f, 0);
_pickupTransform.rotation *= Quaternion.Euler(25, 180, 0);
}
#endif
#endregion SummonPanel
#region KeyInputBlockAPI
#if SUMMON_PANEL_FUNCTION
/// <summary>
/// Can be called by external scripts to block key input on this script. One such script using this API is my Keyboard asset.
/// Avoids that pressing "B" while typing in the Chat System summons the panel by accident.
/// </summary>
public void _BlockKeyInput()
{
_keyInputBlocked = true;
}
/// <summary>
/// Can be called by external scripts to unblock key input on this script. One such script using this API is my Keyboard asset.
/// Avoids that pressing "B" while typing in the Chat System summons the panel by accident.
/// </summary>
public void _UnblockKeyInput()
{
_keyInputBlocked = false;
}
#endif
#endregion KeyInputBlockAPI
#region Audio
/// <summary>
/// Plays the banning sound <see cref="_soundFile"/> for all players (called on network)
/// </summary>
private void PlayBanSound()
{
AudioSource.PlayClipAtPoint(_soundFile1, _pickupTransform.position, SOUND_VOLUME_BAN);
}
/// <summary>
/// Plays a sound when a banned player joined the instance (for admins only)
/// </summary>
private void PlayBannedJoinedSound()
{
AudioSource.PlayClipAtPoint(_soundFile2, _localPlayer.GetPosition(), SOUND_VOLUME_BANNED_PLAYER_JOINED);
}
/// <summary>
/// Mutes all banned players, unmutes everyone else
/// </summary>
private void UpdateMutePlayerStatus()
{
for (int i = 0; i < _playerAmount; i++)
{
VRCPlayerApi player = _allPlayers[i];
if (!Utilities.IsValid(player))
continue;
if (player == _localPlayer)
continue;
string rawName = player.displayName;
#if PERMA_BAN_LIST
if (IsPermaBanned(rawName, localCheck: false))
{
#if DESYNC_BANNED_PLAYERS
if (!_localIsBadBoi)
DesyncPlayer(player);
#endif
#if MUTE_BANNED_PLAYERS
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
if (_isSpecialSnowflake)
continue;
#endif
MutePlayer(player);
MutePlayerAvatar(player);
#endif
}
else
{
#endif
string cleanedName = CleanUserName(rawName);
if (System.Array.IndexOf(_bannedPlayerListArray, cleanedName) != -1)
{
#if MARK_BANNED_PLAYERS
if (!_localIsBadBoi)
AddMarkedPlayer(player);
#endif
#if DESYNC_BANNED_PLAYERS
if (!_localIsBadBoi)
DesyncPlayer(player);
#endif
#if MUTE_BANNED_PLAYERS
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
if (_isSpecialSnowflake)
continue;
#endif
MutePlayer(player);
MutePlayerAvatar(player);
#endif
}
else
{
#if DESYNC_BANNED_PLAYERS
RemoveDesyncedPlayer(player);
#endif
#if MARK_BANNED_PLAYERS
RemoveMarkedPlayer(player);
#endif
#if MUTE_BANNED_PLAYERS
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
if (_isSpecialSnowflake)
continue;
#endif
if (_localIsBadBoi)
{
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
//a banned player should still hear admins
if (IsSnowflake(rawName))
{
UnMutePlayer(player);
UnMutePlayerAvatar(player);
}
else
{
#endif
MutePlayer(player);
MutePlayerAvatar(player);
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
}
#endif
}
else
{
UnMutePlayer(player);
UnMutePlayerAvatar(player);
}
#endif
}
#if PERMA_BAN_LIST
}
#endif
}
}
#if MUTE_BANNED_PLAYERS
/// <summary>
/// Mutes all players in the instance unless they are Admins/Moderators
/// </summary>
private void MuteAllPlayers()
{
int playerCount = VRCPlayerApi.GetPlayerCount();
//using VRChats array because this is already filled in Start() where this method can be called as well
VRCPlayerApi[] players = new VRCPlayerApi[82];
VRCPlayerApi.GetPlayers(players);
for (int i = 0; i < playerCount; i++)
{
VRCPlayerApi player = players[i];
if (player == _localPlayer)
continue;
MutePlayerAvatar(player);
#if BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS
if (IsSnowflake(player.displayName))
continue;
#endif
MutePlayer(player);
}
}
/// <summary>
/// Makes a player inaudible
/// </summary>
private void MutePlayer(VRCPlayerApi player)
{
player.SetVoiceGain(0);
player.SetVoiceDistanceFar(0);
player.SetVoiceDistanceNear(0);
}
/// <summary>
/// Makes a player audible
/// </summary>
private void UnMutePlayer(VRCPlayerApi player)
{
player.SetVoiceGain(PLAYER_VOICE_GAIN);
player.SetVoiceDistanceFar(PLAYER_VOICE_DISTANCE_FAR);
}
/// <summary>
/// Makes an avatar inaudible
/// </summary>
private void MutePlayerAvatar(VRCPlayerApi player)
{
player.SetAvatarAudioGain(0);
player.SetAvatarAudioFarRadius(0);
}
/// <summary>
/// Makes an avatar audible
/// </summary>
private void UnMutePlayerAvatar(VRCPlayerApi player)
{
player.SetAvatarAudioGain(AVATAR_AUDIO_GAIN);
player.SetAvatarAudioFarRadius(AVATAR_AUDIO_DISTANCE_FAR);
}
#endif
#endregion Audio
#region Desync
#if DESYNC_BANNED_PLAYERS
/// <summary>
/// Locally teleports a player away into the ban area
/// </summary>
private void DesyncPlayer(VRCPlayerApi player)
{
if (Array.IndexOf(_usernameToStation, player.displayName) != -1)
return;
#if DEBUG_TEST
Debug.Log($"Start desyncing '{player.displayName}'");
#endif
GameObject newStation = Instantiate(_station.gameObject);
newStation.SetActive(true);
newStation.transform.parent = _station.transform.parent;
newStation.transform.position = _teleportTarget.transform.position;
VRCStation newVRCStation = newStation.GetComponent<VRCStation>();
newVRCStation.UseStation(player);
//initialize an array that is +1 longer
VRCStation[] newStationArray = new VRCStation[_amountOfDesyncedPlayers + 1];
string[] newStringArray = new string[_amountOfDesyncedPlayers + 1];
//copy all values from the old to the new array
System.Array.Copy(_activeStations, newStationArray, _amountOfDesyncedPlayers);
System.Array.Copy(_usernameToStation, newStringArray, _amountOfDesyncedPlayers);
//set new values at last array position
newStationArray.SetValue(newVRCStation, _amountOfDesyncedPlayers);
newStringArray.SetValue(player.displayName, _amountOfDesyncedPlayers);
//overwrite array ref with new array
_activeStations = newStationArray;
_usernameToStation = newStringArray;
//increase counter
_amountOfDesyncedPlayers++;
}
/// <summary>
/// Removes a player from the desynced state
/// </summary>
private void RemoveDesyncedPlayer(VRCPlayerApi player)
{
int arrayIndex = System.Array.IndexOf(_usernameToStation, player.displayName);
if (arrayIndex == -1)
return; //player isn't desynced
#if DEBUG_TEST
Debug.Log($"Stopped desyncing '{player.displayName}'");
#endif
Destroy(_activeStations[arrayIndex].gameObject);
int newLength = _amountOfDesyncedPlayers - 1;
if (newLength <= 0)
{
RemoveAllDesyncedPlayers();
}
else
{
//initialize an array that is -1 shorter
VRCStation[] newStationArray = new VRCStation[newLength];
string[] newStringArray = new string[newLength];
//copy all values from the old to the new array (lower part)
System.Array.Copy(_activeStations, 0, newStationArray, 0, arrayIndex);
System.Array.Copy(_usernameToStation, 0, newStringArray, 0, arrayIndex);
//copy all values from the old to the new array (higher part)
int lowBound = arrayIndex + 1;
int length = _amountOfDesyncedPlayers - arrayIndex - 1;
System.Array.Copy(_activeStations, lowBound, newStationArray, 0, length);
System.Array.Copy(_usernameToStation, lowBound, newStringArray, 0, length);
//decrease counter
_amountOfDesyncedPlayers--;
//overwrite array ref with new array
_activeStations = newStationArray;
_usernameToStation = newStringArray;
}
}
/// <summary>
/// Sets all players as not desynced
/// </summary>
private void RemoveAllDesyncedPlayers()
{
for (int i = 0; i < _amountOfDesyncedPlayers; i++)
{
//destroy all stations that are still there
Destroy(_activeStations[i].gameObject);
}
_activeStations = new VRCStation[0];
_usernameToStation = new string[0];
_amountOfDesyncedPlayers = 0;
}
#endif
#endregion Desync
#region CrasherWarning
#if MARK_BANNED_PLAYERS
/// <summary>
/// Sets all signs to the positions of the watched players and roatates them to look towards localplayer
/// </summary>
private void UpdateSignPositions()
{
for (int i = 0; i < _amountOfMarkedPlayers; i++)
{
Vector3 playerPosition = _markedPlayers[i].GetPosition();
Transform sign = _warningSignsAbovePlayers[i];
Transform capsule = _warningCapsules[i];
capsule.position = playerPosition;
playerPosition.y += _markedPlayerHeight[i];
sign.position = playerPosition;
Vector3 lookAtPos = _localPlayer.GetPosition();
sign.LookAt(lookAtPos, Vector3.up);
sign.eulerAngles = new Vector3(0, sign.eulerAngles.y, 0);
}
if (Time.time - _lastPlayerHeightUpdateTime > PLAYERHEIGHT_UPDATE_INTERVALL)
{
if (_currentPlayerForHeightUpdate >= _amountOfMarkedPlayers)
{
//we've updated all players
_lastPlayerHeightUpdateTime = Time.time;
_currentPlayerForHeightUpdate = 0;
}
else
{
//keep updating the next player
float playerHeight = MeasurePlayerHeight(_markedPlayers[_currentPlayerForHeightUpdate]) * PLAYER_HEIGHT_MULTIPLICATOR;
_markedPlayerHeight[_currentPlayerForHeightUpdate] = playerHeight + WARNING_SIGN_OFFSET;
_warningCapsules[_currentPlayerForHeightUpdate].localScale = new Vector3(1, 0.5f * playerHeight * CAPSULE_HEIGHT_MULTIPLICATOR, 1);
_currentPlayerForHeightUpdate++;
}
}
}
/// <summary>
/// Returns true if a player is currently on the watchlist
/// </summary>
private bool IsPlayerMarked(VRCPlayerApi player)
{
return System.Array.IndexOf(_markedPlayers, player) != -1;
}
/// <summary>
/// Adds a new parent to the _trackParents array and increases the size of all other related arrays.
/// Returns false if no new parent was added because the track ID is wrong.
/// </summary>
private void AddMarkedPlayer(VRCPlayerApi newPlayer)
{
if (IsPlayerMarked(newPlayer))
return; //player is already watched
#if DEBUG_TEST
Debug.Log($"Started watching '{newPlayer.displayName}'");
#endif
//create a new sign from the template
GameObject newSign = Instantiate(_warningSignAbovePlayerTemplate);
newSign.SetActive(true);
GameObject newCapsule = Instantiate(_warningCapsuleTemplate);
newCapsule.SetActive(true);
//initialize an array that is +1 longer
int newLength = _amountOfMarkedPlayers + 1;
float playerHeight = MeasurePlayerHeight(newPlayer) * PLAYER_HEIGHT_MULTIPLICATOR;
float newSignHeight = playerHeight + WARNING_SIGN_OFFSET;
newCapsule.transform.localScale = new Vector3(1, 0.5f * playerHeight * CAPSULE_HEIGHT_MULTIPLICATOR, 1);
VRCPlayerApi[] newWatchedPlayersArray = new VRCPlayerApi[newLength];
Transform[] newSignAbovePlayersArray = new Transform[newLength];
Transform[] newWarningCapsulesArray = new Transform[newLength];
float[] newWatchedPlayerHeight = new float[newLength];
//copy all values from the old to the new array
System.Array.Copy(_markedPlayers, newWatchedPlayersArray, _amountOfMarkedPlayers);
System.Array.Copy(_warningSignsAbovePlayers, newSignAbovePlayersArray, _amountOfMarkedPlayers);
System.Array.Copy(_warningCapsules, newWarningCapsulesArray, _amountOfMarkedPlayers);
System.Array.Copy(_markedPlayerHeight, newWatchedPlayerHeight, _amountOfMarkedPlayers);
//set new values at last array position
newWatchedPlayersArray.SetValue(newPlayer, _amountOfMarkedPlayers);
newSignAbovePlayersArray.SetValue(newSign.transform, _amountOfMarkedPlayers);
newWarningCapsulesArray.SetValue(newCapsule.transform, _amountOfMarkedPlayers);
newWatchedPlayerHeight.SetValue(newSignHeight, _amountOfMarkedPlayers);
//overwrite array ref with new array
_markedPlayers = newWatchedPlayersArray;
_warningSignsAbovePlayers = newSignAbovePlayersArray;
_warningCapsules = newWarningCapsulesArray;
_markedPlayerHeight = newWatchedPlayerHeight;
//increase counter
_amountOfMarkedPlayers++;
#if DEBUG_TEST
Debug.Log($"Now watching {_amountOfMarkedPlayers} players.");
#endif
if (!_isMarkingPlayers)
{
_isMarkingPlayers = true;
}
}
/// <summary>
/// Removes a watched player from the array if the player is in that array, else does nothing
/// </summary>
private void RemoveMarkedPlayer(VRCPlayerApi playerToRemove)
{
//we need the index below, so we are not calling IsPlayerWatched() instead
int arrayIndex = System.Array.IndexOf(_markedPlayers, playerToRemove);
if (arrayIndex == -1)
return; //player isn't watched
#if DEBUG_TEST
Debug.Log($"Stopped watching '{playerToRemove.displayName}'");
#endif
Destroy(_warningSignsAbovePlayers[arrayIndex].gameObject);
Destroy(_warningCapsules[arrayIndex].gameObject);
int newLength = _amountOfMarkedPlayers - 1;
if (newLength <= 0)
{
RemoveAllMarkedPlayers();
}
else
{
//initialize an array that is -1 shorter
VRCPlayerApi[] newWatchedPlayersArray = new VRCPlayerApi[newLength];
Transform[] newSignAbovePlayersArray = new Transform[newLength];
Transform[] newWarningCapsulesArray = new Transform[newLength];
float[] newWatchedPlayerHeight = new float[newLength];
//copy all values from the old to the new array (lower part)
System.Array.Copy(_markedPlayers, 0, newWatchedPlayersArray, 0, arrayIndex);
System.Array.Copy(_warningSignsAbovePlayers, 0, newSignAbovePlayersArray, 0, arrayIndex);
System.Array.Copy(_warningCapsules, 0, newWarningCapsulesArray, 0, arrayIndex);
System.Array.Copy(_markedPlayerHeight, 0, newWatchedPlayerHeight, 0, arrayIndex);
//copy all values from the old to the new array (higher part)
int lowBound = arrayIndex + 1;
int length = _amountOfMarkedPlayers - arrayIndex - 1;
System.Array.Copy(_markedPlayers, lowBound, newWatchedPlayersArray, 0, length);
System.Array.Copy(_warningSignsAbovePlayers, lowBound, newSignAbovePlayersArray, 0, length);
System.Array.Copy(_warningCapsules, lowBound, newWarningCapsulesArray, 0, length);
System.Array.Copy(_markedPlayerHeight, lowBound, newWatchedPlayerHeight, 0, length);
//decrease counter
_amountOfMarkedPlayers--;
//overwrite array ref with new array
_markedPlayers = newWatchedPlayersArray;
_warningSignsAbovePlayers = newSignAbovePlayersArray;
_warningCapsules = newWarningCapsulesArray;
_markedPlayerHeight = newWatchedPlayerHeight;
}
#if DEBUG_TEST
Debug.Log($"Now watching {_amountOfMarkedPlayers} players.");
#endif
}
/// <summary>
/// Sets all players as unmarked & exits marking mode
/// </summary>
private void RemoveAllMarkedPlayers()
{
for (int i = 0; i < _amountOfMarkedPlayers; i++)
{
//destroy all signs that are still there
Destroy(_warningSignsAbovePlayers[i].gameObject);
Destroy(_warningCapsules[i].gameObject);
}
_markedPlayers = new VRCPlayerApi[0];
_warningSignsAbovePlayers = new Transform[0];
_warningCapsules = new Transform[0];
_markedPlayerHeight = new float[0];
_amountOfMarkedPlayers = 0;
_isMarkingPlayers = false;
}
#endif
#endregion CrasherWarning
#region OwnershipControl
/// <summary>
/// Is called for everyone on the network when someone requests ownership. Both the requesting player
/// and the current owner must approve the request, else it is reverted again.
/// Only allow whitelisted players to request ownership for this panel for themselves.
/// </summary>
/// <param name="requestingPlayer">Player who requested ownership</param>
/// <param name="requestedOwner">Player for which ownership is requested</param>
/// <returns>True if ownership should be granted</returns>
public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
{
return _OnRelayedOwnershipRequest(requestingPlayer, requestedOwner);
}
/// <summary>
/// Only allow whitelisted players to request ownership for this panel for themselves.
/// </summary>
/// <param name="requestingPlayer">Player who requested ownership</param>
/// <param name="requestedOwner">Player for which ownership is requested</param>
/// <returns>True if ownership should be granted</returns>
public bool _OnRelayedOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
{
//ensure that this script never crashes, even not when mods mess with it
if (Utilities.IsValid(requestingPlayer) && Utilities.IsValid(requestedOwner))
{
if (requestingPlayer != requestedOwner)
{
#if DEBUG_TEST
Debug.Log($"[AP] Denied ownership request because a player ({requestingPlayer.displayName}) can only request ownership for themselves and not for ({requestedOwner.displayName}).");
#endif
//a player can only request ownership for themselves
return false;
}
if (IsSnowflake(requestedOwner.displayName))
{
#if DEBUG_TEST
Debug.Log($"[AP] Accepted ownership request from {requestingPlayer.displayName} to themselves.");
#endif
//special snowflakes are allowed to request ownership
return true;
}
else
{
#if DEBUG_TEST
Debug.Log($"[AP] Denied ownership request from {requestingPlayer.displayName} because they are not special.");
#endif
//regular players are not allowed to request ownership
return false;
}
}
else
{
#if DEBUG_TEST
Debug.Log("[AP] Denied ownership request because requestingPlayer and requestedOwner are not both valid.");
#endif
return false;
}
}
#endregion OwnershipControl
#region Authentication
#if PASSWORD_AUTHENTICATION
/// <summary>
/// Must be called OnEndEdit() on the password input field.
/// Will disable the input field if the input is correct.
/// </summary>
public void _OnEndEditPasswordInput()
{
#if DEBUG_AUTHENTICATION_TEST
Debug.Log("[Authentication] Called _OnEndEditPasswordInput()");
#endif
if (_optionalPasswordInputField.text == String.Empty)
{
#if DEBUG_AUTHENTICATION_TEST
Debug.Log("[Authentication] Password input field is empty");
#endif
return;
}
if (AuthenticateUser())
{
Debug.Log("[Authentication] Success.");
SetPasswordInputActiveState(false);
FinishSetupForSnowflake();
}
else
{
_optionalPasswordInputField.text = String.Empty;
Debug.LogError("[Authentication] Failed to authenticate (wrong password).");
}
}
/// <summary>
/// Shows/Hides the password input field from the user, depending on <paramref name="setActive"/>
/// </summary>
private void SetPasswordInputActiveState(bool setActive)
{
if (_optionalPasswordInputField == null)
{
Debug.LogError("[Authentication] _optionalPasswordInputField is not assigned to the script.");
return;
}
GetPasswordInputParent().gameObject.SetActive(setActive);
}
/// <summary>
/// Returns the password input field parent (or the object itself if it has no parent)
/// </summary>
private Transform GetPasswordInputParent()
{
//Because of a VRChat bug we can no longer parent the UI button to the input field,
//so we need to make it a child next to it and assume the user has a similar setup.
//https://feedback.vrchat.com/bug-reports/p/parenting-ui-button-to-inputfield-breaks-vrchat-keyboard
//But we still fall back to the original method in case there is no parent.
//Note: storing the reference for a single 2x call isn't good in this case so we rather fetch it two times.
if (_optionalPasswordInputField.transform.parent != null)
{
if (_optionalPasswordInputField.transform.parent.parent != null)
{
return _optionalPasswordInputField.transform.parent.parent;
}
else
{
return _optionalPasswordInputField.transform.parent;
}
}
else
{
return _optionalPasswordInputField.transform;
}
}
/// <summary>
/// Takes a username and password, finds the corresponding user account.
/// Hashes the input password with the salt, and compares to the stored salted hash.
/// <returns>Boolean value of result of password comparison. True == Success, False == Failure</returns>
private bool AuthenticateUser()
{
if (_isSpecialSnowflake)
{
for (int i = 0; i < _credentials.Length; i++)
{
// Split the credential line, will provide the username, salt, and hashed password
string[] credential = (string[])_credentials[i];
// Compare the username stored in shadow and the username provided by user
if (credential[0] == _localPlayerCleanedDisplayName)
{
if (_optionalPasswordInputField == null)
{
Debug.LogError("[Authentication] _optionalPasswordInputField is not assigned to the script.");
return false;
}
#if DEBUG_AUTHENTICATION_TEST
Debug.Log($"[Authentication] User '{_localPlayerCleanedDisplayName}' entered password '{_optionalPasswordInputField.text.Trim()}' which results to '{SHA512_UTF8(_optionalPasswordInputField.text.Trim() + credential[1])}', now authenticating.");
#endif
// add the stored salt to the input and return if the resulting SHA512 matches
return credential[2] == SHA512_UTF8(_optionalPasswordInputField.text.Trim() + credential[1]);
}
}
Debug.LogError($"[Authentication] User '{_localPlayerCleanedDisplayName}' has no authentication keypair (no password set).");
}
#if DEBUG_AUTHENTICATION_TEST
else
{
Debug.Log("[Authentication] User is not allowed to authenticate.");
}
#endif
return false;
}
#endif
#endregion Authentication
#region SHA512
#if PASSWORD_AUTHENTICATION
/*
* The following licence is only valid for the #region SHA256 and does not affect other code regions.
*
*
MIT License
Copyright (c) 2021 Devon (Gorialis) R
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// Udon does not support UTF-8 or expose System.Text.Encoding so we must implement this ourselves
private byte[] ToUTF8(char[] characters)
{
byte[] buffer = new byte[characters.Length * 4];
int writeIndex = 0;
for (int i = 0; i < characters.Length; i++)
{
uint character = characters[i];
if (character < 0x80)
{
buffer[writeIndex++] = (byte)character;
}
else if (character < 0x800)
{
buffer[writeIndex++] = (byte)(0b11000000 | ((character >> 6) & 0b11111));
buffer[writeIndex++] = (byte)(0b10000000 | (character & 0b111111));
}
else if (character < 0x10000)
{
buffer[writeIndex++] = (byte)(0b11100000 | ((character >> 12) & 0b1111));
buffer[writeIndex++] = (byte)(0b10000000 | ((character >> 6) & 0b111111));
buffer[writeIndex++] = (byte)(0b10000000 | (character & 0b111111));
}
else
{
buffer[writeIndex++] = (byte)(0b11110000 | ((character >> 18) & 0b111));
buffer[writeIndex++] = (byte)(0b10000000 | ((character >> 12) & 0b111111));
buffer[writeIndex++] = (byte)(0b10000000 | ((character >> 6) & 0b111111));
buffer[writeIndex++] = (byte)(0b10000000 | (character & 0b111111));
}
}
// We do this to truncate off the end of the array
// This would be a lot easier with Array.Resize, but Udon once again does not allow access to it.
byte[] output = new byte[writeIndex];
for (int i = 0; i < writeIndex; i++)
output[i] = buffer[i];
return output;
}
/* SHA2 */
private readonly ulong[] sha512_init = {
0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, 0x510e527fade682d1,
0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179,
};
private readonly ulong[] sha512_constants = {
0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, 0x3956c25bf348b538,
0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, 0xd807aa98a3030242, 0x12835b0145706fbe,
0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235,
0xc19bf174cf692694, 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, 0x983e5152ee66dfab,
0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, 0xc6e00bf33da88fc2, 0xd5a79147930aa725,
0x06ca6351e003826f, 0x142929670a0e6e70, 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed,
0x53380d139d95b3df, 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, 0xd192e819d6ef5218,
0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, 0x19a4c116b8d2d0c8, 0x1e376c085141ab53,
0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373,
0x682e6ff3d6b2b8a3, 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, 0xca273eceea26619c,
0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, 0x06f067aa72176fba, 0x0a637dc5a2c898a6,
0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817,
};
private readonly int[] sha512_sums =
{
1, 8, 7, // s0
19, 61, 6, // s1
};
private readonly int[] sha512_sigmas =
{
28, 34, 39, // S0
14, 18, 41, // S1
};
private string SHA2_Core(byte[] payload_bytes, ulong[] init, ulong[] constants, int[] sums, int[] sigmas, ulong size_mask, int word_size, int chunk_modulo, int appended_length, int round_count, string output_format, int output_segments)
{
int word_bytes = word_size / 8;
// Working variables h0->h7
ulong[] working_variables = new ulong[8];
init.CopyTo(working_variables, 0);
byte[] input = new byte[chunk_modulo];
ulong[] message_schedule = new ulong[round_count];
// Each 64-byte/512-bit chunk
// 64 bits/8 bytes are required at the end for the bit size
for (int chunk_index = 0; chunk_index < payload_bytes.Length + appended_length + 1; chunk_index += chunk_modulo)
{
int chunk_size = Mathf.Min(chunk_modulo, payload_bytes.Length - chunk_index);
int schedule_index = 0;
// Buffer message
for (; schedule_index < chunk_size; ++schedule_index)
input[schedule_index] = payload_bytes[chunk_index + schedule_index];
// Append a 1-bit if not an even chunk
if (schedule_index < chunk_modulo && chunk_size >= 0)
input[schedule_index++] = 0b10000000;
// Pad with zeros until the end
for (; schedule_index < chunk_modulo; ++schedule_index)
input[schedule_index] = 0x00;
// If the chunk is less than 56 bytes, this will be the final chunk containing the data size in bits
if (chunk_size < chunk_modulo - appended_length)
{
ulong bit_size = (ulong)payload_bytes.Length * 8ul;
input[chunk_modulo - 1] = Convert.ToByte((bit_size >> 0x00) & 0xFFul);
input[chunk_modulo - 2] = Convert.ToByte((bit_size >> 0x08) & 0xFFul);
input[chunk_modulo - 3] = Convert.ToByte((bit_size >> 0x10) & 0xFFul);
input[chunk_modulo - 4] = Convert.ToByte((bit_size >> 0x18) & 0xFFul);
input[chunk_modulo - 5] = Convert.ToByte((bit_size >> 0x20) & 0xFFul);
input[chunk_modulo - 6] = Convert.ToByte((bit_size >> 0x28) & 0xFFul);
input[chunk_modulo - 7] = Convert.ToByte((bit_size >> 0x30) & 0xFFul);
input[chunk_modulo - 8] = Convert.ToByte((bit_size >> 0x38) & 0xFFul);
}
// Copy into w[0..15]
int copy_index = 0;
for (; copy_index < 16; copy_index++)
{
message_schedule[copy_index] = 0ul;
for (int i = 0; i < word_bytes; i++)
{
message_schedule[copy_index] = (message_schedule[copy_index] << 8) | input[(copy_index * word_bytes) + i];
}
message_schedule[copy_index] = message_schedule[copy_index] & size_mask;
}
// Extend
for (; copy_index < round_count; copy_index++)
{
ulong s0_read = message_schedule[copy_index - 15];
ulong s1_read = message_schedule[copy_index - 2];
message_schedule[copy_index] = (
message_schedule[copy_index - 16] +
(((s0_read >> sums[0]) | (s0_read << word_size - sums[0])) ^ ((s0_read >> sums[1]) | (s0_read << word_size - sums[1])) ^ (s0_read >> sums[2])) + // s0
message_schedule[copy_index - 7] +
(((s1_read >> sums[3]) | (s1_read << word_size - sums[3])) ^ ((s1_read >> sums[4]) | (s1_read << word_size - sums[4])) ^ (s1_read >> sums[5])) // s1
) & size_mask;
}
// temp vars
ulong temp1, temp2;
// work is equivalent to a, b, c, d, e, f, g, h
// This copies work from h0, h1, h2, h3, h4, h5, h6, h7
ulong[] work = new ulong[8];
working_variables.CopyTo(work, 0);
// Compression function main loop
for (copy_index = 0; copy_index < round_count; copy_index++)
{
ulong ep1 = ((work[4] >> sigmas[3]) | (work[4] << word_size - sigmas[3])) ^ ((work[4] >> sigmas[4]) | (work[4] << word_size - sigmas[4])) ^ ((work[4] >> sigmas[5]) | (work[4] << word_size - sigmas[5]));
ulong ch = (work[4] & work[5]) ^ ((size_mask ^ work[4]) & work[6]);
ulong ep0 = ((work[0] >> sigmas[0]) | (work[0] << word_size - sigmas[0])) ^ ((work[0] >> sigmas[1]) | (work[0] << word_size - sigmas[1])) ^ ((work[0] >> sigmas[2]) | (work[0] << word_size - sigmas[2]));
ulong maj = (work[0] & work[1]) ^ (work[0] & work[2]) ^ (work[1] & work[2]);
temp1 = work[7] + ep1 + ch + constants[copy_index] + message_schedule[copy_index];
temp2 = ep0 + maj;
work[7] = work[6];
work[6] = work[5];
work[5] = work[4];
work[4] = (work[3] + temp1) & size_mask;
work[3] = work[2];
work[2] = work[1];
work[1] = work[0];
work[0] = (temp1 + temp2) & size_mask;
}
for (copy_index = 0; copy_index < 8; copy_index++)
working_variables[copy_index] = (working_variables[copy_index] + work[copy_index]) & size_mask;
}
// Finalization
string output = "";
for (int character_index = 0; character_index < output_segments; character_index++)
{
output += string.Format(output_format, working_variables[character_index]);
}
return output;
}
/* SHA512 */
public string SHA512_UTF8(string text)
{
return SHA2_Core(ToUTF8(text.ToCharArray()), sha512_init, sha512_constants, sha512_sums, sha512_sigmas, 0xFFFFFFFFFFFFFFFFul, 64, 128, 16, 80, "{0:x16}", 8);
}
#endif
#endregion SHA512
}
}