#region Usings using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using UnityEditor; using UnityEngine; #endregion Usings /// /// Script from Reimajo purchased at https://reimajo.booth.pm/ /// Make sure to join my discord to receive update notifications for this asset and support: https://discord.gg/SWkNA394Mm /// If you have any issues; please contact me on Discord (https://discord.gg/SWkNA394Mm) or Booth or Twitter https://twitter.com/ReimajoChan /// namespace ReimajoBoothAssetsEditorScripts { #region AffectedScripts [CustomEditor(typeof(ReimajoBoothAssets.AdminPanel), true, isFallback = true)] [CanEditMultipleObjects] public class AdminPanelEditor : AdminPanelEditorBase { } [CustomEditor(typeof(ReimajoBoothAssets.PickupSync), true, isFallback = true)] [CanEditMultipleObjects] public class PickupSyncEditor : AdminPanelEditorBase { } [CustomEditor(typeof(ReimajoBoothAssets.AdminPanelDesktopButton), true, isFallback = true)] [CanEditMultipleObjects] public class AdminPanelDesktopButtonEditor : AdminPanelEditorBase { } [CustomEditor(typeof(ReimajoBoothAssets.ResetObjectPositionButton), true, isFallback = true)] [CanEditMultipleObjects] public class ResetObjectPositionButtonEditor : AdminPanelEditorBase { } #endregion AffectedScripts public class AdminPanelEditorBase : Editor { //############################################ private const string VERSION = "V4.3g (UNTESTED BETA VERSION)"; private const string PRODUCT_NAME = "Admin Panel"; private const string DOCUMENTATION = @"https://docs.google.com/document/d/1gh1osC2njNwgyel48KboBF9rc8KfogPUR0vS_KUE2DI/"; //############################################ #region BaseEditor #region PrivateFields private const string UNITY_FOLDER = "Assets"; private const string RMB_FOLDER = "ReimajoBoothAssets"; private const string ASSET_FOLDER = "AdminTool"; private const string SETTINGS_FOLDER = "YOUR_SETTINGS"; private const string AUTH_FOLDER = "Auth"; private const string PASSWORD_UNKNOWN = "***********************"; private const string PASSWORD_UNKNOWN_IN_FILE = "*** password was not stored in cleartext for this user ***"; private const string AUTH_NOT_SET_IN_FILE = "*** auth info not set for this user yet ***"; private static readonly string ASSET_FOLDER_PATH = $"{UNITY_FOLDER}/{RMB_FOLDER}/{ASSET_FOLDER}/"; private static readonly string SCRIPT_FILE_PATH = $"{ASSET_FOLDER_PATH}/Scripts/PickupSync.cs"; private static readonly string SETTINGS_FOLDER_PATH = $"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}/{ASSET_FOLDER}"; private static readonly string CACHE_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/_Cache.JSON"; private static string ADMIN_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/Admins.txt"; private static string MODERATOR_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/Moderators.txt"; private static string PERMA_BAN_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/PermaBans.txt"; private static string SETTINGS_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/Settings.JSON"; private static string SALT_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/{AUTH_FOLDER}/Salt.txt"; private static string HASH_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/{AUTH_FOLDER}/Hash.txt"; private static string PASSWORD_FILE_PATH = $"{SETTINGS_FOLDER_PATH}/{AUTH_FOLDER}/Password.txt"; private static AdminPanelCache _loadedCache; private static bool _scriptIsOutOfDate; private static AdminPanelSettings _loadedSettings; private static List _loadedAdminList; private static List _loadedModeratorList; private static List _loadedPermaBanList; private static List _loadedPasswordList; private static List _loadedHashList; private static List _loadedSaltList; private static List _displayedAdminList; private static List _displayedModeratorList; private static List _displayedPermaBanList; private static List _displayedPasswordList; private static List _displayedHashList; private static List _displayedSaltList; private static bool _lastEditFailed = false; private static bool _currentEditFailed = false; private static string _editorState_REMOTE_LOADING_URL; private static int _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS; #region ToogleStates //New since 06.05.2023 private static bool _toggleState_DESYNC_BANNED_PLAYERS; private static bool _toggleState_STEALTH_PANEL; private static bool _toggleState_NO_BAN_EFFECTS; //------------------------- //private bool _toggleState_#1#; private static bool _toggleState_VRC_GUIDE_COMPLICANCE; private static bool _toggleState_VRC_GUIDE_DEBUG; private static bool _toggleState_PASSWORD_AUTHENTICATION; private static bool _toggleState_MINIMAL_BAN_DEBUG; private static bool _toggleState_ANTI_NAME_SPOOF; private static bool _toggleState_MARK_BANNED_PLAYERS; private static bool _toggleState_MUTE_BANNED_PLAYERS; private static bool _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS; private static bool _toggleState_ANTI_PICKUP_SUMMON_MOD; private static bool _toggleState_ANTI_NO_TELEPORT_MOD; private static bool _toggleState_USE_HONEYPOTS; private static bool _toggleState_PERMA_BAN_LIST; private static bool _toggleState_SURPRESS_WHITESPACE_CHARS; private static bool _toggleState_ADMIN_ONLY_OBJECTS; private static bool _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS; private static bool _toggleState_DESTROY_FOR_OTHER_PLAYERS; private static bool _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION; private static bool _toggleState_ADMIN_CAN_FLY; private static bool _toggleState_MODERATOR_CAN_FLY; private static bool _toggleState_EVERYONE_CAN_FLY; private static bool _toggleState_MODERATOR_CAN_BAN; private static bool _toggleState_CAN_BAN_ADMINS; private static bool _toggleState_SUMMON_PANEL_FUNCTION; private static bool _toggleState_ACCURATE_CAPSULE_POSITIONS; private static bool _toggleState_UNITY_SHOW_PASSWORD; private static bool _toggleState_UNITY_SHOW_HASH_AND_SALT; private static bool _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS; private static bool _toggleState_DEBUG_ADMIN_NAME_DETECTION; private static bool _toggleState_REMOTE_STRING_LOADING; private static bool _toggleState_REMOTE_BAN_LIST; private static bool _toggleState_REMOTE_ADMIN_LIST; private static bool _toggleState_REMOTE_MODERATOR_LIST; private static bool _toggleState_HASH_USERNAMES; //#NEW_PROPERTY# #endregion ToogleStates #endregion PrivateFields #region SetupEnvironment /// /// Ensures that the project has the needed path setup to store the settings files /// private static void SetupStoragePath() { if (!AssetDatabase.IsValidFolder($"{UNITY_FOLDER}/{RMB_FOLDER}")) { AssetDatabase.CreateFolder($"{UNITY_FOLDER}", RMB_FOLDER); } if (!AssetDatabase.IsValidFolder($"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}")) { AssetDatabase.CreateFolder($"{UNITY_FOLDER}/{RMB_FOLDER}", SETTINGS_FOLDER); } if (!AssetDatabase.IsValidFolder($"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}/{ASSET_FOLDER}")) { AssetDatabase.CreateFolder($"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}", ASSET_FOLDER); } if (!AssetDatabase.IsValidFolder($"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}/{ASSET_FOLDER}/{AUTH_FOLDER}")) { AssetDatabase.CreateFolder($"{UNITY_FOLDER}/{RMB_FOLDER}/{SETTINGS_FOLDER}/{ASSET_FOLDER}", AUTH_FOLDER); } } #endregion SetupEnvironment #region CreateDefaultObjects /// /// Returns a list of default admins (people who worked with me on this asset) /// private static List GetDefaultAdminList() { return new List { "Reimajo", "Alex Lotor", "JoRoFox" }; } /// /// Returns a list of default moderators (people who worked with me on this asset) /// private static List GetDefaultModeratorList() { return new List { "NotFish" }; } /// /// Returns a list of default perma bans (here are 3 accounts that are already banned by VRChat permanently, only as an example list) /// private static List GetDefaultPermaBanList() { return new List { "Kirai Chanǃ", "xKirai Chan", "Kirai Chanǃǃ" }; } /// /// Returns a list of default hashes /// private static List GetDefaultHashList() { List emptyList = GetDefaultEmptyAuthList(); if (emptyList.Count == 4) { emptyList[0] = @"32e7bfc9de933cb36d725ae600c9feee564208048cfc27dfdca9b16efd22ac0077051a5d1807aa1a7625a673bbaf20dc12923c2165c142686dcb986bd5904f99"; // for Reimajo emptyList[1] = @"41110cd8c17378448d1f2073c8ed7ba5fd9abb1ead3c604b848fa3fbaa51d7938c275dbbdf215976f6ec72dab6e7c42ceee5400396ba9ff248f36d808842c26d"; // for Alex Lotor emptyList[2] = @"31e35e68b52262aae08909d772c4406415745b65e0656972cc1d4c16bd99c9657f1252abdfa51ac0db16cb9f2cdad126299298cd1ef43a3b4eae0c9911e08413"; // for JoRoFox emptyList[3] = @"31db234dcb50ddab089fbf74877ede21e62e247be075545f3be132da0db62dbe7d7881fe7b5d18a80d53b2ad15268dc7e83834bc4987c2063f7c3eb97460967c"; // for NotFish } return emptyList; } /// /// Returns a list of default salts /// private static List GetDefaultSaltList() { List emptyList = GetDefaultEmptyAuthList(); if (emptyList.Count == 4) { emptyList[0] = @"JsgenVX0IjJlMfxlvH4hwUV5W6QnS89L"; // for Reimajo emptyList[1] = @"remSB7L7ThqOapsUi56MdWdHQP4GTQcx"; // for Alex Lotor emptyList[2] = @"uB0I9IEHvISgYF8rvuA531IAzeIxQwJJ"; // for JoRoFox emptyList[3] = @"AcORRd71xYid6P57fJJNvjo0zlqBRdoX"; // for NotFish } return emptyList; } /// /// Returns the defaulz list of passwords /// private static List GetDefaultPasswordList() { List emptyList = GetDefaultEmptyAuthList(); if (emptyList.Count == 4) { emptyList[0] = PASSWORD_UNKNOWN; // for Reimajo emptyList[1] = PASSWORD_UNKNOWN; // for Alex Lotor emptyList[2] = PASSWORD_UNKNOWN; // for JoRoFox emptyList[3] = PASSWORD_UNKNOWN; // for NotFish } return emptyList; } /// /// Returns a cache object with the default values after import /// private static AdminPanelCache GetDefaultCache() { return new AdminPanelCache() { SETTINGS_PATH_OVERRIDE = String.Empty, //setting them all to zero will enforce a re-write of them all if the cache is missing DateOfLastSettingsFileChange = 0, DateOfLastScriptChange = 0, DateOfLastAdminFileChange = 0, DateOfLastModeratorFileChange = 0, DateOfLastPermaBanFileChange = 0, DateOfLastPasswordFileChange = 0, DateOfLastHashFileChange = 0, DateOfLastSaltFileChange = 0 }; } /// /// Returns a settings object with the default settings after import /// private static AdminPanelSettings GetDefaultSettings() { return new AdminPanelSettings() { //basic VRC_GUIDE_COMPLICANCE = true, VRC_GUIDE_DEBUG = false, //safety PASSWORD_AUTHENTICATION = false, MINIMAL_BAN_DEBUG = false, ANTI_NAME_SPOOF = true, MARK_BANNED_PLAYERS = true, MUTE_BANNED_PLAYERS = true, BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS = true, //risky ANTI_PICKUP_SUMMON_MOD = false, ANTI_NO_TELEPORT_MOD = false, USE_HONEYPOTS = false, PERMA_BAN_LIST = false, SURPRESS_WHITESPACE_CHARS = false, //add-ons ADMIN_ONLY_OBJECTS = false, MODERATOR_AND_ADMIN_ONLY_OBJECTS = false, DESTROY_FOR_OTHER_PLAYERS = false, USE_CUSTOM_ALT_ACCOUNT_DETECTION = false, //settings ADMIN_CAN_FLY = true, MODERATOR_CAN_FLY = false, EVERYONE_CAN_FLY = false, MODERATOR_CAN_BAN = true, CAN_BAN_ADMINS = true, SUMMON_PANEL_FUNCTION = true, ACCURATE_CAPSULE_POSITIONS = true, //unity UNITY_SHOW_HASH_AND_SALT = false, UNITY_SHOW_PASSWORD = true, UNITY_STORE_CLEARTEXT_PASSWORD = true, //New since 06.05.2023 DESYNC_BANNED_PLAYERS = false, STEALTH_PANEL = false, NO_BAN_EFFECTS = false, DEBUG_ADMIN_NAME_DETECTION = false, REMOTE_STRING_LOADING = false, REMOTE_BAN_LIST = false, REMOTE_ADMIN_LIST = false, REMOTE_MODERATOR_LIST = false, HASH_USERNAMES = false, REMOTE_LOADING_URL = @"https://pastebin.com/raw/vTRKDYGw", REMOTE_LOADING_REPEAT_EACH_SECONDS = 60 * 5 //#NEW_PROPERTY# //------------------------- }; } #endregion CreateDefaultObjects #region LoadSettings /// /// Loads the settings from disk, creates a default settings file if there was no such file already /// private AdminPanelSettings LoadSettings() { if (!AssetDatabase.IsValidFolder(SETTINGS_FOLDER_PATH)) { SetupStoragePath(); } if (_loadedCache == null) { //It's a bit of a nightmare to do this check inside the asset-DB https://forum.unity.com/threads/how-to-check-if-assetdatabase-exists.246956/ //so we disregard the DB and check the filesystem instead and hope for the best if (!File.Exists(CACHE_FILE_PATH)) { //create a new JSON file with the default cache WriteTextFile(CACHE_FILE_PATH, JsonUtility.ToJson(GetDefaultCache(), true)); } //load the settings from disk string jsonString = ReadTextFile(CACHE_FILE_PATH); _loadedCache = JsonUtility.FromJson(jsonString); if (_loadedCache.SETTINGS_PATH_OVERRIDE != null && _loadedCache.SETTINGS_PATH_OVERRIDE.Trim() != String.Empty) { string overridePath = _loadedCache.SETTINGS_PATH_OVERRIDE; if (Directory.Exists(overridePath)) { SETTINGS_FILE_PATH = Path.Combine(overridePath, @"Settings.JSON"); ADMIN_FILE_PATH = Path.Combine(overridePath, @"Admins.txt"); MODERATOR_FILE_PATH = Path.Combine(overridePath, @"Moderators.txt"); PERMA_BAN_FILE_PATH = Path.Combine(overridePath, @"PermaBans.txt"); SALT_FILE_PATH = Path.Combine(overridePath, $"{AUTH_FOLDER}/Salt.txt"); HASH_FILE_PATH = Path.Combine(overridePath, $"{AUTH_FOLDER}/Hash.txt"); PASSWORD_FILE_PATH = Path.Combine(overridePath, $"{AUTH_FOLDER}/Password.txt"); } } } if (_loadedSettings != null && _loadedCache.DateOfLastSettingsFileChange != File.GetLastWriteTimeUtc(SETTINGS_FILE_PATH).Ticks) { //force reload _loadedSettings = null; //force script rewrite _scriptIsOutOfDate = true; } if (_loadedSettings == null) { if (!File.Exists(SETTINGS_FILE_PATH)) { //create a new JSON file with the default settings WriteTextFile(SETTINGS_FILE_PATH, JsonUtility.ToJson(GetDefaultSettings(), true)); } //load the settings from disk string jsonString = ReadTextFile(SETTINGS_FILE_PATH); //check if the loaded file is from an older version AdminPanelSettings tmpDefaults = GetDefaultSettings(); _loadedSettings = JsonUtility.FromJson(jsonString); //set correct default for new value from disk if (!jsonString.Contains(nameof(tmpDefaults.MODERATOR_CAN_BAN))) { _loadedSettings.MODERATOR_CAN_BAN = true; Debug.Log($"[AdminPanelEditor] Loaded older file from disk, added new {nameof(tmpDefaults.MODERATOR_CAN_BAN)} default."); } //set correct default for new value from disk if (!jsonString.Contains(nameof(tmpDefaults.REMOTE_LOADING_URL))) { _loadedSettings.REMOTE_LOADING_URL = tmpDefaults.REMOTE_LOADING_URL; Debug.Log($"[AdminPanelEditor] Loaded older file from disk, added new {nameof(tmpDefaults.REMOTE_LOADING_URL)} default."); } //set correct default for new value from disk if (!jsonString.Contains(nameof(tmpDefaults.REMOTE_LOADING_REPEAT_EACH_SECONDS))) { _loadedSettings.REMOTE_LOADING_REPEAT_EACH_SECONDS = tmpDefaults.REMOTE_LOADING_REPEAT_EACH_SECONDS; Debug.Log($"[AdminPanelEditor] Loaded older file from disk, added new {nameof(tmpDefaults.REMOTE_LOADING_REPEAT_EACH_SECONDS)} default."); } ApplySettingsToGUI(_loadedSettings); _loadedCache.DateOfLastSettingsFileChange = File.GetLastWriteTimeUtc(SETTINGS_FILE_PATH).Ticks; } long lastScriptChange = File.GetLastWriteTimeUtc(SCRIPT_FILE_PATH).Ticks; if (lastScriptChange != _loadedCache.DateOfLastScriptChange) { _scriptIsOutOfDate = true; } //any of these can set _reloadAuthFiles = true; LoadAdminList(); LoadModeratorList(); LoadPermaBanList(); LoadPasswordList(); LoadHashList(); LoadSaltList(); // reset _reloadAuthFiles to avoid another reload next time _reloadAuthFiles = false; //return the loaded and applied state return _loadedSettings; } /// /// Loads the list of admins from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadAdminList() { if (_loadedAdminList != null) { _oldCountLoadedAdmins = _loadedAdminList.Count; } else { _oldCountLoadedModerators = -1; } if (LoadUsernameList(ref _loadedAdminList, ref _displayedAdminList, GetDefaultAdminList(), ADMIN_FILE_PATH, ref _loadedCache.DateOfLastAdminFileChange)) { _reloadAuthFiles = true; } } /// /// Loads the list of moderators from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadModeratorList() { if (_loadedModeratorList != null) { _oldCountLoadedModerators = _loadedModeratorList.Count; } else { _oldCountLoadedModerators = -1; } if (LoadUsernameList(ref _loadedModeratorList, ref _displayedModeratorList, GetDefaultModeratorList(), MODERATOR_FILE_PATH, ref _loadedCache.DateOfLastModeratorFileChange)) { _reloadAuthFiles = true; } } /// /// Loads the list of perma bans from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadPermaBanList() { LoadUsernameList(ref _loadedPermaBanList, ref _displayedPermaBanList, GetDefaultPermaBanList(), PERMA_BAN_FILE_PATH, ref _loadedCache.DateOfLastPermaBanFileChange); } /// /// Loads the list of passwords from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadPasswordList() { if (!_reloadAuthFiles && (_loadedPasswordList != null && _loadedCache.DateOfLastPasswordFileChange == File.GetLastWriteTimeUtc(PASSWORD_FILE_PATH).Ticks)) return; _loadedPasswordList = ConvertToDisplayValues(LoadListFromDisk(PASSWORD_FILE_PATH, ref _loadedCache.DateOfLastPasswordFileChange, GetDefaultPasswordList())); Debug.Log($"[AdminPanelEditor] Loaded Password list from disk with {_loadedPasswordList.Count} values."); _displayedPasswordList = new List(_loadedPasswordList); //ensure we have enough values to cover all users AddNewEmptyEntries(ref _displayedPasswordList, false); } /// /// Loads the list of hashes from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadHashList() { if (!_reloadAuthFiles && (_loadedHashList != null && _loadedCache.DateOfLastHashFileChange == File.GetLastWriteTimeUtc(HASH_FILE_PATH).Ticks)) return; _loadedHashList = ConvertToDisplayValues(LoadListFromDisk(HASH_FILE_PATH, ref _loadedCache.DateOfLastHashFileChange, GetDefaultHashList())); Debug.Log($"[AdminPanelEditor] Loaded Hash list from disk with {_loadedHashList.Count} values."); _displayedHashList = new List(_loadedHashList); //ensure we have enough values to cover all users AddNewEmptyEntries(ref _displayedPasswordList, false); } /// /// Loads the list of salts from a text file into and also /// if the loaded list is null or if the list file on disk was edited since it was loaded last time /// private void LoadSaltList() { if (!_reloadAuthFiles && (_loadedSaltList != null && _loadedCache.DateOfLastSaltFileChange == File.GetLastWriteTimeUtc(SALT_FILE_PATH).Ticks)) return; _loadedSaltList = ConvertToDisplayValues(LoadListFromDisk(SALT_FILE_PATH, ref _loadedCache.DateOfLastSaltFileChange, GetDefaultSaltList())); Debug.Log($"[AdminPanelEditor] Loaded Salt list from disk with {_loadedSaltList.Count} values."); _displayedSaltList = new List(_loadedSaltList); //add random salts where no hash is set yet for (int i = 0; i < _displayedSaltList.Count; i++) { if (_displayedSaltList[i] == String.Empty) _displayedSaltList[i] = GetRandomReadableSalt(); } //ensure we have enough values to cover all users AddNewEmptyEntries(ref _displayedPasswordList, true); } /// /// Adds or inserts new default values for needed auth entries /// private void AddNewEmptyEntries(ref List list, bool isSalt) { //first insert new values for added admins if (_reloadAuthFiles && _oldCountLoadedAdmins != -1) { if (list.Count < _displayedAdminList.Count + _displayedModeratorList.Count) { int diff = _displayedAdminList.Count - _oldCountLoadedAdmins; while (diff > 0) { if (isSalt) { list.Insert(_oldCountLoadedAdmins, GetRandomReadableSalt()); } else { list.Insert(_oldCountLoadedAdmins, String.Empty); } diff--; } } } //ensure we have enough values to cover all users while (list.Count < _displayedAdminList.Count + _displayedModeratorList.Count) { list.Add(String.Empty); } } /// /// Converts an auth info list to the displayable values /// private static List ConvertToDisplayValues(in List input) { List newList = new List(input); for (int i = 0; i < newList.Count; i++) { if (newList[i] == AUTH_NOT_SET_IN_FILE) newList[i] = String.Empty; if (newList[i] == PASSWORD_UNKNOWN_IN_FILE) newList[i] = PASSWORD_UNKNOWN; } return newList; } /// /// Converts an auth info list to the storageable values /// private static List ConvertToStorageValues(in List input) { List newList = new List(input); for (int i = 0; i < newList.Count; i++) { if (newList[i] == String.Empty) newList[i] = AUTH_NOT_SET_IN_FILE; if (newList[i] == PASSWORD_UNKNOWN) newList[i] = PASSWORD_UNKNOWN_IN_FILE; } return newList; } /// /// Returns a list with one empty string per moderator and admin /// private static List GetDefaultEmptyAuthList() { List pwList = new List(); foreach (String admin in _loadedAdminList) { pwList.Add(String.Empty); } foreach (String moderator in _loadedModeratorList) { pwList.Add(String.Empty); } return pwList; } private bool _reloadAuthFiles; private int _oldCountLoadedAdmins; private int _oldCountLoadedModerators; /// /// Does nothing if loadedlist is not null and also the list on disk hasn't changed since the last known time. /// Else loads the if there is no file at the specified . /// Else it loads a list of strings from the text file there into /// private bool LoadUsernameList(ref List loadedlist, ref List displayedlist, List defaultList, string path, ref long lastKnownChange) { if (loadedlist != null && lastKnownChange == File.GetLastWriteTimeUtc(path).Ticks) return false; loadedlist = LoadListFromDisk(path, ref lastKnownChange, defaultList); Debug.Log($"[AdminPanelEditor] Loaded Username list from disk with {loadedlist.Count} values."); displayedlist = new List(loadedlist); return true; } /// /// Loads a list of strings (separated by line breaks) from a text file from the specified path /// if the /// private List LoadListFromDisk(string path, ref long lastChangedDateFromSettings, List defaultList) { //load the list from disk if (File.Exists(path)) { long lastFileChange = File.GetLastWriteTimeUtc(path).Ticks; //this check happens here as well to ensure that it is loaded either way (at start) but without setting the flag unless it changed since last time if (lastFileChange != lastChangedDateFromSettings) { _scriptIsOutOfDate = true; lastChangedDateFromSettings = lastFileChange; } return RemoveEmptyLinesAndTrimValues(TextToLineArray(ReadTextFile(path))); } else { WriteTextFile(path, ListToText(defaultList)); return defaultList; } } /// /// Converts a list of strings to a single text with line breaks between each element, while removing empty elements /// private string ListToText(List list) { string result = string.Empty; foreach (string entry in list) { if (entry.Trim() == string.Empty) continue; result += entry.Trim() + '\n'; } return result; } #endregion LoadSettings #region GUI public override void OnInspectorGUI() { ReimajoEditorBase.AddStandardHeader(PRODUCT_NAME, VERSION, DOCUMENTATION, target); if (!File.Exists(SCRIPT_FILE_PATH)) { EditorGUILayout.Space(); ReimajoEditorBase.DrawLabelField(Color.red, "ERROR: The asset path has changed from the default path"); ReimajoEditorBase.DrawLabelField(Color.red, SCRIPT_FILE_PATH); ReimajoEditorBase.DrawLabelField(Color.red, "This is currently not supported."); ReimajoEditorBase.DrawLabelField(Color.red, "Please move the asset back to the original path."); EditorGUILayout.Space(); return; } if (_lastEditFailed) { EditorGUILayout.Space(); ReimajoEditorBase.DrawLabelField(Color.red, "ERROR: Editing the script failed."); ReimajoEditorBase.DrawLabelField(Color.red, SCRIPT_FILE_PATH); EditorGUILayout.Space(); ReimajoEditorBase.DrawLabelField(Color.red, "This is because certain fields were not found inside the script."); ReimajoEditorBase.DrawLabelField(Color.red, "It is most likely that you accidentally made changes to the script directly."); ReimajoEditorBase.DrawLabelField(Color.red, "This is not supported for name lists and compiler options."); ReimajoEditorBase.DrawLabelField(Color.red, "Please restore the original script state by reimporting the asset."); ReimajoEditorBase.DrawLabelField(Color.red, "Only use the inspector in the future to edit those lists & options."); EditorGUILayout.Space(); ReimajoEditorBase.DrawUILine(Color.gray); } AdminPanelSettings settings = LoadSettings(); if (CheckUnappliedSettings(settings)) { ShowSettingsApplyButton(label1: "The script has pending changes.", label2: "Click the button below to apply the changes to the script.", buttonText: "Apply Settings"); } else if (_scriptIsOutOfDate) { ShowSettingsApplyButton(label1: "File(s) got changed on disk. The script maybe no longer has your settings.", label2: "Simply click the button below to (re-)apply your settings to the script.", buttonText: "Re-Apply Settings"); } DisplaySettings(settings); ReimajoEditorBase.DrawUILine(Color.gray); ReimajoEditorBase.DrawLabelField(Color.yellow, "The fields below are part of UdonSharp. Do not modify them unless you know what you are doing."); ReimajoEditorBase.DrawUILine(Color.gray); bool lastHasLineDrawn = true; Color cachedGuiColor = GUI.color; serializedObject.Update(); if (AreValuesMissing(serializedObject)) { if (!lastHasLineDrawn) ReimajoEditorBase.DrawUILine(Color.gray); ReimajoEditorBase.DrawLabelField(Color.red, "ERROR: Some mandatory references are missing; so the asset wont work (see the red fields below)"); ReimajoEditorBase.DrawUILine(Color.gray); lastHasLineDrawn = true; } SerializedProperty property = serializedObject.GetIterator(); bool isVisible = property.NextVisible(true); if (isVisible) do { GUI.color = cachedGuiColor; if (IsPropertyValueMissing(property)) { lastHasLineDrawn = HandlePropertyWithMissingRefs(property, lastHasLineDrawn); } else { lastHasLineDrawn = ReimajoEditorBase.Default_HandleProperty(property, lastHasLineDrawn); } } while (property.NextVisible(false)); EditorGUILayout.Space(); serializedObject.ApplyModifiedProperties(); } #endregion GUI #region SettingsGUI private void DisplaySettings(AdminPanelSettings settings) { // _toggleState_#1# = EditorGUILayoutToggle("#1#", _toggleState_#1#); #region ComplicanceSettings if (_toggleState_VRC_GUIDE_COMPLICANCE) { ReimajoEditorBase.DrawLabelField(Color.green, "This tool is currently fully compliant with the Udon moderation tool guide"); ReimajoEditorBase.DrawLabelField(Color.white, "https://docs.vrchat.com/docs/udon-moderation-tool-guidelines"); EditorGUILayout.Space(); ReimajoEditorBase.DrawLabelField(Color.yellow, "Hint: Disable compliance to see more safety options & features"); } _toggleState_VRC_GUIDE_COMPLICANCE = EditorGUILayoutToggle("VRC GUIDE COMPLICANCE", _toggleState_VRC_GUIDE_COMPLICANCE); if (!_toggleState_VRC_GUIDE_COMPLICANCE) { _toggleState_VRC_GUIDE_DEBUG = EditorGUILayoutToggle("VRC GUIDE DEBUG", _toggleState_VRC_GUIDE_DEBUG); DrawSectionHeader("=== Risky Extensions ==="); ReimajoEditorBase.DrawLabelField(Color.yellow, "Enable the following options at your own risk"); if (!_toggleState_ANTI_PICKUP_SUMMON_MOD || !_toggleState_USE_HONEYPOTS || !_toggleState_ANTI_NO_TELEPORT_MOD) ReimajoEditorBase.DrawLabelField(Color.yellow, "I recommend to enable the first 3 options below"); _toggleState_ANTI_PICKUP_SUMMON_MOD = EditorGUILayoutToggle("ANTI PICKUP SUMMON MOD", _toggleState_ANTI_PICKUP_SUMMON_MOD); _toggleState_ANTI_NO_TELEPORT_MOD = EditorGUILayoutToggle("ANTI NO TELEPORT MOD", _toggleState_ANTI_NO_TELEPORT_MOD); _toggleState_USE_HONEYPOTS = EditorGUILayoutToggle("USE HONEYPOTS", _toggleState_USE_HONEYPOTS); _toggleState_PERMA_BAN_LIST = EditorGUILayoutToggle("PERMA BAN LIST", _toggleState_PERMA_BAN_LIST); _toggleState_SURPRESS_WHITESPACE_CHARS = EditorGUILayoutToggle("SURPRESS WHITESPACE CHARS", _toggleState_SURPRESS_WHITESPACE_CHARS); _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION = EditorGUILayoutToggle("USE CUSTOM ALT ACCOUNT DETECTION", _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION); } #endregion ComplicanceSettings #region SafetySettings DrawSectionHeader("=== Safety Settings ==="); if (!_toggleState_PASSWORD_AUTHENTICATION) { ReimajoEditorBase.DrawLabelField(Color.yellow, "I recommend to use PASSWORD_AUTHENTICATION to prevent name spoofing locally"); } _toggleState_PASSWORD_AUTHENTICATION = EditorGUILayoutToggle("PASSWORD AUTHENTICATION", _toggleState_PASSWORD_AUTHENTICATION); //if (_toggleState_PASSWORD_AUTHENTICATION) //{ // ReimajoEditorBase.DrawLabelField(Color.yellow, "Don't forget to set up passwords for the users (check the documentation)."); //} EditorGUILayout.Space(); _toggleState_ANTI_NAME_SPOOF = EditorGUILayoutToggle("ANTI NAME SPOOF", _toggleState_ANTI_NAME_SPOOF); _toggleState_MARK_BANNED_PLAYERS = EditorGUILayoutToggle("MARK BANNED PLAYERS", _toggleState_MARK_BANNED_PLAYERS); _toggleState_MUTE_BANNED_PLAYERS = EditorGUILayoutToggle("MUTE BANNED PLAYERS", _toggleState_MUTE_BANNED_PLAYERS); _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS = EditorGUILayoutToggle("BANNED PLAYERS CAN TALK WITH MODERATORS", _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS); #endregion SafetySettings #region AddOnSettings DrawSectionHeader("=== Add-Ons ==="); _toggleState_ADMIN_ONLY_OBJECTS = EditorGUILayoutToggle("ADMIN ONLY OBJECTS", _toggleState_ADMIN_ONLY_OBJECTS); _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS = EditorGUILayoutToggle("MODERATOR AND ADMIN ONLY OBJECTS", _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS); _toggleState_DESTROY_FOR_OTHER_PLAYERS = EditorGUILayoutToggle("DESTROY FOR OTHER PLAYERS", _toggleState_DESTROY_FOR_OTHER_PLAYERS); #endregion AddOnSettings #region GeneralSettings DrawSectionHeader("=== General Settings ==="); _toggleState_ADMIN_CAN_FLY = EditorGUILayoutToggle("ADMIN CAN FLY", _toggleState_ADMIN_CAN_FLY); _toggleState_MODERATOR_CAN_FLY = EditorGUILayoutToggle("MODERATOR CAN FLY", _toggleState_MODERATOR_CAN_FLY); _toggleState_EVERYONE_CAN_FLY = EditorGUILayoutToggle("EVERYONE CAN FLY", _toggleState_EVERYONE_CAN_FLY); _toggleState_CAN_BAN_ADMINS = EditorGUILayoutToggle("CAN BAN ADMINS", _toggleState_CAN_BAN_ADMINS); _toggleState_MODERATOR_CAN_BAN = EditorGUILayoutToggle("MODERATOR CAN BAN", _toggleState_MODERATOR_CAN_BAN); _toggleState_SUMMON_PANEL_FUNCTION = EditorGUILayoutToggle("SUMMON PANEL FUNCTION", _toggleState_SUMMON_PANEL_FUNCTION); _toggleState_ACCURATE_CAPSULE_POSITIONS = EditorGUILayoutToggle("ACCURATE CAPSULE POSITIONS", _toggleState_ACCURATE_CAPSULE_POSITIONS); #endregion GeneralSettings #region NotRecommendedSettings DrawSectionHeader("=== Not recommended ==="); _toggleState_MINIMAL_BAN_DEBUG = EditorGUILayoutToggle("MINIMAL BAN DEBUG", _toggleState_MINIMAL_BAN_DEBUG); #endregion NotRecommendedSettings //New since 06.05.2023 #region ExperimentalSettings DrawSectionHeader("=== Experimental / Untested Features ==="); _toggleState_STEALTH_PANEL = EditorGUILayoutToggle("STEALTH PANEL", _toggleState_STEALTH_PANEL); _toggleState_NO_BAN_EFFECTS = EditorGUILayoutToggle("NO BAN EFFECTS", _toggleState_NO_BAN_EFFECTS); //_toggleState_DESYNC_BANNED_PLAYERS = EditorGUILayoutToggle("DESYNC BANNED PLAYERS", _toggleState_DESYNC_BANNED_PLAYERS); //New since 29.05.2023 //_toggleState_HASH_USERNAMES = EditorGUILayoutToggle("HASH USERNAMES", _toggleState_HASH_USERNAMES); _toggleState_REMOTE_STRING_LOADING = EditorGUILayoutToggle("REMOTE STRING LOADING", _toggleState_REMOTE_STRING_LOADING); if (_toggleState_REMOTE_STRING_LOADING) { _toggleState_REMOTE_ADMIN_LIST = EditorGUILayoutToggle("REMOTE ADMIN LIST", _toggleState_REMOTE_ADMIN_LIST); _toggleState_REMOTE_MODERATOR_LIST = EditorGUILayoutToggle("REMOTE MODERATOR LIST", _toggleState_REMOTE_MODERATOR_LIST); if (!_toggleState_VRC_GUIDE_COMPLICANCE) { _toggleState_REMOTE_BAN_LIST = EditorGUILayoutToggle("REMOTE BAN LIST", _toggleState_REMOTE_BAN_LIST); } float labelWidthDefault = EditorGUIUtility.labelWidth; //-------- Remote loading URL label and field ------------- GUILayout.BeginHorizontal(GUIStyle.none, GUILayout.Height(20)); EditorGUIUtility.labelWidth = 240; EditorGUILayout.LabelField(FormatLabel("REMOTE LOADING URL"), GUILayout.Width(EditorGUIUtility.labelWidth - 4)); GUILayout.Space(10); _editorState_REMOTE_LOADING_URL = EditorGUILayout.DelayedTextField(_editorState_REMOTE_LOADING_URL); GUILayout.EndHorizontal(); //-------- Remote loading delay label and field ------------- GUILayout.BeginHorizontal(GUIStyle.none, GUILayout.Height(20)); EditorGUILayout.LabelField(FormatLabel("LOADING REPEAT EACH SECONDS"), GUILayout.Width(EditorGUIUtility.labelWidth - 4)); GUILayout.Space(10); _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS = EditorGUILayout.DelayedIntField(_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS); if (_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS < 10) _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS = 10; GUILayout.EndHorizontal(); //----------------------------------------------------------- EditorGUIUtility.labelWidth = labelWidthDefault; } #endregion ExperimentalSettings #region DebugSettings DrawSectionHeader("=== Only for testing / Disable in live build ==="); _toggleState_DEBUG_ADMIN_NAME_DETECTION = EditorGUILayoutToggle("DEBUG ADMIN NAME DETECTION", _toggleState_DEBUG_ADMIN_NAME_DETECTION); if (_toggleState_DEBUG_ADMIN_NAME_DETECTION) ReimajoEditorBase.DrawLabelField(Color.red, "WARNING: DO NOT UPLOAD YOUR WORLD TO PUBLIC WITH THIS ENABLED, this is dangerous. Only use it for testing."); #endregion ExperimentalSettings //#NEW_PROPERTY# //------------------------- #region AdminList DrawSectionHeader("=== Admins ===", withLineAbove: true); if (_toggleState_PASSWORD_AUTHENTICATION) { _toggleState_UNITY_SHOW_PASSWORD = EditorGUILayoutToggle("Show passwords", _toggleState_UNITY_SHOW_PASSWORD, keepCasing: true); _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS = EditorGUILayoutToggle("Store cleartext passwords (in Unity)", _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS, keepCasing: true); _toggleState_UNITY_SHOW_HASH_AND_SALT = EditorGUILayoutToggle("[EXPERT] Show hash / salt", _toggleState_UNITY_SHOW_HASH_AND_SALT, keepCasing: true); EditorGUILayout.Space(); } try { DrawList(roleName: "Admin", ADMIN_FILE_PATH, ref _loadedAdminList, ref _displayedAdminList, showAuthInfo: true); } catch (Exception ex) { Debug.LogError("Unable to display Admin list in editor."); Debug.LogException(ex); } #endregion AdminList #region ModeratorList DrawSectionHeader("=== Moderators ===", withLineAbove: true); if (_toggleState_PASSWORD_AUTHENTICATION) { _toggleState_UNITY_SHOW_PASSWORD = EditorGUILayoutToggle("Show passwords", _toggleState_UNITY_SHOW_PASSWORD, keepCasing: true); _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS = EditorGUILayoutToggle("Store cleartext passwords", _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS, keepCasing: true); _toggleState_UNITY_SHOW_HASH_AND_SALT = EditorGUILayoutToggle("[EXPERT] Show hash / salt", _toggleState_UNITY_SHOW_HASH_AND_SALT, keepCasing: true); EditorGUILayout.Space(); } try { DrawList(roleName: "Moderator", MODERATOR_FILE_PATH, ref _loadedModeratorList, ref _displayedModeratorList, showAuthInfo: true, addOffset: true); } catch (Exception ex) { Debug.LogError("Unable to display Moderator list in editor."); Debug.LogException(ex); } #endregion AdminList #region PermaBanList if (!_toggleState_VRC_GUIDE_COMPLICANCE && _toggleState_PERMA_BAN_LIST) { DrawSectionHeader("=== Perma Bans ===", withLineAbove: true); DrawList(roleName: "Perma ban", PERMA_BAN_FILE_PATH, ref _loadedPermaBanList, ref _displayedPermaBanList); } #endregion PermaBanList } #endregion SettingsGUI #region ListDrawing /// /// Draws a list of specified VRChat usernames /// /// Name of the role /// Path to the file with the usernames in it /// The loaded list of usernames /// The dispalyed list of usernames /// If the auth info should be displayed next to it /// Offset in the auth info lists for this user private void DrawList(string roleName, string filePath, ref List loadedList, ref List displayedList, bool showAuthInfo = false, bool addOffset = false) { //since both admin and mod has auth info, but both is stored in the same auth file, they contain first the admins and then the moderators info int offsetInAuthLists = addOffset ? _displayedAdminList.Count : 0; for (int i = 0; i < displayedList.Count; i++) { bool isLocked = showAuthInfo && (displayedList[i] == "Reimajo" || displayedList[i] == "JoRoFox" || displayedList[i] == "Alex Lotor" || displayedList[i] == "NotFish"); GUILayout.BeginHorizontal(GUIStyle.none, GUILayout.Height(20)); if (_toggleState_PASSWORD_AUTHENTICATION && showAuthInfo) { float labelWidthDefault = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 110; displayedList[i] = LockableTextField($"VRChat Name:", displayedList[i], isLocked); GUILayout.Space(10); if (_toggleState_UNITY_SHOW_PASSWORD) { //color section yellow if there is no password set yet if (_displayedPasswordList[i + offsetInAuthLists].Trim() == String.Empty) { GUI.color = Color.yellow; } EditorGUIUtility.labelWidth = 70; _displayedPasswordList[i + offsetInAuthLists] = LockableTextField($"Password:", _displayedPasswordList[i + offsetInAuthLists], isLocked); GUI.color = Color.white; GUILayout.Space(10); } if (_toggleState_UNITY_SHOW_HASH_AND_SALT) { EditorGUIUtility.labelWidth = 40; _displayedHashList[i + offsetInAuthLists] = LockableTextField($"Hash:", _displayedHashList[i + offsetInAuthLists], isLocked); GUILayout.Space(10); _displayedSaltList[i + offsetInAuthLists] = LockableTextField($"Salt:", _displayedSaltList[i + offsetInAuthLists], isLocked); GUILayout.Space(10); } EditorGUIUtility.labelWidth = labelWidthDefault; } else { displayedList[i] = LockableTextField($"VRChat Name:", displayedList[i], isLocked); GUILayout.Space(10); } if (GUILayout.Button($"Remove", GUILayout.Height(20f), GUILayout.Width(70f))) { bool cancelRemove = false; if (displayedList[i] == "Reimajo" && showAuthInfo) { cancelRemove = !EditorUtility.DisplayDialog("Removing the asset creator from the admin list", "Are you sure you want to remove the creator of this asset from the admin list? " + "This will make it really hard for them to give you support if any issues occur.", "Yes, remove", "No, I'm a nice person"); if (!cancelRemove) { cancelRemove = !EditorUtility.DisplayDialog("Removing the asset creator from the admin list", "You clicked the wrong option. This one makes me sad, and I don't want to be sad. " + "I'm sure you only clicked it by accident, right? I'll give you another chance." + "The right answer is \"No\" by the way.\n\n" + "Are you sure you want to remove the creator of this asset from the admin list? " + "This will make it really hard for them to give you support if any issues occur.", "Yes, remove", "No, I'm a nice person"); } if (!cancelRemove) { cancelRemove = EditorUtility.DisplayDialog("Removing the asset creator from the admin list", "Whoops, you clicked the wrong option again. No sane person would ever want to remove me from the list. " + "But I'm sure you only clicked it by accident, right? I'll make this game easier for you:\n\n" + "To prevent the wrong click, I swapped the options around this time, because you probably just enjoy " + "clicking the default options, so now you can do so without removing me!\n\n" + "Are you sure you want to remove the creator of this asset from the admin list? " + "This will make it really hard for them to give you support if any issues occur.", "No, I'm a nice person", "Yes, remove"); } if (!cancelRemove) { cancelRemove = EditorUtility.DisplayDialog("Removing the asset creator from the admin list", "Oh wow, you are really mean to me. " + "I'm starting to question our relationship at this point... " + "Why would anyone ever want to not have me as an Admin in their world? " + "Maybe I didn't made the answer options clear enough, so let me try it again.\n\n" + "Are you really really sure you want to be a super mean person and make the creator of this asset really sad by removing them from the admin list? " + "This will make it really hard for them to give you support if any issues occur.", "No, because I'm a super nice person", "Yes, I'm mean and I hate the creator of this asset"); } if (!cancelRemove) { cancelRemove = !EditorUtility.DisplayDialog("Removing the asset creator from the admin list", "You tried really hard to get to this point here, wow. " + "You must hate me a lot. I think all hope is lost then. " + "All I wanted to have is the ability to fly in your world and you won't give it to me :(\n\n" + "I'd never ban anyone, just flying around, wheeeeeeeeeeeee! It would make me so happy! " + "Don't you want me to be happy? Maybe add me as a moderator then so I can at least fly? " + "Okay okay, I won't stop you anymore, but I'll go now and cry in a corner.\n\n" + "Are you really really sure you want to be a super mean person and don't allow the creator of this asset to have fun flying around by removing them from the admin list? " + "This will make it really hard for them to give you support if any issues occur.", "Yes, I'm mean and I hate the creator of this asset", "No, because I'm a super nice person"); } } if (!cancelRemove) { displayedList.RemoveAt(i); Debug.Log($"[AdminPanelEditor] Removed {roleName} at pos {i} / auth pos {i + offsetInAuthLists}."); if (showAuthInfo) { _displayedPasswordList.RemoveAt(i + offsetInAuthLists); _displayedHashList.RemoveAt(i + offsetInAuthLists); _displayedSaltList.RemoveAt(i + offsetInAuthLists); } } } GUILayout.EndHorizontal(); //warn about missing password if (_toggleState_PASSWORD_AUTHENTICATION && showAuthInfo && _displayedPasswordList[i + offsetInAuthLists].Trim() == String.Empty) { ReimajoEditorBase.DrawLabelField(Color.yellow, $"User needs a password to authenticate in game." + (!_toggleState_UNITY_SHOW_PASSWORD ? " Click \"show passwords\" above to add one." : "")); EditorGUILayout.Space(); } } if (ListHasChanged(loadedList, displayedList) || _toggleState_PASSWORD_AUTHENTICATION && (ListHasChanged(_loadedPasswordList, _displayedPasswordList) || ListHasChanged(_loadedHashList, _displayedHashList) || ListHasChanged(_loadedSaltList, _displayedSaltList))) { ReimajoEditorBase.DrawLabelField(Color.red, $"Changes are not applied yet, click the button on top to do so"); } EditorGUILayout.Space(); GUILayout.BeginHorizontal(GUIStyle.none, GUILayout.Height(25)); if (GUILayout.Button($"Add new {roleName}", GUILayout.Height(25f))) { displayedList.Add(String.Empty); Debug.Log($"[AdminPanelEditor] Added new {roleName} at pos {displayedList.Count} / auth pos {displayedList.Count + offsetInAuthLists - 1}."); if (showAuthInfo) { _displayedPasswordList.Insert(displayedList.Count + offsetInAuthLists - 1, String.Empty); _displayedHashList.Insert(displayedList.Count + offsetInAuthLists - 1, String.Empty); _displayedSaltList.Insert(displayedList.Count + offsetInAuthLists - 1, GetRandomReadableSalt()); } } GUILayout.Space(10); if (!_toggleState_PASSWORD_AUTHENTICATION || !showAuthInfo) { ReimajoEditorBase.DrawLabelField(Color.white, $"Hint: You can also add {roleName}s in /{filePath.Substring(UNITY_FOLDER.Length + 1)}"); } GUILayout.EndHorizontal(); EditorGUILayout.Space(); } /// /// A text field that can be locked, always returns the original content if it is locked, else the changed content /// private string LockableTextField(string label, string content, bool isLocked) { if (isLocked) { EditorGUILayout.BeginHorizontal(); { EditorGUILayout.LabelField(label, GUILayout.Width(EditorGUIUtility.labelWidth - 4)); EditorGUILayout.SelectableLabel(content, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight)); } EditorGUILayout.EndHorizontal(); return content; } else { return EditorGUILayout.TextField(label, content); } } private void DrawSectionHeader(string text, bool withLineAbove = false) { if (withLineAbove) { EditorGUILayout.Space(); ReimajoEditorBase.DrawUILine(Color.gray); } EditorGUILayout.Space(); ReimajoEditorBase.DrawLabelField(Color.white, text); EditorGUILayout.Space(); } /// /// Creates a standard GUI toggle but formats the label /// private bool EditorGUILayoutToggle(string label, bool value, bool keepCasing = false) { if (!keepCasing) { label = FormatLabel(label); } return EditorGUILayout.Toggle(label, value); } #endregion ListDrawing #region MissingProperties /// /// Returns true if a mandatory serialized property is missing /// /// /// private bool AreValuesMissing(SerializedObject serializedObject) { SerializedProperty property = serializedObject.GetIterator(); bool isVisible = property.NextVisible(true); if (isVisible) do { if (IsPropertyValueMissing(property)) { return true; } } while (property.NextVisible(false)); return false; } /// /// Draws a property in red because references are missing /// /// private static bool HandlePropertyWithMissingRefs(SerializedProperty property, bool lastHasLineDrawn) { if (property.isArray && property.propertyType != SerializedPropertyType.String) { DrawArrayWithMissingRefs(lastHasLineDrawn, property); return true; } else { GUI.color = Color.red; EditorGUILayout.PropertyField(property, property.isExpanded); GUI.color = Color.white; return false; } } /// /// Draws a property array between two lines and draws missing references red /// /// if a line has been drawn after the last property /// the array property private static void DrawArrayWithMissingRefs(bool lastHasLineDrawn, SerializedProperty property) { if (!lastHasLineDrawn) { ReimajoEditorBase.DrawUILine(Color.gray); } if (property.arraySize > 0) { property.isExpanded = true; } GUI.color = Color.red; //Set the color of the GUI EditorGUILayout.PropertyField(property, property.isExpanded); GUI.color = Color.white; //Reset the color of the GUI to white ReimajoEditorBase.DrawUILine(Color.gray); } /// /// Returns true if a property has a missing mandatory value /// /// /// private static bool IsPropertyValueMissing(SerializedProperty property) { bool isdefaultScriptProperty = property.name.Equals("m_Script") && property.type.Equals("PPtr") && property.propertyType == SerializedPropertyType.ObjectReference && property.propertyPath.Equals("m_Script"); if (isdefaultScriptProperty) return false; if (property.name.ToLower().StartsWith("_optional")) return false; if (property.isArray && property.propertyType != SerializedPropertyType.String) { for (int i = 0; i < property.arraySize; i++) { if (property.GetArrayElementAtIndex(i).propertyType == SerializedPropertyType.ObjectReference && property.GetArrayElementAtIndex(i).objectReferenceValue == null) return true; } return false; } else { if (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue == null) return true; } return false; } #endregion MissingProperties #region CheckUnappliedSettings /// /// Returns true if there are unapplied settings /// private bool CheckUnappliedSettings(AdminPanelSettings settings) { //New since 06.05.2023 if (_toggleState_DESYNC_BANNED_PLAYERS != settings.DESYNC_BANNED_PLAYERS) return true; if (_toggleState_STEALTH_PANEL != settings.STEALTH_PANEL) return true; if (_toggleState_NO_BAN_EFFECTS != settings.NO_BAN_EFFECTS) return true; if (_toggleState_DEBUG_ADMIN_NAME_DETECTION != settings.DEBUG_ADMIN_NAME_DETECTION) return true; if (_toggleState_REMOTE_STRING_LOADING != settings.REMOTE_STRING_LOADING) return true; if (_toggleState_REMOTE_BAN_LIST != settings.REMOTE_BAN_LIST) return true; if (_toggleState_REMOTE_ADMIN_LIST != settings.REMOTE_ADMIN_LIST) return true; if (_toggleState_REMOTE_MODERATOR_LIST != settings.REMOTE_MODERATOR_LIST) return true; if (_toggleState_HASH_USERNAMES != settings.HASH_USERNAMES) return true; if (_editorState_REMOTE_LOADING_URL != settings.REMOTE_LOADING_URL) return true; if (_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS != settings.REMOTE_LOADING_REPEAT_EACH_SECONDS) return true; //#NEW_PROPERTY# //------------------------- // if (_toggleState_#1# != settings.#1#) return true; if (_toggleState_VRC_GUIDE_COMPLICANCE != settings.VRC_GUIDE_COMPLICANCE) return true; if (_toggleState_VRC_GUIDE_DEBUG != settings.VRC_GUIDE_DEBUG) return true; if (_toggleState_PASSWORD_AUTHENTICATION != settings.PASSWORD_AUTHENTICATION) return true; if (_toggleState_MINIMAL_BAN_DEBUG != settings.MINIMAL_BAN_DEBUG) return true; if (_toggleState_ANTI_NAME_SPOOF != settings.ANTI_NAME_SPOOF) return true; if (_toggleState_MARK_BANNED_PLAYERS != settings.MARK_BANNED_PLAYERS) return true; if (_toggleState_MUTE_BANNED_PLAYERS != settings.MUTE_BANNED_PLAYERS) return true; if (_toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS != settings.BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS) return true; if (_toggleState_ANTI_PICKUP_SUMMON_MOD != settings.ANTI_PICKUP_SUMMON_MOD) return true; if (_toggleState_ANTI_NO_TELEPORT_MOD != settings.ANTI_NO_TELEPORT_MOD) return true; if (_toggleState_USE_HONEYPOTS != settings.USE_HONEYPOTS) return true; if (_toggleState_PERMA_BAN_LIST != settings.PERMA_BAN_LIST) return true; if (_toggleState_SURPRESS_WHITESPACE_CHARS != settings.SURPRESS_WHITESPACE_CHARS) return true; if (_toggleState_ADMIN_ONLY_OBJECTS != settings.ADMIN_ONLY_OBJECTS) return true; if (_toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS != settings.MODERATOR_AND_ADMIN_ONLY_OBJECTS) return true; if (_toggleState_DESTROY_FOR_OTHER_PLAYERS != settings.DESTROY_FOR_OTHER_PLAYERS) return true; if (_toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION != settings.USE_CUSTOM_ALT_ACCOUNT_DETECTION) return true; if (_toggleState_ADMIN_CAN_FLY != settings.ADMIN_CAN_FLY) return true; if (_toggleState_MODERATOR_CAN_FLY != settings.MODERATOR_CAN_FLY) return true; if (_toggleState_EVERYONE_CAN_FLY != settings.EVERYONE_CAN_FLY) return true; if (_toggleState_MODERATOR_CAN_BAN != settings.MODERATOR_CAN_BAN) return true; if (_toggleState_CAN_BAN_ADMINS != settings.CAN_BAN_ADMINS) return true; if (_toggleState_SUMMON_PANEL_FUNCTION != settings.SUMMON_PANEL_FUNCTION) return true; if (_toggleState_ACCURATE_CAPSULE_POSITIONS != settings.ACCURATE_CAPSULE_POSITIONS) return true; if (_toggleState_UNITY_SHOW_PASSWORD != settings.UNITY_SHOW_PASSWORD) return true; if (_toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS != settings.UNITY_STORE_CLEARTEXT_PASSWORD) return true; if (_toggleState_UNITY_SHOW_HASH_AND_SALT != settings.UNITY_SHOW_HASH_AND_SALT) return true; if (ListHasChanged(_loadedAdminList, _displayedAdminList)) return true; if (ListHasChanged(_loadedModeratorList, _displayedModeratorList)) return true; if (ListHasChanged(_loadedPermaBanList, _displayedPermaBanList)) return true; if (ListHasChanged(_loadedPasswordList, _displayedPasswordList)) return true; if (ListHasChanged(_loadedHashList, _displayedHashList)) return true; if (ListHasChanged(_loadedSaltList, _displayedSaltList)) return true; return false; } #endregion CheckUnappliedSettings #region ApplySettings /// /// Writes the loaded settings to the inspector window fields /// private void ApplySettingsToGUI(AdminPanelSettings settings) { //apply the settings to the editor state _toggleState_VRC_GUIDE_COMPLICANCE = _loadedSettings.VRC_GUIDE_COMPLICANCE; _toggleState_VRC_GUIDE_DEBUG = _loadedSettings.VRC_GUIDE_DEBUG; _toggleState_PASSWORD_AUTHENTICATION = _loadedSettings.PASSWORD_AUTHENTICATION; _toggleState_MINIMAL_BAN_DEBUG = _loadedSettings.MINIMAL_BAN_DEBUG; _toggleState_ANTI_NAME_SPOOF = _loadedSettings.ANTI_NAME_SPOOF; _toggleState_MARK_BANNED_PLAYERS = _loadedSettings.MARK_BANNED_PLAYERS; _toggleState_MUTE_BANNED_PLAYERS = _loadedSettings.MUTE_BANNED_PLAYERS; _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS = _loadedSettings.BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS; _toggleState_ANTI_PICKUP_SUMMON_MOD = _loadedSettings.ANTI_PICKUP_SUMMON_MOD; _toggleState_ANTI_NO_TELEPORT_MOD = _loadedSettings.ANTI_NO_TELEPORT_MOD; _toggleState_USE_HONEYPOTS = _loadedSettings.USE_HONEYPOTS; _toggleState_PERMA_BAN_LIST = _loadedSettings.PERMA_BAN_LIST; _toggleState_SURPRESS_WHITESPACE_CHARS = _loadedSettings.SURPRESS_WHITESPACE_CHARS; _toggleState_ADMIN_ONLY_OBJECTS = _loadedSettings.ADMIN_ONLY_OBJECTS; _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS = _loadedSettings.MODERATOR_AND_ADMIN_ONLY_OBJECTS; _toggleState_DESTROY_FOR_OTHER_PLAYERS = _loadedSettings.DESTROY_FOR_OTHER_PLAYERS; _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION = _loadedSettings.USE_CUSTOM_ALT_ACCOUNT_DETECTION; _toggleState_ADMIN_CAN_FLY = _loadedSettings.ADMIN_CAN_FLY; _toggleState_MODERATOR_CAN_FLY = _loadedSettings.MODERATOR_CAN_FLY; _toggleState_EVERYONE_CAN_FLY = _loadedSettings.EVERYONE_CAN_FLY; _toggleState_MODERATOR_CAN_BAN = _loadedSettings.MODERATOR_CAN_BAN; _toggleState_CAN_BAN_ADMINS = _loadedSettings.CAN_BAN_ADMINS; _toggleState_SUMMON_PANEL_FUNCTION = _loadedSettings.SUMMON_PANEL_FUNCTION; _toggleState_ACCURATE_CAPSULE_POSITIONS = _loadedSettings.ACCURATE_CAPSULE_POSITIONS; _toggleState_UNITY_SHOW_HASH_AND_SALT = _loadedSettings.UNITY_SHOW_HASH_AND_SALT; _toggleState_UNITY_SHOW_PASSWORD = _loadedSettings.UNITY_SHOW_PASSWORD; _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS = _loadedSettings.UNITY_STORE_CLEARTEXT_PASSWORD; //New since 06.05.2023 _toggleState_DESYNC_BANNED_PLAYERS = _loadedSettings.DESYNC_BANNED_PLAYERS; _toggleState_STEALTH_PANEL = _loadedSettings.STEALTH_PANEL; _toggleState_NO_BAN_EFFECTS = _loadedSettings.NO_BAN_EFFECTS; _toggleState_DEBUG_ADMIN_NAME_DETECTION = _loadedSettings.DEBUG_ADMIN_NAME_DETECTION; _toggleState_REMOTE_STRING_LOADING = _loadedSettings.REMOTE_STRING_LOADING; _toggleState_REMOTE_BAN_LIST = _loadedSettings.REMOTE_BAN_LIST; _toggleState_REMOTE_ADMIN_LIST = _loadedSettings.REMOTE_ADMIN_LIST; _toggleState_REMOTE_MODERATOR_LIST = _loadedSettings.REMOTE_MODERATOR_LIST; _toggleState_HASH_USERNAMES = _loadedSettings.HASH_USERNAMES; _editorState_REMOTE_LOADING_URL = _loadedSettings.REMOTE_LOADING_URL; _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS = _loadedSettings.REMOTE_LOADING_REPEAT_EACH_SECONDS; if (_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS == 0) //0 isn't a valid value, this comes from migration. We set it to the default value instead. _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS = 5 * 60; //#NEW_PROPERTY# //------------------------- } /// /// Draws a button to allow applying the changes to the script /// private void ShowSettingsApplyButton(string label1, string label2, string buttonText) { EditorGUILayout.Space(); GUI.color = Color.red; if (label1 != string.Empty) ReimajoEditorBase.DrawLabelField(Color.white, label1); if (label2 != string.Empty) ReimajoEditorBase.DrawLabelField(Color.white, label2); EditorGUILayout.Space(); GUILayout.BeginHorizontal(GUIStyle.none, GUILayout.Height(25)); if (GUILayout.Button(buttonText, GUILayout.Height(25f))) { Debug.Log("[AdminPanelEditor] User clicked \"Apply settings\""); ApplySettings(); } GUI.color = Color.white; GUILayout.EndHorizontal(); EditorGUILayout.Space(); ReimajoEditorBase.DrawUILine(Color.gray); EditorGUILayout.Space(); } #endregion ApplySettings #region SaveSettings /// /// Writes all changes done in the inspector back to the script file and updates the settings file /// private void ApplySettings() { _currentEditFailed = false; string[] newScript = TextToLineArray(ReadTextFile(SCRIPT_FILE_PATH)); //Check if the whitespace char surpression option changed if (_toggleState_SURPRESS_WHITESPACE_CHARS != _loadedSettings.SURPRESS_WHITESPACE_CHARS) { _scriptIsOutOfDate = true; //force regeneration of user name lists because this option changes how they are generated } #region ApplyToggleChanges /* BLUEPRINT: if (_toggleState_#1# != settings.#1#) { settings.#1# = _toggleState_#1#; ChangeCompilerOptionStateInScript("#1#", _toggleState_#1#); } */ //New since 06.05.2023 if (_scriptIsOutOfDate || _toggleState_DESYNC_BANNED_PLAYERS != _loadedSettings.DESYNC_BANNED_PLAYERS) { _loadedSettings.DESYNC_BANNED_PLAYERS = _toggleState_DESYNC_BANNED_PLAYERS; newScript = ChangeCompilerOptionStateInScript(newScript, "DESYNC_BANNED_PLAYERS", _toggleState_DESYNC_BANNED_PLAYERS); } if (_scriptIsOutOfDate || _toggleState_STEALTH_PANEL != _loadedSettings.STEALTH_PANEL) { _loadedSettings.STEALTH_PANEL = _toggleState_STEALTH_PANEL; newScript = ChangeCompilerOptionStateInScript(newScript, "STEALTH_PANEL", _toggleState_STEALTH_PANEL); } if (_scriptIsOutOfDate || _toggleState_NO_BAN_EFFECTS != _loadedSettings.NO_BAN_EFFECTS) { _loadedSettings.NO_BAN_EFFECTS = _toggleState_NO_BAN_EFFECTS; newScript = ChangeCompilerOptionStateInScript(newScript, "NO_BAN_EFFECTS", _toggleState_NO_BAN_EFFECTS); } //------------------------- if (_scriptIsOutOfDate || _toggleState_VRC_GUIDE_COMPLICANCE != _loadedSettings.VRC_GUIDE_COMPLICANCE) { _loadedSettings.VRC_GUIDE_COMPLICANCE = _toggleState_VRC_GUIDE_COMPLICANCE; newScript = ChangeCompilerOptionStateInScript(newScript, "VRC_GUIDE_COMPLICANCE", _toggleState_VRC_GUIDE_COMPLICANCE); } if (_scriptIsOutOfDate || _toggleState_VRC_GUIDE_DEBUG != _loadedSettings.VRC_GUIDE_DEBUG) { _loadedSettings.VRC_GUIDE_DEBUG = _toggleState_VRC_GUIDE_DEBUG; newScript = ChangeCompilerOptionStateInScript(newScript, "VRC_GUIDE_DEBUG", _toggleState_VRC_GUIDE_DEBUG); } if (_scriptIsOutOfDate || _toggleState_PASSWORD_AUTHENTICATION != _loadedSettings.PASSWORD_AUTHENTICATION) { _loadedSettings.PASSWORD_AUTHENTICATION = _toggleState_PASSWORD_AUTHENTICATION; newScript = ChangeCompilerOptionStateInScript(newScript, "PASSWORD_AUTHENTICATION", _toggleState_PASSWORD_AUTHENTICATION); } if (_scriptIsOutOfDate || _toggleState_MINIMAL_BAN_DEBUG != _loadedSettings.MINIMAL_BAN_DEBUG) { _loadedSettings.MINIMAL_BAN_DEBUG = _toggleState_MINIMAL_BAN_DEBUG; newScript = ChangeCompilerOptionStateInScript(newScript, "MINIMAL_BAN_DEBUG", _toggleState_MINIMAL_BAN_DEBUG); } if (_scriptIsOutOfDate || _toggleState_ANTI_NAME_SPOOF != _loadedSettings.ANTI_NAME_SPOOF) { _loadedSettings.ANTI_NAME_SPOOF = _toggleState_ANTI_NAME_SPOOF; newScript = ChangeCompilerOptionStateInScript(newScript, "ANTI_NAME_SPOOF", _toggleState_ANTI_NAME_SPOOF); } if (_scriptIsOutOfDate || _toggleState_MARK_BANNED_PLAYERS != _loadedSettings.MARK_BANNED_PLAYERS) { _loadedSettings.MARK_BANNED_PLAYERS = _toggleState_MARK_BANNED_PLAYERS; newScript = ChangeCompilerOptionStateInScript(newScript, "MARK_BANNED_PLAYERS", _toggleState_MARK_BANNED_PLAYERS); } if (_scriptIsOutOfDate || _toggleState_MUTE_BANNED_PLAYERS != _loadedSettings.MUTE_BANNED_PLAYERS) { _loadedSettings.MUTE_BANNED_PLAYERS = _toggleState_MUTE_BANNED_PLAYERS; newScript = ChangeCompilerOptionStateInScript(newScript, "MUTE_BANNED_PLAYERS", _toggleState_MUTE_BANNED_PLAYERS); } if (_scriptIsOutOfDate || _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS != _loadedSettings.BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS) { _loadedSettings.BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS = _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS; newScript = ChangeCompilerOptionStateInScript(newScript, "BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS", _toggleState_BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS); } if (_scriptIsOutOfDate || _toggleState_ANTI_PICKUP_SUMMON_MOD != _loadedSettings.ANTI_PICKUP_SUMMON_MOD) { _loadedSettings.ANTI_PICKUP_SUMMON_MOD = _toggleState_ANTI_PICKUP_SUMMON_MOD; newScript = ChangeCompilerOptionStateInScript(newScript, "ANTI_PICKUP_SUMMON_MOD", _toggleState_ANTI_PICKUP_SUMMON_MOD); } if (_scriptIsOutOfDate || _toggleState_ANTI_NO_TELEPORT_MOD != _loadedSettings.ANTI_NO_TELEPORT_MOD) { _loadedSettings.ANTI_NO_TELEPORT_MOD = _toggleState_ANTI_NO_TELEPORT_MOD; newScript = ChangeCompilerOptionStateInScript(newScript, "ANTI_NO_TELEPORT_MOD", _toggleState_ANTI_NO_TELEPORT_MOD); } if (_scriptIsOutOfDate || _toggleState_USE_HONEYPOTS != _loadedSettings.USE_HONEYPOTS) { _loadedSettings.USE_HONEYPOTS = _toggleState_USE_HONEYPOTS; newScript = ChangeCompilerOptionStateInScript(newScript, "USE_HONEYPOTS", _toggleState_USE_HONEYPOTS); } if (_scriptIsOutOfDate || _toggleState_PERMA_BAN_LIST != _loadedSettings.PERMA_BAN_LIST) { _loadedSettings.PERMA_BAN_LIST = _toggleState_PERMA_BAN_LIST; newScript = ChangeCompilerOptionStateInScript(newScript, "PERMA_BAN_LIST", _toggleState_PERMA_BAN_LIST); } if (_scriptIsOutOfDate || _toggleState_SURPRESS_WHITESPACE_CHARS != _loadedSettings.SURPRESS_WHITESPACE_CHARS) { _loadedSettings.SURPRESS_WHITESPACE_CHARS = _toggleState_SURPRESS_WHITESPACE_CHARS; newScript = ChangeCompilerOptionStateInScript(newScript, "SURPRESS_WHITESPACE_CHARS", _toggleState_SURPRESS_WHITESPACE_CHARS); } if (_scriptIsOutOfDate || _toggleState_ADMIN_ONLY_OBJECTS != _loadedSettings.ADMIN_ONLY_OBJECTS) { _loadedSettings.ADMIN_ONLY_OBJECTS = _toggleState_ADMIN_ONLY_OBJECTS; newScript = ChangeCompilerOptionStateInScript(newScript, "ADMIN_ONLY_OBJECTS", _toggleState_ADMIN_ONLY_OBJECTS); } if (_scriptIsOutOfDate || _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS != _loadedSettings.MODERATOR_AND_ADMIN_ONLY_OBJECTS) { _loadedSettings.MODERATOR_AND_ADMIN_ONLY_OBJECTS = _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS; newScript = ChangeCompilerOptionStateInScript(newScript, "MODERATOR_AND_ADMIN_ONLY_OBJECTS", _toggleState_MODERATOR_AND_ADMIN_ONLY_OBJECTS); } if (_scriptIsOutOfDate || _toggleState_DESTROY_FOR_OTHER_PLAYERS != _loadedSettings.DESTROY_FOR_OTHER_PLAYERS) { _loadedSettings.DESTROY_FOR_OTHER_PLAYERS = _toggleState_DESTROY_FOR_OTHER_PLAYERS; newScript = ChangeCompilerOptionStateInScript(newScript, "DESTROY_FOR_OTHER_PLAYERS", _toggleState_DESTROY_FOR_OTHER_PLAYERS); } if (_scriptIsOutOfDate || _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION != _loadedSettings.USE_CUSTOM_ALT_ACCOUNT_DETECTION) { _loadedSettings.USE_CUSTOM_ALT_ACCOUNT_DETECTION = _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION; newScript = ChangeCompilerOptionStateInScript(newScript, "USE_CUSTOM_ALT_ACCOUNT_DETECTION", _toggleState_USE_CUSTOM_ALT_ACCOUNT_DETECTION); } if (_scriptIsOutOfDate || _toggleState_ADMIN_CAN_FLY != _loadedSettings.ADMIN_CAN_FLY) { _loadedSettings.ADMIN_CAN_FLY = _toggleState_ADMIN_CAN_FLY; newScript = ChangeCompilerOptionStateInScript(newScript, "ADMIN_CAN_FLY", _toggleState_ADMIN_CAN_FLY); } if (_scriptIsOutOfDate || _toggleState_MODERATOR_CAN_FLY != _loadedSettings.MODERATOR_CAN_FLY) { _loadedSettings.MODERATOR_CAN_FLY = _toggleState_MODERATOR_CAN_FLY; newScript = ChangeCompilerOptionStateInScript(newScript, "MODERATOR_CAN_FLY", _toggleState_MODERATOR_CAN_FLY); } if (_scriptIsOutOfDate || _toggleState_EVERYONE_CAN_FLY != _loadedSettings.EVERYONE_CAN_FLY) { _loadedSettings.EVERYONE_CAN_FLY = _toggleState_EVERYONE_CAN_FLY; newScript = ChangeCompilerOptionStateInScript(newScript, "EVERYONE_CAN_FLY", _toggleState_EVERYONE_CAN_FLY); } if (_scriptIsOutOfDate || _toggleState_MODERATOR_CAN_BAN != _loadedSettings.MODERATOR_CAN_BAN) { _loadedSettings.MODERATOR_CAN_BAN = _toggleState_MODERATOR_CAN_BAN; newScript = ChangeCompilerOptionStateInScript(newScript, "MODERATOR_CAN_BAN", _toggleState_MODERATOR_CAN_BAN); } if (_scriptIsOutOfDate || _toggleState_CAN_BAN_ADMINS != _loadedSettings.CAN_BAN_ADMINS) { _loadedSettings.CAN_BAN_ADMINS = _toggleState_CAN_BAN_ADMINS; newScript = ChangeCompilerOptionStateInScript(newScript, "CAN_BAN_ADMINS", _toggleState_CAN_BAN_ADMINS); } if (_scriptIsOutOfDate || _toggleState_SUMMON_PANEL_FUNCTION != _loadedSettings.SUMMON_PANEL_FUNCTION) { _loadedSettings.SUMMON_PANEL_FUNCTION = _toggleState_SUMMON_PANEL_FUNCTION; newScript = ChangeCompilerOptionStateInScript(newScript, "SUMMON_PANEL_FUNCTION", _toggleState_SUMMON_PANEL_FUNCTION); } if (_scriptIsOutOfDate || _toggleState_ACCURATE_CAPSULE_POSITIONS != _loadedSettings.ACCURATE_CAPSULE_POSITIONS) { _loadedSettings.ACCURATE_CAPSULE_POSITIONS = _toggleState_ACCURATE_CAPSULE_POSITIONS; newScript = ChangeCompilerOptionStateInScript(newScript, "ACCURATE_CAPSULE_POSITIONS", _toggleState_ACCURATE_CAPSULE_POSITIONS); } if (_scriptIsOutOfDate || _toggleState_DEBUG_ADMIN_NAME_DETECTION != _loadedSettings.DEBUG_ADMIN_NAME_DETECTION) { _loadedSettings.DEBUG_ADMIN_NAME_DETECTION = _toggleState_DEBUG_ADMIN_NAME_DETECTION; newScript = ChangeCompilerOptionStateInScript(newScript, "DEBUG_ADMIN_NAME_DETECTION", _toggleState_DEBUG_ADMIN_NAME_DETECTION); } if (_scriptIsOutOfDate || _toggleState_REMOTE_STRING_LOADING != _loadedSettings.REMOTE_STRING_LOADING) { _loadedSettings.REMOTE_STRING_LOADING = _toggleState_REMOTE_STRING_LOADING; newScript = ChangeCompilerOptionStateInScript(newScript, "REMOTE_STRING_LOADING", _toggleState_REMOTE_STRING_LOADING); } if (_scriptIsOutOfDate || _toggleState_REMOTE_BAN_LIST != _loadedSettings.REMOTE_BAN_LIST) { _loadedSettings.REMOTE_BAN_LIST = _toggleState_REMOTE_BAN_LIST; newScript = ChangeCompilerOptionStateInScript(newScript, "REMOTE_BAN_LIST", _toggleState_REMOTE_BAN_LIST); } if (_scriptIsOutOfDate || _toggleState_REMOTE_ADMIN_LIST != _loadedSettings.REMOTE_ADMIN_LIST) { _loadedSettings.REMOTE_ADMIN_LIST = _toggleState_REMOTE_ADMIN_LIST; newScript = ChangeCompilerOptionStateInScript(newScript, "REMOTE_ADMIN_LIST", _toggleState_REMOTE_ADMIN_LIST); } if (_scriptIsOutOfDate || _toggleState_REMOTE_MODERATOR_LIST != _loadedSettings.REMOTE_MODERATOR_LIST) { _loadedSettings.REMOTE_MODERATOR_LIST = _toggleState_REMOTE_MODERATOR_LIST; newScript = ChangeCompilerOptionStateInScript(newScript, "REMOTE_MODERATOR_LIST", _toggleState_REMOTE_MODERATOR_LIST); } if (_scriptIsOutOfDate || _toggleState_HASH_USERNAMES != _loadedSettings.HASH_USERNAMES) { _loadedSettings.HASH_USERNAMES = _toggleState_HASH_USERNAMES; newScript = ChangeCompilerOptionStateInScript(newScript, "HASH_USERNAMES", _toggleState_HASH_USERNAMES); } if (_scriptIsOutOfDate || _editorState_REMOTE_LOADING_URL != _loadedSettings.REMOTE_LOADING_URL) { _loadedSettings.REMOTE_LOADING_URL = _editorState_REMOTE_LOADING_URL; newScript = ChangeVariableDeclarationInScript(newScript, "private const string URL_REMOTE_LOADING =", $"\t\tprivate const string URL_REMOTE_LOADING = \"{_editorState_REMOTE_LOADING_URL}\";"); } if (_scriptIsOutOfDate || _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS != _loadedSettings.REMOTE_LOADING_REPEAT_EACH_SECONDS) { if (_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS < 10) _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS = 10; _loadedSettings.REMOTE_LOADING_REPEAT_EACH_SECONDS = _editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS; newScript = ChangeVariableDeclarationInScript(newScript, "private const int REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS =", $"\t\tprivate const int REMOTE_LOADING_REPEAT_DELAY_ON_SUCCESS = {_editorState_REMOTE_LOADING_REPEAT_EACH_SECONDS} * 1000;"); } //#NEW_PROPERTY# #endregion ApplyToggleChanges #region RemoveEmptyNames RemoveEmptyPermUserEntry(roleName: "Admin", ref _displayedAdminList, isModeratorList: false); RemoveEmptyPermUserEntry(roleName: "Moderator", ref _displayedModeratorList, isModeratorList: true); RemoveLeftoverAuthInfo(); #endregion RemoveEmptyNames #region ApplyListChanges if (_scriptIsOutOfDate || ListHasChanged(_loadedAdminList, _displayedAdminList)) { _displayedAdminList = TrimValues(_displayedAdminList); Debug.Log($"[AdminPanelEditor] Writing new Admin list with {_displayedAdminList.Count} admins to disk."); //write the new admin list file WriteTextFile(ADMIN_FILE_PATH, string.Join(Environment.NewLine, _displayedAdminList)); _loadedAdminList = new List(_displayedAdminList); newScript = ChangeNameListInScript(newScript, "_superSpecialSnowflakes", _loadedAdminList); } if (_scriptIsOutOfDate || ListHasChanged(_loadedModeratorList, _displayedModeratorList)) { _displayedModeratorList = TrimValues(_displayedModeratorList); //write the new moderator list file Debug.Log($"[AdminPanelEditor] Writing new Moderator list with {_displayedModeratorList.Count} moderators to disk."); WriteTextFile(MODERATOR_FILE_PATH, string.Join(Environment.NewLine, _displayedModeratorList)); _loadedModeratorList = new List(_displayedModeratorList); newScript = ChangeNameListInScript(newScript, "_specialSnowflakes", _loadedModeratorList); } if (_scriptIsOutOfDate || ListHasChanged(_loadedPermaBanList, _displayedModeratorList)) { _displayedPermaBanList = RemoveEmptyLinesAndTrimValues(_displayedPermaBanList, _toggleState_SURPRESS_WHITESPACE_CHARS); //write the new permaban player list file Debug.Log($"[AdminPanelEditor] Writing new permaban list with {_displayedPermaBanList.Count} players to disk."); WriteTextFile(PERMA_BAN_FILE_PATH, string.Join(Environment.NewLine, _displayedPermaBanList)); _loadedPermaBanList = new List(_displayedPermaBanList); newScript = ChangeNameListInScript(newScript, "_permaBannedPlayers", _loadedPermaBanList); } //remove auth entries that have no user RemoveLeftoverAuthInfo(); //first we remove potential user errors _displayedPasswordList = TrimValues(_displayedPasswordList); _displayedHashList = TrimValues(_displayedHashList); _displayedSaltList = TrimValues(_displayedSaltList); //if any of the credential lists changes we do a full re-write if (_scriptIsOutOfDate || ListHasChanged(_loadedPasswordList, _displayedPasswordList) || ListHasChanged(_loadedHashList, _displayedHashList) || ListHasChanged(_loadedSaltList, _displayedSaltList)) { //calculate hashes etc. for (int i = 0; i < _displayedPasswordList.Count; i++) { bool pwIsSecret = _displayedPasswordList[i] == PASSWORD_UNKNOWN; bool pw_set = _displayedPasswordList[i] != string.Empty; bool hash_set = _displayedHashList[i] != string.Empty; bool salt_set = _displayedSaltList[i] != string.Empty; //remove invalid entry where the hash is missing if (_displayedPasswordList[i] == PASSWORD_UNKNOWN && !hash_set) { Debug.Log($"[AdminPanelEditor] Hash was missing for entry {i} with unknown password, removed it."); _displayedPasswordList[i] = string.Empty; pw_set = false; _displayedSaltList[i] = GetRandomReadableSalt(); hash_set = true; } //if no password is set but hash was set, we assume the user wants no cleatext pw to be stored if (!pw_set && hash_set) { Debug.Log($"[AdminPanelEditor] Password not set for entry {i} so we set it to unknown, since a hash is supplied instead."); _displayedPasswordList[i] = PASSWORD_UNKNOWN; } //else if password & hash is set, but no salt, we add a salt and recalculate the hash else if (pw_set && hash_set && !salt_set) { if (!pwIsSecret) { Debug.Log($"[AdminPanelEditor] Salt not set for entry {i} so we set it to a random one and recalculate the hash."); _displayedSaltList[i] = GetRandomReadableSalt(); _displayedHashList[i] = GetHash(_displayedPasswordList[i], _displayedSaltList[i]); } } //else if password is set, we recalculate hash (and set the salt if needed) else if (pw_set) { if (!pwIsSecret) { if (!salt_set) { _displayedSaltList[i] = GetRandomReadableSalt(); } _displayedHashList[i] = GetHash(_displayedPasswordList[i], _displayedSaltList[i]); } } //now purge the password if wanted if (pw_set && !_toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS) { _displayedPasswordList[i] = PASSWORD_UNKNOWN; } } //write the new password list file Debug.Log($"[AdminPanelEditor] Writing new password list with {_displayedPasswordList.Count} entries to disk."); WriteTextFile(PASSWORD_FILE_PATH, string.Join(Environment.NewLine, ConvertToStorageValues(_displayedPasswordList))); Debug.Log($"[AdminPanelEditor] Writing new hash list with {_displayedHashList.Count} entries to disk."); WriteTextFile(HASH_FILE_PATH, string.Join(Environment.NewLine, ConvertToStorageValues(_displayedHashList))); Debug.Log($"[AdminPanelEditor] Writing new salt list with {_displayedSaltList.Count} entries to disk."); WriteTextFile(SALT_FILE_PATH, string.Join(Environment.NewLine, ConvertToStorageValues(_displayedSaltList))); //remember when we did the last file changes _loadedCache.DateOfLastPasswordFileChange = File.GetLastWriteTimeUtc(PASSWORD_FILE_PATH).Ticks; _loadedCache.DateOfLastHashFileChange = File.GetLastWriteTimeUtc(HASH_FILE_PATH).Ticks; _loadedCache.DateOfLastSaltFileChange = File.GetLastWriteTimeUtc(SALT_FILE_PATH).Ticks; _loadedPasswordList = new List(_displayedPasswordList); _loadedHashList = new List(_displayedHashList); _loadedSaltList = new List(_displayedSaltList); //create a list of all users that have credentials List userList = new List(_displayedAdminList); userList.AddRange(_displayedModeratorList); newScript = ChangeCredentialsObjectInScript(newScript, "_credentials", userList, _displayedSaltList, _displayedHashList); } #endregion ApplyListChanges #region ApplyEditorSettingsChange if (_toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS != _loadedSettings.UNITY_STORE_CLEARTEXT_PASSWORD) { _loadedSettings.UNITY_STORE_CLEARTEXT_PASSWORD = _toggleState_UNITY_STORE_CLEARTEXT_PASSWORDS; } if (_toggleState_UNITY_SHOW_PASSWORD != _loadedSettings.UNITY_SHOW_PASSWORD) { _loadedSettings.UNITY_SHOW_PASSWORD = _toggleState_UNITY_SHOW_PASSWORD; } if (_toggleState_UNITY_SHOW_HASH_AND_SALT != _loadedSettings.UNITY_SHOW_HASH_AND_SALT) { _loadedSettings.UNITY_SHOW_HASH_AND_SALT = _toggleState_UNITY_SHOW_HASH_AND_SALT; } #endregion ApplyEditorSettingsChange //write the new script file WriteTextFile(SCRIPT_FILE_PATH, string.Join(Environment.NewLine, newScript)); //remember when we did the last file changes _loadedCache.DateOfLastScriptChange = File.GetLastWriteTimeUtc(SCRIPT_FILE_PATH).Ticks; _loadedCache.DateOfLastAdminFileChange = File.GetLastWriteTimeUtc(ADMIN_FILE_PATH).Ticks; _loadedCache.DateOfLastModeratorFileChange = File.GetLastWriteTimeUtc(MODERATOR_FILE_PATH).Ticks; _loadedCache.DateOfLastPermaBanFileChange = File.GetLastWriteTimeUtc(PERMA_BAN_FILE_PATH).Ticks; _scriptIsOutOfDate = false; //write the new settings to the same settings file WriteTextFile(SETTINGS_FILE_PATH, JsonUtility.ToJson(_loadedSettings, true)); //read the time of the settings file change _loadedCache.DateOfLastSettingsFileChange = File.GetLastWriteTimeUtc(SETTINGS_FILE_PATH).Ticks; //write the new cache values to the same cache file WriteTextFile(CACHE_FILE_PATH, JsonUtility.ToJson(_loadedCache, true)); AssetDatabase.Refresh(); _lastEditFailed = _currentEditFailed; } /// /// Removes an empty username entry in the moderator or admin user list (and their auth info) /// private static void RemoveEmptyPermUserEntry(string roleName, ref List displayedList, bool isModeratorList = false) { //since both admin and mod has auth info, but both is stored in the same auth file, they contain first the admins and then the moderators info int offsetInAuthLists = isModeratorList ? _displayedAdminList.Count : 0; for (int i = 0; i < displayedList.Count; i++) { if (displayedList[i].Trim() == String.Empty) { displayedList.RemoveAt(i); Debug.Log($"[AdminPanelEditor] Removed empty {roleName} entry at pos {i} / auth pos {i + offsetInAuthLists}."); _displayedPasswordList.RemoveAt(i + offsetInAuthLists); _displayedHashList.RemoveAt(i + offsetInAuthLists); _displayedSaltList.RemoveAt(i + offsetInAuthLists); } } } /// /// Removes auth info that has no user assigned /// private static void RemoveLeftoverAuthInfo() { int totalUserCount = _displayedAdminList.Count + _displayedModeratorList.Count; if (_displayedPasswordList.Count > totalUserCount) { _displayedPasswordList.RemoveRange(totalUserCount, _displayedPasswordList.Count - totalUserCount); } if (_displayedHashList.Count > totalUserCount) { _displayedHashList.RemoveRange(totalUserCount, _displayedHashList.Count - totalUserCount); } if (_displayedSaltList.Count > totalUserCount) { _displayedSaltList.RemoveRange(totalUserCount, _displayedSaltList.Count - totalUserCount); } } /// /// Changes the values of a string array inside the script /// private string[] ChangeNameListInScript(string[] newScript, string fieldName, List newListContent) { for (int i = 0; i < newScript.Length; i++) { if (newScript[i].Trim().StartsWith("private string[] " + fieldName)) { if (newScript[i + 1].Trim().StartsWith("{") && newScript[i + 1].Trim().EndsWith("};")) { newScript[i + 1] = CreateStringArrayAsText(newListContent); return newScript; } } } _currentEditFailed = true; return newScript; } /// /// Changes the values of a string array inside the script /// private string[] ChangeCredentialsObjectInScript(string[] newScript, string fieldName, List usernames, List salts, List hashes) { for (int i = 0; i < newScript.Length; i++) { if (newScript[i].Trim().StartsWith("private object[] " + fieldName)) { if (newScript[i + 1].Trim().StartsWith("{") && newScript[i + 1].Trim().EndsWith("};")) { newScript[i + 1] = CreateObjectArrayAsText(usernames, salts, hashes); return newScript; } } } _currentEditFailed = true; return newScript; } /// /// Creates a string object array literal from a list of strings, e.g. /// /// new object[] /// { /// new string[] { @"c1", @"c2", @"c3" }, /// new string[] { @"c1_1", @"c2_1", @"c3_1" }, /// new string[] { @"c1_2", @"c2_2", @"c3_2" } /// }; /// /// to be inserted into a source code file (but formated as one line) /// private static string CreateObjectArrayAsText(List usernames, List salts, List hashes) { string output = "\t\t{ "; int addedCount = 0; for (int i = 0; i < usernames.Count; i++) { if (hashes[i] != String.Empty) { output += "new string[] { @\"" + EscapeQuotationCharacters(usernames[i]) + "\", @\"" + EscapeQuotationCharacters(salts[i]) + "\", @\"" + EscapeQuotationCharacters(hashes[i]) + "\" }, "; addedCount++; } } if (addedCount > 0) { output = output.Substring(0, output.Length - 2); } return output + @" };"; } /// /// Adjusts the compiler option state to the state in the user settings /// and returns the changed script array /// private string[] ChangeCompilerOptionStateInScript(string[] newScript, string settingsName, bool newState) { for (int i = 0; i < newScript.Length; i++) { if (newScript[i].StartsWith("#define " + settingsName) || newScript[i].StartsWith("//#define " + settingsName)) { //already enabled and should be enabled if (newScript[i].StartsWith("#define " + settingsName) && newState) return newScript; //already disabled and should be disabled if (newScript[i].StartsWith("//#define " + settingsName) && !newState) return newScript; //already enabled but should be disabled if (newScript[i].StartsWith("#define " + settingsName) && !newState) { newScript[i] = @"//" + newScript[i]; return newScript; } //already disabled but should be enabled if (newScript[i].StartsWith("//#define " + settingsName) && newState) { newScript[i] = newScript[i].Substring(2); return newScript; } } } //if the compiler setting couldn't be found _currentEditFailed = true; return newScript; } /// /// Adjusts the variable value to the state in the user settings /// and returns the changed script array /// private string[] ChangeVariableDeclarationInScript(string[] newScript, string settingsDeclarationStartsWith, string newSettingsDeclaration) { for (int i = 0; i < newScript.Length; i++) { if (newScript[i].Trim().StartsWith(settingsDeclarationStartsWith)) { //replace the whole line with the new declaration newScript[i] = newSettingsDeclaration; return newScript; } } //if the compiler setting couldn't be found _currentEditFailed = true; return newScript; } #endregion SaveSettings #region HelperFunctions /// /// Essentially Convert.ToHexString() from .NET5 /// public static string ByteArrayToString(byte[] input) { StringBuilder hexResult = new StringBuilder(input.Length * 2); foreach (byte b in input) hexResult.AppendFormat("{0:x2}", b); return hexResult.ToString(); } /// /// Creates a SHA512 hash with password+salt as an input, using UTF8 as a format /// private string GetHash(string password, string salt) { byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(password + salt); using (SHA512 sha512Managed = new SHA512Managed()) { return ByteArrayToString(sha512Managed.ComputeHash(inputBytes)); } } /// /// Lenght of a randomly generated salt /// private static int RANDOM_SALT_LENGHT = 32; private const string READABLE_CHARS = @"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /// /// Returns a random salt of the max lenght /// with readable characters from only /// public static string GetRandomReadableSalt() { RNGCryptoServiceProvider cryptoServiceProvider = new RNGCryptoServiceProvider(); byte[] randomBytes = new byte[RANDOM_SALT_LENGHT]; cryptoServiceProvider.GetNonZeroBytes(randomBytes); char[] bufferCharArray = new char[RANDOM_SALT_LENGHT]; char[] usableCharArray = READABLE_CHARS.ToCharArray(); for (int index = 0; index < RANDOM_SALT_LENGHT; index++) { bufferCharArray[index] = usableCharArray[randomBytes[index] % usableCharArray.Length]; } return new string(bufferCharArray); } /// /// Returns true if the two lists are not equal /// private static bool ListHasChanged(List loadedAdminList, List displayedAdminList) { return !loadedAdminList.SequenceEqual(displayedAdminList); } /// /// Reads a textfile as Unicode /// private static string ReadTextFile(string path) { return File.ReadAllText(path, System.Text.Encoding.UTF8); } /// /// Writes a textfile as Unicode /// private static void WriteTextFile(string path, string content) { File.WriteAllText(path, content, System.Text.Encoding.UTF8); } /// /// Formats the text from a label like Unity does in the inspector, so THIS_IS_A_LABEL becomes "This Is A Label" /// private static string FormatLabel(string label) { string newLabel = String.Empty; bool isCaps = true; foreach (char c in label) { if (c == ' ') { isCaps = true; newLabel += ' '; } else if (c == '_') { isCaps = true; newLabel += ' '; } else { if (isCaps) { newLabel += c.ToString().ToUpper(); isCaps = false; } else { newLabel += c.ToString().ToLower(); } } } return newLabel; } /// /// Converts a text to an array, split by newline breaks /// private static string[] TextToLineArray(string input) { return input.Split( new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None ); } /// /// Trims all lines and then also removes empty lines from an array /// private static List RemoveEmptyLinesAndTrimValues(string[] input) { List result = new List(); foreach (string line in input) { if (!string.IsNullOrEmpty(line.Trim())) result.Add(line.Trim()); } return result; } /// /// Trims all lines and then also removes empty lines from a List /// private static List RemoveEmptyLinesAndTrimValues(List input, bool cleanNames = false) { List result = new List(); foreach (string line in input) { if (!string.IsNullOrEmpty(line.Trim())) { if (cleanNames) result.Add(CleanNameFromWhitespaceCharacters(line.Trim())); else result.Add(line.Trim()); } } return result; } /// /// Trims all lines and then also removes empty lines from a List /// private static List TrimValues(List input, bool cleanNames = false) { List result = new List(); foreach (string line in input) { if (cleanNames) result.Add(CleanNameFromWhitespaceCharacters(line.Trim())); else result.Add(line.Trim()); } return result; } /// /// Creates a string array literal from a list of strings, e.g. { @"line1", @"line2", @"line3" }; /// to be inserted into a source code file /// private static string CreateStringArrayAsText(List list) { string output = "\t\t{ "; foreach (string s in list) { output += "@\"" + EscapeQuotationCharacters(s) + "\", "; } if (list.Count > 0) { output = output.Substring(0, output.Length - 2); } return output + @" };"; } /// /// Replaces " chars with "" so they are escaped in a literal @ string added to a source code file /// private static string EscapeQuotationCharacters(string input) { return input.Replace("\"", "\"\""); } /// /// Removes known alternative whitespace characters except the original space character /// private static string CleanNameFromWhitespaceCharacters(string playerName) { playerName = playerName.Trim(); //Note: I assume VRChat does that already int lenght = playerName.Length; char[] nameAsCharArray = playerName.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; } } return new string(nameAsCharArray, 0, newMaxIndex); ; } #endregion HelperFunctions #endregion BaseEditor } #region JSON /// /// Format of the JSON file where the cache info is stored /// [System.Serializable] public class AdminPanelCache { public string SETTINGS_PATH_OVERRIDE; public long DateOfLastSettingsFileChange; public long DateOfLastScriptChange; public long DateOfLastAdminFileChange; public long DateOfLastModeratorFileChange; public long DateOfLastPermaBanFileChange; public long DateOfLastPasswordFileChange; public long DateOfLastHashFileChange; public long DateOfLastSaltFileChange; } /// /// Format of the JSON file where the user settings are stored /// [System.Serializable] public class AdminPanelSettings { //New since 06.05.2023 public bool DESYNC_BANNED_PLAYERS; public bool STEALTH_PANEL; public bool NO_BAN_EFFECTS; public bool DEBUG_ADMIN_NAME_DETECTION; public bool REMOTE_STRING_LOADING; public bool REMOTE_BAN_LIST; public bool REMOTE_ADMIN_LIST; public bool REMOTE_MODERATOR_LIST; public bool HASH_USERNAMES; public string REMOTE_LOADING_URL; public int REMOTE_LOADING_REPEAT_EACH_SECONDS; //#NEW_PROPERTY# //------------------------- public bool VRC_GUIDE_COMPLICANCE; public bool VRC_GUIDE_DEBUG; public bool PASSWORD_AUTHENTICATION; public bool MINIMAL_BAN_DEBUG; public bool ANTI_NAME_SPOOF; public bool MARK_BANNED_PLAYERS; public bool MUTE_BANNED_PLAYERS; public bool BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS; public bool ANTI_PICKUP_SUMMON_MOD; public bool ANTI_NO_TELEPORT_MOD; public bool USE_HONEYPOTS; public bool PERMA_BAN_LIST; public bool SURPRESS_WHITESPACE_CHARS; public bool ADMIN_ONLY_OBJECTS; public bool MODERATOR_AND_ADMIN_ONLY_OBJECTS; public bool DESTROY_FOR_OTHER_PLAYERS; public bool USE_CUSTOM_ALT_ACCOUNT_DETECTION; public bool ADMIN_CAN_FLY; public bool MODERATOR_CAN_FLY; public bool EVERYONE_CAN_FLY; public bool MODERATOR_CAN_BAN; public bool CAN_BAN_ADMINS; public bool SUMMON_PANEL_FUNCTION; public bool ACCURATE_CAPSULE_POSITIONS; public bool UNITY_SHOW_PASSWORD; public bool UNITY_STORE_CLEARTEXT_PASSWORD; public bool UNITY_SHOW_HASH_AND_SALT; } #endregion JSON } #region DevNotes /* (Just a template for my code generator, don't mind this) VRC_GUIDE_COMPLICANCE,VRC_GUIDE_DEBUG,PASSWORD_AUTHENTICATION,MINIMAL_BAN_DEBUG,ANTI_NAME_SPOOF,MARK_CRASHERS,MUTE_BANNED_PLAYERS,BANNED_PLAYERS_CAN_TALK_WITH_MODERATORS,ANTI_PICKUP_SUMMON_MOD,ANTI_NO_TELEPORT_MOD,USE_HONEYPOTS,PERMA_BAN_LIST,SURPRESS_WHITESPACE_CHARS,ADMIN_ONLY_OBJECTS,MODERATOR_AND_ADMIN_ONLY_OBJECTS,DESTROY_FOR_OTHER_PLAYERS,USE_CUSTOM_ALT_ACCOUNT_DETECTION,ADMIN_CAN_FLY,EVERYONE_CAN_FLY,CAN_BAN_ADMINS,SUMMON_PANEL_FUNCTION,ACCURATE_CAPSULE_POSITIONS */ #endregion DevNotes