using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Oxide.Core; using Oxide.Core.Libraries; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using Oxide.Plugins; using System.Collections.Generic; using UnityEngine; using System.Collections; using System.IO; using System.Linq; using System.Globalization; using ConVar; using Facepunch; using Facepunch.Rust; using ProtoBuf; namespace Oxide.Plugins { [Info("DisbandedTeamRestore", "21mac21", "1.5.1")] internal class DisbandedTeamRestore : CovalencePlugin { /* Changelog 1.0.0 initial release 1.1.0 Bugfix: Fixed problem while saving the data file after the latest rust update 1.2.0 Bugfix: Did a small rewrite since some classes changed after the last rust server update Feature: Added config option to keep all backup files (estimated 20-30 per day, just a few bytes each) instead of just ONE Feature: Added a debug parameter in the .cs file to enable verbose logging to the console 1.3.0 - 1.4.0 I'd rather not tell you what I did in this two releases. major fuckup 1.5.0 Bugfix: Players sometimes were put in a wrong team together with other players (big no-go!) Feature: Added permissions and chat/console commands to list, disband and restore teams manually Feature: Calling the hook "OnTeamAcceptInvite" after restoring the teams, so that the "Automatic authorization" plugin will authorize players on TCs, turrets and doors/locks again 1.5.1 Bugfix: Changed userID to userID.Get() after facepunch's surprise patch */ private static DisbandedTeamRestore _instance; private PluginConfig _config; private bool debug = false; private const string PermissionAdmin = "DisbandedTeamRestore.admin"; private void Init() { _instance = this; permission.RegisterPermission(PermissionAdmin, this); AddCovalenceCommand("dtr.saveteams", nameof(SaveTeamsCommand)); AddCovalenceCommand("dtr.disbandteams", nameof(DisbandTeamsCommand)); AddCovalenceCommand("dtr.restoreteams", nameof(RestoreTeamsCommand)); AddCovalenceCommand("dtr.listteams", nameof(ListTeamsCommand)); } // frees the pool of player teams, clears all dictionaries and resets the lastTeamIndex private void ResetAllTeams() { List list = Facepunch.Pool.GetList(); Facepunch.Pool.FreeList(ref list); RelationshipManager.ServerInstance.cachedPlayers = new Dictionary(); RelationshipManager.ServerInstance.playerToTeam = new Dictionary(); RelationshipManager.ServerInstance.teams = new Dictionary(); RelationshipManager.ServerInstance.lastTeamIndex = (ulong)1; } // lists all teams and members in chat or server console private void ListTeamsCommand(IPlayer player, string command, string[] args) { if(player == null) return; BasePlayer basePlayer = (BasePlayer)player.Object; if(basePlayer != null && !permission.UserHasPermission(basePlayer.UserIDString, PermissionAdmin)) { AlertToPlayer(player, GetMessage("noPermissionForCommand", basePlayer.UserIDString)); return; } foreach(KeyValuePair kvp in RelationshipManager.ServerInstance.teams) { BasePlayer tempPlayer = BasePlayer.FindAwakeOrSleepingByID(kvp.Value.teamLeader); if(basePlayer != null) AlertToPlayer(player, $"Team {kvp.Key} with ID {kvp.Value.teamID} and leader \"{tempPlayer?.displayName}\" ({kvp.Value.teamLeader}):"); else Puts($"Team {kvp.Key} with ID {kvp.Value.teamID} and leader \"{tempPlayer?.displayName}\" ({kvp.Value.teamLeader}):"); foreach(ulong member in kvp.Value.members) { tempPlayer = BasePlayer.FindAwakeOrSleepingByID(member); if(basePlayer != null) AlertToPlayer(player, $"Member: \"{tempPlayer?.displayName}\" ({member})"); else Puts($"Member: \"{tempPlayer?.displayName}\" ({member})"); } } } private void SaveTeamsCommand(IPlayer player, string command, string[] args) { if(player == null) return; BasePlayer basePlayer = (BasePlayer)player.Object; if(basePlayer != null && !permission.UserHasPermission(basePlayer.UserIDString, PermissionAdmin)) { AlertToPlayer(player, GetMessage("noPermissionForCommand", basePlayer.UserIDString)); return; } if(debug) Puts($"Manual team save, teams will be saved to data file"); SaveTeams(true); } private void DisbandTeamsCommand(IPlayer player, string command, string[] args) { if(player == null) return; BasePlayer basePlayer = (BasePlayer)player.Object; if(basePlayer != null && !permission.UserHasPermission(basePlayer.UserIDString, PermissionAdmin)) { AlertToPlayer(player, GetMessage("noPermissionForCommand", basePlayer.UserIDString)); return; } if(debug) Puts($"Manual team disband, all teams will be disbanded"); DisbandAllTeams(); ResetAllTeams(); } private void RestoreTeamsCommand(IPlayer player, string command, string[] args) { if(player == null) return; BasePlayer basePlayer = (BasePlayer)player.Object; if(basePlayer != null && !permission.UserHasPermission(basePlayer.UserIDString, PermissionAdmin)) { AlertToPlayer(player, GetMessage("noPermissionForCommand", basePlayer.UserIDString)); return; } PluginData pluginData = LoadDataFile("SavedTeams"); if(debug) Puts($"Manual team restore, teams will be restored from loaded data file"); DisbandAllTeams(); ResetAllTeams(); if(debug) Puts($"Restoring {pluginData.teams.Count.ToString()} teams from loaded data file"); RestoreAllTeams(pluginData); if(debug) Puts($"Restore successful"); } protected override void LoadDefaultConfig() { Puts("Creating a default config..."); _config = PluginConfig.DefaultConfig(); SaveConfig(); Puts("Creation of the default config completed!"); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) { throw new JsonException(); } if (!_config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys)) { Puts("Configuration appears to be outdated; updating and saving"); SaveConfig(); } } catch { Puts($"Configuration file {Name}.json is invalid; using defaults"); LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_config); private void SaveTeams(bool shutdownWasSuccessful = false) { if(debug) Puts($"Called 'SaveTeams' with parameter 'shutdownWasSuccessful' {shutdownWasSuccessful.ToString()}"); PluginData pluginData = new PluginData(); pluginData.ShutdownWasSuccessful = shutdownWasSuccessful; pluginData.teams = RelationshipManager.ServerInstance.teams; foreach(KeyValuePair kvp in pluginData.teams) { RelationshipManager.PlayerTeam playerTeam = kvp.Value; //omit the realtime connection info since this throws an error on serialization: //playerTeam.onlineMemberConnections = new List(); } SaveDataFile("SavedTeams", pluginData); if(_config.KeepAllBackups) { string formattedDate = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"); SaveDataFile($"SavedTeams_backup_{formattedDate}", pluginData); } if(debug) Puts($"Ended 'SaveTeams', {pluginData.teams.Count.ToString()} teams were saved."); } private class PluginConfig { [JsonProperty("Keep all backups, not only the last")] public bool KeepAllBackups { get; set; } public string ToJson() => JsonConvert.SerializeObject(this); public Dictionary ToDictionary() => JsonConvert.DeserializeObject>(ToJson()); public static PluginConfig DefaultConfig() { return new PluginConfig() { KeepAllBackups = true, }; } } private T LoadDataFile(string fileName) { try { T dataFile = Interface.Oxide.DataFileSystem.ReadObject($"{Name}/{fileName}"); if(debug) Puts($"Called 'LoadDataFile' with parameter 'fileName' {fileName}"); return dataFile; } catch (Exception ex) { PrintError(ex.ToString()); return default(T); } } private void SaveDataFile(string fileName, T data) { if(debug) Puts($"Called 'SaveDataFile' with parameter 'fileName' {fileName}"); Interface.Oxide.DataFileSystem.WriteObject($"{Name}/{fileName}", data); } public class PluginData { public bool ShutdownWasSuccessful; public Dictionary teams; } private void DisbandAllTeams() { List playerTeamIDs = new List(); if(debug) Puts($"Deleting {RelationshipManager.ServerInstance.teams.Count.ToString()} currently existing teams"); // copy, so the original enumeration does not get changed while iterating foreach(KeyValuePair kvp in RelationshipManager.ServerInstance.teams) { RelationshipManager.PlayerTeam playerTeam = kvp.Value; playerTeamIDs.Add(kvp.Key); } for(int i = 0; i < playerTeamIDs.Count; i++) { RelationshipManager.PlayerTeam teamToDisband = RelationshipManager.ServerInstance.FindTeam(playerTeamIDs[i]); if(debug) Puts($"Disbanding team with ID {playerTeamIDs[i]}"); if(teamToDisband != null) { List membersCloned = new List(teamToDisband.members); foreach(ulong memberID in membersCloned) { if(debug) Puts($"Removing member with ID {memberID}"); BasePlayer basePlayer = RelationshipManager.FindByID(memberID); if (basePlayer != null) { teamToDisband.RemovePlayer(basePlayer.userID.Get()); } } try{ RelationshipManager.ServerInstance.teams.Remove(playerTeamIDs[i]); if(teamToDisband != null) { Facepunch.Pool.Free(ref teamToDisband); teamToDisband.Disband(); } } catch (Exception ex) { Puts(ex.Message); } } } if(debug) Puts($"Confirmation: {RelationshipManager.ServerInstance.teams.Count} teams left after disbanding"); } private void RestoreAllTeams(PluginData pluginData) { foreach (RelationshipManager.PlayerTeam playerTeam in pluginData.teams.Values) { if(debug) Puts($"Restoring team with ID {playerTeam.teamID}"); RelationshipManager.PlayerTeam nums = new RelationshipManager.PlayerTeam(); nums.teamLeader = playerTeam.teamLeader; nums.teamID = playerTeam.teamID; nums.teamName = playerTeam.teamName; nums.members = new List(); foreach (ulong member in playerTeam.members) { BasePlayer basePlayer = RelationshipManager.FindByID(member); if(basePlayer != null) { nums.AddPlayer(basePlayer); basePlayer.currentTeam = nums.teamID; } else { Puts($"Player {member} not found"); } } RelationshipManager.ServerInstance.teams.Add(nums.teamID, nums); // short delay to make sure the team is already registered/cached in the relationshipManager's instance _instance.timer.Once(1f, () => { foreach (ulong member in nums.members) { BasePlayer basePlayer = RelationshipManager.FindByID(member); if(basePlayer != null) { if(debug) Puts($"Calling hook \"OnTeamAcceptInvite\" for team ID {nums.teamID} and player ID {member}"); Interface.CallHook("OnTeamAcceptInvite", nums, basePlayer); } } }); if(nums.teamID >= RelationshipManager.ServerInstance.lastTeamIndex) { if(debug) Puts($"Setting lastteamindex to {nums.teamID + 1}"); RelationshipManager.ServerInstance.lastTeamIndex = nums.teamID + 1; } } } private void OnServerInitialized() { PluginData pluginData = LoadDataFile("SavedTeams"); if( pluginData != null && !pluginData.ShutdownWasSuccessful) { Puts($"Last shutdown was not successful, teams will be restored from loaded data file"); DisbandAllTeams(); ResetAllTeams(); if(debug) Puts($"Restoring {pluginData.teams.Count.ToString()} teams from loaded data file"); RestoreAllTeams(pluginData); Puts($"Restore successful"); } } private void Unload() { if(debug) Puts($"Called hook 'Unload'"); SaveTeams(true); } private void OnServerShutdown() { if(debug) Puts($"Called hook 'OnServerShutdown'"); SaveTeams(true); } private void OnServerSave() { if(debug) Puts($"Called hook 'OnServerSave'"); SaveTeams(); } #region Lang protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary { ["noPermissionForCommand"] = "You don't have the permission for this command.", }, this); } private string GetMessage(string langKey, string userID) => lang.GetMessage(langKey, _instance, userID); private string GetMessage(string langKey, string userID, params object[] args) => (args.Length == 0) ? GetMessage(langKey, userID) : string.Format(GetMessage(langKey, userID), args); #endregion Lang #region Alerts private void AlertToAllPlayers(string langKey, params object[] args) { foreach (BasePlayer player in BasePlayer.activePlayerList) AlertToPlayer(player, GetMessage(langKey, player.UserIDString, args)); } private void AlertToPlayer(BasePlayer player, string message) { player.IPlayer.Message(message); } private void AlertToPlayer(IPlayer player, string message) { player.Message(message); } #endregion Alerts } }