├── .github └── workflows │ └── build.yml ├── .gitignore ├── BepInEx.cfg ├── KeyboardOSC ├── ChatMode.cs ├── KeyboardOSC.csproj ├── Patches.cs ├── Plugin.cs ├── PluginSettings.cs ├── ToggleChatButton.cs ├── Tools.cs ├── Twitch │ ├── Core.cs │ ├── Helix.cs │ └── Models.cs ├── XScripts │ ├── ReparentBar.cs │ └── RescaleCamToCanvas.cs └── chat.png ├── LICENSE ├── PluginAppTest ├── Patches.cs ├── Plugin.cs ├── PluginAppTest.csproj └── README.md ├── README.md ├── SettingsKO.html ├── XSOMod.sln ├── install.ps1 ├── settingsKO.js ├── twitchAuth.html ├── twitchAuth.js └── update-xslibs.ps1 /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | concurrency: 4 | group: "build" 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | paths-ignore: 12 | - '**.md' 13 | - '**.js' 14 | - '**.ps1' 15 | workflow_dispatch: 16 | jobs: 17 | build: 18 | runs-on: windows-latest 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Checkout XSO assemblies 25 | uses: actions/checkout@v4 26 | with: 27 | repository: ${{ secrets.xsoRepo }} 28 | token: ${{ secrets.repoToken }} 29 | path: refs 30 | 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v3.2.0 33 | with: 34 | dotnet-version: '7.0.x' 35 | 36 | - name: Add Bepis NuGet & Restore dependencies 37 | run: | 38 | dotnet nuget add source https://nuget.bepinex.dev/v3/index.json 39 | dotnet restore 40 | 41 | - name: Build 42 | run: dotnet build -c Release --no-restore 43 | 44 | - name: Tag version 45 | id: tag_version 46 | uses: mathieudutour/github-tag-action@v6.1 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Create Release 51 | run: | 52 | $short_sha="${{ github.sha }}".Substring(0,7) 53 | gh release create ${{ steps.tag_version.outputs.new_tag }}-$short_sha ./builds/Release/net472/KeyboardOSC.dll ./BepInEx.cfg --title "KeyboardOSC Build $short_sha" --prerelease --notes "${{ steps.tag_version.outputs.changelog }}" 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /modules.xml 6 | /.idea.XSOMod.iml 7 | /contentModel.xml 8 | /projectSettingsUpdater.xml 9 | 10 | # Ignore Visual Studio temporary files, build results, and files generated by popular Visual Studio add-ons. 11 | *.exe 12 | *.dll 13 | *.pdb 14 | *.user 15 | *.aps 16 | *.pch 17 | *.vspscc 18 | *_i.c 19 | *_p.c 20 | *.ncb 21 | *.suo 22 | *.tlb 23 | *.tlh 24 | *.bak 25 | *.cache 26 | *.ilk 27 | *.log 28 | *.lib 29 | *.sbr 30 | *.sdf 31 | *.opensdf 32 | *.unsuccessfulbuild 33 | ipch/ 34 | obj/ 35 | [Bb]in 36 | [Dd]ebug*/ 37 | [Rr]elease*/ 38 | 39 | # Ignore Visual Studio Code temporary files 40 | .vscode/ 41 | 42 | # Ignore JetBrains Rider temporary files 43 | .idea/ 44 | 45 | 46 | [Tt]emp/ 47 | [Oo]bj/ 48 | [Bb]uild/ 49 | [Bb]uilds/ 50 | [Ll]ibrary/ 51 | 52 | *.nupkg 53 | packages/ 54 | !**/packages/build/ 55 | !**/packages/tools/ 56 | *.nuspec 57 | 58 | 59 | NuGet.Config 60 | nuget.config 61 | nuget.targets 62 | 63 | refs/ 64 | -------------------------------------------------------------------------------- /BepInEx.cfg: -------------------------------------------------------------------------------- 1 | [Caching] 2 | 3 | ## Enable/disable assembly metadata cache 4 | ## Enabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers. 5 | # Setting type: Boolean 6 | # Default value: true 7 | EnableAssemblyCache = true 8 | 9 | [Chainloader] 10 | 11 | ## If enabled, hides BepInEx Manager GameObject from Unity. 12 | ## This can fix loading issues in some games that attempt to prevent BepInEx from being loaded. 13 | ## Use this only if you know what this option means, as it can affect functionality of some older plugins. 14 | ## 15 | # Setting type: Boolean 16 | # Default value: false 17 | HideManagerGameObject = true 18 | 19 | [Harmony.Logger] 20 | 21 | ## Specifies which Harmony log channels to listen to. 22 | ## NOTE: IL channel dumps the whole patch methods, use only when needed! 23 | # Setting type: LogChannel 24 | # Default value: Warn, Error 25 | # Acceptable values: None, Info, IL, Warn, Error, Debug, All 26 | # Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning) 27 | LogChannels = Warn, Error 28 | 29 | [Logging] 30 | 31 | ## Enables showing unity log messages in the BepInEx logging system. 32 | # Setting type: Boolean 33 | # Default value: true 34 | UnityLogListening = true 35 | 36 | ## If enabled, writes Standard Output messages to Unity log 37 | ## NOTE: By default, Unity does so automatically. Only use this option if no console messages are visible in Unity log 38 | ## 39 | # Setting type: Boolean 40 | # Default value: false 41 | LogConsoleToUnityLog = false 42 | 43 | [Logging.Console] 44 | 45 | ## Enables showing a console for log output. 46 | # Setting type: Boolean 47 | # Default value: false 48 | Enabled = false 49 | 50 | ## If enabled, will prevent closing the console (either by deleting the close button or in other platform-specific way). 51 | # Setting type: Boolean 52 | # Default value: false 53 | PreventClose = false 54 | 55 | ## If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding. 56 | # Setting type: Boolean 57 | # Default value: false 58 | ShiftJisEncoding = false 59 | 60 | ## Hints console manager on what handle to assign as StandardOut. Possible values: 61 | ## Auto - lets BepInEx decide how to redirect console output 62 | ## ConsoleOut - prefer redirecting to console output; if possible, closes original standard output 63 | ## StandardOut - prefer redirecting to standard output; if possible, closes console out 64 | ## 65 | # Setting type: ConsoleOutRedirectType 66 | # Default value: Auto 67 | # Acceptable values: Auto, ConsoleOut, StandardOut 68 | StandardOutType = Auto 69 | 70 | ## Which log levels to show in the console output. 71 | # Setting type: LogLevel 72 | # Default value: Fatal, Error, Warning, Message, Info 73 | # Acceptable values: None, Fatal, Error, Warning, Message, Info, Debug, All 74 | # Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning) 75 | LogLevels = Fatal, Error, Warning, Message, Info 76 | 77 | [Logging.Disk] 78 | 79 | ## Include unity log messages in log file output. 80 | # Setting type: Boolean 81 | # Default value: false 82 | WriteUnityLog = false 83 | 84 | ## Appends to the log file instead of overwriting, on game startup. 85 | # Setting type: Boolean 86 | # Default value: false 87 | AppendLog = false 88 | 89 | ## Enables writing log messages to disk. 90 | # Setting type: Boolean 91 | # Default value: true 92 | Enabled = true 93 | 94 | ## Which log leves are saved to the disk log output. 95 | # Setting type: LogLevel 96 | # Default value: Fatal, Error, Warning, Message, Info 97 | # Acceptable values: None, Fatal, Error, Warning, Message, Info, Debug, All 98 | # Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning) 99 | LogLevels = Fatal, Error, Warning, Message, Info 100 | 101 | [Preloader] 102 | 103 | ## Enables or disables runtime patches. 104 | ## This should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing. 105 | # Setting type: Boolean 106 | # Default value: true 107 | ApplyRuntimePatches = true 108 | 109 | ## Specifies which MonoMod backend to use for Harmony patches. Auto uses the best available backend. 110 | ## This setting should only be used for development purposes (e.g. debugging in dnSpy). Other code might override this setting. 111 | # Setting type: MonoModBackend 112 | # Default value: auto 113 | # Acceptable values: auto, dynamicmethod, methodbuilder, cecil 114 | HarmonyBackend = auto 115 | 116 | ## If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies. 117 | ## This can be used by developers to inspect and debug preloader patchers. 118 | # Setting type: Boolean 119 | # Default value: false 120 | DumpAssemblies = false 121 | 122 | ## If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory. 123 | ## This can be used to be able to load patched assemblies into debuggers like dnSpy. 124 | ## If set to true, will override DumpAssemblies. 125 | # Setting type: Boolean 126 | # Default value: false 127 | LoadDumpedAssemblies = false 128 | 129 | ## If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies. 130 | ## This can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded. 131 | # Setting type: Boolean 132 | # Default value: false 133 | BreakBeforeLoadAssemblies = false 134 | 135 | [Preloader.Entrypoint] 136 | 137 | ## The local filename of the assembly to target. 138 | # Setting type: String 139 | # Default value: UnityEngine.CoreModule.dll 140 | Assembly = UnityEngine.CoreModule.dll 141 | 142 | ## The name of the type in the entrypoint assembly to search for the entrypoint method. 143 | # Setting type: String 144 | # Default value: Application 145 | Type = Application 146 | 147 | ## The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from. 148 | # Setting type: String 149 | # Default value: .cctor 150 | Method = .cctor 151 | 152 | -------------------------------------------------------------------------------- /KeyboardOSC/ChatMode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Timers; 5 | using BepInEx; 6 | using BepInEx.Logging; 7 | using HarmonyLib; 8 | using KeyboardOSC.Twitch; 9 | using TMPro; 10 | using UnityEngine; 11 | using WindowsInput.Native; 12 | using XSOverlay; 13 | 14 | // ReSharper disable SwitchStatementMissingSomeEnumCasesNoDefault 15 | 16 | namespace KeyboardOSC; 17 | 18 | public static class ChatMode 19 | { 20 | private static bool _isSilentMsg; 21 | private static bool _isFirstMsg; 22 | private static string _currentText = ""; 23 | private static string _lastMsg = ""; 24 | private static readonly ManualLogSource Logger = Plugin.PluginLogger; 25 | private static TextMeshProUGUI _oscBarText; 26 | private static TextMeshProUGUI _charCounter; 27 | private static List _currentlyDownStickyKeys = []; 28 | private static Timer _eventsTimer = new(1300); 29 | 30 | public static void HandleKey(KeyboardKey.VirtualKeyEventData eventData) 31 | { 32 | var scanCode = eventData.Sender.UsingRawVirtualKeyCode 33 | ? (uint)eventData.KeyCode[0] 34 | : eventData.Sender.ScanCode[0]; 35 | var shiftedField = AccessTools.Field(typeof(KeyboardKey), "IsShifted"); 36 | var altedField = AccessTools.Field(typeof(KeyboardKey), "IsAlted"); 37 | var isShifted = (bool)shiftedField.GetValue(eventData.Sender); 38 | var isAlted = (bool)altedField.GetValue(eventData.Sender); // altGr 39 | 40 | foreach (var key in eventData.KeyCode) 41 | { 42 | var character = Tools.ConvertVirtualKeyToUnicode(key, scanCode, isShifted, isAlted); 43 | ProcessKey(key, eventData, character); 44 | } 45 | } 46 | 47 | private static void ProcessKey(VirtualKeyCode key, KeyboardKey.VirtualKeyEventData data, string character) 48 | { 49 | var isCtrlHeld = _currentlyDownStickyKeys 50 | .Any(k => k.Key[0] is VirtualKeyCode.LCONTROL or VirtualKeyCode.RCONTROL); 51 | var sendTyping = PluginSettings.GetSetting("TypingIndicator").Value; 52 | var liveSendMode = PluginSettings.GetSetting("LiveSend").Value; 53 | switch (key) 54 | { 55 | // backspace/delete keys 56 | case VirtualKeyCode.BACK or VirtualKeyCode.DELETE: 57 | { 58 | if (_currentText.Length <= 0) return; 59 | if (isCtrlHeld) 60 | { 61 | var lastSpaceIndex = _currentText.LastIndexOf(' '); 62 | _currentText = lastSpaceIndex >= 0 ? _currentText.Substring(0, lastSpaceIndex) : ""; 63 | UpdateChatText(_currentText); 64 | #if DEBUG 65 | Logger.LogInfo("bulk deleting chat text: " + _currentText); 66 | #endif 67 | if (sendTyping) SendTyping(_currentText.Length != 0); 68 | if (liveSendMode) _eventsTimer.Start(); 69 | return; 70 | } 71 | 72 | _currentText = _currentText.Remove(key is VirtualKeyCode.DELETE ? 0 : _currentText.Length - 1, 1); 73 | if (sendTyping) SendTyping(_currentText.Length != 0); 74 | if (liveSendMode) _eventsTimer.Start(); 75 | UpdateChatText(_currentText); 76 | return; 77 | } 78 | // silent switch (no sound on send, no typing indicator) 79 | case VirtualKeyCode.TAB: 80 | _isSilentMsg = !_isSilentMsg; 81 | UpdateChatColor(); 82 | Logger.LogInfo($"Silent mode: {_isSilentMsg}"); 83 | return; 84 | // clear shortcut 85 | case VirtualKeyCode.ESCAPE: 86 | ClearInput(); 87 | Logger.LogInfo("Input cleared"); 88 | return; 89 | case VirtualKeyCode.END: 90 | Tools.SendOsc("/chatbox/input", string.Empty, true, false); 91 | Logger.LogInfo("Chatbox cleared"); 92 | return; 93 | case VirtualKeyCode.INSERT: 94 | _currentText = _lastMsg; 95 | UpdateChatText(_currentText); 96 | _isFirstMsg = true; 97 | Logger.LogInfo("Inserted last input"); 98 | return; 99 | // copy + paste 100 | case VirtualKeyCode.VK_C: 101 | if (!isCtrlHeld) break; 102 | GUIUtility.systemCopyBuffer = _currentText; 103 | return; 104 | case VirtualKeyCode.VK_V: 105 | if (!isCtrlHeld) break; 106 | _currentText += GUIUtility.systemCopyBuffer; 107 | UpdateChatText(_currentText); 108 | return; 109 | case VirtualKeyCode.RETURN: 110 | if (liveSendMode) 111 | { 112 | Logger.LogInfo($"Sending message (live send enabled): {_currentText.ReplaceShortcodes()}"); 113 | _lastMsg = _currentText; 114 | if (Core.IsTwitchSendingEnabled && !_isSilentMsg) 115 | { 116 | var affixes = Core.GetAffixes(); 117 | var currentText = _currentText.ReplaceShortcodes(); 118 | SendMessage(true); 119 | Task.Run(() => 120 | { 121 | Helix.SendTwitchMessage($"{affixes.Item1} {currentText} {affixes.Item2}"); 122 | ThreadingHelper.Instance.StartSyncInvoke(ClearInput); 123 | }); 124 | } 125 | else 126 | { 127 | SendMessage(true); 128 | ClearInput(); 129 | } 130 | } 131 | else 132 | { 133 | Logger.LogInfo($"Sending message: {_currentText.ReplaceShortcodes()}"); 134 | SendMessage(); 135 | } 136 | 137 | return; 138 | } 139 | 140 | // Normal character inputs 141 | if (sendTyping) SendTyping(_currentText.Length != 0); 142 | if (liveSendMode) _eventsTimer.Start(); 143 | 144 | _currentText += character; 145 | UpdateChatText(_currentText); 146 | } 147 | 148 | private static void TimerElapsed(object sender, ElapsedEventArgs e) 149 | { 150 | if (_isSilentMsg) return; 151 | Logger.LogInfo("Timer elapsed, sending message"); 152 | if (_currentText.IsNullOrWhiteSpace()) 153 | { 154 | InputToChatbox(string.Empty, false); 155 | SendTyping(false); 156 | } 157 | 158 | var sendTyping = PluginSettings.GetSetting("TypingIndicator").Value; 159 | SendMessage(true); 160 | if (sendTyping) SendTyping(true); 161 | } 162 | 163 | private static void SendMessage(bool liveSend = false) 164 | { 165 | var triggerSfx = !_isSilentMsg && _isFirstMsg; 166 | 167 | if (liveSend) 168 | { 169 | _eventsTimer.Stop(); 170 | if (_isFirstMsg) _isFirstMsg = false; 171 | InputToChatbox(_currentText.ReplaceShortcodes(), triggerSfx); 172 | SendTyping(false); 173 | } 174 | else 175 | { 176 | InputToChatbox(_currentText.ReplaceShortcodes(), !_isSilentMsg); 177 | SendTyping(false); 178 | if (Core.IsTwitchSendingEnabled && !_isSilentMsg) 179 | { 180 | var affixes = Core.GetAffixes(); 181 | var currentText = _currentText.ReplaceShortcodes(); 182 | Task.Run(() => 183 | Helix.SendTwitchMessage($"{affixes.Item1} {currentText} {affixes.Item2}")); 184 | } 185 | 186 | _lastMsg = _currentText; 187 | ClearInput(); 188 | } 189 | } 190 | 191 | /// 192 | /// Since i keep forgetting: 193 | /// /chatbox/input s b n Input text into the chatbox. 194 | /// If B is True, send the text in S immediately, bypassing the keyboard. If b is False, open the keyboard and populate it with the provided text. 195 | /// N is an additional bool parameter that when set to False will not trigger the notification SFX (defaults to True if not specified). 196 | /// 197 | private static void InputToChatbox(string text, bool triggerSfx = true) 198 | { 199 | Tools.SendOsc("/chatbox/input", text, true, triggerSfx); 200 | } 201 | 202 | private static void ClearInput() 203 | { 204 | UpdateChatText(string.Empty); 205 | _currentText = string.Empty; 206 | _isSilentMsg = false; 207 | _isFirstMsg = true; 208 | UpdateChatColor(); 209 | Plugin.ReleaseStickyKeys.Invoke(Plugin.Instance.inputHandler, null); 210 | } 211 | 212 | private static void SendTyping(bool typing) 213 | { 214 | if (typing && _isSilentMsg) return; 215 | Tools.SendOsc("/chatbox/typing", typing); 216 | } 217 | 218 | public static void Setup(TextMeshProUGUI barText, TextMeshProUGUI charCounter) 219 | { 220 | _oscBarText = barText; 221 | _charCounter = charCounter; 222 | _eventsTimer.Elapsed += TimerElapsed; 223 | var stickyKeysField = AccessTools.Field(typeof(KeyboardInputHandler), "CurrentlyDownStickyKeys"); 224 | _currentlyDownStickyKeys = (List)stickyKeysField.GetValue(Plugin.Instance.inputHandler); 225 | } 226 | 227 | private static void UpdateChatColor() 228 | { 229 | _oscBarText.color = 230 | _isSilentMsg ? UIThemeHandler.Instance.T_WarningTone : UIThemeHandler.Instance.T_ConstrastingTone; 231 | _charCounter.color = _currentText.Length switch 232 | { 233 | >= 120 => UIThemeHandler.Instance.T_ErrTone, 234 | >= 85 => UIThemeHandler.Instance.T_WarningTone, 235 | _ => UIThemeHandler.Instance.T_ConstrastingTone 236 | }; 237 | } 238 | 239 | private static void UpdateChatText(string text) 240 | { 241 | var disableMaxLength = Core.IsTwitchSendingEnabled || PluginSettings.GetSetting("DisableMaxLength").Value; 242 | if (text.Length > 144 && !disableMaxLength) 243 | { 244 | text = text.Substring(0, 144); 245 | } 246 | 247 | XSTools.SetTMPUIText(_charCounter, $"{text.Length}/144"); 248 | UpdateChatColor(); 249 | 250 | XSTools.SetTMPUIText(_oscBarText, text); 251 | } 252 | 253 | private static string ReplaceShortcodes(this string input) 254 | { 255 | var shortcodes = new Dictionary 256 | { 257 | { "//shrug", "¯\\_(ツ)_/¯" }, 258 | { "//happy", "(¬‿¬)" }, 259 | { "//tflip", "┬─┬" }, 260 | { "//music", "🎵" }, 261 | { "//cookie", "🍪" }, 262 | { "//star", "⭐" }, 263 | { "//hrt", "💗" }, 264 | { "//hrt2", "💕" }, 265 | { "//skull", "💀" }, 266 | { "//skull2", "☠" }, 267 | { "//rx3", "rawr x3" } 268 | }; 269 | 270 | foreach (var shortcode in shortcodes) 271 | { 272 | input = input.Replace(shortcode.Key, shortcode.Value); 273 | } 274 | 275 | return input; 276 | } 277 | } -------------------------------------------------------------------------------- /KeyboardOSC/KeyboardOSC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | KeyboardOSC 6 | Keyboard mod for XSOverlay 7 | 1.0.0 8 | true 9 | latest 10 | $(SolutionDir)\builds\$(Configuration) 11 | false 12 | false 13 | 14 | 15 | Full 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ..\refs\Assembly-CSharp.dll 42 | 43 | 44 | ..\refs\Facepunch.Steamworks.Win64.dll 45 | 46 | 47 | ..\refs\Newtonsoft.Json.dll 48 | 49 | 50 | ..\refs\System.Net.Http.dll 51 | 52 | 53 | ..\refs\Unity.TextMeshPro.dll 54 | 55 | 56 | ..\refs\UnityEngine.CoreModule.dll 57 | 58 | 59 | ..\refs\UnityEngine.IMGUIModule.dll 60 | 61 | 62 | ..\refs\UnityEngine.UI.dll 63 | 64 | 65 | ..\refs\uOSC.Runtime.dll 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /KeyboardOSC/Patches.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using HarmonyLib; 4 | using KeyboardOSC.Twitch; 5 | using Steamworks; 6 | using UnityEngine; 7 | using WindowsInput; 8 | using WindowsInput.Native; 9 | using XSOverlay; 10 | using XSOverlay.WebApp; 11 | using XSOverlay.Websockets.API; 12 | 13 | namespace KeyboardOSC; 14 | 15 | // ReSharper disable InconsistentNaming 16 | public static class Patches 17 | { 18 | private static Harmony Harmony; 19 | 20 | private static bool HasSettingsBeenOpenedOnce; 21 | 22 | public static void PatchAll() 23 | { 24 | Harmony = new Harmony("nwnt.keyboardosc"); 25 | 26 | #region Patch Key Presses 27 | 28 | var sendKeyMethod = AccessTools.Method(typeof(KeyboardInputHandler), "SendKey"); 29 | var sendKeyPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(KeyboardPatch))); 30 | Harmony.Patch(sendKeyMethod, sendKeyPatch); 31 | 32 | #endregion 33 | 34 | #region Stop inputs being sent to overlays 35 | 36 | // stop inputs being sent to overlays 37 | var keyPress = AccessTools.Method(typeof(KeyboardSimulator), nameof(KeyboardSimulator.KeyPress), 38 | new[] { typeof(VirtualKeyCode) }); 39 | var keyDown = AccessTools.Method(typeof(KeyboardSimulator), nameof(KeyboardSimulator.KeyDown), 40 | new[] { typeof(VirtualKeyCode) }); 41 | var keyUp = AccessTools.Method(typeof(KeyboardSimulator), nameof(KeyboardSimulator.KeyUp), 42 | new[] { typeof(VirtualKeyCode) }); 43 | var blockInputPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(BlockInput))); 44 | 45 | Harmony.Patch(keyPress, prefix: blockInputPatch); 46 | Harmony.Patch(keyDown, prefix: blockInputPatch); 47 | Harmony.Patch(keyUp, prefix: blockInputPatch); 48 | 49 | #endregion 50 | 51 | 52 | // Scale bar with keyboard 53 | var scaleMethod = 54 | AccessTools.Method(typeof(WindowMovementManager), nameof(WindowMovementManager.DoScaleWindowFixed)); 55 | var scalePatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(ScalePatch))); 56 | Harmony.Patch(scaleMethod, null, scalePatch); 57 | 58 | // gosh i love enumerators (fix for a problem i created lol) 59 | var attachMethod = AccessTools.Method(typeof(WindowMovementManager), "DelayedTransformSetToTarget"); 60 | var attachPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(AttachedMovePatch))); 61 | Harmony.Patch(attachMethod, null, attachPatch); 62 | 63 | // Disable analytics by default (Xiexe loves seeing my plugin errors im sure XD) 64 | // can be turned back on after launching if you want to send him stuff for some reason 65 | var initAnalytics = AccessTools.Method(typeof(AnalyticsManager), "Initialize"); 66 | var analyticsPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(AnalyticsPatch))); 67 | Harmony.Patch(initAnalytics, postfix: analyticsPatch); 68 | 69 | 70 | #region Settings UI Related Patches 71 | 72 | var toggleAppSettings = 73 | AccessTools.Method(typeof(Overlay_Manager), nameof(Overlay_Manager.ToggleEditMode)); 74 | var settingsOverlayPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(SettingsOverlayPatch))); 75 | 76 | var setSettings = AccessTools.Method(typeof(XSettingsManager), nameof(XSettingsManager.SetSetting)); 77 | var setSettingsPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(SetSettingPatch))); 78 | 79 | var reqSettings = AccessTools.Method(typeof(ApiHandler), "OnRequestCurrentSettings"); 80 | var reqSettingsPatch = new HarmonyMethod(typeof(Patches).GetMethod(nameof(RequestSettingsPatch))); 81 | 82 | Harmony.Patch(toggleAppSettings, postfix: settingsOverlayPatch); 83 | Harmony.Patch(setSettings, setSettingsPatch); 84 | Harmony.Patch(reqSettings, reqSettingsPatch); 85 | 86 | #endregion 87 | } 88 | 89 | public static bool KeyboardPatch(KeyboardKey.VirtualKeyEventData keyEventData) 90 | { 91 | if (Plugin.ChatModeActive) ChatMode.HandleKey(keyEventData); 92 | return true; 93 | } 94 | 95 | public static void AnalyticsPatch() 96 | { 97 | XSettingsManager.Instance.Settings.SendAnalytics = false; 98 | } 99 | 100 | public static void AttachedMovePatch(Unity_Overlay overlay, Transform target, Overlay_TrackDevice deviceTracker, 101 | Unity_Overlay.OverlayTrackedDevice device, Vector3 pos, Quaternion rot, float originalScale, 102 | float originalOpacity) 103 | { 104 | var chatBar = Plugin.Instance.oscBarWindowObj.GetComponent(); 105 | Plugin.Instance.RepositionBar(chatBar, overlay); 106 | } 107 | 108 | public static void ScalePatch(float dist, Unity_Overlay activeOverlay, float StartingWidth) 109 | { 110 | if (!Plugin.ChatModeActive || activeOverlay.overlayKey != "xso.overlay.keyboard") return; 111 | var chatBar = Plugin.Instance.oscBarWindowObj.GetComponent(); 112 | chatBar.opacity = activeOverlay.opacity; 113 | Plugin.Instance.RepositionBar(chatBar, activeOverlay); 114 | } 115 | 116 | public static void SettingsOverlayPatch() 117 | { 118 | if (!Plugin.ModifiedUiSuccess || HasSettingsBeenOpenedOnce) return; 119 | Plugin.PluginLogger.LogInfo("Replacing settings page url!"); 120 | var globalSettings = Overlay_Manager.Instance.GlobalSettingsMenuOverlay; 121 | var loadString = 122 | $"http://localhost:{ExternalMessageHandler.Instance.Config.WebSocketPort + 1}/apps/_UI/Default/Settings/SettingsKO.html"; 123 | globalSettings.OverlayWebView._webView.WebView.LoadUrl(loadString); 124 | HasSettingsBeenOpenedOnce = true; 125 | } 126 | 127 | public static void RequestSettingsPatch(string sender, string data) 128 | { 129 | // create new UiSettings instance 130 | var pluginVersion = Plugin.AssemblyVersion; 131 | if (SteamClient.IsValid && SteamApps.CurrentBetaName != null) 132 | { 133 | pluginVersion += $" — you're on the {SteamApps.CurrentBetaName} branch of XSOverlay! Check the plugin repo releases tab for beta plugin updates/fixes"; 134 | } else if (Tools.UpdateCheckResult.Key) 135 | { 136 | pluginVersion += $" — A newer version {Tools.UpdateCheckResult.Value} is available!"; 137 | } 138 | 139 | var settings = new UiSettings 140 | { 141 | KBCheckForUpdates = PluginSettings.GetSetting("CheckForUpdates").Value, 142 | KBLiveSend = PluginSettings.GetSetting("LiveSend").Value, 143 | KBTypingIndicator = PluginSettings.GetSetting("TypingIndicator").Value, 144 | KBDisableMaxLength = PluginSettings.GetSetting("DisableMaxLength").Value, 145 | KBTwitchSending = PluginSettings.GetSetting("TwitchSending").Value, 146 | KBDisableAffixes = PluginSettings.GetSetting("DisableAffixes").Value, 147 | KBVersion = pluginVersion, 148 | }; 149 | var data2 = JsonUtility.ToJson(settings, false); 150 | ServerClientBridge.Instance.Api.SendMessage("UpdateSettings", data2, null, sender); 151 | } 152 | 153 | public static bool SetSettingPatch(string name, string value, string value1, bool sendAnalytics = false) 154 | { 155 | switch (name) 156 | { 157 | case "GoToOgSettings": 158 | var loadString = 159 | $"http://localhost:{ExternalMessageHandler.Instance.Config.WebSocketPort + 1}/ui/Settings.html"; 160 | Overlay_Manager.Instance.GlobalSettingsMenuOverlay.OverlayWebView._webView.WebView.LoadUrl(loadString); 161 | Overlay_Manager.Instance.ToggleApplicationSettings(); 162 | break; 163 | case "KBCheckForUpdates": 164 | PluginSettings.SetSetting("CheckForUpdates", value); 165 | break; 166 | case "KBLiveSend": 167 | PluginSettings.SetSetting("LiveSend", value); 168 | break; 169 | case "KBTypingIndicator": 170 | PluginSettings.SetSetting("TypingIndicator", value); 171 | break; 172 | case "KBTwitchSending": 173 | PluginSettings.SetSetting("TwitchSending", value); 174 | break; 175 | case "KBDisableAffixes": 176 | PluginSettings.SetSetting("DisableAffixes", value); 177 | break; 178 | case "KBOpenRepo": 179 | Application.OpenURL("https://github.com/nyakowint/xsoverlay-keyboard-osc"); 180 | Tools.SendBread("KeyboardOSC Github link opened in browser!"); 181 | break; 182 | case "KBVersionCheck": 183 | Task.Run(Tools.CheckVersion); 184 | break; 185 | } 186 | 187 | if (name.StartsWith("Twitch")) 188 | Core.SettingsCallback(name, value); 189 | 190 | return true; 191 | } 192 | 193 | public static bool BlockInput(VirtualKeyCode keyCode) 194 | { 195 | if (!Plugin.ChatModeActive) return true; 196 | 197 | // small caveat with the way i'm doing this: 198 | // modifier keys still get passed to windows so that i don't have to reimplement xso's logic for them 199 | var passthroughKeys = new List 200 | { 201 | // Modifier keys 202 | VirtualKeyCode.LSHIFT, VirtualKeyCode.RSHIFT, 203 | VirtualKeyCode.LCONTROL, VirtualKeyCode.RCONTROL, 204 | VirtualKeyCode.LALT, VirtualKeyCode.RALT, 205 | VirtualKeyCode.CAPITAL, VirtualKeyCode.PRNTSCRN, // aka VirtualKeyCode.SNAPSHOT 206 | 207 | // windows keys and media keys so the wrist one still functions+ 208 | VirtualKeyCode.LWIN, VirtualKeyCode.RWIN, 209 | VirtualKeyCode.PLAY, VirtualKeyCode.PAUSE, // no idea if anything uses these buuut just in case 210 | VirtualKeyCode.MEDIA_PLAY_PAUSE, VirtualKeyCode.MEDIA_STOP, 211 | VirtualKeyCode.MEDIA_NEXT_TRACK, VirtualKeyCode.MEDIA_PREV_TRACK, 212 | // and finally arrow keys if needed 213 | VirtualKeyCode.UP, VirtualKeyCode.DOWN, 214 | VirtualKeyCode.LEFT, VirtualKeyCode.RIGHT 215 | }; 216 | return passthroughKeys.Contains(keyCode); 217 | } 218 | } -------------------------------------------------------------------------------- /KeyboardOSC/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Threading.Tasks; 4 | using BepInEx; 5 | using BepInEx.Logging; 6 | using HarmonyLib; 7 | using KeyboardOSC; 8 | using KeyboardOSC.XScripts; 9 | using TMPro; 10 | using UnityEngine; 11 | using UnityEngine.UI; 12 | using XSOverlay; 13 | using XSOverlay.WebApp; 14 | 15 | [assembly: AssemblyVersion(Plugin.AssemblyVersion)] 16 | 17 | namespace KeyboardOSC 18 | { 19 | [BepInPlugin("nwnt.keyboardosc", "KeyboardOSC", AssemblyVersion)] 20 | public class Plugin : BaseUnityPlugin 21 | { 22 | public const string AssemblyVersion = "1.2.4.0"; 23 | public static Plugin Instance; 24 | public static ManualLogSource PluginLogger; 25 | 26 | public static bool IsDebugConfig = false; 27 | public static bool ChatModeActive; 28 | public static bool ModifiedUiSuccess; 29 | 30 | public Overlay_Manager overlayManager; 31 | public KeyboardInputHandler inputHandler; 32 | 33 | public GameObject toggleBarObj; 34 | public GameObject oscBarWindowObj; 35 | public GameObject oscBarCanvas; 36 | 37 | public static MethodInfo ReleaseStickyKeys; 38 | 39 | private void Awake() 40 | { 41 | #if DEBUG 42 | IsDebugConfig = true; 43 | #endif 44 | PluginLogger = Logger; 45 | if (Instance != null) Destroy(this); 46 | PluginSettings.ConfigFile = Config; 47 | PluginSettings.Init(); 48 | 49 | // Download modified settings code 50 | ModifiedUiSuccess = Tools.DownloadModifiedUi(); 51 | 52 | if (!Environment.CommandLine.Contains("-batchmode") || IsDebugConfig) return; 53 | Logger.LogWarning("XSOverlay runs in batchmode normally (headless without a window)."); 54 | Logger.LogWarning("To see extended logs launch XSOverlay directly."); 55 | } 56 | 57 | private void Start() 58 | { 59 | Instance = this; 60 | Logger.LogInfo("hi!"); 61 | Console.Title = "KeyboardOSC - XSOverlay"; 62 | 63 | ReleaseStickyKeys = AccessTools.Method(typeof(KeyboardInputHandler), "ReleaseStickyKeys"); 64 | Patches.PatchAll(); 65 | 66 | ServerClientBridge.Instance.Api.Commands["Keyboard"] = delegate 67 | { 68 | InitializeKeyboard(); 69 | Overlay_Manager.Instance.EnableKeyboard(); 70 | }; 71 | } 72 | 73 | public void InitializeKeyboard() 74 | { 75 | // Plugin startup logic 76 | Logger.LogInfo("Keyboard opened for the first time, initializing KeyboardOSC Plugin."); 77 | overlayManager = Overlay_Manager.Instance; 78 | inputHandler = overlayManager.Keyboard.GetComponent(); 79 | 80 | SetupToggleButton(); 81 | SetupBar(); 82 | 83 | ServerClientBridge.Instance.Api.Commands["Keyboard"] = delegate { Overlay_Manager.Instance.EnableKeyboard(); }; 84 | var checkUpdates = PluginSettings.GetSetting("CheckForUpdates"); 85 | if (checkUpdates.Value) Task.Run(Tools.CheckVersion); 86 | } 87 | 88 | private void SetupToggleButton() 89 | { 90 | var lockButtonObj = overlayManager.Keyboard.GetComponentInChildren(true).gameObject; 91 | toggleBarObj = Instantiate(lockButtonObj, lockButtonObj.transform.parent); 92 | toggleBarObj.DestroyComponent(); 93 | toggleBarObj.AddComponent(); 94 | toggleBarObj.transform.Find("Image").GetComponent().sprite = "chat".GetSprite(); 95 | toggleBarObj.Rename("KeyboardOSC Toggle"); 96 | } 97 | 98 | private void SetupBar() 99 | { 100 | var keyboard = overlayManager.Keyboard; 101 | var keyboardWindow = overlayManager.Keyboard_Overlay; 102 | var keyboardWindowObj = overlayManager.Keyboard_Overlay.gameObject; 103 | keyboardWindowObj.SetActive(false); 104 | 105 | // Create typing bar 106 | var oscBarRoot = new GameObject("KeyboardOSC Root"); 107 | oscBarRoot.SetActive(false); 108 | oscBarRoot.AddComponent(); 109 | 110 | oscBarWindowObj = Instantiate(keyboardWindow.gameObject, oscBarRoot.transform); 111 | oscBarWindowObj.Rename("KeyboardOSC Window"); 112 | oscBarWindowObj.DestroyComponent(); 113 | oscBarWindowObj.DestroyComponent(); 114 | oscBarWindowObj.AddComponent(); 115 | 116 | var oscBarWindow = oscBarWindowObj.GetComponent(); 117 | oscBarWindowObj.GetComponent().OverlayTopLevelObject = oscBarRoot; 118 | oscBarWindow.overlayName = "chatbar"; 119 | oscBarWindow.overlayKey = "chatbar"; 120 | oscBarWindow.isMoveable = false; 121 | 122 | 123 | oscBarCanvas = Instantiate(keyboard.transform.Find("Keyboard Canvas | Manager"), 124 | oscBarRoot.transform, true).gameObject; 125 | oscBarCanvas.Rename("KeyboardOSC Bar Canvas"); 126 | oscBarCanvas.GetComponent().referencePixelsPerUnit = 420; 127 | Destroy(oscBarCanvas.transform.Find("KeyboardToolbar").gameObject); 128 | Destroy(oscBarCanvas.transform.Find("Keyboard Background/KeyboardLayout").gameObject); 129 | var kbBackground = oscBarCanvas.transform.Find("Keyboard Background").gameObject; 130 | 131 | foreach (Transform child in kbBackground.transform.Find("KeyboardSettings/Options")) 132 | { 133 | if (child.transform.GetSiblingIndex() > 0) 134 | { 135 | Destroy(child.gameObject); 136 | } 137 | } 138 | 139 | var oscBarCameraObj = Instantiate(keyboard.GetComponentInChildren().gameObject, 140 | oscBarRoot.transform); 141 | var oscBarCamera = oscBarCameraObj.GetComponent(); 142 | var camTrans = oscBarCamera.transform; 143 | var canvasTrans = oscBarCanvas.transform; 144 | oscBarCameraObj.Rename("Camera_OSCBar"); 145 | 146 | oscBarCameraObj.DestroyComponent(); 147 | var rescaleToCanvas = oscBarCameraObj.AddComponent(); 148 | rescaleToCanvas.canvas = oscBarCanvas.GetComponent(); 149 | rescaleToCanvas.camera = oscBarCamera; 150 | 151 | 152 | oscBarWindow.renderTexHeightOverride = 60; 153 | oscBarWindow.renderTexWidthOverride = 730; 154 | oscBarWindow.widthInMeters = 0.55f; 155 | oscBarWindow.cameraForTexture = oscBarCamera; 156 | oscBarWindow.OverlayCanvas = oscBarCanvas.GetComponent(); 157 | 158 | oscBarWindow.canvasGraphicsCaster = oscBarCanvas.GetComponent(); 159 | 160 | // KEEP or the bar's camera will be borked 161 | oscBarRoot.transform.position = new Vector3(0.3f, 1, 0); 162 | 163 | var keyboardPos = Overlay_Manager.Instance.Keyboard_Overlay.transform; 164 | var obwTransform = oscBarWindow.transform; 165 | obwTransform.position = keyboardPos.TransformDirection(0, 0.01f, 0); 166 | obwTransform.rotation = new Quaternion(0, 0, 0, 0); 167 | 168 | var kbOpacity = (Slider) AccessTools.Field(typeof(XSettingsManager), "KeyboardOpacity") 169 | .GetValue(XSettingsManager.Instance); 170 | kbOpacity.onValueChanged.AddListener(value => { oscBarWindow.opacity = value; }); 171 | 172 | XSOEventSystem.OnGrabbedOrDroppedOverlay += (targetOverlay, _, grabbed) => 173 | { 174 | if (targetOverlay.overlayKey != "xso.overlay.keyboard" || grabbed) return; 175 | RepositionBar(oscBarWindow, targetOverlay); 176 | }; 177 | 178 | kbBackground.transform.localPosition = Vector3.zero; 179 | kbBackground.GetComponent().sizeDelta = new Vector2(4130, 475); 180 | 181 | camTrans.position = canvasTrans.position; 182 | camTrans.rotation = canvasTrans.rotation; 183 | // 184 | var oscBarTextObj = Instantiate(keyboard.transform.Find("Keyboard Canvas | Manager/Keyboard Background/KeyboardSettings/Options/Audio Toggle/Text (TMP)").gameObject, kbBackground.transform); 185 | var barCharCounter = Instantiate(oscBarTextObj, kbBackground.transform); 186 | Destroy(oscBarCanvas.transform.Find("Keyboard Background/KeyboardSettings").gameObject); 187 | oscBarTextObj.Rename("KeyboardOSC Bar Text"); 188 | barCharCounter.Rename("Character Counter"); 189 | 190 | var oscbarText = oscBarTextObj.GetComponent(); 191 | var charCounterText = barCharCounter.GetComponent(); 192 | XSTools.SetTMPUIText(oscbarText, "type something silly!"); 193 | XSTools.SetTMPUIText(charCounterText, "0/144"); 194 | 195 | oscbarText.fontSize = 250f; 196 | oscbarText.fontSizeMax = 250f; 197 | oscbarText.horizontalAlignment = HorizontalAlignmentOptions.Center; 198 | oscbarText.verticalAlignment = VerticalAlignmentOptions.Middle; 199 | 200 | charCounterText.fontSize = 100f; 201 | charCounterText.fontSizeMax = 100f; 202 | charCounterText.fontStyle = FontStyles.Bold; 203 | charCounterText.horizontalAlignment = HorizontalAlignmentOptions.Right; 204 | charCounterText.verticalAlignment = VerticalAlignmentOptions.Top; 205 | 206 | 207 | ChatMode.Setup(oscbarText, charCounterText); 208 | 209 | var trackDevice = keyboardWindowObj.AddComponent(); 210 | trackDevice.TrackedDevice = Unity_Overlay.OverlayTrackedDevice.None; 211 | 212 | oscBarRoot.SetActive(true); 213 | keyboardWindowObj.SetActive(true); 214 | WindowMovementManager.MoveToEdgeOfWindowAndInheritRotation(oscBarWindow, keyboardWindow, 215 | -0.1f, 0f, 1); 216 | } 217 | 218 | public void RepositionBar(Unity_Overlay barOverlay, Unity_Overlay keebOverlay) 219 | { 220 | barOverlay.opacity = keebOverlay.opacity; 221 | WindowMovementManager.ScaleOverlayToScale(keebOverlay.widthInMeters - 0.1f, 0.1f, barOverlay); 222 | WindowMovementManager.MoveToEdgeOfWindowAndInheritRotation(barOverlay, keebOverlay, 223 | Vector3.Distance(keebOverlay.transform.position, barOverlay.transform.position) * 0.05f, 0f, 1); 224 | } 225 | 226 | public void AttachKeyboard(int anchor) 227 | { 228 | WindowMovementManager.WMM_Inst.AttachWindowToDeviceIndex(anchor, Overlay_Manager.Instance.Keyboard_Overlay); 229 | } 230 | 231 | public void ToggleChatMode() 232 | { 233 | ReleaseStickyKeys.Invoke(inputHandler, null); 234 | 235 | ChatModeActive = !ChatModeActive; 236 | oscBarWindowObj.SetActive(ChatModeActive); 237 | 238 | var barOverlay = oscBarWindowObj.GetComponent(); 239 | if (ChatModeActive) 240 | { 241 | RepositionBar(barOverlay, overlayManager.Keyboard_Overlay); 242 | } 243 | 244 | SetToggleColor(); 245 | } 246 | 247 | private void SetToggleColor() 248 | { 249 | var chatButton = toggleBarObj.GetComponent 51 |

Make sure your app's redirect uri is set to this page's URL!

52 |
53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /twitchAuth.js: -------------------------------------------------------------------------------- 1 | import * as Api from '../_Shared/api.js'; 2 | window.Initialize = Initialize; 3 | window.SendAuthCallback = SendAuthCallback; 4 | 5 | window.AuthorizeTwitch = function () { 6 | let clientId = document.getElementById('clientId').value; 7 | let clientSecret = document.getElementById('clientSecret').value; 8 | let redirectUri = window.location.href; 9 | window.localStorage.setItem('twitch', JSON.stringify({ clientId, clientSecret, redirectUri })); 10 | window.location = `https://id.twitch.tv/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=user%3Aread%3Achat+user%3Awrite%3Achat`; 11 | }; 12 | 13 | function SendAuthCallback(code) { 14 | let clientInfo = JSON.parse(window.localStorage.getItem('twitch')); 15 | window.localStorage.removeItem('twitch'); 16 | clientInfo.code = code; 17 | let UpdatedSetting = { 18 | internalName: 'TwitchAuthCode', 19 | value: JSON.stringify(clientInfo), 20 | fullUpdate: true, 21 | } 22 | try { 23 | setTimeout(() => { 24 | console.log("Sending auth callback"); 25 | Api.Send('SetXSOverlaySystemSetting', JSON.stringify(UpdatedSetting), null); 26 | }, 2000); 27 | } catch { 28 | console.log("Failed to send auth callback, retrying in 5 seconds"); 29 | setTimeout(() => { 30 | console.log("Sending auth callback"); 31 | Api.Send('SetXSOverlaySystemSetting', JSON.stringify(UpdatedSetting), null); 32 | }, 5000); 33 | } 34 | } 35 | 36 | function Initialize(name) { 37 | Api.Connect(name); 38 | SubscribeToApiEvents(); 39 | let params = new URLSearchParams(window.location.search); 40 | document.getElementById('warn').innerText += ` ${window.location.href.replace(window.location.search, '')}`; 41 | if (params.get('code')) { 42 | console.log('Found access token'); 43 | SendAuthCallback(getAccessToken(params.get('code'))); 44 | } 45 | } 46 | 47 | function getAccessToken(str) { 48 | if (str.startsWith('?')) { 49 | str = str.substring(1); 50 | } 51 | return str.split('&')[0].replace('code=', ''); 52 | } 53 | 54 | // Message Handling Functions 55 | function SubscribeToApiEvents() { 56 | Api.Client.Socket.addEventListener('open', () => { 57 | Api.Send(Api.Commands.RequestGetSettings, null, null); 58 | }); 59 | 60 | Api.Client.Socket.addEventListener('message', function message(data) { 61 | HandleMessages(data); 62 | }); 63 | 64 | Api.Client.Socket.addEventListener('close', () => { 65 | console.log(`${Api.ApiObject.sender} websocket was disconnected. Attempting reconnect.`); 66 | setTimeout(function () { 67 | Api.Connect('twitch'); 68 | SubscribeToApiEvents(); 69 | console.log("Reconnecting..."); 70 | }, 1000); 71 | }); 72 | } 73 | 74 | 75 | function HandleMessages(msg) { 76 | var decoded = Api.Parse(msg); 77 | switch (decoded.Command) { 78 | case 'UpdateSettings': 79 | break; 80 | } 81 | } -------------------------------------------------------------------------------- /update-xslibs.ps1: -------------------------------------------------------------------------------- 1 | # ai generated script lolol 2 | # Set the paths for source and destination folders 3 | $sourceFolder = "C:\Program Files (x86)\Steam\steamapps\common\XSOverlay_Beta\XSOverlay_Data\Managed" 4 | $destinationFolder = "D:\Stuff\XSOMod\refs" 5 | 6 | # Get a list of files in the source folder 7 | $sourceFiles = Get-ChildItem -Path $sourceFolder 8 | 9 | # Iterate through each file in the source folder 10 | foreach ($file in $sourceFiles) { 11 | # Construct the full path for the corresponding file in the destination folder 12 | $destinationFile = Join-Path $destinationFolder $file.Name 13 | 14 | # Check if the file already exists in the destination folder 15 | if (Test-Path $destinationFile) { 16 | # Copy the file to the destination folder 17 | Copy-Item -Path $file.FullName -Destination $destinationFolder -Force 18 | Write-Host "Copied $($file.Name) to $($destinationFolder)" 19 | } 20 | } 21 | 22 | PAUSE --------------------------------------------------------------------------------