Initial Cult of the Shock plugin

This commit is contained in:
Oshgnacknak 2025-07-23 00:33:05 +02:00
commit 8aa772cc8e
Signed by: Oshgnacknak
GPG key ID: 8CB7375654585956
7 changed files with 320 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
bin
obj

29
Cult_of_the_Shock.csproj Normal file
View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>Cult_of_the_Shock</AssemblyName>
<Product>Cult of the Shock</Product>
<Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<RestoreAdditionalProjectSources>
https://api.nuget.org/v3/index.json;
https://nuget.bepinex.dev/v3/index.json;
https://nuget.samboy.dev/v3/index.json
</RestoreAdditionalProjectSources>
<RootNamespace>Cult_of_the_Shock</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" />
<PackageReference Include="BepInEx.Core" Version="5.*" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" />
<PackageReference Include="CultOfTheLamb.GameLibs" Version="1.4.6.596-r.0" />
<PackageReference Include="UnityEngine.Modules" Version="2021.3.16" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
</ItemGroup>
</Project>

24
Cult_of_the_Shock.sln Normal file
View file

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cult of the Shock", "Cult_of_the_Shock.csproj", "{CEAACF8E-CA24-1B25-FC4E-1D718312D8A1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CEAACF8E-CA24-1B25-FC4E-1D718312D8A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CEAACF8E-CA24-1B25-FC4E-1D718312D8A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEAACF8E-CA24-1B25-FC4E-1D718312D8A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEAACF8E-CA24-1B25-FC4E-1D718312D8A1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2B7C2765-F52A-4A8D-BA5E-B4CBF9C9C57B}
EndGlobalSection
EndGlobal

38
Hooks.cs Normal file
View file

@ -0,0 +1,38 @@
using HarmonyLib;
using UnityEngine;
using static Health;
namespace Cult_of_the_Shock
{
[HarmonyPatch]
public class Hooks
{
private static Shocker Shocker;
public static void OnDamage(GameObject attacker, Vector3 attackLocation, float damage, AttackTypes attackType, AttackFlags attackFlag)
{
Plugin.Logger.LogInfo($"PlayerController.Health.OnDamage: " + damage);
Shocker.Shock(Plugin.Config.GetDamageJson());
}
public static void OnDie(GameObject Attacker, Vector3 AttackLocation, Health Victim, AttackTypes AttackType, AttackFlags AttackFlags)
{
Plugin.Logger.LogInfo($"PlayerController.Health.OnDie");
Shocker.Shock(Plugin.Config.GetDeathJson());
}
public static void OnHit(GameObject Attacker, Vector3 AttackLocation, AttackTypes AttackType, bool FromBehind)
{
Plugin.Logger.LogInfo($"OnHit: " + Attacker);
}
public static void OnEnable(PlayerController __instance)
{
__instance.health.OnDamaged += OnDamage;
__instance.health.OnDie += OnDie;
Shocker = new Shocker();
Plugin.Config.Reload();
}
}
}

53
Plugin.cs Normal file
View file

@ -0,0 +1,53 @@
using UnityEngine;
using System.Reflection;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Unity.VideoHelper;
namespace Cult_of_the_Shock
{
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
internal static new ManualLogSource Logger;
internal static new ShockConfig Config;
internal readonly static Harmony Harmony = new Harmony(MyPluginInfo.PLUGIN_GUID);
public void Awake()
{
Logger = base.Logger;
Config = new ShockConfig(base.Config);
Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded! Prepare for a shocking experiance...");
PatchMethods();
}
private void PatchMethods()
{
MethodInfo method = null;
foreach (var m in typeof(PlayerController).GetRuntimeMethods())
{
if (m.Name == "OnEnable")
{
method = m;
break;
}
}
Logger.LogDebug("A possible method: " + method);
Harmony.Patch(
method,
postfix: new HarmonyMethod(typeof(Hooks).GetMethod("OnEnable"))
);
}
}
}
// TODO: https://github.com/InfernoDragon0/CotLMods/blob/4a157380362dccb1d8564368367ac3588eb5fa06/Patches/KnucklebonesPatch.cs#L54

137
ShockConfig.cs Normal file
View file

