VSMod_AriasServerUtils/AriasServerUtils/ModSystems/ASUServer.cs
2025-05-07 12:15:23 -07:00

641 lines
28 KiB
C#

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<string, PlayerInventory> backupInventory = new Dictionary<string, PlayerInventory>();
internal static Dictionary<string, PlayerStorage> mPlayerData = new Dictionary<string, PlayerStorage>();
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;
internal static string[] saveInvTypes = new string[] {
GlobalConstants.hotBarInvClassName,
GlobalConstants.backpackInvClassName,
GlobalConstants.craftingInvClassName,
GlobalConstants.mousecursorInvClassName,
GlobalConstants.characterInvClassName
};
List<EntityAgent> 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; }
/// <summary>
/// Method to register all mod blocks
/// </summary>
/// <param name="api"></param>
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);
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("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);
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 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<EntityBehaviorTiredness>();
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<EntityBehaviorTiredness>();
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<BlockEntityBed> 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<EntityBehaviorTiredness>();
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<EntityBehaviorTiredness>();
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<CooldownType> 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<PlayerStorage>(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<PlayerStorage>(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<ASUModConfig>(config, GetConfigurationFile("global", ModConfigType.Global));
API.StoreModConfig<Warps>(serverWarps, GetConfigurationFile("warps", ModConfigType.Global));
}
private void OnGameReady()
{
// Mod Setup Info //
// -> Step 1. Load Mod Global Config <-
config = API.LoadModConfig<ASUModConfig>(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<Warps>(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";
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
}