diff --git a/.vscode/launch.json b/.vscode/launch.json index d1cd6d4..26b43b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,12 +8,12 @@ "name": "Launch Client (Debug)", "type": "coreclr", "request": "launch", - "program": "${env:VINTAGE_STORY}/Vintagestory.exe", + "program": ".game/Vintagestory.exe", "linux": { - "program": "${env:VINTAGE_STORY}/Vintagestory" + "program": ".game/Vintagestory" }, "osx": { - "program": "${env:VINTAGE_STORY}/Vintagestory" + "program": ".game/Vintagestory" }, "preLaunchTask": "build", "args": [ @@ -21,7 +21,7 @@ // "--openWorld" , "modding test world", "--tracelog", "--addModPath", - "${workspaceFolder}/ModTemplate/bin/Debug/Mods" + "${workspaceFolder}/AriasServerUtils/bin/Debug/Mods" ], "console": "internalConsole", "stopAtEntry": false @@ -30,18 +30,18 @@ "name": "Launch Server", "type": "coreclr", "request": "launch", - "program": "${env:VINTAGE_STORY}/VintagestoryServer.exe", + "program": ".game/VintagestoryServer.exe", "linux": { - "program": "${env:VINTAGE_STORY}/VintagestoryServer" + "program": ".game/VintagestoryServer" }, "osx": { - "program": "${env:VINTAGE_STORY}/VintagestoryServer" + "program": ".game/VintagestoryServer" }, "preLaunchTask": "build", "args": [ "--tracelog", "--addModPath", - "${workspaceFolder}/ModTemplate/bin/Debug/Mods" + "${workspaceFolder}/AriasServerUtils/bin/Debug/Mods" ], "console": "internalConsole", "stopAtEntry": false diff --git a/.vscode/settings.json b/.vscode/settings.json index 3535556..fbe5f0e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dotnet.defaultSolution": "ModTemplate.sln", + "dotnet.defaultSolution": "AriasServerUtils.sln", "files.associations": { "server-*.txt": "log", "client-*.txt": "log" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3b7b591..8fe9dac 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,7 @@ "build", "-c", "Debug", - "${workspaceFolder}/ModTemplate/ModTemplate.csproj" + "${workspaceFolder}/AriasServerUtils/AriasServerUtils.csproj" ], "problemMatcher": "$msCompile" }, diff --git a/AriasServerUtils/ASUModSystem.cs b/AriasServerUtils/ASUModSystem.cs deleted file mode 100644 index 7e0589b..0000000 --- a/AriasServerUtils/ASUModSystem.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Vintagestory.API.Client; -using Vintagestory.API.Common; -using Vintagestory.API.Common.Entities; -using Vintagestory.API.Config; -using Vintagestory.API.Server; -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 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.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.controlserver).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") - .EndSubCommand() - .BeginSubCommand("help") - .RequiresPlayer() - .RequiresPrivilege(Privilege.chat) - .HandleWith(Events.HandleASU) - .WithDescription("Lists all Aria's Server Utils commands") - .EndSubCommand(); - } - - 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; - - - 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)); - } - - 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(); - } - - 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); - } - } -} diff --git a/AriasServerUtils/EventHandler.cs b/AriasServerUtils/EventHandler.cs index f378e84..667c1f1 100644 --- a/AriasServerUtils/EventHandler.cs +++ b/AriasServerUtils/EventHandler.cs @@ -3,8 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Vintagestory.API.Common; +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 { @@ -12,7 +16,32 @@ namespace AriasServerUtils { internal static TextCommandResult HandleASU(TextCommandCallingArgs args) { - return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:help", ServerUtilities.config.MaxHomes, ServerUtilities.config.AdminsBypassMaxHomes, string.Join(", ", new string[] { "setspawn", "spawn", "delspawn", "sethome", "home", "delhome", "homes", "restoreinv", "asu" }))); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:help", ServerUtilities.config.MaxHomes, ServerUtilities.config.AdminsBypassMaxHomes, ServerUtilities.config.MaxBackCache, string.Join(", ", new string[] { "setspawn", "spawn", "delspawn", "sethome", "home", "delhome", "homes", "restoreinv", "asu", "warp", "setwarp", "delwarp", "warps", "back", "rtp", "listcooldowns" }))); + } + + internal static TextCommandResult HandleBack(TextCommandCallingArgs args) + { + PlayerStorage ps = ServerUtilities.GetPlayerData(args.Caller.Player as IServerPlayer); + if (ps.ActiveCooldowns.ContainsKey(CooldownType.Back)) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:cmd-cooldown", "/back", TimeUtil.EncodeTimeNotation(ps.ActiveCooldowns.Get(CooldownType.Back) - TimeUtil.GetUnixEpochTimestamp()))); + } + + PlayerPosition pos = ServerUtilities.backCaches.ReadAndPopNewestPosition(args.Caller.Player.PlayerName); + if (pos == null) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:back-no")); + } + else + { + // Go back to old position + pos.Merge(args.Caller.Player.Entity); + ps.ActiveCooldowns.Add(CooldownType.Back, TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.Back)) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:back")); + } + } internal static TextCommandResult HandleClearSpawn(TextCommandCallingArgs args) @@ -23,6 +52,35 @@ namespace AriasServerUtils return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:rmspawn")); } + internal static TextCommandResult HandleRTP(TextCommandCallingArgs args) + { + if (args.Caller.Player is IServerPlayer isp) + { + int maxDistance = ServerUtilities.config.MaxRTPBlockDistance; + if (args[0] is int ix) + { + if (ix == -1) ix = maxDistance; + if (ix > maxDistance && !(ServerUtilities.config.AdminsBypassRTPMaxDistance && isp.HasPrivilege(Privilege.controlserver))) + { + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-capped", ix, maxDistance)); + } + else maxDistance = ix; + } + + PlayerStorage ps = ServerUtilities.GetPlayerData(isp); + if (ps.ActiveCooldowns.ContainsKey(CooldownType.RTP)) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:cmd-cooldown", "/rtp", TimeUtil.EncodeTimeNotation(ps.ActiveCooldowns.Get(CooldownType.RTP) - TimeUtil.GetUnixEpochTimestamp()))); + } + + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-search")); + + RTPFactory.TryRTP(isp, maxDistance: maxDistance); + } + return TextCommandResult.Success(); + } + internal static TextCommandResult HandleDelHome(TextCommandCallingArgs args) { string homeName = "default"; @@ -61,9 +119,18 @@ namespace AriasServerUtils if (args.Caller.Player is IServerPlayer isp) { PlayerStorage data = ServerUtilities.GetPlayerData(isp); + if (data.ActiveCooldowns.ContainsKey(CooldownType.Home)) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:cmd-cooldown", "/home", TimeUtil.EncodeTimeNotation(data.ActiveCooldowns.Get(CooldownType.Home) - TimeUtil.GetUnixEpochTimestamp()))); + } + + if (data.Homes.ContainsKey(homeName)) { - data.Homes[homeName].Location.Merge(isp.Entity); + Home home = data.Homes[homeName]; + ServerUtilities.NewBackCacheForPlayer(isp); + + home.Location.Merge(isp.Entity, unmount: !home.CanHaveMount); ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:home-tp")); } @@ -71,6 +138,11 @@ namespace AriasServerUtils { ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:home-no")); } + + + + data.ActiveCooldowns.Add(CooldownType.Home, TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.Home)) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); } return TextCommandResult.Success(); @@ -130,14 +202,20 @@ namespace AriasServerUtils if (args.Caller.Player is IServerPlayer isp) { + bool withMount = false; + if (args.Command.Name == "sethomemount") + { + // Check for the gears and pay here, or show error. + withMount = true; + } + bool bypass = isOp && ServerUtilities.config.AdminsBypassMaxHomes; var data = ServerUtilities.GetPlayerData(isp); if (bypass || data.Homes.Count < ServerUtilities.config.MaxHomes || data.Homes.ContainsKey(homeName)) { - data.Homes[homeName] = Home.MakeHome(args.Caller.Player.Entity, homeName); + data.Homes[homeName] = Home.MakeHome(args.Caller.Player.Entity, homeName, withMount: withMount); ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:home-set")); - } else { @@ -176,9 +254,23 @@ namespace AriasServerUtils { if (args.Caller.Player is IServerPlayer isp) { + + PlayerStorage data = ServerUtilities.GetPlayerData(isp); + if (data.ActiveCooldowns.ContainsKey(CooldownType.Spawn)) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:cmd-cooldown", "/spawn", TimeUtil.EncodeTimeNotation(data.ActiveCooldowns.Get(CooldownType.Spawn) - TimeUtil.GetUnixEpochTimestamp()))); + } + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:tp-spawn")); + ServerUtilities.NewBackCacheForPlayer(isp); ServerUtilities.config.Spawn.Merge(args.Caller.Player.Entity); + + + + + data.ActiveCooldowns.Add(CooldownType.Spawn, TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.Spawn)) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); } } @@ -188,17 +280,30 @@ namespace AriasServerUtils internal static TextCommandResult HandleUpdateASUBypass(TextCommandCallingArgs args) { - if (args[1] is bool bypass) + if (args[0] is bool bypass) { ServerUtilities.config.AdminsBypassMaxHomes = bypass; ServerUtilities.MarkDirty(); - return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig")); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", bypass)); } return TextCommandResult.Success(); } + internal static TextCommandResult HandleUpdateASUMaxBack(TextCommandCallingArgs args) + { + if (args[0] is int max) + { + ServerUtilities.config.MaxBackCache = max; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", max)); + } + + return TextCommandResult.Success(); + } + internal static TextCommandResult HandleUpdateASUMaxHomes(TextCommandCallingArgs args) { if (args[0] is int maxHomes) @@ -206,10 +311,382 @@ namespace AriasServerUtils ServerUtilities.config.MaxHomes = maxHomes; ServerUtilities.MarkDirty(); - return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig")); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", maxHomes)); } return TextCommandResult.Success(); } + + internal static TextCommandResult HandleUpdateASUMgrWarps(TextCommandCallingArgs args) + { + if (args[0] is bool mgr) + { + ServerUtilities.config.onlyAdminsCreateWarps = mgr; + + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", mgr)); + } + else ServerUtilities.config.onlyAdminsCreateWarps = true; + ServerUtilities.MarkDirty(); + + + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", true)); + } + + internal static TextCommandResult HandleUpdateASUPSP(TextCommandCallingArgs args) + { + if (args[0] is int psp) + { + ServerUtilities.config.PlayerSleepingPercentage = psp; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", psp)); + } + + return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleWarp(TextCommandCallingArgs args) + { + string name = "default"; + if (args.ArgCount > 0) name = args[0] as string ?? "default"; + + if (args.Caller.Player is IServerPlayer isp) + { + PlayerStorage data = ServerUtilities.GetPlayerData(isp); + if (data.ActiveCooldowns.ContainsKey(CooldownType.Warp)) + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:cmd-cooldown", "/warp", TimeUtil.EncodeTimeNotation(data.ActiveCooldowns.Get(CooldownType.Warp) - TimeUtil.GetUnixEpochTimestamp()))); + } + + + if (ServerUtilities.serverWarps.warps.ContainsKey(name)) + { + Warp warp = ServerUtilities.serverWarps.warps[name]; + ServerUtilities.NewBackCacheForPlayer(isp); + warp.Location.Merge(isp.Entity, unmount: !warp.CanHaveMount); + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-tp", name)); + + + data.ActiveCooldowns.Add(CooldownType.Warp, TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.Warp)) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); + } + else + { + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-fail", name)); + + } + + } + + return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleWarpDelete(TextCommandCallingArgs args) + { + string name = "default"; + if (args.ArgCount > 0) name = args[0] as string ?? "default"; + + if (args.Caller.Player is IServerPlayer isp) + { + if (isp.HasPrivilege(Privilege.controlserver) || !ServerUtilities.config.onlyAdminsCreateWarps) + { + ServerUtilities.serverWarps.warps.Remove(name); + ServerUtilities.MarkDirty(); + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-del")); + } + else + { + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-no")); + } + } + + return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleWarpList(TextCommandCallingArgs args) + { + List warps = new List(); + foreach (string id in ServerUtilities.serverWarps.warps.Keys) + { + warps.Add(id); + } + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:warp-list", warps.Count, string.Join(", ", warps))); + } + + internal static TextCommandResult HandleWarpUpdate(TextCommandCallingArgs args) + { + string name = "default"; + if (args.ArgCount > 0) name = args[0] as string ?? "default"; + + if (args.Caller.Player is IServerPlayer isp) + { + if (isp.HasPrivilege(Privilege.controlserver) || !ServerUtilities.config.onlyAdminsCreateWarps) + { + ServerUtilities.serverWarps.warps[name] = Warp.Create(isp); + ServerUtilities.MarkDirty(); + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-set", name)); + + } + else + { + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:warp-no")); + } + } + + return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleUpdateASURTPMax(TextCommandCallingArgs args) + { + if (args[0] is int maxDist) + { + ServerUtilities.config.MaxRTPBlockDistance = maxDist; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", maxDist)); + } + + return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleUpdateASUCDBack(TextCommandCallingArgs args) + { + if (args[0] is string CD) + { + ServerUtilities.config.Cooldowns[CooldownType.Back] = CD; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", CD)); + } + else + { + ServerUtilities.config.Cooldowns[CooldownType.Back] = "5s"; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + } + + internal static TextCommandResult HandleUpdateASUCDWarp(TextCommandCallingArgs args) + { + if (args[0] is string CD) + { + ServerUtilities.config.Cooldowns[CooldownType.Warp] = CD; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", CD)); + } + else + { + ServerUtilities.config.Cooldowns[CooldownType.Warp] = "10s"; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + } + + internal static TextCommandResult HandleUpdateASUCDHome(TextCommandCallingArgs args) + { + if (args[0] is string CD) + { + ServerUtilities.config.Cooldowns[CooldownType.Home] = CD; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", CD)); + } + else + { + ServerUtilities.config.Cooldowns[CooldownType.Home] = "5s"; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + } + + internal static TextCommandResult HandleUpdateASUCDSpawn(TextCommandCallingArgs args) + { + if (args[0] is string CD) + { + ServerUtilities.config.Cooldowns[CooldownType.Spawn] = CD; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", CD)); + } + else + { + ServerUtilities.config.Cooldowns[CooldownType.Spawn] = "5s"; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + } + + internal static TextCommandResult HandleUpdateASUCDRTP(TextCommandCallingArgs args) + { + if (args[0] is string CD) + { + ServerUtilities.config.Cooldowns[CooldownType.RTP] = CD; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", CD)); + } + else + { + ServerUtilities.config.Cooldowns[CooldownType.RTP] = "30s"; + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + } + + internal static TextCommandResult HandleUpdateASUCDReset(TextCommandCallingArgs args) + { + ServerUtilities.config.Cooldowns = ServerUtilities.config.GetDefaultCooldowns(); + ServerUtilities.MarkDirty(); + + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:config-value-reset")); + } + + internal static TextCommandResult HandleUpdateASUBypassCD(TextCommandCallingArgs args) + { + if (args[0] is bool bypass) + { + // Update the bypass + ServerUtilities.config.AdminsBypassCooldowns = bypass; + ServerUtilities.MarkDirty(); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", bypass)); + } + else return TextCommandResult.Success(); + } + + internal static TextCommandResult HandleUpdateASUBypassRTPMaxDist(TextCommandCallingArgs args) + { + if (args[0] is bool bypass) + { + // Update the flag + ServerUtilities.config.AdminsBypassRTPMaxDistance = bypass; + ServerUtilities.MarkDirty(); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", bypass)); + } + else return TextCommandResult.Success(); + } + + + internal static TextCommandResult HandleUpdateASUFarmlandDowngrade(TextCommandCallingArgs args) + { + if (args[0] is bool downgrade) + { + // Update the flag + ServerUtilities.config.EnableFarmlandDowngrade = downgrade; + ServerUtilities.MarkDirty(); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", downgrade)); + } + else + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:farmland-downgrade", ServerUtilities.config.EnableFarmlandDowngrade)); + } + } + + + internal static TextCommandResult HandleUpdateASUFarmlandDrop(TextCommandCallingArgs args) + { + if (args[0] is bool drop) + { + // Update the flag + ServerUtilities.config.EnableFarmlandDrop = drop; + ServerUtilities.MarkDirty(); + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:updatedconfig", drop)); + } + else + { + return TextCommandResult.Success(Lang.Get($"{ServerUtilities.MOD_ID}:farmland-drop", ServerUtilities.config.EnableFarmlandDrop)); + } + } + + internal static TextCommandResult HandleListCooldowns(TextCommandCallingArgs args) + { + string sReturn = "SERVER COOLDOWN SETTINGS\n"; + foreach (var cd in ServerUtilities.config.Cooldowns) + { + sReturn += $"{cd.Key}: {cd.Value}\n"; + } + if (args.Caller.Player is IServerPlayer isp) + { + sReturn += "\nYour active cooldowns:"; + foreach (var cd in ServerUtilities.GetPlayerData(isp).ActiveCooldowns) + { + long remain = cd.Value - TimeUtil.GetUnixEpochTimestamp(); + string sCDVal = TimeUtil.EncodeTimeNotation(remain); + sReturn += $"{cd.Key}: {sCDVal}\n"; + } + } + return TextCommandResult.Success(sReturn); + } + + internal static TextCommandResult HandleSleepyDebug(TextCommandCallingArgs args) + { + EntityBehaviorTiredness sleepy = args.Caller.Entity.GetBehavior(); + if (sleepy != null) + { + sleepy.Tiredness = 100; + } + return TextCommandResult.Success(); + } + + internal static void CheckBreakFarmland(IServerPlayer byPlayer, BlockSelection blockSel, ref float dropQuantityMultiplier, ref EnumHandling handling) + { + if (!ServerUtilities.config.EnableFarmlandDrop) + { + return; // Default behavior + } + + if (blockSel.Block is BlockFarmland farmland) + { + BlockEntityFarmland beFarmland = farmland.GetBlockEntity(blockSel.Position); + string farmlandType = blockSel.Block.LastCodePart(); + + if (ServerUtilities.config.EnableFarmlandDowngrade) + { + + switch (farmlandType) + { + case "verylow": + { // barren + break; // Can't downgrade further + } + case "low": + { + farmlandType = "verylow"; + break; + } + case "medium": + { + farmlandType = "low"; + break; + } + case "compost": + { // high + farmlandType = "medium"; + break; + } + case "high": + { // Terra preta + farmlandType = "compost"; + break; + } + } + } + + byPlayer.Entity.World.SpawnItemEntity(new ItemStack(byPlayer.Entity.World.GetBlock(new AssetLocation($"soil-{farmlandType}-none"))), blockSel.Position.ToVec3d().Add(0.5, 0.5, 0.5)); + } + } } } \ No newline at end of file diff --git a/AriasServerUtils/Globals.cs b/AriasServerUtils/Globals.cs new file mode 100644 index 0000000..5da42ab --- /dev/null +++ b/AriasServerUtils/Globals.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ConstrainedExecution; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace AriasServerUtils +{ + public enum ModConfigType + { + Global, + World + } + + [Serializable] + public class ASUModConfig + { + private readonly static Dictionary m_defaultCD = new Dictionary{ + + { CooldownType.Home, "5s" }, + { CooldownType.Warp, "10s" }, + { CooldownType.Spawn, "5s" }, + { CooldownType.RTP, "30s" }, + { CooldownType.Back, "5s" } + }; + private static readonly int CURRENT_VERSION = 6; + + + public int Version { get; set; } = 0; + public int MaxHomes { get; set; } = 20; + public bool AdminsBypassMaxHomes { get; set; } = true; + public bool onlyAdminsCreateWarps { get; set; } = true; + public bool AdminsBypassCooldowns { get; set; } = true; + public bool AdminsBypassRTPMaxDistance { get; set; } = false; + public int MaxBackCache { get; set; } = 10; + public int PlayerSleepingPercentage { get; set; } = 50; + public int MaxRTPBlockDistance { get; set; } = 50000; + public Dictionary Cooldowns { get; set; } = new Dictionary(); + + + /// + /// If true, attempts to downgrade the soil quality when breaking farmland. + /// + public bool EnableFarmlandDowngrade { get; set; } = false; + + /// + /// If true, farmland will drop as soil when broken. + /// + public bool EnableFarmlandDrop { get; set; } = true; + + public Dictionary GetDefaultCooldowns() + { + return m_defaultCD; + } + + /// + /// Performs some checks against known possible invalid values and sets them to sane values. + /// + public void SanityCheck() + { + foreach (var cd in GetDefaultCooldowns()) + { + if (!Cooldowns.ContainsKey(cd.Key)) + { + Cooldowns.Add(cd.Key, cd.Value); + ServerUtilities.MarkDirty(); + } + } + + if (Version < CURRENT_VERSION) + { + Version = CURRENT_VERSION; + ServerUtilities.MarkDirty(); // This is here to ensure that the config gets saved when there is a update. Whenever a new field is added to config, the CURRENT_VERSION should get bumped so that this SanityCheck can properly work + } + } + + + public PlayerPosition Spawn { get; set; } + } + + [Serializable] + public enum CooldownType + { + Home, // Default: 5s + Warp, // Default 10s + Spawn, // Default 5s + RTP, // Default 30s + Back // Default 5s + } + + [Serializable] + public class Warp + { + public PlayerPosition Location; + public string CreatedBy; + public DateTime CreatedAt; + public bool CanHaveMount = false; + + + public static Warp Create(IServerPlayer player, bool withMount = false) + { + Warp warp = new Warp(); + warp.Location = PlayerPosition.from(player.Entity); + warp.CreatedBy = player.PlayerName; + warp.CreatedAt = DateTime.Now; + warp.CanHaveMount = withMount; + + return warp; + } + } + + [Serializable] + public class Warps + { + public Dictionary warps = new Dictionary(); + } + + /// + /// This contains per command costs, as well as helper functions that can refund a player, or check if the player has the required balance, or payment + /// + /// + [Serializable] + public class Costs + { + public Dictionary costs = new Dictionary(); + + /// + /// Checks if the player has the balance required to use the command + /// + /// The command the player is trying to use + /// The player + /// True if the player has the required balance + public bool PlayerHasBalance(string cmd, EntityPlayer player) + { + int gears = 0; + int required = 0; + if (costs.ContainsKey(cmd)) + { + // Get the cost for that command + required = costs[cmd]; + } + + // Scan the player inventory, check gears + + + + + if (gears >= required) return true; + return false; + } + } + + + public class BackCaches + { + private const int MaxCacheSize = 10; + private readonly Dictionary> playerCache = new(); + + public void AddPosition(string playerName, PlayerPosition position) + { + if (!playerCache.TryGetValue(playerName, out var positions)) + { + positions = new LinkedList(); + playerCache[playerName] = positions; + } + + if (positions.Count >= MaxCacheSize) + { + positions.RemoveFirst(); // Remove the oldest position + } + + positions.AddLast(position.Clone()); // Add the new position + } + + public PlayerPosition ReadAndPopNewestPosition(string playerName) + { + if (playerCache.TryGetValue(playerName, out var positions) && positions.Count > 0) + { + var newestPosition = positions.Last.Value; + positions.RemoveLast(); // Remove the newest position + return newestPosition; + } + + return null; // Return null if no positions are available + } + } +} \ No newline at end of file diff --git a/AriasServerUtils/ModConfig.cs b/AriasServerUtils/ModConfig.cs deleted file mode 100644 index d16e04e..0000000 --- a/AriasServerUtils/ModConfig.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace AriasServerUtils -{ - public enum ModConfigType - { - Global, - World - } - - [Serializable] - public class ASUModConfig - { - public int MaxHomes { get; set; } = 20; - public bool AdminsBypassMaxHomes { get; set; } = true; - - - public PlayerPosition Spawn { get; set; } - } -} \ No newline at end of file diff --git a/AriasServerUtils/ModSystems/ASUClient.cs b/AriasServerUtils/ModSystems/ASUClient.cs new file mode 100644 index 0000000..2966dc3 --- /dev/null +++ b/AriasServerUtils/ModSystems/ASUClient.cs @@ -0,0 +1,19 @@ +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; + +public class ASUModClient : ModSystem +{ + public static ICoreClientAPI CAPI; + bool accel = false; + + public override bool ShouldLoad(EnumAppSide forSide) + { + return forSide == EnumAppSide.Client; + } + + public override void StartClientSide(ICoreClientAPI api) + { + CAPI = api; + } +} \ No newline at end of file diff --git a/AriasServerUtils/ModSystems/ASUServer.cs b/AriasServerUtils/ModSystems/ASUServer.cs new file mode 100644 index 0000000..1eed677 --- /dev/null +++ b/AriasServerUtils/ModSystems/ASUServer.cs @@ -0,0 +1,602 @@ +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.Datastructures; +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(); + internal 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 Random rng = new Random((int)TimeUtil.GetUnixEpochTimestamp()); + internal bool isFirstStart = true; + + + List SleepingPlayers { get; set; } = new(); + float OriginalSpeed { get; set; } = 0.0f; + public double Hours { get; private set; } = 0.0; + bool Sleeping { get; set; } = false; + public IServerNetworkChannel ServerNetworkChannel { get; private set; } + + /// + /// 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) + { + } + + + public override bool ShouldLoad(EnumAppSide side) + { + return side == EnumAppSide.Server; + } + + + + // 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.PlayerDeath += OnPlayerDeath; + api.Event.PlayerJoin += OnPlayerJoin; + api.Event.PlayerDisconnect += OnPlayerDC; + api.Event.ChunkColumnLoaded += RTPFactory.ChunkLoaded; + api.Event.BreakBlock += Events.CheckBreakFarmland; + //api.Event.PlayerLeave += OnPlayerDC; + + + + api.ChatCommands.Create("setspawn").RequiresPrivilege(Privilege.controlserver).HandleWith(Events.HandleSetSpawn).WithAlias("ss"); + api.ChatCommands.Create("spawn").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleSpawn).WithAlias("s"); + api.ChatCommands.Create("delspawn").RequiresPrivilege(Privilege.controlserver).HandleWith(Events.HandleClearSpawn).WithAlias("ds"); + + + //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).WithAlias("sh"); + api.ChatCommands.Create("home").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Teleports you to home").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleGoHome).WithAlias("h"); + api.ChatCommands.Create("delhome").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Deletes a home entry").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleDelHome).WithAlias("dh"); + api.ChatCommands.Create("homes").RequiresPlayer().WithDescription("Lists your homes").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleListHomes).WithAlias("lh"); + api.ChatCommands.Create("sethomemount").RequiresPlayer().WithArgs(parsers.OptionalWord("name")).WithDescription("Create a home with the ability to take a mount with you. Costs [undefined] gears.").RequiresPrivilege(Privilege.chat).HandleWith(Events.HandleSetHome).WithAlias("shm", "shb"); + + 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("farmlandDowngrade") + .RequiresPrivilege(Privilege.controlserver) + .WithArgs(parsers.OptionalBool("downgrade")) + .WithDescription("Enables or disables farmland downgrade when breaking farmland") + .HandleWith(Events.HandleUpdateASUFarmlandDowngrade) + .EndSubCommand() + .BeginSubCommand("farmlandDrop") + .RequiresPrivilege(Privilege.controlserver) + .WithArgs(parsers.OptionalBool("drop")) + .WithDescription("Enables or disables dropping soil when breaking farmland") + .HandleWith(Events.HandleUpdateASUFarmlandDrop) + .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() + .BeginSubCommand("test") + .RequiresPlayer() + .RequiresPrivilege(Privilege.controlserver) + .BeginSubCommand("sleep") + .RequiresPlayer() + .RequiresPrivilege(Privilege.controlserver) + .HandleWith(TestSleep) + .EndSubCommand() + .BeginSubCommand("sleepy") + .RequiresPlayer() + .RequiresPrivilege(Privilege.controlserver) + .HandleWith(Events.HandleSleepyDebug) + .EndSubCommand() + .EndSubCommand(); + + api.ChatCommands.Create("setwarp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Creates a new server warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarpUpdate).WithAlias("sw"); + api.ChatCommands.Create("warp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Warp to the specified server warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarp).WithAlias("w"); + api.ChatCommands.Create("delwarp").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Deletes the specified warp").WithArgs(parsers.OptionalWord("name")).HandleWith(Events.HandleWarpDelete).WithAlias("dw"); + api.ChatCommands.Create("warps").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Lists all server warps").HandleWith(Events.HandleWarpList).WithAlias("lw"); + + api.ChatCommands.Create("back").RequiresPlayer().RequiresPrivilege(Privilege.chat).WithDescription("Returns you to the last location you were at").HandleWith(Events.HandleBack).WithAlias("b"); + + 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).WithAlias("lc"); + } + + private TextCommandResult TestSleep(TextCommandCallingArgs args) + { + // Initiate the sleep process + // Basically run all the same commands as we would on a player in bed + Events.HandleSleepyDebug(args); + + OriginalSpeed = API.World.Calendar.CalendarSpeedMul; + if (args.Caller.Player is IServerPlayer isp) + { + Hours = API.World.Calendar.TotalHours; + SleepingPlayers.Add(isp.Entity); + API.Logger.Notification($"Game Hours: {API.World.Calendar.TotalHours}, Difference: {API.World.Calendar.TotalHours - Hours}"); + EntityAgent Agent = isp.Entity; + + EntityBehaviorTiredness ebt = Agent.GetBehavior(); + + ebt.IsSleeping = true; + ebt.Tiredness = 100; + Sleeping = true; + + API.World.Calendar.SetTimeSpeedModifier("asu_psp", 1000); + } + + return TextCommandResult.Success($"Test initiated, original calendar multiplier: '{OriginalSpeed}'"); + } + + private TextCommandResult TestCalendarSpeed(TextCommandCallingArgs args) + { + if (args.Caller.Player is IServerPlayer isp) + { + EntityAgent agent = isp.Entity; + EntityBehaviorTiredness ebt = agent.GetBehavior("tiredness") as EntityBehaviorTiredness; + + SendMessageTo(isp, $"- Current calendar speed: {API.World.Calendar.CalendarSpeedMul}"); + SendMessageTo(isp, $"- Total Hours: {API.World.Calendar.TotalHours}"); + SendMessageTo(isp, $"- Tiredness: {ebt.Tiredness}"); + if (OriginalSpeed == 0) + { + // Apply multiplier + OriginalSpeed = 0.5f; + ebt.IsSleeping = true; + + API.World.Calendar.SetTimeSpeedModifier("asu_psp", 1000); + + SendMessageTo(isp, "Applied calendar speed multiplier"); + } + else + { + // Unapply multiplier + OriginalSpeed = 0; + ebt.IsSleeping = false; + + API.World.Calendar.RemoveTimeSpeedModifier("asu_psp"); + + SendMessageTo(isp, "Restored default calendar speed"); + } + } + + return TextCommandResult.Success(); + } + + private void OnCheckSleepingPlayers() + { + if (API.Side == EnumAppSide.Client) return; // This must only ever be called on the server! + if (isFirstStart) + { + API.World.Calendar.RemoveTimeSpeedModifier("asu_psp"); + isFirstStart = false; + } + + if (Sleeping) + { + //API.Logger.Notification($"Game Hours: {API.World.Calendar.TotalHours}, Difference: {API.World.Calendar.TotalHours - Hours}"); + if (API.World.Calendar.TotalHours - Hours >= 6) + { + Sleeping = false; + foreach (var player in SleepingPlayers) + { + EntityBehaviorTiredness ebt = player.GetBehavior(); + ebt.IsSleeping = false; + ebt.Tiredness = 0; + } + + SleepingPlayers.Clear(); + + API.World.Calendar.RemoveTimeSpeedModifier("asu_psp"); + + //API.Logger.Notification("Stopping PSP Time Acceleration"); + } + return; + } + if (config.PlayerSleepingPercentage == 100) return; // Normal behavior + // Iterate over all players, get their entity, check if mounted on a bed. + // If mounted on a bed, check tiredness + int TotalOnline = API.World.AllOnlinePlayers.Length; + if (TotalOnline == 0) return; // No one on, just abort the checks. + int TotalInBed = 0; + + List BEBs = new(); + + foreach (var player in API.World.AllOnlinePlayers) + { + EntityAgent ePlay = player.Entity; + if (ePlay.MountedOn is BlockEntityBed beb) + { + BEBs.Add(beb); + TotalInBed++; + //API.Logger.Notification($"Bed found for player {player.PlayerName}"); + } + if (ePlay.MountedOn == null) + { + //API.Logger.Notification($"No bed found for player {player.PlayerName}"); + + if (SleepingPlayers.Contains(ePlay)) + { + EntityBehaviorTiredness ebt = ePlay.GetBehavior(); + if (ebt != null) + ebt.IsSleeping = false; + } + } + } + + if (Sleeping) return; // Abort + + SleepingPlayers.Clear(); + + int Percentage = TotalInBed * 100 / TotalOnline; + + //API.Logger.Notification($"Percentage of players in bed: {Percentage}, Required percentage: {config.PlayerSleepingPercentage}"); + + if (Percentage >= config.PlayerSleepingPercentage) + { + + API.World.Calendar.SetTimeSpeedModifier("asu_psp", 1000); + + // Call the API to make sleep happen + foreach (var bed in BEBs) + { + if (bed.MountedBy != null) SleepingPlayers.Add(bed.MountedBy); + + // Start sleep + EntityBehaviorTiredness EBT = bed.MountedBy.GetBehavior(); + + EBT.IsSleeping = true; + + //API.Logger.Notification("Starting PSP Time Acceleration"); + + bed.MountedBy.TryUnmount(); // Stand up. We cant trigger the real sleep phase, but all code for starting time accel has been executed. + } + + // Get current calendar speed + Hours = API.World.Calendar.TotalHours; + Sleeping = true; + } + } + + 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 = RustyGearUtils.GetAllItems(player); + NewBackCacheForPlayer(player); + + + 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(); + + + + API.Event.Timer(OnCheckModDirty, 20); + API.Event.Timer(OnCheckPlayerCooldowns, 1); + API.Event.Timer(OnCheckSleepingPlayers, 5); + API.Event.Timer(RTPFactory.HandleRTPChecking, 1); + } + + 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); + } + } +} diff --git a/AriasServerUtils/PlayerData.cs b/AriasServerUtils/PlayerData.cs index c31560d..6be84eb 100644 --- a/AriasServerUtils/PlayerData.cs +++ b/AriasServerUtils/PlayerData.cs @@ -31,8 +31,15 @@ namespace AriasServerUtils } - public void Merge(Entity entity) + public void Merge(Entity entity, bool unmount = true) { + if (entity is EntityPlayer player) + { + if (unmount && player.MountedOn.Entity != null) + { + player.TryUnmount(); + } + } entity.TeleportTo(new BlockPos(X, Y, Z, Dimension)); entity.Pos.SetYaw(Yaw); entity.Pos.Pitch = Pitch; @@ -78,6 +85,7 @@ namespace AriasServerUtils public class PlayerStorage { public Dictionary Homes = new Dictionary(); + public Dictionary ActiveCooldowns = new(); } /// @@ -87,11 +95,13 @@ namespace AriasServerUtils public class Home { public PlayerPosition Location { get; set; } + public bool CanHaveMount = false; - public static Home MakeHome(Entity player, string homeName) + public static Home MakeHome(Entity player, string homeName, bool withMount = false) { Home home = new Home(); home.Location = PlayerPosition.from(player); + home.CanHaveMount = withMount; return home; diff --git a/AriasServerUtils/RTPFactory.cs b/AriasServerUtils/RTPFactory.cs new file mode 100644 index 0000000..d2544a2 --- /dev/null +++ b/AriasServerUtils/RTPFactory.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Numerics; +using AriasServerUtils; +using Microsoft.Win32.SafeHandles; +using Vintagestory.API.Common; +using Vintagestory.API.Common.Entities; +using Vintagestory.API.Config; +using Vintagestory.API.MathTools; +using Vintagestory.API.Server; +using Vintagestory.GameContent; + +namespace AriasServerUtils; +public class RTPFactory +{ + + private static List RTPCache = new(); + private static List ChunkChecks = new(); + + /* + if (pPos == null) + { + ps.ActiveCooldowns.Add(CooldownType.RTP, (TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.RTP)) / 2) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-fail")); + return TextCommandResult.Success(); + } + Vec2i origin = new((int)isp.Entity.Pos.X, (int)isp.Entity.Pos.Z); + Vec2i npos = new(pPos.X, pPos.Z); + + float distance = RTPFactory.GetDistance(origin, npos); + + pPos.Merge(isp.Entity); + + + ps.ActiveCooldowns.Add(CooldownType.RTP, TimeUtil.DecodeTimeNotation(ServerUtilities.config.Cooldowns.Get(CooldownType.RTP)) + TimeUtil.GetUnixEpochTimestamp()); + ServerUtilities.MarkDirty(); + + ServerUtilities.SendMessageTo(isp, Lang.Get($"{ServerUtilities.MOD_ID}:rtp", distance)); + + */ + + /// + /// This function searches for a safe position + /// + /// The player to be teleported + /// A safe position if able to be found + public static PlayerPosition GetSafePosition(RTPData data, RTPPosition position) + { + PlayerPosition PPos = data.LastPosition.GetPlayerPosition(); + BlockPos check = data.LastPosition.GetBlockPos(); + var BA = ServerUtilities.API.World.BlockAccessor; + check.Y = 1; + int height = BA.GetTerrainMapheightAt(check); + if (height >= 0 && height <= 20) return null; + check.Y = height + 1; + PPos.Y = height + 1; + return PPos; + } + + /// + /// This function will schedule a task to perform an RTP. + /// + /// Player to be teleported + /// Max distance +/- the current position. + public static void TryRTP(IServerPlayer isp, int maxDistance) + { + var data = new RTPData(isp, maxDistance, 10, PlayerPosition.from(isp.Entity)); + RTPCache.Add(data); + } + + public static float GetDistance(Vec2i pos1, Vec2i pos2) + { + return MathF.Sqrt(MathF.Pow(pos2.X - pos1.X, 2) + MathF.Pow(pos2.Y - pos1.Y, 2)); + } + + /// + /// Fired automatically by the internal game timer. This function will handle checking for a RTP Location + /// + /// NOTE: This function will only cause the chunks in question to be force loaded long enough to check their blocks and make sure it is safe to land there. + /// + internal static void HandleRTPChecking() + { + foreach (var chunk in ChunkChecks) + { + chunk.Wait--; + if (chunk.Wait <= 0) + { + ChunkChecks.Remove(chunk); + break; + } + } + // We want to now loop over the entire cache list. + // We'll then generate a position to check + // We'll also then check the loaded status of the chunk + foreach (var rtp in RTPCache) + { + // Check for any chunks still being checked. + int num = ChunkChecks.Select(x => x.rtp.player.PlayerUID == rtp.player.PlayerUID).Count(); + if (num > 0) continue; + + + if (rtp.NumTriesRemaining <= 0) + { + // Send failure message to the player + ServerUtilities.SendMessageTo(rtp.player, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-fail")); + // This check needs to be removed from the queue + RTPCache.Remove(rtp); + HandleRTPChecking(); + return; // We modified the list, so abort the loop. + } + + // Get the world handle, then get chunk size + var worldManager = ServerUtilities.API.WorldManager; + var chunkSize = worldManager.ChunkSize; + var worldSize = new Vec3i(worldManager.MapSizeX, worldManager.MapSizeY, worldManager.MapSizeZ); + + // Generate a new position + var position = rtp.MakeNewPosition(worldSize); + ServerUtilities.SendMessageTo(rtp.player, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-progress", rtp.NumTriesRemaining)); + + // Generate a chunk load check object. + RTPChunk chunk = new RTPChunk(); + chunk.ChunkX = position.x / chunkSize; + chunk.ChunkZ = position.z / chunkSize; + chunk.dim = position.dimension; + chunk.rtp = rtp; + + // Log the request + ChunkChecks.Add(chunk); + + // Check if the chunk's coordinates are loaded + var cxCheck = ServerUtilities.API.World.IsFullyLoadedChunk(position.GetBlockPos()); + if (cxCheck) + { + // Process the check here, no need to load + var posX = GetSafePosition(rtp, position); + if (posX == null) + { + // Let this get checked again + //ServerUtilities.API.Logger.Notification("position null: resume search"); + } + else + { + + // Found! Perform teleport and remove the RTP Check + RTPCache.Remove(rtp); + posX.Merge(rtp.player.Entity); + + ServerUtilities.SendMessageTo(rtp.player, Lang.Get($"{ServerUtilities.MOD_ID}:rtp", GetDistance(new Vec2i(rtp.StartPosition.X, rtp.StartPosition.Z), new Vec2i(posX.X, posX.Z)))); + + //ServerUtilities.API.Logger.Notification("position found"); + } + + ChunkChecks.Remove(chunk); + } + else + { + // Load the chunk + worldManager.LoadChunkColumnPriority(chunk.ChunkX, chunk.ChunkZ); + } + } + } + + internal static void ChunkLoaded(Vec2i chunkCoord, IWorldChunk[] chunks) + { + // Check if this is even a valid check + var num = ChunkChecks.Where(x => x.ChunkX == chunkCoord.X && x.ChunkZ == chunkCoord.Y).Count(); + if (num == 0) return; + + + // Get the chunk from the stack + var chunk = ChunkChecks.Where(x => x.ChunkX == chunkCoord.X && x.ChunkZ == chunkCoord.Y).First(); + + // Attempt to find a landing point. + var data = chunk.rtp; + var pos = GetSafePosition(data, data.LastPosition); + + if (pos == null) + { + // Let this get checked again + //ServerUtilities.API.Logger.Notification("position null: resume search"); + } + else + { + // Found! Perform teleport and remove the RTP Check + RTPCache.Remove(data); + pos.Merge(data.player.Entity); + + ServerUtilities.SendMessageTo(data.player, Lang.Get($"{ServerUtilities.MOD_ID}:rtp", GetDistance(new Vec2i(data.StartPosition.X, data.StartPosition.Z), new Vec2i(pos.X, pos.Z)))); + + //ServerUtilities.API.Logger.Notification("position found"); + } + + // Remove this check + ChunkChecks.Remove(chunk); + } +} + +public class RTPChunk +{ + public int ChunkX; + public int ChunkZ; + public int dim; + public RTPData rtp; + public int Wait = 5; +} + +public class RTPData +{ + public IServerPlayer player; + public int NumTriesRemaining; + public int MaxDistance; + public PlayerPosition StartPosition; + public RTPPosition LastPosition; + + public RTPData(IServerPlayer isp, int maxDistance, int tries, PlayerPosition playerPosition) + { + MaxDistance = maxDistance; + player = isp; + NumTriesRemaining = tries; + StartPosition = playerPosition; + } + + public RTPPosition MakeNewPosition(Vec3i mapSize) + { + NumTriesRemaining--; + LastPosition = new RTPPosition((int)player.Entity.Pos.X, (int)player.Entity.Pos.Z, MaxDistance, player.Entity.Pos.Dimension, mapSize, player); + + return LastPosition; + } +} + +public class RTPPosition +{ + public int x; + public int y; + public int z; + + public int dimension; + + public RTPPosition(int x, int z, int maxDist, int dim, Vec3i mapSize, IServerPlayer player) + { + + int worldx = mapSize.X / 2; + int worldz = mapSize.Z / 2; + + if (maxDist > worldx) + { + ServerUtilities.SendMessageTo(player, Lang.Get($"{ServerUtilities.MOD_ID}:rtp-capped", maxDist, (worldx))); + maxDist = worldx / 2; + } + + int minX = x - maxDist; + int maxX = x + maxDist; + int minZ = z - maxDist; + int maxZ = z + maxDist; + + this.x = ServerUtilities.rng.Next(minX, maxX); + this.y = 1; + this.z = ServerUtilities.rng.Next(minZ, maxZ); + + + + this.dimension = dim; + } + + public PlayerPosition GetPlayerPosition() + { + return new PlayerPosition + { + X = x, + Y = y, + Dimension = dimension, + Z = z + }; + } + + public BlockPos GetBlockPos() + { + return new BlockPos(new Vec3i(x, y, z), dimension); + } +} \ No newline at end of file diff --git a/AriasServerUtils/RustyGearUtils.cs b/AriasServerUtils/RustyGearUtils.cs new file mode 100644 index 0000000..229a7be --- /dev/null +++ b/AriasServerUtils/RustyGearUtils.cs @@ -0,0 +1,131 @@ +using System; +using AriasServerUtils; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Datastructures; +using Vintagestory.API.Server; +using Vintagestory.API.Util; + +public static class RustyGearUtils +{ + + + internal static string[] saveInvTypes = new string[] { + GlobalConstants.hotBarInvClassName, + GlobalConstants.backpackInvClassName, + GlobalConstants.craftingInvClassName, + GlobalConstants.mousecursorInvClassName, + GlobalConstants.characterInvClassName + }; + + // Replace with the correct code if it's different + private const string RustyGearCode = "currency-rustygear"; + + /// + /// Counts the total number of rusty gears in the player's inventory. + /// + public static int CountRustyGears(IServerPlayer player) + { + int total = 0; + + player.Entity.WalkInventory((slot) => + { + if (slot is ItemSlotCreative || !(slot.Inventory is InventoryBasePlayer)) return true; + + total += CurrencyValuePerItem(slot) * slot.StackSize; + + return true; + }); + + return total; + } + + private static int CurrencyValuePerItem(ItemSlot slot) + { + JsonObject obj = slot.Itemstack?.Collectible?.Attributes?["currency"]; + if (obj != null && obj.Exists) + { + JsonObject v = obj["value"]; + return v.Exists ? v.AsInt(0) : 0; + } + return 0; + } + /// + /// Attempts to subtract a specific number of rusty gears from the player's inventory. + /// + /// The player. + /// How many gears to remove. + /// True if the full amount was successfully removed, false if not enough gears. + public static bool SubtractRustyGears(IServerPlayer player, int amount) + { + // Check if the player has enough rusty gears + int currentAmount = CountRustyGears(player); + if (currentAmount < amount) return false; + + // Subtract the specified amount of rusty gears from the player's inventory + player.Entity.WalkInventory((slot) => + { + if (slot is ItemSlotCreative || !(slot.Inventory is InventoryBasePlayer)) return true; + + int value = CurrencyValuePerItem(slot) * slot.StackSize; + if (value > 0 && slot.StackSize > 0) + { + // Calculate the amount of rusty gears to remove from this slot + int amountToRemove = Math.Min(value, amount); + + // If the slot size is less than or equal to the amount to remove, set the slot's itemstack to null + if (slot.StackSize <= amountToRemove) slot.Itemstack = null; + else + { + // Otherwise, subtract the amount to remove from the slot size and decrement the total amount + slot.Itemstack.StackSize -= amountToRemove; + amount -= amountToRemove; + } + } + + // If the total amount has been removed, return true + if (amount <= 0) return true; // we're done + return true; + }); + + // If the player's inventory still contains rusty gears, they don't have enough to remove + return false; + } + + public static PlayerInventory GetAllItems(IServerPlayer player) + { + PlayerInventory inv = new PlayerInventory(); + + var invMgr = player.InventoryManager; + + 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()); + + } + } + + return inv; + } +} diff --git a/AriasServerUtils/TimeUtil.cs b/AriasServerUtils/TimeUtil.cs new file mode 100644 index 0000000..aca557e --- /dev/null +++ b/AriasServerUtils/TimeUtil.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace AriasServerUtils +{ + public static class TimeUtil + { + private static readonly Dictionary TimeUnits = new() + { + { 's', 1 }, // seconds + { 'm', 60 }, // minutes + { 'h', 3600 }, // hours + { 'd', 86400 }, // days + { 'w', 604800 }, // weeks + { 'M', 2592000 }, // months (approx. 30 days) + { 'y', 31536000 } // years (365 days) + }; + + public static long DecodeTimeNotation(string input) + { + if (string.IsNullOrWhiteSpace(input)) return 0; + + long totalSeconds = 0; + int number = 0; + + foreach (char c in input) + { + if (char.IsDigit(c)) + { + number = number * 10 + (c - '0'); + } + else if (TimeUnits.TryGetValue(c, out int unitSeconds)) + { + totalSeconds += number * unitSeconds; + number = 0; + } + else + { + throw new FormatException($"Invalid time unit: {c}"); + } + } + + return totalSeconds; + } + + public static string EncodeTimeNotation(long seconds) + { + if (seconds <= 0) return "0s"; + + var result = new List(); + + foreach (var (unit, unitSeconds) in TimeUnits.OrderByDescending(u => u.Value)) + { + long value = seconds / unitSeconds; + if (value > 0) + { + result.Add($"{value}{unit}"); + seconds %= unitSeconds; + } + } + + return string.Join("", result); + } + + public static long GetUnixEpochTimestamp() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + } +} diff --git a/AriasServerUtils/assets/ariasserverutils/lang/en.json b/AriasServerUtils/assets/ariasserverutils/lang/en.json index 0dc690a..7f983a4 100644 --- a/AriasServerUtils/assets/ariasserverutils/lang/en.json +++ b/AriasServerUtils/assets/ariasserverutils/lang/en.json @@ -18,7 +18,33 @@ "home-del": "Home deleted", "home-list": "You have [{0}] home(s)\n\n{1}", - "help": "All Aria's Server Utilities Commands: \n\nMax Homes: {0}; Admins can bypass max homes: {1}\n{2}", + "help": "All Aria's Server Utilities Commands: \n\nMax Homes: {0}; \nAdmins can bypass max homes: {1}\nMax back positions: {2}\n\n{3}", - "updatedconfig": "[ASU] server config updated" + "updatedconfig": "[ASU] server config updated with the new value: {0}", + "config-value-reset": "[ASU] server config value reset to default", + + "farmland-downgrade": "The current farmland downgrade setting is {0}", + "farmland-drop": "The current farmland drop setting is {0}", + + "warp-tp": "Teleported to warp [{0}]", + "warp-set": "Warp [{0}] created!", + "warp-no": "You lack permissions to manage a warp", + "warp-fail": "Warp [{0}] does not exist", + "warp-del": "Warp [{0}] deleted", + "warp-list": "There are [{0}] total warps\n\n{1}", + + "back-no": "There's no position to go back to", + "back": "You've been taken back to your last position", + + "rtp-search": "Searching for a random position...", + "rtp-progress": "Still searching for a random position... [{0}] tries remaining...", + "rtp": "You have been teleported [{0}] blocks away!", + "rtp-found": "Found a valid landing position after {0} tries.", + "rtp-fail": "Giving up on RTP search. No valid position could be found. Try again later", + "rtp-capped": "The distance you tried to go [{0}] is greater than the maximum allowable by the server [{1}]", + + "cmd-cooldown": "[{0}] is currently on cooldown. You can use this command again in [{1}]", + + "psp": "[ASU] PSP Starting... you do not need to stay in bed", + "psp-ending": "[ASU] PSP Complete" } diff --git a/AriasServerUtils/modinfo.json b/AriasServerUtils/modinfo.json index 5247ac9..47b0a7a 100644 --- a/AriasServerUtils/modinfo.json +++ b/AriasServerUtils/modinfo.json @@ -3,8 +3,8 @@ "modid": "ariasserverutils", "name": "Aria's Server Utilities", "authors": ["zontreck"], - "description": "A collection of server utilities\n\nBuild Date: 01-18-2025 @ 03:18 PM", - "version": "1.0.1", + "description": "A collection of server utilities\n\nBuild Date: 06-05-2025 @ 4:44 PM MST", + "version": "1.1.0-dev.3", "dependencies": { "game": "" }