using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Net.Codecrete.QrCodeGenerator; using Newtonsoft.Json; using UnityEngine; using WebSocketSharp; using WebSocketSharp.Server; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] namespace DGLabPunish; internal enum ContinuousStimState { Idle, Walk, Run, Slide, Punish } internal sealed class StimOverlay { public char Channel; public float StartTime; public int DurationMs; public int Strength; public int Frequency; public StimEnvelopeShape Shape; public double Phase; public int Priority; public int Generation; } internal sealed class ContinuousStimEngine { private struct Sample { public int Strength; public int Frequency; public int BaseStrength; public int OverlayStrength; } private readonly DGLabSocketServer _server; private readonly StimDiagnostics _diagnostics; private readonly List _overlays; private float _lastSendTime; private float _lastActiveTime; private float _lastQueueUpdateTime; private float _lastStrengthTimeA; private float _lastStrengthTimeB; private bool _moving; private bool _sprinting; private bool _sliding; private float _speed; private bool _forceSend; private int _generation; private int _estimatedQueueMs; private int _lastStrengthA; private int _lastStrengthB; internal ContinuousStimEngine(DGLabSocketServer server, StimDiagnostics diagnostics) { _server = server; _diagnostics = diagnostics; _overlays = new List(); _lastSendTime = -999f; _lastActiveTime = -999f; _lastQueueUpdateTime = -999f; _lastStrengthTimeA = -999f; _lastStrengthTimeB = -999f; _forceSend = false; _generation = 0; _estimatedQueueMs = 0; _lastStrengthA = -1; _lastStrengthB = -1; } internal void SetMovement(bool moving, bool sprinting, bool sliding, float speed) { if (moving != _moving || sprinting != _sprinting || sliding != _sliding) { _forceSend = true; } _moving = moving; _sprinting = sprinting; _sliding = sliding; _speed = speed; if (moving || sliding) { _lastActiveTime = Time.realtimeSinceStartup; } } internal void AddOverlay(char channel, int strength, int durationMs, int frequency, StimEnvelopeShape shape, double phase, int priority) { if (priority >= 80) { Interrupt(priority); } AddOverlayInternal(channel, strength, durationMs, frequency, shape, phase, priority); } private void AddOverlayInternal(char channel, int strength, int durationMs, int frequency, StimEnvelopeShape shape, double phase, int priority) { _overlays.Add(new StimOverlay { Channel = channel, StartTime = Time.realtimeSinceStartup, DurationMs = Mathf.Max(25, durationMs), Strength = Mathf.Max(0, strength), Frequency = Mathf.Clamp(frequency, 10, 1000), Shape = shape, Phase = phase, Priority = priority, Generation = _generation }); _forceSend = true; _lastSendTime = -999f; } internal void AddOverlayBoth(int strengthA, int strengthB, int durationMs, int frequency, StimEnvelopeShape shape, int priority) { AddOverlayPair(strengthA, strengthB, durationMs, frequency, shape, 0.0, 0.18, priority); } internal void AddOverlayPair(int strengthA, int strengthB, int durationMs, int frequency, StimEnvelopeShape shape, double phaseA, double phaseB, int priority) { if (priority >= 80) { Interrupt(priority); } AddOverlayInternal('A', strengthA, durationMs, frequency, shape, phaseA, priority); AddOverlayInternal('B', strengthB, durationMs, frequency, shape, phaseB, priority); } internal void Clear() { _overlays.Clear(); _lastSendTime = -999f; _lastActiveTime = -999f; _lastQueueUpdateTime = -999f; _forceSend = false; _generation++; _estimatedQueueMs = 0; _diagnostics.EstimatedQueueMs = 0; _diagnostics.ContinuousGeneration = _generation; } internal void Tick() { if (_server == null || !_server.IsBound || !ModConfig.Armed.Value || !ModConfig.ContinuousMode.Value) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; UpdateEstimatedQueue(realtimeSinceStartup); PruneOverlays(realtimeSinceStartup); int num = Mathf.Clamp(ModConfig.ContinuousSendIntervalMs.Value, 50, 1000); if (!_forceSend && (realtimeSinceStartup - _lastSendTime) * 1000f < (float)num) { return; } ContinuousStimState continuousStimState = CurrentState(realtimeSinceStartup); _diagnostics.ContinuousState = continuousStimState.ToString(); _diagnostics.ActiveOverlays = _overlays.Count; _diagnostics.EstimatedQueueMs = _estimatedQueueMs; _diagnostics.ContinuousGeneration = _generation; if (continuousStimState == ContinuousStimState.Idle && _overlays.Count == 0) { _diagnostics.ContinuousBaseA = 0; _diagnostics.ContinuousBaseB = 0; _diagnostics.ContinuousOverlayA = 0; _diagnostics.ContinuousOverlayB = 0; return; } int num2 = Mathf.Clamp(ModConfig.ContinuousLookaheadMs.Value, 100, 1500); int num3 = Mathf.Clamp(num2 / 2, 25, 80); if (!_forceSend && _estimatedQueueMs > num3) { _diagnostics.LastCommandResult = "队列水位足够,暂不补包"; return; } List list = new List(); List list2 = new List(); List list3 = new List(); List list4 = new List(); int num4 = Mathf.Max(1, (int)Math.Ceiling((double)num2 / 25.0)); int num5 = 0; int num6 = 0; int num7 = 0; int num8 = 0; int num9 = 0; int num10 = 0; for (int i = 0; i < num4; i++) { float sampleTime = realtimeSinceStartup + (float)i * 0.025f; Sample sample = SampleChannel('A', continuousStimState, sampleTime); Sample sample2 = SampleChannel('B', continuousStimState, sampleTime); list.Add(sample.Frequency); list2.Add(sample2.Frequency); list3.Add(sample.Strength); list4.Add(sample2.Strength); num5 = Mathf.Max(num5, sample.Strength); num6 = Mathf.Max(num6, sample2.Strength); num7 = Mathf.Max(num7, sample.BaseStrength); num8 = Mathf.Max(num8, sample2.BaseStrength); num9 = Mathf.Max(num9, sample.OverlayStrength); num10 = Mathf.Max(num10, sample2.OverlayStrength); } PrepareStrength('A', num5); PrepareStrength('B', num6); List list5 = WaveformEncoder.Pulse(list, list3); List list6 = WaveformEncoder.Pulse(list2, list4); bool flag = _server.SendPulse('A', list5); bool flag2 = _server.SendPulse('B', list6); _lastSendTime = realtimeSinceStartup; _forceSend = false; if (flag || flag2) { int num11 = Mathf.Max(flag ? list5.Count : 0, flag2 ? list6.Count : 0); _estimatedQueueMs = Mathf.Clamp(_estimatedQueueMs + num11 * 100, 0, 1000); } _diagnostics.ContinuousBaseA = num7; _diagnostics.ContinuousBaseB = num8; _diagnostics.ContinuousOverlayA = num9; _diagnostics.ContinuousOverlayB = num10; _diagnostics.ContinuousRefillCount++; _diagnostics.EstimatedQueueMs = _estimatedQueueMs; _diagnostics.ContinuousGeneration = _generation; _diagnostics.LastCommandResult = ((flag && flag2) ? ("连续补包已发送,队列约 " + _estimatedQueueMs + "ms") : "连续补包发送失败"); } private ContinuousStimState CurrentState(float now) { if (_sliding) { return ContinuousStimState.Slide; } if (_sprinting && _moving) { return ContinuousStimState.Run; } if (_moving) { return ContinuousStimState.Walk; } int num = Mathf.Max(0, ModConfig.ContinuousFadeOutMs.Value); if (num > 0 && (now - _lastActiveTime) * 1000f < (float)num) { return ContinuousStimState.Walk; } if (_overlays.Count <= 0) { return ContinuousStimState.Idle; } return ContinuousStimState.Punish; } private Sample SampleChannel(char channel, ContinuousStimState state, float sampleTime) { int frequency = 180; int num = BaseStrength(state, sampleTime, out frequency); int num2 = 0; int num3 = frequency; for (int i = 0; i < _overlays.Count; i++) { StimOverlay stimOverlay = _overlays[i]; if (stimOverlay.Channel != channel || stimOverlay.Generation != _generation) { continue; } double num4 = (double)(sampleTime - stimOverlay.StartTime) * 1000.0 / (double)stimOverlay.DurationMs; if (!(num4 < 0.0) && !(num4 > 1.0)) { int num5 = (int)Math.Round((double)stimOverlay.Strength * WaveformEncoder.EvaluateEnvelope(stimOverlay.Shape, num4, stimOverlay.Phase)); if (num5 > num2) { num2 = num5; num3 = stimOverlay.Frequency; } } } int strength = Mathf.Clamp(num + num2, 0, ModConfig.MaxWaveIntensity.Value); if (num2 > 0) { frequency = num3; } Sample result = default(Sample); result.Strength = strength; result.Frequency = Mathf.Clamp(frequency, 10, 1000); result.BaseStrength = num; result.OverlayStrength = num2; return result; } private int BaseStrength(ContinuousStimState state, float sampleTime, out int frequency) { frequency = 180; int num; StimEnvelopeShape shape; switch (state) { case ContinuousStimState.Idle: case ContinuousStimState.Punish: return 0; case ContinuousStimState.Run: num = ModConfig.RunBaseIntensity.Value; frequency = ModConfig.RunBaseFrequency.Value; shape = ModConfig.RunBaseShape.Value; break; case ContinuousStimState.Slide: num = ModConfig.SlideBaseIntensity.Value; frequency = ModConfig.SlideBaseFrequency.Value; shape = StimEnvelopeShape.Tremor; break; default: num = ModConfig.WalkBaseIntensity.Value; frequency = ModConfig.WalkBaseFrequency.Value; shape = ModConfig.WalkBaseShape.Value; break; } int num2 = Mathf.Max(1, ModConfig.ContinuousFadeOutMs.Value); if (!_moving && !_sliding) { float num3 = (sampleTime - _lastActiveTime) * 1000f; if (num3 >= (float)num2) { return 0; } num = (int)Math.Round((double)num * (1.0 - (double)(num3 / (float)num2))); } double t = (double)sampleTime * 1000.0 % 900.0 / 900.0; double num4 = WaveformEncoder.EvaluateEnvelope(shape, t, 0.0); double num5 = 0.55; return Mathf.Clamp((int)Math.Round((double)num * (num5 + (1.0 - num5) * num4)), 0, ModConfig.MaxWaveIntensity.Value); } private void PruneOverlays(float now) { for (int num = _overlays.Count - 1; num >= 0; num--) { StimOverlay stimOverlay = _overlays[num]; if (stimOverlay.Generation != _generation || (now - stimOverlay.StartTime) * 1000f > (float)(stimOverlay.DurationMs + 100)) { _overlays.RemoveAt(num); } } } private void Interrupt(int priority) { _generation++; for (int num = _overlays.Count - 1; num >= 0; num--) { if (_overlays[num].Priority <= priority) { _overlays.RemoveAt(num); } } _server.ClearAll(); _estimatedQueueMs = 0; _lastQueueUpdateTime = Time.realtimeSinceStartup; _lastSendTime = -999f; _forceSend = true; _diagnostics.EstimatedQueueMs = 0; _diagnostics.ContinuousGeneration = _generation; } private void UpdateEstimatedQueue(float now) { if (_lastQueueUpdateTime < -100f) { _lastQueueUpdateTime = now; return; } int num = Mathf.Max(0, (int)Math.Round((now - _lastQueueUpdateTime) * 1000f)); if (num > 0) { _estimatedQueueMs = Mathf.Max(0, _estimatedQueueMs - num); _lastQueueUpdateTime = now; } } private void PrepareStrength(char channel, int eventStrength) { AutoStrengthMode autoStrengthMode = ModConfig.AutoStrengthMode.Value; if (ModConfig.AllowStrengthControl.Value) { autoStrengthMode = AutoStrengthMode.EventScaled; } if (autoStrengthMode == AutoStrengthMode.Off || eventStrength <= 0) { return; } StrengthState strength = _server.Strength; int channel2 = ((channel == 'A') ? 1 : 2); int num = ((channel == 'A') ? strength.ACurrent : strength.BCurrent); int num2 = ((channel == 'A') ? strength.AMax : strength.BMax); int num3 = Mathf.Clamp(ModConfig.MinimumChannelStrength.Value, 0, 200); int num4 = ((autoStrengthMode == AutoStrengthMode.MinimumOnly) ? Mathf.Max(num, num3) : Mathf.Max(num3, eventStrength)); if (num2 > 0) { num4 = Mathf.Min(num4, num2); } num4 = Mathf.Clamp(num4, 0, 200); float realtimeSinceStartup = Time.realtimeSinceStartup; int num5 = ((channel == 'A') ? _lastStrengthA : _lastStrengthB); float num6 = ((channel == 'A') ? _lastStrengthTimeA : _lastStrengthTimeB); if (num5 < 0 || Mathf.Abs(num4 - num5) >= 2 || !(realtimeSinceStartup - num6 < 0.35f)) { _server.SetStrength(channel2, num4); if (channel == 'A') { _lastStrengthA = num4; _lastStrengthTimeA = realtimeSinceStartup; } else { _lastStrengthB = num4; _lastStrengthTimeB = realtimeSinceStartup; } } } } internal sealed class ControlPanel { private readonly DGLabSocketServer _server; private readonly StimController _stim; private Rect _window; private Vector2 _scroll; private Texture2D _qrTexture; private string _qrText; private bool _visible; private bool _cursorSaved; private bool _previousCursorVisible; private CursorLockMode _previousLockState; private bool _draftsInitialized; private string _hostDraft; private string _portDraft; private string _publishUriDraft; private string _remoteUriDraft; private string _remoteCodeDraft; private string _panelKeyDraft; private string _stopKeyDraft; private string _settingsMessage; private int _panelTab; private int _selectedProfileIndex; private int _selectedWaveIndex; private string _manualStrengthA; private string _manualStrengthB; internal bool Visible => _visible; internal ControlPanel(DGLabSocketServer server, StimController stim) { //IL_0029: 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_0034: 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) _server = server; _stim = stim; _window = new Rect(40f, 40f, 560f, 760f); _scroll = Vector2.zero; _visible = false; _settingsMessage = ""; _panelTab = 0; _selectedProfileIndex = 0; _selectedWaveIndex = 0; _manualStrengthA = "20"; _manualStrengthB = "20"; } internal void ToggleVisible() { SetVisible(!_visible); } internal void SetVisible(bool visible) { //IL_002b: 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_0056: Unknown result type (might be due to invalid IL or missing references) if (_visible != visible) { _visible = visible; if (_visible) { InitDrafts(); _previousCursorVisible = Cursor.visible; _previousLockState = Cursor.lockState; _cursorSaved = true; UnlockCursor(); } else if (_cursorSaved) { Cursor.visible = _previousCursorVisible; Cursor.lockState = _previousLockState; _cursorSaved = false; } } } internal void UpdateCursor() { if (_visible) { UnlockCursor(); } } private static void UnlockCursor() { Cursor.visible = true; Cursor.lockState = (CursorLockMode)0; } internal void Draw() { //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Expected O, but got Unknown //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) if (_visible) { float num = Mathf.Max(420f, (float)Screen.width - 20f); float num2 = Mathf.Max(360f, (float)Screen.height * 0.85f); ((Rect)(ref _window)).width = Mathf.Min(((Rect)(ref _window)).width, num); ((Rect)(ref _window)).height = Mathf.Min(((Rect)(ref _window)).height, num2); if (((Rect)(ref _window)).xMax > (float)Screen.width) { ((Rect)(ref _window)).x = Mathf.Max(0f, (float)Screen.width - ((Rect)(ref _window)).width - 10f); } if (((Rect)(ref _window)).yMax > (float)Screen.height) { ((Rect)(ref _window)).y = Mathf.Max(0f, (float)Screen.height - ((Rect)(ref _window)).height - 10f); } _window = GUI.Window(88991, _window, new WindowFunction(DrawWindow), "郊狼 3.0 连接面板"); } } private void DrawWindow(int id) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) InitDrafts(); _scroll = GUILayout.BeginScrollView(_scroll, false, true, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(Mathf.Max(260f, ((Rect)(ref _window)).height - 34f)) }); DrawTabs(); if (_panelTab == 0) { DrawClientPage(); FinishWindow(); return; } if (_panelTab == 1) { DrawWaveLibraryPage(); FinishWindow(); return; } if (_panelTab == 2) { DrawEventMappingPage(); FinishWindow(); return; } if (_panelTab == 3) { DrawImportExportPage(); FinishWindow(); return; } Section("连接"); GUILayout.Label("协议:DG-LAB 郊狼 3.0 App Socket", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("模式:" + _server.ConnectionModeText(), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("状态:" + _server.StatusText(), (GUILayoutOption[])(object)new GUILayoutOption[0]); if (!string.IsNullOrEmpty(_server.LastRemoteEndpoint)) { GUILayout.Label("连接来源/远程地址:" + _server.LastRemoteEndpoint, (GUILayoutOption[])(object)new GUILayoutOption[0]); } EnumCycle("连接模式", ModConfig.ConnectionMode); GUILayout.Label("二维码地址(用于核对):", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.TextArea(_server.GetQrUrl(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(48f) }); EnsureQrTexture(_server.GetQrUrl()); if ((Object)(object)_qrTexture != (Object)null) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.FlexibleSpace(); GUILayout.Box((Texture)(object)_qrTexture, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(180f), GUILayout.Height(180f) }); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } GUILayout.Label("检测到的电脑 IPv4:" + _server.LocalAddressSummary(), (GUILayoutOption[])(object)new GUILayoutOption[0]); string[] localIPv4Addresses = _server.GetLocalIPv4Addresses(); if (localIPv4Addresses.Length > 0) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); for (int i = 0; i < localIPv4Addresses.Length && i < 3; i++) { if (GUILayout.Button("使用 " + localIPv4Addresses[i], (GUILayoutOption[])(object)new GUILayoutOption[0])) { _hostDraft = localIPv4Addresses[i]; } } GUILayout.EndHorizontal(); } TextRow("局域网 IP/域名", ref _hostDraft); TextRow("本地端口", ref _portDraft); TextRow("公网 Socket URI", ref _publishUriDraft); TextRow("远程 Socket URI", ref _remoteUriDraft); TextRow("自建 Relay Code", ref _remoteCodeDraft); GUILayout.Label("说明:Relay Code 不是 DG-LAB App 原生远程口令,需要你自己的兼容 relay 服务。优先用公网/远程 Socket。", (GUILayoutOption[])(object)new GUILayoutOption[0]); TextRow("面板键", ref _panelKeyDraft); TextRow("急停键", ref _stopKeyDraft); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("保存连接/键位并重启", (GUILayoutOption[])(object)new GUILayoutOption[0])) { SavePanelSettings(restartServer: true); } if (GUILayout.Button(_server.IsRunning ? "重启服务" : "启动服务", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.Stop(); _server.Start(); RefreshQr(); } GUILayout.EndHorizontal(); if (GUILayout.Button("停止服务", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.Stop(); } if (!string.IsNullOrEmpty(_settingsMessage)) { GUILayout.Label(_settingsMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } Section("状态与测试"); GUILayout.Label("触发总开关:" + (ModConfig.Armed.Value ? "已启用" : "未启用") + " 绑定后自动启用:" + (ModConfig.AutoArmOnBind.Value ? "开启" : "关闭"), (GUILayoutOption[])(object)new GUILayoutOption[0]); if (!ModConfig.Armed.Value) { GUILayout.Label("注意:真实游戏事件需要启用触发;下方模拟按钮会绕过此开关,只用于测试波形。", (GUILayoutOption[])(object)new GUILayoutOption[0]); } GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button(ModConfig.Armed.Value ? "解除触发" : "启用触发", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ModConfig.Armed.Value = !ModConfig.Armed.Value; SaveConfig(); } if (GUILayout.Button("紧急停止", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.EmergencyStop(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("测试 A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SendTest('A'); } if (GUILayout.Button("测试 B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SendTest('B'); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟左脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.TriggerFootstepForced(left: true); } if (GUILayout.Button("模拟右脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.TriggerFootstepForced(left: false); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟奔跑左脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateRunFootstep(left: true); } if (GUILayout.Button("模拟奔跑右脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateRunFootstep(left: false); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟持续走路 2秒", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateContinuousMovement(run: false); } if (GUILayout.Button("模拟持续奔跑 2秒", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateContinuousMovement(run: true); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟跳跃", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateJump(); } if (GUILayout.Button("模拟落地", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateLand(); } if (GUILayout.Button("模拟滑行", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateSlide(); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟包络受击", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateHurt(10); } if (GUILayout.Button("模拟死亡", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateDeath(); } GUILayout.EndHorizontal(); StrengthState strength = _server.Strength; GUILayout.Label("A 强度:" + strength.ACurrent + " / " + strength.AMax + " B 强度:" + strength.BCurrent + " / " + strength.BMax, (GUILayoutOption[])(object)new GUILayoutOption[0]); if ((strength.ACurrent == 0 || strength.BCurrent == 0) && ModConfig.AutoStrengthMode.Value == AutoStrengthMode.Off && !ModConfig.AllowStrengthControl.Value) { GUILayout.Label("提示:有通道当前强度为 0,且自动调强关闭;该通道可能没有体感。", (GUILayoutOption[])(object)new GUILayoutOption[0]); } StimDiagnostics diagnostics = _stim.Diagnostics; GUILayout.Label("连续:" + diagnostics.ContinuousState + " 补包:" + diagnostics.ContinuousRefillCount + " 队列估算:" + diagnostics.EstimatedQueueMs + "ms Gen:" + diagnostics.ContinuousGeneration, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("Overlay:" + diagnostics.ActiveOverlays, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("连续强度:A 基础 " + diagnostics.ContinuousBaseA + " + " + diagnostics.ContinuousOverlayA + " / B 基础 " + diagnostics.ContinuousBaseB + " + " + diagnostics.ContinuousOverlayB, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("步态:" + diagnostics.MovementState + " 来源:" + diagnostics.LastFootstepSource + " 间隔:" + diagnostics.LastFootstepIntervalMs + "ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("计数:左脚 " + diagnostics.LeftFootsteps + " / 右脚 " + diagnostics.RightFootsteps + " / 动画脚步 " + diagnostics.AnimationFootsteps + " / 兜底 " + diagnostics.FallbackFootsteps, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("去重:" + diagnostics.DuplicateFootsteps + " / 奔跑补拍 " + diagnostics.RunSupplementFootsteps + " / 左右间隔 " + diagnostics.LastLeftRightDeltaMs + "ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("动作:跳跃 " + diagnostics.JumpCount + " / 落地 " + diagnostics.LandCount + " / 滑行 " + diagnostics.SlideCount + " / 敌脚 " + diagnostics.EnemyFootsteps, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("事件:受击 " + diagnostics.HurtCount + " / 死亡 " + diagnostics.DeathCount, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("最后事件:" + diagnostics.LastEvent + " 通道:" + diagnostics.LastChannel + " 结果:" + diagnostics.LastCommandResult, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("最后拦截:" + diagnostics.LastBlockedReason, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("下一步:" + (diagnostics.NextFootLeft ? "左脚" : "右脚"), (GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("重置下一步为配置的第一脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.ResetFootToConfiguredFirst(); } Section("事件开关"); Toggle("本地玩家受击触发", ModConfig.HurtEnabled); Toggle("本地玩家死亡触发", ModConfig.DeathEnabled); Toggle("本地玩家脚步触发", ModConfig.LocalFootstepEnabled); Toggle("跳跃触发", ModConfig.JumpEnabled); Toggle("落地触发", ModConfig.LandEnabled); Toggle("滑行触发", ModConfig.SlideEnabled); Toggle("跳跃/落地/滑行总开关", ModConfig.LandJumpEnabled); Toggle("猎人脚步近距离提示", ModConfig.EnemyFootstepEnabled); Section("安全与自动强度"); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("舒适持续", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ApplyComfortPreset(); } if (GUILayout.Button("标准游戏", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ApplyPreset(20, 24, 30, 45, 60, 30, 65); } if (GUILayout.Button("强惩罚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ApplyPreset(28, 36, 42, 65, 85, 35, 90); } if (GUILayout.Button("调试同步", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ApplyPreset(36, 45, 55, 70, 90, 35, 90); } GUILayout.EndHorizontal(); Toggle("连续模式", ModConfig.ContinuousMode); IntSlider("补包间隔 ms", ModConfig.ContinuousSendIntervalMs, 50, 1000); IntSlider("Lookahead ms", ModConfig.ContinuousLookaheadMs, 100, 1500); IntSlider("停止淡出 ms", ModConfig.ContinuousFadeOutMs, 0, 1500); Toggle("绑定后自动启用触发", ModConfig.AutoArmOnBind); EnumCycle("自动调强模式", ModConfig.AutoStrengthMode); Toggle("兼容旧自动调强开关", ModConfig.AllowStrengthControl); IntSlider("最低可感强度", ModConfig.MinimumChannelStrength, 0, 80); IntSlider("总强度上限", ModConfig.MaxWaveIntensity, 1, 100); IntSlider("测试强度", ModConfig.TestIntensity, 1, 100); IntSlider("最长持续 ms", ModConfig.MaxEventDurationMs, 100, 6000); IntSlider("Clear 延迟 ms", ModConfig.ClearDelayMs, 0, 1000); Section("脚步"); Toggle("第一步为左脚", ModConfig.FirstFootLeft); EnumCycle("左脚通道", ModConfig.LeftFootChannel); EnumCycle("右脚通道", ModConfig.RightFootChannel); Toggle("站停后重置左右脚", ModConfig.ResetFootOnIdle); IntSlider("站停重置 ms", ModConfig.FootstepIdleResetMs, 100, 3000); IntSlider("脚步强度", ModConfig.FootstepIntensity, 1, 50); IntSlider("脚步持续 ms", ModConfig.FootstepDurationMs, 25, 600); IntSlider("脚步频率", ModConfig.FootstepFrequency, 10, 1000); EnumCycle("脚步波形", ModConfig.FootstepWaveShape); IntSlider("走路基础强度", ModConfig.WalkBaseIntensity, 0, 50); IntSlider("走路基础频率", ModConfig.WalkBaseFrequency, 10, 1000); EnumCycle("走路基础波形", ModConfig.WalkBaseShape); IntSlider("走路脚步增量", ModConfig.WalkStepBumpIntensity, 0, 60); IntSlider("走路增量持续 ms", ModConfig.WalkStepBumpDurationMs, 25, 600); IntSlider("动画去重 ms", ModConfig.FootstepDuplicateWindowMs, 0, 200); IntSlider("走路最小间隔 ms", ModConfig.WalkFootstepMinIntervalMs, 20, 500); IntSlider("奔跑基础强度", ModConfig.RunBaseIntensity, 0, 60); IntSlider("奔跑基础频率", ModConfig.RunBaseFrequency, 10, 1000); EnumCycle("奔跑基础波形", ModConfig.RunBaseShape); IntSlider("奔跑脚步强度", ModConfig.RunFootstepIntensity, 1, 80); IntSlider("奔跑持续 ms", ModConfig.RunFootstepDurationMs, 25, 600); IntSlider("奔跑脚步增量", ModConfig.RunStepBumpIntensity, 0, 80); IntSlider("奔跑增量持续 ms", ModConfig.RunStepBumpDurationMs, 25, 600); IntSlider("奔跑最小间隔 ms", ModConfig.RunFootstepMinIntervalMs, 20, 500); Toggle("兜底脚步补发", ModConfig.FallbackFootstepSupplement); IntSlider("兜底等待 ms", ModConfig.FallbackFootstepNoAnimationMs, 100, 2000); Toggle("奔跑节奏补拍", ModConfig.RunCadenceSupplement); IntSlider("奔跑补拍间隔 ms", ModConfig.RunSupplementIntervalMs, 55, 500); Section("动作"); EnumCycle("跳跃通道", ModConfig.JumpChannelMode); IntSlider("跳跃强度", ModConfig.JumpIntensity, 1, 80); IntSlider("跳跃持续 ms", ModConfig.JumpDurationMs, 25, 1000); IntSlider("跳跃频率", ModConfig.JumpFrequency, 10, 1000); IntSlider("跳跃冷却 ms", ModConfig.JumpCooldownMs, 40, 2000); EnumCycle("落地通道", ModConfig.LandChannelMode); IntSlider("落地强度", ModConfig.LandIntensity, 1, 80); IntSlider("落地持续 ms", ModConfig.LandDurationMs, 25, 1000); IntSlider("落地频率", ModConfig.LandFrequency, 10, 1000); EnumCycle("落地包络", ModConfig.LandEnvelope); IntSlider("落地冷却 ms", ModConfig.LandCooldownMs, 40, 2000); EnumCycle("滑行通道", ModConfig.SlideChannelMode); IntSlider("滑行强度", ModConfig.SlideIntensity, 1, 80); IntSlider("滑行持续 ms", ModConfig.SlideDurationMs, 25, 1000); IntSlider("滑行频率", ModConfig.SlideFrequency, 10, 1000); EnumCycle("滑行包络", ModConfig.SlideEnvelope); IntSlider("滑行冷却 ms", ModConfig.SlideCooldownMs, 40, 2000); Section("受击"); EnumCycle("受击通道", ModConfig.HurtChannelMode); IntSlider("受击基础强度", ModConfig.HurtMinIntensity, 1, 100); IntSlider("受击最大强度", ModConfig.HurtMaxIntensity, 1, 100); FloatSlider("伤害倍率", ModConfig.HurtDamageMultiplier, 0f, 10f); FloatSlider("A 通道倍率", ModConfig.HurtAMultiplier, 0f, 2f); FloatSlider("B 通道倍率", ModConfig.HurtBMultiplier, 0f, 2f); IntSlider("受击最短 ms", ModConfig.HurtDurationMinMs, 25, 6000); IntSlider("受击最长 ms", ModConfig.HurtDurationMaxMs, 25, 6000); IntSlider("受击频率", ModConfig.HurtFrequency, 10, 1000); EnumCycle("受击包络", ModConfig.HurtEnvelope); IntSlider("受击冷却 ms", ModConfig.HurtCooldownMs, 100, 3000); Section("死亡"); EnumCycle("死亡通道", ModConfig.DeathChannelMode); EnumCycle("死亡 A 波形", ModConfig.DeathAWaveShape); EnumCycle("死亡 B 波形", ModConfig.DeathBWaveShape); EnumCycle("死亡包络", ModConfig.DeathEnvelope); Toggle("死亡前清空旧队列", ModConfig.DeathClearBeforePulse); IntSlider("死亡强度", ModConfig.DeathIntensity, 1, 100); IntSlider("死亡持续 ms", ModConfig.DeathDurationMs, 100, 6000); IntSlider("死亡频率", ModConfig.DeathFrequency, 10, 1000); IntSlider("死亡冷却 ms", ModConfig.DeathCooldownMs, 500, 10000); Section("说明"); GUILayout.Label("电脑不需要蓝牙。手机连接郊狼 3.0,Mod 只负责 Socket。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("公网/远程模式请使用可被手机访问的 ws:// 或 wss:// 地址。正式远程建议 wss。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("DG-LAB App 原生远程口令不是 Socket v2 API,本 Mod 不把它作为稳定主线接入。", (GUILayoutOption[])(object)new GUILayoutOption[0]); FinishWindow(); } private void FinishWindow() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) GUILayout.EndScrollView(); GUI.DragWindow(new Rect(0f, 0f, 10000f, 22f)); } private void DrawTabs() { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Toggle(_panelTab == 0, "客户端", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[0])) { _panelTab = 0; } if (GUILayout.Toggle(_panelTab == 1, "波形库", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[0])) { _panelTab = 1; } if (GUILayout.Toggle(_panelTab == 2, "事件映射", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[0])) { _panelTab = 2; } if (GUILayout.Toggle(_panelTab == 3, "导入/导出", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[0])) { _panelTab = 3; } if (GUILayout.Toggle(_panelTab == 4, "事件参数", GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[0])) { _panelTab = 4; } GUILayout.EndHorizontal(); } private void DrawClientPage() { Section("Socket 控制端"); GUILayout.Label("协议:DG-LAB 郊狼 3.0 App Socket v2。电脑不需要蓝牙,手机 App 负责蓝牙连接主机。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("模式:" + _server.ConnectionModeText(), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("状态:" + _server.StatusText(), (GUILayoutOption[])(object)new GUILayoutOption[0]); if (!string.IsNullOrEmpty(_server.LastRemoteEndpoint)) { GUILayout.Label("连接来源/远程地址:" + _server.LastRemoteEndpoint, (GUILayoutOption[])(object)new GUILayoutOption[0]); } if (!string.IsNullOrEmpty(_server.LastError)) { GUILayout.Label("最后错误:" + _server.LastError, (GUILayoutOption[])(object)new GUILayoutOption[0]); } GUILayout.Label("二维码地址:", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.TextArea(_server.GetQrUrl(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(48f) }); EnsureQrTexture(_server.GetQrUrl()); if ((Object)(object)_qrTexture != (Object)null) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.FlexibleSpace(); GUILayout.Box((Texture)(object)_qrTexture, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(180f), GUILayout.Height(180f) }); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); } GUILayout.Label("检测到的电脑 IPv4:" + _server.LocalAddressSummary(), (GUILayoutOption[])(object)new GUILayoutOption[0]); string[] localIPv4Addresses = _server.GetLocalIPv4Addresses(); if (localIPv4Addresses.Length > 0) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); for (int i = 0; i < localIPv4Addresses.Length && i < 3; i++) { if (GUILayout.Button("使用 " + localIPv4Addresses[i], (GUILayoutOption[])(object)new GUILayoutOption[0])) { _hostDraft = localIPv4Addresses[i]; } } GUILayout.EndHorizontal(); } EnumCycle("连接模式", ModConfig.ConnectionMode); TextRow("局域网 IP/域名", ref _hostDraft); TextRow("本地端口", ref _portDraft); TextRow("公网 Socket URI", ref _publishUriDraft); TextRow("远程 Socket URI", ref _remoteUriDraft); TextRow("自建 Relay Code", ref _remoteCodeDraft); GUILayout.Label("Relay Code 是本 Mod 自建 relay 口令,不是 DG-LAB App 原生远程口令。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("保存连接/键位并重启", (GUILayoutOption[])(object)new GUILayoutOption[0])) { SavePanelSettings(restartServer: true); } if (GUILayout.Button(_server.IsRunning ? "重启服务" : "启动服务", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.Stop(); _server.Start(); RefreshQr(); } if (GUILayout.Button("停止服务", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.Stop(); } GUILayout.EndHorizontal(); Section("强度控制"); StrengthState strength = _server.Strength; GUILayout.Label("A 当前/上限:" + strength.ACurrent + " / " + strength.AMax + " B 当前/上限:" + strength.BCurrent + " / " + strength.BMax, (GUILayoutOption[])(object)new GUILayoutOption[0]); TextRow("手动 A 强度", ref _manualStrengthA); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("设置 A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { SetManualStrength(1, _manualStrengthA); } if (GUILayout.Button("A -5", (GUILayoutOption[])(object)new GUILayoutOption[0])) { AdjustStrength(1, -5); } if (GUILayout.Button("A +5", (GUILayoutOption[])(object)new GUILayoutOption[0])) { AdjustStrength(1, 5); } if (GUILayout.Button("Clear A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.ClearChannel(1); } GUILayout.EndHorizontal(); TextRow("手动 B 强度", ref _manualStrengthB); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("设置 B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { SetManualStrength(2, _manualStrengthB); } if (GUILayout.Button("B -5", (GUILayoutOption[])(object)new GUILayoutOption[0])) { AdjustStrength(2, -5); } if (GUILayout.Button("B +5", (GUILayoutOption[])(object)new GUILayoutOption[0])) { AdjustStrength(2, 5); } if (GUILayout.Button("Clear B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.ClearChannel(2); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("启用触发", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ModConfig.Armed.Value = true; SaveConfig(); } if (GUILayout.Button("解除触发", (GUILayoutOption[])(object)new GUILayoutOption[0])) { ModConfig.Armed.Value = false; SaveConfig(); } if (GUILayout.Button("紧急停止", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.EmergencyStop(); } GUILayout.EndHorizontal(); Section("快速测试"); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("测试 A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SendTest('A'); } if (GUILayout.Button("测试 B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SendTest('B'); } GUILayout.EndHorizontal(); WavePreset wavePreset = SelectedWave(); GUILayout.Label("选中波形:" + ((wavePreset == null) ? "无" : StimProfileManager.DisplayName(wavePreset)), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("播放选中波形 A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.SendWave(_server, wavePreset, 'A'); } if (GUILayout.Button("播放选中波形 B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.SendWave(_server, wavePreset, 'B'); } if (GUILayout.Button("Clear A/B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _server.ClearAll(); } GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(_settingsMessage)) { GUILayout.Label(_settingsMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } if (!string.IsNullOrEmpty(StimProfileManager.LastMessage)) { GUILayout.Label(StimProfileManager.LastMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } } private void DrawWaveLibraryPage() { Section("波形包"); DrawProfileSelector(); StimProfile stimProfile = SelectedProfile(); if (stimProfile == null) { GUILayout.Label("没有可用波形包。", (GUILayoutOption[])(object)new GUILayoutOption[0]); return; } GUILayout.Label("当前波形包:" + StimProfileManager.DisplayName(stimProfile) + " 版本:" + stimProfile.version + " 上限:" + stimProfile.maxWaveIntensity, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("应用此波形包", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.ApplyProfile(stimProfile); _settingsMessage = StimProfileManager.LastMessage; } if (GUILayout.Button("刷新波形包", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.RefreshProfiles(); ClampSelections(); } GUILayout.EndHorizontal(); Section("波形列表"); if (stimProfile.waves == null || stimProfile.waves.Count == 0) { GUILayout.Label("该波形包没有波形。", (GUILayoutOption[])(object)new GUILayoutOption[0]); return; } for (int i = 0; i < stimProfile.waves.Count; i++) { WavePreset wave = stimProfile.waves[i]; GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Toggle(_selectedWaveIndex == i, StimProfileManager.DisplayName(wave), GUI.skin.button, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) })) { _selectedWaveIndex = i; } GUILayout.Label(WaveSummary(wave), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.EndHorizontal(); } WavePreset wavePreset = SelectedWave(); if (wavePreset != null) { Section("波形测试"); GUILayout.Label("名称:" + StimProfileManager.DisplayName(wavePreset), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("类型:" + wavePreset.type + " / " + wavePreset.shape + " 通道建议:" + wavePreset.channel, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("频率:" + wavePreset.frequency + " 强度:" + wavePreset.minStrength + "-" + wavePreset.maxStrength + " 时长:" + wavePreset.durationMs + "ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("测试 A", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.SendWave(_server, wavePreset, 'A'); } if (GUILayout.Button("测试 B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.SendWave(_server, wavePreset, 'B'); } if (GUILayout.Button("测试 A+B", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.SendWave(_server, wavePreset, 'A'); StimProfileManager.SendWave(_server, wavePreset, 'B'); } GUILayout.EndHorizontal(); } if (!string.IsNullOrEmpty(StimProfileManager.LastMessage)) { GUILayout.Label(StimProfileManager.LastMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } } private void DrawEventMappingPage() { Section("事件映射"); DrawProfileSelector(); StimProfile stimProfile = SelectedProfile(); if (stimProfile == null || stimProfile.events == null || stimProfile.events.Count == 0) { GUILayout.Label("当前波形包没有事件映射。", (GUILayoutOption[])(object)new GUILayoutOption[0]); return; } GUILayout.Label("这里展示波形包对事件的推荐映射;应用波形包会把持续状态参数写入配置。", (GUILayoutOption[])(object)new GUILayoutOption[0]); for (int i = 0; i < stimProfile.events.Count; i++) { EventRule eventRule = stimProfile.events[i]; GUILayout.Label(eventRule.eventName + " -> " + eventRule.waveName + " 通道:" + eventRule.channelMode + " 倍率:" + eventRule.strengthMultiplier.ToString("0.00"), (GUILayoutOption[])(object)new GUILayoutOption[0]); if (!string.IsNullOrEmpty(eventRule.note)) { GUILayout.Label(" " + eventRule.note, (GUILayoutOption[])(object)new GUILayoutOption[0]); } } Section("当前诊断"); StimDiagnostics diagnostics = _stim.Diagnostics; GUILayout.Label("连续状态:" + diagnostics.ContinuousState + " 补包:" + diagnostics.ContinuousRefillCount + " 队列估算:" + diagnostics.EstimatedQueueMs + "ms Gen:" + diagnostics.ContinuousGeneration, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("连续强度:A " + diagnostics.ContinuousBaseA + "+" + diagnostics.ContinuousOverlayA + " / B " + diagnostics.ContinuousBaseB + "+" + diagnostics.ContinuousOverlayB, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("脚步:左 " + diagnostics.LeftFootsteps + " / 右 " + diagnostics.RightFootsteps + " / 来源 " + diagnostics.LastFootstepSource + " / 下一步 " + (diagnostics.NextFootLeft ? "左" : "右"), (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("动作:跳跃 " + diagnostics.JumpCount + " / 落地 " + diagnostics.LandCount + " / 滑行 " + diagnostics.SlideCount + " / 受击 " + diagnostics.HurtCount + " / 死亡 " + diagnostics.DeathCount, (GUILayoutOption[])(object)new GUILayoutOption[0]); Section("模拟事件"); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟左脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.TriggerFootstepForced(left: true); } if (GUILayout.Button("模拟右脚", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.TriggerFootstepForced(left: false); } if (GUILayout.Button("模拟走路 2秒", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateContinuousMovement(run: false); } if (GUILayout.Button("模拟奔跑 2秒", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateContinuousMovement(run: true); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("模拟跳跃", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateJump(); } if (GUILayout.Button("模拟落地", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateLand(); } if (GUILayout.Button("模拟滑行", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateSlide(); } if (GUILayout.Button("模拟受击", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateHurt(10); } if (GUILayout.Button("模拟死亡", (GUILayoutOption[])(object)new GUILayoutOption[0])) { _stim.SimulateDeath(); } GUILayout.EndHorizontal(); } private void DrawImportExportPage() { Section("导入/导出"); GUILayout.Label("导入目录:", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.TextArea(StimProfileManager.ProfilesDirectory, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(40f) }); GUILayout.Label("支持 DGLabPunishProfile.json、原始 HEX JSON 数组和 .pulse 文本。第三方项目建议只导入数据,不复制代码。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("HEX 示例:type=hex,hex=[\"0A0A0A0A64646464\"];.pulse 可每行写 16 位 HEX 或 frequency strength。", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); if (GUILayout.Button("刷新导入目录", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.RefreshProfiles(); ClampSelections(); } if (GUILayout.Button("导出当前配置", (GUILayoutOption[])(object)new GUILayoutOption[0])) { try { StimProfileManager.ExportCurrentConfig(); } catch (Exception ex) { StimProfileManager.LastMessage = "导出失败:" + ex.Message; } } GUILayout.EndHorizontal(); DrawProfileSelector(); if (GUILayout.Button("应用选中的导入/内置波形包", (GUILayoutOption[])(object)new GUILayoutOption[0])) { StimProfileManager.ApplyProfile(SelectedProfile()); _settingsMessage = StimProfileManager.LastMessage; } if (!string.IsNullOrEmpty(_settingsMessage)) { GUILayout.Label(_settingsMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } if (!string.IsNullOrEmpty(StimProfileManager.LastMessage)) { GUILayout.Label(StimProfileManager.LastMessage, (GUILayoutOption[])(object)new GUILayoutOption[0]); } Section("当前核心配置"); GUILayout.Label("连续模式:" + ModConfig.ContinuousMode.Value + " 自动调强:" + ModConfig.AutoStrengthMode.Value, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("补包/Lookahead:" + ModConfig.ContinuousSendIntervalMs.Value + " / " + ModConfig.ContinuousLookaheadMs.Value + " ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("走路基础/增量:" + ModConfig.WalkBaseIntensity.Value + " / " + ModConfig.WalkStepBumpIntensity.Value, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("奔跑基础/增量:" + ModConfig.RunBaseIntensity.Value + " / " + ModConfig.RunStepBumpIntensity.Value, (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("受击:" + ModConfig.HurtMinIntensity.Value + "-" + ModConfig.HurtMaxIntensity.Value + " / " + ModConfig.HurtDurationMaxMs.Value + "ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("死亡:" + ModConfig.DeathIntensity.Value + " / " + ModConfig.DeathDurationMs.Value + "ms", (GUILayoutOption[])(object)new GUILayoutOption[0]); } private void DrawProfileSelector() { ClampSelections(); GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label("波形包", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); if (GUILayout.Button("<", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(32f) })) { _selectedProfileIndex--; ClampSelections(); } StimProfile stimProfile = SelectedProfile(); GUILayout.Label((stimProfile == null) ? "无" : StimProfileManager.DisplayName(stimProfile), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(220f) }); if (GUILayout.Button(">", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(32f) })) { _selectedProfileIndex++; ClampSelections(); } GUILayout.EndHorizontal(); } private StimProfile SelectedProfile() { ClampSelections(); if (StimProfileManager.Profiles.Count == 0) { return null; } return StimProfileManager.Profiles[_selectedProfileIndex]; } private WavePreset SelectedWave() { StimProfile stimProfile = SelectedProfile(); if (stimProfile == null || stimProfile.waves == null || stimProfile.waves.Count == 0) { return null; } ClampSelections(); return stimProfile.waves[_selectedWaveIndex]; } private void ClampSelections() { if (StimProfileManager.Profiles.Count == 0) { _selectedProfileIndex = 0; _selectedWaveIndex = 0; return; } if (_selectedProfileIndex < 0) { _selectedProfileIndex = StimProfileManager.Profiles.Count - 1; } if (_selectedProfileIndex >= StimProfileManager.Profiles.Count) { _selectedProfileIndex = 0; } StimProfile stimProfile = StimProfileManager.Profiles[_selectedProfileIndex]; int num = ((stimProfile != null && stimProfile.waves != null) ? stimProfile.waves.Count : 0); if (num == 0) { _selectedWaveIndex = 0; return; } if (_selectedWaveIndex < 0) { _selectedWaveIndex = num - 1; } if (_selectedWaveIndex >= num) { _selectedWaveIndex = 0; } } private string WaveSummary(WavePreset wave) { if (wave == null) { return ""; } if (wave.hex != null && wave.hex.Count > 0) { return "HEX " + wave.hex.Count + " 条 / 建议 " + wave.channel; } return wave.type + " " + wave.shape + " / " + wave.frequency + "Hz / " + wave.minStrength + "-" + wave.maxStrength + " / " + wave.durationMs + "ms"; } private void SetManualStrength(int channel, string value) { if (!int.TryParse(value, out var result)) { _settingsMessage = "强度必须是数字。"; return; } result = Mathf.Clamp(result, 0, 200); bool flag = _server.SetStrength(channel, result); _settingsMessage = (flag ? ("已设置 " + ((channel == 1) ? "A" : "B") + " 强度为 " + result) : "设置失败:App 尚未绑定或连接不可用。"); } private void AdjustStrength(int channel, int delta) { StrengthState strength = _server.Strength; int num = ((channel == 1) ? strength.ACurrent : strength.BCurrent); int num2 = Mathf.Clamp(num + delta, 0, 200); if (channel == 1) { _manualStrengthA = num2.ToString(); } else { _manualStrengthB = num2.ToString(); } bool flag = _server.SetStrength(channel, num2); _settingsMessage = (flag ? ("已调整 " + ((channel == 1) ? "A" : "B") + " 强度为 " + num2) : "调整失败:App 尚未绑定或连接不可用。"); } private void InitDrafts() { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) if (!_draftsInitialized) { _hostDraft = ModConfig.AdvertiseHost.Value; if (string.IsNullOrEmpty(_hostDraft)) { _hostDraft = _server.GetAdvertiseHost(); } _portDraft = ModConfig.Port.Value.ToString(); _publishUriDraft = ModConfig.PublishSocketUri.Value; _remoteUriDraft = ModConfig.RemoteServerUri.Value; _remoteCodeDraft = ModConfig.RemotePairCode.Value; _panelKeyDraft = ((object)ModConfig.PanelKey.Value).ToString(); _stopKeyDraft = ((object)ModConfig.EmergencyStopKey.Value).ToString(); _draftsInitialized = true; } } private void SavePanelSettings(bool restartServer) { //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) if (!int.TryParse(_portDraft, out var result) || result < 1 || result > 65535) { _settingsMessage = "端口无效,应为 1-65535。"; return; } if (!TryParseKey(_panelKeyDraft, out var keyCode)) { _settingsMessage = "面板键无效,例如 P、F8、Insert。"; return; } if (!TryParseKey(_stopKeyDraft, out var keyCode2)) { _settingsMessage = "急停键无效,例如 I、F9、BackQuote。"; return; } ModConfig.AdvertiseHost.Value = (_hostDraft ?? "").Trim(); ModConfig.Port.Value = result; ModConfig.PublishSocketUri.Value = (_publishUriDraft ?? "").Trim(); ModConfig.RemoteServerUri.Value = (_remoteUriDraft ?? "").Trim(); ModConfig.RemotePairCode.Value = (_remoteCodeDraft ?? "").Trim(); ModConfig.PanelKey.Value = keyCode; ModConfig.EmergencyStopKey.Value = keyCode2; SaveConfig(); RefreshQr(); if (restartServer) { _server.Stop(); _server.Start(); } _settingsMessage = "设置已保存。当前二维码已刷新。"; } private static bool TryParseKey(string value, out KeyCode keyCode) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected I4, but got Unknown try { keyCode = (KeyCode)(int)(KeyCode)Enum.Parse(typeof(KeyCode), (value ?? "").Trim(), ignoreCase: true); return true; } catch { keyCode = (KeyCode)0; return false; } } private static void SaveConfig() { try { if ((Object)(object)Plugin.Instance != (Object)null) { ((BaseUnityPlugin)Plugin.Instance).Config.Save(); } } catch { } } private static void Section(string label) { GUILayout.Space(8f); GUILayout.Label("=== " + label + " ===", (GUILayoutOption[])(object)new GUILayoutOption[0]); } private void TextRow(string label, ref string value) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label(label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); value = GUILayout.TextField(value ?? "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(360f) }); GUILayout.EndHorizontal(); } private void Toggle(string label, ConfigEntry entry) { bool flag = GUILayout.Toggle(entry.Value, label, (GUILayoutOption[])(object)new GUILayoutOption[0]); if (flag != entry.Value) { entry.Value = flag; SaveConfig(); } } private void ApplyPreset(int footstep, int slide, int jump, int land, int death, int minimum, int hurtMax) { ModConfig.AutoStrengthMode.Value = AutoStrengthMode.EventScaled; ModConfig.ContinuousMode.Value = true; ModConfig.MinimumChannelStrength.Value = minimum; ModConfig.MaxWaveIntensity.Value = Mathf.Max(hurtMax, death); ModConfig.FootstepIntensity.Value = footstep; ModConfig.FootstepDurationMs.Value = ((footstep >= 24) ? 320 : 250); ModConfig.FootstepWaveShape.Value = StimWaveShape.Sin; ModConfig.WalkBaseIntensity.Value = Mathf.Max(4, footstep / 2); ModConfig.WalkBaseFrequency.Value = 25; ModConfig.WalkBaseShape.Value = StimEnvelopeShape.Tremor; ModConfig.WalkStepBumpIntensity.Value = Mathf.Max(8, footstep - 4); ModConfig.WalkStepBumpDurationMs.Value = 160; ModConfig.WalkFootstepMinIntervalMs.Value = 90; ModConfig.RunBaseIntensity.Value = Mathf.Max(6, footstep / 2 + 5); ModConfig.RunBaseFrequency.Value = 260; ModConfig.RunBaseShape.Value = StimEnvelopeShape.SawBurst; ModConfig.RunFootstepIntensity.Value = Mathf.Max(footstep + 4, footstep); ModConfig.RunFootstepDurationMs.Value = ((footstep >= 28) ? 170 : 140); ModConfig.RunStepBumpIntensity.Value = Mathf.Max(12, footstep); ModConfig.RunStepBumpDurationMs.Value = 110; ModConfig.RunFootstepMinIntervalMs.Value = 55; ModConfig.SlideIntensity.Value = slide; ModConfig.JumpIntensity.Value = jump; ModConfig.LandIntensity.Value = land; ModConfig.HurtMaxIntensity.Value = hurtMax; ModConfig.HurtDurationMinMs.Value = ((hurtMax >= 80) ? 1800 : 1500); ModConfig.HurtDurationMaxMs.Value = ((hurtMax >= 80) ? 4500 : 3500); ModConfig.HurtFrequency.Value = 60; ModConfig.DeathIntensity.Value = death; ModConfig.DeathDurationMs.Value = ((death >= 80) ? 5000 : 4000); ModConfig.DeathFrequency.Value = 150; ModConfig.MaxEventDurationMs.Value = Mathf.Max(ModConfig.DeathDurationMs.Value, ModConfig.HurtDurationMaxMs.Value); ModConfig.MaxQueuedPulseItems.Value = ((death >= 80) ? 55 : 45); ModConfig.TestIntensity.Value = minimum; ModConfig.HurtEnvelope.Value = StimEnvelopeShape.RampUpHoldDown; ModConfig.LandEnvelope.Value = StimEnvelopeShape.SingleTap; ModConfig.SlideEnvelope.Value = StimEnvelopeShape.Tremor; ModConfig.DeathEnvelope.Value = StimEnvelopeShape.DeathWave; SaveConfig(); _settingsMessage = "已应用预设,可继续微调各事件强度。"; } private void ApplyComfortPreset() { ModConfig.AutoStrengthMode.Value = AutoStrengthMode.EventScaled; ModConfig.ContinuousMode.Value = true; ModConfig.MinimumChannelStrength.Value = 10; ModConfig.MaxWaveIntensity.Value = 40; ModConfig.MaxEventDurationMs.Value = 3500; ModConfig.MaxQueuedPulseItems.Value = 35; ModConfig.ContinuousSendIntervalMs.Value = 80; ModConfig.ContinuousLookaheadMs.Value = 120; ModConfig.ContinuousFadeOutMs.Value = 450; ModConfig.FootstepIntensity.Value = 12; ModConfig.FootstepDurationMs.Value = 220; ModConfig.FootstepWaveShape.Value = StimWaveShape.Sin; ModConfig.WalkBaseIntensity.Value = 10; ModConfig.WalkBaseFrequency.Value = 25; ModConfig.WalkBaseShape.Value = StimEnvelopeShape.Tremor; ModConfig.WalkStepBumpIntensity.Value = 8; ModConfig.WalkStepBumpDurationMs.Value = 150; ModConfig.WalkFootstepMinIntervalMs.Value = 90; ModConfig.RunBaseIntensity.Value = 12; ModConfig.RunBaseFrequency.Value = 240; ModConfig.RunBaseShape.Value = StimEnvelopeShape.SawBurst; ModConfig.RunFootstepIntensity.Value = 14; ModConfig.RunFootstepDurationMs.Value = 130; ModConfig.RunStepBumpIntensity.Value = 10; ModConfig.RunStepBumpDurationMs.Value = 100; ModConfig.RunFootstepMinIntervalMs.Value = 55; ModConfig.SlideIntensity.Value = 14; ModConfig.JumpIntensity.Value = 16; ModConfig.LandIntensity.Value = 18; ModConfig.HurtMinIntensity.Value = 10; ModConfig.HurtMaxIntensity.Value = 40; ModConfig.HurtDurationMinMs.Value = 1200; ModConfig.HurtDurationMaxMs.Value = 3000; ModConfig.HurtFrequency.Value = 60; ModConfig.DeathIntensity.Value = 40; ModConfig.DeathDurationMs.Value = 3500; ModConfig.DeathFrequency.Value = 150; ModConfig.TestIntensity.Value = 10; ModConfig.HurtEnvelope.Value = StimEnvelopeShape.RampUpHoldDown; ModConfig.LandEnvelope.Value = StimEnvelopeShape.SingleTap; ModConfig.SlideEnvelope.Value = StimEnvelopeShape.Tremor; ModConfig.DeathEnvelope.Value = StimEnvelopeShape.DeathWave; SaveConfig(); _settingsMessage = "已应用舒适模式:最高 40,基础强度 10。"; } private void IntSlider(string label, ConfigEntry entry, int min, int max) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label(label + ":" + entry.Value, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }); int num = (int)Math.Round(GUILayout.HorizontalSlider((float)entry.Value, (float)min, (float)max, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(230f) })); if (num != entry.Value) { entry.Value = num; SaveConfig(); } GUILayout.EndHorizontal(); } private void FloatSlider(string label, ConfigEntry entry, float min, float max) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label(label + ":" + entry.Value.ToString("0.00"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }); float num = GUILayout.HorizontalSlider(entry.Value, min, max, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(230f) }); num = (float)Math.Round(num, 2); if (Math.Abs(num - entry.Value) > 0.001f) { entry.Value = num; SaveConfig(); } GUILayout.EndHorizontal(); } private void EnumCycle(string label, ConfigEntry entry) { GUILayout.BeginHorizontal((GUILayoutOption[])(object)new GUILayoutOption[0]); GUILayout.Label(label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(180f) }); if (GUILayout.Button(entry.Value.ToString(), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(160f) })) { T[] array = (T[])Enum.GetValues(typeof(T)); int num = Array.IndexOf(array, entry.Value); if (num < 0) { num = 0; } entry.Value = array[(num + 1) % array.Length]; SaveConfig(); RefreshQr(); } GUILayout.EndHorizontal(); } private void RefreshQr() { _qrText = null; if ((Object)(object)_qrTexture != (Object)null) { Object.Destroy((Object)(object)_qrTexture); _qrTexture = null; } } private void EnsureQrTexture(string text) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_qrTexture != (Object)null && _qrText == text) { return; } RefreshQr(); _qrText = text; try { QrCode val = QrCode.EncodeText(text, Ecc.Medium); int num = 4; int num2 = Math.Max(3, 180 / (val.Size + num * 2)); int num3 = (val.Size + num * 2) * num2; Texture2D val2 = new Texture2D(num3, num3, (TextureFormat)4, false); Color32 val3 = default(Color32); ((Color32)(ref val3))..ctor((byte)0, (byte)0, (byte)0, byte.MaxValue); Color32 val4 = default(Color32); ((Color32)(ref val4))..ctor(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); for (int i = 0; i < num3; i++) { for (int j = 0; j < num3; j++) { int num4 = j / num2 - num; int num5 = i / num2 - num; bool flag = num4 >= 0 && num5 >= 0 && num4 < val.Size && num5 < val.Size && val.GetModule(num4, num5); val2.SetPixel(j, num3 - 1 - i, Color32.op_Implicit(flag ? val3 : val4)); } } val2.Apply(false, true); _qrTexture = val2; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to generate QR texture: " + ex.Message)); } } } internal sealed class DgLabMessage { [JsonProperty("type")] public string Type { get; set; } [JsonProperty("clientId")] public string ClientId { get; set; } [JsonProperty("targetId")] public string TargetId { get; set; } [JsonProperty("message")] public string Message { get; set; } public static DgLabMessage Create(string type, string clientId, string targetId, string message) { DgLabMessage dgLabMessage = new DgLabMessage(); dgLabMessage.Type = type; dgLabMessage.ClientId = clientId; dgLabMessage.TargetId = targetId; dgLabMessage.Message = message; return dgLabMessage; } public string ToJson() { return JsonConvert.SerializeObject((object)this); } } internal sealed class StrengthState { public int ACurrent; public int BCurrent; public int AMax; public int BMax; public StrengthState Clone() { StrengthState strengthState = new StrengthState(); strengthState.ACurrent = ACurrent; strengthState.BCurrent = BCurrent; strengthState.AMax = AMax; strengthState.BMax = BMax; return strengthState; } } internal sealed class DGLabSocketServer { internal sealed class AppSocketBehavior : WebSocketBehavior { public DGLabSocketServer Owner { get; set; } public string RemoteEndpoint { get { try { return (((WebSocketBehavior)this).Context != null && ((WebSocketBehavior)this).Context.UserEndPoint != null) ? ((WebSocketBehavior)this).Context.UserEndPoint.ToString() : ""; } catch { return ""; } } } protected override void OnOpen() { Owner.OnOpen(this); } protected override void OnMessage(MessageEventArgs e) { Owner.OnMessage(this, e.Data); } protected override void OnClose(CloseEventArgs e) { Owner.OnClose(this); } public void SendText(string text) { ((WebSocketBehavior)this).Send(text); } } private static readonly Regex StrengthRegex = new Regex("^strength-(\\d+)\\+(\\d+)\\+(\\d+)\\+(\\d+)$", RegexOptions.Compiled); private static readonly Regex SafeCodeRegex = new Regex("[^A-Za-z0-9_-]", RegexOptions.Compiled); private readonly object _sync = new object(); private string _terminalId; private WebSocketServer _server; private WebSocket _remoteSocket; private AppSocketBehavior _session; private string _appId; private bool _bound; private bool _remoteConnected; private bool _remoteClientIdAssigned; private DateTime _lastHeartbeatUtc; private StrengthState _strength; private string _lastError; private string _lastRemoteEndpoint; private string _remoteStatus; internal string ClientId { get { lock (_sync) { return _terminalId; } } } internal bool IsRunning { get { if (IsRemoteMode()) { lock (_sync) { return _remoteSocket != null && _remoteConnected; } } if (_server != null) { return _server.IsListening; } return false; } } internal bool IsConnected { get { lock (_sync) { if (IsRemoteMode()) { return _remoteSocket != null && _remoteConnected; } return _session != null; } } } internal bool IsBound { get { lock (_sync) { return _bound && (IsRemoteMode() ? _remoteConnected : (_session != null)); } } } internal StrengthState Strength { get { lock (_sync) { return _strength.Clone(); } } } internal string LastError { get { lock (_sync) { return _lastError; } } } internal string LastRemoteEndpoint { get { lock (_sync) { return _lastRemoteEndpoint; } } } internal DGLabSocketServer() { _terminalId = Guid.NewGuid().ToString(); _appId = ""; _strength = new StrengthState(); _lastError = ""; _lastRemoteEndpoint = ""; _remoteStatus = ""; } internal void Start() { if (!IsRunning) { if (IsRemoteMode()) { StartRemoteClient(); } else { StartLocalServer(); } } } private void StartLocalServer() { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected O, but got Unknown int num = Clamp(ModConfig.Port.Value, 1, 65535); try { _server = new WebSocketServer(IPAddress.Any, num); _server.AddWebSocketService("/", (Action)delegate(AppSocketBehavior behavior) { behavior.Owner = this; }); _server.AddWebSocketService("/" + _terminalId, (Action)delegate(AppSocketBehavior behavior) { behavior.Owner = this; }); _server.Start(); _lastHeartbeatUtc = DateTime.UtcNow; lock (_sync) { _lastError = ""; _remoteStatus = ""; } Plugin.Log.LogInfo((object)("DG-LAB local Socket server listening on port " + num + " with terminalId " + _terminalId)); } catch (Exception ex) { _server = null; lock (_sync) { _lastError = ex.Message; } Plugin.Log.LogError((object)("Failed to start DG-LAB Socket server on port " + num + ": " + ex.Message)); } } private void StartRemoteClient() { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown string uri = NormalizeBaseUri(ModConfig.RemoteServerUri.Value); if (string.IsNullOrEmpty(uri)) { lock (_sync) { _lastError = "远程 Socket 服务地址为空。"; _remoteStatus = _lastError; return; } } if (ModConfig.ConnectionMode.Value == SocketConnectionMode.RelayCode) { string text = SafeRelayCode(ModConfig.RemotePairCode.Value); if (!string.IsNullOrEmpty(text)) { lock (_sync) { _terminalId = text; } } } try { _remoteSocket = new WebSocket(uri, new string[0]); _remoteSocket.OnOpen += delegate { lock (_sync) { _remoteConnected = true; _remoteClientIdAssigned = ModConfig.ConnectionMode.Value == SocketConnectionMode.RelayCode && !string.IsNullOrEmpty(SafeRelayCode(ModConfig.RemotePairCode.Value)); _bound = false; _appId = ""; _lastError = ""; _remoteStatus = "已连接远程服务,等待终端 ID/绑定。"; _lastRemoteEndpoint = uri; } Plugin.Log.LogInfo((object)("Connected to remote DG-LAB Socket service: " + uri)); }; _remoteSocket.OnMessage += delegate(object sender, MessageEventArgs e) { HandleRemoteMessage(e.Data); }; _remoteSocket.OnClose += delegate(object sender, CloseEventArgs e) { lock (_sync) { _remoteConnected = false; _remoteClientIdAssigned = false; _bound = false; _appId = ""; _strength = new StrengthState(); _remoteStatus = "远程服务已断开:" + e.Reason; } Plugin.Log.LogInfo((object)("Remote DG-LAB Socket closed: " + e.Reason)); }; _remoteSocket.OnError += delegate(object sender, ErrorEventArgs e) { lock (_sync) { _lastError = e.Message; _remoteStatus = "远程服务错误:" + e.Message; } Plugin.Log.LogWarning((object)("Remote DG-LAB Socket error: " + e.Message)); }; _remoteSocket.ConnectAsync(); } catch (Exception ex) { _remoteSocket = null; lock (_sync) { _lastError = ex.Message; _remoteStatus = "远程服务启动失败:" + ex.Message; } Plugin.Log.LogError((object)("Failed to connect remote DG-LAB Socket service: " + ex.Message)); } } internal void Stop() { lock (_sync) { _bound = false; _session = null; _appId = ""; _lastRemoteEndpoint = ""; _remoteConnected = false; _remoteClientIdAssigned = false; } if (_remoteSocket != null) { try { _remoteSocket.Close(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to close remote DG-LAB Socket: " + ex.Message)); } _remoteSocket = null; } if (_server != null) { try { _server.Stop(); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Failed to stop DG-LAB Socket server: " + ex2.Message)); } _server = null; } } internal void Tick() { if (!IsBound) { return; } int num = Math.Max(5000, ModConfig.HeartbeatIntervalMs.Value); if ((DateTime.UtcNow - _lastHeartbeatUtc).TotalMilliseconds >= (double)num) { if (IsRemoteMode()) { SendRemoteRaw("heartbeat", "200"); } else { SendEnvelope("heartbeat", "200"); } _lastHeartbeatUtc = DateTime.UtcNow; } } internal string GetQrUrl() { return "https://www.dungeon-lab.com/app-download.php#DGLAB-SOCKET#" + GetSocketBaseUri() + "/" + ClientId; } internal string GetSocketBaseUri() { SocketConnectionMode value = ModConfig.ConnectionMode.Value; if (value == SocketConnectionMode.RemoteServer || value == SocketConnectionMode.RelayCode) { string text = NormalizeBaseUri(ModConfig.RemoteServerUri.Value); if (!string.IsNullOrEmpty(text)) { return text; } return "ws://127.0.0.1:" + Clamp(ModConfig.Port.Value, 1, 65535); } string text2 = NormalizeBaseUri(ModConfig.PublishSocketUri.Value); if (value == SocketConnectionMode.PublicSocket && !string.IsNullOrEmpty(text2)) { return text2; } if (!string.IsNullOrEmpty(text2)) { return text2; } return "ws://" + GetAdvertiseHost() + ":" + Clamp(ModConfig.Port.Value, 1, 65535); } internal string GetAdvertiseHost() { string value = ModConfig.AdvertiseHost.Value; if (!string.IsNullOrEmpty(value)) { return value.Trim(); } string[] localIPv4Addresses = GetLocalIPv4Addresses(); if (localIPv4Addresses.Length > 0) { return localIPv4Addresses[0]; } return "127.0.0.1"; } internal string[] GetLocalIPv4Addresses() { List list = new List(); try { IPAddress[] addressList = Dns.GetHostEntry(Dns.GetHostName()).AddressList; foreach (IPAddress iPAddress in addressList) { if (iPAddress.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(iPAddress)) { list.Add(iPAddress.ToString()); } } } catch { } return list.ToArray(); } internal string LocalAddressSummary() { StringBuilder stringBuilder = new StringBuilder(); string[] localIPv4Addresses = GetLocalIPv4Addresses(); for (int i = 0; i < localIPv4Addresses.Length; i++) { if (i > 0) { stringBuilder.Append(" / "); } stringBuilder.Append(localIPv4Addresses[i]); } if (stringBuilder.Length != 0) { return stringBuilder.ToString(); } return "未检测到局域网 IPv4"; } internal string StatusText() { if (IsRemoteMode()) { string remoteStatus; string lastError; lock (_sync) { remoteStatus = _remoteStatus; lastError = _lastError; } if (IsBound) { return "远程 Socket 已绑定"; } if (IsConnected) { if (!string.IsNullOrEmpty(remoteStatus)) { return remoteStatus; } return "远程 Socket 已连接,等待绑定"; } if (!string.IsNullOrEmpty(lastError)) { return "远程 Socket 未连接:" + lastError; } return "远程 Socket 未连接"; } if (!IsRunning) { string lastError2 = LastError; if (!string.IsNullOrEmpty(lastError2)) { return "服务未启动:" + lastError2; } return "服务未启动"; } if (IsBound) { return "已绑定"; } if (IsConnected) { return "已连接,等待绑定"; } return "等待手机 App 连接"; } internal string ConnectionModeText() { return ModConfig.ConnectionMode.Value switch { SocketConnectionMode.LocalServer => "本地局域网", SocketConnectionMode.PublicSocket => "公网/隧道地址", SocketConnectionMode.RemoteServer => "远程 Socket 后端", _ => "自建 Relay Code", }; } internal void OnOpen(AppSocketBehavior behavior) { lock (_sync) { if (_session != null && !object.ReferenceEquals(_session, behavior)) { behavior.SendText(DgLabMessage.Create("error", "", "", "400").ToJson()); return; } _session = behavior; _bound = false; _appId = Guid.NewGuid().ToString(); _strength = new StrengthState(); _lastRemoteEndpoint = behavior.RemoteEndpoint; } behavior.SendText(DgLabMessage.Create("bind", _appId, "", "targetId").ToJson()); Plugin.Log.LogInfo((object)("DG-LAB app connected; assigned appId " + _appId + " from " + behavior.RemoteEndpoint)); } internal void OnClose(AppSocketBehavior behavior) { lock (_sync) { if (object.ReferenceEquals(_session, behavior)) { _session = null; _bound = false; _appId = ""; _strength = new StrengthState(); _lastRemoteEndpoint = ""; } } Plugin.Log.LogInfo((object)"DG-LAB app disconnected."); } internal void OnMessage(AppSocketBehavior behavior, string raw) { DgLabMessage dgLabMessage; try { dgLabMessage = JsonConvert.DeserializeObject(raw); } catch { behavior.SendText(DgLabMessage.Create("msg", "", "", "403").ToJson()); return; } if (dgLabMessage == null || string.IsNullOrEmpty(dgLabMessage.Type)) { behavior.SendText(DgLabMessage.Create("msg", "", "", "403").ToJson()); } else if (dgLabMessage.Type == "bind") { HandleBind(behavior, dgLabMessage); } else if (dgLabMessage.Type == "msg") { HandleAppMessage(dgLabMessage); } } private void HandleBind(AppSocketBehavior behavior, DgLabMessage message) { bool flag; lock (_sync) { flag = object.ReferenceEquals(_session, behavior) && message.ClientId == _terminalId && message.TargetId == _appId; if (flag) { _bound = true; } } if (flag) { behavior.SendText(DgLabMessage.Create("bind", _terminalId, _appId, "200").ToJson()); Plugin.Log.LogInfo((object)"DG-LAB app bound successfully."); } else { behavior.SendText(DgLabMessage.Create("bind", message.ClientId ?? "", message.TargetId ?? "", "400").ToJson()); } } private void HandleRemoteMessage(string raw) { DgLabMessage dgLabMessage; try { dgLabMessage = JsonConvert.DeserializeObject(raw); } catch { Plugin.Log.LogWarning((object)"Remote DG-LAB Socket sent invalid JSON."); return; } if (dgLabMessage == null || string.IsNullOrEmpty(dgLabMessage.Type)) { return; } if (dgLabMessage.Type == "bind") { HandleRemoteBind(dgLabMessage); } else if (dgLabMessage.Type == "msg") { HandleAppMessage(dgLabMessage); } else if (dgLabMessage.Type == "break") { lock (_sync) { _bound = false; _appId = ""; _remoteStatus = "远程配对已断开:" + dgLabMessage.Message; } } } private void HandleRemoteBind(DgLabMessage message) { lock (_sync) { if (message.Message == "targetId") { if ((ModConfig.ConnectionMode.Value != SocketConnectionMode.RelayCode || string.IsNullOrEmpty(SafeRelayCode(ModConfig.RemotePairCode.Value))) && !string.IsNullOrEmpty(message.ClientId)) { _terminalId = message.ClientId; } _remoteClientIdAssigned = true; _remoteStatus = "已获取远程终端 ID,等待手机扫码绑定。"; } else if (message.Message == "200") { _bound = true; _appId = message.TargetId ?? ""; _lastHeartbeatUtc = DateTime.UtcNow; _remoteStatus = "远程 Socket 已绑定。"; Plugin.Log.LogInfo((object)"Remote DG-LAB app bound successfully."); } else { _remoteStatus = "远程绑定失败:" + message.Message; _lastError = _remoteStatus; } } } private void HandleAppMessage(DgLabMessage message) { Match match = StrengthRegex.Match(message.Message ?? ""); if (!match.Success) { return; } lock (_sync) { _strength.ACurrent = ParseInt(match.Groups[1].Value); _strength.BCurrent = ParseInt(match.Groups[2].Value); _strength.AMax = ParseInt(match.Groups[3].Value); _strength.BMax = ParseInt(match.Groups[4].Value); } } internal bool SendPulse(char channel, IList pulses) { if (pulses == null || pulses.Count == 0) { return false; } int val = Clamp(ModConfig.MaxQueuedPulseItems.Value, 1, 100); int count = Math.Min(val, pulses.Count); string command = "pulse-" + channel + ":" + WaveformEncoder.ToJsonArray(pulses, count); return SendEnvelope("msg", command); } internal bool ClearChannel(int channel) { return SendEnvelope("msg", "clear-" + channel); } internal void ClearAll() { ClearChannel(1); ClearChannel(2); } internal bool SetStrength(int channel, int value) { int num = Clamp(value, 0, 200); return SendEnvelope("msg", "strength-" + channel + "+2+" + num); } private bool SendEnvelope(string type, string command) { if (IsRemoteMode()) { if (type == "msg") { return SendRemoteDirect(command); } return SendRemoteRaw(type, command); } AppSocketBehavior session; string appId; lock (_sync) { if (_session == null || !_bound) { return false; } session = _session; appId = _appId; } if (command != null && command.Length > 1950) { Plugin.Log.LogWarning((object)"DG-LAB command was too long and has been dropped."); return false; } try { session.SendText(DgLabMessage.Create(type, _terminalId, appId, command).ToJson()); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to send DG-LAB command: " + ex.Message)); return false; } } private bool SendRemoteDirect(string command) { if (command != null && command.Length > 1950) { Plugin.Log.LogWarning((object)"DG-LAB command was too long and has been dropped."); return false; } WebSocket remoteSocket; string terminalId; string appId; lock (_sync) { if (_remoteSocket == null || !_remoteConnected || !_bound) { return false; } remoteSocket = _remoteSocket; terminalId = _terminalId; appId = _appId; } try { string text = JsonConvert.SerializeObject((object)new { type = 4, message = command, clientId = terminalId, targetId = appId }); remoteSocket.Send(text); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to send remote DG-LAB command: " + ex.Message)); return false; } } private bool SendRemoteRaw(string type, string command) { WebSocket remoteSocket; string terminalId; string appId; lock (_sync) { if (_remoteSocket == null || !_remoteConnected) { return false; } remoteSocket = _remoteSocket; terminalId = _terminalId; appId = _appId; } try { remoteSocket.Send(DgLabMessage.Create(type, terminalId, appId, command).ToJson()); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to send remote DG-LAB raw message: " + ex.Message)); return false; } } private bool IsRemoteMode() { SocketConnectionMode value = ModConfig.ConnectionMode.Value; if (value != SocketConnectionMode.RemoteServer) { return value == SocketConnectionMode.RelayCode; } return true; } private static string NormalizeBaseUri(string value) { string text = (value ?? "").Trim(); while (text.EndsWith("/", StringComparison.Ordinal)) { text = text.Substring(0, text.Length - 1); } return text; } private static string SafeRelayCode(string value) { string text = (value ?? "").Trim(); if (string.IsNullOrEmpty(text)) { return ""; } return SafeCodeRegex.Replace(text, ""); } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } private static int ParseInt(string value) { if (!int.TryParse(value, out var result)) { return 0; } return result; } } internal enum SocketConnectionMode { LocalServer, PublicSocket, RemoteServer, RelayCode } internal enum StimChannel { A, B } internal enum StimChannelMode { A, B, Both, Alternate } internal enum AutoStrengthMode { Off, MinimumOnly, EventScaled } internal enum StimWaveShape { Smooth, Gradient, Sin } internal enum StimEnvelopeShape { SingleTap, DoubleTap, RampUpHoldDown, SawBurst, Tremor, DeathWave } internal static class ModConfig { internal static ConfigEntry AutoStartServer; internal static ConfigEntry ConnectionMode; internal static ConfigEntry Port; internal static ConfigEntry AdvertiseHost; internal static ConfigEntry PublishSocketUri; internal static ConfigEntry RemoteServerUri; internal static ConfigEntry RemotePairCode; internal static ConfigEntry HeartbeatIntervalMs; internal static ConfigEntry Armed; internal static ConfigEntry AutoArmOnBind; internal static ConfigEntry AllowStrengthControl; internal static ConfigEntry AutoStrengthMode; internal static ConfigEntry MinimumChannelStrength; internal static ConfigEntry MaxWaveIntensity; internal static ConfigEntry MaxEventDurationMs; internal static ConfigEntry MaxQueuedPulseItems; internal static ConfigEntry ClearDelayMs; internal static ConfigEntry ContinuousMode; internal static ConfigEntry ContinuousSendIntervalMs; internal static ConfigEntry ContinuousLookaheadMs; internal static ConfigEntry ContinuousFadeOutMs; internal static ConfigEntry PanelKey; internal static ConfigEntry EmergencyStopKey; internal static ConfigEntry HurtEnabled; internal static ConfigEntry DeathEnabled; internal static ConfigEntry LocalFootstepEnabled; internal static ConfigEntry JumpEnabled; internal static ConfigEntry LandEnabled; internal static ConfigEntry SlideEnabled; internal static ConfigEntry LandJumpEnabled; internal static ConfigEntry EnemyFootstepEnabled; internal static ConfigEntry HurtCooldownMs; internal static ConfigEntry FootstepCooldownMs; internal static ConfigEntry JumpCooldownMs; internal static ConfigEntry LandCooldownMs; internal static ConfigEntry SlideCooldownMs; internal static ConfigEntry MovementCooldownMs; internal static ConfigEntry DeathCooldownMs; internal static ConfigEntry FirstFootLeft; internal static ConfigEntry LeftFootChannel; internal static ConfigEntry RightFootChannel; internal static ConfigEntry ResetFootOnIdle; internal static ConfigEntry FootstepIdleResetMs; internal static ConfigEntry FootstepIntensity; internal static ConfigEntry FootstepDurationMs; internal static ConfigEntry FootstepFrequency; internal static ConfigEntry FootstepWaveShape; internal static ConfigEntry FootstepDuplicateWindowMs; internal static ConfigEntry WalkFootstepMinIntervalMs; internal static ConfigEntry RunFootstepIntensity; internal static ConfigEntry RunFootstepDurationMs; internal static ConfigEntry RunFootstepMinIntervalMs; internal static ConfigEntry FallbackFootstepSupplement; internal static ConfigEntry FallbackFootstepNoAnimationMs; internal static ConfigEntry RunCadenceSupplement; internal static ConfigEntry RunSupplementIntervalMs; internal static ConfigEntry WalkBaseIntensity; internal static ConfigEntry WalkBaseFrequency; internal static ConfigEntry WalkBaseShape; internal static ConfigEntry WalkStepBumpIntensity; internal static ConfigEntry WalkStepBumpDurationMs; internal static ConfigEntry RunBaseIntensity; internal static ConfigEntry RunBaseFrequency; internal static ConfigEntry RunBaseShape; internal static ConfigEntry RunStepBumpIntensity; internal static ConfigEntry RunStepBumpDurationMs; internal static ConfigEntry JumpIntensity; internal static ConfigEntry JumpDurationMs; internal static ConfigEntry JumpFrequency; internal static ConfigEntry JumpChannelMode; internal static ConfigEntry LandIntensity; internal static ConfigEntry LandDurationMs; internal static ConfigEntry LandFrequency; internal static ConfigEntry LandChannelMode; internal static ConfigEntry LandEnvelope; internal static ConfigEntry SlideIntensity; internal static ConfigEntry SlideDurationMs; internal static ConfigEntry SlideFrequency; internal static ConfigEntry SlideChannelMode; internal static ConfigEntry SlideEnvelope; internal static ConfigEntry SlideBaseIntensity; internal static ConfigEntry SlideBaseFrequency; internal static ConfigEntry HurtMinIntensity; internal static ConfigEntry HurtMaxIntensity; internal static ConfigEntry HurtDamageMultiplier; internal static ConfigEntry HurtAMultiplier; internal static ConfigEntry HurtBMultiplier; internal static ConfigEntry HurtDurationMinMs; internal static ConfigEntry HurtDurationMaxMs; internal static ConfigEntry HurtFrequency; internal static ConfigEntry HurtChannelMode; internal static ConfigEntry HurtEnvelope; internal static ConfigEntry DeathIntensity; internal static ConfigEntry DeathDurationMs; internal static ConfigEntry DeathFrequency; internal static ConfigEntry DeathChannelMode; internal static ConfigEntry DeathAWaveShape; internal static ConfigEntry DeathBWaveShape; internal static ConfigEntry DeathEnvelope; internal static ConfigEntry DeathClearBeforePulse; internal static ConfigEntry EnemyFootstepIntensity; internal static ConfigEntry EnemyFootstepRange; internal static ConfigEntry TestIntensity; internal static void Bind(ConfigFile config) { AutoStartServer = config.Bind("Connection", "AutoStartServer", true, "Mod 加载时自动启动 DG-LAB Socket 连接。"); ConnectionMode = config.Bind("Connection", "ConnectionMode", SocketConnectionMode.LocalServer, "LocalServer=本机监听;PublicSocket=本机监听但二维码使用公网地址;RemoteServer=连接公网 Socket v2 后端;RelayCode=自建 relay code 兼容模式。"); Port = config.Bind("Connection", "Port", 9999, "本机 DG-LAB App Socket 监听端口。"); AdvertiseHost = config.Bind("Connection", "AdvertiseHost", "", "局域网二维码里展示给手机连接的电脑 IP/域名。留空时自动选择局域网 IPv4。"); PublishSocketUri = config.Bind("Connection", "PublishSocketUri", "", "公网/隧道模式二维码使用的 WebSocket 基础地址,例如 wss://ws.example.com。留空时使用局域网地址。"); RemoteServerUri = config.Bind("Connection", "RemoteServerUri", "", "RemoteServer/RelayCode 模式连接的公网 DG-LAB Socket v2 后端地址,例如 wss://ws.example.com。"); RemotePairCode = config.Bind("Connection", "RemotePairCode", "", "RelayCode 模式使用的自定义终端码。该功能需要兼容自定义码的 relay 服务,不能直接填写 DG-LAB App 原生远程口令。"); HeartbeatIntervalMs = config.Bind("Connection", "HeartbeatIntervalMs", 60000, "发送给 DG-LAB App 或远程服务的心跳间隔,单位毫秒。"); Armed = config.Bind("Safety", "Enabled", false, "为 false 时,游戏事件不会触发波形。可在面板里点击“启用触发”。"); AutoArmOnBind = config.Bind("Safety", "AutoArmOnBind", true, "绑定 DG-LAB App 后自动启用游戏事件触发。关闭后需手动点击“启用触发”。"); AllowStrengthControl = config.Bind("Safety", "AllowStrengthControl", false, "兼容旧配置:开启后等同于 EventScaled 自动调强。"); AutoStrengthMode = config.Bind("Safety", "AutoStrengthMode", DGLabPunish.AutoStrengthMode.EventScaled, "Off=不调强;MinimumOnly=只补到最低可感强度;EventScaled=按事件强度设置通道强度。"); MinimumChannelStrength = config.Bind("Safety", "MinimumChannelStrength", 30, "MinimumOnly 模式下 A/B 通道会被补到的最低强度。"); MaxWaveIntensity = config.Bind("Safety", "MaxWaveIntensity", 60, "本 Mod 使用的波形强度上限。DG-LAB 波形强度范围为 0-100。"); MaxEventDurationMs = config.Bind("Safety", "MaxEventDurationMs", 4000, "单次事件波形最长持续时间,单位毫秒。"); MaxQueuedPulseItems = config.Bind("Safety", "MaxQueuedPulseItems", 45, "单次事件最多发送多少条 100ms 波形数据。"); ClearDelayMs = config.Bind("Safety", "ClearDelayMs", 60, "高优先级事件 clear 后等待多久再发送新波形,单位毫秒。"); ContinuousMode = config.Bind("Safety", "ContinuousMode", true, "开启后使用持续状态机:走路/跑步/滑行期间持续输出基础波形,事件叠加强度。关闭后使用旧版单发事件模式。"); ContinuousSendIntervalMs = config.Bind("Continuous", "SendIntervalMs", 70, "连续模式补包间隔,单位毫秒。"); ContinuousLookaheadMs = config.Bind("Continuous", "LookaheadMs", 100, "连续模式每次生成未来多久的波形,单位毫秒。"); ContinuousFadeOutMs = config.Bind("Continuous", "FadeOutMs", 450, "移动停止后的淡出时间,单位毫秒。"); PanelKey = config.Bind("Safety", "PanelKey", (KeyCode)112, "打开/关闭游戏内郊狼连接面板。"); EmergencyStopKey = config.Bind("Safety", "EmergencyStopKey", (KeyCode)105, "清空 A/B 波形队列并将 A/B 通道强度设为 0。"); HurtEnabled = config.Bind("Events", "HurtEnabled", true, "本地玩家受击时触发波形。"); DeathEnabled = config.Bind("Events", "DeathEnabled", true, "本地玩家死亡时触发波形。"); LocalFootstepEnabled = config.Bind("Events", "LocalFootstepEnabled", true, "本地玩家脚步时触发低强度波形。"); JumpEnabled = config.Bind("Events", "JumpEnabled", true, "本地玩家跳跃时触发低强度波形。"); LandEnabled = config.Bind("Events", "LandEnabled", true, "本地玩家落地时触发低强度波形。"); SlideEnabled = config.Bind("Events", "SlideEnabled", true, "本地玩家滑行时触发低强度波形。"); LandJumpEnabled = config.Bind("Events", "LandJumpEnabled", true, "兼容旧配置:总开关,关闭后跳跃、落地、滑行都不触发。"); EnemyFootstepEnabled = config.Bind("Events", "EnemyFootstepEnabled", false, "猎人敌人近距离脚步提示,默认关闭。"); HurtCooldownMs = config.Bind("Timing", "HurtCooldownMs", 450, "两次受击波形之间的最短间隔,单位毫秒。"); FootstepCooldownMs = config.Bind("Timing", "FootstepCooldownMs", 120, "两次脚步波形之间的最短间隔,单位毫秒。"); JumpCooldownMs = config.Bind("Timing", "JumpCooldownMs", 250, "两次跳跃波形之间的最短间隔,单位毫秒。"); LandCooldownMs = config.Bind("Timing", "LandCooldownMs", 250, "两次落地波形之间的最短间隔,单位毫秒。"); SlideCooldownMs = config.Bind("Timing", "SlideCooldownMs", 300, "两次滑行波形之间的最短间隔,单位毫秒。"); MovementCooldownMs = config.Bind("Timing", "MovementCooldownMs", 300, "兼容旧配置:旧版跳跃/落地/滑行冷却。"); DeathCooldownMs = config.Bind("Timing", "DeathCooldownMs", 5000, "两次死亡波形之间的最短间隔,单位毫秒。"); FirstFootLeft = config.Bind("Footstep", "FirstFootLeft", true, "站停重置后第一步是否按左脚处理。默认左脚。"); LeftFootChannel = config.Bind("Footstep", "LeftFootChannel", StimChannel.A, "左脚对应通道。默认 A。"); RightFootChannel = config.Bind("Footstep", "RightFootChannel", StimChannel.B, "右脚对应通道。默认 B。"); ResetFootOnIdle = config.Bind("Footstep", "ResetFootOnIdle", true, "长时间没有脚步后,下一步是否重置为 FirstFootLeft。"); FootstepIdleResetMs = config.Bind("Footstep", "FootstepIdleResetMs", 900, "多久没有脚步后重置左右脚,单位毫秒。"); FootstepIntensity = config.Bind("Footstep", "FootstepIntensity", 20, "本地脚步波形强度。"); FootstepDurationMs = config.Bind("Footstep", "FootstepDurationMs", 250, "本地脚步波形持续时间,单位毫秒。"); FootstepFrequency = config.Bind("Footstep", "FootstepFrequency", 180, "脚步波形频率值。"); FootstepWaveShape = config.Bind("Footstep", "FootstepWaveShape", StimWaveShape.Sin, "脚步波形形状。Sin 更像短促踩踏,Smooth 更平滑。"); FootstepDuplicateWindowMs = config.Bind("Footstep", "FootstepDuplicateWindowMs", 35, "极短时间内重复脚步事件的去重窗口,单位毫秒。"); WalkFootstepMinIntervalMs = config.Bind("Footstep", "WalkFootstepMinIntervalMs", 90, "走路脚步最小发送间隔,单位毫秒。"); RunFootstepIntensity = config.Bind("Footstep", "RunFootstepIntensity", 24, "奔跑脚步波形强度。"); RunFootstepDurationMs = config.Bind("Footstep", "RunFootstepDurationMs", 140, "奔跑脚步波形持续时间,单位毫秒。"); RunFootstepMinIntervalMs = config.Bind("Footstep", "RunFootstepMinIntervalMs", 55, "奔跑脚步最小发送间隔,单位毫秒。"); FallbackFootstepSupplement = config.Bind("Footstep", "FallbackFootstepSupplement", false, "动画脚步长时间没有触发时,是否允许 PlayerAvatar.Footstep 兜底补发。默认关闭。"); FallbackFootstepNoAnimationMs = config.Bind("Footstep", "FallbackFootstepNoAnimationMs", 450, "多久没有动画脚步后允许兜底补发,单位毫秒。"); RunCadenceSupplement = config.Bind("Footstep", "RunCadenceSupplement", false, "奔跑时如果动画脚步偏慢,是否按间隔补一拍。默认关闭。"); RunSupplementIntervalMs = config.Bind("Footstep", "RunSupplementIntervalMs", 170, "奔跑补拍间隔,单位毫秒。"); WalkBaseIntensity = config.Bind("ContinuousWalk", "WalkBaseIntensity", 10, "走路持续基础波形强度。"); WalkBaseFrequency = config.Bind("ContinuousWalk", "WalkBaseFrequency", 25, "走路持续基础波形频率。"); WalkBaseShape = config.Bind("ContinuousWalk", "WalkBaseShape", StimEnvelopeShape.Tremor, "走路持续基础波形形状。"); WalkStepBumpIntensity = config.Bind("ContinuousWalk", "WalkStepBumpIntensity", 16, "走路每步叠加强度。"); WalkStepBumpDurationMs = config.Bind("ContinuousWalk", "WalkStepBumpDurationMs", 160, "走路每步叠加强度持续时间。"); RunBaseIntensity = config.Bind("ContinuousRun", "RunBaseIntensity", 15, "奔跑持续基础波形强度。"); RunBaseFrequency = config.Bind("ContinuousRun", "RunBaseFrequency", 260, "奔跑持续基础波形频率。"); RunBaseShape = config.Bind("ContinuousRun", "RunBaseShape", StimEnvelopeShape.SawBurst, "奔跑持续基础波形形状。"); RunStepBumpIntensity = config.Bind("ContinuousRun", "RunStepBumpIntensity", 22, "奔跑每步叠加强度。"); RunStepBumpDurationMs = config.Bind("ContinuousRun", "RunStepBumpDurationMs", 110, "奔跑每步叠加强度持续时间。"); JumpIntensity = config.Bind("Actions", "JumpIntensity", 18, "跳跃波形强度。"); JumpDurationMs = config.Bind("Actions", "JumpDurationMs", 250, "跳跃波形持续时间,单位毫秒。"); JumpFrequency = config.Bind("Actions", "JumpFrequency", 260, "跳跃波形频率值。"); JumpChannelMode = config.Bind("Actions", "JumpChannelMode", StimChannelMode.Both, "跳跃触发通道。"); LandIntensity = config.Bind("Actions", "LandIntensity", 22, "落地波形强度。"); LandDurationMs = config.Bind("Actions", "LandDurationMs", 300, "落地波形持续时间,单位毫秒。"); LandFrequency = config.Bind("Actions", "LandFrequency", 300, "落地波形频率值。"); LandChannelMode = config.Bind("Actions", "LandChannelMode", StimChannelMode.Both, "落地触发通道。"); LandEnvelope = config.Bind("Actions", "LandEnvelope", StimEnvelopeShape.SingleTap, "落地包络波形。"); SlideIntensity = config.Bind("Actions", "SlideIntensity", 16, "滑行波形强度。"); SlideDurationMs = config.Bind("Actions", "SlideDurationMs", 300, "滑行波形持续时间,单位毫秒。"); SlideFrequency = config.Bind("Actions", "SlideFrequency", 220, "滑行波形频率值。"); SlideChannelMode = config.Bind("Actions", "SlideChannelMode", StimChannelMode.Both, "滑行触发通道。"); SlideEnvelope = config.Bind("Actions", "SlideEnvelope", StimEnvelopeShape.Tremor, "滑行包络波形。"); SlideBaseIntensity = config.Bind("Actions", "SlideBaseIntensity", 18, "连续模式下滑行基础颤动强度。"); SlideBaseFrequency = config.Bind("Actions", "SlideBaseFrequency", 240, "连续模式下滑行基础颤动频率。"); HurtMinIntensity = config.Bind("Hurt", "HurtMinIntensity", 18, "受击波形基础强度。"); HurtMaxIntensity = config.Bind("Hurt", "HurtMaxIntensity", 60, "受击波形最大强度。"); HurtDamageMultiplier = config.Bind("Hurt", "HurtDamageMultiplier", 3f, "受击伤害换算为波形强度的倍率。"); HurtAMultiplier = config.Bind("Hurt", "HurtAMultiplier", 1f, "受击 A 通道倍率。"); HurtBMultiplier = config.Bind("Hurt", "HurtBMultiplier", 0.7f, "受击 B 通道倍率。"); HurtDurationMinMs = config.Bind("Hurt", "HurtDurationMinMs", 1500, "受击波形最短持续时间,单位毫秒。"); HurtDurationMaxMs = config.Bind("Hurt", "HurtDurationMaxMs", 3500, "受击波形最长持续时间,单位毫秒。"); HurtFrequency = config.Bind("Hurt", "HurtFrequency", 60, "受击波形频率值。"); HurtChannelMode = config.Bind("Hurt", "HurtChannelMode", StimChannelMode.Both, "受击触发通道。"); HurtEnvelope = config.Bind("Hurt", "HurtEnvelope", StimEnvelopeShape.RampUpHoldDown, "受击包络波形。"); DeathIntensity = config.Bind("Death", "DeathIntensity", 45, "死亡波形强度。"); DeathDurationMs = config.Bind("Death", "DeathDurationMs", 4000, "死亡波形持续时间,单位毫秒。"); DeathFrequency = config.Bind("Death", "DeathFrequency", 150, "死亡波形频率值。"); DeathChannelMode = config.Bind("Death", "DeathChannelMode", StimChannelMode.Both, "死亡触发通道。"); DeathAWaveShape = config.Bind("Death", "DeathAWaveShape", StimWaveShape.Sin, "死亡 A 通道波形。"); DeathBWaveShape = config.Bind("Death", "DeathBWaveShape", StimWaveShape.Gradient, "死亡 B 通道波形。"); DeathEnvelope = config.Bind("Death", "DeathEnvelope", StimEnvelopeShape.DeathWave, "死亡包络波形。"); DeathClearBeforePulse = config.Bind("Death", "DeathClearBeforePulse", true, "死亡触发前是否清空 A/B 旧波形队列。"); EnemyFootstepIntensity = config.Bind("Enemy", "EnemyFootstepIntensity", 10, "敌人脚步提示波形强度。"); EnemyFootstepRange = config.Bind("Enemy", "EnemyFootstepRange", 12f, "敌人脚步提示触发范围,单位为 Unity 距离。"); TestIntensity = config.Bind("Waveform", "TestIntensity", 20, "面板测试波形强度。"); MigrateOldSoftDefaults(); config.Save(); } private static void MigrateOldSoftDefaults() { if (MinimumChannelStrength.Value <= 20 && MaxWaveIntensity.Value > 40) { MinimumChannelStrength.Value = 30; } if (MaxEventDurationMs.Value <= 2200) { MaxEventDurationMs.Value = 4000; } if (MaxQueuedPulseItems.Value <= 30) { MaxQueuedPulseItems.Value = 45; } if (ContinuousSendIntervalMs.Value >= 100) { ContinuousSendIntervalMs.Value = 70; } if (ContinuousLookaheadMs.Value > 150) { ContinuousLookaheadMs.Value = 100; } if (ClearDelayMs.Value >= 150) { ClearDelayMs.Value = 60; } if (WalkBaseFrequency.Value >= 180) { WalkBaseFrequency.Value = 25; } if (HurtDurationMinMs.Value < 1500) { HurtDurationMinMs.Value = 1500; } if (HurtDurationMaxMs.Value <= 2200) { HurtDurationMaxMs.Value = 3500; } if (HurtFrequency.Value >= 100) { HurtFrequency.Value = 60; } if (DeathDurationMs.Value <= 2200) { DeathDurationMs.Value = 4000; } if (DeathFrequency.Value >= 160) { DeathFrequency.Value = 150; } if (FootstepIntensity.Value <= 10) { FootstepIntensity.Value = 20; } if (FootstepDurationMs.Value <= 150) { FootstepDurationMs.Value = 250; } if (AutoStrengthMode.Value == DGLabPunish.AutoStrengthMode.MinimumOnly && AllowStrengthControl.Value) { AutoStrengthMode.Value = DGLabPunish.AutoStrengthMode.EventScaled; } } } [BepInPlugin("com.angelcomilk.repo.dglabpunish", "DGLabPunish", "0.6.1")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.angelcomilk.repo.dglabpunish"; public const string PluginName = "DGLabPunish"; public const string PluginVersion = "0.6.1"; internal static Plugin Instance; internal static ManualLogSource Log; private Harmony _harmony; private DGLabSocketServer _server; private StimController _stim; private ControlPanel _panel; private bool _autoArmedForCurrentBind; internal DGLabSocketServer Server => _server; internal StimController Stim => _stim; private void Awake() { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); StimProfileManager.RefreshProfiles(); _server = new DGLabSocketServer(); _stim = new StimController(_server); _panel = new ControlPanel(_server, _stim); _autoArmedForCurrentBind = false; if (ModConfig.AutoStartServer.Value) { _server.Start(); } _harmony = new Harmony("com.angelcomilk.repo.dglabpunish"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"DGLabPunish loaded."); ((BaseUnityPlugin)this).Logger.LogInfo((object)("DG-LAB QR URL: " + _server.GetQrUrl())); } private void Update() { //IL_0005: 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) if (Input.GetKeyDown(ModConfig.PanelKey.Value)) { _panel.ToggleVisible(); } if (Input.GetKeyDown(ModConfig.EmergencyStopKey.Value)) { _stim.EmergencyStop(); } if (_server != null) { _server.Tick(); if (_server.IsBound) { if (ModConfig.AutoArmOnBind.Value && !_autoArmedForCurrentBind) { ModConfig.Armed.Value = true; ((BaseUnityPlugin)this).Config.Save(); _autoArmedForCurrentBind = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"DGLabPunish auto-armed after DG-LAB binding."); } } else { _autoArmedForCurrentBind = false; } } if (_stim != null) { _stim.Tick(); } if (_panel != null) { _panel.UpdateCursor(); } } private void OnGUI() { if (_panel != null) { _panel.Draw(); } } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); _harmony = null; } if (_server != null) { _server.Stop(); } if (_panel != null) { _panel.SetVisible(visible: false); } } } internal static class RepoGuards { internal static bool IsLocalAvatar(PlayerAvatar avatar) { if ((Object)(object)avatar == (Object)null) { return false; } if (object.ReferenceEquals(avatar, PlayerAvatar.instance)) { return true; } try { return (Object)(object)avatar.photonView != (Object)null && avatar.photonView.IsMine; } catch { return false; } } internal static bool IsLocalHealth(PlayerHealth health) { PlayerAvatar instance = PlayerAvatar.instance; if ((Object)(object)instance != (Object)null) { return object.ReferenceEquals(instance.playerHealth, health); } return false; } internal static StimController Stim() { if (!((Object)(object)Plugin.Instance == (Object)null)) { return Plugin.Instance.Stim; } return null; } } [HarmonyPatch(typeof(PlayerHealth), "Hurt")] internal static class PlayerHealthHurtPatch { private static void Postfix(PlayerHealth __instance, int damage, bool savingGrace, int enemyIndex, bool hurtByHeal) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalHealth(__instance)) { stimController.TriggerHurt(damage); } } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")] internal static class PlayerAvatarDeathPatch { private static void Postfix(PlayerAvatar __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalAvatar(__instance)) { stimController.TriggerDeath(); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "FootstepLight")] internal static class PlayerAvatarVisualsFootstepLightPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootstepFromVisuals("轻"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "FootstepMedium")] internal static class PlayerAvatarVisualsFootstepMediumPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && !__instance.isMenuAvatar && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootstepFromVisuals("中"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "FootstepHeavy")] internal static class PlayerAvatarVisualsFootstepHeavyPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootstepFromVisuals("重"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "LeftFootDown")] internal static class PlayerAvatarVisualsLeftFootDownPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootDown(left: true, "左脚落地"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "RightFootDown")] internal static class PlayerAvatarVisualsRightFootDownPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootDown(left: false, "右脚落地"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "LeftFootDownSlow")] internal static class PlayerAvatarVisualsLeftFootDownSlowPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootDown(left: true, "左脚慢落地"); } } } [HarmonyPatch(typeof(PlayerAvatarVisuals), "RightFootDownSlow")] internal static class PlayerAvatarVisualsRightFootDownSlowPatch { private static void Prefix(PlayerAvatarVisuals __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null && RepoGuards.IsLocalAvatar(__instance.playerAvatar)) { stimController.TriggerFootDown(left: false, "右脚慢落地"); } } } [HarmonyPatch(typeof(PlayerAvatar), "Footstep")] internal static class PlayerAvatarFootstepPatch { private static void Postfix(PlayerAvatar __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalAvatar(__instance)) { stimController.TriggerFootstepFallback(); } } } [HarmonyPatch(typeof(PlayerAvatar), "Jump")] internal static class PlayerAvatarJumpPatch { private static void Postfix(PlayerAvatar __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalAvatar(__instance)) { stimController.TriggerJump(); } } } [HarmonyPatch(typeof(PlayerAvatar), "Land")] internal static class PlayerAvatarLandPatch { private static void Postfix(PlayerAvatar __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalAvatar(__instance)) { stimController.TriggerLand(); } } } [HarmonyPatch(typeof(PlayerAvatar), "Slide")] internal static class PlayerAvatarSlidePatch { private static void Postfix(PlayerAvatar __instance) { StimController stimController = RepoGuards.Stim(); if (stimController != null && RepoGuards.IsLocalAvatar(__instance)) { stimController.TriggerSlide(); } } } [HarmonyPatch(typeof(PlayerController), "Update")] internal static class PlayerControllerUpdatePatch { private static void Postfix(PlayerController __instance) { RepoGuards.Stim()?.UpdatePlayerMovementState(__instance); } } [HarmonyPatch(typeof(EnemyHunterAnim), "FootstepShort")] internal static class EnemyHunterFootstepShortPatch { private static void Postfix(EnemyHunterAnim __instance) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null) { stimController.TriggerEnemyFootstep(((Component)__instance).transform.position); } } } [HarmonyPatch(typeof(EnemyHunterAnim), "FootstepLong")] internal static class EnemyHunterFootstepLongPatch { private static void Postfix(EnemyHunterAnim __instance) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) StimController stimController = RepoGuards.Stim(); if (stimController != null && (Object)(object)__instance != (Object)null) { stimController.TriggerEnemyFootstep(((Component)__instance).transform.position); } } } internal sealed class StimDiagnostics { public int LeftFootsteps; public int RightFootsteps; public int AnimationFootsteps; public int FallbackFootsteps; public int DuplicateFootsteps; public int RunSupplementFootsteps; public int JumpCount; public int LandCount; public int SlideCount; public int HurtCount; public int DeathCount; public int EnemyFootsteps; public string LastEvent = ""; public string LastChannel = ""; public string LastBlockedReason = ""; public string LastCommandResult = ""; public string LastFootstepSource = ""; public string MovementState = ""; public int LastFootstepIntervalMs; public string ContinuousState = ""; public int ContinuousBaseA; public int ContinuousBaseB; public int ContinuousOverlayA; public int ContinuousOverlayB; public int ContinuousRefillCount; public int ActiveOverlays; public int EstimatedQueueMs; public int ContinuousGeneration; public int LastLeftRightDeltaMs; public bool NextFootLeft = true; } internal sealed class StimController { private readonly DGLabSocketServer _server; private readonly StimDiagnostics _diagnostics; private readonly ContinuousStimEngine _continuous; private float _lastHurt; private float _lastFootstep; private float _lastJump; private float _lastLand; private float _lastSlide; private float _lastEnemyFootstep; private float _lastDeath; private float _lastAnimationFootstep; private float _lastRunSupplement; private bool _nextFootLeft; private bool _alternateA; private bool _isMoving; private bool _isSprinting; private bool _isSliding; private float _speed; private float _lastLeftFootTime; private float _lastRightFootTime; private float _simulateMovementUntil; private bool _simulateRun; internal StimDiagnostics Diagnostics => _diagnostics; public StimController(DGLabSocketServer server) { _server = server; _diagnostics = new StimDiagnostics(); _continuous = new ContinuousStimEngine(server, _diagnostics); _lastHurt = -999f; _lastFootstep = -999f; _lastJump = -999f; _lastLand = -999f; _lastSlide = -999f; _lastEnemyFootstep = -999f; _lastDeath = -999f; _lastAnimationFootstep = -999f; _lastRunSupplement = -999f; _nextFootLeft = ModConfig.FirstFootLeft.Value; _alternateA = true; _diagnostics.NextFootLeft = _nextFootLeft; } internal void Tick() { if (_simulateMovementUntil > Time.realtimeSinceStartup) { _isMoving = true; _isSprinting = _simulateRun; _isSliding = false; _diagnostics.MovementState = (_simulateRun ? "Run(Sim)" : "Walk(Sim)"); _continuous.SetMovement(moving: true, _simulateRun, sliding: false, _simulateRun ? 5f : 2f); } _continuous.Tick(); if (ModConfig.RunCadenceSupplement.Value && _isMoving && _isSprinting && IsArmedAndBound("奔跑补拍")) { float realtimeSinceStartup = Time.realtimeSinceStartup; int num = Mathf.Max(55, ModConfig.RunSupplementIntervalMs.Value); if (!((realtimeSinceStartup - _lastRunSupplement) * 1000f < (float)num) && !((realtimeSinceStartup - _lastAnimationFootstep) * 1000f < (float)num)) { _lastRunSupplement = realtimeSinceStartup; _diagnostics.RunSupplementFootsteps++; SendFootstep("奔跑补拍", _nextFootLeft, fallback: false, advancePhase: true); } } } internal void UpdatePlayerMovementState(PlayerController controller) { if (!((Object)(object)controller == (Object)null) && object.ReferenceEquals(controller, PlayerController.instance)) { _isMoving = controller.moving; _isSprinting = controller.sprinting; _isSliding = controller.Sliding; _speed = ((Vector3)(ref controller.Velocity)).magnitude; _diagnostics.MovementState = (_isSliding ? "Slide" : (_isSprinting ? "Run" : (_isMoving ? "Walk" : "Idle"))); _continuous.SetMovement(_isMoving, _isSprinting, _isSliding, _speed); } } internal void ResetFootToConfiguredFirst() { _nextFootLeft = ModConfig.FirstFootLeft.Value; _diagnostics.NextFootLeft = _nextFootLeft; _diagnostics.LastEvent = "重置脚步"; _diagnostics.LastChannel = ""; } internal void TriggerHurt(int damage) { if (CanTrigger("受击", ModConfig.HurtEnabled.Value, _lastHurt, ModConfig.HurtCooldownMs.Value)) { _lastHurt = Time.realtimeSinceStartup; SendHurt("受击", damage); } } internal void SimulateHurt(int damage) { SendHurt("模拟受击", damage); } private void SendHurt(string eventName, int damage) { _diagnostics.HurtCount++; int max = Clamp(ModConfig.HurtMaxIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); int num = Clamp((int)((float)ModConfig.HurtMinIntensity.Value + (float)damage * ModConfig.HurtDamageMultiplier.Value), 1, max); int num2 = Clamp((int)((float)num * ClampMultiplier(ModConfig.HurtAMultiplier.Value)), 1, ModConfig.MaxWaveIntensity.Value); int num3 = Clamp((int)((float)num * ClampMultiplier(ModConfig.HurtBMultiplier.Value)), 1, ModConfig.MaxWaveIntensity.Value); int minStrength = Clamp(num2 / 4, 1, num2); int minStrength2 = Clamp(num3 / 4, 1, num3); int durationMs = Clamp(ModConfig.HurtDurationMinMs.Value + damage * 20, ModConfig.HurtDurationMinMs.Value, ModConfig.HurtDurationMaxMs.Value); if (ModConfig.ContinuousMode.Value) { _continuous.AddOverlayBoth(num2, num3, durationMs, ModConfig.HurtFrequency.Value, ModConfig.HurtEnvelope.Value, 80); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "已加入连续惩罚包络"; } else { List pulseA = WaveformEncoder.EnvelopePulse(ModConfig.HurtEnvelope.Value, ModConfig.HurtFrequency.Value, minStrength, num2, durationMs, 0.0); List pulseB = WaveformEncoder.EnvelopePulse(ModConfig.HurtEnvelope.Value, ModConfig.HurtFrequency.Value, minStrength2, num3, durationMs, 0.16); SendByMode(eventName, ModConfig.HurtChannelMode.Value, pulseA, pulseB, num2, num3); } } internal void TriggerDeath() { if (CanTrigger("死亡", ModConfig.DeathEnabled.Value, _lastDeath, ModConfig.DeathCooldownMs.Value)) { _lastDeath = Time.realtimeSinceStartup; SendDeath("死亡"); } } internal void SimulateDeath() { SendDeath("模拟死亡"); } private void SendDeath(string eventName) { _diagnostics.DeathCount++; int num = Clamp(ModConfig.DeathIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); int num2 = Clamp(ModConfig.DeathDurationMs.Value, 300, ModConfig.MaxEventDurationMs.Value); List pulseA = WaveformEncoder.EnvelopePulse(ModConfig.DeathEnvelope.Value, ModConfig.DeathFrequency.Value, num / 4, num, num2, 0.0); List pulseB = WaveformEncoder.EnvelopePulse(ModConfig.DeathEnvelope.Value, ModConfig.DeathFrequency.Value, num / 5, num, num2, 0.33); if (ModConfig.DeathClearBeforePulse.Value) { _server.ClearAll(); _continuous.Clear(); } if (ModConfig.ContinuousMode.Value) { int num3 = (ModConfig.DeathClearBeforePulse.Value ? Clamp(ModConfig.ClearDelayMs.Value, 0, 1000) : 0); if (num3 > 0 && (Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(AddDeathOverlayAfterDelay(eventName, num3, num, num2)); } else { AddDeathOverlay(eventName, num, num2); } } else { int num4 = (ModConfig.DeathClearBeforePulse.Value ? Clamp(ModConfig.ClearDelayMs.Value, 0, 1000) : 0); if (num4 > 0 && (Object)(object)Plugin.Instance != (Object)null) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(SendDeathAfterDelay(eventName, num4, pulseA, pulseB, num)); } else { SendByMode(eventName, ModConfig.DeathChannelMode.Value, pulseA, pulseB, num, num); } } } private IEnumerator AddDeathOverlayAfterDelay(string eventName, int delayMs, int strength, int duration) { yield return (object)new WaitForSecondsRealtime((float)delayMs / 1000f); AddDeathOverlay(eventName, strength, duration); } private void AddDeathOverlay(string eventName, int strength, int duration) { _continuous.AddOverlayPair(strength, strength, duration, ModConfig.DeathFrequency.Value, ModConfig.DeathEnvelope.Value, 0.0, 0.33, 100); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "已加入连续死亡波"; } private IEnumerator SendDeathAfterDelay(string eventName, int delayMs, List pulseA, List pulseB, int strength) { yield return (object)new WaitForSecondsRealtime((float)delayMs / 1000f); SendByMode(eventName, ModConfig.DeathChannelMode.Value, pulseA, pulseB, strength, strength); } internal void TriggerFootstepFromVisuals(string weight) { _diagnostics.LastFootstepSource = "声音脚步-" + weight; if (!ModConfig.FallbackFootstepSupplement.Value) { Block("声音脚步-" + weight, "已使用左右脚落地事件,声音脚步仅作诊断"); return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if ((realtimeSinceStartup - _lastAnimationFootstep) * 1000f < (float)Mathf.Max(100, ModConfig.FallbackFootstepNoAnimationMs.Value)) { Block("声音脚步-" + weight, "左右脚落地事件正常,声音脚步不参与相位"); } else { TriggerAnimationFootstep("声音脚步兜底-" + weight, null); } } internal void TriggerFootDown(bool left, string source) { TriggerAnimationFootstep(source, left); } internal void TriggerFootstepFallback() { _diagnostics.FallbackFootsteps++; _diagnostics.LastFootstepSource = "兜底"; if (!ModConfig.FallbackFootstepSupplement.Value) { Block("脚步-兜底", "兜底补发已关闭"); return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if ((realtimeSinceStartup - _lastAnimationFootstep) * 1000f < (float)Mathf.Max(100, ModConfig.FallbackFootstepNoAnimationMs.Value)) { Block("脚步-兜底", "动画脚步正常,兜底不参与相位"); } else if (CanTrigger("脚步-兜底补发", ModConfig.LocalFootstepEnabled.Value, _lastFootstep, GetFootstepMinIntervalMs())) { _lastFootstep = realtimeSinceStartup; SendFootstep("脚步-兜底补发", _nextFootLeft, fallback: true, advancePhase: false); } } internal void TriggerFootstepForced(bool left) { SendFootstep(left ? "模拟左脚" : "模拟右脚", left, fallback: false, advancePhase: false); } internal void SimulateRunFootstep(bool left) { bool isMoving = _isMoving; bool isSprinting = _isSprinting; _isMoving = true; _isSprinting = true; _diagnostics.MovementState = "Run"; SendFootstep(left ? "模拟奔跑左脚" : "模拟奔跑右脚", left, fallback: false, advancePhase: false); _isMoving = isMoving; _isSprinting = isSprinting; } internal void SimulateContinuousMovement(bool run) { _simulateRun = run; _simulateMovementUntil = Time.realtimeSinceStartup + 2f; _diagnostics.LastEvent = (run ? "模拟持续奔跑" : "模拟持续走路"); _diagnostics.LastCommandResult = "已启动 2 秒基础波形"; } private void TriggerAnimationFootstep(string eventName, bool? explicitLeft) { if (!CanTrigger(eventName, ModConfig.LocalFootstepEnabled.Value, _lastFootstep, 0)) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; int num = (int)((realtimeSinceStartup - _lastFootstep) * 1000f); _diagnostics.LastFootstepIntervalMs = num; if (num < Mathf.Max(0, ModConfig.FootstepDuplicateWindowMs.Value)) { _diagnostics.DuplicateFootsteps++; Block(eventName, "动画脚步极短重复 " + num + "ms"); return; } int footstepMinIntervalMs = GetFootstepMinIntervalMs(); if (num < footstepMinIntervalMs) { _diagnostics.DuplicateFootsteps++; Block(eventName, (_isSprinting ? "奔跑" : "走路") + "脚步节奏限制 " + num + "/" + footstepMinIntervalMs + "ms"); return; } if (!explicitLeft.HasValue && ModConfig.ResetFootOnIdle.Value && (realtimeSinceStartup - _lastFootstep) * 1000f >= (float)Mathf.Max(100, ModConfig.FootstepIdleResetMs.Value)) { _nextFootLeft = ModConfig.FirstFootLeft.Value; } _lastFootstep = realtimeSinceStartup; _lastAnimationFootstep = realtimeSinceStartup; bool flag = (explicitLeft.HasValue ? explicitLeft.Value : _nextFootLeft); _nextFootLeft = !flag; _diagnostics.NextFootLeft = _nextFootLeft; _diagnostics.AnimationFootsteps++; _diagnostics.LastFootstepSource = eventName; SendFootstep(eventName + (flag ? " 左脚" : " 右脚"), flag, fallback: false, advancePhase: false); } private void SendFootstep(string eventName, bool left, bool fallback, bool advancePhase) { if (advancePhase) { _nextFootLeft = !left; _diagnostics.NextFootLeft = _nextFootLeft; } if (left) { _diagnostics.LeftFootsteps++; } else { _diagnostics.RightFootsteps++; } if (fallback) { _diagnostics.FallbackFootsteps++; } char channel = ChannelToChar(left ? ModConfig.LeftFootChannel.Value : ModConfig.RightFootChannel.Value); bool flag = _isSprinting && _isMoving; int num = Clamp(flag ? ModConfig.RunFootstepIntensity.Value : ModConfig.FootstepIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); int durationMs = Clamp(flag ? ModConfig.RunFootstepDurationMs.Value : ModConfig.FootstepDurationMs.Value, 25, ModConfig.MaxEventDurationMs.Value); int strength = Clamp(flag ? ModConfig.RunStepBumpIntensity.Value : ModConfig.WalkStepBumpIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); int durationMs2 = Clamp(flag ? ModConfig.RunStepBumpDurationMs.Value : ModConfig.WalkStepBumpDurationMs.Value, 25, ModConfig.MaxEventDurationMs.Value); if (left) { _lastLeftFootTime = Time.realtimeSinceStartup; if (_lastRightFootTime > 0f) { _diagnostics.LastLeftRightDeltaMs = (int)Mathf.Abs((_lastLeftFootTime - _lastRightFootTime) * 1000f); } } else { _lastRightFootTime = Time.realtimeSinceStartup; if (_lastLeftFootTime > 0f) { _diagnostics.LastLeftRightDeltaMs = (int)Mathf.Abs((_lastRightFootTime - _lastLeftFootTime) * 1000f); } } if (ModConfig.ContinuousMode.Value) { _continuous.AddOverlay(channel, strength, durationMs2, ModConfig.FootstepFrequency.Value, StimEnvelopeShape.SingleTap, 0.0, 20); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = channel.ToString(); _diagnostics.LastCommandResult = "脚步 bump 已叠加"; } else { List pulse = BuildPulse(ModConfig.FootstepWaveShape.Value, ModConfig.FootstepFrequency.Value, num / 3, num, durationMs); SendChannel(eventName, channel, pulse, num); } } internal void TriggerJump() { if (CanTrigger("跳跃", ModConfig.LandJumpEnabled.Value && ModConfig.JumpEnabled.Value, _lastJump, ModConfig.JumpCooldownMs.Value)) { _lastJump = Time.realtimeSinceStartup; SendJump("跳跃"); } } internal void SimulateJump() { SendJump("模拟跳跃"); } private void SendJump(string eventName) { _diagnostics.JumpCount++; if (ModConfig.ContinuousMode.Value) { _continuous.AddOverlayBoth(ModConfig.JumpIntensity.Value, ModConfig.JumpIntensity.Value, ModConfig.JumpDurationMs.Value, ModConfig.JumpFrequency.Value, StimEnvelopeShape.DoubleTap, 40); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "跳跃包络已叠加"; } else { SendAction(eventName, ModConfig.JumpChannelMode.Value, ModConfig.JumpFrequency.Value, ModConfig.JumpIntensity.Value, ModConfig.JumpDurationMs.Value, StimEnvelopeShape.DoubleTap); } } internal void TriggerLand() { if (CanTrigger("落地", ModConfig.LandJumpEnabled.Value && ModConfig.LandEnabled.Value, _lastLand, ModConfig.LandCooldownMs.Value)) { _lastLand = Time.realtimeSinceStartup; SendLand("落地"); } } internal void SimulateLand() { SendLand("模拟落地"); } private void SendLand(string eventName) { _diagnostics.LandCount++; if (ModConfig.ContinuousMode.Value) { _continuous.AddOverlayBoth(ModConfig.LandIntensity.Value, ModConfig.LandIntensity.Value, ModConfig.LandDurationMs.Value, ModConfig.LandFrequency.Value, ModConfig.LandEnvelope.Value, 45); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "落地包络已叠加"; } else { SendAction(eventName, ModConfig.LandChannelMode.Value, ModConfig.LandFrequency.Value, ModConfig.LandIntensity.Value, ModConfig.LandDurationMs.Value, ModConfig.LandEnvelope.Value); } } internal void TriggerSlide() { if (CanTrigger("滑行", ModConfig.LandJumpEnabled.Value && ModConfig.SlideEnabled.Value, _lastSlide, ModConfig.SlideCooldownMs.Value)) { _lastSlide = Time.realtimeSinceStartup; SendSlide("滑行"); } } internal void SimulateSlide() { SendSlide("模拟滑行"); } private void SendSlide(string eventName) { _diagnostics.SlideCount++; if (ModConfig.ContinuousMode.Value) { _continuous.AddOverlayBoth(ModConfig.SlideIntensity.Value, ModConfig.SlideIntensity.Value, ModConfig.SlideDurationMs.Value, ModConfig.SlideFrequency.Value, ModConfig.SlideEnvelope.Value, 35); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "滑行包络已叠加"; } else { SendAction(eventName, ModConfig.SlideChannelMode.Value, ModConfig.SlideFrequency.Value, ModConfig.SlideIntensity.Value, ModConfig.SlideDurationMs.Value, ModConfig.SlideEnvelope.Value); } } internal void TriggerMovement(string kind) { switch (kind) { case "jump": TriggerJump(); break; case "land": TriggerLand(); break; case "slide": TriggerSlide(); break; } } internal void TriggerEnemyFootstep(Vector3 sourcePosition) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnemyFootstepEnabled.Value || !IsArmedAndBound("敌人脚步")) { return; } PlayerAvatar instance = PlayerAvatar.instance; if ((Object)(object)instance == (Object)null) { Block("敌人脚步", "找不到本地玩家"); return; } float num = Mathf.Max(0.1f, ModConfig.EnemyFootstepRange.Value); if (Vector3.Distance(((Component)instance).transform.position, sourcePosition) > num) { Block("敌人脚步", "超过触发距离"); } else if (CanTrigger("敌人脚步", eventEnabled: true, _lastEnemyFootstep, ModConfig.FootstepCooldownMs.Value)) { _lastEnemyFootstep = Time.realtimeSinceStartup; _diagnostics.EnemyFootsteps++; int num2 = Clamp(ModConfig.EnemyFootstepIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); List pulse = WaveformEncoder.SmoothPulse(ModConfig.FootstepFrequency.Value, num2, ModConfig.FootstepDurationMs.Value); SendChannel("敌人脚步", 'B', pulse, num2); } } internal void SendTest(char channel) { int num = Clamp(ModConfig.TestIntensity.Value, 1, ModConfig.MaxWaveIntensity.Value); List pulse = WaveformEncoder.SinPulse(300, num / 3, num, 350); SendChannel("测试 " + channel, channel, pulse, num); } internal void EmergencyStop() { _server.ClearAll(); _continuous.Clear(); _server.SetStrength(1, 0); _server.SetStrength(2, 0); _diagnostics.LastEvent = "紧急停止"; _diagnostics.LastChannel = "A+B"; _diagnostics.LastCommandResult = "已发送 clear 与强度归零"; Plugin.Log.LogWarning((object)"DG-LAB emergency stop sent."); } private void SendAction(string eventName, StimChannelMode mode, int frequency, int intensity, int durationMs, StimEnvelopeShape shape) { int num = Clamp(intensity, 1, ModConfig.MaxWaveIntensity.Value); int durationMs2 = Clamp(durationMs, 25, ModConfig.MaxEventDurationMs.Value); List pulseA = WaveformEncoder.EnvelopePulse(shape, frequency, num / 3, num, durationMs2, 0.0); List pulseB = WaveformEncoder.EnvelopePulse(shape, frequency, num / 4, num, durationMs2, 0.18); SendByMode(eventName, mode, pulseA, pulseB, num, num); } private void SendByMode(string eventName, StimChannelMode mode, List pulseA, List pulseB, int strengthA, int strengthB) { switch (mode) { case StimChannelMode.A: SendChannel(eventName, 'A', pulseA, strengthA); break; case StimChannelMode.B: SendChannel(eventName, 'B', pulseB, strengthB); break; case StimChannelMode.Alternate: { char c = (_alternateA ? 'A' : 'B'); _alternateA = !_alternateA; SendChannel(eventName, c, (c == 'A') ? pulseA : pulseB, (c == 'A') ? strengthA : strengthB); break; } default: SendChannel(eventName, 'A', pulseA, strengthA); SendChannel(eventName, 'B', pulseB, strengthB); break; } } private void SendChannel(string eventName, char channel, List pulse, int eventStrength) { PrepareStrength(channel, eventStrength); bool flag = _server.SendPulse(channel, pulse); _diagnostics.LastEvent = eventName; _diagnostics.LastChannel = channel.ToString(); _diagnostics.LastCommandResult = (flag ? "已发送" : "发送失败"); if (!flag) { _diagnostics.LastBlockedReason = "发送失败,可能未绑定或已断线"; } } private void PrepareStrength(char channel, int eventStrength) { AutoStrengthMode autoStrengthMode = ModConfig.AutoStrengthMode.Value; if (ModConfig.AllowStrengthControl.Value) { autoStrengthMode = AutoStrengthMode.EventScaled; } if (autoStrengthMode != 0) { StrengthState strength = _server.Strength; int channel2 = ((channel == 'A') ? 1 : 2); int num = ((channel == 'A') ? strength.ACurrent : strength.BCurrent); int num2 = ((channel == 'A') ? strength.AMax : strength.BMax); int num3 = Clamp(ModConfig.MinimumChannelStrength.Value, 0, 200); int num4 = ((autoStrengthMode == AutoStrengthMode.MinimumOnly) ? Mathf.Max(num, num3) : Mathf.Max(num3, eventStrength)); if (num2 > 0) { num4 = Mathf.Min(num4, num2); } num4 = Clamp(num4, 0, 200); _server.SetStrength(channel2, num4); } } private bool CanTrigger(string eventName, bool eventEnabled, float lastTime, int cooldownMs) { if (!eventEnabled) { Block(eventName, "事件已关闭"); return false; } if (!IsArmedAndBound(eventName)) { return false; } float num = (Time.realtimeSinceStartup - lastTime) * 1000f; if (num < (float)Mathf.Max(0, cooldownMs)) { Block(eventName, "冷却中 " + (int)num + "/" + cooldownMs + "ms"); return false; } return true; } private int GetFootstepMinIntervalMs() { return Mathf.Max(Mathf.Max(0, ModConfig.FootstepDuplicateWindowMs.Value), (_isSprinting && _isMoving) ? ModConfig.RunFootstepMinIntervalMs.Value : ModConfig.WalkFootstepMinIntervalMs.Value); } private bool IsArmedAndBound(string eventName) { if (!ModConfig.Armed.Value) { Block(eventName, "触发未启用"); return false; } if (_server == null || !_server.IsBound) { Block(eventName, "DG-LAB 未绑定"); return false; } return true; } private void Block(string eventName, string reason) { _diagnostics.LastEvent = eventName; _diagnostics.LastBlockedReason = reason; _diagnostics.LastCommandResult = "未发送"; } private static List BuildPulse(StimWaveShape shape, int frequency, int startStrength, int endStrength, int durationMs) { int num = Clamp(startStrength, 0, 100); int num2 = Clamp(endStrength, 0, 100); return shape switch { StimWaveShape.Smooth => WaveformEncoder.SmoothPulse(frequency, num2, durationMs), StimWaveShape.Sin => WaveformEncoder.SinPulse(frequency, Mathf.Min(num, num2), Mathf.Max(num, num2), durationMs), _ => WaveformEncoder.GradientPulse(frequency, num, num2, durationMs), }; } private static char ChannelToChar(StimChannel channel) { if (channel != 0) { return 'B'; } return 'A'; } private static float ClampMultiplier(float value) { if (value < 0f) { return 0f; } if (value > 2f) { return 2f; } return value; } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } } internal sealed class StimProfile { public string name; public string version; public int maxWaveIntensity; public ContinuousStatePreset continuous; public List waves; public List events; } internal sealed class ContinuousStatePreset { public int sendIntervalMs; public int lookaheadMs; public int fadeOutMs; public int maxEventDurationMs; public int maxQueuedPulseItems; public int minimumChannelStrength; public int walkBaseIntensity; public int walkBaseFrequency; public string walkBaseShape; public int walkStepBumpIntensity; public int walkStepBumpDurationMs; public int runBaseIntensity; public int runBaseFrequency; public string runBaseShape; public int runStepBumpIntensity; public int runStepBumpDurationMs; public int slideBaseIntensity; public int slideBaseFrequency; public int hurtMinIntensity; public int hurtMaxIntensity; public int hurtDurationMinMs; public int hurtDurationMaxMs; public int hurtFrequency; public int deathIntensity; public int deathDurationMs; public int deathFrequency; } internal sealed class WavePreset { public string name; public string displayName; public string type; public string shape; public string channel; public int frequency; public int minStrength; public int maxStrength; public int durationMs; public double phase; public List hex; public List frames; } internal sealed class EventRule { public string eventName; public string waveName; public string channelMode; public float strengthMultiplier; public bool clearBeforeSend; public bool continuousState; public string note; } internal static class StimProfileManager { internal static readonly List Profiles = new List(); internal static StimProfile CurrentProfile; internal static string LastMessage = ""; internal static string ProfilesDirectory => Path.Combine(Paths.ConfigPath, "DGLabPunish", "profiles"); internal static void RefreshProfiles() { Profiles.Clear(); AddBuiltInProfiles(); EnsureProfilesDirectory(); try { LoadProfileFiles("*.json"); LoadProfileFiles("*.pulse"); } catch (Exception ex) { LastMessage = "读取导入目录失败:" + ex.Message; } if (CurrentProfile == null && Profiles.Count > 0) { CurrentProfile = Profiles[0]; } if (string.IsNullOrEmpty(LastMessage)) { LastMessage = "已加载 " + Profiles.Count + " 个波形包。"; } } internal static bool ApplyProfile(StimProfile profile) { if (profile == null) { LastMessage = "没有可应用的波形包。"; return false; } CurrentProfile = profile; ContinuousStatePreset continuous = profile.continuous; if (continuous != null) { ModConfig.ContinuousMode.Value = true; ModConfig.AutoStrengthMode.Value = AutoStrengthMode.EventScaled; ModConfig.MinimumChannelStrength.Value = Clamp(continuous.minimumChannelStrength, 0, 200); ModConfig.ContinuousSendIntervalMs.Value = Clamp(continuous.sendIntervalMs, 50, 2000); ModConfig.ContinuousLookaheadMs.Value = Clamp(continuous.lookaheadMs, 100, 5000); ModConfig.ContinuousFadeOutMs.Value = Clamp(continuous.fadeOutMs, 0, 3000); ModConfig.MaxEventDurationMs.Value = Clamp(continuous.maxEventDurationMs, 100, 10000); ModConfig.MaxQueuedPulseItems.Value = Clamp(continuous.maxQueuedPulseItems, 1, 100); ModConfig.WalkBaseIntensity.Value = Clamp(continuous.walkBaseIntensity, 0, 100); ModConfig.WalkBaseFrequency.Value = Clamp(continuous.walkBaseFrequency, 10, 1000); ModConfig.WalkBaseShape.Value = ParseEnvelope(continuous.walkBaseShape, ModConfig.WalkBaseShape.Value); ModConfig.WalkStepBumpIntensity.Value = Clamp(continuous.walkStepBumpIntensity, 0, 100); ModConfig.WalkStepBumpDurationMs.Value = Clamp(continuous.walkStepBumpDurationMs, 25, 2000); ModConfig.RunBaseIntensity.Value = Clamp(continuous.runBaseIntensity, 0, 100); ModConfig.RunBaseFrequency.Value = Clamp(continuous.runBaseFrequency, 10, 1000); ModConfig.RunBaseShape.Value = ParseEnvelope(continuous.runBaseShape, ModConfig.RunBaseShape.Value); ModConfig.RunStepBumpIntensity.Value = Clamp(continuous.runStepBumpIntensity, 0, 100); ModConfig.RunStepBumpDurationMs.Value = Clamp(continuous.runStepBumpDurationMs, 25, 2000); ModConfig.SlideBaseIntensity.Value = Clamp(continuous.slideBaseIntensity, 0, 100); ModConfig.SlideBaseFrequency.Value = Clamp(continuous.slideBaseFrequency, 10, 1000); ModConfig.HurtMinIntensity.Value = Clamp(continuous.hurtMinIntensity, 0, 100); ModConfig.HurtMaxIntensity.Value = Clamp(continuous.hurtMaxIntensity, 0, 100); ModConfig.HurtDurationMinMs.Value = Clamp(continuous.hurtDurationMinMs, 100, 10000); ModConfig.HurtDurationMaxMs.Value = Clamp(continuous.hurtDurationMaxMs, ModConfig.HurtDurationMinMs.Value, 10000); if (continuous.hurtFrequency > 0) { ModConfig.HurtFrequency.Value = Clamp(continuous.hurtFrequency, 10, 1000); } ModConfig.DeathIntensity.Value = Clamp(continuous.deathIntensity, 0, 100); ModConfig.DeathDurationMs.Value = Clamp(continuous.deathDurationMs, 100, 10000); if (continuous.deathFrequency > 0) { ModConfig.DeathFrequency.Value = Clamp(continuous.deathFrequency, 10, 1000); } } if (profile.maxWaveIntensity > 0) { ModConfig.MaxWaveIntensity.Value = Clamp(profile.maxWaveIntensity, 0, 100); } SaveConfig(); LastMessage = "已应用波形包:" + DisplayName(profile); return true; } internal static string ExportCurrentConfig() { EnsureProfilesDirectory(); StimProfile stimProfile = CaptureCurrentProfile(); string path = "DGLabPunishProfile-export-" + DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + ".json"; string text = Path.Combine(ProfilesDirectory, path); File.WriteAllText(text, JsonConvert.SerializeObject((object)stimProfile, (Formatting)1), Encoding.UTF8); LastMessage = "已导出:" + text; RefreshProfiles(); return text; } internal static bool SendWave(DGLabSocketServer server, WavePreset wave, char channel) { if (server == null || wave == null) { LastMessage = "没有可发送的波形。"; return false; } string error; List list = BuildWave(wave, out error); if (list == null) { LastMessage = error; return false; } bool flag = server.SendPulse(channel, list); LastMessage = (flag ? ("已发送 " + DisplayName(wave) + " 到 " + channel + " 通道。") : "发送失败:App 尚未绑定或连接不可用。"); return flag; } internal static List BuildWave(WavePreset wave, out string error) { error = ""; if (wave == null) { error = "波形为空。"; return null; } if (IsHexWave(wave)) { List list = new List(); for (int i = 0; i < wave.hex.Count && i < 100; i++) { string text = (wave.hex[i] ?? "").Trim(); if (!IsPulseHex(text)) { error = "HEX 波形格式错误:" + text; return null; } list.Add(text.ToUpperInvariant()); } if (list.Count == 0) { error = "HEX 波形为空。"; return null; } string text2 = WaveformEncoder.ToJsonArray(list, list.Count); if (text2.Length > 1950) { error = "HEX 波形过长,单次指令超过 1950 字符。"; return null; } return list; } if (wave.frames != null && wave.frames.Count > 0) { List list2 = new List(); List list3 = new List(); int num = Math.Min(400, wave.frames.Count); for (int j = 0; j < num; j++) { int[] array = wave.frames[j]; if (array == null || array.Length < 2) { error = "frames 波形包含非法帧。"; return null; } list2.Add(Clamp(array[0], 10, 1000)); list3.Add(Clamp(array[1], 0, 100)); } List list4 = WaveformEncoder.Pulse(list2, list3); string text3 = WaveformEncoder.ToJsonArray(list4, list4.Count); if (text3.Length > 1950) { error = "frames 波形过长,单次指令超过 1950 字符。"; return null; } return list4; } int frequency = Clamp((wave.frequency <= 0) ? 220 : wave.frequency, 10, 1000); int num2 = Clamp(wave.minStrength, 0, 100); int num3 = Clamp((wave.maxStrength <= 0) ? ModConfig.TestIntensity.Value : wave.maxStrength, num2, 100); int durationMs = Clamp((wave.durationMs <= 0) ? 400 : wave.durationMs, 25, Math.Max(25, ModConfig.MaxEventDurationMs.Value)); switch ((wave.type ?? "").Trim().ToLowerInvariant()) { case "smooth": return WaveformEncoder.SmoothPulse(frequency, num3, durationMs); case "gradient": return WaveformEncoder.GradientPulse(frequency, num2, num3, durationMs); case "sin": case "sine": return WaveformEncoder.SinPulse(frequency, num2, num3, durationMs); default: { StimEnvelopeShape shape = ParseEnvelope(wave.shape, StimEnvelopeShape.RampUpHoldDown); return WaveformEncoder.EnvelopePulse(shape, frequency, num2, num3, durationMs, wave.phase); } } } internal static string DisplayName(StimProfile profile) { if (profile == null) { return ""; } if (!string.IsNullOrEmpty(profile.name)) { return profile.name; } return "未命名波形包"; } internal static string DisplayName(WavePreset wave) { if (wave == null) { return ""; } if (!string.IsNullOrEmpty(wave.displayName)) { return wave.displayName; } if (!string.IsNullOrEmpty(wave.name)) { return wave.name; } return "未命名波形"; } internal static WavePreset FindWave(StimProfile profile, string name) { if (profile == null || profile.waves == null || string.IsNullOrEmpty(name)) { return null; } for (int i = 0; i < profile.waves.Count; i++) { WavePreset wavePreset = profile.waves[i]; if (wavePreset != null && string.Equals(wavePreset.name, name, StringComparison.OrdinalIgnoreCase)) { return wavePreset; } } return null; } private static void LoadProfileFiles(string pattern) { string[] files = Directory.GetFiles(ProfilesDirectory, pattern); for (int i = 0; i < files.Length; i++) { TryLoadProfileFile(files[i]); } } private static void TryLoadProfileFile(string path) { try { string a = Path.GetExtension(path) ?? ""; if (string.Equals(a, ".pulse", StringComparison.OrdinalIgnoreCase)) { Profiles.Add(CreatePulseImportProfile(path)); return; } string text = File.ReadAllText(path, Encoding.UTF8); string text2 = (text ?? "").Trim(); StimProfile stimProfile; if (text2.StartsWith("[")) { List hex = JsonConvert.DeserializeObject>(text2); stimProfile = CreateHexImportProfile(Path.GetFileNameWithoutExtension(path), hex); } else { stimProfile = JsonConvert.DeserializeObject(text); } if (stimProfile == null) { LastMessage = "导入失败,JSON 为空:" + Path.GetFileName(path); return; } ValidateProfile(stimProfile); if (string.IsNullOrEmpty(stimProfile.name)) { stimProfile.name = Path.GetFileNameWithoutExtension(path); } Profiles.Add(stimProfile); } catch (Exception ex) { LastMessage = "导入失败 " + Path.GetFileName(path) + ":" + ex.Message; } } private static void ValidateProfile(StimProfile profile) { if (profile.waves == null) { profile.waves = new List(); } for (int i = 0; i < profile.waves.Count; i++) { WavePreset wavePreset = profile.waves[i]; if (wavePreset == null) { continue; } if (IsHexWave(wavePreset)) { if (wavePreset.hex.Count > 100) { throw new InvalidDataException("波形 " + DisplayName(wavePreset) + " 超过 100 条 HEX。"); } for (int j = 0; j < wavePreset.hex.Count; j++) { if (!IsPulseHex((wavePreset.hex[j] ?? "").Trim())) { throw new InvalidDataException("波形 " + DisplayName(wavePreset) + " 包含非法 HEX。"); } } } if (wavePreset.frames == null || wavePreset.frames.Count <= 0) { continue; } if (wavePreset.frames.Count > 400) { throw new InvalidDataException("波形 " + DisplayName(wavePreset) + " 超过 400 个 frames。"); } for (int k = 0; k < wavePreset.frames.Count; k++) { int[] array = wavePreset.frames[k]; if (array == null || array.Length < 2) { throw new InvalidDataException("波形 " + DisplayName(wavePreset) + " 包含非法 frame。"); } } } if (profile.events == null) { profile.events = new List(); } } private static bool IsHexWave(WavePreset wave) { if (wave != null && wave.hex != null) { return wave.hex.Count > 0; } return false; } private static StimProfile CreateHexImportProfile(string name, List hex) { StimProfile stimProfile = new StimProfile(); stimProfile.name = (string.IsNullOrEmpty(name) ? "ImportedHex" : name); stimProfile.version = "hex"; stimProfile.maxWaveIntensity = 100; stimProfile.continuous = null; stimProfile.waves = new List(); WavePreset wavePreset = new WavePreset(); wavePreset.name = stimProfile.name + ".Hex"; wavePreset.displayName = "导入 HEX 波形"; wavePreset.type = "hex"; wavePreset.shape = "Raw"; wavePreset.channel = "Both"; wavePreset.frequency = 0; wavePreset.minStrength = 0; wavePreset.maxStrength = 0; wavePreset.durationMs = ((hex != null) ? (hex.Count * 100) : 0); wavePreset.phase = 0.0; wavePreset.hex = hex ?? new List(); stimProfile.waves.Add(wavePreset); stimProfile.events = new List(); return stimProfile; } private static StimProfile CreatePulseImportProfile(string path) { string text = File.ReadAllText(path, Encoding.UTF8); List list = new List(); List list2 = new List(); string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string text2 = StripComment(array[i]).Trim(); if (string.IsNullOrEmpty(text2)) { continue; } if (IsPulseHex(text2)) { list.Add(text2.ToUpperInvariant()); continue; } List list3 = ExtractIntegers(text2); if (list3.Count >= 2) { list2.Add(new int[2] { Clamp(list3[0], 10, 1000), Clamp(list3[1], 0, 100) }); } } if (list.Count == 0 && list2.Count == 0) { throw new InvalidDataException("没有找到可导入的 HEX 或 frequency/strength 帧。"); } StimProfile stimProfile = new StimProfile(); stimProfile.name = Path.GetFileNameWithoutExtension(path); stimProfile.version = "pulse"; stimProfile.maxWaveIntensity = 100; stimProfile.continuous = null; stimProfile.waves = new List(); stimProfile.events = new List(); if (list.Count > 0) { WavePreset wavePreset = new WavePreset(); wavePreset.name = stimProfile.name + ".Hex"; wavePreset.displayName = "导入 .pulse HEX"; wavePreset.type = "hex"; wavePreset.shape = "Raw"; wavePreset.channel = "Both"; wavePreset.durationMs = Math.Min(list.Count, 100) * 100; wavePreset.hex = list; stimProfile.waves.Add(wavePreset); } if (list2.Count > 0) { WavePreset wavePreset2 = new WavePreset(); wavePreset2.name = stimProfile.name + ".Frames"; wavePreset2.displayName = "导入 .pulse 帧"; wavePreset2.type = "frames"; wavePreset2.shape = "Raw"; wavePreset2.channel = "Both"; wavePreset2.durationMs = Math.Min(list2.Count, 400) * 25; wavePreset2.frames = list2; wavePreset2.hex = new List(); stimProfile.waves.Add(wavePreset2); } return stimProfile; } private static string StripComment(string line) { string text = line ?? ""; int num = text.IndexOf('#'); if (num >= 0) { text = text.Substring(0, num); } int num2 = text.IndexOf("//", StringComparison.Ordinal); if (num2 >= 0) { text = text.Substring(0, num2); } return text; } private static List ExtractIntegers(string line) { List list = new List(); StringBuilder stringBuilder = new StringBuilder(); foreach (char c in line) { if ((c >= '0' && c <= '9') || c == '-') { stringBuilder.Append(c); } else { FlushInteger(stringBuilder, list); } } FlushInteger(stringBuilder, list); return list; } private static void FlushInteger(StringBuilder current, List values) { if (current.Length != 0) { if (int.TryParse(current.ToString(), out var result)) { values.Add(result); } current.Length = 0; } } private static bool IsPulseHex(string value) { if (string.IsNullOrEmpty(value) || value.Length != 16) { return false; } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { return false; } } return true; } private static void EnsureProfilesDirectory() { if (!Directory.Exists(ProfilesDirectory)) { Directory.CreateDirectory(ProfilesDirectory); } } private static void AddBuiltInProfiles() { Profiles.Add(CreateProfile("StandardGame", 70, 11, 18, 17, 24, 65, 3500, 62, 4000, 100, 220)); Profiles.Add(CreateProfile("ComfortContinuous", 40, 10, 8, 12, 10, 40, 3000, 40, 3500, 80, 120)); Profiles.Add(CreateProfile("StrongPunish", 95, 14, 24, 22, 32, 90, 4500, 88, 5000, 90, 210)); Profiles.Add(CreateProfile("DebugSync", 90, 8, 34, 10, 42, 75, 3500, 72, 4000, 90, 200)); } private static StimProfile CreateProfile(string name, int maxWave, int walkBase, int walkBump, int runBase, int runBump, int hurtMax, int hurtMs, int deathIntensity, int deathMs, int sendInterval, int lookahead) { StimProfile stimProfile = new StimProfile(); stimProfile.name = name; stimProfile.version = "1"; stimProfile.maxWaveIntensity = maxWave; stimProfile.continuous = new ContinuousStatePreset(); stimProfile.continuous.sendIntervalMs = sendInterval; stimProfile.continuous.lookaheadMs = lookahead; stimProfile.continuous.fadeOutMs = 400; stimProfile.continuous.maxEventDurationMs = Math.Max(deathMs, hurtMs); stimProfile.continuous.maxQueuedPulseItems = 30; stimProfile.continuous.minimumChannelStrength = ((name == "ComfortContinuous") ? 10 : Math.Max(30, walkBase + 8)); stimProfile.continuous.walkBaseIntensity = walkBase; stimProfile.continuous.walkBaseFrequency = 25; stimProfile.continuous.walkBaseShape = "Tremor"; stimProfile.continuous.walkStepBumpIntensity = walkBump; stimProfile.continuous.walkStepBumpDurationMs = 170; stimProfile.continuous.runBaseIntensity = runBase; stimProfile.continuous.runBaseFrequency = 280; stimProfile.continuous.runBaseShape = "SawBurst"; stimProfile.continuous.runStepBumpIntensity = runBump; stimProfile.continuous.runStepBumpDurationMs = 120; stimProfile.continuous.slideBaseIntensity = Math.Max(16, runBase); stimProfile.continuous.slideBaseFrequency = 260; stimProfile.continuous.hurtMinIntensity = ((name == "ComfortContinuous") ? 10 : Math.Max(30, hurtMax / 3)); stimProfile.continuous.hurtMaxIntensity = hurtMax; stimProfile.continuous.hurtDurationMinMs = Math.Max(800, hurtMs / 2); stimProfile.continuous.hurtDurationMaxMs = hurtMs; stimProfile.continuous.hurtFrequency = 60; stimProfile.continuous.deathIntensity = deathIntensity; stimProfile.continuous.deathDurationMs = deathMs; stimProfile.continuous.deathFrequency = 150; stimProfile.waves = CreateCommonWaves(name, walkBase, walkBump, runBase, runBump, hurtMax, hurtMs, deathIntensity, deathMs); stimProfile.events = CreateCommonEvents(); return stimProfile; } private static List CreateCommonWaves(string pack, int walkBase, int walkBump, int runBase, int runBump, int hurtMax, int hurtMs, int deathIntensity, int deathMs) { List list = new List(); list.Add(Wave(pack + ".WalkBase", "走路基础波", "envelope", "Tremor", "Both", 25, Math.Max(2, walkBase / 2), walkBase, 800, 0.0)); list.Add(Wave(pack + ".WalkStepBump", "走路脚步叠加", "envelope", "SingleTap", "Alternate", 240, 0, walkBump, 180, 0.0)); list.Add(Wave(pack + ".RunBase", "奔跑基础波", "envelope", "SawBurst", "Both", 280, Math.Max(4, runBase / 2), runBase, 700, 0.12)); list.Add(Wave(pack + ".RunStepBump", "奔跑脚步叠加", "envelope", "SingleTap", "Alternate", 320, 0, runBump, 130, 0.0)); int minStrength = ((pack == "ComfortContinuous") ? 10 : Math.Max(30, hurtMax / 3)); int minStrength2 = ((pack == "ComfortContinuous") ? 10 : Math.Max(30, deathIntensity / 4)); list.Add(Wave(pack + ".HurtPunish", "受击长惩罚", "envelope", "RampUpHoldDown", "Both", 60, minStrength, hurtMax, hurtMs, 0.0)); list.Add(Wave(pack + ".DeathWave", "死亡长波", "envelope", "DeathWave", "Both", 150, minStrength2, deathIntensity, deathMs, 0.17)); list.Add(Wave(pack + ".SlideTremor", "滑行持续颤动", "envelope", "Tremor", "Both", 260, Math.Max(8, runBase / 2), Math.Max(20, runBase + 8), 900, 0.33)); list.Add(Wave(pack + ".LandImpact", "落地冲击衰减", "envelope", "SingleTap", "Both", 320, 0, Math.Max(24, walkBump + 8), 450, 0.0)); list.Add(Wave(pack + ".EnemyHeartbeat", "敌人靠近提示", "envelope", "DoubleTap", "Both", 180, 4, 18, 900, 0.0)); return list; } private static WavePreset Wave(string name, string displayName, string type, string shape, string channel, int frequency, int minStrength, int maxStrength, int durationMs, double phase) { WavePreset wavePreset = new WavePreset(); wavePreset.name = name; wavePreset.displayName = displayName; wavePreset.type = type; wavePreset.shape = shape; wavePreset.channel = channel; wavePreset.frequency = frequency; wavePreset.minStrength = minStrength; wavePreset.maxStrength = maxStrength; wavePreset.durationMs = durationMs; wavePreset.phase = phase; wavePreset.hex = new List(); return wavePreset; } private static List CreateCommonEvents() { List list = new List(); list.Add(Event("Walk", ".WalkBase", "Both", 1f, clear: false, continuous: true, "移动期间持续基础波,脚步只做通道叠加。")); list.Add(Event("Run", ".RunBase", "Both", 1f, clear: false, continuous: true, "奔跑基础波更密,脚步 bump 更尖。")); list.Add(Event("Hurt", ".HurtPunish", "Both", 1f, clear: false, continuous: true, "受击进入更长时间的高频高强度惩罚基底。")); list.Add(Event("Death", ".DeathWave", "Both", 1f, clear: true, continuous: true, "死亡清空旧队列后发送长波。")); list.Add(Event("Slide", ".SlideTremor", "Both", 1f, clear: false, continuous: true, "滑行期间持续颤动。")); list.Add(Event("Land", ".LandImpact", "Both", 1f, clear: false, continuous: false, "落地瞬间冲击后衰减。")); return list; } private static EventRule Event(string eventName, string waveName, string channelMode, float multiplier, bool clear, bool continuous, string note) { EventRule eventRule = new EventRule(); eventRule.eventName = eventName; eventRule.waveName = waveName; eventRule.channelMode = channelMode; eventRule.strengthMultiplier = multiplier; eventRule.clearBeforeSend = clear; eventRule.continuousState = continuous; eventRule.note = note; return eventRule; } private static StimProfile CaptureCurrentProfile() { StimProfile stimProfile = new StimProfile(); stimProfile.name = "Exported"; stimProfile.version = "1"; stimProfile.maxWaveIntensity = ModConfig.MaxWaveIntensity.Value; stimProfile.continuous = new ContinuousStatePreset(); stimProfile.continuous.sendIntervalMs = ModConfig.ContinuousSendIntervalMs.Value; stimProfile.continuous.lookaheadMs = ModConfig.ContinuousLookaheadMs.Value; stimProfile.continuous.fadeOutMs = ModConfig.ContinuousFadeOutMs.Value; stimProfile.continuous.maxEventDurationMs = ModConfig.MaxEventDurationMs.Value; stimProfile.continuous.maxQueuedPulseItems = ModConfig.MaxQueuedPulseItems.Value; stimProfile.continuous.minimumChannelStrength = ModConfig.MinimumChannelStrength.Value; stimProfile.continuous.walkBaseIntensity = ModConfig.WalkBaseIntensity.Value; stimProfile.continuous.walkBaseFrequency = ModConfig.WalkBaseFrequency.Value; stimProfile.continuous.walkBaseShape = ModConfig.WalkBaseShape.Value.ToString(); stimProfile.continuous.walkStepBumpIntensity = ModConfig.WalkStepBumpIntensity.Value; stimProfile.continuous.walkStepBumpDurationMs = ModConfig.WalkStepBumpDurationMs.Value; stimProfile.continuous.runBaseIntensity = ModConfig.RunBaseIntensity.Value; stimProfile.continuous.runBaseFrequency = ModConfig.RunBaseFrequency.Value; stimProfile.continuous.runBaseShape = ModConfig.RunBaseShape.Value.ToString(); stimProfile.continuous.runStepBumpIntensity = ModConfig.RunStepBumpIntensity.Value; stimProfile.continuous.runStepBumpDurationMs = ModConfig.RunStepBumpDurationMs.Value; stimProfile.continuous.slideBaseIntensity = ModConfig.SlideBaseIntensity.Value; stimProfile.continuous.slideBaseFrequency = ModConfig.SlideBaseFrequency.Value; stimProfile.continuous.hurtMinIntensity = ModConfig.HurtMinIntensity.Value; stimProfile.continuous.hurtMaxIntensity = ModConfig.HurtMaxIntensity.Value; stimProfile.continuous.hurtDurationMinMs = ModConfig.HurtDurationMinMs.Value; stimProfile.continuous.hurtDurationMaxMs = ModConfig.HurtDurationMaxMs.Value; stimProfile.continuous.hurtFrequency = ModConfig.HurtFrequency.Value; stimProfile.continuous.deathIntensity = ModConfig.DeathIntensity.Value; stimProfile.continuous.deathDurationMs = ModConfig.DeathDurationMs.Value; stimProfile.continuous.deathFrequency = ModConfig.DeathFrequency.Value; stimProfile.waves = CreateCommonWaves("Exported", ModConfig.WalkBaseIntensity.Value, ModConfig.WalkStepBumpIntensity.Value, ModConfig.RunBaseIntensity.Value, ModConfig.RunStepBumpIntensity.Value, ModConfig.HurtMaxIntensity.Value, ModConfig.HurtDurationMaxMs.Value, ModConfig.DeathIntensity.Value, ModConfig.DeathDurationMs.Value); stimProfile.events = CreateCommonEvents(); return stimProfile; } private static StimEnvelopeShape ParseEnvelope(string value, StimEnvelopeShape fallback) { try { if (string.IsNullOrEmpty(value)) { return fallback; } return (StimEnvelopeShape)Enum.Parse(typeof(StimEnvelopeShape), value, ignoreCase: true); } catch { return fallback; } } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } private static void SaveConfig() { try { if ((Object)(object)Plugin.Instance != (Object)null) { ((BaseUnityPlugin)Plugin.Instance).Config.Save(); } } catch { } } } internal static class WaveformEncoder { internal static List SmoothPulse(int frequency, int strength, int durationMs) { int num = DurationToSegments(durationMs); List list = new List(); List list2 = new List(); for (int i = 0; i < num; i++) { list.Add(frequency); list2.Add(strength); } return Pulse(list, list2); } internal static List GradientPulse(int frequency, int startStrength, int endStrength, int durationMs) { int num = DurationToSegments(durationMs); List list = new List(); List list2 = new List(); for (int i = 0; i < num; i++) { double num2 = ((num <= 1) ? 1.0 : ((double)i / (double)(num - 1))); list.Add(frequency); list2.Add((int)Math.Round((double)startStrength + (double)(endStrength - startStrength) * num2)); } return Pulse(list, list2); } internal static List SinPulse(int frequency, int minStrength, int maxStrength, int durationMs) { int num = DurationToSegments(durationMs); List list = new List(); List list2 = new List(); for (int i = 0; i < num; i++) { double num2 = ((num <= 1) ? 1.0 : ((double)i / (double)(num - 1))); double num3 = Math.Sin(Math.PI * num2); list.Add(frequency); list2.Add((int)Math.Round((double)minStrength + (double)(maxStrength - minStrength) * num3)); } return Pulse(list, list2); } internal static List EnvelopePulse(StimEnvelopeShape shape, int frequency, int minStrength, int maxStrength, int durationMs, double phase) { int num = DurationToSegments(durationMs); List list = new List(); List list2 = new List(); int num2 = Clamp(minStrength, 0, 100); int num3 = Clamp(maxStrength, num2, 100); for (int i = 0; i < num; i++) { double t = ((num <= 1) ? 1.0 : ((double)i / (double)(num - 1))); double num4 = EnvelopeCurve(shape, t, phase); list.Add(frequency); list2.Add((int)Math.Round((double)num2 + (double)(num3 - num2) * num4)); } return Pulse(list, list2); } internal static double EvaluateEnvelope(StimEnvelopeShape shape, double t, double phase) { return EnvelopeCurve(shape, t, phase); } internal static List Pulse(IList frequencies, IList strengths) { if (frequencies == null || strengths == null || frequencies.Count != strengths.Count) { throw new ArgumentException("Frequency and strength counts must match."); } List list = new List(); StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder2 = new StringBuilder(); for (int i = 0; i < strengths.Count; i++) { stringBuilder.Append(ToHexByte(ConvertFrequency(frequencies[i]))); stringBuilder2.Append(ToHexByte(Clamp(strengths[i], 0, 100))); if ((i + 1) % 4 == 0) { list.Add(stringBuilder.ToString() + stringBuilder2.ToString()); stringBuilder.Length = 0; stringBuilder2.Length = 0; } } if (stringBuilder.Length > 0 || stringBuilder2.Length > 0) { while (stringBuilder.Length < 8) { stringBuilder.Append('0'); } while (stringBuilder2.Length < 8) { stringBuilder2.Append('0'); } list.Add(stringBuilder.ToString() + stringBuilder2.ToString()); } return list; } internal static string ToJsonArray(IList pulses, int count) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("["); for (int i = 0; i < count && i < pulses.Count; i++) { if (i > 0) { stringBuilder.Append(","); } stringBuilder.Append("\""); stringBuilder.Append(pulses[i]); stringBuilder.Append("\""); } stringBuilder.Append("]"); return stringBuilder.ToString(); } private static int DurationToSegments(int durationMs) { int num = Clamp(durationMs, 25, Math.Max(25, ModConfig.MaxEventDurationMs.Value)); return Math.Max(1, (int)Math.Ceiling((double)num / 25.0)); } private static double EnvelopeCurve(StimEnvelopeShape shape, double t, double phase) { switch (shape) { case StimEnvelopeShape.SingleTap: return Math.Pow(Math.Max(0.0, 1.0 - t), 1.35); case StimEnvelopeShape.DoubleTap: { double val = Math.Sin(Math.PI * Clamp01(t / 0.42)); double num4 = Math.Sin(Math.PI * Clamp01((t - 0.52) / 0.42)); return Math.Max(0.0, Math.Max(val, num4 * 0.9)); } case StimEnvelopeShape.RampUpHoldDown: if (t < 0.32) { return Smooth01(t / 0.32); } if (t < 0.64) { return 1.0; } return Smooth01(1.0 - (t - 0.64) / 0.36); case StimEnvelopeShape.SawBurst: { double num3 = (t * 5.0 + phase) % 1.0; return 0.25 + 0.75 * (1.0 - num3); } case StimEnvelopeShape.Tremor: { double value = Math.Sin((t * 8.0 + phase) * Math.PI * 2.0); return 0.45 + 0.55 * Math.Abs(value); } default: { double num = Math.Sin(Math.PI * t); double num2 = 0.18 * Math.Sin((t * 6.0 + phase) * Math.PI * 2.0); return Clamp01(num + num2); } } } private static double Smooth01(double t) { double num = Clamp01(t); return num * num * (3.0 - 2.0 * num); } private static double Clamp01(double value) { if (value < 0.0) { return 0.0; } if (value > 1.0) { return 1.0; } return value; } private static int ConvertFrequency(int frequency) { int num = Clamp(frequency, 10, 1000); if (num <= 100) { return num; } if (num < 600) { return (num - 100) / 5 + 100; } return (num - 600) / 10 + 200; } private static string ToHexByte(int value) { return Clamp(value, 0, 255).ToString("X2", CultureInfo.InvariantCulture); } internal static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value > max) { return max; } return value; } }