diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fa2cb80
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,403 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+dump/*
+build/*
+zip/*
+*.zip
\ No newline at end of file
diff --git a/BloodRefill/BloodRefill.csproj b/BloodRefill/BloodRefill.csproj
new file mode 100644
index 0000000..989b97b
--- /dev/null
+++ b/BloodRefill/BloodRefill.csproj
@@ -0,0 +1,474 @@
+
+
+ netstandard2.1
+ VMods.BloodRefill
+ VMods.BloodRefill
+ Allows a player to refill his/her blood pool
+ 0.0.1
+ 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/BloodRefill/Configs/BloodRefillConfig.cs b/BloodRefill/Configs/BloodRefillConfig.cs
new file mode 100644
index 0000000..62171be
--- /dev/null
+++ b/BloodRefill/Configs/BloodRefillConfig.cs
@@ -0,0 +1,49 @@
+using BepInEx.Configuration;
+
+namespace VMods.BloodRefill
+{
+ public static class BloodRefillConfig
+ {
+ #region Properties
+
+ public static ConfigEntry BloodRefillEnabled { get; private set; }
+
+ public static ConfigEntry BloodRefillRequiresFeeding { get; private set; }
+
+ public static ConfigEntry BloodRefillRequiresSameBloodType { get; private set; }
+
+ public static ConfigEntry BloodRefillExcludeVBloodFromSameBloodTypeCheck { get; private set; }
+
+ public static ConfigEntry BloodRefillDifferentBloodTypeMultiplier { get; private set; }
+
+ public static ConfigEntry BloodRefillVBloodRefillType { get; private set; }
+
+ public static ConfigEntry BloodRefillVBloodRefillMultiplier { get; private set; }
+
+ public static ConfigEntry BloodRefillRandomRefill { get; private set; }
+
+ public static ConfigEntry BloodRefillAmount { get; private set; }
+
+ public static ConfigEntry BloodRefillMultiplier { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ BloodRefillEnabled = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillEnabled), false, "Enabled/disable the blood refilling system.");
+ BloodRefillRequiresFeeding = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillRequiresFeeding), true, "When enabled, blood can only be refilled when feeding (i.e. when aborting the feed).");
+ BloodRefillRequiresSameBloodType = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillRequiresSameBloodType), false, "When enabled, blood can only be refilled when the target has the same blood type.");
+ BloodRefillExcludeVBloodFromSameBloodTypeCheck = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillExcludeVBloodFromSameBloodTypeCheck), true, "When enabled, V-blood is excluded from the 'same blood type' check (i.e. it's always considered to be 'the same blood type' as the player's blood type).");
+ BloodRefillVBloodRefillType = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillVBloodRefillType), 2, "0 = disabled (i.e. normal refill); 1 = fully refill; 2 = refill based on V-blood monster level.");
+ BloodRefillVBloodRefillMultiplier = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillVBloodRefillMultiplier), 0.1f, $"[Only applies when {nameof(BloodRefillVBloodRefillType)} is set to 2] The multiplier used in the v-blood refill calculation ('EnemyLevel' * '{nameof(BloodRefillVBloodRefillMultiplier)}' * '{nameof(BloodRefillMultiplier)}').");
+ BloodRefillRandomRefill = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillRandomRefill), true, "When enabled, the amount of refilled blood is randomized (between 1 and the calculated refillable amount).");
+ BloodRefillAmount = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillAmount), 2.0f, "The maximum amount of blood to refill with no level difference, a matching blood type and quality (Expressed in Litres of blood).");
+ BloodRefillMultiplier = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillMultiplier), 1.0f, $"The multiplier used in the blood refill calculation. [Formula: (('Enemy Level' / 'Player Level') * ((100 - ('Player Blood Quality %' - 'Enemy Blood Quality %')) / 100)) * '{nameof(BloodRefillAmount)}' * '(If applicable) {nameof(BloodRefillDifferentBloodTypeMultiplier)}' * '{nameof(BloodRefillMultiplier)}']");
+ BloodRefillDifferentBloodTypeMultiplier = config.Bind(nameof(BloodRefillConfig), nameof(BloodRefillDifferentBloodTypeMultiplier), 0.1f, $"The multiplier used in the blood refill calculation as a penalty for feeding on a different blood type (only works when {nameof(BloodRefillRequiresSameBloodType)} is disabled).");
+ }
+
+ #endregion
+ }
+}
diff --git a/BloodRefill/Hooks/DeathHook.cs b/BloodRefill/Hooks/DeathHook.cs
new file mode 100644
index 0000000..ef437cc
--- /dev/null
+++ b/BloodRefill/Hooks/DeathHook.cs
@@ -0,0 +1,39 @@
+using HarmonyLib;
+using ProjectM;
+using Unity.Collections;
+using Wetstone.API;
+
+namespace VMods.BloodRefill
+{
+ [HarmonyPatch]
+ public static class DeathHook
+ {
+ #region Events
+
+ public delegate void DeathEventHandler(DeathEvent deathEvent);
+ public static event DeathEventHandler DeathEvent;
+ private static void FireDeathEvent(DeathEvent deathEvent) => DeathEvent?.Invoke(deathEvent);
+
+ #endregion
+
+ #region Public Methods
+
+ [HarmonyPatch(typeof(DeathEventListenerSystem), nameof(DeathEventListenerSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void OnUpdate(DeathEventListenerSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance._DeathEventQuery == null)
+ {
+ return;
+ }
+
+ NativeArray deathEvents = __instance._DeathEventQuery.ToComponentDataArray(Allocator.Temp);
+ foreach(DeathEvent deathEvent in deathEvents)
+ {
+ FireDeathEvent(deathEvent);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/BloodRefill/Plugin.cs b/BloodRefill/Plugin.cs
new file mode 100644
index 0000000..8c1ccd9
--- /dev/null
+++ b/BloodRefill/Plugin.cs
@@ -0,0 +1,59 @@
+using BepInEx;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using System.Reflection;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.BloodRefill
+{
+ [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);
+ BloodRefillConfig.Initialize(Config);
+
+ CommandSystem.Initialize();
+ BloodRefillSystem.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;
+ }
+ _hooks?.UnpatchSelf();
+ BloodRefillSystem.Deinitialize();
+ CommandSystem.Deinitialize();
+ Config.Clear();
+ Utils.Deinitialize();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/BloodRefill/Systems/BloodRefillSystem.cs b/BloodRefill/Systems/BloodRefillSystem.cs
new file mode 100644
index 0000000..0e7ff69
--- /dev/null
+++ b/BloodRefill/Systems/BloodRefillSystem.cs
@@ -0,0 +1,242 @@
+using ProjectM;
+using ProjectM.Network;
+using System;
+using System.Linq;
+using Unity.Entities;
+using VMods.Shared;
+using Wetstone.API;
+using Random = UnityEngine.Random;
+
+namespace VMods.BloodRefill
+{
+ public static class BloodRefillSystem
+ {
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ DeathHook.DeathEvent += OnDeath;
+ }
+
+ public static void Deinitialize()
+ {
+ DeathHook.DeathEvent -= OnDeath;
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void OnDeath(DeathEvent deathEvent)
+ {
+ EntityManager entityManager = VWorld.Server.EntityManager;
+
+ // Make sure a player killed an appropriate monster
+ if(!BloodRefillConfig.BloodRefillEnabled.Value ||
+ !entityManager.HasComponent(deathEvent.Killer) ||
+ !entityManager.HasComponent(deathEvent.Killer) ||
+ !entityManager.HasComponent(deathEvent.Killer) ||
+ !entityManager.HasComponent(deathEvent.Died) ||
+ !entityManager.HasComponent(deathEvent.Died) ||
+ !entityManager.HasComponent(deathEvent.Died))
+ {
+ return;
+ }
+
+ PlayerCharacter playerCharacter = entityManager.GetComponentData(deathEvent.Killer);
+ Equipment playerEquipment = entityManager.GetComponentData(deathEvent.Killer);
+ Blood playerBlood = entityManager.GetComponentData(deathEvent.Killer);
+ UnitLevel unitLevel = entityManager.GetComponentData(deathEvent.Died);
+ BloodConsumeSource bloodConsumeSource = entityManager.GetComponentData(deathEvent.Died);
+
+#if DEBUG
+ Utils.Logger.LogMessage($"DE.Killer = {deathEvent.Killer.Index}");
+ Utils.Logger.LogMessage($"DE.Died = {deathEvent.Died.Index}");
+ Utils.Logger.LogMessage($"DE.Source = {deathEvent.Source.Index}");
+#endif
+
+ Entity userEntity = playerCharacter.UserEntity._Entity;
+ User user = entityManager.GetComponentData(userEntity);
+
+ bool killedByFeeding = deathEvent.Killer.Index == deathEvent.Source.Index;
+
+ if(!playerBlood.BloodType.ParseBloodType(out BloodType playerBloodType))
+ {
+ // Invalid/unknown blood type
+ return;
+ }
+
+ if(!bloodConsumeSource.UnitBloodType.ParseBloodType(out BloodType bloodType))
+ {
+ // Invalid/unknown blood type
+ return;
+ }
+
+ bool isVBlood = bloodType == BloodType.VBlood;
+
+ // Allow V-Bloods to skip the 'killed by feeding' check, otherwise additional feeders won't get a refill.
+ if(!isVBlood && BloodRefillConfig.BloodRefillRequiresFeeding.Value && !killedByFeeding)
+ {
+ // Can only gain blood when killing the enemy while feeding (i.e. abort the feed)
+ return;
+ }
+
+ bool isSameBloodType = playerBloodType == bloodType || (BloodRefillConfig.BloodRefillExcludeVBloodFromSameBloodTypeCheck.Value && isVBlood);
+
+ if(BloodRefillConfig.BloodRefillRequiresSameBloodType.Value && !isSameBloodType)
+ {
+ // Can only gain blood when killing an enemy of the same blood type
+ return;
+ }
+
+ float bloodTypeMultiplier = isSameBloodType ? 1f : BloodRefillConfig.BloodRefillDifferentBloodTypeMultiplier.Value;
+
+ float playerLevel = playerEquipment.WeaponLevel + playerEquipment.ArmorLevel + playerEquipment.SpellLevel;
+ float enemyLevel = unitLevel.Level;
+
+#if DEBUG
+ Utils.Logger.LogMessage($"Player Blood Quality: {playerBlood.Quality}");
+ Utils.Logger.LogMessage($"Player Blood Value: {playerBlood.Value}");
+ Utils.Logger.LogMessage($"Player Level: {playerLevel}");
+
+ Utils.Logger.LogMessage($"Enemy Blood Quality: {bloodConsumeSource.BloodQuality}");
+ Utils.Logger.LogMessage($"Enemy Level {enemyLevel}");
+#endif
+
+ float levelRatio = enemyLevel / playerLevel;
+
+ float qualityRatio = (100f - (playerBlood.Quality - bloodConsumeSource.BloodQuality)) / 100f;
+
+ float refillRatio = levelRatio * qualityRatio;
+
+ // Config amount is expressed in 'Litres of blood' -> the game's formule is 'blood value / 10', hence the * 10 multiplier here.
+ float refillAmount = BloodRefillConfig.BloodRefillAmount.Value * 10f * refillRatio;
+
+ refillAmount *= bloodTypeMultiplier;
+
+#if DEBUG
+ Utils.Logger.LogMessage($"Lvl Ratio: {levelRatio}");
+ Utils.Logger.LogMessage($"Quality Ratio: {qualityRatio}");
+ Utils.Logger.LogMessage($"Refill Ratio: {refillRatio}");
+ Utils.Logger.LogMessage($"Blood Type Multiplier: {bloodTypeMultiplier}");
+ Utils.Logger.LogMessage($"Refill Amount: {refillAmount}");
+#endif
+
+ if(BloodRefillConfig.BloodRefillRandomRefill.Value)
+ {
+ refillAmount = Random.RandomRange(1f, refillAmount);
+
+#if DEBUG
+ Utils.Logger.LogMessage($"Refill Roll: {refillAmount}");
+#endif
+ }
+
+ if(isVBlood)
+ {
+ switch(BloodRefillConfig.BloodRefillVBloodRefillType.Value)
+ {
+ case 1: // V-blood fully refills the blood pool
+ refillAmount = playerBlood.MaxBlood - playerBlood.Value;
+ break;
+
+ case 2: // V-blood refills based on the unit's level
+ refillAmount = enemyLevel * BloodRefillConfig.BloodRefillVBloodRefillMultiplier.Value;
+ break;
+ }
+ }
+
+ refillAmount *= BloodRefillConfig.BloodRefillMultiplier.Value;
+
+ if(refillAmount > 0f)
+ {
+ int roundedRefillAmount = (int)Math.Ceiling(refillAmount);
+
+ if(roundedRefillAmount > 0)
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"New Blood Amount: {playerBlood.Value + roundedRefillAmount}");
+#endif
+
+ float newTotalBlood = Math.Min(playerBlood.MaxBlood, playerBlood.Value + roundedRefillAmount);
+ float actualBloodGained = newTotalBlood - playerBlood.Value;
+ float refillAmountInLitres = (int)(actualBloodGained * 10f) / 100f;
+ float newTotalBloodInLitres = (int)Math.Round(newTotalBlood) / 10f;
+ Utils.SendMessage(userEntity, $"+{refillAmountInLitres}L Blood ({newTotalBloodInLitres}L)", ServerChatMessageType.Lore);
+
+ ChangeBloodType(user, playerBloodType, playerBlood.Quality, roundedRefillAmount);
+ return;
+ }
+ }
+
+ Utils.SendMessage(userEntity, $"No blood gained from the enemy.", ServerChatMessageType.Lore);
+ }
+
+ private static void ChangeBloodType(User user, BloodType bloodType, float quality, int addAmount)
+ {
+ ChangeBloodDebugEvent bloodChangeEvent = new()
+ {
+ Source = bloodType.ToPrefabGUID(),
+ Quality = quality,
+ Amount = addAmount,
+ };
+ VWorld.Server.GetExistingSystem().ChangeBloodEvent(user.Index, ref bloodChangeEvent);
+ }
+
+ [Command("setblood", "setblood []", "Sets your blood type to the specified blood-type and blood-quality, and optionally adds a given amount of blood (in Litres).", true)]
+ private static void OnSetBloodCommand(Command command)
+ {
+ var user = command.User;
+ var argCount = command.Args.Length;
+ if(argCount >= 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)
+ {
+ float? addBloodAmount = null;
+ if(argCount >= 3)
+ {
+ var searchLitres = command.Args[2];
+ if(float.TryParse(searchLitres.Replace("L", string.Empty), out float parsedAddBloodAmount) && parsedAddBloodAmount >= -10f && parsedAddBloodAmount <= 10f)
+ {
+ addBloodAmount = parsedAddBloodAmount;
+ }
+ else
+ {
+ user.SendSystemMessage($"Invalid gain-amount '{searchBloodQuality}'. Should be between -10 and 10");
+ }
+ }
+ else
+ {
+ addBloodAmount = 10f;
+ }
+
+ if(addBloodAmount.HasValue)
+ {
+ ChangeBloodType(user, bloodType, bloodQuality, (int)(addBloodAmount.Value * 10f));
+ user.SendSystemMessage($"Changed blood type to {bloodQuality}% {searchBloodType} and added {addBloodAmount.Value}L");
+ }
+ }
+ else
+ {
+ user.SendSystemMessage($"Invalid blood-quality '{searchBloodQuality}'. Should be between 1 and 100");
+ }
+ }
+ else
+ {
+ user.SendSystemMessage($"Invalid blood-type '{searchBloodType}'. Options are: {string.Join(", ", validBloodTypes.Select(x => x.ToString()))}");
+ }
+ }
+ else
+ {
+ CommandSystem.SendInvalidCommandMessage(command);
+ }
+ command.Use();
+ }
+
+ #endregion
+ }
+}
diff --git a/BloodRefill/Systems/BloodType.cs b/BloodRefill/Systems/BloodType.cs
new file mode 100644
index 0000000..1ca1aa2
--- /dev/null
+++ b/BloodRefill/Systems/BloodType.cs
@@ -0,0 +1,63 @@
+using ProjectM;
+using System;
+using System.Collections.Generic;
+
+namespace VMods.BloodRefill
+{
+ public enum BloodType
+ {
+ Frailed = -899826404,
+ Creature = -77658840,
+ Warrior = -1094467405,
+ Rogue = 793735874,
+ Brute = 581377887,
+ Scholar = -586506765,
+ Worker = -540707191,
+ VBlood = 1557174542,
+ }
+
+ public static class BloodTypeExtensions
+ {
+ #region Consts
+
+ public static readonly Dictionary BloodTypeToPrefabGUIDMapping = new()
+ {
+ [BloodType.Creature] = new PrefabGUID(1897056612),
+ [BloodType.Warrior] = new PrefabGUID(-1128238456),
+ [BloodType.Rogue] = new PrefabGUID(-1030822544),
+ [BloodType.Brute] = new PrefabGUID(-1464869978),
+ [BloodType.Scholar] = new PrefabGUID(-700632469),
+ [BloodType.Worker] = new PrefabGUID(-1342764880),
+ };
+
+ #endregion
+
+ #region Public Methods
+
+ public static bool ParseBloodType(this PrefabGUID prefabGUID, out BloodType bloodType)
+ {
+ int guidHash = prefabGUID.GuidHash;
+ if(!Enum.IsDefined(typeof(BloodType), guidHash))
+ {
+ bloodType = BloodType.Frailed;
+ return false;
+ }
+ bloodType = (BloodType)guidHash;
+ return true;
+ }
+
+ public static BloodType? ToBloodType(this PrefabGUID prefabGUID)
+ {
+ int guidHash = prefabGUID.GuidHash;
+ if(!Enum.IsDefined(typeof(BloodType), guidHash))
+ {
+ return null;
+ }
+ return (BloodType)guidHash;
+ }
+
+ public static PrefabGUID ToPrefabGUID(this BloodType bloodType) => BloodTypeToPrefabGUIDMapping[bloodType];
+
+ #endregion
+ }
+}
diff --git a/ExperimentalMod/ExperimentalMod.csproj b/ExperimentalMod/ExperimentalMod.csproj
new file mode 100644
index 0000000..84646f9
--- /dev/null
+++ b/ExperimentalMod/ExperimentalMod.csproj
@@ -0,0 +1,475 @@
+
+
+ netstandard2.1
+ VMods.ExperimentalMod
+ VMods.ExperimentalMod
+ A mod that only contains experimental/debug code in order to further develop other mods
+ 0.0.1
+ 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/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..8f0078f
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PvPLeaderboard/Configs/PvPLeaderboardConfig.cs b/PvPLeaderboard/Configs/PvPLeaderboardConfig.cs
new file mode 100644
index 0000000..26ca912
--- /dev/null
+++ b/PvPLeaderboard/Configs/PvPLeaderboardConfig.cs
@@ -0,0 +1,28 @@
+using BepInEx.Configuration;
+
+namespace VMods.PvPLeaderboard
+{
+ public static class PvPLeaderboardConfig
+ {
+ #region Properties
+
+ public static ConfigEntry PvPLeaderboardEnabled { get; private set; }
+ public static ConfigEntry PvPLeaderboardLevelDifference { get; private set; }
+ public static ConfigEntry PvPLeaderboardAnnounceKill { get; private set; }
+ public static ConfigEntry PvPLeaderboardAnnounceLowLevelKill { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ PvPLeaderboardEnabled = config.Bind(nameof(PvPLeaderboardConfig), nameof(PvPLeaderboardEnabled), false, "Enabled/disable the PvP Leaderboard system.");
+ PvPLeaderboardLevelDifference = config.Bind(nameof(PvPLeaderboardConfig), nameof(PvPLeaderboardLevelDifference), 10, "The level difference at which the K/D isn't counting anymore for the leaderboard.");
+ PvPLeaderboardAnnounceKill = config.Bind(nameof(PvPLeaderboardConfig), nameof(PvPLeaderboardAnnounceKill), true, "When enabled, a legitemate kill is announced server-wide.");
+ PvPLeaderboardAnnounceLowLevelKill = config.Bind(nameof(PvPLeaderboardConfig), nameof(PvPLeaderboardAnnounceLowLevelKill), false, "When enabled, a kill of a lower level player is announced server-wide.");
+ }
+
+ #endregion
+ }
+}
diff --git a/PvPLeaderboard/Plugin.cs b/PvPLeaderboard/Plugin.cs
new file mode 100644
index 0000000..8eb34e4
--- /dev/null
+++ b/PvPLeaderboard/Plugin.cs
@@ -0,0 +1,64 @@
+using BepInEx;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using System.Reflection;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.PvPLeaderboard
+{
+ [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);
+ PvPLeaderboardConfig.Initialize(Config);
+
+ CommandSystem.Initialize();
+ HighestGearScoreSystem.Initialize();
+ PvPLeaderboardSystem.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();
+ PvPLeaderboardSystem.Deinitialize();
+ HighestGearScoreSystem.Deinitialize();
+ CommandSystem.Deinitialize();
+ Config.Clear();
+ Utils.Deinitialize();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/PvPLeaderboard/PvPLeaderboard.csproj b/PvPLeaderboard/PvPLeaderboard.csproj
new file mode 100644
index 0000000..9d4cb71
--- /dev/null
+++ b/PvPLeaderboard/PvPLeaderboard.csproj
@@ -0,0 +1,477 @@
+
+
+ netstandard2.1
+ VMods.PvPLeaderboard
+ VMods.PvPLeaderboard
+ A mod that keeps track of player's K/D and adds a pvp leaderboard
+ 0.0.1
+ 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/PvPLeaderboard/Shared/CommandExtensions.cs b/PvPLeaderboard/Shared/CommandExtensions.cs
new file mode 100644
index 0000000..1e8424e
--- /dev/null
+++ b/PvPLeaderboard/Shared/CommandExtensions.cs
@@ -0,0 +1,38 @@
+using ProjectM.Network;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ public static class CommandExtensions
+ {
+ public static (string searchUsername, FromCharacter? fromCharacter) GetFromCharacter(this Command command, int argIdx = 0, bool sendCannotBeFoundMessage = true, EntityManager? entityManager = null)
+ {
+ FromCharacter? fromCharacter;
+ string searchUsername;
+
+ entityManager ??= Utils.CurrentWorld.EntityManager;
+
+ if(argIdx >= 0 && command.Args.Length >= (argIdx + 1))
+ {
+ searchUsername = command.Args[0];
+ fromCharacter = Utils.GetFromCharacter(searchUsername, entityManager);
+ }
+ else
+ {
+ searchUsername = command.User.CharacterName.ToString();
+ fromCharacter = new FromCharacter()
+ {
+ User = command.SenderUserEntity,
+ Character = command.SenderCharEntity,
+ };
+ }
+
+ if(sendCannotBeFoundMessage && !fromCharacter.HasValue)
+ {
+ command.User.SendSystemMessage($"Vampire {searchUsername} couldn't be found.");
+ }
+ return (searchUsername, fromCharacter);
+ }
+ }
+}
diff --git a/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs b/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs
new file mode 100644
index 0000000..8a844d9
--- /dev/null
+++ b/PvPLeaderboard/Systems/PvPLeaderboardSystem.cs
@@ -0,0 +1,227 @@
+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.PvPLeaderboard
+{
+ public static class PvPLeaderboardSystem
+ {
+ #region Consts
+
+ private const string PvPPunishmentFileName = "PvPLeaderboard.json";
+
+ #endregion
+
+ #region Variables
+
+ private static Dictionary _pvpStats;
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ _pvpStats = VModStorage.Load(PvPPunishmentFileName, () => new Dictionary());
+
+ VModStorage.SaveEvent += Save;
+ VampireDownedHook.VampireDownedEvent += OnVampireDowned;
+ }
+
+ public static void Deinitialize()
+ {
+ VampireDownedHook.VampireDownedEvent -= OnVampireDowned;
+ VModStorage.SaveEvent -= Save;
+ }
+
+ public static void Save()
+ {
+ VModStorage.Save(PvPPunishmentFileName, _pvpStats);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void OnVampireDowned(Entity killer, Entity victim)
+ {
+ if(!PvPLeaderboardConfig.PvPLeaderboardEnabled.Value)
+ {
+ return;
+ }
+ var entityManager = VWorld.Server.EntityManager;
+
+ Entity killerUserEntity = entityManager.GetComponentData(killer).UserEntity._Entity;
+ var killerUser = entityManager.GetComponentData(killerUserEntity);
+ ulong killerSteamID = killerUser.PlatformId;
+ float killerLevel = HighestGearScoreSystem.GetCurrentOrHighestGearScore(new FromCharacter()
+ {
+ User = killerUserEntity,
+ Character = killer,
+ });
+
+ 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 DEBUG
+ var msg = $"{killerUser.CharacterName} ({killerSteamID}) [Lv: {killerLevel}] killed {victimUser.CharacterName} ({victimSteamID}) [Lv: {victimLevel}]. - Diff: {diff}";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+
+ if(diff >= PvPLeaderboardConfig.PvPLeaderboardLevelDifference.Value)
+ {
+ Utils.Logger.LogMessage($"Vampire {killerUser.CharacterName} (Lv: {killerLevel}; Current Lv: {HighestGearScoreSystem.GetCurrentGearScore(killer, entityManager)}) has grief-killed{victimUser.CharacterName} (Lv {victimLevel}; Current Lv: {HighestGearScoreSystem.GetCurrentGearScore(killer, entityManager)})!");
+ if(PvPLeaderboardConfig.PvPLeaderboardAnnounceLowLevelKill.Value)
+ {
+ ServerChatUtils.SendSystemMessageToAllClients(entityManager, $"Vampire {killerUser.CharacterName} (Lv {killerLevel}) has grief-killed {victimUser.CharacterName} (Lv {victimLevel})!");
+ }
+ return;
+ }
+
+ if(!_pvpStats.TryGetValue(killerSteamID, out var killerPvPStats))
+ {
+ killerPvPStats = new PvPStats();
+ _pvpStats.Add(killerSteamID, killerPvPStats);
+ }
+ if(!_pvpStats.TryGetValue(victimSteamID, out var victimPvPStats))
+ {
+ victimPvPStats = new PvPStats();
+ _pvpStats.Add(victimSteamID, victimPvPStats);
+ }
+
+ killerPvPStats.AddKill();
+ victimPvPStats.AddDeath();
+
+ if(PvPLeaderboardConfig.PvPLeaderboardAnnounceKill.Value)
+ {
+ ServerChatUtils.SendSystemMessageToAllClients(entityManager, $"Vampire \"{killerUser.CharacterName}\" has killed \"{victimUser.CharacterName}\"!");
+ }
+ }
+
+ [Command("pvpstats,pvp", "pvpstats", "Shows your current pvp stats (kills, deaths & K/D ratio).")]
+ private static void OnPvPStatsCommand(Command command)
+ {
+ var user = command.User;
+ if(!_pvpStats.TryGetValue(user.PlatformId, out var pvpStats))
+ {
+ pvpStats = new PvPStats();
+ _pvpStats[user.PlatformId] = pvpStats;
+ }
+ user.SendSystemMessage($"{user.CharacterName} K/D: {pvpStats.KDRatio} [{pvpStats.Kills}/{pvpStats.Deaths}]");
+ command.Use();
+ }
+
+ [Command("pvplb,pvpleaderboard", "pvplb []", "Shows the 5 players on the requested page of the leaderboard (or top 5 if no page is given).")]
+ private static void OnPvPLeaderboardCommand(Command command)
+ {
+ int page = 0;
+ if(command.Args.Length >= 1 && int.TryParse(command.Args[0], out page))
+ {
+ page -= 1;
+ }
+
+ var recordsPerPage = 5;
+
+ var maxPage = (int)Math.Ceiling(_pvpStats.Count / (double)recordsPerPage);
+ page = Math.Min(maxPage - 1, page);
+
+ var user = command.User;
+ var entityManager = VWorld.Server.EntityManager;
+ var leaderboard = _pvpStats.OrderByDescending(x => x.Value.KDRatio).ThenByDescending(x => x.Value.Kills).ThenBy(x => x.Value.Deaths).Skip(page * recordsPerPage).Take(recordsPerPage);
+ user.SendSystemMessage("========== PvP Leaderboard ==========");
+ int rank = (page * recordsPerPage) + 1;
+ foreach((var platformId, var pvpStats) in leaderboard)
+ {
+ user.SendSystemMessage($"{rank}. {Utils.GetCharacterName(platformId, entityManager)} : {pvpStats.KDRatio} [{pvpStats.Kills}/{pvpStats.Deaths}]");
+ rank++;
+ }
+ user.SendSystemMessage($"=============== {page + 1}/{maxPage} ===============");
+
+ command.Use();
+ }
+
+ #endregion
+
+ #region Nested
+
+ private class PvPStats
+ {
+ #region Properties
+
+ public int Kills { get; private set; }
+ public int Deaths { get; private set; }
+ public double KDRatio { get; private set; }
+
+ #endregion
+
+ #region Lifecycle
+
+ [JsonConstructor]
+ public PvPStats(int kills, int deaths, double kdRatio)
+ {
+ (Kills, Deaths, KDRatio) = (kills, deaths, kdRatio);
+
+ CalcKDRatio();
+ }
+
+ public PvPStats()
+ {
+ Kills = 0;
+ Deaths = 0;
+ KDRatio = 1d;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public void AddKill()
+ {
+ Kills++;
+ CalcKDRatio();
+ }
+
+ public void AddDeath()
+ {
+ Deaths++;
+ CalcKDRatio();
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private void CalcKDRatio()
+ {
+ if(Deaths == 0)
+ {
+ KDRatio = Kills;
+ }
+ else
+ {
+ KDRatio = Kills / (double)Deaths;
+ }
+ }
+
+ #endregion
+ }
+
+ #endregion
+ }
+}
diff --git a/PvPPunishment/Configs/PvPPunishmentConfig.cs b/PvPPunishment/Configs/PvPPunishmentConfig.cs
new file mode 100644
index 0000000..b4d0190
--- /dev/null
+++ b/PvPPunishment/Configs/PvPPunishmentConfig.cs
@@ -0,0 +1,50 @@
+using BepInEx.Configuration;
+
+namespace VMods.PvPPunishment
+{
+ public static class PvPPunishmentConfig
+ {
+ #region Properties
+
+ public static ConfigEntry PvPPunishmentEnabled { get; private set; }
+ public static ConfigEntry PvPPunishmentLevelDifference { get; private set; }
+ public static ConfigEntry PvPPunishmentOffenseLimit { get; private set; }
+ public static ConfigEntry PvPPunishmentOffenseCooldown { get; private set; }
+ public static ConfigEntry PvPPunishmentDuration { get; private set; }
+ public static ConfigEntry PvPPunishmentMovementSpeedReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentMaxHealthReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentPhysResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentSpellResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentFireResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentHolyResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentSunResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentSilverResistReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentPhysPowerReduction { get; private set; }
+ public static ConfigEntry PvPPunishmentSpellPowerReduction { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ PvPPunishmentEnabled = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentEnabled), false, "Enabled/disable the PvP Punishment system.");
+ PvPPunishmentLevelDifference = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentLevelDifference), 10, "The level difference at which to apply a punishment to the killer.");
+ PvPPunishmentOffenseLimit = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentOffenseLimit), 3, "The amount of offenses a player can commit before being punished.");
+ PvPPunishmentOffenseCooldown = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentOffenseCooldown), 300f, "The amount of seconds since the last offense at which the offense counter resets.");
+ PvPPunishmentDuration = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentDuration), 1800f, "The amount of seconds the punishment buff lasts.");
+ PvPPunishmentMovementSpeedReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentMovementSpeedReduction), 15f, "The percentage of reduced Movement Speed when a player is punished.");
+ PvPPunishmentMaxHealthReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentMaxHealthReduction), 15f, "The percentage of reduced Max Health when a player is punished.");
+ PvPPunishmentPhysResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentPhysResistReduction), 15f, "The amount of reduced Physical Resistance when a player is punished.");
+ PvPPunishmentSpellResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentSpellResistReduction), 15f, "The amount of reduced Spell Resistance when a player is punished.");
+ PvPPunishmentFireResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentFireResistReduction), 15f, "The amount of reduced Fire Resistance when a player is punished.");
+ PvPPunishmentHolyResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentHolyResistReduction), 15f, "The amount of reduced Holy Resistance when a player is punished.");
+ PvPPunishmentSunResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentSunResistReduction), 15f, "The amount of reduced Sun Resistance when a player is punished.");
+ PvPPunishmentSilverResistReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentSilverResistReduction), 15f, "The amount of reduced Silver Resistance when a player is punished.");
+ PvPPunishmentPhysPowerReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentPhysPowerReduction), 15f, "The percentage of reduced Physical Power when a player is punished.");
+ PvPPunishmentSpellPowerReduction = config.Bind(nameof(PvPPunishmentConfig), nameof(PvPPunishmentSpellPowerReduction), 15f, "The percentage of reduced Spell Power when a player is punished.");
+ }
+
+ #endregion
+ }
+}
diff --git a/PvPPunishment/Plugin.cs b/PvPPunishment/Plugin.cs
new file mode 100644
index 0000000..faa212a
--- /dev/null
+++ b/PvPPunishment/Plugin.cs
@@ -0,0 +1,64 @@
+using BepInEx;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using System.Reflection;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.PvPPunishment
+{
+ [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);
+ PvPPunishmentConfig.Initialize(Config);
+
+ CommandSystem.Initialize();
+ HighestGearScoreSystem.Initialize();
+ PvPPunishmentSystem.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();
+ PvPPunishmentSystem.Deinitialize();
+ HighestGearScoreSystem.Deinitialize();
+ CommandSystem.Deinitialize();
+ Config.Clear();
+ Utils.Deinitialize();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/PvPPunishment/PvPPunishment.csproj b/PvPPunishment/PvPPunishment.csproj
new file mode 100644
index 0000000..f65af41
--- /dev/null
+++ b/PvPPunishment/PvPPunishment.csproj
@@ -0,0 +1,478 @@
+
+
+ netstandard2.1
+ VMods.PvPPunishment
+ VMods.PvPPunishment
+ A mod that punishes high-level players that kill low-level players
+ 0.0.1
+ 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/PvPPunishment/Systems/PvPPunishmentSystem.cs b/PvPPunishment/Systems/PvPPunishmentSystem.cs
new file mode 100644
index 0000000..425bbc0
--- /dev/null
+++ b/PvPPunishment/Systems/PvPPunishmentSystem.cs
@@ -0,0 +1,279 @@
+using ProjectM;
+using ProjectM.Network;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Unity.Entities;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.PvPPunishment
+{
+ public static class PvPPunishmentSystem
+ {
+ #region Consts
+
+ private const string PvPPunishmentFileName = "PvPPunishment.json";
+
+ #endregion
+
+ #region Variables
+
+ private static Dictionary _offenses;
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ _offenses = VModStorage.Load(PvPPunishmentFileName, () => new Dictionary());
+
+ PruneOffenses();
+
+ VModStorage.SaveEvent += Save;
+ VampireDownedHook.VampireDownedEvent += OnVampireDowned;
+ BuffSystemHook.ProcessBuffEvent += OnProcessBuff;
+ }
+
+ public static void Deinitialize()
+ {
+ BuffSystemHook.ProcessBuffEvent -= OnProcessBuff;
+ VampireDownedHook.VampireDownedEvent -= OnVampireDowned;
+ VModStorage.SaveEvent -= Save;
+ }
+
+ public static void Save()
+ {
+ PruneOffenses();
+
+ VModStorage.Save(PvPPunishmentFileName, _offenses);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void OnVampireDowned(Entity killer, Entity victim)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+
+ Entity killerUserEntity = entityManager.GetComponentData(killer).UserEntity._Entity;
+ var killerUser = entityManager.GetComponentData(killerUserEntity);
+ ulong killerSteamID = killerUser.PlatformId;
+ float killerLevel = HighestGearScoreSystem.GetCurrentOrHighestGearScore(new FromCharacter()
+ {
+ User = killerUserEntity,
+ Character = killer,
+ });
+
+ 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 DEBUG
+ var msg = $"{killerUser.CharacterName} ({killerSteamID}) [Lv: {killerLevel}] killed {victimUser.CharacterName} ({victimSteamID}) [Lv: {victimLevel}]. - Diff: {diff}";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+
+ if(diff >= PvPPunishmentConfig.PvPPunishmentLevelDifference.Value)
+ {
+ if(!_offenses.TryGetValue(killerSteamID, out var offense))
+ {
+ offense = new OffenseData();
+ _offenses.Add(killerSteamID, offense);
+ }
+
+#if DEBUG
+ msg = $"Last Offense was at: {offense.LastOffenseTime}";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+
+ TimeSpan timeSpan = DateTime.UtcNow - offense.LastOffenseTime;
+
+#if DEBUG
+ msg = $"Time Diff since last offense: {timeSpan}";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+ if(timeSpan.TotalSeconds > PvPPunishmentConfig.PvPPunishmentOffenseCooldown.Value)
+ {
+ offense.OffenseCount = 1;
+ }
+ else
+ {
+ offense.OffenseCount++;
+ }
+ offense.LastOffenseTime = DateTime.UtcNow;
+
+#if DEBUG
+ msg = $"New Offense Count: {offense.OffenseCount}";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+
+ if(offense.OffenseCount >= PvPPunishmentConfig.PvPPunishmentOffenseLimit.Value)
+ {
+ Utils.ApplyBuff(killerUserEntity, killer, Utils.SevereGarlicDebuff);
+#if DEBUG
+ msg = $"Punishment applied for {killerUser.CharacterName} ({killerSteamID})";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+#endif
+ }
+#if DEBUG
+ else
+ {
+ msg = $"Punishment count has increased!";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+ }
+#endif
+ }
+#if DEBUG
+ else
+ {
+ msg = $"No punishment appled -> kill was within appropriate level";
+ Utils.Logger.LogMessage(msg);
+ //Utils.SendMessage(killerUserEntity, msg, ServerChatMessageType.System);
+ //Utils.SendMessage(victimUserEntity, msg, ServerChatMessageType.System);
+ }
+#endif
+ }
+
+ private static void OnProcessBuff(Entity entity, PrefabGUID buffGUID)
+ {
+ if(!VWorld.IsServer || buffGUID != Utils.SevereGarlicDebuff)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+
+ var buffLifeTime = entityManager.GetComponentData(entity);
+ buffLifeTime.Duration = PvPPunishmentConfig.PvPPunishmentDuration.Value;
+ entityManager.SetComponentData(entity, buffLifeTime);
+
+ var buffer = entityManager.AddBuffer(entity);
+ TryAddReductionBuff(buffer, UnitStatType.MovementSpeed, ModificationType.Multiply, PvPPunishmentConfig.PvPPunishmentMovementSpeedReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.MaxHealth, ModificationType.Multiply, PvPPunishmentConfig.PvPPunishmentMaxHealthReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.PhysicalResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentPhysResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.SpellResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentSpellResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.FireResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentFireResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.HolyResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentHolyResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.SunResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentSunResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.SilverResistance, ModificationType.Add, PvPPunishmentConfig.PvPPunishmentSilverResistReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.PhysicalPower, ModificationType.Multiply, PvPPunishmentConfig.PvPPunishmentPhysPowerReduction.Value);
+ TryAddReductionBuff(buffer, UnitStatType.SpellPower, ModificationType.Multiply, PvPPunishmentConfig.PvPPunishmentSpellPowerReduction.Value);
+ }
+
+ private static void TryAddReductionBuff(DynamicBuffer buffer, UnitStatType unitStatType, ModificationType modificationType, float value)
+ {
+ if(value > 0f)
+ {
+ buffer.Add(new ModifyUnitStatBuff_DOTS()
+ {
+ StatType = unitStatType,
+ Value = modificationType switch
+ {
+ ModificationType.Multiply => (100f - value) / 100f,
+ ModificationType.Add => -value,
+ _ => value,
+ },
+ ModificationType = modificationType,
+ Id = ModificationId.NewId(0),
+ });
+ }
+ }
+
+ private static void PruneOffenses()
+ {
+ var now = DateTime.UtcNow;
+ var keys = _offenses.Keys.ToList();
+ var offenseCooldown = PvPPunishmentConfig.PvPPunishmentOffenseCooldown.Value;
+ foreach(var key in keys)
+ {
+ var offenseData = _offenses[key];
+ if(now.Subtract(offenseData.LastOffenseTime).TotalSeconds > offenseCooldown)
+ {
+ _offenses.Remove(key);
+ }
+ }
+ }
+
+ [Command("ispunished", "ispunished []", "Tell you if the the given player (or yourself when no playername is given) currently has the PvP Punishment buff", true)]
+ private static void OnIsPunishedPlayerCommand(Command command)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ (var searchUsername, var fromCharacter) = command.GetFromCharacter(entityManager: entityManager);
+
+ if(fromCharacter.HasValue)
+ {
+ if(BuffUtility.HasBuff(entityManager, fromCharacter.Value.Character, Utils.SevereGarlicDebuff))
+ {
+ command.User.SendSystemMessage($"Vampire {searchUsername} is currently punished.");
+ }
+ else
+ {
+ command.User.SendSystemMessage($"Vampire {searchUsername} isn't punished.");
+ }
+ }
+ command.Use();
+ }
+
+ [Command("punish", "punish []", "Adds (or refreshes) the PvP Punishment buff for the given player (or yourself when no playername is given)", true)]
+ private static void OnPunishPlayerCommand(Command command)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ (var searchUsername, var fromCharacter) = command.GetFromCharacter(entityManager: entityManager);
+
+ if(fromCharacter.HasValue)
+ {
+ Utils.ApplyBuff(fromCharacter.Value, Utils.SevereGarlicDebuff);
+ command.User.SendSystemMessage($"Vampire {searchUsername} has been punished.");
+ }
+ command.Use();
+ }
+
+ [Command("unpunish", "unpunish []", "Removes the PvP Punishment buff for the given player (or yourself when no playername is given)", true)]
+ private static void OnUnPunishPlayerCommand(Command command)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ (var searchUsername, var fromCharacter) = command.GetFromCharacter(entityManager: entityManager);
+
+ if(fromCharacter.HasValue)
+ {
+ Utils.RemoveBuff(fromCharacter.Value, Utils.SevereGarlicDebuff);
+ command.User.SendSystemMessage($"Vampire {searchUsername} has been un-punished.");
+ }
+ command.Use();
+ }
+
+ #endregion
+
+ #region Nested
+
+ private class OffenseData
+ {
+ public DateTime LastOffenseTime { get; set; }
+ public int OffenseCount { get; set; }
+ }
+
+ #endregion
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c313ae1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,91 @@
+# VMods
+A selection of Mods for V-Rising
+
+## General Mod info (Applies to most mods)
+### Commands
+Most of the mods 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.
+
+## Blood Refill
+A server-side only mod that allows players to refill their blood pool.
+
+When feed-killing an enemy, you'll be able to regain some blood.
+The amount of blood regained is based on the level difference, blood type and blood quality of the killed enemy with V-Bloods refilling your blood pool for a much larger amount.
+
+
+Configuration Options
+
+* Enable/disable requiring feed-killing (when disabled, any kill grants some blood).
+* Choose the amount of blood gained on a 'regular refill' (i.e. a refill without any level, blood type or quality punishments applied)
+* A multiplier to reduce the amount of gained blood when feeding on an enemy of a different blood type. (blood dilution)
+* The ability to disable different blood type refilling (i.e. a 0 multiplier for different blood types)
+* Switch between having V-Blood act as diluted or pure blood, or have V-Blood completely refill your blood pool
+* The options to make refilling random between 0.1L and the calculated amount (which then acts as a max refill amount)
+* A global refill multiplier (applied after picking a random refill value)
+
+
+
+## Recover Empty Containers
+A server-side only mod that allows players to recover empty containers.
+
+When a player drinks a potion or brew, an empty container (glass bottle or canteen) is given back to the player (or dropped on the floor when the player's inventory is full)
+
+## Resource Stash Withdrawal
+A server & client side mod that allows players to withdraw items for a recipe directly from their stash.
+
+When a player is at a crafting or other workstation, he/she can click on the recipe or an individual component of a recipe with their middle-mouse button to withdraw the missing item(s) directly from their stash.
+(The withdraw the full amount, CTRL+Middle Mouse Button can be used)
+
+Note: Players without the client-side mod can still join and play the server, but won't be able to make use of this feature.
+
+## PvP Leaderboard
+A server-side only mod that keeps track of Kills, Death and the K/D ratio of players and ranks them in a leaderboard.
+
+This mod also has the option to exclude low-level (or grief-kills) from counting towards the K/D of the leaderboard.
+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.
+Both legitimate and grief-kills can be announced server-wide.
+
+Players can see their own stats and the leaderboard itself using a command (By default: `!pvpstats`).
+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: `!pvplb []`).
+
+
+Configuration Options
+
+* Enable/disable announcing of legitimate kills
+* Enable/disable announcing of grief-kills
+* 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
+
+
+
+## PvP Punishment
+A server-side only mod that punishes low-level kills.
+
+This mod also has the option prevent cheesing the low-level restriction where the highest gear score (in the past X minutes) is used instead of the current gear score.
+Both the amount of offenses before being punishes as well as the punishment itself can be tweaked.
+
+
+Configuration Options
+
+* Set a Level Difference at which an offense is being recorded
+* Enable/disable usage of the anti-cheesing system (highest gear score tracking)
+* Change the amount of offenses a player can make before actually being punished
+* Change the offense cooldown time before the offense counter resets
+* Change the duration of the punishment
+* Change the following for the actual punishment:
+ * % reduced Movement Speed
+ * % reduced Max Health
+ * % reduced Physical Resistance
+ * % reduced Spell Resistance
+ * amount of reduced Fire Resistance
+ * amount of reduced Holy Resistance
+ * amount of reduced Sun Resistance
+ * amount of reduced Silver Resistance
+ * % of reduced Physical Power
+ * % of reduced Spell Power
+
+
diff --git a/RecoverEmptyContainers/Configs/RecoverEmptyContainersConfig.cs b/RecoverEmptyContainers/Configs/RecoverEmptyContainersConfig.cs
new file mode 100644
index 0000000..8d31af1
--- /dev/null
+++ b/RecoverEmptyContainers/Configs/RecoverEmptyContainersConfig.cs
@@ -0,0 +1,22 @@
+using BepInEx.Configuration;
+
+namespace VMods.RecoverEmptyContainers
+{
+ public static class RecoverEmptyContainersConfig
+ {
+ #region Properties
+
+ public static ConfigEntry RecoverEmptyContainersEnabled { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ RecoverEmptyContainersEnabled = config.Bind("Server", nameof(RecoverEmptyContainersEnabled), false, "Enabled/disable the recovery of empty containers system.");
+ }
+
+ #endregion
+ }
+}
diff --git a/RecoverEmptyContainers/Hooks/UseConsumableHook.cs b/RecoverEmptyContainers/Hooks/UseConsumableHook.cs
new file mode 100644
index 0000000..3adff25
--- /dev/null
+++ b/RecoverEmptyContainers/Hooks/UseConsumableHook.cs
@@ -0,0 +1,69 @@
+using HarmonyLib;
+using ProjectM;
+using ProjectM.Network;
+using System.Collections.Generic;
+using Unity.Collections;
+using Unity.Entities;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.RecoverEmptyContainers
+{
+ [HarmonyPatch]
+ public class TestHook
+ {
+ #region Consts
+
+ private static readonly Dictionary RecipeItemToReturnedItemMapping = new()
+ {
+ [new PrefabGUID(-1322000172)] = new PrefabGUID(-810738866),// Water-filled Canteen -> Empty Canteen
+ [new PrefabGUID(-1382451936)] = new PrefabGUID(-437611596),// Water-filled Bottle -> Empty Glass Bottle
+ };
+
+ #endregion
+
+ #region Public Methods
+
+ [HarmonyPatch(typeof(UseConsumableSystem), nameof(UseConsumableSystem.CastAbility))]
+ [HarmonyPostfix]
+ public static void CastAbility(UseConsumableSystem __instance, InventoryBuffer inventoryItem, FromCharacter fromCharacter, NativeHashMap prefabLookupMap, Entity itemEntity, bool removeByItemEntity, ref bool shouldConsumeItem)
+ {
+ if(!VWorld.IsServer)
+ {
+ return;
+ }
+
+ var server = VWorld.Server;
+ var entityManager = server.EntityManager;
+ var gameDataSystem = server.GetExistingSystem();
+
+ foreach(var kvp in gameDataSystem.RecipeHashLookupMap)
+ {
+ var recipeData = kvp.Value;
+ if(entityManager.HasComponent(recipeData.Entity))
+ {
+ var outputBuffer = entityManager.GetBuffer(recipeData.Entity);
+ foreach(var output in outputBuffer)
+ {
+ if(output.Guid == inventoryItem.ItemType)
+ {
+ if(entityManager.HasComponent(recipeData.Entity))
+ {
+ var requirements = entityManager.GetBuffer(recipeData.Entity);
+ foreach(var requirement in requirements)
+ {
+ if(RecipeItemToReturnedItemMapping.TryGetValue(requirement.Guid, out var returnItemGUID))
+ {
+ Utils.TryGiveItem(entityManager, gameDataSystem.ItemHashLookupMap, fromCharacter.Character, returnItemGUID, requirement.Stacks, out _, out _, dropRemainder: true);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/RecoverEmptyContainers/Plugin.cs b/RecoverEmptyContainers/Plugin.cs
new file mode 100644
index 0000000..d49fd68
--- /dev/null
+++ b/RecoverEmptyContainers/Plugin.cs
@@ -0,0 +1,52 @@
+using BepInEx;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using System.Reflection;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.RecoverEmptyContainers
+{
+ [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);
+ RecoverEmptyContainersConfig.Initialize(Config);
+
+ _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;
+ }
+ _hooks?.UnpatchSelf();
+ Config.Clear();
+ Utils.Deinitialize();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/RecoverEmptyContainers/RecoverEmptyContainers.csproj b/RecoverEmptyContainers/RecoverEmptyContainers.csproj
new file mode 100644
index 0000000..f9339a3
--- /dev/null
+++ b/RecoverEmptyContainers/RecoverEmptyContainers.csproj
@@ -0,0 +1,469 @@
+
+
+ netstandard2.1
+ VMods.RecoverEmptyContainers
+ VMods.RecoverEmptyContainers
+ Allows a player to recover an empty container when consuming a potion/brew
+ 0.0.1
+ 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/ResourceStashWithdrawal/Configs/ResourceStashWithdrawalConfig.cs b/ResourceStashWithdrawal/Configs/ResourceStashWithdrawalConfig.cs
new file mode 100644
index 0000000..671f80e
--- /dev/null
+++ b/ResourceStashWithdrawal/Configs/ResourceStashWithdrawalConfig.cs
@@ -0,0 +1,22 @@
+using BepInEx.Configuration;
+
+namespace VMods.ResourceStashWithdrawal
+{
+ public static class ResourceStashWithdrawalConfig
+ {
+ #region Properties
+
+ public static ConfigEntry ResourceStashWithdrawalEnabled { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ ResourceStashWithdrawalEnabled = config.Bind("Server", nameof(ResourceStashWithdrawalEnabled), false, "Enabled/disable the resource stash withdrawal system.");
+ }
+
+ #endregion
+ }
+}
diff --git a/ResourceStashWithdrawal/Hooks/UIClickHook.cs b/ResourceStashWithdrawal/Hooks/UIClickHook.cs
new file mode 100644
index 0000000..7a675cb
--- /dev/null
+++ b/ResourceStashWithdrawal/Hooks/UIClickHook.cs
@@ -0,0 +1,261 @@
+using HarmonyLib;
+using ProjectM;
+using ProjectM.UI;
+using System;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.ResourceStashWithdrawal
+{
+ [HarmonyPatch]
+ public class UIClickHook
+ {
+ #region Variables
+
+ private static DateTime _lastResourceRequest = DateTime.UtcNow;
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Reset()
+ {
+ _lastResourceRequest = DateTime.UtcNow;
+ }
+
+ [HarmonyPatch(typeof(GridSelectionEntry), nameof(GridSelectionEntry.OnPointerClick))]
+ [HarmonyPostfix]
+ public static void OnPointerClick(GridSelectionEntry __instance, PointerEventData eventData)
+ {
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+
+ if(!VWorld.IsClient || eventData.button != PointerEventData.InputButton.Middle || DateTime.UtcNow.Subtract(_lastResourceRequest).TotalSeconds <= 0.2f)
+ {
+ return;
+ }
+ _lastResourceRequest = DateTime.UtcNow;
+
+ bool withdrawFullAmount = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
+
+ var client = VWorld.Client;
+ var entityManager = client.EntityManager;
+ var gameDataSystem = client.GetExistingSystem();
+ var itemHashLookupMap = gameDataSystem.ItemHashLookupMap;
+ var prefabCollectionSystem = client.GetExistingSystem();
+ var prefabLookupMap = prefabCollectionSystem.PrefabLookupMap;
+
+ RefinementstationRecipeEntry refinementstationRecipeEntry = __instance.GetComponent();
+ RefinementstationRecipeItem refinementstationRecipeItem = __instance.GetComponent();
+ WorkstationRecipeGridSelectionEntry workstationRecipeGridSelectionEntry = __instance.GetComponent();
+ if(refinementstationRecipeEntry != null)
+ {
+ var refinementstationSubMenu = __instance.GetComponentInParent();
+ var unitSpawnerstationSubMenu = __instance.GetComponentInParent();
+ if(refinementstationSubMenu != null)
+ {
+ var recipe = refinementstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+ foreach(var requirement in recipe.Requirements)
+ {
+ SendWithdrawRequest(refinementstationSubMenu.InputInventorySelectionGroup, refinementstationSubMenu.OutputInventorySelectionGroup, recipe, requirement);
+ }
+ }
+ else if(unitSpawnerstationSubMenu != null)
+ {
+ var recipe = unitSpawnerstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+ foreach(var requirement in recipe.Requirements)
+ {
+ SendWithdrawRequest(unitSpawnerstationSubMenu.InputInventorySelectionGroup, unitSpawnerstationSubMenu.OutputInventorySelectionGroup, recipe, requirement);
+ }
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled SubMenu for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+
+ // Force update the tooltip
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+ }
+ else if(refinementstationRecipeItem != null)
+ {
+ refinementstationRecipeEntry = refinementstationRecipeItem.GetComponentInParent();
+
+ var refinementstationSubMenu = __instance.GetComponentInParent();
+ var unitSpawnerstationSubMenu = __instance.GetComponentInParent();
+ if(refinementstationSubMenu != null)
+ {
+ var recipe = refinementstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+
+ foreach(var requirement in recipe.Requirements)
+ {
+ if(requirement.Guid == refinementstationRecipeItem.Guid)
+ {
+ SendWithdrawRequest(refinementstationSubMenu.InputInventorySelectionGroup, refinementstationSubMenu.OutputInventorySelectionGroup, recipe, requirement);
+
+ // Force update the tooltip
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+ return;
+ }
+ }
+
+ foreach(var output in recipe.OutputItems)
+ {
+ if(output.Guid == refinementstationRecipeItem.Guid)
+ {
+ int requiredAmount = output.Stacks;
+#if DEBUG
+ var name = Utils.GetItemName(output.Guid, gameDataSystem, entityManager, prefabLookupMap);
+ Utils.Logger.LogMessage($"Withdraw Recipe item: {requiredAmount}x {name} ({output.Guid.GuidHash})");
+#endif
+
+ VNetwork.SendToServerStruct(new ResourceStashWithdrawalRequest()
+ {
+ ItemGUIDHash = output.Guid.GuidHash,
+ Amount = requiredAmount,
+ });
+
+ // Force update the tooltip
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+ return;
+ }
+ }
+ }
+ else if(unitSpawnerstationSubMenu != null)
+ {
+ var recipe = unitSpawnerstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+
+ foreach(var requirement in recipe.Requirements)
+ {
+ if(requirement.Guid == refinementstationRecipeItem.Guid)
+ {
+ SendWithdrawRequest(unitSpawnerstationSubMenu.InputInventorySelectionGroup, unitSpawnerstationSubMenu.OutputInventorySelectionGroup, recipe, requirement);
+
+ // Force update the tooltip
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+ return;
+ }
+ }
+
+ // Don't look at the output recipes, since those are units (and not inventory items)
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled {nameof(refinementstationRecipeItem)} SubMenu for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+ }
+ else if(workstationRecipeGridSelectionEntry != null)
+ {
+ var workstationSubMenu = __instance.GetComponentInParent();
+ if(workstationSubMenu == null)
+ {
+ // Only allow withdrawing when it's a workstation (and NOT when you're in your crafting tab of the Inventory Sub Menu!)
+ return;
+ }
+ float resourceMultiplier = 1f;
+ // Hacky solution to find the bonus -> this is done because the 'BonusType' is incorrect/bugged.
+ var bonuses = workstationSubMenu.BonusesSelectionGroup.Entries;
+ var lastIndex = bonuses.Count - 1;
+ var lastBonus = bonuses[lastIndex];
+ if(lastBonus.Unlocked)
+ {
+ resourceMultiplier = 1f - (lastBonus.Value / 100f);
+ }
+ var recipe = workstationSubMenu.RecipesGridSelectionGroup.Entries[workstationRecipeGridSelectionEntry.EntryIndex];
+ if(gameDataSystem.RecipeHashLookupMap.ContainsKey(recipe.EntryId))
+ {
+ var recipeData = gameDataSystem.RecipeHashLookupMap[recipe.EntryId];
+ if(entityManager.HasComponent(recipeData.Entity))
+ {
+ var requirements = entityManager.GetBuffer(recipeData.Entity);
+ foreach(var requirement in requirements)
+ {
+ int requiredAmount = (int)Math.Ceiling(requirement.Stacks * resourceMultiplier);
+ var itemGUID = requirement.Guid;
+#if DEBUG
+ var name = Utils.GetItemName(itemGUID, gameDataSystem, entityManager, prefabLookupMap);
+ Utils.Logger.LogMessage($"Withdraw Recipe item: {requiredAmount}x {name} ({itemGUID})");
+#endif
+ if(!withdrawFullAmount)
+ {
+ foreach(var stationItem in workstationSubMenu.ItemOutputGridSelectionGroup.Entries)
+ {
+ if(stationItem.EntryId == itemGUID)
+ {
+ requiredAmount -= stationItem.Stacks;
+ }
+ }
+ requiredAmount -= InventoryUtilities.ItemCount(entityManager, EntitiesHelper.GetLocalCharacterEntity(entityManager), itemGUID);
+ }
+
+ if(requiredAmount > 0)
+ {
+ VNetwork.SendToServerStruct(new ResourceStashWithdrawalRequest()
+ {
+ ItemGUIDHash = itemGUID.GuidHash,
+ Amount = requiredAmount,
+ });
+ }
+ }
+
+ // Force update the tooltip
+ UITooltipHook.OnPointerEnter(__instance, eventData);
+ }
+ }
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled {nameof(GridSelectionEntry)} Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+
+ // Nested Method(s)
+ void SendWithdrawRequest(GridSelectionGroup inputInventorySelectionGroup, GridSelectionGroup outputInventorySelectionGroup, RefinementstationRecipeEntry.Data recipe, RecipeRequirementBuffer requirement)
+ {
+ int requiredAmount = (int)Math.Ceiling(requirement.Stacks * recipe.ResourceMultiplier);
+#if DEBUG
+ var name = Utils.GetItemName(requirement.Guid, gameDataSystem, entityManager, prefabLookupMap);
+ Utils.Logger.LogMessage($"Withdraw Recipe item: {requiredAmount}x {name} ({requirement.Guid.GuidHash})");
+#endif
+
+ if(!withdrawFullAmount)
+ {
+ foreach(var stationItem in inputInventorySelectionGroup.Entries)
+ {
+ if(stationItem.EntryId == requirement.Guid)
+ {
+ requiredAmount -= stationItem.Stacks;
+ }
+ }
+ foreach(var stationItem in outputInventorySelectionGroup.Entries)
+ {
+ if(stationItem.EntryId == requirement.Guid)
+ {
+ requiredAmount -= stationItem.Stacks;
+ }
+ }
+ requiredAmount -= InventoryUtilities.ItemCount(entityManager, EntitiesHelper.GetLocalCharacterEntity(entityManager), requirement.Guid);
+ }
+
+ if(requiredAmount > 0)
+ {
+ VNetwork.SendToServerStruct(new ResourceStashWithdrawalRequest()
+ {
+ ItemGUIDHash = requirement.Guid.GuidHash,
+ Amount = requiredAmount,
+ });
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/ResourceStashWithdrawal/Hooks/UITooltipHook.cs b/ResourceStashWithdrawal/Hooks/UITooltipHook.cs
new file mode 100644
index 0000000..028125d
--- /dev/null
+++ b/ResourceStashWithdrawal/Hooks/UITooltipHook.cs
@@ -0,0 +1,377 @@
+using HarmonyLib;
+using ProjectM;
+using ProjectM.Scripting;
+using ProjectM.Shared;
+using ProjectM.UI;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Unity.Entities;
+using UnityEngine;
+using UnityEngine.EventSystems;
+using VMods.Shared;
+using Wetstone.API;
+using static BepInEx.IL2CPP.Utils.MonoBehaviourExtensions;
+
+namespace VMods.ResourceStashWithdrawal
+{
+ [HarmonyPatch]
+ public class UITooltipHook
+ {
+ #region Public Methods
+
+ [HarmonyPatch(typeof(GridSelectionEntry), nameof(GridSelectionEntry.OnPointerEnter))]
+ [HarmonyPostfix]
+ public static void OnPointerEnter(GridSelectionEntry __instance, PointerEventData eventData)
+ {
+ if(!VWorld.IsClient)
+ {
+ return;
+ }
+
+ RefinementstationRecipeEntry refinementstationRecipeEntry = __instance.GetComponent();
+ RefinementstationRecipeItem refinementstationRecipeItem = __instance.GetComponent();
+ ItemGridSelectionEntry itemGridSelectionEntry = __instance.GetComponent();
+ WorkstationRecipeGridSelectionEntry workstationRecipeGridSelectionEntry = __instance.GetComponent();
+ ResearchEntry researchEntry = __instance.GetComponent();
+ BuildMenu_StructureEntry buildMenuStructureEntry = __instance.GetComponent();
+ if(refinementstationRecipeEntry == null && refinementstationRecipeItem == null && itemGridSelectionEntry == null &&
+ workstationRecipeGridSelectionEntry == null && researchEntry == null && buildMenuStructureEntry == null)
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled {nameof(GridSelectionEntry)} PointerEnter for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+
+ // Find the current tooltip
+ var refinementstationSubMenu = __instance.GetComponentInParent();
+ var unitSpawnerstationSubMenu = __instance.GetComponentInParent();
+ var workstationSubMenu = __instance.GetComponentInParent();
+ var researchstationSubMenu = __instance.GetComponentInParent();
+ var inventorySubMenu = __instance.GetComponentInParent();
+ var buildMenu = __instance.GetComponentInParent();
+ var servantInventorySubMenu = __instance.GetComponentInParent();
+ var salvagestationSubMenu = __instance.GetComponentInParent();
+ FakeTooltip tooltip = null;
+ if(refinementstationSubMenu != null)
+ {
+ tooltip = refinementstationSubMenu.FakeTooltip;
+ }
+ else if(unitSpawnerstationSubMenu != null)
+ {
+ tooltip = unitSpawnerstationSubMenu.FakeTooltip;
+ }
+ else if(workstationSubMenu != null)
+ {
+ tooltip = workstationSubMenu.FakeTooltip;
+ }
+ else if(researchstationSubMenu != null)
+ {
+ tooltip = researchstationSubMenu.FakeTooltip;
+ }
+ else if(inventorySubMenu != null)
+ {
+ tooltip = inventorySubMenu.FakeTooltip;
+ }
+ else if(buildMenu != null)
+ {
+ tooltip = buildMenu.FakeTooltip;
+ }
+ else if(servantInventorySubMenu != null)
+ {
+ tooltip = servantInventorySubMenu.FakeTooltip;
+ }
+ else if(salvagestationSubMenu != null)
+ {
+ tooltip = salvagestationSubMenu.FakeTooltip;
+ }
+
+ if(tooltip == null)
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled Tooltip for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+
+ var client = VWorld.Client;
+ if(client.Systems.Count == 0)
+ {
+ // No systems -> No tooltip
+ return;
+ }
+ var entityManager = client.EntityManager;
+ var gameDataSystem = client.GetExistingSystem();
+ var clientGameManager = client.GetExistingSystem()?._ClientGameManager;
+ var teamChecker = clientGameManager._TeamChecker;
+ var character = EntitiesHelper.GetLocalCharacterEntity(entityManager);
+
+ // Ensure we're hovering a single item or the recipe as a whole
+ PrefabGUID? itemGUID = null;
+ StoredBlood? storedBlood = null;
+ List requiredItemGUIDs = null;
+ List repairItemGUIDs = null;
+ if(refinementstationRecipeEntry != null)
+ {
+ RefinementstationRecipeEntry.Data recipe;
+ if(refinementstationSubMenu != null)
+ {
+ recipe = refinementstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+ }
+ else if(unitSpawnerstationSubMenu != null)
+ {
+ recipe = unitSpawnerstationSubMenu.RecipesSelectionGroup.Entries[refinementstationRecipeEntry.EntryIndex];
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled itemGUID SubMenu for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+ requiredItemGUIDs = new();
+ foreach(var requiredItem in recipe.Requirements)
+ {
+ requiredItemGUIDs.Add(requiredItem.Guid);
+ }
+ itemGUID = recipe.OutputItems[0].Guid;
+ }
+ else if(refinementstationRecipeItem != null)
+ {
+ itemGUID = refinementstationRecipeItem.Guid;
+ }
+ else if(itemGridSelectionEntry != null)
+ {
+ if(itemGridSelectionEntry.EntryId == PrefabGUID.Empty)
+ {
+ // It's an empty slot.
+ return;
+ }
+ itemGUID = itemGridSelectionEntry.EntryId;
+
+ if(inventorySubMenu != null && itemGridSelectionEntry.SyncedEntity != Entity.Null)
+ {
+ if(entityManager.HasComponent(itemGridSelectionEntry.SyncedEntity))
+ {
+ var prefabCollectionSystem = client.GetExistingSystem();
+ var prefabLookupMap = prefabCollectionSystem.PrefabLookupMap;
+
+ repairItemGUIDs = new();
+ var durability = entityManager.GetComponentData(itemGridSelectionEntry.SyncedEntity);
+ var repairCosts = durability.GetRepairCost(prefabLookupMap, entityManager);
+ foreach(var repairCost in repairCosts)
+ {
+ repairItemGUIDs.Add(repairCost.Guid);
+ }
+ }
+ if(entityManager.HasComponent(itemGridSelectionEntry.SyncedEntity))
+ {
+ storedBlood = entityManager.GetComponentData(itemGridSelectionEntry.SyncedEntity);
+ }
+ }
+ }
+ else if(workstationRecipeGridSelectionEntry != null)
+ {
+ WorkstationRecipeGridSelectionEntry.Data recipe;
+ if(workstationSubMenu != null)
+ {
+ recipe = workstationSubMenu.RecipesGridSelectionGroup.Entries[workstationRecipeGridSelectionEntry.EntryIndex];
+ }
+ else if(inventorySubMenu != null)
+ {
+ recipe = inventorySubMenu.RecipesGridSelectionGroup.Entries[workstationRecipeGridSelectionEntry.EntryIndex];
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled itemGUID SubMenu for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+ if(gameDataSystem.RecipeHashLookupMap.ContainsKey(recipe.EntryId))
+ {
+ var recipeData = gameDataSystem.RecipeHashLookupMap[recipe.EntryId];
+ if(entityManager.HasComponent(recipeData.Entity))
+ {
+ var requirements = entityManager.GetBuffer(recipeData.Entity);
+ requiredItemGUIDs = new();
+ foreach(var requirement in requirements)
+ {
+ requiredItemGUIDs.Add(requirement.Guid);
+ }
+ }
+ if(entityManager.HasComponent(recipeData.Entity))
+ {
+ itemGUID = entityManager.GetBuffer(recipeData.Entity)[0].Guid;
+ }
+ }
+ }
+ else if(researchEntry != null)
+ {
+ foreach(var category in researchstationSubMenu.ResearchCategories)
+ {
+ if(category.ResearchGridSelectionGroup.Entries.Count > researchEntry.EntryIndex &&
+ category.ResearchGridSelectionGroup.Entries[researchEntry.EntryIndex].EntryId == researchEntry.EntryId)
+ {
+ var recipe = category.ResearchGridSelectionGroup.Entries[researchEntry.EntryIndex];
+ requiredItemGUIDs = new();
+ foreach(var requiredItem in recipe.Requirements)
+ {
+ if(Utils.TryGetPrefabGUIDForItemName(gameDataSystem, requiredItem.ItemName, out var requiredItemGUID))
+ {
+ requiredItemGUIDs.Add(requiredItemGUID);
+ }
+ }
+ break;
+ }
+ }
+ }
+ else if(buildMenuStructureEntry != null)
+ {
+ if(gameDataSystem.BlueprintHashLookupMap.ContainsKey(buildMenuStructureEntry.PrefabGuid))
+ {
+ var blueprintData = gameDataSystem.BlueprintHashLookupMap[buildMenuStructureEntry.PrefabGuid];
+ if(entityManager.HasComponent(blueprintData.Entity))
+ {
+ requiredItemGUIDs = new();
+ var requirements = entityManager.GetBuffer(blueprintData.Entity);
+ foreach(var requirement in requirements)
+ {
+ requiredItemGUIDs.Add(requirement.PrefabGUID);
+ }
+ }
+ }
+ }
+ else
+ {
+#if DEBUG
+ Utils.Logger.LogMessage($"Unknown/unhandled ItemGUID Retrieval for Type: {__instance.GetScriptClassName()}");
+#endif
+ return;
+ }
+
+ int? stashCount = itemGUID == null ? null : Utils.GetStashItemCount(entityManager, teamChecker, character, itemGUID.Value, storedBlood);
+
+ var requiredItemStashCount = requiredItemGUIDs?.Select(x => Utils.GetStashItemCount(entityManager, teamChecker, character, x)).ToList();
+ var repairItemStashCount = repairItemGUIDs?.Select(x => Utils.GetStashItemCount(entityManager, teamChecker, character, x)).ToList();
+
+ if((stashCount.HasValue && stashCount.Value == -1) ||
+ (requiredItemStashCount != null && requiredItemStashCount.Contains(-1)) ||
+ (repairItemStashCount != null && repairItemStashCount.Contains(-1)))
+ {
+ // Don't add stash values outside of the castle, we can't count them anyway!
+ return;
+ }
+
+ var id = tooltip.GetInstanceID();
+ bool shouldStartRoutine = _tooltipInfo.TryGetValue(id, out var record);
+ _tooltipInfo[id] = (DateTime.UtcNow, stashCount, requiredItemStashCount, repairItemStashCount);
+ if(shouldStartRoutine || DateTime.UtcNow.Subtract(record.creationTime).TotalSeconds >= 0.2f)
+ {
+ __instance.StartCoroutine(UpdateTooltip(tooltip));
+ }
+ }
+
+ private static readonly Dictionary requiredItemStashCount, List repairItemStashCount)> _tooltipInfo = new();
+
+ private static IEnumerator UpdateTooltip(FakeTooltip tooltip)
+ {
+ var id = tooltip.GetInstanceID();
+ var lastUpdateTime = DateTime.MinValue;
+ int? stashCount;
+ List requiredItemStashCount;
+ List repairItemStashCount;
+
+ while(_tooltipInfo.TryGetValue(id, out var record) && (lastUpdateTime != record.creationTime || (!AllTextsContainStashInfo() && DateTime.UtcNow.Subtract(lastUpdateTime).TotalSeconds < 10f)))
+ {
+ yield return null;
+
+ if(tooltip == null || tooltip.Name == null || tooltip.Name.Text == null || !_tooltipInfo.ContainsKey(id))
+ {
+ break;
+ }
+
+ (lastUpdateTime, stashCount, requiredItemStashCount, repairItemStashCount) = _tooltipInfo[id];
+
+ if(stashCount.HasValue)
+ {
+ tooltip.Name.Text.SetText($"{tooltip.Name._LocalizedString.Text} (Stash: {stashCount.Value})");
+ }
+
+ if(requiredItemStashCount != null)
+ {
+ for(int i = 0; i < requiredItemStashCount.Count; i++)
+ {
+ var requiredItem = tooltip.RequiredItemsList[i];
+ requiredItem.Name.Text.SetText($"{requiredItem.Name._LocalizedString.Text} (Stash: {requiredItemStashCount[i]})");
+ }
+ }
+
+ if(repairItemStashCount != null)
+ {
+ for(int i = 0; i < repairItemStashCount.Count; i++)
+ {
+ var requiredItem = tooltip.RepairCostList[i];
+ requiredItem.Name.Text.SetText($"{requiredItem.Name._LocalizedString.Text} (Stash: {repairItemStashCount[i]})");
+ }
+ }
+
+ var endTime = Time.realtimeSinceStartup + 0.2f;
+ while(endTime > Time.realtimeSinceStartup && _tooltipInfo.TryGetValue(id, out record) && lastUpdateTime == record.creationTime && AllTextsContainStashInfo())
+ {
+ yield return null;
+ }
+ }
+
+ _tooltipInfo.Remove(id);
+
+ // Nested Method(s)
+ bool AllTextsContainStashInfo()
+ {
+ if(tooltip == null || tooltip.Name == null || tooltip.Name.Text == null || !tooltip.isActiveAndEnabled)
+ {
+ return true;
+ }
+
+ string endPhrase = "";
+
+ if(!tooltip.Name.Text.text.EndsWith(endPhrase))
+ {
+ return false;
+ }
+
+ // Seems to cause some kind of infinite loop???
+ /*foreach(var requiredItem in tooltip.RequiredItemsList)
+ {
+ if(!requiredItem.isActiveAndEnabled)
+ {
+ continue;
+ }
+ if(!requiredItem.Name.Text.text.EndsWith(endPhrase))
+ {
+ return false;
+ }
+ }
+
+ foreach(var repairItem in tooltip.RepairItemsList)
+ {
+ if(!repairItem.isActiveAndEnabled)
+ {
+ continue;
+ }
+ if(!repairItem.Name.Text.text.EndsWith(endPhrase))
+ {
+ return false;
+ }
+ }
+ */
+
+ return true;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/ResourceStashWithdrawal/Plugin.cs b/ResourceStashWithdrawal/Plugin.cs
new file mode 100644
index 0000000..f759d79
--- /dev/null
+++ b/ResourceStashWithdrawal/Plugin.cs
@@ -0,0 +1,50 @@
+using BepInEx;
+using BepInEx.IL2CPP;
+using HarmonyLib;
+using System.Reflection;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.ResourceStashWithdrawal
+{
+ [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()
+ {
+ Utils.Initialize(Log, PluginInfo.PLUGIN_NAME);
+ ResourceStashWithdrawalConfig.Initialize(Config);
+ if(VWorld.IsClient)
+ {
+ UIClickHook.Reset();
+ }
+
+ ResourceStashWithdrawalSystem.Initialize();
+
+ _hooks = Harmony.CreateAndPatchAll(Assembly.GetExecutingAssembly());
+
+ Log.LogInfo($"Plugin {PluginInfo.PLUGIN_NAME} (v{PluginInfo.PLUGIN_VERSION}) is loaded!");
+ }
+
+ public sealed override bool Unload()
+ {
+ _hooks?.UnpatchSelf();
+ ResourceStashWithdrawalSystem.Deinitialize();
+ Config.Clear();
+ Utils.Deinitialize();
+ return true;
+ }
+
+ #endregion
+ }
+}
diff --git a/ResourceStashWithdrawal/ResourceStashWithdrawal.csproj b/ResourceStashWithdrawal/ResourceStashWithdrawal.csproj
new file mode 100644
index 0000000..f7a410a
--- /dev/null
+++ b/ResourceStashWithdrawal/ResourceStashWithdrawal.csproj
@@ -0,0 +1,469 @@
+
+
+ netstandard2.1
+ VMods.ResourceStashWithdrawal
+ VMods.ResourceStashWithdrawal
+ Allows a player to withdraw required resources from his/her stash when clicking a recipe at a work or refinement station
+ 0.0.1
+ 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/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalRequest.cs b/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalRequest.cs
new file mode 100644
index 0000000..89a22d2
--- /dev/null
+++ b/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalRequest.cs
@@ -0,0 +1,8 @@
+namespace VMods.ResourceStashWithdrawal
+{
+ public struct ResourceStashWithdrawalRequest
+ {
+ public int ItemGUIDHash;
+ public int Amount;
+ }
+}
diff --git a/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalSystem.cs b/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalSystem.cs
new file mode 100644
index 0000000..2f47562
--- /dev/null
+++ b/ResourceStashWithdrawal/Systems/ResourceStashWithdrawalSystem.cs
@@ -0,0 +1,109 @@
+using ProjectM;
+using ProjectM.Network;
+using ProjectM.Scripting;
+using System;
+using Unity.Entities;
+using VMods.Shared;
+using Wetstone.API;
+
+namespace VMods.ResourceStashWithdrawal
+{
+ public static class ResourceStashWithdrawalSystem
+ {
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ VNetworkRegistry.RegisterServerboundStruct(OnResourceStashWithdrawalRequest);
+ }
+
+ public static void Deinitialize()
+ {
+ VNetworkRegistry.UnregisterStruct();
+ }
+
+ private static void OnResourceStashWithdrawalRequest(FromCharacter fromCharacter, ResourceStashWithdrawalRequest request)
+ {
+ if(!VWorld.IsServer || fromCharacter.Character == Entity.Null)
+ {
+ // This isn't running on a server, or a non-existing character made the request -> stop trying to move items
+ return;
+ }
+
+ var server = VWorld.Server;
+ var gameManager = server.GetExistingSystem()?._ServerGameManager;
+ var teamChecker = gameManager._TeamChecker;
+ var gameDataSystem = server.GetExistingSystem();
+ var itemHashLookupMap = gameDataSystem.ItemHashLookupMap;
+ var prefabCollectionSystem = server.GetExistingSystem();
+ var prefabLookupMap = prefabCollectionSystem.PrefabLookupMap;
+ var entityManager = server.EntityManager;
+
+ if(!InventoryUtilities.TryGetInventoryEntity(entityManager, fromCharacter.Character, out Entity playerInventory) || playerInventory == Entity.Null)
+ {
+ // Player inventory couldn't be found -> stop trying to move items
+ return;
+ }
+
+ var remainingAmount = request.Amount;
+
+ var stashes = Utils.GetAlliedStashes(entityManager, teamChecker, fromCharacter.Character);
+ foreach(var stash in stashes)
+ {
+ var stashInventory = entityManager.GetBuffer(stash);
+
+ for(int i = 0; i < stashInventory.Length; i++)
+ {
+ var stashItem = stashInventory[i];
+
+ // Only withdraw the requested item
+ if(stashItem.ItemType.GuidHash != request.ItemGUIDHash)
+ {
+ continue;
+ }
+
+ var transferAmount = Math.Min(remainingAmount, stashItem.Stacks);
+ if(!Utils.TryGiveItem(entityManager, itemHashLookupMap, playerInventory, stashItem.ItemType, transferAmount, out int remainingStacks, out _))
+ {
+ // Failed to add the item(s) to the player's inventory -> stop trying to move any items at all
+ return;
+ }
+ transferAmount -= remainingStacks;
+ if(!InventoryUtilitiesServer.TryRemoveItem(entityManager, stash, stashItem.ItemType, transferAmount))
+ {
+ // Failed to remove the item from the stash -> Remove the items from the player's inventory & stop trying to move any items at all
+ InventoryUtilitiesServer.TryRemoveItem(entityManager, playerInventory, stashItem.ItemType, transferAmount);
+ return;
+ }
+
+ InventoryUtilitiesServer.CreateInventoryChangedEvent(entityManager, fromCharacter.Character, stashItem.ItemType, stashItem.Stacks, InventoryChangedEventType.Moved);
+ remainingAmount -= transferAmount;
+ if(remainingAmount <= 0)
+ {
+ break;
+ }
+ }
+
+ if(remainingAmount <= 0)
+ {
+ break;
+ }
+ }
+
+ if(remainingAmount > 0)
+ {
+ var name = Utils.GetItemName(new PrefabGUID(request.ItemGUIDHash), gameDataSystem, entityManager, prefabLookupMap);
+ if(remainingAmount == request.Amount)
+ {
+ Utils.SendMessage(fromCharacter.User, $"Couldn't find any {name} in the stash(es).", ServerChatMessageType.System);
+ }
+ else
+ {
+ Utils.SendMessage(fromCharacter.User, $"Couldn't find all {name} in the stash(es). {remainingAmount} {(remainingAmount == 1 ? "is" : "are")} missing.", ServerChatMessageType.System);
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/BuffSystemHook.cs b/Shared/BuffSystemHook.cs
new file mode 100644
index 0000000..36088ce
--- /dev/null
+++ b/Shared/BuffSystemHook.cs
@@ -0,0 +1,43 @@
+using HarmonyLib;
+using ProjectM;
+using Unity.Collections;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ [HarmonyPatch]
+ public static class BuffSystemHook
+ {
+ #region Events
+
+ public delegate void ProcessBuffEventHandler(Entity entity, PrefabGUID buffGUID);
+ public static event ProcessBuffEventHandler ProcessBuffEvent;
+ private static void FireProcessBuffEvent(Entity entity, PrefabGUID buffGUID) => ProcessBuffEvent?.Invoke(entity, buffGUID);
+
+ #endregion
+
+ #region Private Methods
+
+ [HarmonyPatch(typeof(BuffSystem_Spawn_Server), nameof(BuffSystem_Spawn_Server.OnUpdate))]
+ [HarmonyPrefix]
+ private static void OnUpdate(BuffSystem_Spawn_Server __instance)
+ {
+ if(!VWorld.IsServer || __instance.__OnUpdate_LambdaJob0_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = __instance.EntityManager;
+
+ var entities = __instance.__OnUpdate_LambdaJob0_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ PrefabGUID buffGUID = entityManager.GetComponentData(entity);
+ FireProcessBuffEvent(entity, buffGUID);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/CommandSystem/Command.cs b/Shared/CommandSystem/Command.cs
new file mode 100644
index 0000000..87a89b6
--- /dev/null
+++ b/Shared/CommandSystem/Command.cs
@@ -0,0 +1,34 @@
+using ProjectM.Network;
+using Unity.Entities;
+
+namespace VMods.Shared
+{
+ public class Command
+ {
+ #region Properties
+
+ public string Name { get; }
+ public string[] Args { get; }
+
+ public User User { get; }
+ public Entity SenderUserEntity { get; }
+ public Entity SenderCharEntity { get; }
+
+ public bool Used { get; private set; }
+
+ #endregion
+
+ #region Lifecycle
+
+ public Command(User user, Entity senderUserEntity, Entity senderCharEntity, string name, params string[] args)
+ => (User, SenderUserEntity, SenderCharEntity, Name, Args) = (user, senderUserEntity, senderCharEntity, name, args);
+
+ #endregion
+
+ #region Public Methods
+
+ public void Use() => Used = true;
+
+ #endregion
+ }
+}
diff --git a/Shared/CommandSystem/CommandAttribute.cs b/Shared/CommandSystem/CommandAttribute.cs
new file mode 100644
index 0000000..8689557
--- /dev/null
+++ b/Shared/CommandSystem/CommandAttribute.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace VMods.Shared
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class CommandAttribute : Attribute
+ {
+ #region Properties
+
+ public IReadOnlyList Names { get; }
+ public string Usage { get; }
+ public string Description { get; }
+ public bool ReqAdmin { get; }
+
+ #endregion
+
+ #region Livecycle
+
+ public CommandAttribute(string name, string usage = "", string description = "", bool reqAdmin = false)
+ {
+ Names = name.Split(',').Select(x => x.Trim()).ToList();
+ Usage = usage;
+ Description = description;
+ ReqAdmin = reqAdmin;
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/CommandSystem/CommandExtensions.cs b/Shared/CommandSystem/CommandExtensions.cs
new file mode 100644
index 0000000..1d5126a
--- /dev/null
+++ b/Shared/CommandSystem/CommandExtensions.cs
@@ -0,0 +1,38 @@
+using ProjectM.Network;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ public static class CommandExtensions
+ {
+ public static (string searchUsername, FromCharacter? fromCharacter) GetFromCharacter(this Command command, int argIdx = 0, bool sendCannotBeFoundMessage = true, EntityManager? entityManager = null)
+ {
+ FromCharacter? fromCharacter;
+ string searchUsername;
+
+ entityManager ??= Utils.CurrentWorld.EntityManager;
+
+ if(argIdx >= 0 && command.Args.Length >= (argIdx + 1))
+ {
+ searchUsername = command.Args[0];
+ fromCharacter = Utils.GetFromCharacter(searchUsername, entityManager);
+ }
+ else
+ {
+ searchUsername = command.User.CharacterName.ToString();
+ fromCharacter = new FromCharacter()
+ {
+ User = command.SenderUserEntity,
+ Character = command.SenderCharEntity,
+ };
+ }
+
+ if(sendCannotBeFoundMessage && !fromCharacter.HasValue)
+ {
+ command.User.SendSystemMessage($"[{Utils.PluginName}] Vampire {searchUsername} couldn't be found.");
+ }
+ return (searchUsername, fromCharacter);
+ }
+ }
+}
diff --git a/Shared/CommandSystem/CommandSystem.cs b/Shared/CommandSystem/CommandSystem.cs
new file mode 100644
index 0000000..afb708b
--- /dev/null
+++ b/Shared/CommandSystem/CommandSystem.cs
@@ -0,0 +1,265 @@
+using ProjectM.Network;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Wetstone.API;
+using Wetstone.Hooks;
+
+namespace VMods.Shared
+{
+ public static class CommandSystem
+ {
+ #region Consts
+
+ private const char CommandSplitChar = ' ';
+
+ #endregion
+
+ #region Variables
+
+ private static readonly Dictionary _lastUsedCommandTimes = new();
+
+ private static List<(MethodInfo method, CommandAttribute attribute)> _commandReflectionMethods;
+ private static readonly List<(Action method, CommandAttribute attribute)> _commandMethods = new();
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ if(!VWorld.IsServer)
+ {
+ Utils.Logger.LogMessage($"{nameof(CommandSystem)} only needs to be called server-side.");
+ return;
+ }
+
+ _commandReflectionMethods = Assembly.GetExecutingAssembly().GetTypes().SelectMany(x => x.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Where(y => y.GetCustomAttributes(false).Count() > 0)).Select(x => (x, x.GetCustomAttribute(false))).ToList();
+
+ Chat.OnChatMessage += OnChatMessage;
+ }
+
+ public static void Deinitialize()
+ {
+ Chat.OnChatMessage -= OnChatMessage;
+ _commandReflectionMethods?.Clear();
+ _commandMethods.Clear();
+ }
+
+ public static void RegisterCommand(Action commandMethod, CommandAttribute commandAttribute)
+ {
+ _commandMethods.Add((commandMethod, commandAttribute));
+ }
+
+ public static void UnregisterCommand(Action commandMethod, CommandAttribute commandAttribute)
+ {
+ _commandMethods.RemoveAll(x => x.method == commandMethod && x.attribute == commandAttribute);
+ }
+
+ public static void SendInvalidCommandMessage(Command command, bool invalidArgument = false)
+ {
+ SendInvalidCommandMessage(command.User, invalidArgument);
+ }
+
+ public static void SendInvalidCommandMessage(User user, bool invalidArgument = false)
+ {
+ user.SendSystemMessage($"Invalid command{(invalidArgument ? " argument" : string.Empty)}. Check {CommandSystemConfig.CommandSystemPrefix.Value}help [] for more information.");
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void OnChatMessage(VChatEvent chatEvent)
+ {
+ if(!CommandSystemConfig.CommandSystemEnabled.Value)
+ {
+ return;
+ }
+ if(chatEvent.Cancelled)
+ {
+ return;
+ }
+ string commandPrefix = CommandSystemConfig.CommandSystemPrefix.Value;
+ string message = chatEvent.Message;
+ if(chatEvent.Cancelled || !message.StartsWith(commandPrefix, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ PruneCommandTimes();
+
+ // Extract the command name and arguments
+ string name;
+ string[] args;
+ if(message.Contains(CommandSplitChar))
+ {
+ var splitted = message.Split(CommandSplitChar);
+ name = splitted[0][commandPrefix.Length..];
+ args = splitted.Skip(1).ToArray();
+ }
+ else
+ {
+ name = message[commandPrefix.Length..];
+ args = new string[0];
+ }
+
+ // Anti-spam for non-admins
+ var user = chatEvent.User;
+ if(!user.IsAdmin)
+ {
+ if(_lastUsedCommandTimes.TryGetValue(user.PlatformId, out var lastUsedCommandTime))
+ {
+ var timeDiff = DateTime.UtcNow.Subtract(lastUsedCommandTime);
+ if(timeDiff.TotalSeconds < CommandSystemConfig.CommandSystemCommandCooldown.Value)
+ {
+ int waitTime = (int)Math.Ceiling(CommandSystemConfig.CommandSystemCommandCooldown.Value - timeDiff.TotalSeconds);
+ chatEvent.User.SendSystemMessage($"Please wait for {waitTime} second(s) before sending another command.");
+ chatEvent.Cancel();
+ return;
+ }
+ }
+ _lastUsedCommandTimes[user.PlatformId] = DateTime.UtcNow;
+ }
+
+ // Fire the command (so an event handler can actually handle/execute it)
+ Command command = new(user, chatEvent.SenderUserEntity, chatEvent.SenderCharacterEntity, name, args);
+
+ foreach((var method, var attribute) in _commandMethods)
+ {
+ if(!attribute.Names.Contains(command.Name) || (attribute.ReqAdmin && !user.IsAdmin))
+ {
+ continue;
+ }
+ try
+ {
+ method.Invoke(command);
+ }
+ catch(Exception ex)
+ {
+ SendInvalidCommandMessage(command);
+ throw ex;
+ }
+ if(command.Used)
+ {
+ break;
+ }
+ }
+
+ if(!command.Used)
+ {
+ foreach((var method, var attribute) in _commandReflectionMethods)
+ {
+ if(!attribute.Names.Contains(command.Name) || (attribute.ReqAdmin && !user.IsAdmin))
+ {
+ continue;
+ }
+ try
+ {
+ method.Invoke(null, new[] { command });
+ }
+ catch(Exception ex)
+ {
+ SendInvalidCommandMessage(command);
+ throw ex;
+ }
+ if(command.Used)
+ {
+ break;
+ }
+ }
+ }
+
+ if(command.Used)
+ {
+ chatEvent.Cancel();
+ }
+ }
+
+ private static void PruneCommandTimes()
+ {
+ var now = DateTime.UtcNow;
+ var keys = _lastUsedCommandTimes.Keys.ToList();
+ var cooldown = CommandSystemConfig.CommandSystemCommandCooldown.Value;
+ foreach(var key in keys)
+ {
+ var lastUsedCommandTime = _lastUsedCommandTimes[key];
+ if(now.Subtract(lastUsedCommandTime).TotalSeconds >= cooldown)
+ {
+ _lastUsedCommandTimes.Remove(key);
+ }
+ }
+ }
+
+ [Command("help", "help []", "Shows a list of commands, or details about a command.")]
+ private static void OnHelpCommand(Command command)
+ {
+ var commandPrefix = CommandSystemConfig.CommandSystemPrefix.Value;
+ var user = command.User;
+ switch(command.Args.Length)
+ {
+ case 0:
+ {
+ user.SendSystemMessage($"List of {Utils.PluginName} commands:");
+ _commandMethods.ForEach(x => SendCommandInfo(x.attribute));
+ _commandReflectionMethods.ForEach(x => SendCommandInfo(x.attribute));
+
+ // Nested Method(s)
+ void SendCommandInfo(CommandAttribute attribute)
+ {
+ if(attribute.ReqAdmin && !user.IsAdmin)
+ {
+ return;
+ }
+ string message = $"{string.Join(", ", attribute.Names.Select(x => $"{commandPrefix}{x}"))}";
+ if(attribute.ReqAdmin)
+ {
+ message += " - [ADMIN]";
+ }
+ message += $" - {attribute.Description}";
+ user.SendSystemMessage(message);
+ }
+ }
+ break;
+
+ case 1:
+ {
+ string searchCommandName = command.Args[0];
+
+ // Find the command info
+ CommandAttribute attribute = null;
+ if(_commandMethods.Exists(x => x.attribute.Names.Contains(searchCommandName)))
+ {
+ attribute = _commandMethods.Find(x => x.attribute.Names.Contains(searchCommandName)).attribute;
+ }
+ else if(_commandReflectionMethods.Exists(x => x.attribute.Names.Contains(searchCommandName)))
+ {
+ attribute = _commandReflectionMethods.Find(x => x.attribute.Names.Contains(searchCommandName)).attribute;
+ }
+
+ // Check the found info
+ if(attribute == null || attribute.ReqAdmin && !user.IsAdmin)
+ {
+ return;
+ }
+
+ user.SendSystemMessage($"Help for {commandPrefix}{attribute.Names[0]}");
+ if(attribute.Names.Count > 1)
+ {
+ user.SendSystemMessage($"Aliases: {string.Join(", ", attribute.Names.Skip(1).Select(x => $"{commandPrefix}{x}"))}");
+ }
+ user.SendSystemMessage($"Description: {attribute.Description}");
+ user.SendSystemMessage($"Usage: {commandPrefix}{attribute.Usage}");
+ }
+ return;
+
+ default:
+ SendInvalidCommandMessage(command);
+ return;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/CommandSystem/CommandSystemConfig.cs b/Shared/CommandSystem/CommandSystemConfig.cs
new file mode 100644
index 0000000..0058b5a
--- /dev/null
+++ b/Shared/CommandSystem/CommandSystemConfig.cs
@@ -0,0 +1,26 @@
+using BepInEx.Configuration;
+
+namespace VMods.Shared
+{
+ public static class CommandSystemConfig
+ {
+ #region Properties
+
+ public static ConfigEntry CommandSystemEnabled { get; private set; }
+ public static ConfigEntry CommandSystemPrefix { get; private set; }
+ public static ConfigEntry CommandSystemCommandCooldown { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ CommandSystemEnabled = config.Bind(nameof(CommandSystemConfig), nameof(CommandSystemEnabled), true, "Enabled/disable the Commands system (for this specific mod).");
+ CommandSystemPrefix = config.Bind(nameof(CommandSystemConfig), nameof(CommandSystemPrefix), "!", "The prefix that needs to be used to execute a command (for this specific mod).");
+ CommandSystemCommandCooldown = config.Bind(nameof(CommandSystemConfig), nameof(CommandSystemCommandCooldown), 5f, "The amount of seconds between two commands (for non-admins).");
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/ExtensionMethods.cs b/Shared/ExtensionMethods.cs
new file mode 100644
index 0000000..a7f9f39
--- /dev/null
+++ b/Shared/ExtensionMethods.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace VMods.Shared
+{
+ public static class ExtensionMethods
+ {
+ public static string ToAgoString(this TimeSpan timeSpan)
+ {
+ if(timeSpan.TotalDays >= 1d)
+ {
+ return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours}h {timeSpan.Minutes}m {timeSpan.Seconds}s";
+ }
+ else if(timeSpan.TotalHours >= 1d)
+ {
+ return $"{timeSpan.Hours}h {timeSpan.Minutes}m {timeSpan.Seconds}s";
+ }
+ else if(timeSpan.TotalMinutes >= 1d)
+ {
+ return $"{timeSpan.Minutes}m {timeSpan.Seconds}s";
+ }
+ else if(timeSpan.TotalSeconds >= 1d)
+ {
+ return $"{timeSpan.Seconds}s";
+ }
+ return $"{timeSpan.Milliseconds}ms";
+ }
+ }
+}
diff --git a/Shared/HighestGearScoreSystem/EquipmentHooks.cs b/Shared/HighestGearScoreSystem/EquipmentHooks.cs
new file mode 100644
index 0000000..421b07d
--- /dev/null
+++ b/Shared/HighestGearScoreSystem/EquipmentHooks.cs
@@ -0,0 +1,218 @@
+using HarmonyLib;
+using ProjectM;
+using ProjectM.Network;
+using System.Collections.Generic;
+using Unity.Collections;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ [HarmonyPatch]
+ public static class EquipmentHooks
+ {
+ #region Events
+
+ public delegate void EquipmentChangedEventHandler(FromCharacter fromCharacter);
+ public static event EquipmentChangedEventHandler EquipmentChangedEvent;
+ private static void FireEquipmentChangedEvent(FromCharacter fromCharacter) => EquipmentChangedEvent?.Invoke(fromCharacter);
+
+ #endregion
+
+ #region Private Methods
+
+ [HarmonyPatch(typeof(EquipItemSystem), nameof(EquipItemSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void EquipItem(EquipItemSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__OnUpdate_LambdaJob0_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__OnUpdate_LambdaJob0_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ FireEquipmentChangedEvent(fromCharacter);
+ }
+ }
+
+ [HarmonyPatch(typeof(EquipItemFromInventorySystem), nameof(EquipItemFromInventorySystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void EquipItemFromInventory(EquipItemFromInventorySystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__EquipItemFromInventoryJob_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__EquipItemFromInventoryJob_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ FireEquipmentChangedEvent(fromCharacter);
+ }
+ }
+
+ [HarmonyPatch(typeof(UnequipItemSystem), nameof(UnequipItemSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void UnequipItem(UnequipItemSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__OnUpdate_LambdaJob0_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__OnUpdate_LambdaJob0_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ FireEquipmentChangedEvent(fromCharacter);
+ }
+ }
+
+ [HarmonyPatch(typeof(MoveItemBetweenInventoriesSystem), nameof(MoveItemBetweenInventoriesSystem.OnUpdate))]
+ private static class MoveItemBetweenInventories
+ {
+ private static void Prefix(MoveItemBetweenInventoriesSystem __instance, out List __state)
+ {
+ __state = new List();
+ if(!VWorld.IsServer || __instance._MoveItemBetweenInventoriesEventQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance._MoveItemBetweenInventoriesEventQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ if(!__state.Contains(fromCharacter))
+ {
+ __state.Add(fromCharacter);
+ }
+ }
+ }
+
+ private static void Postfix(List __state)
+ {
+ __state.ForEach(FireEquipmentChangedEvent);
+ }
+ }
+
+ [HarmonyPatch(typeof(MoveAllItemsBetweenInventoriesSystem), nameof(MoveAllItemsBetweenInventoriesSystem.OnUpdate))]
+ private static class MoveAllItemsBetweenInventories
+ {
+ private static void Prefix(MoveAllItemsBetweenInventoriesSystem __instance, out List __state)
+ {
+ __state = new List();
+ if(!VWorld.IsServer || __instance.__MoveAllItemsJob_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__MoveAllItemsJob_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ if(!__state.Contains(fromCharacter))
+ {
+ __state.Add(fromCharacter);
+ }
+ }
+ }
+
+ private static void Postfix(List __state)
+ {
+ __state.ForEach(FireEquipmentChangedEvent);
+ }
+ }
+
+ [HarmonyPatch(typeof(DropInventoryItemSystem), nameof(DropInventoryItemSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void DropInventoryItem(DropInventoryItemSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__DropInventoryItemJob_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__DropInventoryItemJob_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ FireEquipmentChangedEvent(fromCharacter);
+ }
+ }
+
+ [HarmonyPatch(typeof(DropItemSystem), nameof(DropItemSystem.OnUpdate))]
+ private static class DropItem
+ {
+ private static void Prefix(DropItemSystem __instance, out List __state)
+ {
+ __state = new List();
+ if(!VWorld.IsServer || __instance.__DropEquippedItemJob_entityQuery == null || __instance.__DropEquippedItemJob_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__DropEquippedItemJob_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ if(!__state.Contains(fromCharacter))
+ {
+ __state.Add(fromCharacter);
+ }
+ }
+
+ entities = __instance.__DropItemsJob_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var fromCharacter = entityManager.GetComponentData(entity);
+ if(!__state.Contains(fromCharacter))
+ {
+ __state.Add(fromCharacter);
+ }
+ }
+ }
+
+ private static void Postfix(List __state)
+ {
+ __state.ForEach(FireEquipmentChangedEvent);
+ }
+ }
+
+ [HarmonyPatch(typeof(ItemPickupSystem), nameof(ItemPickupSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void ItemPickup(ItemPickupSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__OnUpdate_LambdaJob0_entityQuery == null)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var entities = __instance.__OnUpdate_LambdaJob0_entityQuery.ToEntityArray(Allocator.Temp);
+ foreach(var entity in entities)
+ {
+ var ownerData = entityManager.GetComponentData(entity);
+ var characterEntity = ownerData.Owner;
+ var playerCharacter = entityManager.GetComponentData(characterEntity);
+ FireEquipmentChangedEvent(new FromCharacter()
+ {
+ Character = characterEntity,
+ User = playerCharacter.UserEntity._Entity,
+ });
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs b/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs
new file mode 100644
index 0000000..1b5e6d3
--- /dev/null
+++ b/Shared/HighestGearScoreSystem/HighestGearScoreSystem.cs
@@ -0,0 +1,184 @@
+using ProjectM;
+using ProjectM.Network;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ public static class HighestGearScoreSystem
+ {
+ #region Variables
+
+ private static Dictionary _gearScoreData;
+
+ #endregion
+
+ #region Properties
+
+ private static string HighestGearScoreFileName => $"{Utils.PluginName}-HighestGearScore.json";
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize()
+ {
+ _gearScoreData = VModStorage.Load(HighestGearScoreFileName, () => new Dictionary());
+
+ VModStorage.SaveEvent += Save;
+ EquipmentHooks.EquipmentChangedEvent += OnEquipmentChanged;
+ VampireDownedHook.VampireDownedEvent += OnVampireDowned;
+ }
+
+ public static void Deinitialize()
+ {
+ VampireDownedHook.VampireDownedEvent -= OnVampireDowned;
+ EquipmentHooks.EquipmentChangedEvent -= OnEquipmentChanged;
+ VModStorage.SaveEvent -= Save;
+ }
+
+ public static void Save()
+ {
+ PruneHighestGearScores();
+
+ VModStorage.Save(HighestGearScoreFileName, _gearScoreData);
+ }
+
+ public static float GetCurrentOrHighestGearScore(FromCharacter fromCharacter)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ if(HighestGearScoreSystemConfig.HighestGearScoreSystemEnabled.Value)
+ {
+ PruneHighestGearScores();
+
+ var user = entityManager.GetComponentData(fromCharacter.User);
+ if(_gearScoreData.TryGetValue(user.PlatformId, out var gearScoreData))
+ {
+ return gearScoreData.HighestGearScore;
+ }
+ }
+ return GetCurrentGearScore(fromCharacter, entityManager);
+ }
+
+ public static float GetCurrentGearScore(FromCharacter fromCharacter, EntityManager entityManager)
+ {
+ return GetCurrentGearScore(fromCharacter.Character, entityManager);
+ }
+
+ public static float GetCurrentGearScore(Entity characterEntity, EntityManager entityManager)
+ {
+ var equipment = entityManager.GetComponentData(characterEntity);
+ return equipment.ArmorLevel + equipment.WeaponLevel + equipment.SpellLevel;
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ private static void PruneHighestGearScores()
+ {
+ var now = DateTime.UtcNow;
+ var keys = _gearScoreData.Keys.ToList();
+ var duration = HighestGearScoreSystemConfig.HighestGearScoreDuration.Value;
+ foreach(var key in keys)
+ {
+ var gearScoreData = _gearScoreData[key];
+ if(now.Subtract(gearScoreData.LastUpdated).TotalSeconds > duration)
+ {
+ _gearScoreData.Remove(key);
+ }
+ }
+ }
+
+ private static void OnEquipmentChanged(FromCharacter fromCharacter)
+ {
+ if(!HighestGearScoreSystemConfig.HighestGearScoreSystemEnabled.Value)
+ {
+ return;
+ }
+
+ var entityManager = VWorld.Server.EntityManager;
+ var user = entityManager.GetComponentData(fromCharacter.User);
+ if(!_gearScoreData.TryGetValue(user.PlatformId, out var gearScoreData))
+ {
+ gearScoreData = new GearScoreData();
+ _gearScoreData.Add(user.PlatformId, gearScoreData);
+ }
+
+ if(DateTime.UtcNow.Subtract(gearScoreData.LastUpdated).TotalSeconds >= 30f)
+ {
+ gearScoreData.HighestGearScore = 0f;
+ }
+
+ float gearScore = GetCurrentGearScore(fromCharacter.Character, entityManager);
+ gearScoreData.HighestGearScore = Math.Max(gearScoreData.HighestGearScore, gearScore);
+ gearScoreData.LastUpdated = DateTime.UtcNow;
+
+#if DEBUG
+ //var message = $"Highest Gearscore Updated: {gearScoreData.HighestGearScore} (Current: {gearScore})";
+ //Utils.Logger.LogMessage(message);
+ //user.SendSystemMessage($"Highest Gearscore Updated: {gearScoreData.HighestGearScore} (Current: {gearScore})");
+#endif
+ }
+
+ private static void OnVampireDowned(Entity killer, Entity victim)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ var victimCharacter = entityManager.GetComponentData(victim);
+ var victumUserEntity = victimCharacter.UserEntity._Entity;
+ var victumUser = entityManager.GetComponentData(victumUserEntity);
+
+ _gearScoreData.Remove(victumUser.PlatformId);
+ }
+
+ [Command("highestgs,hgs,higs,highgs,highestgearscore", "highestgs []", "Tells you what the highest gear score is for the given player (or yourself when noplayername is given)", true)]
+ private static void OnHighestGearScoreCommand(Command command)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ (var searchUsername, var fromCharacter) = command.GetFromCharacter(entityManager: entityManager);
+
+ if(fromCharacter.HasValue)
+ {
+ var user = entityManager.GetComponentData(fromCharacter.Value.User);
+ if(_gearScoreData.TryGetValue(user.PlatformId, out var gearScoreData))
+ {
+ TimeSpan diff = DateTime.UtcNow.Subtract(gearScoreData.LastUpdated);
+ command.User.SendSystemMessage($"[{Utils.PluginName}] The Highest Gear Score for {searchUsername} (Lv: {GetCurrentGearScore(fromCharacter.Value, entityManager)}) was {gearScoreData.HighestGearScore} (Last updated {diff.ToAgoString()} ago).");
+ }
+ else
+ {
+ command.User.SendSystemMessage($"[{Utils.PluginName}] No Highest Gear Score is recorded for {searchUsername} (Lv: {GetCurrentGearScore(fromCharacter.Value, entityManager)}).");
+ }
+ }
+ }
+
+ [Command("clearhgs,resethgs,clearhighestgearscore,resethighestgearscore", "clearhgs []", "Removes the current Highest Gear Score record for the given player (or yourself when noplayername is given)", true)]
+ private static void OnResetHighestGearScoreCommand(Command command)
+ {
+ var entityManager = VWorld.Server.EntityManager;
+ (var searchUsername, var fromCharacter) = command.GetFromCharacter(entityManager: entityManager);
+
+ if(fromCharacter.HasValue)
+ {
+ var user = entityManager.GetComponentData(fromCharacter.Value.User);
+ _gearScoreData.Remove(user.PlatformId);
+ command.User.SendSystemMessage($"[{Utils.PluginName}] Removed the Highest Gear Score record for {searchUsername}.");
+ }
+ }
+
+ #endregion
+
+ #region Nested
+
+ private class GearScoreData
+ {
+ public float HighestGearScore { get; set; }
+ public DateTime LastUpdated { get; set; }
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/HighestGearScoreSystem/HighestGearScoreSystemConfig.cs b/Shared/HighestGearScoreSystem/HighestGearScoreSystemConfig.cs
new file mode 100644
index 0000000..a7a0581
--- /dev/null
+++ b/Shared/HighestGearScoreSystem/HighestGearScoreSystemConfig.cs
@@ -0,0 +1,24 @@
+using BepInEx.Configuration;
+
+namespace VMods.Shared
+{
+ public static class HighestGearScoreSystemConfig
+ {
+ #region Properties
+
+ public static ConfigEntry HighestGearScoreSystemEnabled { get; private set; }
+ public static ConfigEntry HighestGearScoreDuration { get; private set; }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ConfigFile config)
+ {
+ HighestGearScoreSystemEnabled = config.Bind(nameof(HighestGearScoreSystemConfig), nameof(HighestGearScoreSystemEnabled), true, "Enabled/disable the Highest Gear Score system (for this specific mod).");
+ HighestGearScoreDuration = config.Bind(nameof(HighestGearScoreSystemConfig), nameof(HighestGearScoreDuration), 600f, "The amount of seconds the highest gear score is remembered/stored.");
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/SaveHook.cs b/Shared/SaveHook.cs
new file mode 100644
index 0000000..8364391
--- /dev/null
+++ b/Shared/SaveHook.cs
@@ -0,0 +1,27 @@
+using HarmonyLib;
+using ProjectM;
+
+namespace VMods.Shared
+{
+ [HarmonyPatch]
+ public static class SaveHook
+ {
+ #region Private Methods
+
+ [HarmonyPatch(typeof(TriggerPersistenceSaveSystem), nameof(TriggerPersistenceSaveSystem.TriggerSave))]
+ [HarmonyPrefix]
+ private static void TriggerSave()
+ {
+ VModStorage.SaveAll();
+ }
+
+ [HarmonyPatch(typeof(ServerBootstrapSystem), nameof(ServerBootstrapSystem.OnDestroy))]
+ [HarmonyPrefix]
+ private static void OnDestroy()
+ {
+ VModStorage.SaveAll();
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj
new file mode 100644
index 0000000..f05534d
--- /dev/null
+++ b/Shared/Shared.csproj
@@ -0,0 +1,461 @@
+
+
+ netstandard2.1
+ VMods.Shared
+ VMods.Shared
+ A set of shared classes and utilites for all VMods
+ 0.0.1
+ 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/Shared/Utils.cs b/Shared/Utils.cs
new file mode 100644
index 0000000..9b9a159
--- /dev/null
+++ b/Shared/Utils.cs
@@ -0,0 +1,327 @@
+using BepInEx.Logging;
+using ProjectM;
+using ProjectM.CastleBuilding;
+using ProjectM.Network;
+using ProjectM.UI;
+using StunLocalization;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using UnhollowerRuntimeLib;
+using Unity.Collections;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ public static class Utils
+ {
+ #region Consts
+
+ public static readonly PrefabGUID SevereGarlicDebuff = new(1582196539);
+
+ #endregion
+
+ #region Variables
+
+ private static World _currentWorld = null;
+
+ private static ComponentType[] _containerComponents = null;
+
+ #endregion
+
+ #region Properties
+
+ public static World CurrentWorld => _currentWorld ??= VWorld.IsServer ? VWorld.Server : VWorld.Client;
+
+ public static ManualLogSource Logger { get; private set; }
+ public static string PluginName { get; private set; }
+
+ private static ComponentType[] ContainerComponents
+ {
+ get
+ {
+ if(_containerComponents == null)
+ {
+ _containerComponents = new[]
+ {
+ ComponentType.ReadOnly(Il2CppType.Of()),
+ ComponentType.ReadOnly(Il2CppType.Of()),
+ ComponentType.ReadOnly(Il2CppType.Of()),
+ ComponentType.ReadOnly(Il2CppType.Of()),
+ };
+ }
+ return _containerComponents;
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ public static void Initialize(ManualLogSource logger, string pluginName)
+ {
+ Logger = logger;
+ PluginName = pluginName;
+ }
+
+ public static void Deinitialize()
+ {
+ Logger = null;
+ }
+
+ public static NativeArray GetStashEntities(EntityManager entityManager)
+ {
+ var query = entityManager.CreateEntityQuery(ContainerComponents);
+ return query.ToEntityArray(Allocator.Temp);
+ }
+
+ public static IEnumerable GetAlliedStashes(EntityManager entityManager, TeamCheckerMain teamChecker, Entity character)
+ {
+ foreach(var stash in GetStashEntities(entityManager))
+ {
+ if(teamChecker.IsAllies(character, stash))
+ {
+ yield return stash;
+ }
+ }
+ }
+
+ public static int GetStashItemCount(EntityManager entityManager, TeamCheckerMain teamChecker, Entity character, PrefabGUID itemGUID, StoredBlood? storedBlood = null)
+ {
+ int stashCount = 0;
+ int stashesCounted = 0;
+ var stashes = GetAlliedStashes(entityManager, teamChecker, character);
+ foreach(var stash in stashes)
+ {
+ var stashInventory = entityManager.GetBuffer(stash);
+
+ for(int i = 0; i < stashInventory.Length; i++)
+ {
+ var stashItem = stashInventory[i];
+
+ if(stashItem.ItemType == itemGUID)
+ {
+ if(storedBlood != null)
+ {
+ var itemStoredBlood = entityManager.GetComponentData(stashItem.ItemEntity._Entity);
+ if(storedBlood.Value.BloodType != itemStoredBlood.BloodType || storedBlood.Value.BloodQuality != itemStoredBlood.BloodQuality)
+ {
+ continue;
+ }
+ }
+ stashCount += stashItem.Stacks;
+ }
+ }
+ stashesCounted++;
+ }
+ return stashesCounted == 0 ? -1 : stashCount;
+ }
+
+ public static string GetItemName(PrefabGUID itemGUID, GameDataSystem gameDataSystem = null, EntityManager? entityManager = null, NativeHashMap prefabLookupMap = null)
+ {
+ if(itemGUID == PrefabGUID.Empty)
+ {
+ return string.Empty;
+ }
+ entityManager ??= CurrentWorld.EntityManager;
+ gameDataSystem ??= CurrentWorld.GetExistingSystem();
+ prefabLookupMap ??= CurrentWorld.GetExistingSystem().PrefabLookupMap;
+ try
+ {
+ var itemName = GameplayHelper.TryGetItemName(gameDataSystem, entityManager.Value, prefabLookupMap, itemGUID);
+ if(Localization.HasKey(itemName))
+ {
+ return Localization.Get(itemName);
+ }
+ }
+ catch(Exception)
+ {
+ }
+ return $"[{itemGUID}]";
+ }
+
+ public static void SendMessage(Entity userEntity, string message, ServerChatMessageType messageType)
+ {
+ if(!VWorld.IsServer)
+ {
+ return;
+ }
+ EntityManager em = VWorld.Server.EntityManager;
+ int index = em.GetComponentData(userEntity).Index;
+ NetworkId id = em.GetComponentData(userEntity);
+
+ Entity entity = em.CreateEntity(
+ ComponentType.ReadOnly(),
+ ComponentType.ReadOnly(),
+ ComponentType.ReadOnly()
+ );
+
+ ChatMessageServerEvent ev = new()
+ {
+ MessageText = message,
+ MessageType = messageType,
+ FromUser = id,
+ TimeUTC = DateTime.Now.ToFileTimeUtc()
+ };
+
+ em.SetComponentData(entity, new()
+ {
+ UserIndex = index
+ });
+ em.SetComponentData(entity, new()
+ {
+ EventId = NetworkEvents.EventId_ChatMessageServerEvent,
+ IsAdminEvent = false,
+ IsDebugEvent = false
+ });
+
+ em.SetComponentData(entity, ev);
+ }
+
+ public static bool TryGetPrefabGUIDForItemName(GameDataSystem gameDataSystem, LocalizationKey itemName, out PrefabGUID prefabGUID)
+ => TryGetPrefabGUIDForItemName(gameDataSystem, Localization.Get(itemName), out prefabGUID);
+
+ public static bool TryGetPrefabGUIDForItemName(GameDataSystem gameDataSystem, string itemName, out PrefabGUID prefabGUID)
+ {
+ foreach(var entry in gameDataSystem.ItemHashLookupMap)
+ {
+ var item = gameDataSystem.ManagedDataRegistry.GetOrDefault(entry.Key);
+ if(Localization.Get(item.Name, false) == itemName)
+ {
+ prefabGUID = entry.Key;
+ return true;
+ }
+ }
+ prefabGUID = PrefabGUID.Empty;
+ return false;
+ }
+
+ public static bool TryGiveItem(EntityManager entityManager, NativeHashMap itemDataMap, Entity target, PrefabGUID itemType, int itemStacks, out int remainingStacks, out Entity newEntity, bool dropRemainder = false)
+ {
+ if(!VWorld.IsServer)
+ {
+ remainingStacks = itemStacks;
+ newEntity = Entity.Null;
+ return false;
+ }
+ itemDataMap ??= CurrentWorld.GetExistingSystem().ItemHashLookupMap;
+
+ unsafe
+ {
+ // Some hacky code to create a null-able that won't be GC'ed by the IL2CPP domain.
+ var bytes = stackalloc byte[Marshal.SizeOf()];
+ var bytePtr = new IntPtr(bytes);
+ Marshal.StructureToPtr(new()
+ {
+ value = 7,
+ has_value = true
+ }, bytePtr, false);
+ var boxedBytePtr = IntPtr.Subtract(bytePtr, 0x10);
+ var fakeInt = new Il2CppSystem.Nullable(boxedBytePtr);
+
+ return InventoryUtilitiesServer.TryAddItem(entityManager, itemDataMap, target, itemType, itemStacks, out remainingStacks, out newEntity, startIndex: fakeInt, dropRemainder: dropRemainder);
+ }
+ }
+
+ public static void ApplyBuff(Entity user, Entity character, PrefabGUID buffGUID)
+ {
+ ApplyBuff(new FromCharacter()
+ {
+ User = user,
+ Character = character,
+ }, buffGUID);
+ }
+
+ public static void ApplyBuff(FromCharacter fromCharacter, PrefabGUID buffGUID)
+ {
+ var des = VWorld.Server.GetExistingSystem();
+ var buffEvent = new ApplyBuffDebugEvent()
+ {
+ BuffPrefabGUID = buffGUID
+ };
+ des.ApplyBuff(fromCharacter, buffEvent);
+ }
+
+ public static void RemoveBuff(FromCharacter fromCharacter, PrefabGUID buffGUID)
+ {
+ RemoveBuff(fromCharacter.Character, buffGUID);
+ }
+
+ public static void RemoveBuff(Entity charEntity, PrefabGUID buffGUID)
+ {
+ var entityManager = CurrentWorld.EntityManager;
+ if(BuffUtility.HasBuff(entityManager, charEntity, buffGUID))
+ {
+ BuffUtility.TryGetBuff(entityManager, charEntity, buffGUID, out var buffEntity);
+ entityManager.AddComponent(buffEntity);
+ }
+ }
+
+ public static string GetCharacterName(ulong platformId, EntityManager? entityManager = null)
+ {
+ entityManager ??= CurrentWorld.EntityManager;
+ var users = entityManager.Value.CreateEntityQuery(ComponentType.ReadOnly()).ToEntityArray(Allocator.Temp);
+ foreach(var userEntity in users)
+ {
+ var userData = entityManager.Value.GetComponentData(userEntity);
+ if(userData.PlatformId == platformId)
+ {
+ return userData.CharacterName.ToString();
+ }
+ }
+ return null;
+ }
+
+ public static FromCharacter? GetFromCharacter(string charactername, EntityManager? entityManager = null)
+ {
+ entityManager ??= CurrentWorld.EntityManager;
+ var characters = entityManager.Value.CreateEntityQuery(ComponentType.ReadOnly()).ToEntityArray(Allocator.Temp);
+ foreach(var charEntity in characters)
+ {
+ var playerCharacter = entityManager.Value.GetComponentData(charEntity);
+ var userEntity = playerCharacter.UserEntity._Entity;
+ var userData = entityManager.Value.GetComponentData(userEntity);
+ if(userData.CharacterName.ToString() == charactername)
+ {
+ return new FromCharacter()
+ {
+ User = userEntity,
+ Character = charEntity,
+ };
+ }
+ }
+ return null;
+ }
+
+ public static void LogAllComponentTypes(Entity entity, EntityManager? entityManager = null)
+ {
+ if(entity == Entity.Null)
+ {
+ return;
+ }
+
+ entityManager ??= CurrentWorld.EntityManager;
+
+ Logger.LogMessage($"---");
+ var types = entityManager.Value.GetComponentTypes(entity);
+ foreach(var t in types)
+ {
+ Logger.LogMessage($"Component Type: {t} (Shared? {t.IsSharedComponent}) | {t.GetManagedType().FullName}");
+ }
+ Logger.LogMessage($"---");
+ }
+
+ #endregion
+
+ #region Nested
+
+ private struct FakeNull
+ {
+ public int value;
+ public bool has_value;
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/VModStorage.cs b/Shared/VModStorage.cs
new file mode 100644
index 0000000..853142c
--- /dev/null
+++ b/Shared/VModStorage.cs
@@ -0,0 +1,85 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ public static class VModStorage
+ {
+ #region Consts
+
+ public const string StoragePath = "BepInEx/config/VMods/Storage";
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = false,
+ IncludeFields = false,
+ };
+
+ #endregion
+
+ #region Events
+
+ public delegate void SaveEventHandler();
+ public static event SaveEventHandler SaveEvent;
+ private static void FireSaveEvent() => SaveEvent?.Invoke();
+
+ #endregion
+
+ #region Public Methods
+
+ public static void SaveAll() => FireSaveEvent();
+
+ public static void Save(string filename, T data)
+ {
+ try
+ {
+ File.WriteAllText(Path.Combine(StoragePath, filename), JsonSerializer.Serialize(data, JsonOptions));
+#if DEBUG
+ Utils.Logger.LogInfo($"{filename} has been saved.");
+#endif
+ }
+ catch(Exception ex)
+ {
+ Utils.Logger.LogError($"Failed to save {filename}! - Error: {ex.Message}\r\n{ex.StackTrace}");
+ }
+ }
+
+ public static T Load(string filename, Func getDefaultValue)
+ {
+ try
+ {
+ if(!Directory.Exists(StoragePath))
+ {
+ Directory.CreateDirectory(StoragePath);
+ }
+ var fullPath = Path.Combine(StoragePath, filename);
+ if(!File.Exists(fullPath))
+ {
+ return getDefaultValue();
+ }
+ string json = File.ReadAllText(fullPath);
+ return JsonSerializer.Deserialize(json);
+ }
+ catch(Exception ex)
+ {
+ Utils.Logger.LogError($"Failed to load {filename}! - Error: {ex.Message}\r\n{ex.StackTrace}");
+ return getDefaultValue();
+ }
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ [Command("saveall", "saveall", "Saves all data of all VMod plugins", true)]
+ private static void OnSaveAllCommand(Command command)
+ {
+ SaveAll();
+ command.User.SendSystemMessage($"VMod Plugin '{Utils.PluginName}' saved successfully.");
+ }
+
+ #endregion
+ }
+}
diff --git a/Shared/VampireDownedHook.cs b/Shared/VampireDownedHook.cs
new file mode 100644
index 0000000..5f33ebf
--- /dev/null
+++ b/Shared/VampireDownedHook.cs
@@ -0,0 +1,49 @@
+using HarmonyLib;
+using ProjectM;
+using Unity.Collections;
+using Unity.Entities;
+using Wetstone.API;
+
+namespace VMods.Shared
+{
+ [HarmonyPatch]
+ 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);
+
+ #endregion
+
+ #region Private Methods
+
+ [HarmonyPatch(typeof(VampireDownedServerEventSystem), nameof(VampireDownedServerEventSystem.OnUpdate))]
+ [HarmonyPostfix]
+ private static void OnUpdate(VampireDownedServerEventSystem __instance)
+ {
+ if(!VWorld.IsServer || __instance.__OnUpdate_LambdaJob0_entityQuery == null)
+ {
+ return;
+ }
+
+ EntityManager entityManager = __instance.EntityManager;
+ var eventsQuery = __instance.__OnUpdate_LambdaJob0_entityQuery.ToEntityArray(Allocator.Temp);
+
+ foreach(var entity in eventsQuery)
+ {
+ VampireDownedServerEventSystem.TryFindRootOwner(entity, 1, entityManager, out var victim);
+ Entity source = entityManager.GetComponentData(entity).Source;
+ VampireDownedServerEventSystem.TryFindRootOwner(source, 1, entityManager, out var killer);
+
+ if(entityManager.HasComponent(killer) && entityManager.HasComponent(victim) && !killer.Equals(victim))
+ {
+ FireVampireDownedEvent(killer, victim);
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/VMods.sln b/VMods.sln
new file mode 100644
index 0000000..424c735
--- /dev/null
+++ b/VMods.sln
@@ -0,0 +1,61 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.32602.291
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BloodRefill", "BloodRefill\BloodRefill.csproj", "{44119668-E27A-4864-B5D5-2788FF84BE9E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResourceStashWithdrawal", "ResourceStashWithdrawal\ResourceStashWithdrawal.csproj", "{93569862-92C5-48C2-98F7-AFE5DDE91C8B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecoverEmptyContainers", "RecoverEmptyContainers\RecoverEmptyContainers.csproj", "{2F2580ED-81F0-44D1-B803-82B127BB2633}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExperimentalMod", "ExperimentalMod\ExperimentalMod.csproj", "{695DC4E6-1831-4826-9A64-B85107AB555A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PvPPunishment", "PvPPunishment\PvPPunishment.csproj", "{FBB60595-8F29-49D5-A9A1-0CEA1B9CFF91}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{81DE42A0-8493-49B4-9F0E-353198C1E2AD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PvPLeaderboard", "PvPLeaderboard\PvPLeaderboard.csproj", "{8D3A6ED6-46CD-41CD-BE61-5587F05EAB4E}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {44119668-E27A-4864-B5D5-2788FF84BE9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {44119668-E27A-4864-B5D5-2788FF84BE9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44119668-E27A-4864-B5D5-2788FF84BE9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {44119668-E27A-4864-B5D5-2788FF84BE9E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {93569862-92C5-48C2-98F7-AFE5DDE91C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {93569862-92C5-48C2-98F7-AFE5DDE91C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {93569862-92C5-48C2-98F7-AFE5DDE91C8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {93569862-92C5-48C2-98F7-AFE5DDE91C8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2F2580ED-81F0-44D1-B803-82B127BB2633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2F2580ED-81F0-44D1-B803-82B127BB2633}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2F2580ED-81F0-44D1-B803-82B127BB2633}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2F2580ED-81F0-44D1-B803-82B127BB2633}.Release|Any CPU.Build.0 = Release|Any CPU
+ {695DC4E6-1831-4826-9A64-B85107AB555A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {695DC4E6-1831-4826-9A64-B85107AB555A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {695DC4E6-1831-4826-9A64-B85107AB555A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {695DC4E6-1831-4826-9A64-B85107AB555A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBB60595-8F29-49D5-A9A1-0CEA1B9CFF91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBB60595-8F29-49D5-A9A1-0CEA1B9CFF91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBB60595-8F29-49D5-A9A1-0CEA1B9CFF91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBB60595-8F29-49D5-A9A1-0CEA1B9CFF91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {81DE42A0-8493-49B4-9F0E-353198C1E2AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {81DE42A0-8493-49B4-9F0E-353198C1E2AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {81DE42A0-8493-49B4-9F0E-353198C1E2AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {81DE42A0-8493-49B4-9F0E-353198C1E2AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8D3A6ED6-46CD-41CD-BE61-5587F05EAB4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8D3A6ED6-46CD-41CD-BE61-5587F05EAB4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8D3A6ED6-46CD-41CD-BE61-5587F05EAB4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8D3A6ED6-46CD-41CD-BE61-5587F05EAB4E}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {43965DDD-4EA7-4404-8CEA-1B424850710A}
+ EndGlobalSection
+EndGlobal