diff --git a/CHANGELOG.md b/CHANGELOG.md index f6aa9c1..028a5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,4 +47,8 @@ * Fixed an issue related to retrieving Inventories from the NetworkIdMapping causing the mod to crash/not work correctly ### v1.0.0 -* Initial release \ No newline at end of file +* Initial release + +## Generic Chat Commands +### v1.0.0 +* Initial release diff --git a/GenericChatCommands/Configs/GenericChatCommandsConfig.cs b/GenericChatCommands/Configs/GenericChatCommandsConfig.cs new file mode 100644 index 0000000..92e4790 --- /dev/null +++ b/GenericChatCommands/Configs/GenericChatCommandsConfig.cs @@ -0,0 +1,30 @@ +using BepInEx.Configuration; + +namespace VMods.GenericChatCommands +{ + public static class GenericChatCommandsConfig + { + #region Properties + + public static ConfigEntry GenericChatCommandsAnnounceRename { get; private set; } + public static ConfigEntry GenericChatCommandsAnnounceBloodMoonSkip { get; private set; } + public static ConfigEntry GenericChatCommandsAllowNextBloodMoonServerWideAnnouncing { get; private set; } + public static ConfigEntry GenericChatCommandsAnnounceGlobalCommandChanges { get; private set; } + public static ConfigEntry GenericChatCommandsAnnounceAdminLevelChanges { get; private set; } + + #endregion + + #region Public Methods + + public static void Initialize(ConfigFile config) + { + GenericChatCommandsAnnounceRename = config.Bind(nameof(GenericChatCommandsConfig), nameof(GenericChatCommandsAnnounceRename), true, "When enabled, the renaming of players is announced server-wide."); + GenericChatCommandsAnnounceBloodMoonSkip = config.Bind(nameof(GenericChatCommandsConfig), nameof(GenericChatCommandsAnnounceBloodMoonSkip), true, "When enabled, the skipping to the next bloodmoon is announced server-wide."); + GenericChatCommandsAllowNextBloodMoonServerWideAnnouncing = config.Bind(nameof(GenericChatCommandsConfig), nameof(GenericChatCommandsAllowNextBloodMoonServerWideAnnouncing), true, "When enabled, it is possible to announce the time until the next blood moon server-wide."); + GenericChatCommandsAnnounceGlobalCommandChanges = config.Bind(nameof(GenericChatCommandsConfig), nameof(GenericChatCommandsAnnounceGlobalCommandChanges), true, "When enabled, all the commands starting with 'global-' will be announced server-wide."); + GenericChatCommandsAnnounceAdminLevelChanges = config.Bind(nameof(GenericChatCommandsConfig), nameof(GenericChatCommandsAnnounceAdminLevelChanges), true, "When enabled, all the changes made to a player's admin-level will be announced server-wide."); + } + + #endregion + } +} diff --git a/GenericChatCommands/Configs/MutePlayerChatConfig.cs b/GenericChatCommands/Configs/MutePlayerChatConfig.cs new file mode 100644 index 0000000..2be9b8c --- /dev/null +++ b/GenericChatCommands/Configs/MutePlayerChatConfig.cs @@ -0,0 +1,28 @@ +using BepInEx.Configuration; + +namespace VMods.GenericChatCommands +{ + public static class MutePlayerChatConfig + { + #region Properties + + public static ConfigEntry GenericChatCommandsMutingEnabled { get; private set; } + public static ConfigEntry GenericChatCommandsModsCanMute { get; private set; } + public static ConfigEntry GenericChatCommandsAnnounceMutes { get; private set; } + public static ConfigEntry GenericChatCommandsAnnounceUnmutes { get; private set; } + + #endregion + + #region Public Methods + + public static void Initialize(ConfigFile config) + { + GenericChatCommandsMutingEnabled = config.Bind(nameof(MutePlayerChatConfig), nameof(GenericChatCommandsMutingEnabled), true, "Enabled/disable the Mute Player Chat system."); + GenericChatCommandsModsCanMute = config.Bind(nameof(MutePlayerChatConfig), nameof(GenericChatCommandsModsCanMute), true, "When enabled, players with the Moderator permission level can use the Mute Player Chat system commands."); + GenericChatCommandsAnnounceMutes = config.Bind(nameof(MutePlayerChatConfig), nameof(GenericChatCommandsAnnounceMutes), true, "When enabled, the muting of players is announced server-wide."); + GenericChatCommandsAnnounceUnmutes = config.Bind(nameof(MutePlayerChatConfig), nameof(GenericChatCommandsAnnounceUnmutes), true, "When enabled, the unmiting of players is announced server-wide."); + } + + #endregion + } +} diff --git a/GenericChatCommands/GenericChatCommands.csproj b/GenericChatCommands/GenericChatCommands.csproj new file mode 100644 index 0000000..b379c58 --- /dev/null +++ b/GenericChatCommands/GenericChatCommands.csproj @@ -0,0 +1,479 @@ + + + netstandard2.1 + VMods.GenericChatCommands + VMods.GenericChatCommands + A mod that adds a number of generic/general usage commands + 1.0.0 + true + latest + False + + + + M:\Games\Steam\steamapps\common\VRising\BepInEx\unhollowed + M:\Games\Steam\steamapps\common\VRising\BepInEx\WetstonePlugins + M:\Games\Steam\steamapps\common\VRising\VRising_Server\BepInEx\WetstonePlugins + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(UnhollowedDllPath)\com.stunlock.console.dll + + + $(UnhollowedDllPath)\com.stunlock.metrics.dll + + + $(UnhollowedDllPath)\com.stunlock.network.lidgren.dll + + + $(UnhollowedDllPath)\com.stunlock.network.steam.dll + + + $(UnhollowedDllPath)\Il2CppMono.Security.dll + + + $(UnhollowedDllPath)\Il2CppSystem.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Configuration.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Core.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Data.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Numerics.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Runtime.Serialization.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Xml.dll + + + $(UnhollowedDllPath)\Il2CppSystem.Xml.Linq.dll + + + $(UnhollowedDllPath)\Lidgren.Network.dll + + + $(UnhollowedDllPath)\MagicaCloth.dll + + + $(UnhollowedDllPath)\Malee.ReorderableList.dll + + + $(UnhollowedDllPath)\Newtonsoft.Json.dll + + + $(UnhollowedDllPath)\ProjectM.Behaviours.dll + + + $(UnhollowedDllPath)\ProjectM.Camera.dll + + + $(UnhollowedDllPath)\ProjectM.CastleBuilding.Systems.dll + + + $(UnhollowedDllPath)\ProjectM.Conversion.dll + + + $(UnhollowedDllPath)\ProjectM.Gameplay.Scripting.dll + + + $(UnhollowedDllPath)\ProjectM.Gameplay.Systems.dll + + + $(UnhollowedDllPath)\ProjectM.GeneratedNetCode.dll + + + $(UnhollowedDllPath)\ProjectM.Misc.Systems.dll + + + $(UnhollowedDllPath)\ProjectM.Pathfinding.dll + + + $(UnhollowedDllPath)\ProjectM.Presentation.Systems.dll + + + $(UnhollowedDllPath)\ProjectM.Roofs.dll + + + $(UnhollowedDllPath)\ProjectM.ScriptableSystems.dll + + + $(UnhollowedDllPath)\ProjectM.Shared.dll + + + $(UnhollowedDllPath)\Il2Cppmscorlib.dll + + + $(UnhollowedDllPath)\ProjectM.dll + + + $(UnhollowedDllPath)\com.stunlock.network.dll + + + $(UnhollowedDllPath)\ProjectM.Shared.Systems.dll + + + $(UnhollowedDllPath)\ProjectM.Terrain.dll + + + $(UnhollowedDllPath)\RootMotion.dll + + + $(UnhollowedDllPath)\Sequencer.dll + + + $(UnhollowedDllPath)\Stunlock.Fmod.dll + + + $(UnhollowedDllPath)\Unity.Burst.dll + + + $(UnhollowedDllPath)\Unity.Burst.Unsafe.dll + + + $(UnhollowedDllPath)\Unity.Collections.dll + + + $(UnhollowedDllPath)\Unity.Collections.LowLevel.ILSupport.dll + + + $(UnhollowedDllPath)\Unity.Deformations.dll + + + $(UnhollowedDllPath)\Unity.Entities.dll + + + $(UnhollowedDllPath)\ProjectM.HUD.dll + + + $(UnhollowedDllPath)\Unity.Entities.Hybrid.dll + + + $(UnhollowedDllPath)\Unity.Jobs.dll + + + $(UnhollowedDllPath)\Unity.Mathematics.dll + + + $(UnhollowedDllPath)\Unity.Mathematics.Extensions.dll + + + $(UnhollowedDllPath)\Unity.Mathematics.Extensions.Hybrid.dll + + + $(UnhollowedDllPath)\Unity.Physics.dll + + + $(UnhollowedDllPath)\Unity.Physics.Hybrid.dll + + + $(UnhollowedDllPath)\Unity.Properties.dll + + + $(UnhollowedDllPath)\Unity.Rendering.Hybrid.dll + + + $(UnhollowedDllPath)\Unity.RenderPipelines.Core.Runtime.dll + + + $(UnhollowedDllPath)\Unity.RenderPipelines.HighDefinition.Config.Runtime.dll + + + $(UnhollowedDllPath)\Unity.RenderPipelines.HighDefinition.Runtime.dll + + + $(UnhollowedDllPath)\Unity.Scenes.dll + + + $(UnhollowedDllPath)\Unity.Serialization.dll + + + $(UnhollowedDllPath)\Unity.Services.Analytics.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Configuration.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Device.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Environments.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Environments.Internal.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Internal.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Registration.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Scheduler.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Telemetry.dll + + + $(UnhollowedDllPath)\Unity.Services.Core.Threading.dll + + + $(UnhollowedDllPath)\Unity.TextMeshPro.dll + + + $(UnhollowedDllPath)\Unity.Transforms.dll + + + $(UnhollowedDllPath)\Unity.Transforms.Hybrid.dll + + + $(UnhollowedDllPath)\Unity.VisualEffectGraph.Runtime.dll + + + $(UnhollowedDllPath)\UnityEngine.dll + + + $(UnhollowedDllPath)\UnityEngine.AccessibilityModule.dll + + + $(UnhollowedDllPath)\UnityEngine.AIModule.dll + + + $(UnhollowedDllPath)\UnityEngine.AndroidJNIModule.dll + + + $(UnhollowedDllPath)\UnityEngine.AnimationModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ARModule.dll + + + $(UnhollowedDllPath)\UnityEngine.AssetBundleModule.dll + + + $(UnhollowedDllPath)\UnityEngine.AudioModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ClothModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ClusterInputModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ClusterRendererModule.dll + + + $(UnhollowedDllPath)\UnityEngine.CoreModule.dll + + + $(UnhollowedDllPath)\ProjectM.CodeGeneration.dll + + + $(UnhollowedDllPath)\Stunlock.Core.dll + + + $(UnhollowedDllPath)\UnityEngine.CrashReportingModule.dll + + + $(UnhollowedDllPath)\UnityEngine.DirectorModule.dll + + + $(UnhollowedDllPath)\UnityEngine.DSPGraphModule.dll + + + $(UnhollowedDllPath)\UnityEngine.GameCenterModule.dll + + + $(UnhollowedDllPath)\UnityEngine.GIModule.dll + + + $(UnhollowedDllPath)\UnityEngine.GridModule.dll + + + $(UnhollowedDllPath)\UnityEngine.HotReloadModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ImageConversionModule.dll + + + $(UnhollowedDllPath)\UnityEngine.IMGUIModule.dll + + + $(UnhollowedDllPath)\UnityEngine.InputLegacyModule.dll + + + $(UnhollowedDllPath)\UnityEngine.InputModule.dll + + + $(UnhollowedDllPath)\UnityEngine.JSONSerializeModule.dll + + + $(UnhollowedDllPath)\UnityEngine.LocalizationModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ParticleSystemModule.dll + + + $(UnhollowedDllPath)\UnityEngine.PerformanceReportingModule.dll + + + $(UnhollowedDllPath)\UnityEngine.Physics2DModule.dll + + + $(UnhollowedDllPath)\UnityEngine.PhysicsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ProfilerModule.dll + + + $(UnhollowedDllPath)\UnityEngine.RuntimeInitializeOnLoadManagerInitializerModule.dll + + + $(UnhollowedDllPath)\UnityEngine.ScreenCaptureModule.dll + + + $(UnhollowedDllPath)\UnityEngine.SharedInternalsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.SpriteMaskModule.dll + + + $(UnhollowedDllPath)\UnityEngine.SpriteShapeModule.dll + + + $(UnhollowedDllPath)\UnityEngine.StreamingModule.dll + + + $(UnhollowedDllPath)\UnityEngine.SubstanceModule.dll + + + $(UnhollowedDllPath)\UnityEngine.SubsystemsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TerrainModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TerrainPhysicsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TextCoreModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TextRenderingModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TilemapModule.dll + + + $(UnhollowedDllPath)\UnityEngine.TLSModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UI.dll + + + $(UnhollowedDllPath)\UnityEngine.UIElementsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UIElementsNativeModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UIModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UmbraModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UNETModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityAnalyticsModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityConnectModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityCurlModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityTestProtocolModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityWebRequestAssetBundleModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityWebRequestAudioModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityWebRequestModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityWebRequestTextureModule.dll + + + $(UnhollowedDllPath)\UnityEngine.UnityWebRequestWWWModule.dll + + + $(UnhollowedDllPath)\UnityEngine.VehiclesModule.dll + + + $(UnhollowedDllPath)\UnityEngine.VFXModule.dll + + + $(UnhollowedDllPath)\UnityEngine.VideoModule.dll + + + $(UnhollowedDllPath)\UnityEngine.VirtualTexturingModule.dll + + + $(UnhollowedDllPath)\UnityEngine.VRModule.dll + + + $(UnhollowedDllPath)\UnityEngine.WindModule.dll + + + $(UnhollowedDllPath)\UnityEngine.XRModule.dll + + + $(UnhollowedDllPath)\VivoxUnity.dll + + + + + + + diff --git a/GenericChatCommands/Plugin.cs b/GenericChatCommands/Plugin.cs new file mode 100644 index 0000000..f3dee90 --- /dev/null +++ b/GenericChatCommands/Plugin.cs @@ -0,0 +1,64 @@ +using BepInEx; +using BepInEx.IL2CPP; +using HarmonyLib; +using System.Reflection; +using VMods.Shared; +using Wetstone.API; + +namespace VMods.GenericChatCommands +{ + [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] + [BepInDependency("xyz.molenzwiebel.wetstone")] + [Reloadable] + public class Plugin : BasePlugin + { + #region Variables + + private Harmony _hooks; + + #endregion + + #region Public Methods + + public sealed override void Load() + { + if(VWorld.IsClient) + { + Log.LogMessage($"{PluginInfo.PLUGIN_NAME} only needs to be installed server side."); + return; + } + Utils.Initialize(Log, PluginInfo.PLUGIN_NAME); + + CommandSystemConfig.Initialize(Config); + GenericChatCommandsConfig.Initialize(Config); + MutePlayerChatConfig.Initialize(Config); + + CommandSystem.Initialize(); + GenericChatCommandsSystem.Initialize(); + MutePlayerChatSystem.Initialize(); + + _hooks = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly()); + + Log.LogInfo($"Plugin {PluginInfo.PLUGIN_NAME} (v{PluginInfo.PLUGIN_VERSION}) is loaded!"); + } + + public sealed override bool Unload() + { + if(VWorld.IsClient) + { + return true; + } + VModStorage.SaveAll(); + + _hooks?.UnpatchSelf(); + MutePlayerChatSystem.Deinitialize(); + GenericChatCommandsSystem.Deinitialize(); + CommandSystem.Deinitialize(); + Config.Clear(); + Utils.Deinitialize(); + return true; + } + + #endregion + } +} diff --git a/GenericChatCommands/Systems/GenericChatCommandsSystem.cs b/GenericChatCommands/Systems/GenericChatCommandsSystem.cs new file mode 100644 index 0000000..927bdf2 --- /dev/null +++ b/GenericChatCommands/Systems/GenericChatCommandsSystem.cs @@ -0,0 +1,837 @@ +using ProjectM; +using ProjectM.Network; +using ProjectM.Scripting; +using System; +using System.Collections.Generic; +using System.Linq; +using Unity.Collections; +using Unity.Entities; +using Unity.Transforms; +using VMods.Shared; +using Wetstone.API; +using AdminLevel = VMods.Shared.CommandAttribute.AdminLevel; + +namespace VMods.GenericChatCommands +{ + public static class GenericChatCommandsSystem + { + #region Consts + + private static readonly Dictionary DebugSettingTypeToCommandNameMapping = new() + { + [DebugSettingType.SunDamageDisabled] = ("sun-damage", true), + [DebugSettingType.DurabilityDisabled] = ("durability-loss", true), + [DebugSettingType.BloodDrainDisabled] = ("blood-drain", true), + [DebugSettingType.CooldownsDisabled] = ("cooldowns", true), + [DebugSettingType.BuildCostsDisabled] = ("build-costs", true), + [DebugSettingType.AllProgressionUnlocked] = ("all-progression-unlocked", false), + [DebugSettingType.PlayerInvulernabilityEnabled] = ("play-invul", false), + [DebugSettingType.DayNightCycleDisabled] = ("day-night-cycle", true), + [DebugSettingType.NPCsDisabled] = ("npc-movement", true), + [DebugSettingType.BuildingAreaRestrictionDisabled] = ("building-area-restrictions", true), + [DebugSettingType.AllWaypointsUnlocked] = ("all-waypoints-unlocked", false), + [DebugSettingType.AggroDisabled] = ("aggro", true), + [DebugSettingType.UseDeathSequencesInsteadOfRagdolls] = ("death-sequence-instead-of-ragdolls", false), + [DebugSettingType.DropsDisabled] = ("drops", true), + [DebugSettingType.TutorialPopupsDisabled] = ("tutorial-popups", true), + [DebugSettingType.BuildingPlacementRestrictionsDisabled] = ("building-placement-restrictions", true), + [DebugSettingType.Use3DHeight] = ("3d-height", false), + [DebugSettingType.TileCollisionDisabled] = ("tile-collision", true), + [DebugSettingType.DynamicCollisionDisabled] = ("dynamic-collision", true), + [DebugSettingType.BuildingReplacementDisabled] = ("building-replacement", true), + [DebugSettingType.DynamicCloudsDisabled] = ("dynamic-clouds", true), + [DebugSettingType.HitEffectsDisabled] = ("hit-effects", true), + [DebugSettingType.HighCastleRoofsEnabled] = ("high-castle-roofs", false), + [DebugSettingType.FeedWoundedRequirementDisabled] = ("feed-at-any-hp", true), + [DebugSettingType.LinnCastleRoofsEnabled] = ("linn-castle-roofs", false), + [DebugSettingType.FreeBuildingPlacementEnabled] = ("free-building-placement", false), + [DebugSettingType.BuildingFloorTerritoryDisabled] = ("building-floor-territory", true), + [DebugSettingType.BuildingEnableDebugging] = ("building-debugging", false), + [DebugSettingType.UseSunblockerChecksForFly] = ("bat-sun-damage", true), + [DebugSettingType.CastleHeartBloodEssenceDisabled] = ("castle-heart-blood-ess", true), + [DebugSettingType.CastleLimitsDisabled] = ("castle-limits", true), + }; + + #endregion + + #region Variables + + private static readonly List _registeredCommands = new(); + + #endregion + + #region Public Methods + + public static void Initialize() + { + var debugSettingTypes = (DebugSettingType[])Enum.GetValues(typeof(DebugSettingType)); + foreach(var debugSettingType in debugSettingTypes) + { + if(!DebugSettingTypeToCommandNameMapping.TryGetValue(debugSettingType, out var commandData)) + { + Utils.Logger.LogWarning($"Missing Command Name for '{debugSettingType}'."); + continue; + } + + var id = Guid.NewGuid().ToString(); + var name = debugSettingType.ToString(); + var commandName = $"global-{commandData.commandName}"; + var commandAttribute = new CommandAttribute(commandName, $"{commandName} [on/off]", $"Turns the '{commandData.commandName}' setting on/off Server-Wide", AdminLevel.SuperAdmin); + CommandSystem.RegisterCommand(id, command => OnDebugSettingCommand(command, debugSettingType), commandAttribute); + _registeredCommands.Add(id); + } + } + + public static void Deinitialize() + { + _registeredCommands.ForEach(x => CommandSystem.UnregisterCommand(x)); + } + + #endregion + + #region Private Methods + + private static void OnDebugSettingCommand(Command command, DebugSettingType debugSettingType) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + var toggleValue = command.Args[0]; + var commandData = DebugSettingTypeToCommandNameMapping[debugSettingType]; + + switch(toggleValue) + { + case "on": + case "true": + case "1": + SetDebugSetting(true); + break; + + case "off": + case "false": + case "0": + SetDebugSetting(false); + break; + + default: + command.VModCharacter.SendSystemMessage($"Invalid toggle options. Options are: on, off"); + break; + } + + // Nested Method(s) + void SetDebugSetting(bool enabled) + { + var enabledName = enabled ? "on" : "off"; + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has turned the '{debugSettingType}' setting {enabledName}."); + + SetDebugSettingEvent setDebugSettingEvent = new() + { + SettingType = debugSettingType, + Value = commandData.reverseOnOff ? !enabled : enabled, + }; + server.GetExistingSystem().SetDebugSetting(command.VModCharacter.User.Index, ref setDebugSettingEvent); + + if(GenericChatCommandsConfig.GenericChatCommandsAnnounceGlobalCommandChanges.Value) + { + ServerChatUtils.SendSystemMessageToAllClients(entityManager, $"{command.VModCharacter.CharacterName} has been turned {commandData.commandName} {enabledName}"); + } + else + { + command.VModCharacter.SendSystemMessage($"{commandData.commandName} has been turned {enabledName}"); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("set-admin-level,set-admin-lvl,set-adminlvl,setadminlevel,setadminlvl", "set-admin-level ", "Changes the given player's Admin Level to the given level", AdminLevel.SuperAdmin)] + private static void OnSetAdminLevelCommand(Command command) + { + if(command.Args.Length >= 2) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + + if(vmodCharacter.HasValue) + { + AdminLevel? newAdminLevel = null; + switch(command.Args[1].ToLowerInvariant()) + { + case "none": + newAdminLevel = AdminLevel.None; + break; + + case "mod": + case "moderator": + newAdminLevel = AdminLevel.Moderator; + break; + + case "admin": + newAdminLevel = AdminLevel.Admin; + break; + + case "superadmin": + case "super-admin": + newAdminLevel = AdminLevel.SuperAdmin; + break; + + default: + command.VModCharacter.SendSystemMessage($"Invalid admin-level. Options are: none, mod, admin, superadmin"); + break; + } + + if(newAdminLevel.HasValue) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has changed the Admin Level of {searchUsername} from {vmodCharacter.Value.AdminLevel} to {newAdminLevel.Value}"); + + var entity = entityManager.CreateEntity( + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + ); + entityManager.SetComponentData(entity, command.VModCharacter.FromCharacter); + entityManager.SetComponentData(entity, new SetUserAdminLevelAdminEvent() + { + UserNetworkId = entityManager.GetComponentData(vmodCharacter.Value.FromCharacter.User), + AdminLevel = newAdminLevel.Value.ToAdminLevel(), + }); + + if(GenericChatCommandsConfig.GenericChatCommandsAnnounceAdminLevelChanges.Value) + { + string message = newAdminLevel.Value switch + { + AdminLevel.None => $"Vampire {searchUsername} had his/her {vmodCharacter.Value.AdminLevel} privileges revoked", + _ => $"Vampire {searchUsername} has been granted {newAdminLevel.Value} privileges!", + }; + ServerChatUtils.SendSystemMessageToAllClients(entityManager, message); + } + else + { + if(vmodCharacter.Value != command.VModCharacter) + { + string message = newAdminLevel.Value switch + { + AdminLevel.None => $"{command.VModCharacter.CharacterName} has revoked your {vmodCharacter.Value.AdminLevel} privileges", + _ => $"{command.VModCharacter.CharacterName} has granted you {newAdminLevel.Value} privileges!", + }; + vmodCharacter.Value.SendSystemMessage(message); + } + // No need to send it to the command sender because the game itself already tells you what happend. + } + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("admin-level,get-admin-level,get-admin-lvl,get-adminlvl,getadminevel,adminlevel,adminlvl", "admin-level []", "Tells you the Admin Level of yourself (or the give player)")] + private static void OnGetAdminLevelCommand(Command command) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + + if(vmodCharacter.HasValue) + { + var adminLevel = vmodCharacter.Value.AdminLevel.ToAdminLevel(); + string message = adminLevel switch + { + AdminLevel.None => $"Vampire {searchUsername} has no special privileges", + _ => $"Vampire {searchUsername} has {adminLevel} privileges", + }; + command.VModCharacter.SendSystemMessage(message); + } + command.Use(); + } + + [Command("rename", "rename [] ", "Renames a given player (or yourself) to a new name", AdminLevel.Admin)] + private static void OnRenameCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + var newUsername = command.Args.Length switch + { + 1 => command.Args[0], + _ => command.Args[1], + }; + + if(vmodCharacter.HasValue) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has renamed {searchUsername} to {newUsername}"); + + server.GetExistingSystem().RenameUser(vmodCharacter.Value.FromCharacter, new RenameUserDebugEvent() + { + NewName = newUsername, + Target = entityManager.GetComponentData(vmodCharacter.Value.FromCharacter.User), + }); + + string message = $"Vampire {searchUsername} is now known as {newUsername}!"; + if(GenericChatCommandsConfig.GenericChatCommandsAnnounceRename.Value) + { + ServerChatUtils.SendSystemMessageToAllClients(entityManager, message); + } + else + { + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has renamed you to {newUsername}"); + } + command.VModCharacter.SendSystemMessage(message); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("nxtbm,nextbm,nextbloodmoon", "nxtbm [server-wide]", "Tells you (or the entire server) when the next Blood Moon will appear", AdminLevel.Admin)] + private static void OnNextBloodMoonWhenCommand(Command command) + { + bool serverWide = command.Args.Length >= 1 && command.Args[0] == "server-wide"; + + var server = VWorld.Server; + var dayNightCycle = server.GetExistingSystem()._DayNightCycleAccessor.GetSingleton(); + string message; + if(dayNightCycle.IsBloodMoonDay()) + { + message = $"Today is Blood Moon day!"; + } + else + { + var totalDays = dayNightCycle.TotalDays(); + var nextBloodMoonDay = dayNightCycle.NextBloodMoonDay; + var remainingDays = nextBloodMoonDay - totalDays; + if(remainingDays >= 1) + { + message = $"Next Blood Moon will be in {nextBloodMoonDay - totalDays} days"; + } + else if(remainingDays == 0) + { + message = $"Next Blood Moon will be later today"; + } + else + { + message = $"The Blood Moon is still thinking about when to appear again..."; + } + } + if(serverWide && GenericChatCommandsConfig.GenericChatCommandsAllowNextBloodMoonServerWideAnnouncing.Value) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has announced the time until the next Blood Moon server-wide."); + ServerChatUtils.SendSystemMessageToAllClients(server.EntityManager, message); + } + else + { + command.VModCharacter.SendSystemMessage(message); + } + command.Use(); + } + + [Command("skiptobm,skiptobloodmoon", "skiptobm", "Skips time to the next Blood Moon", AdminLevel.Admin)] + private static void OnSkipToBloodMoonCommand(Command command) + { + var server = VWorld.Server; + server.GetExistingSystem().JumpToNextBloodMoon(); + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has skipped time to the next Blood Moon."); + + var message = "Time has been skipped to the next Blood Moon!"; + if(GenericChatCommandsConfig.GenericChatCommandsAnnounceBloodMoonSkip.Value) + { + ServerChatUtils.SendSystemMessageToAllClients(server.EntityManager, message); + } + else + { + command.VModCharacter.SendSystemMessage(message); + } + command.Use(); + } + + [Command("buff", "buff [] ", "Adds the buff defined by the buff-id to yourself (or the given player)", AdminLevel.Admin)] + private static void OnBuffCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + if(vmodCharacter.HasValue) + { + if(int.TryParse(command.Args.Length switch + { + 1 => command.Args[0], + _ => command.Args[1], + }, out int guidHash)) + { + var buffGUID = new PrefabGUID(guidHash); + var prefabNameLookupMap = server.GetExistingSystem().PrefabNameLookupMap; + if(prefabNameLookupMap.ContainsKey(buffGUID)) + { + var buffName = prefabNameLookupMap[buffGUID]; + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has applied the {buffName} ({guidHash}) to {searchUsername}"); + + vmodCharacter.Value.ApplyBuff(buffGUID); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.Character} has buffed you with {buffName}"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} has been buffed with {buffName} ({guidHash})"); + } + else + { + command.VModCharacter.SendSystemMessage($"Buff ID {guidHash} does not exist."); + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command, true); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("unbuff", "unbuff [] ", "Remove the buff defined by the prefab-GUID to yourself (or the given player)", AdminLevel.Admin)] + private static void OnUnBuffCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + if(vmodCharacter.HasValue) + { + if(int.TryParse(command.Args.Length switch + { + 1 => command.Args[0], + _ => command.Args[1], + }, out int guidHash)) + { + var buffGUID = new PrefabGUID(guidHash); + var prefabNameLookupMap = server.GetExistingSystem().PrefabNameLookupMap; + var buffName = prefabNameLookupMap[buffGUID]; + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has removed the {buffName} ({guidHash}) to {searchUsername}"); + + vmodCharacter.Value.RemoveBuff(buffGUID); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has removed your {buffName} buff"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} has been un-buffed from {buffName} ({guidHash})"); + } + else + { + CommandSystem.SendInvalidCommandMessage(command, true); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("ping", "ping []", "Tells you how much ping/latency you ([ADMIN] or the given player) has")] + private static void OnPingCommand(Command command) + { + var entityManager = VWorld.Server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.VModCharacter.IsAdmin ? command.Args.Length - 1 : -1, entityManager: entityManager); + if(vmodCharacter.HasValue) + { + var latency = entityManager.GetComponentData(vmodCharacter.Value.FromCharacter.User); + if(searchUsername == command.VModCharacter.CharacterName) + { + command.VModCharacter.SendSystemMessage($"You have {latency.Value}ms ping"); + } + else + { + command.VModCharacter.SendSystemMessage($"{searchUsername} has {latency.Value}ms ping"); + } + } + command.Use(); + } + + [Command("health,hp", "health [] ", "Sets the Health of yourself (or the given player) to the given percentage", AdminLevel.Admin)] + private static void OnHealthCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + if(vmodCharacter.HasValue) + { + if(int.TryParse((command.Args.Length switch + { + 1 => command.Args[0], + _ => command.Args[1], + }).Replace("%", string.Empty), out int percentage) && percentage >= 0 && percentage <= 100) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has set the health of {searchUsername} to {percentage}%"); + + var healthData = entityManager.GetComponentData(vmodCharacter.Value.FromCharacter.Character); + + var changeHealthEvent = new ChangeHealthDebugEvent() + { + Amount = (int)((healthData.MaxHealth / 100f * percentage) - healthData.Value), + }; + server.GetExistingSystem().ChangeHealthEvent(command.VModCharacter.User.Index, ref changeHealthEvent); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has changed your health to {percentage}%"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} has had his health changed to {percentage}%"); + } + else + { + CommandSystem.SendInvalidCommandMessage(command, true); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("complete-all-achievements,complete-achievements,completeallachievements,completeachievements", "complete-all-achievements []", "Completes all achievements for yourself (or the given player)", AdminLevel.Admin)] + private static void OnCompletedAllAchievementsCommand(Command command) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + if(vmodCharacter.HasValue) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has set all achievements completed for {searchUsername}"); + + server.GetExistingSystem().CompleteAllAchievements(command.VModCharacter.FromCharacter); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has completed all achievements for you"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} now has all achievements completed"); + } + command.Use(); + } + + [Command("unlock-all-research,unlock-research,unlockallresearch,unlockresearch", "unlock-all-research []", "Unlocks all research for yourself (or the given player)", AdminLevel.Admin)] + private static void OnUnlockAllResearchCommand(Command command) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + if(vmodCharacter.HasValue) + { + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has unlocked all research for {searchUsername}"); + + server.GetExistingSystem().UnlockAllResearch(command.VModCharacter.FromCharacter); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has unlocked all research for you"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} now has all research unlocked"); + } + command.Use(); + } + + [Command("unlock-all-v-blood,unlock-all-vblood,unlock-v-blood,unlock-vblood,unlockallvblood,unlockvblood", "unlock-all-v-blood [] ", "Unlocks all V-Blood Abilities/Passives/Shapshifts or all three of these for yourself (or the given player)", AdminLevel.Admin)] + private static void OnUnlockAllVBloodCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + if(vmodCharacter.HasValue) + { + string unlockOption = command.Args.Length switch + { + 1 => command.Args[0], + _ => command.Args[1], + }; + switch(unlockOption) + { + case "all": + server.GetExistingSystem().UnlockAllVBloods(command.VModCharacter.FromCharacter); + unlockOption = "abilities, passives & shapeshifts"; + break; + + case "ability": + case "abilities": + server.GetExistingSystem().UnlockVBloodFeatures(command.VModCharacter.FromCharacter, DebugEventsSystem.VBloodFeatureType.Ability); + unlockOption = "abilities"; + break; + + case "passive": + case "passives": + server.GetExistingSystem().UnlockVBloodFeatures(command.VModCharacter.FromCharacter, DebugEventsSystem.VBloodFeatureType.Passive); + unlockOption = "passives"; + break; + + case "shapeshift": + case "shapeshifts": + server.GetExistingSystem().UnlockVBloodFeatures(command.VModCharacter.FromCharacter, DebugEventsSystem.VBloodFeatureType.Shapeshift); + unlockOption = "shapeshifts"; + break; + + default: + command.VModCharacter.SendSystemMessage($"Invalid unlock options. Options are: all, ability, passive, shapeshift"); + break; + } + + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has unlocked all V-Blood {unlockOption} for {searchUsername}"); + + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has unlocked all V-Blood {unlockOption} for you"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} now has all V-Blood {unlockOption} unlocked"); + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("spawn-npc,spawnnpc,spn", "spawnnpc [] []", "Spawns the given amount of npcs based on their name or prefab-GUID, and they'll stay alive of the given amount of time (or untill killed when the life-time argument is omitted)", AdminLevel.Admin)] + private static void OnSpawnNpcCommand(Command command) + { + if(command.Args.Length >= 1) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + var prefabCollection = server.GetExistingSystem(); + + string npcNameOrGUID = command.Args[0]; + string npcName = string.Empty; + PrefabGUID npcGUID = PrefabGUID.Empty; + if(int.TryParse(npcNameOrGUID, out var prefabGUIDHash)) + { + var guid = new PrefabGUID(prefabGUIDHash); + if(prefabCollection.PrefabGuidLookupMap.ContainsKey(guid)) + { + npcName = prefabCollection.PrefabGuidLookupMap[guid].ToString(); + npcGUID = guid; + } + } + else + { + FixedString128 prefabName = new(npcNameOrGUID); + if(prefabCollection.PrefabNameToPrefabGuidLookupMap.ContainsKey(prefabName)) + { + npcGUID = prefabCollection.PrefabNameToPrefabGuidLookupMap[prefabName]; + npcName = npcNameOrGUID; + } + } + + if(npcGUID != PrefabGUID.Empty) + { + if(npcName.StartsWith("CHAR_")) + { + if(int.TryParse(command.Args.Length switch + { + 1 => "1", + _ => command.Args[1], + }, out var amount) && amount >= 0) + { + if(int.TryParse(command.Args.Length switch + { + 1 => "-1", + 2 => "-1", + _ => command.Args[2] + }, out int lifeTime)) + { + lifeTime = Math.Max(lifeTime, -1); + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has spawned {amount} {npcName} (Life-time: {lifeTime}s)"); + + var translation = entityManager.GetComponentData(command.VModCharacter.FromCharacter.User); + + server.GetExistingSystem().SpawnUnit(Entity.Null, npcGUID, translation.Value, amount, 1f, 2f, lifeTime); + + command.VModCharacter.SendSystemMessage($"{command.VModCharacter.CharacterName} has spawned {amount} {npcName}{(lifeTime == -1 ? "" : $" (Life-time: {lifeTime}s)")}"); + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid life-time for the NPC(s)"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid amount of NPC(s) to spawn"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"'{npcNameOrGUID}' is a valid name/GUID, but it isn't an NPC (and thus cannot be spawned)"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Unknown NPC name or GUID '{npcNameOrGUID}'"); + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + [Command("set-blood,setblood", "set-blood [] []", "Sets your (or the given player's) blood type to the specified blood-type and blood-quality, and optionally adds a given amount of blood (in Litres)", AdminLevel.Admin)] + private static void OnSetBloodCommand(Command command) + { + var argCount = command.Args.Length; + if(argCount >= 2) + { + bool isFirstArgBloodType = Enum.TryParse(command.Args[0].ToLowerInvariant(), true, out BloodType _); + + var entityManager = VWorld.Server.EntityManager; + int argIdx = argCount switch + { + 3 when isFirstArgBloodType => -1, + 3 => 0, + 4 => 0, + _ => -1, + }; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx, entityManager: entityManager); + + if(vmodCharacter.HasValue) + { + argIdx++; + var searchBloodType = command.Args[argIdx]; + var validBloodTypes = BloodTypeExtensions.BloodTypeToPrefabGUIDMapping.Keys.ToList(); + if(Enum.TryParse(searchBloodType.ToLowerInvariant(), true, out BloodType bloodType) && validBloodTypes.Contains(bloodType)) + { + argIdx++; + var searchBloodQuality = command.Args[argIdx]; + if(int.TryParse(searchBloodQuality.Replace("%", string.Empty), out var bloodQuality) && bloodQuality >= 1 && bloodQuality <= 100) + { + float? addBloodAmount = null; + if((argCount >= 3 && isFirstArgBloodType) || argCount >= 4) + { + argIdx++; + var searchLitres = command.Args[argIdx]; + if(float.TryParse(searchLitres.Replace("L", string.Empty), out float parsedAddBloodAmount) && parsedAddBloodAmount >= -10f && parsedAddBloodAmount <= 10f) + { + addBloodAmount = parsedAddBloodAmount; + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid gain-amount '{searchBloodQuality}'. Should be between -10 and 10"); + } + } + else + { + addBloodAmount = 10f; + } + + if(addBloodAmount.HasValue) + { + Utils.Logger.LogMessage($"{command.VModCharacter.Character} has changed blood type of {searchUsername} to {bloodQuality}% {searchBloodType} and added {addBloodAmount.Value}L"); + + bloodType.ApplyToPlayer(vmodCharacter.Value.User, bloodQuality, (int)(addBloodAmount.Value * 10f)); + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.Character} has changed your blood type to {bloodQuality}% {searchBloodType} and added {addBloodAmount.Value}L"); + } + command.VModCharacter.SendSystemMessage($"Changed blood type of {searchUsername} to {bloodQuality}% {searchBloodType} and added {addBloodAmount.Value}L"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid blood-quality '{searchBloodQuality}'. Should be between 1 and 100"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid blood-type '{searchBloodType}'. Options are: {string.Join(", ", validBloodTypes.Select(x => x.ToString()))}"); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + + [Command("blood-potion,bloodpotion,bloodpot", "blood-potion ", "Creates a Blood Potion with the given Blood Type and Blood Quality", AdminLevel.Admin)] + private static void OnBloodPotionCommand(Command command) + { + if(command.Args.Length >= 2) + { + var searchBloodType = command.Args[0]; + var validBloodTypes = BloodTypeExtensions.BloodTypeToPrefabGUIDMapping.Keys.ToList(); + if(Enum.TryParse(searchBloodType.ToLowerInvariant(), true, out BloodType bloodType) && validBloodTypes.Contains(bloodType)) + { + var searchBloodQuality = command.Args[1]; + if(int.TryParse(searchBloodQuality.Replace("%", string.Empty), out var bloodQuality) && bloodQuality >= 1 && bloodQuality <= 100) + { + var server = VWorld.Server; + var entityManager = server.EntityManager; + var itemHashLookupMap = server.GetExistingSystem()._GameDataSystem.ItemHashLookupMap; + + if(Utils.TryGiveItem(entityManager, itemHashLookupMap, command.VModCharacter.FromCharacter.Character, Utils.BloodPotion, 1, out _, out var itemEntity) && itemEntity != Entity.Null && entityManager.HasComponent(itemEntity)) + { + Utils.Logger.LogMessage($"{command.VModCharacter.Character} has created a {bloodQuality}% {searchBloodType} blood potion"); + + var storedBlood = entityManager.GetComponentData(itemEntity); + storedBlood.BloodType = new PrefabGUID((int)bloodType); + storedBlood.BloodQuality = bloodQuality; + entityManager.SetComponentData(itemEntity, storedBlood); + + command.VModCharacter.SendSystemMessage($"You received a {bloodQuality}% {searchBloodType} Blood Potion"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid blood-quality '{searchBloodQuality}'. Should be between 1 and 100"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid blood-type '{searchBloodType}'. Options are: {string.Join(", ", validBloodTypes.Select(x => x.ToString()))}"); + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + command.Use(); + } + + #endregion + } +} diff --git a/GenericChatCommands/Systems/MutePlayerChatSystem.cs b/GenericChatCommands/Systems/MutePlayerChatSystem.cs new file mode 100644 index 0000000..c9552ee --- /dev/null +++ b/GenericChatCommands/Systems/MutePlayerChatSystem.cs @@ -0,0 +1,285 @@ +using ProjectM; +using ProjectM.Network; +using System; +using System.Collections.Generic; +using System.Linq; +using VMods.Shared; +using Wetstone.API; +using Wetstone.Hooks; +using AdminLevel = VMods.Shared.CommandAttribute.AdminLevel; + +namespace VMods.GenericChatCommands +{ + public static class MutePlayerChatSystem + { + #region Consts + + private const string MutedPlayersFileName = "MutedPlayers.json"; + + #endregion + + #region Variables + + private static Dictionary _mutes; + + #endregion + + #region Public Methods + + public static void Initialize() + { + _mutes = VModStorage.Load(MutedPlayersFileName, () => new Dictionary()); + + PruneMutes(); + + VModStorage.SaveEvent += Save; + Chat.OnChatMessage += OnChatMessage; + } + + public static void Deinitialize() + { + Chat.OnChatMessage -= OnChatMessage; + VModStorage.SaveEvent -= Save; + } + + public static void Save() + { + PruneMutes(); + + VModStorage.Save(MutedPlayersFileName, _mutes); + } + + #endregion + + #region Private Methods + + private static void PruneMutes() + { + var now = DateTime.UtcNow; + var keys = _mutes.Keys.ToList(); + foreach(var key in keys) + { + var muteData = _mutes[key]; + if(now > muteData.MutedUntil) + { + _mutes.Remove(key); + } + } + } + + private static void OnChatMessage(VChatEvent chatEvent) + { + if(chatEvent.Cancelled || chatEvent.User.IsAdmin || !MutePlayerChatConfig.GenericChatCommandsMutingEnabled.Value) + { + return; + } + + var now = DateTime.UtcNow; + if(_mutes.TryGetValue(chatEvent.User.PlatformId, out var muteData) && muteData.MutedUntil > now) + { + switch(muteData.ChatType) + { + case -1: + // Always mute everything! + break; + + default: + Utils.Logger.LogMessage($"{chatEvent.Type}"); + if((int)chatEvent.Type != muteData.ChatType) + { + // Not muted for the given chat type. + return; + } + break; + } + + chatEvent.Cancel(); + chatEvent.User.SendSystemMessage($"You have been muted for {Math.Ceiling(muteData.MutedUntil.Subtract(now).TotalMinutes)} more minutes."); + } + } + + [Command("mute", "mute [global/local]", "Mutes the given player for the given number of minutes in the given chat/channel (or all chats/channels when omitted) - commands can still be used by the muted player", AdminLevel.Moderator)] + private static void OnMuteCommand(Command command) + { + if(MutePlayerChatConfig.GenericChatCommandsModsCanMute.Value || command.VModCharacter.AdminLevel.HasReqLevel(AdminLevel.Admin)) + { + if(command.Args.Length >= 2) + { + var entityManager = VWorld.Server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + if(vmodCharacter.HasValue) + { + if(int.TryParse(command.Args[1], out var minutes) && minutes >= 1) + { + int? chatType = null; + if(command.Args.Length >= 3) + { + switch(command.Args[2].ToLowerInvariant()) + { + case "global": + chatType = (int)ChatMessageType.Global; + break; + + case "local": + chatType = (int)ChatMessageType.Local; + break; + + default: + command.VModCharacter.SendSystemMessage($"Invalid chat-type option. Options are: global, local"); + break; + } + } + else + { + chatType = -1; + } + + if(chatType.HasValue) + { + if(vmodCharacter.Value.AdminLevel != ProjectM.AdminLevel.None) + { + var inChat = chatType.Value != -1 ? $" in {(ChatMessageType)chatType.Value} chat" : string.Empty; + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has muted {searchUsername} for {minutes} minutes{inChat}"); + + PruneMutes(); + + var platformId = vmodCharacter.Value.User.PlatformId; + if(!_mutes.TryGetValue(platformId, out var muteData)) + { + muteData = new MuteData(); + _mutes.Add(platformId, muteData); + } + muteData.MutedUntil = DateTime.UtcNow.AddMinutes(minutes); + muteData.ChatType = chatType.Value; + + if(MutePlayerChatConfig.GenericChatCommandsAnnounceMutes.Value) + { + ServerChatUtils.SendSystemMessageToAllClients(entityManager, $"Vampire {searchUsername} has been muted for {minutes} minutes!"); + } + else + { + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has muted you for {minutes} minutes"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} has been muted for {minutes} minutes{inChat}"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"{vmodCharacter.Value.AdminLevel}s cannot be muted!"); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command, true); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + } + command.Use(); + } + + [Command("unmute", "unmute ", "Unmutes the given player", AdminLevel.Moderator)] + private static void OnUnmuteCommand(Command command) + { + if(MutePlayerChatConfig.GenericChatCommandsModsCanMute.Value || command.VModCharacter.AdminLevel.HasReqLevel(AdminLevel.Admin)) + { + if(command.Args.Length >= 1) + { + var entityManager = VWorld.Server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + if(vmodCharacter.HasValue) + { + PruneMutes(); + + var platformId = vmodCharacter.Value.User.PlatformId; + if(_mutes.TryGetValue(platformId, out var muteData)) + { + var remainingMinutes = Math.Ceiling(muteData.MutedUntil.Subtract(DateTime.UtcNow).TotalMinutes); + var inChat = muteData.ChatType != -1 ? $" in {(ChatMessageType)muteData.ChatType} chat" : string.Empty; + + Utils.Logger.LogMessage($"{command.VModCharacter.User.CharacterName} has unmuted {searchUsername} (who had {remainingMinutes} minutes of mute remaining{inChat})"); + + _mutes.Remove(platformId); + + if(MutePlayerChatConfig.GenericChatCommandsAnnounceUnmutes.Value) + { + ServerChatUtils.SendSystemMessageToAllClients(entityManager, $"Vampire {searchUsername} has been unmuted"); + } + else + { + if(vmodCharacter.Value != command.VModCharacter) + { + vmodCharacter.Value.SendSystemMessage($"{command.VModCharacter.CharacterName} has unmuted you"); + } + command.VModCharacter.SendSystemMessage($"{searchUsername} has been unmuted"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"{searchUsername} wasn't even muted to begin with"); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + } + command.Use(); + } + + [Command("remaining-mute,remainingmute", "remaining-mute ", "Tells you how many more minutes the mute for the given player will last", AdminLevel.Moderator)] + private static void OnRemainingMuteCommand(Command command) + { + if(MutePlayerChatConfig.GenericChatCommandsModsCanMute.Value || command.VModCharacter.AdminLevel.HasReqLevel(AdminLevel.Admin)) + { + if(command.Args.Length >= 1) + { + var entityManager = VWorld.Server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(entityManager: entityManager); + if(vmodCharacter.HasValue) + { + PruneMutes(); + + var platformId = vmodCharacter.Value.User.PlatformId; + if(_mutes.TryGetValue(platformId, out var muteData)) + { + var remainingMinutes = Math.Ceiling(muteData.MutedUntil.Subtract(DateTime.UtcNow).TotalMinutes); + var inChat = muteData.ChatType != -1 ? $" in {(ChatMessageType)muteData.ChatType} chat" : string.Empty; + + command.VModCharacter.SendSystemMessage($"{searchUsername} will remain muted for another {remainingMinutes} minutes{inChat}"); + } + else + { + command.VModCharacter.SendSystemMessage($"{searchUsername} isn't even muted to begin with"); + } + } + } + else + { + CommandSystem.SendInvalidCommandMessage(command); + } + } + command.Use(); + } + + #endregion + + #region Nested + + private class MuteData + { + public DateTime MutedUntil { get; set; } + public int ChatType { get; set; } + } + + #endregion + } +} diff --git a/README.md b/README.md index 922b959..39b8e7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # VMods -A selection of Mods for V-Rising +VMods is a selection of Mods for V-Rising made using a common/shared codebase that all follow the same coding principals and in-game usage. + +## List of VMods +* [Blood Refill](#blood-refill) +* [Recover Empty Containers](#recover-empty-containers) +* [Resource Stash Withdrawal](#resource-stash-withdrawal) +* [PvP Leaderboard](#pvp-leaderboard) +* [PvP Punishment](#pvp-punishment) +* [Chest PvP Protection](#chest-pvp-protection) +* [Generic Chat Commands](#generic-chat-commands) ## General Mod info (Applies to most mods) ### How to manually install @@ -106,3 +115,94 @@ Both the amount of offenses before being punishes as well as the punishment itse A server-side only mod that prevents looting of enemy player chests/workstations by players with the PvP Protection buff. Besides looting, it'll also prevent moving, sorting, swapping and merging ("compilsively count") of items within those chests/workstations. + +## Generic Chat Commands +A server-side only mod that adds a fair amount of generic chat commands (mostly for Mods & Admins only though) and a player chat muting system. + +The player chat mute system can be used by Moderators & Admins to mute players (Admins, Super Admins and Moderators cannot be muted). +A Super Admin can grant Moderator or Admin provileges to other players (using one of the commands). + +
+List of available player commands + +* `!ping`: Tells you how much ping/latency you have. +* `!admin-level []`: Tells you the Admin Level of yourself (or the give player) + +
+ +
+List of available Moderator commands + +_Note: These commands can be made Admin-only through a config setting_ +* `!mute [global/local]`: Mutes the given player for the given number of minutes in the given chat/channel (or all chats/channels when omitted) - commands can still be used by the muted player +* `!unmute `: Unmutes the given player +* `!remaining-mute `: Tells you how many more minutes the mute for the given player will last + +
+ +
+List of available (Super)Admin commands + +* [SuperAdmin] `!set-admin-level `: Changes the given player's Admin Level to the given level +* `!ping []`: Tells you how much ping/latency you or the given player has +* `!rename [] `: Renames a given player (or yourself) to a new name +* `!nxtbm [server-wide]`: Tells you (or the entire server) when the next Blood Moon will appear +* `!skiptobm`: Skips time to the next Blood Moon +* `!buff [] `: Adds the buff defined by the prefab-GUID to yourself (or the given player) +* `!unbuff [] `: Removes the buff defined by the prefab-GUID to yourself (or the given player) +* `!health [] `: Sets the Health of yourself (or the given player) to the given percentage +* `!complete-all-achievements []`: Completes all achievements for yourself (or the given player) +* `!unlock-all-research []`: Unlocks all research for yourself (or the given player) +* `!unlock-all-v-blood [] `: Unlocks all V-Blood Abilities/Passives/Shapshifts or all three of these for yourself (or the given player) +* `!spawn-npc [] []`: Spawns the given amount of npcs based on their name or prefab-GUID, and they'll stay alive of the given amount of time (or untill killed when the life-time argument is omitted +* `!set-blood [] []`: Sets your (or the given player's) blood type to the specified blood-type and blood-quality, and optionally adds a given amount of blood (in Litres) +* `!blood-potion `: Creates a Blood Potion with the given Blood Type and Blood Quality +* [SuperAdmin] `!global-... [on/off]`: A set of commands that change the settings **server-wide** (i.e. for everyone!) - Note: these might be dangerous! so use them carefully + * `sun-damage` + * `durability-loss` + * `blood-drain` + * `cooldowns` + * `build-costs` + * `all-progression-unlocked` + * `play-invul` + * `day-night-cycle`: This pauses the Day/Night cycle completely (time stops moving forward) + * `npc-movement` + * `building-area-restrictions`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `all-waypoints-unlocked` + * `aggro` + * `death-sequence-instead-of-ragdolls` + * `drops`: Be extra careful using this one, it'll remove all current drops on the floor AND anything, anyone drops to the floor will be deleted from the game too! + * `tutorial-popups` + * `building-placement-restrictions`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `3d-height`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `tile-collision`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `dynamic-collision`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `building-replacement`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `dynamic-clouds` + * `hit-effects` + * `high-castle-roofs` + * `feed-at-any-hp`: Allows you to feed on npcs, regardless of their hp (i.e. they no longer have to be low health to feed) + * `linn-castle-roofs` + * `free-building-placement` + * `building-floor-territory` + * `building-debugging` + * `bat-sun-damage` + * `castle-heart-blood-ess` + * `castle-limits`: This allows anyone in the server to place more than the server-config defined limit of castle hearts + +
+ +
+Configuration Options + +* Enable/disable server-wide announcing when a player is renamed +* Enable/disable server-wide announcing when time is being skipped to the next blood moon +* Enable/disable the option to allow server-wide announcing of the time until next blood moon +* Enable/disable server-wide announcing when any of the `global-...` options are changed +* Enable/disable server-wide announcing when a player's privileges have been changed +* Enable/disable of the entire mute system +* Enable/disable the ability for players with the Moderator privilege to mute/unmute other players +* Enable/disable server-wide announcing when a player gets muted +* Enable/disable server-wide announcing when a player gets unmuted + +
diff --git a/Shared/Utils.cs b/Shared/Utils.cs index b1143b4..e57c50c 100644 --- a/Shared/Utils.cs +++ b/Shared/Utils.cs @@ -22,6 +22,8 @@ public static class Utils public static readonly PrefabGUID PvPProtectionBuff = new(1111481396); + public static readonly PrefabGUID BloodPotion = new(828432508); + #endregion #region Variables diff --git a/Thunderstone/GenericChatCommands/README.md b/Thunderstone/GenericChatCommands/README.md new file mode 100644 index 0000000..44c96fc --- /dev/null +++ b/Thunderstone/GenericChatCommands/README.md @@ -0,0 +1,111 @@ +# Generic Chat Commands +A server-side only mod that adds a fair amount of generic chat commands (mostly for Mods & Admins only though) and a player chat muting system. + +The player chat mute system can be used by Moderators & Admins to mute players (Admins, Super Admins and Moderators cannot be muted). +A Super Admin can grant Moderator or Admin provileges to other players (using one of the commands). + +
+List of available player commands + +* `!ping`: Tells you how much ping/latency you have. +* `!admin-level []`: Tells you the Admin Level of yourself (or the give player) + +
+ +
+List of available Moderator commands + +_Note: These commands can be made Admin-only through a config setting_ +* `!mute [global/local]`: Mutes the given player for the given number of minutes in the given chat/channel (or all chats/channels when omitted) - commands can still be used by the muted player +* `!unmute `: Unmutes the given player +* `!remaining-mute `: Tells you how many more minutes the mute for the given player will last + +
+ +
+List of available (Super)Admin commands + +* [SuperAdmin] `!set-admin-level `: Changes the given player's Admin Level to the given level +* `!ping []`: Tells you how much ping/latency you or the given player has +* `!rename [] `: Renames a given player (or yourself) to a new name +* `!nxtbm [server-wide]`: Tells you (or the entire server) when the next Blood Moon will appear +* `!skiptobm`: Skips time to the next Blood Moon +* `!buff [] `: Adds the buff defined by the prefab-GUID to yourself (or the given player) +* `!unbuff [] `: Removes the buff defined by the prefab-GUID to yourself (or the given player) +* `!health [] `: Sets the Health of yourself (or the given player) to the given percentage +* `!complete-all-achievements []`: Completes all achievements for yourself (or the given player) +* `!unlock-all-research []`: Unlocks all research for yourself (or the given player) +* `!unlock-all-v-blood [] `: Unlocks all V-Blood Abilities/Passives/Shapshifts or all three of these for yourself (or the given player) +* `!spawn-npc [] []`: Spawns the given amount of npcs based on their name or prefab-GUID, and they'll stay alive of the given amount of time (or untill killed when the life-time argument is omitted +* `!set-blood [] []`: Sets your (or the given player's) blood type to the specified blood-type and blood-quality, and optionally adds a given amount of blood (in Litres) +* `!blood-potion `: Creates a Blood Potion with the given Blood Type and Blood Quality +* [SuperAdmin] `!global-... [on/off]`: A set of commands that change the settings **server-wide** (i.e. for everyone!) - Note: these might be dangerous! so use them carefully + * `sun-damage` + * `durability-loss` + * `blood-drain` + * `cooldowns` + * `build-costs` + * `all-progression-unlocked` + * `play-invul` + * `day-night-cycle`: This pauses the Day/Night cycle completely (time stops moving forward) + * `npc-movement` + * `building-area-restrictions`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `all-waypoints-unlocked` + * `aggro` + * `death-sequence-instead-of-ragdolls` + * `drops`: Be extra careful using this one, it'll remove all current drops on the floor AND anything, anyone drops to the floor will be deleted from the game too! + * `tutorial-popups` + * `building-placement-restrictions`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `3d-height`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `tile-collision`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `dynamic-collision`: Be careful using this one, it might cause clipping through objects and/or the world which results in players getting stuck. + * `building-replacement`: Be careful using this one, it might cause ruins, vegitation and others objects to spawn in player's bases. + * `dynamic-clouds` + * `hit-effects` + * `high-castle-roofs` + * `feed-at-any-hp`: Allows you to feed on npcs, regardless of their hp (i.e. they no longer have to be low health to feed) + * `linn-castle-roofs` + * `free-building-placement` + * `building-floor-territory` + * `building-debugging` + * `bat-sun-damage` + * `castle-heart-blood-ess` + * `castle-limits`: This allows anyone in the server to place more than the server-config defined limit of castle hearts + +
+ +
+Configuration Options + +* Enable/disable server-wide announcing when a player is renamed +* Enable/disable server-wide announcing when time is being skipped to the next blood moon +* Enable/disable the option to allow server-wide announcing of the time until next blood moon +* Enable/disable server-wide announcing when any of the `global-...` options are changed +* Enable/disable server-wide announcing when a player's privileges have been changed +* Enable/disable of the entire mute system +* Enable/disable the ability for players with the Moderator privilege to mute/unmute other players +* Enable/disable server-wide announcing when a player gets muted +* Enable/disable server-wide announcing when a player gets unmuted + +
+ +## How to manually install +* Install [BepInEx](https://v-rising.thunderstore.io/package/BepInEx/BepInExPack_V_Rising/) +* Install [Wetstone](https://v-rising.thunderstore.io/package/molenzwiebel/Wetstone/) +* (Locally hosted games only) Install [ServerLaunchFix](https://v-rising.thunderstore.io/package/Mythic/ServerLaunchFix/) +* Extract the Vmods._mod-name_.dll +* Move the desired mod(s) to the `[VRising (server) folder]/BepInEx/WetstonePlugins/` +* Launch the server (or game) to auto-generate the config files +* Edit the configs as you desire (found in `[VRising (server) folder]/BepInEx/config/`) +* Reload the mods using the Wetstone commands (by default F6 for client-side mods, and/or `!reload` for server-side mods) + * If this doesn't work, or isn't enabled, restart the server/game + +## Commands +Most of the VMods come with a set of commands that can be used. To see the available commands, by default a player or admin can use `!help`. +Normal players won't see the Admin-only commands listed. +The prefix (`!`) can be changed on a per-mod basis. +To prevent spam/abuse there's also a command cooldown for non-admins, this value can also be tweaked on a per-mod basis. +Commands can also be disabled completely on a per-mod basis. + +## More Details +* [ChangeLog](https://github.com/WhiteFang5/VMods/blob/master/CHANGELOG.md#generic-chat-commands) diff --git a/Thunderstone/GenericChatCommands/manifest.json b/Thunderstone/GenericChatCommands/manifest.json new file mode 100644 index 0000000..2580901 --- /dev/null +++ b/Thunderstone/GenericChatCommands/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "VMods_Generic_Chat_Commands", + "description": "A mod that adds a fair amount of generic chat commands (mostly for Mods & Admins only though) and a player chat muting system.", + "version_number": "1.0.0", + "dependencies": [ + "BepInEx-BepInExPack_V_Rising-1.0.0", + "molenzwiebel-Wetstone-1.1.0" + ], + "website_url": "https://github.com/WhiteFang5/VMods#generic-chat-commands" +} diff --git a/VMods.sln b/VMods.sln index 985f2bd..9c42292 100644 --- a/VMods.sln +++ b/VMods.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PvPLeaderboard", "PvPLeader EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChestPvPProtection", "ChestPvPProtection\ChestPvPProtection.csproj", "{081DA46E-5AAB-4825-89E8-8B167988CC20}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericChatCommands", "GenericChatCommands\GenericChatCommands.csproj", "{D3A41E94-FC9F-4678-B680-41BA151B9E4A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {081DA46E-5AAB-4825-89E8-8B167988CC20}.Debug|Any CPU.Build.0 = Debug|Any CPU {081DA46E-5AAB-4825-89E8-8B167988CC20}.Release|Any CPU.ActiveCfg = Release|Any CPU {081DA46E-5AAB-4825-89E8-8B167988CC20}.Release|Any CPU.Build.0 = Release|Any CPU + {D3A41E94-FC9F-4678-B680-41BA151B9E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3A41E94-FC9F-4678-B680-41BA151B9E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3A41E94-FC9F-4678-B680-41BA151B9E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3A41E94-FC9F-4678-B680-41BA151B9E4A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE