diff --git a/ChatBoxExtensions/ChatBoxExtensions.csproj b/ChatBoxExtensions/ChatBoxExtensions.csproj
new file mode 100644
index 0000000..3260322
--- /dev/null
+++ b/ChatBoxExtensions/ChatBoxExtensions.csproj
@@ -0,0 +1,43 @@
+
+
+
+
+ net472
+ enable
+ latest
+ false
+
+
+
+
+ ..\_ManagedLibs\0Harmony.dll
+
+
+ ..\_ManagedLibs\Assembly-CSharp.dll
+
+
+ ..\_ManagedLibs\Assembly-CSharp-firstpass.dll
+
+
+ ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\ChatBox.dll
+
+
+ ..\_ManagedLibs\MelonLoader.dll
+
+
+ ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\ml_prm.dll
+
+
+ ..\_ManagedLibs\UnityEngine.CoreModule.dll
+
+
+ ..\_ManagedLibs\UnityEngine.InputLegacyModule.dll
+
+
+
+
+
+
+
+
+
diff --git a/ChatBoxExtensions/ChatBoxExtensions.sln b/ChatBoxExtensions/ChatBoxExtensions.sln
new file mode 100644
index 0000000..d9d0892
--- /dev/null
+++ b/ChatBoxExtensions/ChatBoxExtensions.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32630.192
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatBoxExtensions", "ChatBoxExtensions.csproj", "{319281C7-C2CC-4350-ACE9-A52467C3E1EF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {319281C7-C2CC-4350-ACE9-A52467C3E1EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {319281C7-C2CC-4350-ACE9-A52467C3E1EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {319281C7-C2CC-4350-ACE9-A52467C3E1EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {319281C7-C2CC-4350-ACE9-A52467C3E1EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {67285FB2-B0B0-44AF-AC34-C47B3BF054ED}
+ EndGlobalSection
+EndGlobal
diff --git a/ChatBoxExtensions/HarmonyPatches.cs b/ChatBoxExtensions/HarmonyPatches.cs
new file mode 100644
index 0000000..3ab129d
--- /dev/null
+++ b/ChatBoxExtensions/HarmonyPatches.cs
@@ -0,0 +1,14 @@
+using ABI_RC.Core.Savior;
+using HarmonyLib;
+
+namespace NAK.Melons.ChatBoxExtensions.HarmonyPatches;
+
+public class CVRInputManagerPatches
+{
+ [HarmonyPostfix]
+ [HarmonyPatch(typeof(CVRInputManager), "Start")]
+ static void Postfix_CVRInputManager_Start(ref CVRInputManager __instance)
+ {
+ ChatBoxExtensions.InputModule = __instance.gameObject.AddComponent();
+ }
+}
diff --git a/ChatBoxExtensions/InputModules/InputModuleChatBoxExtensions.cs b/ChatBoxExtensions/InputModules/InputModuleChatBoxExtensions.cs
new file mode 100644
index 0000000..ff94027
--- /dev/null
+++ b/ChatBoxExtensions/InputModules/InputModuleChatBoxExtensions.cs
@@ -0,0 +1,25 @@
+using ABI_RC.Core.Savior;
+using UnityEngine;
+
+namespace NAK.Melons.ChatBoxExtensions.InputModules;
+
+internal class InputModuleChatBoxExtensions : CVRInputModule
+{
+ public bool jump = false;
+ public float emote = -1;
+ public Vector2 lookVector = Vector2.zero;
+ public Vector3 movementVector = Vector3.zero;
+
+ public override void UpdateInput()
+ {
+ CVRInputManager.Instance.jump |= jump;
+ CVRInputManager.Instance.movementVector += movementVector;
+ CVRInputManager.Instance.lookVector += lookVector;
+ if (emote > 0) CVRInputManager.Instance.emote = emote;
+
+ jump = false;
+ emote = -1;
+ lookVector = Vector3.zero;
+ movementVector = Vector3.zero;
+ }
+}
diff --git a/ChatBoxExtensions/Integrations/Base.cs b/ChatBoxExtensions/Integrations/Base.cs
new file mode 100644
index 0000000..96208ce
--- /dev/null
+++ b/ChatBoxExtensions/Integrations/Base.cs
@@ -0,0 +1,56 @@
+using ABI_RC.Core.Player;
+using ABI_RC.Core.Savior;
+
+namespace NAK.Melons.ChatBoxExtensions.Integrations;
+
+public class CommandBase
+{
+ internal static bool IsCommandForAll(string argument)
+ {
+ if (String.IsNullOrWhiteSpace(argument)) return false;
+ return argument == "*" || argument.StartsWith("@a") || argument.StartsWith("@e");
+ }
+
+ internal static bool IsCommandForLocalPlayer(string argument)
+ {
+ if (String.IsNullOrWhiteSpace(argument)) return false;
+ if (argument.Contains("*"))
+ {
+ string partialName = argument.Replace("*", "").Trim();
+ if (String.IsNullOrWhiteSpace(partialName)) return false;
+ return MetaPort.Instance.username.Contains(partialName);
+ }
+ return MetaPort.Instance.username == argument;
+ }
+
+ internal static void LocalCommandIgnoreOthers(string argument, Action callback)
+ {
+ string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
+
+ // will fail if arguments are specified which arent local player
+ if (args.Length == 0 || IsCommandForAll(args[0]) || IsCommandForLocalPlayer(args[0])) callback(args);
+ }
+
+ //remote must specify exact player, wont respawn to all
+ internal static void RemoteCommandListenForSelf(string argument, Action callback)
+ {
+ string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
+
+ if (args.Length == 0) return;
+ if (IsCommandForLocalPlayer(args[0])) callback(args);
+ }
+
+ // remote must specify player or all, ignore commands without arguments
+ internal static void RemoteCommandListenForAll(string argument, Action callback)
+ {
+ string[] args = argument.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).ToArray();
+
+ if (args.Length == 0) return;
+ if (IsCommandForAll(args[0]) || IsCommandForLocalPlayer(args[0])) callback(args);
+ }
+
+ internal static string GetPlayerUsername(string guid)
+ {
+ return CVRPlayerManager.Instance.TryGetPlayerName(guid);
+ }
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/Integrations/ChatBox.cs b/ChatBoxExtensions/Integrations/ChatBox.cs
new file mode 100644
index 0000000..1f1824b
--- /dev/null
+++ b/ChatBoxExtensions/Integrations/ChatBox.cs
@@ -0,0 +1,43 @@
+using Kafe.ChatBox;
+
+namespace NAK.Melons.ChatBoxExtensions.Integrations;
+
+internal class ChatBoxCommands : CommandBase
+{
+ public static void RegisterCommands()
+ {
+ bool awaitingPing = false;
+ DateTime pingTime = DateTime.MinValue; // store the time when "ping" command was sent
+
+ Commands.RegisterCommand("ping",
+ onCommandSent: (message, sound) =>
+ {
+ pingTime = DateTime.Now;
+ awaitingPing = true;
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForSelf(message, args =>
+ {
+ ChatBox.SendMessage("/pong " + GetPlayerUsername(sender), false);
+ });
+ });
+
+ Commands.RegisterCommand("pong",
+ onCommandSent: null,
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForSelf(message, args =>
+ {
+ if (awaitingPing)
+ {
+ awaitingPing = false;
+ TimeSpan timeSincePing = DateTime.Now - pingTime; // calculate the time difference
+ ChatBox.SendMessage($"Time since ping: {timeSincePing.TotalMilliseconds}ms", false);
+ return;
+ }
+ ChatBox.SendMessage($"You have to ping first, {GetPlayerUsername(sender)}!", false);
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/Integrations/ChilloutVRBase.cs b/ChatBoxExtensions/Integrations/ChilloutVRBase.cs
new file mode 100644
index 0000000..ae05883
--- /dev/null
+++ b/ChatBoxExtensions/Integrations/ChilloutVRBase.cs
@@ -0,0 +1,43 @@
+using ABI_RC.Core;
+using ABI_RC.Core.Base;
+using Kafe.ChatBox;
+
+namespace NAK.Melons.ChatBoxExtensions.Integrations;
+
+internal class ChilloutVRBaseCommands : CommandBase
+{
+ public static void RegisterCommands()
+ {
+ Commands.RegisterCommand("respawn",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, args =>
+ {
+ RootLogic.Instance.Respawn();
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, (args) =>
+ {
+ RootLogic.Instance.Respawn();
+ });
+ });
+
+ Commands.RegisterCommand("mute",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, args =>
+ {
+ Audio.SetMicrophoneActive(true);
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, args =>
+ {
+ Audio.SetMicrophoneActive(true);
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/Integrations/ChilloutVRInput.cs b/ChatBoxExtensions/Integrations/ChilloutVRInput.cs
new file mode 100644
index 0000000..8e8a1b4
--- /dev/null
+++ b/ChatBoxExtensions/Integrations/ChilloutVRInput.cs
@@ -0,0 +1,58 @@
+using ABI_RC.Core;
+using ABI_RC.Core.Base;
+using ABI_RC.Core.Savior;
+using Kafe.ChatBox;
+
+namespace NAK.Melons.ChatBoxExtensions.Integrations;
+
+internal class ChilloutVRInputCommands : CommandBase
+{
+ public static void RegisterCommands()
+ {
+ Commands.RegisterCommand("emote",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, args =>
+ {
+ if (args.Length > 0 && int.TryParse(args[0], out int emote))
+ {
+ ChatBoxExtensions.InputModule.emote = (float)emote;
+ }
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, args =>
+ {
+ if (args.Length > 1 && int.TryParse(args[1], out int emote))
+ {
+ ChatBoxExtensions.InputModule.emote = (float)emote;
+ }
+ });
+ });
+
+ Commands.RegisterCommand("jump",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, args =>
+ {
+ if (args.Length > 0 && bool.TryParse(args[0], out bool jump))
+ {
+ ChatBoxExtensions.InputModule.jump = jump;
+ return;
+ }
+ ChatBoxExtensions.InputModule.jump = true;
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, args =>
+ {
+ if (bool.TryParse(args[0], out bool jump))
+ {
+ ChatBoxExtensions.InputModule.jump = jump;
+ }
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/Integrations/PlayerRagdollMod.cs b/ChatBoxExtensions/Integrations/PlayerRagdollMod.cs
new file mode 100644
index 0000000..775d39d
--- /dev/null
+++ b/ChatBoxExtensions/Integrations/PlayerRagdollMod.cs
@@ -0,0 +1,90 @@
+using Kafe.ChatBox;
+using ml_prm;
+
+namespace NAK.Melons.ChatBoxExtensions.Integrations;
+
+internal class PlayerRagdollModCommands : CommandBase
+{
+ public static void RegisterCommands()
+ {
+ Commands.RegisterCommand("unragdoll",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, (args) =>
+ {
+ if (RagdollController.Instance.IsRagdolled())
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, (args) =>
+ {
+ if (RagdollController.Instance.IsRagdolled())
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ });
+
+ Commands.RegisterCommand("ragdoll",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, (args) =>
+ {
+ bool switchRagdoll = true;
+
+ if (args.Length > 0 && bool.TryParse(args[0], out bool state))
+ {
+ switchRagdoll = state != RagdollController.Instance.IsRagdolled();
+ }
+
+ if (switchRagdoll)
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, (args) =>
+ {
+ bool switchRagdoll = true;
+
+ if (args.Length > 1 && bool.TryParse(args[1], out bool state))
+ {
+ switchRagdoll = state != RagdollController.Instance.IsRagdolled();
+ }
+
+ if (switchRagdoll)
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ });
+
+ Commands.RegisterCommand("kill",
+ onCommandSent: (message, sound) =>
+ {
+ LocalCommandIgnoreOthers(message, (args) =>
+ {
+ if (!RagdollController.Instance.IsRagdolled())
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ },
+ onCommandReceived: (sender, message, sound) =>
+ {
+ RemoteCommandListenForAll(message, (args) =>
+ {
+ if (!RagdollController.Instance.IsRagdolled())
+ {
+ RagdollController.Instance.SwitchRagdoll();
+ }
+ });
+ });
+ }
+}
diff --git a/ChatBoxExtensions/Main.cs b/ChatBoxExtensions/Main.cs
new file mode 100644
index 0000000..86e2064
--- /dev/null
+++ b/ChatBoxExtensions/Main.cs
@@ -0,0 +1,48 @@
+using MelonLoader;
+using NAK.Melons.ChatBoxExtensions.InputModules;
+
+namespace NAK.Melons.ChatBoxExtensions;
+
+public class ChatBoxExtensions : MelonMod
+{
+ internal static MelonLogger.Instance Logger;
+ internal static InputModuleChatBoxExtensions InputModule;
+
+ public override void OnInitializeMelon()
+ {
+ Logger = LoggerInstance;
+
+ if (!MelonMod.RegisteredMelons.Any(it => it.Info.Name == "ChatBox"))
+ {
+ Logger.Error("ChatBox was not found!");
+ return;
+ }
+
+ ApplyIntegrations();
+ }
+
+ void ApplyIntegrations()
+ {
+ Integrations.ChatBoxCommands.RegisterCommands();
+ Integrations.ChilloutVRBaseCommands.RegisterCommands();
+ ApplyPatches(typeof(HarmonyPatches.CVRInputManagerPatches));
+
+ if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "PlayerRagdollMod"))
+ {
+ Integrations.PlayerRagdollModCommands.RegisterCommands();
+ }
+ }
+
+ void ApplyPatches(Type type)
+ {
+ try
+ {
+ HarmonyInstance.PatchAll(type);
+ }
+ catch (Exception e)
+ {
+ Logger.Msg($"Failed while patching {type.Name}!");
+ Logger.Error(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/Properties/AssemblyInfo.cs b/ChatBoxExtensions/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..1b5d805
--- /dev/null
+++ b/ChatBoxExtensions/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+using ChatBoxExtensions.Properties;
+using MelonLoader;
+using System.Reflection;
+
+
+[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
+[assembly: AssemblyTitle(nameof(NAK.Melons.ChatBoxExtensions))]
+[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
+[assembly: AssemblyProduct(nameof(NAK.Melons.ChatBoxExtensions))]
+
+[assembly: MelonInfo(
+ typeof(NAK.Melons.ChatBoxExtensions.ChatBoxExtensions),
+ nameof(NAK.Melons.ChatBoxExtensions),
+ AssemblyInfoParams.Version,
+ AssemblyInfoParams.Author,
+ downloadLink: "https://github.com/NotAKidOnSteam/ChatBoxExtensions"
+)]
+
+[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
+[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
+[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
+[assembly: MelonOptionalDependencies("ChatBox", "PlayerRagdollMod")]
+[assembly: HarmonyDontPatchAll]
+
+namespace ChatBoxExtensions.Properties;
+internal static class AssemblyInfoParams
+{
+ public const string Version = "1.0.0";
+ public const string Author = "NotAKidoS";
+}
\ No newline at end of file
diff --git a/ChatBoxExtensions/format.json b/ChatBoxExtensions/format.json
new file mode 100644
index 0000000..93fc8ff
--- /dev/null
+++ b/ChatBoxExtensions/format.json
@@ -0,0 +1,23 @@
+{
+ "_id": 129,
+ "name": "ChatBoxExtensions",
+ "modversion": "1.0.1",
+ "gameversion": "2022r170",
+ "loaderversion": "0.5.7",
+ "modtype": "Mod",
+ "author": "NotAKidoS",
+ "description": "Prevents VRIK from using toe bones in VR & optionaly FBT.\n\nVRIK calculates weird center of mass when toes are mapped, so it is sometimes desired to unmap toes to prevent an avatars feet from resting far back.\n\nPlease see the README for relevant imagery detailing the problem.",
+ "searchtags": [
+ "toes",
+ "vrik",
+ "ik",
+ "feet"
+ ],
+ "requirements": [
+ "None"
+ ],
+ "downloadlink": "https://github.com/NotAKidOnSteam/ChatBoxExtensions/releases/download/v1.0.1/ChatBoxExtensions.dll",
+ "sourcelink": "https://github.com/NotAKidOnSteam/ChatBoxExtensions/",
+ "changelog": "- Initial Release\n- No double patching. Bad. Stinky. Dont do it.",
+ "embedcolor": "#ffc700"
+}
\ No newline at end of file