diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae1859..83a1846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,3 +63,7 @@ ### v1.0.0 * Initial release + +## PvE Leaderboard +### v1.0.0 +* Initial release diff --git a/PvELeaderboard/Configs/PvELeaderboardConfig.cs b/PvELeaderboard/Configs/PvELeaderboardConfig.cs new file mode 100644 index 0000000..734ca62 --- /dev/null +++ b/PvELeaderboard/Configs/PvELeaderboardConfig.cs @@ -0,0 +1,24 @@ +using BepInEx.Configuration; + +namespace VMods.PvELeaderboard +{ + public static class PvELeaderboardConfig + { + #region Properties + + public static ConfigEntry PvELeaderboardEnabled { get; private set; } + public static ConfigEntry PvELeaderboardLevelDifference { get; private set; } + + #endregion + + #region Public Methods + + public static void Initialize(ConfigFile config) + { + PvELeaderboardEnabled = config.Bind(nameof(PvELeaderboardConfig), nameof(PvELeaderboardEnabled), false, "Enabled/disable the PvE Leaderboard system."); + PvELeaderboardLevelDifference = config.Bind(nameof(PvELeaderboardConfig), nameof(PvELeaderboardLevelDifference), 10, "The level difference at which the K/D isn't counting anymore for the leaderboard."); + } + + #endregion + } +} diff --git a/PvELeaderboard/Plugin.cs b/PvELeaderboard/Plugin.cs new file mode 100644 index 0000000..b7f5a99 --- /dev/null +++ b/PvELeaderboard/Plugin.cs @@ -0,0 +1,64 @@ +using BepInEx; +using BepInEx.IL2CPP; +using HarmonyLib; +using System.Reflection; +using VMods.Shared; +using Wetstone.API; + +namespace VMods.PvELeaderboard +{ + [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); + HighestGearScoreSystemConfig.Initialize(Config); + PvELeaderboardConfig.Initialize(Config); + + CommandSystem.Initialize(); + HighestGearScoreSystem.Initialize(); + PvELeaderboardSystem.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(); + PvELeaderboardSystem.Deinitialize(); + HighestGearScoreSystem.Deinitialize(); + CommandSystem.Deinitialize(); + Config.Clear(); + Utils.Deinitialize(); + return true; + } + + #endregion + } +} diff --git a/PvELeaderboard/PvELeaderboard.csproj b/PvELeaderboard/PvELeaderboard.csproj new file mode 100644 index 0000000..98aedcd --- /dev/null +++ b/PvELeaderboard/PvELeaderboard.csproj @@ -0,0 +1,482 @@ + + + netstandard2.1 + VMods.PvELeaderboard + VMods.PvELeaderboard + A mod that keeps track of player's PvE K/D and adds a PvE leaderboard + 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/PvELeaderboard/Systems/PvELeaderboardSystem.cs b/PvELeaderboard/Systems/PvELeaderboardSystem.cs new file mode 100644 index 0000000..751f90b --- /dev/null +++ b/PvELeaderboard/Systems/PvELeaderboardSystem.cs @@ -0,0 +1,544 @@ +using ProjectM; +using ProjectM.Network; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Unity.Entities; +using VMods.Shared; +using Wetstone.API; + +namespace VMods.PvELeaderboard +{ + public static class PvELeaderboardSystem + { + #region Consts + + private const string PvELeaderboardFileName = "PvELeaderboard.json"; + + private static readonly List TrackedFactions = new() + { + FactionEnum.Wolves, + FactionEnum.Undead, + FactionEnum.Militia, + FactionEnum.ChurchOfLum, + FactionEnum.Prisoners, + FactionEnum.Bandits, + FactionEnum.Bear, + FactionEnum.Plants, + FactionEnum.Harpy, + FactionEnum.Critters, + FactionEnum.Werewolves, + FactionEnum.NatureSpirit, + FactionEnum.Spiders, + FactionEnum.VampireHunters, + FactionEnum.Cursed, + FactionEnum.Ashfolk, + FactionEnum.Elementals, + }; + + private static readonly List TrackedBloodTypes = new() + { + BloodType.Creature, + BloodType.Warrior, + BloodType.Rogue, + BloodType.Brute, + BloodType.Scholar, + BloodType.Worker, + BloodType.VBlood, + }; + + #endregion + + #region Variables + + private static Dictionary _pveStats; + + #endregion + + #region Public Methods + + public static void Initialize() + { + _pveStats = VModStorage.Load(PvELeaderboardFileName, () => new Dictionary()); + + VModStorage.SaveEvent += Save; + VampireDownedHook.VampireDownedByMonsterEvent += OnVampireDowned; + DeathHook.DeathEvent += OnDeath; + } + + public static void Deinitialize() + { + DeathHook.DeathEvent -= OnDeath; + VampireDownedHook.VampireDownedByMonsterEvent -= OnVampireDowned; + VModStorage.SaveEvent -= Save; + } + + public static void Save() + { + VModStorage.Save(PvELeaderboardFileName, _pveStats); + } + + #endregion + + #region Private Methods + + private static void OnDeath(DeathEvent deathEvent) + { + var entityManager = VWorld.Server.EntityManager; + + if(!PvELeaderboardConfig.PvELeaderboardEnabled.Value || + !entityManager.HasComponent(deathEvent.Killer) || + !entityManager.HasComponent(deathEvent.Died) || + !entityManager.HasComponent(deathEvent.Died)) + { + return; + } + + VModCharacter killerVModCharacter = new VModCharacter(deathEvent.Killer, entityManager); + float killerLevel = HighestGearScoreSystem.GetCurrentOrHighestGearScore(killerVModCharacter); + + var victim = deathEvent.Died; + var victimUnitLevel = entityManager.GetComponentData(victim); + float victimLevel = victimUnitLevel.Level; + var victimFaction = entityManager.GetComponentData(victim).FactionGuid.Value.ToFactionEnum(); + BloodType? victimBloodType = null; + if(entityManager.HasComponent(victim)) + { + var bloodconsumeSource = entityManager.GetComponentData(victim); + var bloodType = bloodconsumeSource.UnitBloodType.ToBloodType(); + if(bloodType.HasValue && TrackedBloodTypes.Contains(bloodType.Value)) + { + victimBloodType = bloodType; + } + } + + if(!TrackedFactions.Exists(x => victimFaction.HasFlag(x))) + { + return; + } + + var diff = killerLevel - victimLevel; + if(diff >= PvELeaderboardConfig.PvELeaderboardLevelDifference.Value) + { + return; + } + + if(!_pveStats.TryGetValue(killerVModCharacter.PlatformId, out var killerPvEStats)) + { + killerPvEStats = new PvEStats(); + _pveStats.Add(killerVModCharacter.PlatformId, killerPvEStats); + } + + killerPvEStats.AddKill((ulong)victimLevel, victimFaction, victimBloodType); + } + + private static void OnVampireDowned(Entity killer, Entity victim) + { + if(!PvELeaderboardConfig.PvELeaderboardEnabled.Value) + { + return; + } + var entityManager = VWorld.Server.EntityManager; + + if(!entityManager.HasComponent(killer)) + { + return; + } + + var killerUnitLevel = entityManager.GetComponentData(killer); + float killerLevel = killerUnitLevel.Level; + var killerFaction = entityManager.GetComponentData(killer).FactionGuid.Value.ToFactionEnum(); + BloodType? killerBloodType = null; + if(entityManager.HasComponent(killer)) + { + var bloodConsumeSource = entityManager.GetComponentData(killer); + var bloodType = bloodConsumeSource.UnitBloodType.ToBloodType(); + if(bloodType.HasValue && TrackedBloodTypes.Contains(bloodType.Value)) + { + killerBloodType = bloodType; + } + } + + if(!TrackedFactions.Exists(x => killerFaction.HasFlag(x))) + { + return; + } + + Entity victimUserEntity = entityManager.GetComponentData(victim).UserEntity._Entity; + var victimUser = entityManager.GetComponentData(victimUserEntity); + ulong victimSteamID = victimUser.PlatformId; + float victimLevel = HighestGearScoreSystem.GetCurrentOrHighestGearScore(new FromCharacter() + { + User = victimUserEntity, + Character = victim, + }); + + var diff = killerLevel - victimLevel; + if(diff >= PvELeaderboardConfig.PvELeaderboardLevelDifference.Value) + { + return; + } + + if(!_pveStats.TryGetValue(victimSteamID, out var victimPvEStats)) + { + victimPvEStats = new PvEStats(); + _pveStats.Add(victimSteamID, victimPvEStats); + } + + victimPvEStats.AddDeath(killerFaction, killerBloodType); + } + + [Command("pvestats,pve", "pvestats [] [//all/overall]", "Shows your current PvE stats (kills, deaths, K/D ratio & lvl kills) for the given faction, blood-type, the overall or all of them together.")] + private static void OnPvEStatsCommand(Command command) + { + var entityManager = VWorld.Server.EntityManager; + (var searchUsername, var vmodCharacter) = command.FindVModCharacter(argIdx: command.Args.Length - 2, entityManager: entityManager); + + if(vmodCharacter.HasValue) + { + var user = vmodCharacter.Value.User; + if(!_pveStats.TryGetValue(user.PlatformId, out var pveStats)) + { + pveStats = new PvEStats(); + _pveStats[user.PlatformId] = pveStats; + } + + if(command.Args.Length >= 1) + { + var searchLeaderboard = command.Args[command.Args.Length switch + { + 1 => 0, + _ => 1, + }]; + switch(searchLeaderboard) + { + case "all": + { + SendKDStats(pveStats.OverallKDStats, "Overall PvE", FactionEnum.None, null); + foreach(var faction in TrackedFactions) + { + if(pveStats.FactionKDStats.TryGetValue(faction, out var kdStats)) + { + SendKDStats(kdStats, $"{faction} faction", faction, null); + } + } + } + break; + + case "overall": + SendKDStats(pveStats.OverallKDStats, "Overall PvE", FactionEnum.None, null); + break; + + default: + { + if(Enum.TryParse(searchLeaderboard, true, out var faction)) + { + if(pveStats.FactionKDStats.TryGetValue(faction, out var kdStats)) + { + SendKDStats(kdStats, $"{faction}", faction, null); + } + else + { + command.VModCharacter.SendSystemMessage($"{searchUsername} is currently unranked for the {faction} leaderboard"); + } + } + else if(Enum.TryParse(searchLeaderboard, true, out var bloodType)) + { + if(pveStats.BloodTypeKDStats.TryGetValue(bloodType, out var kdStats)) + { + SendKDStats(kdStats, $"{bloodType}", null, bloodType); + } + else + { + command.VModCharacter.SendSystemMessage($"{searchUsername} is currently unranked for the {bloodType} leaderboard"); + } + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid faction/blood-type option. Options are: {string.Join(", ", TrackedFactions)}, {string.Join(", ", TrackedBloodTypes)}, all, overall"); + } + } + break; + } + } + else + { + SendKDStats(pveStats.OverallKDStats, "Overall PvE", FactionEnum.None, null); + } + + void SendKDStats(KDStats kdStats, string name, FactionEnum? lbFaction, BloodType? lbBloodType) + { + command.VModCharacter.SendSystemMessage($"{searchUsername} {name} K/D: {kdStats.KDRatio} [{kdStats.Kills}/{kdStats.Deaths}] - Lvl Kills: {kdStats.LevelKills} - Rank {GetLeaderboard(lbFaction, lbBloodType).ToList().FindIndex(x => x.platformId == user.PlatformId) + 1} - Lvl Kills Rank {GetLvlKillsLeaderboard(lbFaction, lbBloodType).ToList().FindIndex(x => x.platformId == user.PlatformId) + 1}"); + } + } + command.Use(); + } + + [Command("pvelb,pveleaderboard", "pvelb [] []", "Shows the 5 players on the requested page of the given faction, blood-type or the overall PvE leaderboard (or top 5 if no page is given).")] + private static void OnPvELeaderboardCommand(Command command) + { + OnPvELeaderboardCommand(command, GetLeaderboard, string.Empty); + } + + [Command("pvelklb,pvelvlkillsleaderboard", "pvelklb [] []", "Shows the 5 players on the requested page of the given faction, blood-type or the overall PvE leaderboard (or top 5 if no page is given) sorted by the Lvl Kills stat.")] + private static void OnPvELvlKillsLeaderboardCommand(Command command) + { + OnPvELeaderboardCommand(command, GetLvlKillsLeaderboard, " (Lvl Kills)"); + } + + private static void OnPvELeaderboardCommand(Command command, Func> getLeaderboardMethod, string leaderboardNameSuffix) + { + int argCount = command.Args.Length; + int argIdx = 0; + FactionEnum? lbFaction = null; + BloodType? lbBloodType = null; + string leaderboardName = null; + if(command.Args.Length >= 1) + { + var firstArg = command.Args[argIdx]; + bool hasTwoArgs = argCount >= 2; + if(hasTwoArgs || (argCount == 1 && !int.TryParse(firstArg, out _))) + { + if(Enum.TryParse(firstArg, true, out var faction)) + { + lbFaction = faction; + leaderboardName = faction.ToString(); + } + else if(Enum.TryParse(firstArg, true, out var bloodType)) + { + lbBloodType = bloodType; + leaderboardName = bloodType.ToString(); + } + else + { + command.VModCharacter.SendSystemMessage($"Invalid faction/blood-type option. Options are: {string.Join(", ", TrackedFactions)}, {string.Join(", ", TrackedBloodTypes)}"); + } + argIdx = Math.Min(argIdx + 1, command.Args.Length - 1); + } + else + { + lbFaction = FactionEnum.None; + leaderboardName = "Overall"; + } + } + else + { + lbFaction = FactionEnum.None; + leaderboardName = "Overall"; + } + + if(lbFaction.HasValue || lbBloodType.HasValue) + { + var leaderboard = getLeaderboardMethod(lbFaction, lbBloodType); + + int page = 0; + if(argCount >= 1 && int.TryParse(command.Args[argIdx], out page)) + { + page -= 1; + } + + var recordsPerPage = 5; + + var maxPage = (int)Math.Ceiling(leaderboard.Count() / (double)recordsPerPage); + page = Math.Min(maxPage - 1, page); + + var vmodCharacter = command.VModCharacter; + var entityManager = VWorld.Server.EntityManager; + var visibleLeaderboard = leaderboard.Skip(page * recordsPerPage).Take(recordsPerPage); + vmodCharacter.SendSystemMessage($"===== {leaderboardName} PvE Leaderboard{leaderboardNameSuffix} ====="); + int rank = (page * recordsPerPage) + 1; + foreach((var platformId, var kdStats) in visibleLeaderboard) + { + vmodCharacter.SendSystemMessage($"{rank}. {Utils.GetCharacterName(platformId, entityManager)} : {kdStats.LevelKills} - K/D: {kdStats.KDRatio} [{kdStats.Kills}/{kdStats.Deaths}]"); + rank++; + } + vmodCharacter.SendSystemMessage($"============ {page + 1}/{maxPage} ============"); + } + command.Use(); + } + + private static IEnumerable<(ulong platformId, KDStats kdStats)> GetUnsortedLeaderboard(FactionEnum? faction, BloodType? bloodType) + { + return faction switch + { + null => bloodType switch + { + _ => _pveStats + .Where(x => x.Value.BloodTypeKDStats.ContainsKey(bloodType.Value)) + .Select(x => (x.Key, x.Value.BloodTypeKDStats[bloodType.Value])) + }, + FactionEnum.None => _pveStats + .Select(x => (x.Key, x.Value.OverallKDStats)), + _ => _pveStats + .Where(x => x.Value.FactionKDStats.ContainsKey(faction.Value)) + .Select(x => (x.Key, x.Value.FactionKDStats[faction.Value])), + }; + } + + private static IEnumerable<(ulong platformId, KDStats kdStats)> GetLeaderboard(FactionEnum? faction, BloodType? bloodType) + { + return GetUnsortedLeaderboard(faction, bloodType) + .OrderByDescending(x => x.kdStats.KDRatio) + .ThenByDescending(x => x.kdStats.Kills) + .ThenBy(x => x.kdStats.Deaths) + .ThenByDescending(x => x.kdStats.LevelKills); + } + + private static IEnumerable<(ulong platformId, KDStats kdStats)> GetLvlKillsLeaderboard(FactionEnum? faction, BloodType? bloodType) + { + return GetUnsortedLeaderboard(faction, bloodType) + .OrderByDescending(x => x.kdStats.LevelKills) + .ThenByDescending(x => x.kdStats.KDRatio) + .ThenByDescending(x => x.kdStats.Kills) + .ThenBy(x => x.kdStats.Deaths); + } + + #endregion + + #region Nested + + private class KDStats + { + #region Properties + + public ulong Kills { get; private set; } + public ulong Deaths { get; private set; } + public double KDRatio { get; private set; } + + public ulong LevelKills { get; private set; } + + #endregion + + #region Lifecycle + + [JsonConstructor] + public KDStats(ulong kills, ulong deaths, double kdRatio, ulong levelKills) + { + (Kills, Deaths, KDRatio, LevelKills) = (kills, deaths, kdRatio, levelKills); + + CalcKDRatio(); + } + + public KDStats() + { + Kills = 0; + Deaths = 0; + KDRatio = 1d; + LevelKills = 0; + } + + #endregion + + #region Public Methods + + public void AddKill(ulong victimLevel) + { + Kills++; + LevelKills += victimLevel; + CalcKDRatio(); + } + + public void AddDeath() + { + Deaths++; + CalcKDRatio(); + } + + #endregion + + #region Private Methods + + private void CalcKDRatio() + { + if(Deaths == 0) + { + KDRatio = Kills; + } + else + { + KDRatio = Kills / (double)Deaths; + } + } + + #endregion + } + + private class PvEStats + { + #region Properties + + public KDStats OverallKDStats { get; private set; } + + public Dictionary FactionKDStats { get; private set; } + public Dictionary BloodTypeKDStats { get; private set; } + + #endregion + + #region Lifecycle + + [JsonConstructor] + public PvEStats(KDStats overallKDStats, Dictionary factionKDStats, Dictionary bloodTypeKDStats) + { + (OverallKDStats, FactionKDStats, BloodTypeKDStats) = (overallKDStats ?? new(), factionKDStats ?? new(), bloodTypeKDStats ?? new()); + } + + public PvEStats() + { + OverallKDStats = new(); + FactionKDStats = new(); + BloodTypeKDStats = new(); + } + + #endregion + + #region Public Methods + + public void AddKill(ulong victimLevel, FactionEnum victimFaction, BloodType? bloodType) + { + OverallKDStats.AddKill(victimLevel); + GetOrCreateFactionKDStats(victimFaction).AddKill(victimLevel); + GetOrCreateBloodTypeKDStats(bloodType)?.AddKill(victimLevel); + } + + public void AddDeath(FactionEnum killerFaction, BloodType? killerBloodType) + { + OverallKDStats.AddDeath(); + GetOrCreateFactionKDStats(killerFaction).AddDeath(); + GetOrCreateBloodTypeKDStats(killerBloodType)?.AddDeath(); + } + + #endregion + + #region Private Methods + + private KDStats GetOrCreateFactionKDStats(FactionEnum faction) + { + if(!FactionKDStats.TryGetValue(faction, out var kdStats)) + { + kdStats = new(); + FactionKDStats.Add(faction, kdStats); + } + return kdStats; + } + + private KDStats GetOrCreateBloodTypeKDStats(BloodType? bloodType) + { + if(!bloodType.HasValue) + { + return null; + } + if(!BloodTypeKDStats.TryGetValue(bloodType.Value, out var kdStats)) + { + kdStats = new(); + BloodTypeKDStats.Add(bloodType.Value, kdStats); + } + return kdStats; + } + + #endregion + } + + #endregion + } +} diff --git a/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs b/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs index d74424f..aea5b84 100644 --- a/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs +++ b/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs @@ -37,12 +37,12 @@ public static void Initialize() _pvpStats = VModStorage.Load(PvPLeaderboardFileName, () => new Dictionary()); VModStorage.SaveEvent += Save; - VampireDownedHook.VampireDownedEvent += OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent += OnVampireDowned; } public static void Deinitialize() { - VampireDownedHook.VampireDownedEvent -= OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent -= OnVampireDowned; VModStorage.SaveEvent -= Save; } diff --git a/PvPPunishment/Systems/PvPPunishmentSystem.cs b/PvPPunishment/Systems/PvPPunishmentSystem.cs index 8331299..79921de 100644 --- a/PvPPunishment/Systems/PvPPunishmentSystem.cs +++ b/PvPPunishment/Systems/PvPPunishmentSystem.cs @@ -33,14 +33,14 @@ public static void Initialize() PruneOffenses(); VModStorage.SaveEvent += Save; - VampireDownedHook.VampireDownedEvent += OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent += OnVampireDowned; BuffSystemHook.ProcessBuffEvent += OnProcessBuff; } public static void Deinitialize() { BuffSystemHook.ProcessBuffEvent -= OnProcessBuff; - VampireDownedHook.VampireDownedEvent -= OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent -= OnVampireDowned; VModStorage.SaveEvent -= Save; } diff --git a/README.md b/README.md index e492ac2..58bd701 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ VMods is a selection of Mods for V-Rising made using a common/shared codebase th * [PvP Punishment](#pvp-punishment) * [Chest PvP Protection](#chest-pvp-protection) * [Generic Chat Commands](#generic-chat-commands) +* [PvE Leaderboard](#pve-leaderboard) ## General Mod info (Applies to most mods) ### How to manually install @@ -207,3 +208,25 @@ _Note: These commands can be made Admin-only through a config setting_ * Enable/disable server-wide announcing when a player gets unmuted + +## PvE Leaderboard +A server-side only mod that keeps track of PvE Kills, Death, K/D ratio and Lvl Kills of players and ranks them in multiple leaderboards. + +There are leaderboards for each enemy faction, blood type and an overall leaderboard. +Additionally when killing an enemy, the level of that enemy is added to your "Lvl Kills" score. + +This mod also has the option to exclude low-level kills from counting towards the K/D of the leaderboard(s). +There's also an option to prevent cheesing this restriction where the highest gear score (in the past X minutes) is used instead of the current gear score. + +Players can see their own stats and the leaderboard itself using a command (By default: `!pvestats [//all/overall]`). +The leaderboard shows up to 5 ranks at a time and allows players to input a page number so they can "browse" the leaderboard (By default: `!pvelb [] []`). +The Lvl Kills leaderboards can also be browsed using a command (By default: `!pvelklb [] []`). + +
+Configuration Options + +* Set a Level Difference at which the K/D isn't counting anymore of the leaderboard. +* Enable/disable usage of the anti-cheesing system (highest gear score tracking) +* Change the amount of time the highest gear score is remembered/tracked + +
diff --git a/Shared/FactionEnumExtensions.cs b/Shared/FactionEnumExtensions.cs new file mode 100644 index 0000000..3f237ef --- /dev/null +++ b/Shared/FactionEnumExtensions.cs @@ -0,0 +1,59 @@ +using ProjectM; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VMods.Shared +{ + public static class FactionEnumExtensions + { + #region Consts + + private static readonly Dictionary PrefabGUIDToFactionEnumMapping = new() + { + [new PrefabGUID(-1632475814)] = FactionEnum.Ashfolk, + [new PrefabGUID(-413163549)] = FactionEnum.Bandits, + [new PrefabGUID(1344481611)] = FactionEnum.Bear, + [new PrefabGUID(1094603131)] = FactionEnum.ChurchOfLum, + [new PrefabGUID(2395673)] = FactionEnum.ChurchOfLum | FactionEnum.Spot_ShapeshiftHuman, + [new PrefabGUID(10678632)] = FactionEnum.Critters, + [new PrefabGUID(1522496317)] = FactionEnum.Cursed, + [new PrefabGUID(1513046884)] = FactionEnum.Elementals, + [new PrefabGUID(1731533561)] = FactionEnum.Harpy, + [new PrefabGUID(-1430861195)] = FactionEnum.Ignored, + [new PrefabGUID(1057375699)] = FactionEnum.Militia, + [new PrefabGUID(1597367490)] = FactionEnum.NatureSpirit, + [new PrefabGUID(-1414061934)] = FactionEnum.Plants, + [new PrefabGUID(1106458752)] = FactionEnum.Players, + [new PrefabGUID(-394968526)] = FactionEnum.PlayerCastlePrisoner, + [new PrefabGUID(-1036907707)] = FactionEnum.Players_ShapeshiftHuman, + [new PrefabGUID(-1632009503)] = FactionEnum.Spiders, + //[new PrefabGUID(887347866)] = FactionEnum.None,//Faction_Traders + [new PrefabGUID(929074293)] = FactionEnum.Undead, + [new PrefabGUID(2120169232)] = FactionEnum.VampireHunters, + [new PrefabGUID(-2024618997)] = FactionEnum.Werewolves, + [new PrefabGUID(-1671358863)] = FactionEnum.Wolves, + [new PrefabGUID(1977351396)] = FactionEnum.Prisoners, + }; + + #endregion + + #region Public Methods + + public static FactionEnum ToFactionEnum(this PrefabGUID prefabGUID) + { + if(PrefabGUIDToFactionEnumMapping.TryGetValue(prefabGUID, out var faction)) + { + return faction; + } + return FactionEnum.None; + } + + public static FactionEnum[] Split(this FactionEnum factions) + { + return ((FactionEnum[])Enum.GetValues(typeof(FactionEnum))).Where(x => x != FactionEnum.None && factions.HasFlag(x)).ToArray(); + } + + #endregion + } +} diff --git a/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs b/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs index 28c7ba8..884180f 100644 --- a/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs +++ b/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs @@ -31,12 +31,12 @@ public static void Initialize() VModStorage.SaveEvent += Save; EquipmentHooks.EquipmentChangedEvent += OnEquipmentChanged; - VampireDownedHook.VampireDownedEvent += OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent += OnVampireDowned; } public static void Deinitialize() { - VampireDownedHook.VampireDownedEvent -= OnVampireDowned; + VampireDownedHook.VampireDownedByVampireEvent -= OnVampireDowned; EquipmentHooks.EquipmentChangedEvent -= OnEquipmentChanged; VModStorage.SaveEvent -= Save; } @@ -48,6 +48,11 @@ public static void Save() VModStorage.Save(HighestGearScoreFileName, _gearScoreData); } + public static float GetCurrentOrHighestGearScore(VModCharacter vmodCharacter) + { + return GetCurrentOrHighestGearScore(vmodCharacter.FromCharacter); + } + public static float GetCurrentOrHighestGearScore(FromCharacter fromCharacter) { var entityManager = VWorld.Server.EntityManager; diff --git a/Shared/VModCharacter.cs b/Shared/VModCharacter.cs index c231a0a..6d3f911 100644 --- a/Shared/VModCharacter.cs +++ b/Shared/VModCharacter.cs @@ -65,6 +65,19 @@ public VModCharacter(Entity userEntity, Entity charEntity, EntityManager? entity AdminLevel = Utils.GetAdminLevel(FromCharacter.User, entityManager); } + public VModCharacter(Entity charEntity, EntityManager? entityManager = null) + { + entityManager ??= Utils.CurrentWorld.EntityManager; + Character = entityManager.Value.GetComponentData(charEntity); + FromCharacter = new FromCharacter() + { + User = Character.UserEntity._Entity, + Character = charEntity, + }; + User = entityManager.Value.GetComponentData(FromCharacter.User); + AdminLevel = Utils.GetAdminLevel(FromCharacter.User, entityManager); + } + #endregion #region Public Methods diff --git a/Shared/VampireDownedHook.cs b/Shared/VampireDownedHook.cs index 5f33ebf..4665229 100644 --- a/Shared/VampireDownedHook.cs +++ b/Shared/VampireDownedHook.cs @@ -11,9 +11,13 @@ public static class VampireDownedHook { #region Events - public delegate void VampireDownedEventHandler(Entity killer, Entity victim); - public static event VampireDownedEventHandler VampireDownedEvent; - private static void FireVampireDownedEvent(Entity killer, Entity victim) => VampireDownedEvent?.Invoke(killer, victim); + public delegate void VampireDownedByVampireEventHandler(Entity killer, Entity victim); + public static event VampireDownedByVampireEventHandler VampireDownedByVampireEvent; + private static void FireVampireDownedByVampireEvent(Entity killer, Entity victim) => VampireDownedByVampireEvent?.Invoke(killer, victim); + + public delegate void VampireDownedByMonsterEventHandler(Entity killer, Entity victim); + public static event VampireDownedByMonsterEventHandler VampireDownedByMonsterEvent; + private static void FireVampireDownedByMonsterEvent(Entity killer, Entity victim) => VampireDownedByMonsterEvent?.Invoke(killer, victim); #endregion @@ -37,9 +41,21 @@ private static void OnUpdate(VampireDownedServerEventSystem __instance) Entity source = entityManager.GetComponentData(entity).Source; VampireDownedServerEventSystem.TryFindRootOwner(source, 1, entityManager, out var killer); - if(entityManager.HasComponent(killer) && entityManager.HasComponent(victim) && !killer.Equals(victim)) + if(killer.Equals(victim)) + { + continue; + } + + if(entityManager.HasComponent(victim)) { - FireVampireDownedEvent(killer, victim); + if(entityManager.HasComponent(killer)) + { + FireVampireDownedByVampireEvent(killer, victim); + } + else if(entityManager.HasComponent(killer)) + { + FireVampireDownedByMonsterEvent(killer, victim); + } } } } diff --git a/Thunderstore/PvELeaderboard/README.md b/Thunderstore/PvELeaderboard/README.md new file mode 100644 index 0000000..c5a5f56 --- /dev/null +++ b/Thunderstore/PvELeaderboard/README.md @@ -0,0 +1,42 @@ +# PvE Leaderboard +A server-side only mod that keeps track of PvE Kills, Death, K/D ratio and Lvl Kills of players and ranks them in multiple leaderboards. + +There are leaderboards for each enemy faction, blood type and an overall leaderboard. +Additionally when killing an enemy, the level of that enemy is added to your "Lvl Kills" score. + +This mod also has the option to exclude low-level kills from counting towards the K/D of the leaderboard(s). +There's also an option to prevent cheesing this restriction where the highest gear score (in the past X minutes) is used instead of the current gear score. + +Players can see their own stats and the leaderboard itself using a command (By default: `!pvestats [//all/overall]`). +The leaderboard shows up to 5 ranks at a time and allows players to input a page number so they can "browse" the leaderboard (By default: `!pvelb [] []`). +The Lvl Kills leaderboards can also be browsed using a command (By default: `!pvelklb [] []`). + +
+Configuration Options + +* Set a Level Difference at which the K/D isn't counting anymore of the leaderboard. +* Enable/disable usage of the anti-cheesing system (highest gear score tracking) +* Change the amount of time the highest gear score is remembered/tracked + +
+ +## 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#pve-leaderboard) diff --git a/Thunderstore/PvELeaderboard/manifest.json b/Thunderstore/PvELeaderboard/manifest.json new file mode 100644 index 0000000..f8b91c9 --- /dev/null +++ b/Thunderstore/PvELeaderboard/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "VMods_PvE_Leaderboard", + "description": "A server-side only mod that keeps track of PvE Kills, Death, K/D ratio and Lvl Kills of players and ranks them in multiple leaderboards.", + "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#pve-leaderboard" +} diff --git a/VMods.sln b/VMods.sln index 9c42292..6c01a52 100644 --- a/VMods.sln +++ b/VMods.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChestPvPProtection", "Chest EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericChatCommands", "GenericChatCommands\GenericChatCommands.csproj", "{D3A41E94-FC9F-4678-B680-41BA151B9E4A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PvELeaderboard", "PvELeaderboard\PvELeaderboard.csproj", "{5ABC09AF-92EA-4EBA-88F9-29FD8730472C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {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 + {5ABC09AF-92EA-4EBA-88F9-29FD8730472C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ABC09AF-92EA-4EBA-88F9-29FD8730472C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ABC09AF-92EA-4EBA-88F9-29FD8730472C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ABC09AF-92EA-4EBA-88F9-29FD8730472C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE