Decompiled source of VibingBattleGolf v0.4.25
BepInEx\plugins\VibingYorunyan-VibingBattleGolf\com.vegancat.vibingbattlegolf.dll
Decompiled 2 months ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mirror; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Networking; using VibingBattleGolf.Config; using VibingBattleGolf.Lovense; using VibingBattleGolf.Patches; using VibingBattleGolf.Services; using VibingBattleGolf.Tips; using VibingBattleGolf.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("GameAssembly")] [assembly: IgnoresAccessChecksTo("SharedAssembly")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("com.vegancat.vibingbattlegolf")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.4.25.0")] [assembly: AssemblyInformationalVersion("0.4.25+6ba6d9d2931833bdc27fb2d4d3f75ff874986e46")] [assembly: AssemblyProduct("Vibing Battle Golf")] [assembly: AssemblyTitle("com.vegancat.vibingbattlegolf")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.4.25.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace VibingBattleGolf { [BepInPlugin("com.vegancat.vibingbattlegolf", "Vibing Battle Golf", "0.4.25")] public sealed class Plugin : BaseUnityPlugin { private Harmony _harmony; private LovenseClient _client; private TipController _tips; internal static ManualLogSource Log { get; private set; } internal static VibeController Vibes { get; private set; } internal static LovenseClient Lovense { get; private set; } internal static TipBridgeServer TipBridge { get; private set; } internal static OrbitalLaserVibeController OrbitalLaserVibes { get; private set; } private void Awake() { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; ModConfig modConfig = ModConfig.Load(((BaseUnityPlugin)this).Config); GameObject val = new GameObject("VibingBattleGolfTracker"); Object.DontDestroyOnLoad((Object)val); LovenseRequestQueue lovenseRequestQueue = val.AddComponent<LovenseRequestQueue>(); VibeActivationTimer vibeActivationTimer = val.AddComponent<VibeActivationTimer>(); _client = new LovenseClient(Log, modConfig.LovenseBaseUrl.Value, modConfig.LovensePlatformName.Value, modConfig.LovenseAutoDiscover.Value, lovenseRequestQueue); lovenseRequestQueue.Initialize(_client); vibeActivationTimer.Initialize(_client); _client.SetTimer(vibeActivationTimer); Lovense = _client; Vibes = new VibeController(Log, _client); _tips = new TipController(Log, _client); TipBridgeServer tipBridgeServer = val.AddComponent<TipBridgeServer>(); tipBridgeServer.Initialize(Log, _tips); TipBridge = tipBridgeServer; OrbitalLaserVibeController orbitalLaserVibeController = val.AddComponent<OrbitalLaserVibeController>(); orbitalLaserVibeController.Initialize(Log, _client); OrbitalLaserVibes = orbitalLaserVibeController; _harmony = new Harmony("com.vegancat.vibingbattlegolf"); ApplyHarmonyPatches(); val.AddComponent<LocalPlayerTracker>(); val.AddComponent<TestHotkeyComponent>(); val.AddComponent<SettingsGuiComponent>(); _client.DiscoverAsync(); Log.LogInfo((object)"Vibing Battle Golf v0.4.25 loaded (built for Super Battle Golf Steam update 2026-07-29 / SBG 1.2+)."); } private void ApplyHarmonyPatches() { Type[] array = new Type[5] { typeof(PlayerMovementPatches), typeof(PlayerInventoryPatches), typeof(PlayerInfoPatches), typeof(PlayerGolferPatches), typeof(OrbitalLaserPatches) }; foreach (Type type in array) { try { _harmony.CreateClassProcessor(type).Patch(); Log.LogDebug((object)("Harmony patched " + type.Name)); } catch (Exception ex) { Log.LogError((object)("Harmony failed to patch " + type.Name + ": " + ex.Message)); } } } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _client?.Dispose(); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.vegancat.vibingbattlegolf"; public const string PLUGIN_NAME = "Vibing Battle Golf"; public const string PLUGIN_VERSION = "0.4.25"; } } namespace VibingBattleGolf.UI { internal sealed class SettingsGuiComponent : MonoBehaviour { private const int WindowWidth = 460; private const int WindowHeight = 600; private static readonly string[] TabLabels = new string[4] { "Events", "Items", "Tips", "Settings" }; private bool _visible; private int _tab; private Vector2 _eventsScroll; private Vector2 _itemsScroll; private Vector2 _tipsScroll; private Rect _windowRect; private string _statusMessage = ""; private float _statusExpires; private GUIStyle _headerStyle; private GUIStyle _toggleStyle; private bool _stylesReady; private bool _showModelToken; private string _openPresetKey; private void Awake() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) _windowRect = new Rect(20f, 80f, 460f, 600f); } private void Update() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current == null) { return; } if (((ButtonControl)current[ModConfig.Instance.SettingsGuiKey.Value]).wasPressedThisFrame) { _visible = !_visible; if (_visible) { ResetWindowSize(); } } if (_statusExpires > 0f && Time.unscaledTime > _statusExpires) { _statusMessage = ""; _statusExpires = 0f; } } private void OnGUI() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (_visible) { EnsureStyles(); _windowRect = GUI.Window(78257, _windowRect, new WindowFunction(DrawWindow), "Vibing Battle Golf"); ResetWindowSize(); } } private void DrawWindow(int id) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(Array.Empty<GUILayoutOption>()); DrawMasterRow(); DrawTabBar(); GUILayout.Space(4f); switch (_tab) { case 0: DrawEventsTab(); break; case 1: DrawItemsTab(); break; case 2: DrawTipsTab(); break; default: DrawSettingsTab(); break; } if (!string.IsNullOrEmpty(_statusMessage)) { GUILayout.Label(_statusMessage, Array.Empty<GUILayoutOption>()); } GUILayout.Space(8f); GUILayout.Label($"Press {ModConfig.Instance.SettingsGuiKey.Value} to close", GUI.skin.label, Array.Empty<GUILayoutOption>()); GUILayout.EndVertical(); GUI.DragWindow(new Rect(0f, 0f, ((Rect)(ref _windowRect)).width, 24f)); } private void ResetWindowSize() { ((Rect)(ref _windowRect)).width = 460f; ((Rect)(ref _windowRect)).height = 600f; } private void DrawMasterRow() { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); ModConfig instance = ModConfig.Instance; bool flag = GUILayout.Toggle(instance.MasterEnabled.Value, " Master Enable", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != instance.MasterEnabled.Value) { instance.MasterEnabled.Value = flag; instance.Save(); } TipConfig tips = instance.Tips; if (tips.Enabled.Value) { bool flag2 = GUILayout.Toggle(tips.ReactToTips.Value, " Tip React", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag2 != tips.ReactToTips.Value) { tips.ReactToTips.Value = flag2; instance.Save(); } } GUILayout.FlexibleSpace(); if (GUILayout.Button("Test Vibe", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { Plugin.Vibes.Trigger(VibeEvent.TestPing); SetStatus("Test vibe sent."); } GUILayout.EndHorizontal(); } private void DrawTabBar() { _tab = GUILayout.Toolbar(_tab, TabLabels, Array.Empty<GUILayoutOption>()); } private void DrawEventsTab() { //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Enable All", Array.Empty<GUILayoutOption>())) { ModConfig.Instance.SetAllEventsEnabled(enabled: true); SetStatus("All events enabled."); } if (GUILayout.Button("Disable All", Array.Empty<GUILayoutOption>())) { ModConfig.Instance.SetAllEventsEnabled(enabled: false); SetStatus("All events disabled."); } GUILayout.EndHorizontal(); GUILayout.Space(4f); _eventsScroll = GUILayout.BeginScrollView(_eventsScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(360f) }); foreach (EventGuiEntry eventEntry in ModConfig.Instance.EventEntries) { DrawEventRow(eventEntry); } DrawOrbitalLaserSection(); GUILayout.EndScrollView(); } private void DrawOrbitalLaserSection() { OrbitalLaserConfig orbitalLaser = ModConfig.Instance.OrbitalLaser; GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>()); GUILayout.Label("Orbital Laser (special)", _headerStyle, Array.Empty<GUILayoutOption>()); bool flag = GUILayout.Toggle(orbitalLaser.Enabled.Value, " Enabled (charge ramp + max hit)", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != orbitalLaser.Enabled.Value) { orbitalLaser.Enabled.Value = flag; ModConfig.Instance.Save(); } DrawStrengthSlider("Start", orbitalLaser.ChargeStartStrength); DrawStrengthSlider("End", orbitalLaser.ChargeEndStrength); DrawStrengthSlider("Hit", orbitalLaser.HitStrength); DrawDurationSlider("Dur", orbitalLaser.HitDuration); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Test start", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) })) { Plugin.Vibes.Preview(orbitalLaser.ChargeStartStrength.Value, 1.5f, "", "Orbital charge start"); SetStatus("Test orbital start " + StrengthScale.Format(orbitalLaser.ChargeStartStrength.Value) + " for 1.5s"); } if (GUILayout.Button("Test end", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(72f) })) { Plugin.Vibes.Preview(orbitalLaser.ChargeEndStrength.Value, 1.5f, "", "Orbital charge end"); SetStatus("Test orbital end " + StrengthScale.Format(orbitalLaser.ChargeEndStrength.Value) + " for 1.5s"); } if (GUILayout.Button("Test hit", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(72f) })) { Plugin.Vibes.Preview(orbitalLaser.HitStrength.Value, orbitalLaser.HitDuration.Value, "", "Orbital hit"); SetStatus($"Test orbital hit {StrengthScale.Format(orbitalLaser.HitStrength.Value)} for {orbitalLaser.HitDuration.Value:0.##}s"); } GUILayout.EndHorizontal(); bool flag2 = GUILayout.Toggle(orbitalLaser.CancelMaxIfNotDestroyed.Value, " Max only if KO/eliminated", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag2 != orbitalLaser.CancelMaxIfNotDestroyed.Value) { orbitalLaser.CancelMaxIfNotDestroyed.Value = flag2; ModConfig.Instance.Save(); } GUILayout.EndVertical(); GUILayout.Space(4f); } private void DrawStrengthSlider(string label, ConfigEntry<int> entry) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (!string.IsNullOrEmpty(label)) { GUILayout.Label(label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(40f) }); } int num = Mathf.RoundToInt(GUILayout.HorizontalSlider((float)StrengthScale.ToPercent(entry.Value), 0f, 100f, Array.Empty<GUILayoutOption>()) / 5f) * 5; num = Mathf.Clamp(num, 0, 100); GUILayout.Label($"{num}%", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(40f) }); GUILayout.EndHorizontal(); int num2 = StrengthScale.FromPercent(num); if (num2 != entry.Value) { entry.Value = num2; ModConfig.Instance.Save(); } } private void DrawDurationSlider(string label, ConfigEntry<float> entry) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (!string.IsNullOrEmpty(label)) { GUILayout.Label(label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(40f) }); } float num = DurationScale.Clamp(GUILayout.HorizontalSlider(entry.Value, 0.1f, 120f, Array.Empty<GUILayoutOption>())); GUILayout.Label(DurationScale.Format(num), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(44f) }); GUILayout.EndHorizontal(); if (!Mathf.Approximately(num, entry.Value)) { entry.Value = num; ModConfig.Instance.Save(); } } private void DrawEventRow(EventGuiEntry entry) { EventConfig config = entry.Config; GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag = GUILayout.Toggle(config.Enabled.Value, " " + entry.Label, _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != config.Enabled.Value) { config.Enabled.Value = flag; ModConfig.Instance.Save(); } GUILayout.EndHorizontal(); DrawStrengthSlider("Str", config.Strength); DrawDurationSlider("Dur", config.Duration); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.FlexibleSpace(); if (GUILayout.Button("Test", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(48f) })) { Plugin.Vibes.Preview(config, entry.Label); SetStatus("Test " + entry.Label + ": " + StrengthScale.Format(config.Strength.Value) + " for " + DurationScale.Format(config.Duration.Value)); } GUILayout.EndHorizontal(); DrawPresetDropdown("event:" + entry.Label, config.Preset); GUILayout.EndVertical(); GUILayout.Space(2f); } private void DrawItemsTab() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) _itemsScroll = GUILayout.BeginScrollView(_itemsScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(390f) }); foreach (KeyValuePair<ItemType, ItemVibeConfig> item in ModConfig.Instance.Items) { DrawItemRow(item.Value); } GUILayout.EndScrollView(); } private void DrawItemRow(ItemVibeConfig item) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_028f: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>()); GUILayout.Label(((object)item.ItemType/*cast due to .constrained prefix*/).ToString(), _headerStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag = GUILayout.Toggle(item.UseEnabled.Value, " Use", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != item.UseEnabled.Value) { item.UseEnabled.Value = flag; ModConfig.Instance.Save(); } GUILayout.FlexibleSpace(); if (GUILayout.Button("Test", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(44f) })) { Plugin.Vibes.Preview(item.UseStrength.Value, item.UseDuration.Value, item.UsePreset.Value, $"{item.ItemType} use"); SetStatus($"Test {item.ItemType} use: {StrengthScale.Format(item.UseStrength.Value)} for {DurationScale.Format(item.UseDuration.Value)}"); } GUILayout.EndHorizontal(); DrawStrengthSlider("Str", item.UseStrength); DrawDurationSlider("Dur", item.UseDuration); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag2 = GUILayout.Toggle(item.HitEnabled.Value, " Hit", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag2 != item.HitEnabled.Value) { item.HitEnabled.Value = flag2; ModConfig.Instance.Save(); } GUILayout.FlexibleSpace(); if (GUILayout.Button("Test", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(44f) })) { Plugin.Vibes.Preview(item.HitStrength.Value, item.HitDuration.Value, item.HitPreset.Value, $"{item.ItemType} hit"); SetStatus($"Test {item.ItemType} hit: {StrengthScale.Format(item.HitStrength.Value)} for {DurationScale.Format(item.HitDuration.Value)}"); } GUILayout.EndHorizontal(); DrawStrengthSlider("Str", item.HitStrength); DrawDurationSlider("Dur", item.HitDuration); DrawPresetDropdown($"item:{item.ItemType}:use", item.UsePreset, "Use pattern"); DrawPresetDropdown($"item:{item.ItemType}:hit", item.HitPreset, "Hit pattern"); GUILayout.EndVertical(); GUILayout.Space(2f); } private void DrawPresetDropdown(string key, ConfigEntry<string> entry, string label = "Preset") { int num = LovensePresets.IndexOf(entry.Value); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(72f) }); if (GUILayout.Button(LovensePresets.Labels[num], (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) })) { _openPresetKey = ((_openPresetKey == key) ? null : key); } GUILayout.EndHorizontal(); if (!(_openPresetKey != key)) { int num2 = GUILayout.SelectionGrid(num, LovensePresets.Labels, 3, Array.Empty<GUILayoutOption>()); if (num2 != num) { entry.Value = LovensePresets.Values[num2]; ModConfig.Instance.Save(); _openPresetKey = null; } } } private void DrawTipsTab() { //IL_0653: Unknown result type (might be due to invalid IL or missing references) //IL_066b: Unknown result type (might be due to invalid IL or missing references) //IL_0670: Unknown result type (might be due to invalid IL or missing references) TipConfig tips = ModConfig.Instance.Tips; string text = (TipFeed.BridgeConnected ? "Connected" : "Waiting for tip-bridge.html"); GUILayout.Label("Bridge: " + text, _headerStyle, Array.Empty<GUILayoutOption>()); GUILayout.Label($"Relay: http://127.0.0.1:{tips.RelayPort.Value}/tip", Array.Empty<GUILayoutOption>()); GUILayout.Label("Lovense mToken", _headerStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (_showModelToken) { string text2 = GUILayout.TextField(tips.ModelToken.Value ?? "", Array.Empty<GUILayoutOption>()); if (!string.Equals(text2, tips.ModelToken.Value, StringComparison.Ordinal)) { tips.ModelToken.Value = text2; ModConfig.Instance.Save(); } } else { GUILayout.Label(string.IsNullOrWhiteSpace(tips.ModelToken.Value) ? "No token saved" : "Token saved", Array.Empty<GUILayoutOption>()); } if (GUILayout.Button(_showModelToken ? "Hide" : "Edit", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(44f) })) { _showModelToken = !_showModelToken; } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Open Tip Bridge", Array.Empty<GUILayoutOption>())) { if (TipBridgeLauncher.TryOpen(Plugin.Log, out var error)) { SetStatus("Tip bridge opened in browser."); } else { SetStatus(error ?? "Could not open tip bridge."); } } if (GUILayout.Button("Save token", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) })) { ModConfig.Instance.Save(); SetStatus(string.IsNullOrWhiteSpace(tips.ModelToken.Value) ? "Token cleared." : "Token saved to config."); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag = GUILayout.Toggle(tips.Enabled.Value, " Tip integration enabled", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != tips.Enabled.Value) { tips.Enabled.Value = flag; ModConfig.Instance.Save(); Plugin.TipBridge.RestartListener(); } if (GUILayout.Button("Restart relay", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(100f) })) { Plugin.TipBridge.RestartListener(); SetStatus("Tip relay restarted."); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag2 = GUILayout.Toggle(tips.ReactToTips.Value, " React to tips (vibes + block golf)", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag2 != tips.ReactToTips.Value) { tips.ReactToTips.Value = flag2; ModConfig.Instance.Save(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Mode", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(36f) }); int value = (int)tips.Mode.Value; int num = GUILayout.SelectionGrid(value, new string[3] { "Pass", "Mirror", "Bonus" }, 3, Array.Empty<GUILayoutOption>()); if (num != value) { tips.Mode.Value = (TipIntegrationMode)num; ModConfig.Instance.Save(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag3 = GUILayout.Toggle(tips.BlockGameDuringTips.Value, " Block golf vibes during tips", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag3 != tips.BlockGameDuringTips.Value) { tips.BlockGameDuringTips.Value = flag3; ModConfig.Instance.Save(); } bool flag4 = GUILayout.Toggle(tips.UseConnectLevels.Value, " Use Connect levels", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag4 != tips.UseConnectLevels.Value) { tips.UseConnectLevels.Value = flag4; ModConfig.Instance.Save(); } GUILayout.EndHorizontal(); GUILayout.Label("Tip tiers", _headerStyle, Array.Empty<GUILayoutOption>()); DrawStrengthSlider("T1", tips.Tier1Strength); DrawDurationSlider("T1", tips.Tier1Duration); DrawStrengthSlider("T2", tips.Tier2Strength); DrawDurationSlider("T2", tips.Tier2Duration); DrawStrengthSlider("T3", tips.Tier3Strength); DrawDurationSlider("T3", tips.Tier3Duration); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button($"Test T1 {tips.Tier1Duration.Value:0.#}s", Array.Empty<GUILayoutOption>())) { Plugin.Vibes.Preview(tips.Tier1Strength.Value, tips.Tier1Duration.Value, tips.Tier1Preset.Value, "Tip tier 1"); SetStatus($"Test tip tier 1: {StrengthScale.Format(tips.Tier1Strength.Value)} for {tips.Tier1Duration.Value:0.##}s"); } if (GUILayout.Button($"Test T2 {tips.Tier2Duration.Value:0.#}s", Array.Empty<GUILayoutOption>())) { Plugin.Vibes.Preview(tips.Tier2Strength.Value, tips.Tier2Duration.Value, tips.Tier2Preset.Value, "Tip tier 2"); SetStatus($"Test tip tier 2: {StrengthScale.Format(tips.Tier2Strength.Value)} for {tips.Tier2Duration.Value:0.##}s"); } if (GUILayout.Button($"Test T3 {tips.Tier3Duration.Value:0.#}s", Array.Empty<GUILayoutOption>())) { Plugin.Vibes.Preview(tips.Tier3Strength.Value, tips.Tier3Duration.Value, tips.Tier3Preset.Value, "Tip tier 3"); SetStatus($"Test tip tier 3: {StrengthScale.Format(tips.Tier3Strength.Value)} for {tips.Tier3Duration.Value:0.##}s"); } GUILayout.EndHorizontal(); DrawPresetDropdown("tip:t1", tips.Tier1Preset, "T1 pattern"); DrawPresetDropdown("tip:t2", tips.Tier2Preset, "T2 pattern"); DrawPresetDropdown("tip:t3", tips.Tier3Preset, "T3 pattern"); GUILayout.Label("Recent tips", _headerStyle, Array.Empty<GUILayoutOption>()); _tipsScroll = GUILayout.BeginScrollView(_tipsScroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(160f) }); if (TipFeed.RecentTips.Count == 0) { GUILayout.Label("No tips yet. Open tip-bridge.html in your browser.", Array.Empty<GUILayoutOption>()); } else { foreach (TipEvent recentTip in TipFeed.RecentTips) { GUILayout.Label(recentTip.Summary, Array.Empty<GUILayoutOption>()); } } GUILayout.EndScrollView(); } private void DrawSettingsTab() { //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) ModConfig instance = ModConfig.Instance; GUILayout.Label("Lovense", _headerStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("URL", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(30f) }); string text = GUILayout.TextField(instance.LovenseBaseUrl.Value, Array.Empty<GUILayoutOption>()); if (!string.Equals(text, instance.LovenseBaseUrl.Value, StringComparison.Ordinal)) { instance.LovenseBaseUrl.Value = text; instance.Save(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); bool flag = GUILayout.Toggle(instance.LovenseAutoDiscover.Value, " Auto-discover endpoints", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag != instance.LovenseAutoDiscover.Value) { instance.LovenseAutoDiscover.Value = flag; instance.Save(); } GUILayout.EndHorizontal(); if (GUILayout.Button("Reconnect Lovense", Array.Empty<GUILayoutOption>())) { Plugin.Lovense.DiscoverAsync().ContinueWith(delegate(Task<bool> task) { SetStatus(task.Result ? "Lovense connected." : "Lovense not reachable."); }); } GUILayout.Space(8f); GUILayout.Label("Hotkeys", _headerStyle, Array.Empty<GUILayoutOption>()); GUILayout.Label($"Settings panel: {instance.SettingsGuiKey.Value}", Array.Empty<GUILayoutOption>()); GUILayout.Label($"Test vibe: {instance.TestVibeKey.Value}", Array.Empty<GUILayoutOption>()); GUILayout.Space(8f); bool flag2 = GUILayout.Toggle(instance.HoleInOneReplacesScored.Value, " Hole-in-one replaces scored event", _toggleStyle, Array.Empty<GUILayoutOption>()); if (flag2 != instance.HoleInOneReplacesScored.Value) { instance.HoleInOneReplacesScored.Value = flag2; instance.Save(); } } private void SetStatus(string message) { _statusMessage = message; _statusExpires = Time.unscaledTime + 3f; } private void EnsureStyles() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown if (!_stylesReady) { _headerStyle = new GUIStyle(GUI.skin.label) { fontStyle = (FontStyle)1 }; _toggleStyle = new GUIStyle(GUI.skin.toggle) { fontSize = 12 }; _stylesReady = true; } } } } namespace VibingBattleGolf.Tips { internal static class TipBridgeLauncher { public static bool TryGetBridgePath(out string path) { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); path = Path.Combine(directoryName ?? "", "tip-bridge", "tip-bridge.html"); return File.Exists(path); } public static bool TryOpen(ManualLogSource log, out string error) { error = null; if (!TryGetBridgePath(out var path)) { error = "tip-bridge.html not found next to the mod DLL."; return false; } TipConfig tips = ModConfig.Instance.Tips; string text = BuildHashFragment(tips.ModelToken.Value, tips.RelayPort.Value, tips.RelaySecret.Value); string fileName = new Uri(Path.GetFullPath(path)).AbsoluteUri + "#" + text; try { Process.Start(new ProcessStartInfo { FileName = fileName, UseShellExecute = true }); log.LogInfo((object)("Opened tip bridge: " + path)); return true; } catch (Exception ex) { error = ex.Message; log.LogWarning((object)("Failed to open tip bridge: " + ex.Message)); return false; } } private static string BuildHashFragment(string modelToken, int relayPort, string relaySecret) { List<string> list = new List<string> { $"relayPort={relayPort}", "autoconnect=1" }; if (!string.IsNullOrWhiteSpace(modelToken)) { list.Add("mToken=" + Uri.EscapeDataString(modelToken.Trim())); } if (!string.IsNullOrWhiteSpace(relaySecret)) { list.Add("relaySecret=" + Uri.EscapeDataString(relaySecret.Trim())); } return string.Join("&", list); } } internal sealed class TipBridgeServer : MonoBehaviour { private readonly ConcurrentQueue<string> _pendingPayloads = new ConcurrentQueue<string>(); private HttpListener _listener; private Thread _listenerThread; private volatile bool _running; private ManualLogSource _log; private TipController _controller; private int _activePort; public void Initialize(ManualLogSource log, TipController controller) { _log = log; _controller = controller; RestartListener(); } private void Update() { string result; while (_pendingPayloads.TryDequeue(out result)) { _controller?.HandlePayload(result); } } private void OnDestroy() { StopListener(); } public void RestartListener() { StopListener(); TipConfig tips = ModConfig.Instance.Tips; if (!tips.Enabled.Value) { TipFeed.MarkBridgeDisconnected(); return; } int num = Mathf.Clamp(tips.RelayPort.Value, 1024, 65535); try { _listener = new HttpListener(); _listener.Prefixes.Add($"http://127.0.0.1:{num}/"); _listener.Start(); _running = true; _activePort = num; _listenerThread = new Thread(ListenLoop) { IsBackground = true, Name = "VibingBattleGolfTipBridge" }; _listenerThread.Start(); _log.LogInfo((object)$"Tip bridge listening on http://127.0.0.1:{num}/tip"); } catch (Exception ex) { _log.LogError((object)$"Tip bridge failed to start on port {num}: {ex.Message}"); TipFeed.MarkBridgeDisconnected(); } } private void ListenLoop() { while (_running) { try { HttpListenerContext context = _listener.GetContext(); HandleRequest(context); } catch (HttpListenerException) when (!_running) { break; } catch (ObjectDisposedException) when (!_running) { break; } catch (Exception ex3) { _log.LogWarning((object)("Tip bridge request error: " + ex3.Message)); } } } private void HandleRequest(HttpListenerContext context) { HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; AddCorsHeaders(response); if (request.HttpMethod == "OPTIONS") { WriteResponse(response, 204, ""); return; } string text = request.Url.AbsolutePath.TrimEnd('/'); if (text.Equals("/health", StringComparison.OrdinalIgnoreCase)) { _pendingPayloads.Enqueue("{\"type\":\"bridge-ping\"}"); WriteResponse(response, 200, "{\"ok\":true}"); return; } if (!text.Equals("/tip", StringComparison.OrdinalIgnoreCase) || !request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) { WriteResponse(response, 404, "{\"error\":\"not found\"}"); return; } if (!Authorize(request)) { WriteResponse(response, 401, "{\"error\":\"unauthorized\"}"); return; } string text2; using (StreamReader streamReader = new StreamReader(request.InputStream, request.ContentEncoding ?? Encoding.UTF8)) { text2 = streamReader.ReadToEnd(); } if (!string.IsNullOrWhiteSpace(text2)) { _pendingPayloads.Enqueue(text2); } WriteResponse(response, 200, "{\"ok\":true}"); } private bool Authorize(HttpListenerRequest request) { string value = ModConfig.Instance.Tips.RelaySecret.Value; if (string.IsNullOrWhiteSpace(value)) { return true; } return string.Equals(request.Headers["X-Vibing-Secret"], value, StringComparison.Ordinal); } private static void AddCorsHeaders(HttpListenerResponse response) { response.Headers.Add("Access-Control-Allow-Origin", "*"); response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, X-Vibing-Secret"); } private static void WriteResponse(HttpListenerResponse response, int statusCode, string body) { response.StatusCode = statusCode; byte[] bytes = Encoding.UTF8.GetBytes(body); response.ContentType = "application/json"; response.ContentLength64 = bytes.Length; response.OutputStream.Write(bytes, 0, bytes.Length); response.OutputStream.Close(); } private void StopListener() { _running = false; if (_listener != null) { try { _listener.Stop(); _listener.Close(); } catch { } _listener = null; } _listenerThread = null; TipFeed.MarkBridgeDisconnected(); } } internal sealed class TipController { private readonly struct TipVibeProfile { public readonly string Tipper; public readonly int Amount; public readonly float Duration; public readonly int Strength; public readonly string Preset; public TipVibeProfile(string tipper, int amount, float duration, int strength, string preset) { Tipper = tipper; Amount = amount; Duration = duration; Strength = strength; Preset = preset; } } private readonly ManualLogSource _log; private readonly LovenseClient _client; public TipController(ManualLogSource log, LovenseClient client) { _log = log; _client = client; } public void HandlePayload(string json) { if (json.IndexOf("bridge-ping", StringComparison.Ordinal) >= 0) { TipFeed.RecordBridgePing(Time.unscaledTime); return; } ModConfig instance = ModConfig.Instance; if (!instance.MasterEnabled.Value || !instance.Tips.Enabled.Value) { return; } TipFeed.RecordBridgePing(Time.unscaledTime); ParsedTipPayload parsed = TipJsonParser.Parse(json); if (!parsed.Found) { _log.LogDebug((object)"Tip payload ignored (no tip fields found)."); return; } TipVibeProfile profile = BuildProfile(parsed); TipEvent tip = new TipEvent(profile.Tipper, profile.Amount, profile.Duration, profile.Strength, profile.Preset, Time.unscaledTime); TipFeed.RecordTip(tip); if (!instance.Tips.ReactToTips.Value) { _log.LogInfo((object)("Tip received (react off): " + tip.Summary)); return; } _log.LogInfo((object)("Tip received: " + tip.Summary)); VibeCommandCoordinator.RegisterTipActive(profile.Duration); TipIntegrationMode value = instance.Tips.Mode.Value; float num; switch (value) { case TipIntegrationMode.Passthrough: return; default: num = 1f; break; case TipIntegrationMode.Bonus: num = instance.Tips.BonusStrengthScale.Value; break; } float strengthScale = num; SendTipVibe(profile, strengthScale, value); } private TipVibeProfile BuildProfile(ParsedTipPayload parsed) { TipConfig tips = ModConfig.Instance.Tips; if (tips.UseConnectLevels.Value && (parsed.VibrateStrength > 0 || parsed.DurationSeconds > 0f)) { int num = Mathf.Clamp(parsed.VibrateStrength, 0, 20); float duration = ((parsed.DurationSeconds > 0f) ? parsed.DurationSeconds : tips.Tier1Duration.Value); string preset = NormalizePreset(parsed.SpecialType); if (num <= 0) { num = ResolveTierStrength(parsed.Amount); } return new TipVibeProfile(parsed.TipperName, parsed.Amount, duration, num, preset); } return ResolveTierProfile(parsed.TipperName, parsed.Amount); } private TipVibeProfile ResolveTierProfile(string tipper, int amount) { TipConfig tips = ModConfig.Instance.Tips; if (amount >= tips.Tier3MinAmount.Value) { return new TipVibeProfile(tipper, amount, tips.Tier3Duration.Value, tips.Tier3Strength.Value, tips.Tier3Preset.Value); } if (amount >= tips.Tier2MinAmount.Value) { return new TipVibeProfile(tipper, amount, tips.Tier2Duration.Value, tips.Tier2Strength.Value, tips.Tier2Preset.Value); } return new TipVibeProfile(tipper, amount, tips.Tier1Duration.Value, tips.Tier1Strength.Value, tips.Tier1Preset.Value); } private int ResolveTierStrength(int amount) { return ResolveTierProfile("x", amount).Strength; } private void SendTipVibe(TipVibeProfile profile, float strengthScale, TipIntegrationMode mode) { int num = Mathf.Clamp(Mathf.RoundToInt((float)profile.Strength * strengthScale), 1, 20); float num2 = VibeRequest.NormalizeDuration(profile.Duration); string text = ((mode == TipIntegrationMode.Bonus) ? "TipBonus" : "TipMirror"); VibeRequest request = (string.IsNullOrWhiteSpace(profile.Preset) ? new VibeRequest($"Vibrate:{num}", num2) : new VibeRequest(null, num2, profile.Preset)); _log.LogInfo((object)$"Tip vibe: {text} {profile.Tipper} ({profile.Amount}) ({num2:0.###}s) -> {request.ToJson()}"); _client.Send(request); } private static string NormalizePreset(string specialType) { if (string.IsNullOrWhiteSpace(specialType) || specialType.Equals("undefined", StringComparison.OrdinalIgnoreCase)) { return ""; } return specialType.Trim().ToLowerInvariant(); } } internal readonly struct TipEvent { public readonly string TipperName; public readonly int Amount; public readonly float DurationSeconds; public readonly int Strength; public readonly string Preset; public readonly float ReceivedAt; public string Summary { get { if (!string.IsNullOrEmpty(Preset)) { return $"{TipperName}: {Amount} tokens ({Preset}, {DurationSeconds:0.#}s)"; } return $"{TipperName}: {Amount} tokens ({Strength}, {DurationSeconds:0.#}s)"; } } public TipEvent(string tipperName, int amount, float durationSeconds, int strength, string preset, float receivedAt) { TipperName = tipperName ?? "Anonymous"; Amount = amount; DurationSeconds = durationSeconds; Strength = strength; Preset = preset ?? ""; ReceivedAt = receivedAt; } } internal static class TipFeed { private const int MaxEntries = 12; private static readonly List<TipEvent> Entries = new List<TipEvent>(); private static float _lastBridgePingAt; private static bool _bridgeConnected; public static IReadOnlyList<TipEvent> RecentTips => Entries; public static bool BridgeConnected => _bridgeConnected; public static float LastBridgePingAt => _lastBridgePingAt; public static void RecordTip(TipEvent tip) { Entries.Insert(0, tip); while (Entries.Count > 12) { Entries.RemoveAt(Entries.Count - 1); } } public static void RecordBridgePing(float time) { _lastBridgePingAt = time; _bridgeConnected = true; } public static void MarkBridgeDisconnected() { _bridgeConnected = false; } } internal readonly struct ParsedTipPayload { public readonly bool Found; public readonly string TipperName; public readonly int Amount; public readonly float DurationSeconds; public readonly int VibrateStrength; public readonly string SpecialType; public ParsedTipPayload(bool found, string tipperName, int amount, float durationSeconds, int vibrateStrength, string specialType) { Found = found; TipperName = tipperName; Amount = amount; DurationSeconds = durationSeconds; VibrateStrength = vibrateStrength; SpecialType = specialType; } } internal static class TipJsonParser { public static ParsedTipPayload Parse(string json) { if (string.IsNullOrWhiteSpace(json)) { return Empty(); } int valueOrDefault = ExtractInt(json, "amount").GetValueOrDefault(); string tipperName = ExtractString(json, "tipperName") ?? ExtractString(json, "tipper") ?? "Anonymous"; float valueOrDefault2 = ExtractFloat(json, "time").GetValueOrDefault(); int valueOrDefault3 = ExtractInt(json, "vibrate").GetValueOrDefault(); string specialType = ExtractString(json, "specialType") ?? ""; if (valueOrDefault <= 0 && valueOrDefault3 <= 0 && valueOrDefault2 <= 0f) { return Empty(); } return new ParsedTipPayload(found: true, tipperName, valueOrDefault, valueOrDefault2, valueOrDefault3, specialType); } private static ParsedTipPayload Empty() { return new ParsedTipPayload(found: false, null, 0, 0f, 0, null); } private static string ExtractString(string json, string field) { string text = "\"" + field + "\":\""; int num = json.IndexOf(text, StringComparison.Ordinal); if (num < 0) { return null; } num += text.Length; int num2 = json.IndexOf('"', num); if (num2 >= 0) { return json.Substring(num, num2 - num); } return null; } private static int? ExtractInt(string json, string field) { string text = "\"" + field + "\":"; int num = json.IndexOf(text, StringComparison.Ordinal); if (num < 0) { return null; } for (num += text.Length; num < json.Length && char.IsWhiteSpace(json[num]); num++) { } int i; for (i = num; i < json.Length && (char.IsDigit(json[i]) || json[i] == '-'); i++) { } if (!int.TryParse(json.Substring(num, i - num), out var result)) { return null; } return result; } private static float? ExtractFloat(string json, string field) { string text = "\"" + field + "\":"; int num = json.IndexOf(text, StringComparison.Ordinal); if (num < 0) { return null; } for (num += text.Length; num < json.Length && char.IsWhiteSpace(json[num]); num++) { } int i; for (i = num; i < json.Length && "0123456789.-".IndexOf(json[i]) >= 0; i++) { } if (!float.TryParse(json.Substring(num, i - num), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return null; } return result; } } } namespace VibingBattleGolf.Services { internal sealed class LocalPlayerTracker : MonoBehaviour { private static readonly FieldRef<HoleProgressBarUi, int> DisplayedStrokes = AccessTools.FieldRefAccess<HoleProgressBarUi, int>("displayedStrokes"); private PlayerGolfer _localGolfer; private bool _wasScored; private bool _wasEliminated; private int _lastHoleIndex = -1; private void Update() { if (!NetworkClient.active) { ResetState(); return; } TrackHoleChanges(); if (_localGolfer == null) { _localGolfer = FindLocalGolfer(); } if (!((Object)(object)_localGolfer == (Object)null)) { TrackScored(); TrackEliminated(); } } private void TrackHoleChanges() { int currentHoleCourseIndex = CourseManager.CurrentHoleCourseIndex; if (_lastHoleIndex != -1 && currentHoleCourseIndex != _lastHoleIndex) { _wasScored = false; } _lastHoleIndex = currentHoleCourseIndex; } private void TrackScored() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 bool flag = (int)_localGolfer.MatchResolution == 1 || ((Object)(object)_localGolfer.OwnBall != (Object)null && _localGolfer.OwnBall.isInHole); if (flag && !_wasScored) { if (TryTriggerHoleInOne()) { _wasScored = true; return; } Plugin.Vibes.Trigger(VibeEvent.Scored); } _wasScored = flag; } private bool TryTriggerHoleInOne() { if (!ModConfig.Instance.HoleInOne.Enabled.Value) { return false; } if (GetDisplayedStrokes() != 1) { return false; } Plugin.Vibes.Trigger(VibeEvent.HoleInOne); return ModConfig.Instance.HoleInOneReplacesScored.Value; } private static int GetDisplayedStrokes() { HoleProgressBarUi instance = SingletonBehaviour<HoleProgressBarUi>.Instance; if (!((Object)(object)instance == (Object)null)) { return DisplayedStrokes.Invoke(instance); } return -1; } private void TrackEliminated() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) bool flag = (int)_localGolfer.MatchResolution == 2; if (flag && !_wasEliminated) { try { EliminationReason localPlayerLatestImmediateEliminationReason = _localGolfer.LocalPlayerLatestImmediateEliminationReason; Plugin.OrbitalLaserVibes?.OnLocalPlayerWillBeEliminated(localPlayerLatestImmediateEliminationReason); } catch { } Plugin.Vibes.Trigger(VibeEvent.Eliminated); } _wasEliminated = flag; } private static PlayerGolfer FindLocalGolfer() { PlayerGolfer[] array = Object.FindObjectsByType<PlayerGolfer>((FindObjectsSortMode)0); foreach (PlayerGolfer val in array) { if ((Object)(object)val != (Object)null && ((NetworkBehaviour)val).isLocalPlayer) { return val; } } return null; } private void ResetState() { _localGolfer = null; _wasScored = false; _wasEliminated = false; _lastHoleIndex = -1; } } internal sealed class OrbitalLaserVibeController : MonoBehaviour { private enum Phase { Idle, Charging, MaxHit } private const float LostThreatGraceSeconds = 0.35f; private const float ProximityPadding = 2.5f; private const float SessionTailSeconds = 6f; private ManualLogSource _log; private LovenseClient _client; private Phase _phase; private int _activeLaserId; private Coroutine _rampCoroutine; private Coroutine _maxHitResetCoroutine; private float _lastThreatSeenAt = -999f; private float _maxHitUntil = -999f; private bool _sessionArmed; private float _sessionArmedUntil = -999f; private bool _wasLocalTargetThisSession; private bool _hadDamageContactThisSession; private bool _pollWasKnockedOut; private PlayerMatchResolution _pollLastResolution; private float _nextPollLogAt; private OrbitalLaserState _lastLoggedState = (OrbitalLaserState)(-1); private bool _lastLoggedTargeted; internal bool IsHandlingOrbitalHits { get { OrbitalLaserConfig orbitalLaser = ModConfig.Instance.OrbitalLaser; if (orbitalLaser != null && orbitalLaser.Enabled.Value) { return ModConfig.Instance.MasterEnabled.Value; } return false; } } private bool IsSessionArmed { get { if (_sessionArmed) { return Time.unscaledTime <= _sessionArmedUntil; } return false; } } internal void Initialize(ManualLogSource log, LovenseClient client) { _log = log; _client = client; } private void Update() { //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) if (_phase == Phase.MaxHit && Time.unscaledTime >= _maxHitUntil) { FinishMaxHit("max duration elapsed"); } if (!IsHandlingOrbitalHits) { if (_phase == Phase.Charging) { CancelCharge("feature disabled"); } ClearSessionIfExpired(); } else { if (_phase == Phase.MaxHit) { return; } if (IsSessionArmed) { PollForDestroyOutcome(); } ClearSessionIfExpired(); if (_phase == Phase.MaxHit) { return; } Vector3 position; Hittable hittable; if (!NetworkClient.active && !NetworkServer.active) { if (_phase == Phase.Charging) { CancelCharge("left match"); } DisarmSession("left match"); } else if (TryGetLocalPlayerPosition(out position, out hittable)) { RefreshSessionFromAllLasers(position, hittable); bool wasTarget; OrbitalLaser val = FindThreateningLaser(position, hittable, out wasTarget); if ((Object)(object)val != (Object)null) { _lastThreatSeenAt = Time.unscaledTime; ExtendSession(((Object)val).GetInstanceID(), wasTarget); BeginCharge(((Object)val).GetInstanceID()); } else if (_phase == Phase.Charging && Time.unscaledTime - _lastThreatSeenAt > 0.35f) { CancelCharge("anticipation ended (session still armed for KO/elim)"); } } } } internal void OnLaserStateChanged(OrbitalLaser laser, OrbitalLaserState previous, OrbitalLaserState current) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Invalid comparison between Unknown and I4 //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Invalid comparison between Unknown and I4 //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) if (!IsHandlingOrbitalHits || (Object)(object)laser == (Object)null || _phase == Phase.MaxHit || !TryGetLocalPlayerPosition(out var position, out var hittable)) { return; } bool flag = IsLocalPlayerTarget(laser, hittable); bool flag2 = IsThreateningLaser(laser, position, hittable); bool flag3 = HorizontalDistance(((Component)laser).transform.position, position) <= GetThreatRadius(); if (flag2 || flag) { ExtendSession(((Object)laser).GetInstanceID(), flag); } if (flag2) { _lastThreatSeenAt = Time.unscaledTime; BeginCharge(((Object)laser).GetInstanceID()); } if ((int)current == 3 || (int)current == 4 || (int)current == 0) { if (flag || flag3 || _wasLocalTargetThisSession || _hadDamageContactThisSession) { ExtendSession(((Object)laser).GetInstanceID(), flag || _wasLocalTargetThisSession, forceFullTail: true); } if (_phase == Phase.Charging) { CancelCharge($"laser {current} (session armed={IsSessionArmed})"); } } LogStateOnce(laser, current, flag2 || flag || flag3); } internal void OnDamageHit() { if (!IsHandlingOrbitalHits) { return; } if (!VibeCommandCoordinator.CanProcessGameEvent()) { _log.LogDebug((object)"Orbital laser hit vibe skipped: tip active."); } else if (_phase != Phase.MaxHit) { StopRampCoroutine(); if (_phase == Phase.Charging) { _phase = Phase.Idle; } _hadDamageContactThisSession = true; ExtendSession(_activeLaserId, _wasLocalTargetThisSession, forceFullTail: true); TryStopOwnedGameVibe(12, "laser contact"); if (!ModConfig.Instance.OrbitalLaser.CancelMaxIfNotDestroyed.Value) { StartMaxHit("hit (max-on-contact mode)"); return; } _log.LogInfo((object)("Orbital laser contact — session armed; max on NEW KO/elimination " + $"(~{_sessionArmedUntil - Time.unscaledTime:0.0}s left).")); PollForDestroyOutcome(); } } internal void OnLocalPlayerWillBeEliminated(EliminationReason reason) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (IsHandlingOrbitalHits && _phase != Phase.MaxHit) { if (!IsOrbitalEliminationReason(reason)) { _log.LogDebug((object)$"Elimination {reason} ignored for orbital max (not an orbital reason)."); } else { StartMaxHit($"eliminated by {reason}"); } } } internal void OnOrbitalLaserKnockout(KnockoutType knockoutType) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) if (IsHandlingOrbitalHits && _phase != Phase.MaxHit) { StartMaxHit($"knockout {knockoutType}"); } } internal static bool IsOrbitalKnockoutType(KnockoutType knockoutType) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if ((int)knockoutType != 17 && (int)knockoutType != 25 && (int)knockoutType != 32) { return (int)knockoutType == 35; } return true; } private void PollForDestroyOutcome() { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Invalid comparison between Unknown and I4 //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Invalid comparison between Unknown and I4 //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) try { PlayerGolfer[] array = Object.FindObjectsByType<PlayerGolfer>((FindObjectsSortMode)0); foreach (PlayerGolfer val in array) { if ((Object)(object)val == (Object)null || !((NetworkBehaviour)val).isLocalPlayer) { continue; } PlayerMatchResolution matchResolution = val.MatchResolution; if ((int)matchResolution == 2 && (int)_pollLastResolution != 2) { _pollLastResolution = matchResolution; EliminationReason localPlayerLatestImmediateEliminationReason = val.LocalPlayerLatestImmediateEliminationReason; if (IsOrbitalEliminationReason(localPlayerLatestImmediateEliminationReason)) { OnLocalPlayerWillBeEliminated(localPlayerLatestImmediateEliminationReason); break; } } else { _pollLastResolution = matchResolution; } PlayerMovement val2 = (((Object)(object)val.PlayerInfo != (Object)null) ? val.PlayerInfo.Movement : null); _pollWasKnockedOut = (Object)(object)val2 != (Object)null && val2.IsKnockedOut; break; } } catch { } } internal static bool IsOrbitalEliminationReason(EliminationReason reason) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if ((int)reason != 24 && (int)reason != 25 && (int)reason != 40) { return (int)reason == 43; } return true; } private void StartMaxHit(string reason) { if (_phase != Phase.MaxHit) { StopRampCoroutine(); StopMaxHitResetCoroutine(); OrbitalLaserConfig orbitalLaser = ModConfig.Instance.OrbitalLaser; int strength = Mathf.Clamp(orbitalLaser.HitStrength.Value, 0, 20); float num = Mathf.Max(0.1f, orbitalLaser.HitDuration.Value); _phase = Phase.MaxHit; _maxHitUntil = Time.unscaledTime + num + 0.15f; ResetSessionFlags(); TrySendGameVibe(strength, num, "OrbitalMax:" + reason); _maxHitResetCoroutine = ((MonoBehaviour)this).StartCoroutine(MaxHitResetRoutine(num + 0.15f)); } } private bool TrySendGameVibe(int strength, float duration, string label) { if (!VibeCommandCoordinator.TryAcceptGameVibe(strength, duration, label, out var rejectReason)) { _log.LogInfo((object)("Orbital vibe skipped (" + label + "): " + rejectReason)); return false; } _log.LogInfo((object)$"Orbital vibe: {label} ({duration:0.###}s) strength={strength}"); _client.Send(new VibeRequest($"Vibrate:{strength}", duration)); return true; } private void TryStopOwnedGameVibe(int maxStrengthWeMayStop, string reason) { if (!VibeCommandCoordinator.TryAcceptGameStop(maxStrengthWeMayStop, out var rejectReason)) { _log.LogDebug((object)("Orbital stop skipped (" + reason + "): " + rejectReason)); } else { _client.SendStop(); } } private IEnumerator MaxHitResetRoutine(float seconds) { yield return (object)new WaitForSecondsRealtime(Mathf.Max(0.1f, seconds)); if (_phase == Phase.MaxHit) { FinishMaxHit("max reset coroutine"); } _maxHitResetCoroutine = null; } private void FinishMaxHit(string reason) { if (_phase == Phase.MaxHit) { StopMaxHitResetCoroutine(); _phase = Phase.Idle; _maxHitUntil = -999f; ResetSessionFlags(); _log.LogInfo((object)("Orbital laser max phase cleared (" + reason + ") — ready for next laser.")); } } private void StopMaxHitResetCoroutine() { if (_maxHitResetCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_maxHitResetCoroutine); _maxHitResetCoroutine = null; } } private void ExtendSession(int laserId, bool wasTarget, bool forceFullTail = false) { if (_phase != Phase.MaxHit) { if (laserId != 0) { _activeLaserId = laserId; } if (wasTarget) { _wasLocalTargetThisSession = true; } float num = Mathf.Max(6f, ModConfig.Instance.OrbitalLaser.DestroyGraceSeconds.Value); float num2 = Time.unscaledTime + num; if (forceFullTail || !_sessionArmed || num2 > _sessionArmedUntil) { _sessionArmedUntil = num2; } if (!_sessionArmed) { _sessionArmed = true; SnapshotPollBaselines(); _log.LogInfo((object)($"Orbital laser session armed (target={_wasLocalTargetThisSession}, " + $"contact={_hadDamageContactThisSession}, {num:0.##}s tail).")); } } } private void SnapshotPollBaselines() { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) try { PlayerGolfer[] array = Object.FindObjectsByType<PlayerGolfer>((FindObjectsSortMode)0); foreach (PlayerGolfer val in array) { if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).isLocalPlayer) { _pollLastResolution = val.MatchResolution; PlayerMovement val2 = (((Object)(object)val.PlayerInfo != (Object)null) ? val.PlayerInfo.Movement : null); _pollWasKnockedOut = (Object)(object)val2 != (Object)null && val2.IsKnockedOut; break; } } } catch { _pollLastResolution = (PlayerMatchResolution)0; _pollWasKnockedOut = false; } } private void RefreshSessionFromAllLasers(Vector3 localPos, Hittable localHittable) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Invalid comparison between Unknown and I4 OrbitalLaser[] array; try { array = Object.FindObjectsByType<OrbitalLaser>((FindObjectsSortMode)0); } catch { return; } OrbitalLaser[] array2 = array; foreach (OrbitalLaser val in array2) { if ((Object)(object)val == (Object)null) { continue; } OrbitalLaserState networkstate; try { networkstate = val.Networkstate; } catch { continue; } if ((int)networkstate == 3 || (int)networkstate == 4) { bool flag = IsLocalPlayerTarget(val, localHittable); bool flag2 = HorizontalDistance(((Component)val).transform.position, localPos) <= GetThreatRadius(); if (flag || flag2 || _wasLocalTargetThisSession) { ExtendSession(((Object)val).GetInstanceID(), flag || _wasLocalTargetThisSession); } } } } private void DisarmSession(string reason) { if (_sessionArmed) { _log.LogDebug((object)("Orbital laser session disarmed (" + reason + ").")); } ResetSessionFlags(); if (_phase != Phase.MaxHit) { _phase = Phase.Idle; } } private void ClearSessionIfExpired() { if (_sessionArmed && Time.unscaledTime > _sessionArmedUntil) { _log.LogInfo((object)"Orbital laser session expired without KO/elim — max skipped."); ResetSessionFlags(); if (_phase != Phase.MaxHit) { _phase = Phase.Idle; } } } private void ResetSessionFlags() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) _sessionArmed = false; _sessionArmedUntil = -999f; _wasLocalTargetThisSession = false; _hadDamageContactThisSession = false; _activeLaserId = 0; _pollWasKnockedOut = false; _pollLastResolution = (PlayerMatchResolution)0; } private OrbitalLaser FindThreateningLaser(Vector3 localPos, Hittable localHittable, out bool wasTarget) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) wasTarget = false; OrbitalLaser[] array; try { array = Object.FindObjectsByType<OrbitalLaser>((FindObjectsSortMode)0); } catch { return null; } OrbitalLaser result = null; float num = float.MaxValue; bool flag = false; OrbitalLaser[] array2 = array; foreach (OrbitalLaser val in array2) { if (!((Object)(object)val == (Object)null) && IsThreateningLaser(val, localPos, localHittable)) { bool flag2 = IsLocalPlayerTarget(val, localHittable); float num2 = HorizontalDistance(((Component)val).transform.position, localPos); if (num2 < num) { num = num2; result = val; flag = flag2; } } } wasTarget = flag; return result; } private bool IsThreateningLaser(OrbitalLaser laser, Vector3 localPos, Hittable localHittable) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Invalid comparison between Unknown and I4 //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) OrbitalLaserState val; try { val = laser.Networkstate; } catch { try { val = laser.state; } catch { return false; } } if ((int)val != 1 && (int)val != 2) { return false; } if (IsLocalPlayerTarget(laser, localHittable)) { return true; } return HorizontalDistance(((Component)laser).transform.position, localPos) <= GetThreatRadius(); } private static bool IsLocalPlayerTarget(OrbitalLaser laser, Hittable localHittable) { try { Hittable val = laser.Networktarget; if ((Object)(object)val == (Object)null) { val = laser.target; } if ((Object)(object)val == (Object)null) { return false; } if ((Object)(object)localHittable != (Object)null && (Object)(object)val == (Object)(object)localHittable) { return true; } Entity asEntity = val.AsEntity; if ((Object)(object)asEntity == (Object)null || !asEntity.IsPlayer) { return false; } PlayerInfo playerInfo = asEntity.PlayerInfo; return (Object)(object)playerInfo != (Object)null && ((NetworkBehaviour)playerInfo).isLocalPlayer; } catch { return false; } } private float GetThreatRadius() { try { ItemSettings itemSettings = GameManager.ItemSettings; if ((Object)(object)itemSettings != (Object)null) { float orbitalLaserExplosionMaxRange = itemSettings.OrbitalLaserExplosionMaxRange; if (orbitalLaserExplosionMaxRange > 0.5f) { return orbitalLaserExplosionMaxRange + 2.5f; } } } catch { } return 10.5f; } private static float HorizontalDistance(Vector3 a, Vector3 b) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) a.y = 0f; b.y = 0f; return Vector3.Distance(a, b); } private static bool TryGetLocalPlayerPosition(out Vector3 position, out Hittable hittable) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) position = default(Vector3); hittable = null; try { PlayerInfo[] array = Object.FindObjectsByType<PlayerInfo>((FindObjectsSortMode)0); foreach (PlayerInfo val in array) { if (!((Object)(object)val == (Object)null) && ((NetworkBehaviour)val).isLocalPlayer) { hittable = val.AsHittable; position = ((Component)val).transform.position; return true; } } } catch { } return false; } private void BeginCharge(int laserId) { if (VibeCommandCoordinator.CanProcessGameEvent() && _phase != Phase.MaxHit && (_phase != Phase.Charging || _activeLaserId != laserId)) { StopRampCoroutine(); _activeLaserId = laserId; _phase = Phase.Charging; _lastThreatSeenAt = Time.unscaledTime; float num = ResolveChargeDuration(); _log.LogInfo((object)$"Orbital laser charge ramp start ({num:0.###}s)."); _rampCoroutine = ((MonoBehaviour)this).StartCoroutine(ChargeRampRoutine(num)); } } private void CancelCharge(string reason) { if (_phase == Phase.Charging) { _log.LogInfo((object)("Orbital laser charge cancelled (" + reason + ").")); StopRampCoroutine(); int maxStrengthWeMayStop = Mathf.Clamp(ModConfig.Instance.OrbitalLaser.ChargeEndStrength.Value, 0, 20); TryStopOwnedGameVibe(maxStrengthWeMayStop, reason); _phase = Phase.Idle; } } private IEnumerator ChargeRampRoutine(float durationSeconds) { OrbitalLaserConfig orbitalLaser = ModConfig.Instance.OrbitalLaser; int start = Mathf.Clamp(orbitalLaser.ChargeStartStrength.Value, 0, 20); int end = Mathf.Clamp(orbitalLaser.ChargeEndStrength.Value, 0, 20); if (end < start) { end = start; } float duration = Mathf.Max(0.25f, durationSeconds); float elapsed = 0f; float step = 0.2f; while (_phase == Phase.Charging && elapsed < duration) { float num = Mathf.Clamp01(elapsed / duration); int num2 = Mathf.RoundToInt(Mathf.Lerp((float)start, (float)end, num)); TrySendGameVibe(num2, 1.5f, $"OrbitalCharge:{num2}"); yield return (object)new WaitForSecondsRealtime(step); elapsed += step; } if (_phase == Phase.Charging) { TrySendGameVibe(end, 2f, $"OrbitalChargeHold:{end}"); while (_phase == Phase.Charging) { yield return (object)new WaitForSecondsRealtime(0.5f); if (_phase == Phase.Charging) { TrySendGameVibe(end, 1.5f, $"OrbitalChargeHold:{end}"); } } } _rampCoroutine = null; } private float ResolveChargeDuration() { float value = ModConfig.Instance.OrbitalLaser.ChargeDuration.Value; try { ItemSettings itemSettings = GameManager.ItemSettings; if ((Object)(object)itemSettings != (Object)null) { float orbitalLaserAnticipationDuration = itemSettings.OrbitalLaserAnticipationDuration; float orbitalLaserActivationTotalDuration = itemSettings.OrbitalLaserActivationTotalDuration; float num = Mathf.Max(orbitalLaserAnticipationDuration, orbitalLaserActivationTotalDuration); if (num > 0.25f) { return num; } } } catch { } return Mathf.Max(0.5f, value); } private void LogStateOnce(OrbitalLaser laser, OrbitalLaserState state, bool relevant) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) if ((state != _lastLoggedState || relevant != _lastLoggedTargeted) && (!(Time.unscaledTime < _nextPollLogAt) || state != _lastLoggedState)) { _lastLoggedState = state; _lastLoggedTargeted = relevant; _nextPollLogAt = Time.unscaledTime + 0.5f; _log.LogInfo((object)($"Orbital laser state={state}, relevant={relevant}, phase={_phase}, " + $"session={IsSessionArmed}, target={_wasLocalTargetThisSession}, contact={_hadDamageContactThisSession}")); } } private void StopRampCoroutine() { if (_rampCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_rampCoroutine); _rampCoroutine = null; } } private void OnDestroy() { StopRampCoroutine(); StopMaxHitResetCoroutine(); ResetSessionFlags(); _phase = Phase.Idle; } } internal sealed class TestHotkeyComponent : MonoBehaviour { private void Update() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current != null) { Key value = ModConfig.Instance.TestVibeKey.Value; if (((ButtonControl)current[value]).wasPressedThisFrame) { Plugin.Log.LogInfo((object)$"Test vibe hotkey pressed ({value}). Retrying Lovense connection if needed."); Plugin.Vibes.Trigger(VibeEvent.TestPing); } } } } internal static class VibeCommandCoordinator { private static float _tipActiveUntil; private static float _gameActiveUntil; private static int _gameActiveStrength; private static string _gameActiveLabel = ""; public static bool IsTipBlockingGameEvents { get { if (ModConfig.Instance.Tips.ReactToTips.Value && ModConfig.Instance.Tips.BlockGameDuringTips.Value) { return Time.unscaledTime < _tipActiveUntil; } return false; } } public static bool CanProcessGameEvent() { return !IsTipBlockingGameEvents; } public static void RegisterTipActive(float durationSeconds) { if (!(durationSeconds <= 0f)) { float num = Time.unscaledTime + durationSeconds; if (num > _tipActiveUntil) { _tipActiveUntil = num; } ClearGameVibeTracking(); } } public static bool TryAcceptGameVibe(int strength, float durationSeconds, string label, out string rejectReason) { rejectReason = null; if (!CanProcessGameEvent()) { rejectReason = "tip active"; return false; } strength = Mathf.Clamp(strength, 0, 20); durationSeconds = Mathf.Max(0.1f, durationSeconds); float unscaledTime = Time.unscaledTime; float num = unscaledTime + durationSeconds; if (unscaledTime < _gameActiveUntil) { if (strength < _gameActiveStrength) { rejectReason = "weaker than active '" + _gameActiveLabel + "' " + $"(new {strength} < active {_gameActiveStrength}, " + $"{_gameActiveUntil - unscaledTime:0.0}s left)"; return false; } if (strength == _gameActiveStrength && num <= _gameActiveUntil + 0.05f) { rejectReason = "does not outrank active '" + _gameActiveLabel + "' " + $"(same strength {strength}, shorter/equal remaining)"; return false; } } _gameActiveUntil = num; _gameActiveStrength = strength; _gameActiveLabel = label ?? ""; return true; } public static bool TryAcceptGameStop(int maxStrengthWeMayStop, out string rejectReason) { rejectReason = null; if (IsTipBlockingGameEvents) { rejectReason = "tip active"; return false; } if (Time.unscaledTime >= _gameActiveUntil) { ClearGameVibeTracking(); return true; } if (_gameActiveStrength > maxStrengthWeMayStop) { rejectReason = "stronger vibe '" + _gameActiveLabel + "' still active " + $"({_gameActiveStrength} > {maxStrengthWeMayStop})"; return false; } ClearGameVibeTracking(); return true; } public static void ClearGameVibeTracking() { _gameActiveUntil = 0f; _gameActiveStrength = 0; _gameActiveLabel = ""; } } internal enum VibeEvent { ItemHit, BallHit, SwingProjectileHit, DiveHit, HornBlast, Knockout, Scored, HoleInOne, ItemUsed, Eliminated, EnemyKill, TestPing } internal readonly struct VibeTriggerContext { public readonly ItemType? ItemType; public readonly float? Distance; public readonly float? Power; public VibeTriggerContext(ItemType? itemType = null, float? distance = null, float? power = null) { ItemType = itemType; Distance = distance; Power = power; } } internal sealed class VibeController { private readonly ManualLogSource _log; private readonly LovenseClient _client; private float _lastHornBlastAt = -999f; public VibeController(ManualLogSource log, LovenseClient client) { _log = log; _client = client; } public void Preview(int strength, float duration, string preset, string label) { SendValues(strength, duration, preset, 1f, "Preview:" + label, force: true); } public void Preview(EventConfig eventConfig, string label) { if (eventConfig != null) { Preview(eventConfig.Strength.Value, eventConfig.Duration.Value, eventConfig.Preset.Value, label); } } public void Trigger(VibeEvent vibeEvent, VibeTriggerContext context = default(VibeTriggerContext)) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Invalid comparison between Unknown and I4 if (!ModConfig.Instance.MasterEnabled.Value) { return; } if (!VibeCommandCoordinator.CanProcessGameEvent()) { _log.LogDebug((object)$"Game vibe skipped ({vibeEvent}): tip active."); return; } if (vibeEvent == VibeEvent.HornBlast) { if (Time.unscaledTime - _lastHornBlastAt < 0.75f) { _log.LogDebug((object)"Horn blast vibe debounced."); return; } _lastHornBlastAt = Time.unscaledTime; } if (vibeEvent == VibeEvent.ItemHit && (int)context.ItemType.GetValueOrDefault() == 10 && ModConfig.Instance.OrbitalLaser.Enabled.Value) { _log.LogDebug((object)"Skipping generic OrbitalLaser ItemHit — handled by orbital sequence."); } else if (!TryTriggerItemSpecific(vibeEvent, context)) { EventConfig eventConfig = GetEventConfig(vibeEvent); if (eventConfig != null && eventConfig.Enabled.Value) { float scale = ComputeScale(vibeEvent, eventConfig, context); Send(eventConfig, scale, vibeEvent.ToString()); } } } private bool TryTriggerItemSpecific(VibeEvent vibeEvent, VibeTriggerContext context) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (!context.ItemType.HasValue || (int)context.ItemType.Value == 0) { return false; } if (!ModConfig.Instance.Items.TryGetValue(context.ItemType.Value, out var value)) { return false; } switch (vibeEvent) { case VibeEvent.ItemHit: if (value.HitEnabled.Value) { SendItemProfile(value.HitStrength.Value, value.HitDuration.Value, value.HitPreset.Value, ComputeDistanceScale(ModConfig.Instance.ItemHitScaling, context.Distance), $"ItemHit:{context.ItemType}"); return true; } break; case VibeEvent.ItemUsed: if (value.UseEnabled.Value) { SendItemProfile(value.UseStrength.Value, value.UseDuration.Value, value.UsePreset.Value, 1f, $"ItemUsed:{context.ItemType}"); return true; } break; } return false; } private EventConfig GetEventConfig(VibeEvent vibeEvent) { ModConfig instance = ModConfig.Instance; return vibeEvent switch { VibeEvent.ItemHit => instance.ItemHit, VibeEvent.BallHit => instance.BallHit, VibeEvent.SwingProjectileHit => instance.SwingProjectileHit, VibeEvent.DiveHit => instance.DiveHit, VibeEvent.HornBlast => instance.HornBlast, VibeEvent.Knockout => instance.Knockout, VibeEvent.Scored => instance.Scored, VibeEvent.HoleInOne => instance.HoleInOne, VibeEvent.ItemUsed => instance.ItemUsed, VibeEvent.Eliminated => instance.Eliminated, VibeEvent.EnemyKill => instance.EnemyKill, VibeEvent.TestPing => instance.TestPing, _ => null, }; } private float ComputeScale(VibeEvent vibeEvent, EventConfig eventConfig, VibeTriggerContext context) { float num = 1f; if (vibeEvent == VibeEvent.ItemHit && context.Distance.HasValue) { num *= ComputeDistanceScale(ModConfig.Instance.ItemHitScaling, context.Distance.Value); } if (vibeEvent == VibeEvent.Knockout && context.Distance.HasValue) { num *= ComputeDistanceScale(ModConfig.Instance.KnockoutScaling, context.Distance.Value); } if (eventConfig.ScaleWithPower.Value && context.Power.HasValue) { float num2 = Mathf.Clamp01(context.Power.Value); num *= Mathf.Lerp(eventConfig.MinPowerScale.Value, eventConfig.MaxPowerScale.Value, num2); } return num; } private static float ComputeDistanceScale(DistanceScalingConfig scaling, float? distance) { if (!distance.HasValue || !scaling.Enabled.Value) { return 1f; } float num = Mathf.Max(scaling.MinDistance.Value, scaling.MaxDistance.Value); float num2 = Mathf.Min(scaling.MinDistance.Value, scaling.MaxDistance.Value); float num3 = Mathf.InverseLerp(num, num2, distance.Value); return Mathf.Lerp(scaling.MinScale.Value, scaling.MaxScale.Value, num3); } private void SendItemProfile(int strength, float duration, string preset, float scale, string label) { SendValues(strength, duration, preset, scale, label); } private void Send(EventConfig eventConfig, float scale, string label) { SendValues(eventConfig.Strength.Value, eventConfig.Duration.Value, eventConfig.Preset.Value, scale, label); } private void SendValues(int strength, float duration, string preset, float scale, string label, bool force = false) { float num = VibeRequest.NormalizeDuration(duration); int num2 = ClampStrength((float)strength * scale); string rejectReason; if (force) { VibeCommandCoordinator.ClearGameVibeTracking(); } else if (!VibeCommandCoordinator.TryAcceptGameVibe(num2, num, label, out rejectReason)) { _log.LogInfo((object)("Vibe skipped (" + label + "): " + rejectReason)); return; } if (force) { VibeCommandCoordinator.TryAcceptGameVibe(num2, num, label, out var _); } VibeRequest request = (string.IsNullOrWhiteSpace(preset) ? new VibeRequest($"Vibrate:{num2}", num) : new VibeRequest(null, num, preset.Trim())); _log.LogInfo((object)$"Vibe trigger: {label} ({num:0.###}s) -> {request.ToJson()}"); _client.Send(request); } private static int ClampStrength(float value) { return Mathf.Clamp(Mathf.RoundToInt(value), 0, 20); } } } namespace VibingBattleGolf.Patches { [HarmonyPatch(typeof(OrbitalLaser))] internal static class OrbitalLaserPatches { [HarmonyPatch("OnStateChanged")] [HarmonyPostfix] private static void OnStateChanged_Postfix(OrbitalLaser __instance, OrbitalLaserState previousState, OrbitalLaserState currentState) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && !((Object)(object)Plugin.OrbitalLaserVibes == (Object)null)) { Plugin.OrbitalLaserVibes.OnLaserStateChanged(__instance, previousState, currentState); } } [HarmonyPatch("OnStartClient")] [HarmonyPostfix] private static void OnStartClient_Postfix(OrbitalLaser __instance) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null || (Object)(object)Plugin.OrbitalLaserVibes == (Object)null) { return; } try { Plugin.OrbitalLaserVibes.OnLaserStateChanged(__instance, (OrbitalLaserState)0, __instance.Networkstate); } catch { } } } [HarmonyPatch(typeof(PlayerGolfer))] internal static class PlayerGolferPatches { [HarmonyPatch("OnLocalPlayerWillBeEliminated")] [HarmonyPostfix] private static void OnLocalPlayerWillBeEliminated_Postfix(PlayerGolfer __instance, EliminationReason immediateEliminationReason, Vector3 eliminationPosition) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Plugin.Log.LogInfo((object)$"Local player will be eliminated: {immediateEliminationReason}"); Plugin.OrbitalLaserVibes?.OnLocalPlayerWillBeEliminated(immediateEliminationReason); } } [HarmonyPatch("UserCode_RpcInformWillBeEliminated__EliminationReason__Vector3")] [HarmonyPostfix] private static void UserCode_RpcInformWillBeEliminated_Postfix(PlayerGolfer __instance) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null || !((NetworkBehaviour)__instance).isLocalPlayer) { return; } try { EliminationReason localPlayerLatestImmediateEliminationReason = __instance.LocalPlayerLatestImmediateEliminationReason; Plugin.OrbitalLaserVibes?.OnLocalPlayerWillBeEliminated(localPlayerLatestImmediateEliminationReason); } catch { } } } [HarmonyPatch(typeof(PlayerInfo))] internal static class PlayerInfoPatches { [HarmonyPatch("UserCode_RpcInformOfBlownAirhorn__PlayerInventory")] [HarmonyPostfix] private static void UserCode_RpcInformOfBlownAirhorn_Postfix(PlayerInfo __instance) { TriggerHornBlastIfLocal(__instance, "RpcInformOfBlownAirhorn"); } [HarmonyPatch("PlayAirhornReactionVfxInternal")] [HarmonyPostfix] private static void PlayAirhornReactionVfxInternal_Postfix(PlayerInfo __instance) { TriggerHornBlastIfLocal(__instance, "PlayAirhornReactionVfxInternal"); } [HarmonyPatch("UserCode_RpcInformOfFlashCameraHit__Single")] [HarmonyPostfix] private static void UserCode_RpcInformOfFlashCameraHit_Postfix(PlayerInfo __instance) { if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Plugin.Log.LogInfo((object)"Flash camera hit detected."); Plugin.Vibes.Trigger(VibeEvent.ItemHit, new VibeTriggerContext((ItemType)14)); } } private static void TriggerHornBlastIfLocal(PlayerInfo instance, string source) { if (!((Object)(object)instance == (Object)null) && ((NetworkBehaviour)instance).isLocalPlayer) { Plugin.Log.LogInfo((object)("Horn blast detected (" + source + ").")); Plugin.Vibes.Trigger(VibeEvent.HornBlast); } } } [HarmonyPatch(typeof(PlayerInventory))] internal static class PlayerInventoryPatches { private static ItemType _lastUsedItem; [HarmonyPatch("TryUseItem")] [HarmonyPrefix] private static void TryUseItem_Prefix(PlayerInventory __instance, bool isAirhornReaction) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null || !((NetworkBehaviour)__instance).isLocalPlayer || isAirhornReaction) { _lastUsedItem = (ItemType)0; } else { _lastUsedItem = __instance.GetEffectivelyEquippedItem(false); } } [HarmonyPatch("TryUseItem")] [HarmonyPostfix] private static void TryUseItem_Postfix(PlayerInventory __instance, bool isAirhornReaction, bool __result) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) if (!(!__result || isAirhornReaction) && !((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { ItemType value = (((int)_lastUsedItem != 0) ? _lastUsedItem : __instance.GetEffectivelyEquippedItem(false)); Plugin.Vibes.Trigger(VibeEvent.ItemUsed, new VibeTriggerContext(value)); } } } [HarmonyPatch(typeof(PlayerMovement))] internal static class PlayerMovementPatches { [HarmonyPatch("OnLocalPlayerWillApplyItemHitPhysics")] [HarmonyPostfix] private static void OnLocalPlayerWillApplyItemHitPhysics_Postfix(PlayerMovement __instance, ItemType itemType, float distance, Vector3 incomingVelocityChange) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { if ((int)itemType == 10 && (Object)(object)Plugin.OrbitalLaserVibes != (Object)null && Plugin.OrbitalLaserVibes.IsHandlingOrbitalHits) { Plugin.OrbitalLaserVibes.OnDamageHit(); return; } Vector3 val = incomingVelocityChange; float value = ((Vector3)(ref val)).magnitude / 20f; Plugin.Vibes.Trigger(VibeEvent.ItemHit, new VibeTriggerContext(itemType, distance, value)); } } [HarmonyPatch("OnLocalPlayerWillApplyGolfSwingHitPhysics")] [HarmonyPostfix] private static void OnLocalPlayerWillApplyGolfSwingHitPhysics_Postfix(PlayerMovement __instance, float power, Vector3 incomingVelocityChange) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Vector3 val = incomingVelocityChange; float num = ((Vector3)(ref val)).magnitude / 25f; float value = Mathf.Clamp01(Mathf.Max(power, num)); VibeController vibes = Plugin.Vibes; float? power2 = value; vibes.Trigger(VibeEvent.BallHit, new VibeTriggerContext(null, null, power2)); } } [HarmonyPatch("OnLocalPlayerWillApplySwingProjectileHitPhysics")] [HarmonyPostfix] private static void OnLocalPlayerWillApplySwingProjectileHitPhysics_Postfix(PlayerMovement __instance, Vector3 incomingVelocityChange) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Vector3 val = incomingVelocityChange; float value = Mathf.Clamp01(((Vector3)(ref val)).magnitude / 20f); VibeController vibes = Plugin.Vibes; float? power = value; vibes.Trigger(VibeEvent.SwingProjectileHit, new VibeTriggerContext(null, null, power)); } } [HarmonyPatch("OnLocalPlayerWillApplyDiveHitPhysics")] [HarmonyPostfix] private static void OnLocalPlayerWillApplyDiveHitPhysics_Postfix(PlayerMovement __instance, Vector3 appliedVelocity) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Vector3 val = appliedVelocity; float value = Mathf.Clamp01(((Vector3)(ref val)).magnitude / 15f); VibeController vibes = Plugin.Vibes; float? power = value; vibes.Trigger(VibeEvent.DiveHit, new VibeTriggerContext(null, null, power)); } } [HarmonyPatch("TryKnockOut")] [HarmonyPostfix] private static void TryKnockOut_Postfix(PlayerMovement __instance, KnockoutType knockoutType, ref bool isNewKnockout, float distance) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (isNewKnockout && !((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { if (OrbitalLaserVibeController.IsOrbitalKnockoutType(knockoutType)) { Plugin.Log.LogInfo((object)$"Local player knocked out by {knockoutType}."); Plugin.OrbitalLaserVibes?.OnOrbitalLaserKnockout(knockoutType); } VibeController vibes = Plugin.Vibes; float? distance2 = distance; vibes.Trigger(VibeEvent.Knockout, new VibeTriggerContext(null, distance2)); } } [HarmonyPatch("RpcInformKnockedOutOtherPlayer")] [HarmonyPostfix] private static void RpcInformKnockedOutOtherPlayer_Postfix(PlayerMovement __instance) { if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Plugin.Vibes.Trigger(VibeEvent.EnemyKill); } } [HarmonyPatch("UserCode_RpcInformKnockedOutOtherPlayer__PlayerInfo__Boolean")] [HarmonyPostfix] private static void UserCode_RpcInformKnockedOutOtherPlayer_Postfix(PlayerMovement __instance) { if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Plugin.Vibes.Trigger(VibeEvent.EnemyKill); } } [HarmonyPatch("InformReactedToAirhorn")] [HarmonyPostfix] private static void InformReactedToAirhorn_Postfix(PlayerMovement __instance) { if (!((Object)(object)__instance == (Object)null) && ((NetworkBehaviour)__instance).isLocalPlayer) { Plugin.Log.LogInfo((object)"Horn blast detected (InformReactedToAirhorn)."); Plugin.Vibes.Trigger(VibeEvent.HornBlast); } } } } namespace VibingBattleGolf.Lovense { internal sealed class LovenseClient : IDisposable { private static readonly string[] DefaultEndpoints = new string[5] { "http://127.0.0.1:20010", "https://127-0-0-1.lovense.club:30010", "http://127.0.0.1:30010", "http://127.0.0.1:34567", "https://127-0-0-1.lovense.club:34568" }; private readonly ManualLogSource _log; private readonly string _configuredBaseUrl; private readonly bool _autoDiscover; private readonly LovenseRequestQueue _queue; private VibeActivationTimer _timer; private string _activeBaseUrl; private bool _discoveryInProgress; public string PlatformName { get; } public LovenseClient(ManualLogSource log, string baseUrl, string platformName, bool autoDiscover, LovenseRequestQueue queue) { _log = log; _configuredBaseUrl = (string.IsNullOrWhiteSpace(baseUrl) ? null : baseUrl.Trim().TrimEnd('/')); PlatformName = platformName; _autoDiscover = autoDiscover; _queue = queue; _activeBaseUrl = ((_autoDiscover || string.IsNullOrWhiteSpace(_configuredBaseUrl)) ? null : _configuredBaseUrl); } public void SetTimer(VibeActivationTimer timer) { _timer = timer; } public void Send(VibeRequest request) { if (string.IsNullOrWhiteSpace(_activeBaseUrl)) { DiscoverAsync().ContinueWith(delegate(Task<bool> discovered) { if (discovered.Result) { Dispatch(request); } }); } else { Dispatch(request); } } public void SendStop() { Dispatch(VibeRequest.Stop); } private void Dispatch(VibeRequest request) { QueueCommand(request.ToJson()); if (!request.IsStop && !((Object)(object)_timer == (Object)null)) { _timer.ScheduleStop(request.DurationSeconds); } } public Task<bool> DiscoverAsync() { TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); if (_discoveryInProgress) { if (!string.IsNullOrWhiteSpace(_activeBaseUrl)) { taskCompletionSource.SetResult(result: true); return taskCompletionSource.Task; } taskCompletionSource.SetResult(result: false); return taskCompletionSource.Task; } _discoveryInProgress = true; TryNextEndpoint(GetCandidateEndpoints().GetEnumerator(), taskCompletionSource); return taskCompletionSource.Task; } private void TryNextEndpoint(IEnumerator<string> endpoints, TaskCompletionSource<bool> tcs) { if (!endpoints.MoveNext()) { _discoveryInProgress = false; _log.LogWarning((object)"Lovense not reachable. Open Lovense Connect, pair your toy, keep it running, then press F9 to retry."); tcs.SetResult(result: false); return; } string endpoint = endpoints.Current; _queue.Enqueue(endpoint, "{\"command\":\"GetToys\"}", _log, delegate(LovenseResult result) { if (result.Success) { _activeBaseUrl = endpoint; _discoveryInProgress = false; _log.LogInfo((object)("Lovense connected via " + endpoint + ".")); if (!string.IsNullOrWhiteSpace(result.ToySummary)) { _log.LogInfo((object)("Lovense toys: " + result.ToySummary)); } tcs.TrySetResult(result: true); } else { _log.LogDebug((object)("Lovense probe failed for " + endpoint + ": " + result.Error)); TryNextEndpoint(endpoints, tcs); } }); } private void QueueCommand(string json) { if (string.IsNullOrWhiteSpace(_activeBaseUrl)) { _log.LogWarning((object)"Lovense command skipped because no endpoint is connected."); return; } _queue.Enqueue(_activeBaseUrl, json, _log, delegate(LovenseResult result) { if (!result.Success) { _log.LogWarning((object)("Lovense command failed on " + _activeBaseUrl + ": " + result.Error)); _activeBaseUrl = null; } }); } private IEnumerable<string> GetCandidateEndpoints() { HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrWhiteSpace(_configuredBaseUrl) && seen.Add(_configuredBaseUrl)) { yield return _configuredBaseUrl; } if (!_autoDiscover) { yield break; } string[] defaultEndpoints = DefaultEndpoints; foreach (string text in defaultEndpoints) { if (seen.Add(text)) { yield return text; } } } public void Dispose() { } } internal readonly struct VibeRequest { private const float LovenseMinimumTimedSeconds = 1f; public readonly string Action; public readonly float DurationSeconds; public readonly string PresetName; public bool IsStop => string.Equals(Action, "Stop", StringComparison.Ordinal); public static VibeRequest Stop => new VibeRequest("Stop", 0f); public VibeRequest(string action, float durationSeconds, string presetName = null) { Action = action; DurationSeconds = NormalizeDuration(durationSeconds); PresetName = presetName; } public string ToJson() { if (IsStop) { return "{\"command\":\"Function\",\"action\":\"Stop\",\"timeSec\":0,\"apiVer\":1}"; } string text = FormatApiDuration(DurationSeconds); if (!string.IsNullOrEmpty(PresetName)) { return "{\"command\":\"Preset\",\"name\":\"" + PresetName + "\",\"timeSec\":" + text + ",\"apiVer\":1}"; } return "{\"command\":\"Function\",\"action\":\"" + Action + "\",\"timeSec\":" + text + ",\"stopPrevious\":1,\"apiVer\":1}"; } public static float NormalizeDuration(float seconds) { if (seconds <= 0f) { return 1f; } return seconds; } private static string FormatApiDuration(float seconds) { return Mathf.Max(1f, seconds).ToString("0.###", CultureInfo.InvariantCulture); } } internal readonly struct LovenseResult { public readonly bool Success; public readonly string Error; public readonly string ToySummary; private LovenseResult(bool success, string error, string toySummary) { Success = success; Error = error; ToySummary = toySummary; } public static LovenseResult Fail(string error) { return new LovenseResult(success: false, error, null); } public static LovenseResult Ok(string toySummary = null) { return new LovenseResult(success: true, null, toySummary); } } internal static class LovenseResponseParser { public static LovenseResult Parse(string body) { if (string.IsNullOrWhiteSpace(body)) { return LovenseResult.Fail("Empty response body."); } int? num = ExtractIntField(body, "code"); if (num == 502) { return LovenseResult.Fail("Game Mode is off. Enable it in Lovense Connect."); } if (num.HasValue && num.Value != 200) { return