@ -0,0 +1,137 @@
using BepInEx.Configuration;
using System.Collections.Generic;
using System.Linq;
namespace Cult_of_the_Shock
{
public class ShockConfig
{
private static HashSet<string> ACTIONS = new HashSet<string> { "Shock", "Vibrate", "Sound" };
private readonly ConfigFile config;
private readonly ConfigEntry<string> url;
private readonly ConfigEntry<string> token;
private readonly ConfigEntry<string> shockerIds;
private readonly ConfigEntry<string> deathAction;
private readonly ConfigEntry<string> deathIntensity;
private readonly ConfigEntry<string> deathDuration;
private readonly ConfigEntry<string> damageAction;
private readonly ConfigEntry<string> damageIntensity;
private readonly ConfigEntry<string> damageDuration;
public ShockConfig(ConfigFile config)
{
this.config = config;
url = config.Bind("Shocker",
"url",
"https://api.openshock.app",
"The base url of your api (without trailing slash)");
token = config.Bind("Shocker",
"token",
"secrettoken",
"Your API token");
shockerIds = config.Bind("Shocker",
"shockerIds",
"uuid1,uuid2",
"Ids of your shockers (separated by commas, to spaces)");
deathAction = config.Bind("Death",
"action",
"Nothing",
"What to do on death (Nothing, Shock, Vibrate, Sound)");
deathIntensity = config.Bind("Death",
"intensity",
"1",
"Intensity on death (1 - 100)");
deathDuration = config.Bind("Death",
"duration",
"300",
"Duration on death in milliseconds (min 300)");
damageAction = config.Bind("Damage",
"action",
"Nothing",
"What to do on damage (Nothing, Shock, Vibrate, Sound)");
damageIntensity = config.Bind("Damage",
"intensity",
"1",
"Intensity on death (1 - 100)");
damageDuration = config.Bind("Damage",
"duration",
"300",
"Duration on damage in milliseconds (min 300)");
config.ConfigReloaded += (_, _) => OnReload();
}
private void OnReload()
{
Plugin.Logger.LogInfo($"Config reloaded");
}
public string GetDeathJson()
{
if (!ACTIONS.Contains(deathAction.Value))
{
return null;
}
var controls = GetIds().Select(id =>
$@"{{
""id"": ""{id}"",
""type"": ""{deathAction.Value}"",
""intensity"": {deathIntensity.Value},
""duration"": {deathDuration.Value},
""exclusive"": false
}}"
);
return "[" + string.Join(",\n", controls) + "]";
}
public string GetDamageJson()
{
if (!ACTIONS.Contains(damageAction.Value))
{
return null;
}
var controls = GetIds().Select(id =>
$@"{{
""id"": ""{id}"",
""type"": ""{damageAction.Value}"",
""intensity"": {damageIntensity.Value},
""duration"": {damageDuration.Value},
""exclusive"": false
}}"
);
return "[" + string.Join(",\n", controls) + "]";
}
public void Reload()
{
config.Reload();
}
private string[] GetIds()
{
return shockerIds.Value.Split(',');
}
public string GetToken()
{
return token.Value;
}
public string GetURL()
{
return url.Value;
}
}
}

37
Shocker.cs Normal file
View file

@ -0,0 +1,37 @@
using System;
using System.Text;
using System.Threading;
using BepInEx.Configuration;
using UnityEngine.Networking;
namespace Cult_of_the_Shock
{
public class Shocker
{
public void Shock(string json)
{
var uwr = new UnityWebRequest(Plugin.Config.GetURL() + "/1/shockers/control", "POST");
uwr.SetTimeoutMsec(1000);
uwr.SetRequestHeader("OpenShockToken", Plugin.Config.GetToken());
uwr.SetRequestHeader("Content-Type", "application/json");
byte[] requestData = new UTF8Encoding().GetBytes(json);
uwr.uploadHandler = new UploadHandlerRaw(requestData);
uwr.downloadHandler = new DownloadHandlerBuffer();
uwr.SendWebRequest();
while (!uwr.isDone)
{
Thread.Yield();
}
if (uwr.result != UnityWebRequest.Result.Success)
{
Plugin.Logger.LogError("Error while Sending Request: " + uwr.error);
Plugin.Logger.LogError(uwr.downloadHandler.text);
Plugin.Logger.LogError("Sent JSON: " + json);
}
}
}
}