using System; using System.Collections.Generic; using System.IO; using System.Linq; using Vintagestory.API.Client; using Vintagestory.API.Common; using Vintagestory.API.Common.CommandAbbr; using Vintagestory.API.Common.Entities; using Vintagestory.API.Config; using Vintagestory.API.MathTools; using Vintagestory.API.Server; using Vintagestory.API.Util; using Vintagestory.GameContent; namespace AriasServerUtils { public class ServerUtilities : ModSystem { public static string MOD_ID = "ariasserverutils"; public static ASUModConfig config = new ASUModConfig(); private static ICoreServerAPI API; private static bool bDirty = false; internal static Dictionary backupInventory = new Dictionary(); internal static Dictionary mPlayerData = new Dictionary(); internal static BackCaches backCaches = new BackCaches(); internal static Warps serverWarps = new Warps(); internal static string[] saveInvTypes = new string[] { GlobalConstants.hotBarInvClassName, GlobalConstants.backpackInvClassName, GlobalConstants.craftingInvClassName, GlobalConstants.mousecursorInvClassName, GlobalConstants.characterInvClassName }; /// /// Method to register all mod blocks /// /// private void RegisterBlocks(ICoreAPI api) { api.Logger.Notification("Begin registering block classes for Aria's Server Utils..."); api.Logger.Notification("Block Classes have been registered for Aria's Server Utils!"); } private void RegisterBlockEntities(ICoreAPI api) { } // Called on server and client public override void Start(ICoreAPI api) { api.Logger.Notification(Lang.Get($"{MOD_ID}:start")); RegisterBlocks(api); RegisterBlockEntities(api); } public override void StartServerSide(ICoreServerAPI api) { API = api; api.Logger.Notification(Lang.Get($"{MOD_ID}:start")); api.Event.ServerRunPhase(EnumServerRunPhase.GameReady, OnGameReady); api.Event.ServerRunPhase(EnumServerRunPhase.Shutdown, OnShutdown); api.Event.Timer(OnCheckModDirty, 20); api.Event.Timer(OnCheckPlayerCooldowns, 1); api.Event.PlayerDeath += OnPlayerDeath; api.Event.PlayerJoin += OnPlayerJoin; api.Event.PlayerDisconnect += OnPlayerDC; //api.Event.PlayerLeave += OnPlayerDC; api.ChatCommands.Create("setspawn").RequiresPrivilege(Privilege.controlserver).HandleWith(Events.HandleSetSpawn); api.ChatCommands.Create("spawn").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleSpawn); api.ChatCommands.Create("delspawn").RequiresPrivilege(Privilege.controlserver).HandleWith(Events.HandleClearSpawn); //api.ChatCommands.Create("test_death").RequiresPlayer().RequiresPrivilege(Privilege.controlserver).HandleWith(TestDeath); var parsers = api.ChatCommands.Parsers; api.ChatCommands.Create("restoreinv").RequiresPlayer().WithArgs(parsers.OnlinePlayer("player")).HandleWith(Events.HandleReturnItems).WithDescription("Returns items to a player in the event of a problem").RequiresPrivilege(Privilege.controlserver); api.ChatCommands.Create("sethome").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Creates a home").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleSetHome); api.ChatCommands.Create("home").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Teleports you to home").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleGoHome); api.ChatCommands.Create("delhome").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Deletes a home entry").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleDelHome); api.ChatCommands.Create("homes").RequiresPlayer().WithDescription("Lists your homes").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleListHomes); api.ChatCommands.Create("asu") .RequiresPrivilege(Privilege.chat) .BeginSubCommand("update") .BeginSubCommand("maxhomes") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Int("maxHomes") ) .WithDescription("Updates the maximum number of homes") .HandleWith(Events.HandleUpdateASUMaxHomes) .EndSubCommand() .BeginSubCommand("adminhomes") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Bool("adminsBypass") ) .WithDescription("Updates the flag deciding whether admins can bypass max number of homes") .HandleWith(Events.HandleUpdateASUBypass) .EndSubCommand() .WithDescription("Updates the ASU mod configuration") .BeginSubCommand("onlyAdminManageWarps") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Bool("manageWarps") ) .WithDescription("DANGER: This updates the flag allowing anybody to create warps, even non-admins. It is recommended to leave this alone. The default is true/on/yes") .HandleWith(Events.HandleUpdateASUMgrWarps) .EndSubCommand() .BeginSubCommand("adminBypassCooldown") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Bool("bypass") ) .WithDescription("This flag controls whether admins can or can not bypass the cooldown system (Default: true)") .HandleWith(Events.HandleUpdateASUBypassCD) .EndSubCommand() .BeginSubCommand("adminsBypassRTPMaxDist") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Bool("bypass") ) .WithDescription("This flag would allow admins to go further than the max server RTP distance when manually specifying a distance to RTP (Default: false)") .HandleWith(Events.HandleUpdateASUBypassRTPMaxDist) .EndSubCommand() .BeginSubCommand("maxback") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.OptionalInt("max") ) .WithDescription("Max number of back positions cached for players") .HandleWith(Events.HandleUpdateASUMaxBack) .EndSubCommand() .BeginSubCommand("playerSleepingPercentage") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.OptionalIntRange("psp", 1, 100, 50) ) .WithDescription("Percentage of players required to sleep before sleeping through the night") .HandleWith(Events.HandleUpdateASUPSP) .EndSubCommand() .BeginSubCommand("rtp") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Int("maxDistance") ) .WithDescription("Update RTP Max block distance. Plus and/or minus this distance from player current position (Default is 50000)") .HandleWith(Events.HandleUpdateASURTPMax) .EndSubCommand() .BeginSubCommand("cooldowns") .WithDescription("Commands related to all the various cooldowns") .BeginSubCommand("back") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Word("cooldown") ) .WithDescription("Updates the cooldown time on /back (Default is 5s)") .HandleWith(Events.HandleUpdateASUCDBack) .EndSubCommand() .BeginSubCommand("warp") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Word("cooldown") ) .WithDescription("Updates the cooldown time on /warp (Default is 10s)") .HandleWith(Events.HandleUpdateASUCDWarp) .EndSubCommand() .BeginSubCommand("home") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Word("cooldown") ) .WithDescription("Updates the cooldown time on /home (Default is 5s)") .HandleWith(Events.HandleUpdateASUCDHome) .EndSubCommand() .BeginSubCommand("spawn") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Word("cooldown") ) .WithDescription("Updates the cooldown time on /spawn (Default is 5s)") .HandleWith(Events.HandleUpdateASUCDSpawn) .EndSubCommand() .BeginSubCommand("rtp") .RequiresPrivilege(Privilege.controlserver) .WithArgs( parsers.Word("cooldown") ) .WithDescription("Updates the cooldown time on /rtp (Default is 30s)") .HandleWith(Events.HandleUpdateASUCDRTP) .EndSubCommand() .BeginSubCommand("reset") .RequiresPrivilege(Privilege.controlserver) .WithDescription("Resets all cooldowns to default values") .HandleWith(Events.HandleUpdateASUCDReset) .EndSubCommand() .EndSubCommand() .EndSubCommand() .BeginSubCommand("help") .RequiresPrivilege(Privilege.chat) .HandleWith(Events.HandleASU) .WithDescription("Lists all Aria's Server Utils commands") .EndSubCommand(); api.ChatCommands.Create("setwarp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Creates a new server warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarpUpdate); api.ChatCommands.Create("warp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Warp to the specified server warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarp); api.ChatCommands.Create("delwarp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Deletes the specified warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarpDelete); api.ChatCommands.Create("warps").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Lists all server warps").HandleWith(Events.HandleWarpList); api.ChatCommands.Create("back").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Returns you to the last location you were at").HandleWith(Events.HandleBack); api.ChatCommands.Create("rtp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithArgs(parsers.OptionalInt("maxDist", defaultValue: -1)).WithDescription("Seeks a position possibly thousands of blocks away to teleport to.").HandleWith(Events.HandleRTP); api.ChatCommands.Create("listcooldowns").RequiresPrivilege(Privilege.chat).WithDescription("Lists the cooldown settings on the server, as well as your own active cooldowns if applicable.").HandleWith(Events.HandleListCooldowns); } private void OnCheckPlayerCooldowns() { foreach (var cdEntry in ServerUtilities.mPlayerData) { // Obtain the IServerPlayer instance for this player. IServerPlayer player = API.Server.Players.First(x => x.PlayerName == cdEntry.Key); if (player.HasPrivilege(Privilege.controlserver) && ServerUtilities.config.AdminsBypassCooldowns) { cdEntry.Value.ActiveCooldowns.Clear(); // Problem solved. } List toRemove = new(); foreach (var cd in cdEntry.Value.ActiveCooldowns) { if (cd.Value < TimeUtil.GetUnixEpochTimestamp()) { toRemove.Add(cd.Key); } } foreach (var item in toRemove) { cdEntry.Value.ActiveCooldowns.Remove(item); } if (toRemove.Count > 0) MarkDirty(); } } public static void NewBackCacheForPlayer(IServerPlayer player) { backCaches.AddPosition(player.PlayerName, PlayerPosition.from(player.Entity)); } private void OnPlayerDC(IServerPlayer byPlayer) { OnCheckModDirty(); mPlayerData.Remove(byPlayer.PlayerName); } private void OnPlayerJoin(IServerPlayer byPlayer) { API.Logger.Notification($"[ASU] {Lang.Get($"{MOD_ID}:playerjoin")}"); PlayerStorage data = API.LoadModConfig(GetConfigurationFile(byPlayer.PlayerName, ModConfigType.World)); if (data == null) data = new PlayerStorage(); mPlayerData[byPlayer.PlayerName] = data; } public static PlayerStorage GetPlayerData(IServerPlayer player) { if (mPlayerData.ContainsKey(player.PlayerName)) { return mPlayerData[player.PlayerName]; } else { return new PlayerStorage(); } } private TextCommandResult TestDeath(TextCommandCallingArgs args) { if (args.Caller.Player is IServerPlayer isp) OnPlayerDeath(isp, null); return TextCommandResult.Success(); } private void OnPlayerDeath(IServerPlayer player, DamageSource damageSource) { PlayerInventory inv = new PlayerInventory(); var invMgr = player.InventoryManager; NewBackCacheForPlayer(player); var iBackpackSlotNum = 0; foreach (var type in saveInvTypes) { foreach (var stack in invMgr.GetOwnInventory(type)) { if (iBackpackSlotNum >= 4) { continue; } if (type == GlobalConstants.backpackInvClassName) { iBackpackSlotNum++; } if (stack.Empty) continue; if (stack.Inventory.ClassName == GlobalConstants.characterInvClassName) { if (stack.Itemstack.ItemAttributes?["protectionModifiers"].Exists ?? false) { inv.Items.Add(stack.Itemstack.Clone()); } } else inv.Items.Add(stack.Itemstack.Clone()); API.Logger.Notification($"SAVED STORAGE ITEM TYPE: {stack.Itemstack}"); } } backupInventory[player.PlayerName] = inv; } private void OnCheckModDirty() { if (bDirty) { //API.Logger.Notification(Lang.Get($"{MOD_ID}:timer")); bDirty = false; SaveGlobalConfig(); SavePlayerData(); } } private void SavePlayerData() { foreach (var data in mPlayerData) { API.StoreModConfig(data.Value, GetConfigurationFile(data.Key, ModConfigType.World)); } } private void OnShutdown() { // Mod Shutdown // // Handle any remaining tasks before shutdown API.Logger.Notification(Lang.Get($"{MOD_ID}:halt")); OnCheckModDirty(); } public void SaveGlobalConfig() { API.StoreModConfig(config, GetConfigurationFile("global", ModConfigType.Global)); API.StoreModConfig(serverWarps, GetConfigurationFile("warps", ModConfigType.Global)); } private void OnGameReady() { // Mod Setup Info // // -> Step 1. Load Mod Global Config <- config = API.LoadModConfig(GetConfigurationFile("global", ModConfigType.Global)); if (config == null) config = new ASUModConfig(); // Step 2. Check if config needs Migrations config.SanityCheck(); // -> Step 3. Load Mod Warps <- serverWarps = API.LoadModConfig(GetConfigurationFile("warps", ModConfigType.Global)); if (serverWarps == null) serverWarps = new Warps(); } public string GetConfigurationFile(string sName, ModConfigType type) { if (type == ModConfigType.Global) { return $"ariaserverconfig/{sName}.json"; } else if (type == ModConfigType.World) { return $"ariaserverconfig/{GetWorldName()}/{sName}.json"; } else return $"ariaserverconfig/global.json"; } /// /// This function is used to mark the mod's global config, and all loaded player configs as dirty. They will be flushed to disk, then the dirty flag will be cleared. /// public static void MarkDirty() { bDirty = true; } public string GetWorldName() { string[] lName = API.WorldManager.CurrentWorldName.Split(Path.DirectorySeparatorChar); string sName = lName[lName.Length - 1]; return sName.Substring(0, sName.Length - 6); } public override void StartClientSide(ICoreClientAPI api) { api.Logger.Notification(Lang.Get($"{MOD_ID}:start")); } public static void SendMessageTo(IServerPlayer player, string sMsg) { player.SendMessage(0, sMsg, EnumChatType.CommandSuccess); } } }