Branch data Line data Source code
1 : : #include "GuiLayer.h"
2 : : #include "Version.h"
3 : : #include "Config.h"
4 : : #include "Tooltips.h"
5 : : #include "StringUtils.h" // Added StringUtils.h
6 : : #include "DirectInputFFB.h"
7 : : #include "GameConnector.h"
8 : : #include "GuiWidgets.h"
9 : : #include "AsyncLogger.h"
10 : : #include "VehicleUtils.h"
11 : : #include "HealthMonitor.h"
12 : : #include <iostream>
13 : : #include <vector>
14 : : #include <cmath>
15 : : #include <algorithm>
16 : : #include <mutex>
17 : : #include <chrono>
18 : : #include <ctime>
19 : : #include <filesystem>
20 : :
21 : : #ifdef ENABLE_IMGUI
22 : : #include "imgui.h"
23 : :
24 : : #ifdef _WIN32
25 : : #define WIN32_LEAN_AND_MEAN
26 : : #include <windows.h>
27 : : #endif
28 : :
29 : 372 : static void DisplayRate(const char* label, double rate, double target) {
30 [ + - ]: 372 : ImGui::Text("%s", label);
31 : :
32 : : // Status colors for performance metrics
33 : : static const ImVec4 COLOR_RED(1.0F, 0.4F, 0.4F, 1.0F);
34 : : static const ImVec4 COLOR_GREEN(0.4F, 1.0F, 0.4F, 1.0F);
35 : : static const ImVec4 COLOR_YELLOW(1.0F, 1.0F, 0.4F, 1.0F);
36 : :
37 : 372 : ImVec4 color = COLOR_RED;
38 [ + + ]: 372 : if (rate >= target * 0.95) {
39 : 6 : color = COLOR_GREEN;
40 [ - + ]: 366 : } else if (rate >= target * 0.75) {
41 : 0 : color = COLOR_YELLOW;
42 : : }
43 : :
44 [ + - ]: 372 : ImGui::TextColored(color, "%.1f Hz", rate);
45 : 372 : }
46 : :
47 : :
48 : : // External linkage to FFB loop status
49 : : extern std::atomic<bool> g_running;
50 : : extern std::recursive_mutex g_engine_mutex;
51 : :
52 : : float GuiLayer::m_latest_steering_range = 0.0f;
53 : : float GuiLayer::m_latest_steering_angle = 0.0f;
54 : :
55 : : static const float CONFIG_PANEL_WIDTH = 500.0f;
56 : : static const int LATENCY_WARNING_THRESHOLD_MS = 15;
57 : :
58 : : // Professional "Flat Dark" Theme
59 : 1 : void GuiLayer::SetupGUIStyle() {
60 [ + - ]: 1 : ImGuiStyle& style = ImGui::GetStyle();
61 : :
62 : 1 : style.WindowRounding = 5.0f;
63 : 1 : style.ChildRounding = 5.0f;
64 : 1 : style.PopupRounding = 5.0f;
65 : 1 : style.FrameRounding = 4.0f;
66 : 1 : style.GrabRounding = 4.0f;
67 : 1 : style.WindowPadding = ImVec2(10, 10);
68 : 1 : style.FramePadding = ImVec2(8, 4);
69 : 1 : style.ItemSpacing = ImVec2(8, 6);
70 : :
71 : 1 : ImVec4* colors = style.Colors;
72 : :
73 : 1 : colors[ImGuiCol_WindowBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f);
74 : 1 : colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.15f, 0.15f, 1.00f);
75 : 1 : colors[ImGuiCol_PopupBg] = ImVec4(0.15f, 0.15f, 0.15f, 0.98f);
76 : :
77 : 1 : colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.20f, 0.00f);
78 : 1 : colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.25f, 0.25f, 0.50f);
79 : 1 : colors[ImGuiCol_HeaderActive] = ImVec4(0.30f, 0.30f, 0.30f, 0.50f);
80 : :
81 : 1 : colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
82 : 1 : colors[ImGuiCol_FrameBgHovered] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
83 : 1 : colors[ImGuiCol_FrameBgActive] = ImVec4(0.30f, 0.30f, 0.30f, 1.00f);
84 : :
85 : 1 : ImVec4 accent = ImVec4(0.00f, 0.60f, 0.85f, 1.00f);
86 : 1 : colors[ImGuiCol_SliderGrab] = accent;
87 : 1 : colors[ImGuiCol_SliderGrabActive] = ImVec4(0.00f, 0.70f, 0.95f, 1.00f);
88 : 1 : colors[ImGuiCol_Button] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f);
89 : 1 : colors[ImGuiCol_ButtonHovered] = accent;
90 : 1 : colors[ImGuiCol_ButtonActive] = ImVec4(0.00f, 0.50f, 0.75f, 1.00f);
91 : 1 : colors[ImGuiCol_CheckMark] = accent;
92 : :
93 : 1 : colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
94 : 1 : colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
95 : 1 : colors[ImGuiCol_MenuBarBg] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f);
96 : 1 : }
97 : :
98 : 1 : void GuiLayer::DrawMenuBar(FFBEngine& engine) {
99 [ + - ]: 1 : if (ImGui::BeginMainMenuBar()) {
100 [ - + ]: 1 : if (ImGui::BeginMenu("Logs")) {
101 [ # # ]: 0 : if (ImGui::MenuItem("Analyze last log")) {
102 : : namespace fs = std::filesystem;
103 : 0 : fs::path latest_path;
104 : 0 : fs::file_time_type latest_time;
105 : 0 : bool found = false;
106 : :
107 [ # # # # : 0 : fs::path search_path = Config::m_log_path.empty() ? "." : Config::m_log_path;
# # # # #
# # # ]
108 : :
109 : : try {
110 [ # # # # ]: 0 : if (fs::exists(search_path)) {
111 [ # # # # : 0 : for (const auto& entry : fs::directory_iterator(search_path)) {
# # ]
112 [ # # # # ]: 0 : if (entry.is_regular_file()) {
113 [ # # # # ]: 0 : std::string filename = entry.path().filename().string();
114 : 0 : bool looks_like_log = filename.find("lmuffb_log_") == 0;
115 [ # # # # : 0 : bool has_ext = (filename.length() >= 4 && (filename.substr(filename.length() - 4) == ".bin" || filename.substr(filename.length() - 4) == ".csv"));
# # # # #
# # # # #
# # # # #
# # # ]
116 [ # # # # ]: 0 : if (looks_like_log && has_ext) {
117 [ # # ]: 0 : auto ftime = fs::last_write_time(entry);
118 [ # # # # : 0 : if (!found || ftime > latest_time) {
# # # # ]
119 : 0 : latest_time = ftime;
120 [ # # ]: 0 : latest_path = entry.path();
121 : 0 : found = true;
122 : : }
123 : : }
124 : 0 : }
125 : 0 : }
126 : : }
127 [ - - ]: 0 : } catch (const std::exception& e) {
128 [ - - - - : 0 : std::cerr << "Log analysis error: " << e.what() << "\n";
- - ]
129 : 0 : } catch (...) {
130 [ - - ]: 0 : std::cerr << "Log analysis unknown error\n";
131 [ - - ]: 0 : }
132 : :
133 [ # # ]: 0 : if (found) {
134 [ # # ]: 0 : std::string log_file = latest_path.string();
135 : :
136 : : // Get executable directory to find tools/ relative to the binary
137 [ # # ]: 0 : fs::path exe_dir = fs::current_path();
138 : : #ifdef _WIN32
139 : : char buffer[MAX_PATH];
140 : : if (GetModuleFileNameA(NULL, buffer, MAX_PATH)) {
141 : : exe_dir = fs::path(buffer).parent_path();
142 : : }
143 : : #endif
144 : :
145 : : // Robust PYTHONPATH lookup
146 [ # # # # : 0 : std::string python_path = (exe_dir / "tools").string();
# # ]
147 [ # # # # : 0 : if (!fs::exists(exe_dir / "tools/lmuffb_log_analyzer")) {
# # # # ]
148 : : // Dev environment fallbacks from CWD
149 [ # # # # : 0 : if (fs::exists("tools/lmuffb_log_analyzer")) python_path = "tools";
# # # # ]
150 [ # # # # : 0 : else if (fs::exists("../tools/lmuffb_log_analyzer")) python_path = "../tools";
# # # # ]
151 [ # # # # : 0 : else if (fs::exists("../../tools/lmuffb_log_analyzer")) python_path = "../../tools";
# # # # ]
152 : : }
153 : :
154 [ # # # # : 0 : std::string cmd = "start cmd /c \"set PYTHONPATH=" + python_path + " && python -m lmuffb_log_analyzer.cli analyze-full \"" + log_file + "\" & pause\"";
# # # # ]
155 [ # # ]: 0 : system(cmd.c_str());
156 : 0 : }
157 : 0 : }
158 : 0 : ImGui::EndMenu();
159 : : }
160 : 1 : ImGui::EndMainMenuBar();
161 : : }
162 : 1 : }
163 : :
164 : :
165 : : static constexpr std::chrono::seconds CONNECT_ATTEMPT_INTERVAL(2);
166 : :
167 : 377 : void GuiLayer::DrawTuningWindow(FFBEngine& engine) {
168 [ + - ]: 377 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
169 : :
170 [ + - ]: 377 : ImGuiViewport* viewport = ImGui::GetMainViewport();
171 [ + + ]: 377 : float current_width = Config::show_graphs ? CONFIG_PANEL_WIDTH : viewport->WorkSize.x;
172 : :
173 [ + - ]: 377 : ImGui::SetNextWindowPos(viewport->WorkPos);
174 [ + - ]: 377 : ImGui::SetNextWindowSize(ImVec2(current_width, viewport->WorkSize.y));
175 : :
176 : 377 : ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
177 [ + - ]: 377 : ImGui::Begin("MainUI", nullptr, flags);
178 : :
179 [ + + + - ]: 377 : static std::chrono::steady_clock::time_point last_check_time = std::chrono::steady_clock::now();
180 : :
181 [ + - + - : 377 : if (!GameConnector::Get().IsConnected()) {
+ + ]
182 [ + - ]: 208 : ImGui::TextColored(ImVec4(1, 1, 0, 1), "Connecting to LMU...");
183 [ + - + - : 208 : if (std::chrono::steady_clock::now() - last_check_time > CONNECT_ATTEMPT_INTERVAL) {
- + ]
184 : 0 : last_check_time = std::chrono::steady_clock::now();
185 [ # # # # ]: 0 : GameConnector::Get().TryConnect();
186 : : }
187 : : } else {
188 [ + - ]: 169 : ImGui::TextColored(ImVec4(0, 1, 0, 1), "Connected to LMU");
189 : : }
190 : :
191 [ + + + - ]: 377 : static std::vector<DeviceInfo> devices;
192 : : static int selected_device_idx = -1;
193 : :
194 [ + + ]: 377 : if (devices.empty()) {
195 [ + - + - ]: 1 : devices = DirectInputFFB::Get().EnumerateDevices();
196 [ + - + - : 1 : if (selected_device_idx == -1 && !Config::m_last_device_guid.empty()) {
+ - ]
197 [ + - ]: 1 : GUID target = DirectInputFFB::StringToGuid(Config::m_last_device_guid);
198 [ + - ]: 1 : for (int i = 0; i < (int)devices.size(); i++) {
199 [ + - ]: 1 : if (memcmp(&devices[i].guid, &target, sizeof(GUID)) == 0) {
200 : 1 : selected_device_idx = i;
201 [ + - + - ]: 1 : DirectInputFFB::Get().SelectDevice(devices[i].guid);
202 : 1 : break;
203 : : }
204 : : }
205 : : }
206 : : }
207 : :
208 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
209 [ + - + - : 377 : if (ImGui::BeginCombo("FFB Device", selected_device_idx >= 0 ? devices[selected_device_idx].name.c_str() : "Select Device...")) {
- + ]
210 [ # # ]: 0 : for (int i = 0; i < (int)devices.size(); i++) {
211 : 0 : bool is_selected = (selected_device_idx == i);
212 [ # # ]: 0 : ImGui::PushID(i);
213 [ # # # # ]: 0 : if (ImGui::Selectable(devices[i].name.c_str(), is_selected)) {
214 : 0 : selected_device_idx = i;
215 [ # # # # ]: 0 : DirectInputFFB::Get().SelectDevice(devices[i].guid);
216 [ # # ]: 0 : Config::m_last_device_guid = DirectInputFFB::GuidToString(devices[i].guid);
217 [ # # # # ]: 0 : Config::Save(engine);
218 : : }
219 [ # # # # ]: 0 : if (is_selected) ImGui::SetItemDefaultFocus();
220 [ # # ]: 0 : ImGui::PopID();
221 : : }
222 [ # # ]: 0 : ImGui::EndCombo();
223 : : }
224 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_SELECT);
+ - ]
225 : :
226 [ + - ]: 377 : ImGui::SameLine();
227 [ + - - + ]: 377 : if (ImGui::Button("Rescan")) {
228 [ # # # # ]: 0 : devices = DirectInputFFB::Get().EnumerateDevices();
229 : 0 : selected_device_idx = -1;
230 : : }
231 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_RESCAN);
+ - ]
232 [ + - ]: 377 : ImGui::SameLine();
233 [ + - - + ]: 377 : if (ImGui::Button("Unbind")) {
234 [ # # # # ]: 0 : DirectInputFFB::Get().ReleaseDevice();
235 : 0 : selected_device_idx = -1;
236 : : }
237 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::DEVICE_UNBIND);
- - ]
238 : :
239 [ + - + - ]: 377 : if (DirectInputFFB::Get().IsActive()) {
240 [ + - + - ]: 377 : if (DirectInputFFB::Get().IsExclusive()) {
241 [ + - ]: 377 : ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Mode: EXCLUSIVE (Game FFB Blocked)");
242 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_EXCLUSIVE);
+ - ]
243 : : } else {
244 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.4f, 1.0f), "Mode: SHARED (Potential Conflict)");
245 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MODE_SHARED);
# # ]
246 : : }
247 : : } else {
248 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "No device selected.");
249 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::NO_DEVICE);
# # ]
250 : : }
251 : :
252 [ + - - + ]: 377 : if (ImGui::Checkbox("Always on Top", &Config::m_always_on_top)) {
253 [ # # ]: 0 : SetWindowAlwaysOnTopPlatform(Config::m_always_on_top);
254 [ # # # # ]: 0 : Config::Save(engine);
255 : : }
256 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::ALWAYS_ON_TOP);
+ - ]
257 [ + - ]: 377 : ImGui::SameLine();
258 : :
259 : 377 : bool toggled = Config::show_graphs;
260 [ + - - + ]: 377 : if (ImGui::Checkbox("Graphs", &toggled)) {
261 [ # # ]: 0 : SaveCurrentWindowGeometryPlatform(Config::show_graphs);
262 : 0 : Config::show_graphs = toggled;
263 [ # # ]: 0 : int target_w = Config::show_graphs ? Config::win_w_large : Config::win_w_small;
264 [ # # ]: 0 : int target_h = Config::show_graphs ? Config::win_h_large : Config::win_h_small;
265 [ # # ]: 0 : ResizeWindowPlatform(Config::win_pos_x, Config::win_pos_y, target_w, target_h);
266 [ # # # # ]: 0 : Config::Save(engine);
267 : : }
268 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::SHOW_GRAPHS);
- - ]
269 : :
270 [ + - ]: 377 : ImGui::Separator();
271 : 377 : bool is_logging = Config::m_auto_start_logging;
272 [ + - ]: 377 : if (is_logging) {
273 [ + - - + ]: 377 : if (ImGui::Button("STOP LOG", ImVec2(80, 0))) {
274 : 0 : Config::m_auto_start_logging = false;
275 [ # # ]: 0 : AsyncLogger::Get().Stop();
276 [ # # # # ]: 0 : Config::Save(engine);
277 : : }
278 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_STOP);
- - ]
279 [ + - ]: 377 : ImGui::SameLine();
280 [ + - + + ]: 377 : if (AsyncLogger::Get().IsLogging()) {
281 [ + - ]: 3 : float time = (float)ImGui::GetTime();
282 : 3 : bool blink = (fmod(time, 1.0f) < 0.5f);
283 [ + + + - ]: 3 : ImGui::TextColored(blink ? ImVec4(1, 0, 0, 1) : ImVec4(0.6f, 0, 0, 1), "REC");
284 [ + - - + : 3 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_REC);
- - ]
285 : :
286 [ + - ]: 3 : ImGui::SameLine();
287 [ + - ]: 3 : size_t bytes = AsyncLogger::Get().GetFileSizeBytes();
288 [ + - ]: 3 : if (bytes < 1024ULL * 1024ULL)
289 [ + - + - ]: 3 : ImGui::Text("%zu f (%.0f KB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / 1024.0f);
290 : : else
291 [ # # # # ]: 0 : ImGui::Text("%zu f (%.1f MB)", AsyncLogger::Get().GetFrameCount(), (float)bytes / (1024.0f * 1024.0f));
292 : :
293 [ + - ]: 3 : ImGui::SameLine();
294 [ + - - + ]: 3 : if (ImGui::Button("MARKER")) {
295 [ # # ]: 0 : AsyncLogger::Get().SetMarker();
296 : : }
297 [ + - - + : 3 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_MARKER);
- - ]
298 : : } else {
299 [ + - ]: 374 : ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "ARMED");
300 [ + - + + : 374 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("Waiting for driving to start...");
+ - ]
301 : : }
302 : : } else {
303 [ # # # # ]: 0 : if (ImGui::Button("START LOGGING", ImVec2(120, 0))) {
304 : 0 : Config::m_auto_start_logging = true;
305 [ # # # # ]: 0 : Config::Save(engine);
306 : : }
307 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_START);
# # ]
308 [ # # ]: 0 : ImGui::SameLine();
309 [ # # ]: 0 : ImGui::TextDisabled("(Diagnostics)");
310 : : }
311 : :
312 : :
313 [ + - ]: 377 : ImGui::Separator();
314 : :
315 : : static int selected_preset = 0;
316 : :
317 : 4867 : auto FormatDecoupled = [&](float val, float base_nm) {
318 : 4867 : float estimated_nm = val * base_nm;
319 : : static char buf[64];
320 : 4867 : StringUtils::SafeFormat(buf, sizeof(buf), "%.1f%%%% (~%.1f Nm)", val * 100.0f, estimated_nm);
321 : 4867 : return (const char*)buf;
322 : : };
323 : :
324 : 3770 : auto FormatPct = [&](float val) {
325 : : static char buf[32];
326 : 3770 : StringUtils::SafeFormat(buf, sizeof(buf), "%.1f%%%%", val * 100.0f);
327 : 3770 : return (const char*)buf;
328 : : };
329 : :
330 : 26577 : auto FloatSetting = [&](const char* label, float* v, float min, float max, const char* fmt = "%.2f", const char* tooltip = nullptr, std::function<void()> decorator = nullptr) {
331 [ + - + - ]: 26577 : GuiWidgets::Result res = GuiWidgets::Float(label, v, min, max, fmt, tooltip, decorator);
332 [ - + ]: 26577 : if (res.deactivated) {
333 [ # # # # ]: 0 : Config::Save(engine);
334 : : }
335 : 26577 : };
336 : :
337 : 4901 : auto BoolSetting = [&](const char* label, bool* v, const char* tooltip = nullptr) {
338 [ + - ]: 4901 : GuiWidgets::Result res = GuiWidgets::Checkbox(label, v, tooltip);
339 [ - + ]: 4901 : if (res.deactivated) {
340 [ # # # # ]: 0 : Config::Save(engine);
341 : : }
342 : 4901 : };
343 : :
344 : 961 : auto IntSetting = [&](const char* label, int* v, const char* const items[], int items_count, const char* tooltip = nullptr) {
345 [ + - ]: 961 : GuiWidgets::Result res = GuiWidgets::Combo(label, v, items, items_count, tooltip);
346 [ - + ]: 961 : if (res.changed) {
347 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
348 : : // v is already updated by ImGui, but we lock to ensure visibility and consistency
349 [ # # # # ]: 0 : Config::Save(engine);
350 : 0 : }
351 : 961 : };
352 : :
353 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Presets and Configuration", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
354 : : static bool first_run = true;
355 [ + + + - : 377 : if (first_run && !Config::presets.empty()) {
+ + ]
356 [ + - ]: 1 : for (int i = 0; i < (int)Config::presets.size(); i++) {
357 [ + - ]: 1 : if (Config::presets[i].name == Config::m_last_preset_name) {
358 : 1 : selected_preset = i;
359 : 1 : break;
360 : : }
361 : : }
362 : 1 : first_run = false;
363 : : }
364 : :
365 [ + + + - ]: 377 : static std::string preview_buf;
366 : 377 : const char* preview_value = "Custom";
367 [ + - + - : 377 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
+ - ]
368 [ + - ]: 377 : preview_buf = Config::presets[selected_preset].name;
369 [ + - + + ]: 377 : if (Config::IsEngineDirtyRelativeToPreset(selected_preset, engine)) {
370 [ + - ]: 370 : preview_buf += "*";
371 : : }
372 : 377 : preview_value = preview_buf.c_str();
373 : : }
374 : :
375 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.6f);
376 [ + - - + ]: 377 : if (ImGui::BeginCombo("Load Preset", preview_value)) {
377 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
378 : 0 : bool is_selected = (selected_preset == i);
379 [ # # ]: 0 : ImGui::PushID(i);
380 [ # # # # ]: 0 : if (ImGui::Selectable(Config::presets[i].name.c_str(), is_selected)) {
381 : 0 : selected_preset = i;
382 [ # # ]: 0 : Config::ApplyPreset(i, engine);
383 : : }
384 [ # # # # ]: 0 : if (is_selected) ImGui::SetItemDefaultFocus();
385 [ # # ]: 0 : ImGui::PopID();
386 : : }
387 [ # # ]: 0 : ImGui::EndCombo();
388 : : }
389 : :
390 : : static char new_preset_name[64] = "";
391 [ + - + - ]: 377 : ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.4f);
392 [ + - ]: 377 : ImGui::InputText("##NewPresetName", new_preset_name, 64);
393 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_NAME);
+ - ]
394 [ + - ]: 377 : ImGui::SameLine();
395 [ + - - + ]: 377 : if (ImGui::Button("Save New")) {
396 [ # # ]: 0 : if (strlen(new_preset_name) > 0) {
397 [ # # # # ]: 0 : Config::AddUserPreset(std::string(new_preset_name), engine);
398 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
399 [ # # # # ]: 0 : if (Config::presets[i].name == std::string(new_preset_name)) {
400 : 0 : selected_preset = i;
401 : 0 : break;
402 : : }
403 : : }
404 : 0 : new_preset_name[0] = '\0';
405 : : }
406 : : }
407 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_NEW);
- - ]
408 : :
409 [ + - - + ]: 377 : if (ImGui::Button("Save Current Config")) {
410 [ # # # # : 0 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin) {
# # # # ]
411 [ # # ]: 0 : Config::AddUserPreset(Config::presets[selected_preset].name, engine);
412 : : } else {
413 [ # # # # ]: 0 : Config::Save(engine);
414 : : }
415 : : }
416 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_SAVE_CURRENT);
+ - ]
417 [ + - ]: 377 : ImGui::SameLine();
418 [ + - - + ]: 377 : if (ImGui::Button("Reset Defaults")) {
419 [ # # ]: 0 : Config::ApplyPreset(0, engine);
420 : 0 : selected_preset = 0;
421 : : }
422 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_RESET);
- - ]
423 [ + - ]: 377 : ImGui::SameLine();
424 [ + - - + ]: 377 : if (ImGui::Button("Duplicate")) {
425 [ # # ]: 0 : if (selected_preset >= 0) {
426 [ # # ]: 0 : Config::DuplicatePreset(selected_preset, engine);
427 [ # # ]: 0 : for (int i = 0; i < (int)Config::presets.size(); i++) {
428 [ # # ]: 0 : if (Config::presets[i].name == Config::m_last_preset_name) {
429 : 0 : selected_preset = i;
430 : 0 : break;
431 : : }
432 : : }
433 : : }
434 : : }
435 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DUPLICATE);
+ - ]
436 [ + - ]: 377 : ImGui::SameLine();
437 [ + - + - : 377 : bool can_delete = (selected_preset >= 0 && selected_preset < (int)Config::presets.size() && !Config::presets[selected_preset].is_builtin);
+ - ]
438 [ - + - - ]: 377 : if (!can_delete) ImGui::BeginDisabled();
439 [ + - - + ]: 377 : if (ImGui::Button("Delete")) {
440 [ # # ]: 0 : Config::DeletePreset(selected_preset, engine);
441 : 0 : selected_preset = 0;
442 [ # # ]: 0 : Config::ApplyPreset(0, engine);
443 : : }
444 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_DELETE);
- - ]
445 [ - + - - ]: 377 : if (!can_delete) ImGui::EndDisabled();
446 : :
447 [ + - ]: 377 : ImGui::Separator();
448 [ + - - + ]: 377 : if (ImGui::Button("Import Preset...")) {
449 : 0 : std::string path;
450 [ # # # # ]: 0 : if (OpenPresetFileDialogPlatform(path)) {
451 [ # # # # ]: 0 : if (Config::ImportPreset(path, engine)) {
452 : 0 : selected_preset = (int)Config::presets.size() - 1;
453 : : }
454 : : }
455 : 0 : }
456 [ + - + + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_IMPORT);
+ - ]
457 [ + - ]: 377 : ImGui::SameLine();
458 [ + - - + ]: 377 : if (ImGui::Button("Export Selected...")) {
459 [ # # # # : 0 : if (selected_preset >= 0 && selected_preset < (int)Config::presets.size()) {
# # ]
460 : 0 : std::string path;
461 [ # # ]: 0 : std::string defaultName = Config::presets[selected_preset].name + ".ini";
462 [ # # # # ]: 0 : if (SavePresetFileDialogPlatform(path, defaultName)) {
463 [ # # ]: 0 : Config::ExportPreset(selected_preset, path);
464 : : }
465 : 0 : }
466 : : }
467 [ + - - + : 377 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::PRESET_EXPORT);
- - ]
468 : :
469 [ + - ]: 377 : ImGui::TreePop();
470 : : }
471 : :
472 [ + - ]: 377 : ImGui::Spacing();
473 : :
474 [ + - ]: 377 : ImGui::Columns(2, "SettingsGrid", false);
475 [ + - + - ]: 377 : ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.45f);
476 : :
477 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("General FFB", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
478 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
479 : :
480 [ + - ]: 377 : ImGui::TextDisabled("Steering: %.1f° (%.0f)", m_latest_steering_angle, m_latest_steering_range);
481 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
482 : :
483 [ + - ]: 377 : BoolSetting("Steerlock from REST API", &engine.m_rest_api_enabled, Tooltips::REST_API_ENABLE);
484 : :
485 [ + - ]: 377 : ImGui::Spacing();
486 : 377 : bool use_in_game_ffb = (engine.m_torque_source == 1);
487 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Use In-Game FFB (400Hz Native)", &use_in_game_ffb, Tooltips::USE_INGAME_FFB).changed) {
488 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
489 [ # # ]: 0 : engine.m_torque_source = use_in_game_ffb ? 1 : 0;
490 [ # # # # ]: 0 : Config::Save(engine);
491 : 0 : }
492 : :
493 [ + - ]: 377 : BoolSetting("Invert FFB Signal", &engine.m_invert_force, Tooltips::INVERT_FFB);
494 : :
495 : 377 : bool prev_structural = engine.m_general.dynamic_normalization_enabled;
496 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Enable Dynamic Normalization (Session Peak)", &engine.m_general.dynamic_normalization_enabled, Tooltips::DYNAMIC_NORMALIZATION_ENABLE).changed) {
497 [ # # # # ]: 0 : if (prev_structural && !engine.m_general.dynamic_normalization_enabled) {
498 [ # # ]: 0 : engine.ResetNormalization();
499 : : }
500 [ # # # # ]: 0 : Config::Save(engine);
501 : : }
502 [ + - ]: 377 : FloatSetting("Master Gain", &engine.m_general.gain, 0.0f, 2.0f, FormatPct(engine.m_general.gain), Tooltips::MASTER_GAIN);
503 [ + - ]: 377 : FloatSetting("Wheelbase Max Torque", &engine.m_general.wheelbase_max_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::WHEELBASE_MAX_TORQUE);
504 [ + - ]: 377 : FloatSetting("Target Rim Torque", &engine.m_general.target_rim_nm, 1.0f, 50.0f, "%.1f Nm", Tooltips::TARGET_RIM_TORQUE);
505 [ + - ]: 377 : FloatSetting("Min Force", &engine.m_general.min_force, 0.0f, 0.20f, "%.3f", Tooltips::MIN_FORCE);
506 : :
507 : : // Dynamically format the tooltip to show the exact fade-out speed in km/h
508 : : char stat_damp_tooltip[512];
509 : 377 : StringUtils::SafeFormat(stat_damp_tooltip, sizeof(stat_damp_tooltip),
510 : : "%s\n\nCurrently fades to 0%% at: %.1f km/h (See Advanced Settings -> Speed Gate).",
511 : 377 : Tooltips::STATIONARY_DAMPING, (float)engine.m_speed_gate_upper * 3.6f);
512 : :
513 [ + - ]: 377 : FloatSetting("Stationary Damping", &engine.m_stationary_damping, 0.0f, 1.0f, FormatPct(engine.m_stationary_damping), stat_damp_tooltip);
514 : :
515 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Soft Lock", ImGuiTreeNodeFlags_DefaultOpen)) {
516 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
517 [ + - ]: 377 : BoolSetting("Enable Soft Lock", &engine.m_soft_lock_enabled, Tooltips::SOFT_LOCK_ENABLE);
518 [ + - ]: 377 : if (engine.m_soft_lock_enabled) {
519 [ + - ]: 377 : FloatSetting(" Stiffness", &engine.m_soft_lock_stiffness, 0.0f, 100.0f, "%.1f", Tooltips::SOFT_LOCK_STIFFNESS);
520 [ + - ]: 377 : FloatSetting(" Damping", &engine.m_soft_lock_damping, 0.0f, 5.0f, "%.2f", Tooltips::SOFT_LOCK_DAMPING);
521 : : }
522 [ + - ]: 377 : ImGui::TreePop();
523 [ + - ]: 377 : ImGui::Separator();
524 : : }
525 : :
526 [ + - ]: 377 : ImGui::TreePop();
527 : : } else {
528 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
529 : : }
530 : :
531 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Front Axle (Understeer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
532 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
533 : :
534 [ + + ]: 377 : if (engine.m_torque_source == 1) {
535 [ + - ]: 166 : FloatSetting("In-Game FFB Gain", &engine.m_ingame_ffb_gain, 0.0f, 2.0f, FormatPct(engine.m_ingame_ffb_gain), Tooltips::INGAME_FFB_GAIN);
536 : : } else {
537 [ + - ]: 211 : FloatSetting("Steering Shaft Gain", &engine.m_steering_shaft_gain, 0.0f, 2.0f, FormatPct(engine.m_steering_shaft_gain), Tooltips::STEERING_SHAFT_GAIN);
538 : : }
539 : :
540 [ + - ]: 377 : FloatSetting("Steering Shaft Smoothing", &engine.m_steering_shaft_smoothing, 0.000f, 0.100f, "%.3f s",
541 : : Tooltips::STEERING_SHAFT_SMOOTHING,
542 : 377 : [&]() {
543 : 377 : int ms = (int)std::lround(engine.m_steering_shaft_smoothing * 1000.0f);
544 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
545 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
546 : 377 : });
547 : :
548 [ + - ]: 377 : FloatSetting("Understeer Effect", &engine.m_understeer_effect, 0.0f, 2.0f, FormatPct(engine.m_understeer_effect),
549 : : Tooltips::UNDERSTEER_EFFECT);
550 : :
551 [ + - ]: 377 : FloatSetting("Response Curve (Gamma)", &engine.m_understeer_gamma, 0.1f, 4.0f, "%.1f",
552 : : Tooltips::UNDERSTEER_GAMMA);
553 : :
554 : 377 : const char* torque_sources[] = { "Shaft Torque (100Hz Legacy)", "In-Game FFB (400Hz LMU 1.2+)" };
555 [ + - ]: 377 : IntSetting("Torque Source", &engine.m_torque_source, torque_sources, sizeof(torque_sources)/sizeof(torque_sources[0]),
556 : : Tooltips::TORQUE_SOURCE);
557 : :
558 [ + + ]: 377 : if (engine.m_torque_source == 0) {
559 : 211 : const char* recon_modes[] = { "Zero Latency (Extrapolation)", "Smooth (Interpolation)" };
560 [ + - ]: 211 : IntSetting(" Reconstruction", &engine.m_steering_100hz_reconstruction, recon_modes, sizeof(recon_modes)/sizeof(recon_modes[0]),
561 : : Tooltips::STEERING_100HZ_RECONSTRUCTION);
562 : : }
563 : :
564 [ + - ]: 377 : BoolSetting("Pure Passthrough", &engine.m_torque_passthrough, Tooltips::PURE_PASSTHROUGH);
565 : :
566 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Signal Filtering", ImGuiTreeNodeFlags_DefaultOpen)) {
567 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
568 : :
569 [ + - ]: 377 : BoolSetting(" Flatspot Suppression", &engine.m_flatspot_suppression, Tooltips::FLATSPOT_SUPPRESSION);
570 [ + + ]: 377 : if (engine.m_flatspot_suppression) {
571 [ + - ]: 364 : FloatSetting(" Filter Width (Q)", &engine.m_notch_q, 0.5f, 10.0f, "Q: %.2f", Tooltips::NOTCH_Q);
572 [ + - ]: 364 : FloatSetting(" Suppression Strength", &engine.m_flatspot_strength, 0.0f, 1.0f, "%.2f", Tooltips::SUPPRESSION_STRENGTH);
573 [ + - ]: 364 : ImGui::Text(" Est. / Theory Freq");
574 [ + - ]: 364 : ImGui::NextColumn();
575 [ + - ]: 364 : ImGui::TextDisabled("%.1f Hz / %.1f Hz", engine.m_debug_freq, engine.m_theoretical_freq);
576 [ + - ]: 364 : ImGui::NextColumn();
577 : : }
578 : :
579 [ + - ]: 377 : BoolSetting(" Static Noise Filter", &engine.m_static_notch_enabled, Tooltips::STATIC_NOISE_FILTER);
580 [ + + ]: 377 : if (engine.m_static_notch_enabled) {
581 [ + - ]: 363 : FloatSetting(" Target Frequency", &engine.m_static_notch_freq, 10.0f, 100.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_FREQ);
582 [ + - ]: 363 : FloatSetting(" Filter Width", &engine.m_static_notch_width, 0.1f, 10.0f, "%.1f Hz", Tooltips::STATIC_NOTCH_WIDTH);
583 : : }
584 : :
585 [ + - ]: 377 : ImGui::TreePop();
586 : : } else {
587 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
588 : : }
589 : :
590 [ + - ]: 377 : ImGui::TreePop();
591 : : } else {
592 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
593 : : }
594 : :
595 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("FFB Safety Features", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
596 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
597 : :
598 [ + - + - ]: 754 : FloatSetting("Safety Duration", &engine.m_safety.m_safety_window_duration, 0.0f, 10.0f, "%.1f s", Tooltips::SAFETY_WINDOW_DURATION, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
599 [ + - + - ]: 754 : FloatSetting("Gain Reduction", &engine.m_safety.m_safety_gain_reduction, 0.0f, 1.0f, FormatPct(engine.m_safety.m_safety_gain_reduction), Tooltips::SAFETY_GAIN_REDUCTION, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
600 [ + - + - ]: 754 : FloatSetting("Safety Smoothing", &engine.m_safety.m_safety_smoothing_tau, 0.001f, 1.0f, "%.3f s", Tooltips::SAFETY_SMOOTHING_TAU, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
601 [ + - + - ]: 754 : FloatSetting("Slew Restriction", &engine.m_safety.m_safety_slew_full_scale_time_s, 0.1f, 5.0f, "%.2f s", Tooltips::SAFETY_SLEW_FULL_SCALE_TIME_S, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
602 : :
603 [ + - ]: 377 : ImGui::Separator();
604 [ + - ]: 377 : ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Stuttering (Lost Frames)");
605 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
606 : :
607 [ + - ]: 377 : BoolSetting("Safety on Stuttering", &engine.m_safety.m_stutter_safety_enabled, Tooltips::STUTTER_SAFETY_ENABLE);
608 [ - + ]: 377 : if (engine.m_safety.m_stutter_safety_enabled) {
609 [ # # # # ]: 0 : FloatSetting("Stutter Threshold", &engine.m_safety.m_stutter_threshold, 1.1f, 5.0f, "%.2fx", Tooltips::STUTTER_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
610 : : }
611 : :
612 [ + - ]: 377 : ImGui::Separator();
613 [ + - ]: 377 : ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Spike Detection");
614 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
615 : :
616 [ + - + - ]: 754 : FloatSetting("Spike Threshold", &engine.m_safety.m_spike_detection_threshold, 10.0f, 2000.0f, "%.0f u/s", Tooltips::SPIKE_DETECTION_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
617 [ + - + - ]: 754 : FloatSetting("Immediate Spike", &engine.m_safety.m_immediate_spike_threshold, 100.0f, 5000.0f, "%.0f u/s", Tooltips::IMMEDIATE_SPIKE_THRESHOLD, [&]() { std::lock_guard<std::recursive_mutex> lock(g_engine_mutex); });
618 : :
619 [ + - ]: 377 : ImGui::TreePop();
620 : : } else {
621 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
622 : : }
623 : :
624 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Load Forces", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
625 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
626 : :
627 [ + - ]: 377 : FloatSetting("Lateral Load", &engine.m_lat_load_effect, 0.0f, 10.0f, FormatDecoupled(engine.m_lat_load_effect, FFBEngine::BASE_NM_SOP_LATERAL), Tooltips::LATERAL_LOAD);
628 : :
629 : 377 : const char* load_transforms[] = { "Linear (Raw)", "Cubic (Smooth)", "Quadratic (Broad)", "Hermite (Locked Center)" };
630 : 377 : int lat_transform = static_cast<int>(engine.m_lat_load_transform);
631 [ + - - + ]: 377 : if (GuiWidgets::Combo(" Lateral Transform", &lat_transform, load_transforms, 4, "Mathematical transformation to soften the lateral load limits and remove 'notchiness'.").changed) {
632 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
633 : 0 : engine.m_lat_load_transform = static_cast<LoadTransform>(lat_transform);
634 [ # # # # ]: 0 : Config::Save(engine);
635 : 0 : }
636 : :
637 [ + - ]: 377 : ImGui::Spacing();
638 : :
639 [ + - ]: 377 : FloatSetting("Longitudinal G-Force", &engine.m_long_load_effect, 0.0f, 10.0f, FormatPct(engine.m_long_load_effect), Tooltips::DYNAMIC_WEIGHT);
640 [ + - ]: 377 : FloatSetting(" G-Force Smoothing", &engine.m_long_load_smoothing, 0.000f, 0.500f, "%.3f s", Tooltips::WEIGHT_SMOOTHING);
641 : :
642 : 377 : int long_transform = static_cast<int>(engine.m_long_load_transform);
643 [ + - - + ]: 377 : if (GuiWidgets::Combo(" G-Force Transform", &long_transform, load_transforms, 4, "Mathematical transformation to soften the longitudinal load limits and remove 'notchiness'.").changed) {
644 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
645 : 0 : engine.m_long_load_transform = static_cast<LoadTransform>(long_transform);
646 [ # # # # ]: 0 : Config::Save(engine);
647 : 0 : }
648 : :
649 [ + - ]: 377 : ImGui::TreePop();
650 : : } else {
651 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
652 : : }
653 : :
654 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Rear Axle (Oversteer)", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
655 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
656 : :
657 [ + - ]: 377 : FloatSetting("Lateral G Boost (Slide)", &engine.m_oversteer_boost, 0.0f, 4.0f, FormatPct(engine.m_oversteer_boost),
658 : : Tooltips::OVERSTEER_BOOST);
659 [ + - ]: 377 : FloatSetting("Lateral G", &engine.m_sop_effect, 0.0f, 2.0f, FormatDecoupled(engine.m_sop_effect, FFBEngine::BASE_NM_SOP_LATERAL), Tooltips::LATERAL_G);
660 : :
661 [ + - ]: 377 : FloatSetting("SoP Self-Aligning Torque", &engine.m_rear_align_effect, 0.0f, 2.0f, FormatDecoupled(engine.m_rear_align_effect, FFBEngine::BASE_NM_REAR_ALIGN),
662 : : Tooltips::REAR_ALIGN_TORQUE);
663 [ + - ]: 377 : FloatSetting(" Kerb Strike Rejection", &engine.m_kerb_strike_rejection, 0.0f, 1.0f, FormatPct(engine.m_kerb_strike_rejection),
664 : : Tooltips::KERB_STRIKE_REJECTION);
665 [ + - ]: 377 : FloatSetting("Yaw Kick", &engine.m_sop_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_sop_yaw_gain, FFBEngine::BASE_NM_YAW_KICK),
666 : : Tooltips::YAW_KICK);
667 [ + - ]: 377 : FloatSetting(" Activation Threshold", &engine.m_yaw_kick_threshold, 0.0f, 10.0f, "%.2f rad/s²", Tooltips::YAW_KICK_THRESHOLD);
668 : :
669 [ + - ]: 377 : FloatSetting(" Kick Response", &engine.m_yaw_accel_smoothing, 0.000f, 0.050f, "%.3f s",
670 : : Tooltips::YAW_KICK_RESPONSE,
671 : 377 : [&]() {
672 : 377 : int ms = (int)std::lround(engine.m_yaw_accel_smoothing * 1000.0f);
673 [ + - ]: 377 : ImVec4 color = (ms <= 15) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
674 [ + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms", ms);
675 : 377 : });
676 : :
677 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Unloaded Yaw Kick (Braking)", ImGuiTreeNodeFlags_DefaultOpen)) {
678 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
679 [ + - ]: 377 : FloatSetting(" Gain", &engine.m_unloaded_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_unloaded_yaw_gain, FFBEngine::BASE_NM_YAW_KICK), Tooltips::UNLOADED_YAW_GAIN);
680 [ + - ]: 377 : FloatSetting(" Threshold", &engine.m_unloaded_yaw_threshold, 0.0f, 2.0f, "%.2f rad/s²", Tooltips::UNLOADED_YAW_THRESHOLD);
681 [ + - ]: 377 : FloatSetting(" Unload Sens.", &engine.m_unloaded_yaw_sens, 0.1f, 5.0f, "%.1fx", Tooltips::UNLOADED_YAW_SENS);
682 [ + - ]: 377 : FloatSetting(" Gamma", &engine.m_unloaded_yaw_gamma, 0.1f, 2.0f, "%.1f", Tooltips::UNLOADED_YAW_GAMMA);
683 [ + - ]: 377 : FloatSetting(" Punch (Jerk)", &engine.m_unloaded_yaw_punch, 0.0f, 0.2f, "%.2fx", Tooltips::UNLOADED_YAW_PUNCH);
684 [ + - ]: 377 : ImGui::TreePop();
685 : : } else {
686 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
687 : : }
688 : :
689 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Power Yaw Kick (Acceleration)", ImGuiTreeNodeFlags_DefaultOpen)) {
690 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
691 [ + - ]: 377 : FloatSetting(" Gain", &engine.m_power_yaw_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_power_yaw_gain, FFBEngine::BASE_NM_YAW_KICK), Tooltips::POWER_YAW_GAIN);
692 [ + - ]: 377 : FloatSetting(" Threshold", &engine.m_power_yaw_threshold, 0.0f, 2.0f, "%.2f rad/s²", Tooltips::POWER_YAW_THRESHOLD);
693 [ + - ]: 377 : FloatSetting(" TC Slip Target", &engine.m_power_slip_threshold, 0.01f, 0.5f, FormatPct(engine.m_power_slip_threshold), Tooltips::POWER_SLIP_THRESHOLD);
694 [ + - ]: 377 : FloatSetting(" Gamma", &engine.m_power_yaw_gamma, 0.1f, 2.0f, "%.1f", Tooltips::POWER_YAW_GAMMA);
695 [ + - ]: 377 : FloatSetting(" Punch (Jerk)", &engine.m_power_yaw_punch, 0.0f, 0.2f, "%.2fx", Tooltips::POWER_YAW_PUNCH);
696 [ + - ]: 377 : ImGui::TreePop();
697 : : } else {
698 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
699 : : }
700 : :
701 [ + - ]: 377 : FloatSetting("Gyro Damping", &engine.m_gyro_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_gyro_gain, FFBEngine::BASE_NM_GYRO_DAMPING), Tooltips::GYRO_DAMPING);
702 : :
703 [ + - ]: 377 : FloatSetting(" Gyro Smooth", &engine.m_gyro_smoothing, 0.000f, 0.050f, "%.3f s",
704 : : Tooltips::GYRO_SMOOTH,
705 : 377 : [&]() {
706 : 377 : int ms = (int)std::lround(engine.m_gyro_smoothing * 1000.0f);
707 [ + - ]: 377 : ImVec4 color = (ms <= 20) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
708 [ + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms", ms);
709 : 377 : });
710 : :
711 [ + - ]: 377 : ImGui::TextColored(ImVec4(0.0f, 0.6f, 0.85f, 1.0f), "Advanced SoP");
712 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
713 : :
714 [ + - ]: 377 : FloatSetting("SoP Smoothing", &engine.m_sop_smoothing_factor, 0.0f, 1.0f, "%.2f",
715 : : Tooltips::SOP_SMOOTHING,
716 : 377 : [&]() {
717 : 377 : int ms = (int)std::lround(engine.m_sop_smoothing_factor * 100.0f);
718 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
719 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
720 : 377 : });
721 : :
722 [ + - ]: 377 : FloatSetting("Grip Smoothing", &engine.m_grip_smoothing_steady, 0.000f, 0.100f, "%.3f s",
723 : : Tooltips::GRIP_SMOOTHING);
724 : :
725 [ + - ]: 377 : FloatSetting(" SoP Scale", &engine.m_sop_scale, 0.0f, 20.0f, "%.2f", Tooltips::SOP_SCALE);
726 : :
727 [ + - ]: 377 : ImGui::TreePop();
728 : : } else {
729 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
730 : : }
731 : :
732 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Grip & Slip Angle Estimation", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
733 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
734 : :
735 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Enable Dynamic Load Sensitivity", &engine.m_load_sensitivity_enabled, Tooltips::LOAD_SENSITIVITY_ENABLE).deactivated) {
736 [ # # ]: 0 : std::lock_guard<std::recursive_mutex> lock(g_engine_mutex);
737 [ # # # # ]: 0 : Config::Save(engine);
738 : 0 : }
739 : :
740 [ + - ]: 377 : FloatSetting("Slip Angle Smoothing", &engine.m_slip_angle_smoothing, 0.000f, 0.100f, "%.3f s",
741 : : Tooltips::SLIP_ANGLE_SMOOTHING,
742 : 377 : [&]() {
743 : 377 : int ms = (int)std::lround(engine.m_slip_angle_smoothing * 1000.0f);
744 [ + - ]: 377 : ImVec4 color = (ms < LATENCY_WARNING_THRESHOLD_MS) ? ImVec4(0,1,0,1) : ImVec4(1,0,0,1);
745 [ + - + - ]: 377 : ImGui::TextColored(color, "Latency: %d ms - %s", ms, (ms < LATENCY_WARNING_THRESHOLD_MS) ? "OK" : "High");
746 : 377 : });
747 : :
748 [ + - ]: 377 : FloatSetting("Chassis Inertia (Load)", &engine.m_chassis_inertia_smoothing, 0.000f, 0.100f, "%.3f s",
749 : : Tooltips::CHASSIS_INERTIA,
750 : 377 : [&]() {
751 : 377 : int ms = (int)std::lround(engine.m_chassis_inertia_smoothing * 1000.0f);
752 [ + - ]: 377 : ImGui::TextColored(ImVec4(0.5f, 0.5f, 1.0f, 1.0f), "Simulation: %d ms", ms);
753 : 377 : });
754 : :
755 [ + - ]: 377 : FloatSetting("Optimal Slip Angle", &engine.m_optimal_slip_angle, 0.040f, 0.200f, "%.3f rad",
756 : : Tooltips::OPTIMAL_SLIP_ANGLE);
757 [ + - ]: 377 : FloatSetting("Optimal Slip Ratio", &engine.m_optimal_slip_ratio, 0.04f, 0.20f, "%.3f",
758 : : Tooltips::OPTIMAL_SLIP_RATIO);
759 : :
760 [ + - ]: 377 : ImGui::Separator();
761 [ + - ]: 377 : ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "Slope Detection (Experimental)");
762 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
763 : :
764 : 377 : bool prev_slope_enabled = engine.m_slope_detection_enabled;
765 [ + - ]: 377 : GuiWidgets::Result slope_res = GuiWidgets::Checkbox("Enable Slope Detection", &engine.m_slope_detection_enabled,
766 : : Tooltips::SLOPE_DETECTION_ENABLE);
767 : :
768 [ - + ]: 377 : if (slope_res.changed) {
769 [ # # # # ]: 0 : if (!prev_slope_enabled && engine.m_slope_detection_enabled) {
770 : 0 : engine.m_slope_buffer_count = 0;
771 : 0 : engine.m_slope_buffer_index = 0;
772 : 0 : engine.m_slope_smoothed_output = 1.0;
773 : : }
774 : : }
775 [ - + ]: 377 : if (slope_res.deactivated) {
776 [ # # # # ]: 0 : Config::Save(engine);
777 : : }
778 : :
779 [ + + + + ]: 377 : if (engine.m_slope_detection_enabled && engine.m_oversteer_boost > 0.01f) {
780 [ + - ]: 362 : ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
781 : : "Note: Lateral G Boost (Slide) is auto-disabled when Slope Detection is ON.");
782 [ + - + - ]: 362 : ImGui::NextColumn(); ImGui::NextColumn();
783 : : }
784 : :
785 [ + + ]: 377 : if (engine.m_slope_detection_enabled) {
786 : 363 : int window = engine.m_slope_sg_window;
787 [ + - - + ]: 363 : if (ImGui::SliderInt(" Filter Window", &window, 5, 41)) {
788 [ # # ]: 0 : if (window % 2 == 0) window++;
789 : 0 : engine.m_slope_sg_window = window;
790 : : }
791 [ + - - + ]: 363 : if (ImGui::IsItemHovered()) {
792 [ # # ]: 0 : ImGui::SetTooltip("%s", Tooltips::SLOPE_FILTER_WINDOW);
793 : : }
794 [ + - - + : 363 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
- - - - ]
795 : :
796 [ + - ]: 363 : ImGui::SameLine();
797 : 363 : float latency_ms = (static_cast<float>(engine.m_slope_sg_window) / 2.0f) * 2.5f;
798 [ + - ]: 363 : ImVec4 color = (latency_ms < 25.0f) ? ImVec4(0,1,0,1) : ImVec4(1,0.5f,0,1);
799 [ + - ]: 363 : ImGui::TextColored(color, "~%.0f ms latency", latency_ms);
800 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
801 : :
802 [ + - ]: 363 : FloatSetting(" Sensitivity", &engine.m_slope_sensitivity, 0.1f, 5.0f, "%.1fx",
803 : : Tooltips::SLOPE_SENSITIVITY);
804 : :
805 [ + - - + ]: 363 : if (ImGui::TreeNode("Advanced Slope Settings")) {
806 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
807 [ # # ]: 0 : FloatSetting(" Slope Threshold", &engine.m_slope_min_threshold, -1.0f, 0.0f, "%.2f", Tooltips::SLOPE_THRESHOLD);
808 [ # # ]: 0 : FloatSetting(" Output Smoothing", &engine.m_slope_smoothing_tau, 0.005f, 0.100f, "%.3f s", Tooltips::SLOPE_OUTPUT_SMOOTHING);
809 : :
810 [ # # ]: 0 : ImGui::Separator();
811 [ # # ]: 0 : ImGui::Text("Stability Fixes (v0.7.3)");
812 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
813 [ # # ]: 0 : FloatSetting(" Alpha Threshold", &engine.m_slope_alpha_threshold, 0.001f, 0.100f, "%.3f", Tooltips::SLOPE_ALPHA_THRESHOLD);
814 [ # # ]: 0 : FloatSetting(" Decay Rate", &engine.m_slope_decay_rate, 0.5f, 20.0f, "%.1f", Tooltips::SLOPE_DECAY_RATE);
815 [ # # ]: 0 : BoolSetting(" Confidence Gate", &engine.m_slope_confidence_enabled, Tooltips::SLOPE_CONFIDENCE_GATE);
816 : :
817 [ # # ]: 0 : ImGui::TreePop();
818 : : } else {
819 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
820 : : }
821 : :
822 : 363 : ImGui::Text(" Live Slope: %.3f | Grip: %.0f%%",
823 : : engine.m_slope_current,
824 [ + - ]: 363 : engine.m_slope_smoothed_output * 100.0f);
825 [ + - + - ]: 363 : ImGui::NextColumn(); ImGui::NextColumn();
826 : : }
827 : :
828 [ + - ]: 377 : ImGui::TreePop();
829 : : } else {
830 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
831 : : }
832 : :
833 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Braking & Lockup", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
834 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
835 : :
836 [ + - ]: 377 : BoolSetting("Lockup Vibration", &engine.m_lockup_enabled, Tooltips::LOCKUP_VIBRATION);
837 [ + + ]: 377 : if (engine.m_lockup_enabled) {
838 [ + - ]: 373 : FloatSetting(" Lockup Strength", &engine.m_lockup_gain, 0.0f, 3.0f, FormatDecoupled(engine.m_lockup_gain, FFBEngine::BASE_NM_LOCKUP_VIBRATION), Tooltips::LOCKUP_STRENGTH);
839 [ + - ]: 373 : FloatSetting(" Brake Load Cap", &engine.m_brake_load_cap, 1.0f, 10.0f, "%.2fx", Tooltips::BRAKE_LOAD_CAP);
840 [ + - ]: 373 : FloatSetting(" Vibration Pitch", &engine.m_lockup_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::VIBRATION_PITCH);
841 : :
842 [ + - ]: 373 : ImGui::Separator();
843 [ + - ]: 373 : ImGui::Text("Response Curve");
844 [ + - + - ]: 373 : ImGui::NextColumn(); ImGui::NextColumn();
845 : :
846 [ + - ]: 373 : FloatSetting(" Gamma", &engine.m_lockup_gamma, 0.1f, 3.0f, "%.1f", Tooltips::LOCKUP_GAMMA);
847 [ + - ]: 373 : FloatSetting(" Start Slip %", &engine.m_lockup_start_pct, 1.0f, 10.0f, "%.1f%%", Tooltips::LOCKUP_START_PCT);
848 [ + - ]: 373 : FloatSetting(" Full Slip %", &engine.m_lockup_full_pct, 5.0f, 25.0f, "%.1f%%", Tooltips::LOCKUP_FULL_PCT);
849 : :
850 [ + - ]: 373 : ImGui::Separator();
851 [ + - ]: 373 : ImGui::Text("Prediction (Advanced)");
852 [ + - + - ]: 373 : ImGui::NextColumn(); ImGui::NextColumn();
853 : :
854 [ + - ]: 373 : FloatSetting(" Sensitivity", &engine.m_lockup_prediction_sens, 10.0f, 100.0f, "%.0f", Tooltips::LOCKUP_PREDICTION_SENS);
855 [ + - ]: 373 : FloatSetting(" Bump Rejection", &engine.m_lockup_bump_reject, 0.1f, 5.0f, "%.1f m/s", Tooltips::LOCKUP_BUMP_REJECT);
856 [ + - ]: 373 : FloatSetting(" Rear Boost", &engine.m_lockup_rear_boost, 1.0f, 10.0f, "%.2fx", Tooltips::LOCKUP_REAR_BOOST);
857 : : }
858 : :
859 [ + - ]: 377 : ImGui::Separator();
860 [ + - ]: 377 : ImGui::Text("ABS & Hardware");
861 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
862 : :
863 [ + - ]: 377 : BoolSetting("ABS Pulse", &engine.m_abs_pulse_enabled, Tooltips::ABS_PULSE);
864 [ + + ]: 377 : if (engine.m_abs_pulse_enabled) {
865 [ + - ]: 360 : FloatSetting(" Pulse Gain", &engine.m_abs_gain, 0.0f, 10.0f, "%.2f", Tooltips::ABS_PULSE_GAIN);
866 [ + - ]: 360 : FloatSetting(" Pulse Frequency", &engine.m_abs_freq_hz, 10.0f, 50.0f, "%.1f Hz", Tooltips::ABS_PULSE_FREQ);
867 : : }
868 : :
869 [ + - ]: 377 : ImGui::TreePop();
870 : : } else {
871 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
872 : : }
873 : :
874 [ + - + - ]: 377 : if (ImGui::TreeNodeEx("Vibration Effects", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_Framed)) {
875 [ + - + - ]: 377 : ImGui::NextColumn(); ImGui::NextColumn();
876 : :
877 : 377 : bool prev_vibration_norm = engine.m_general.auto_load_normalization_enabled;
878 [ + - - + ]: 377 : if (GuiWidgets::Checkbox("Enable Dynamic Load Normalization", &engine.m_general.auto_load_normalization_enabled, Tooltips::DYNAMIC_LOAD_NORMALIZATION_ENABLE).changed) {
879 [ # # # # ]: 0 : if (prev_vibration_norm && !engine.m_general.auto_load_normalization_enabled) {
880 [ # # ]: 0 : engine.ResetNormalization();
881 : : }
882 [ # # # # ]: 0 : Config::Save(engine);
883 : : }
884 : :
885 [ + - ]: 377 : FloatSetting("Texture Load Cap", &engine.m_texture_load_cap, 1.0f, 3.0f, "%.2fx", Tooltips::TEXTURE_LOAD_CAP);
886 [ + - ]: 377 : FloatSetting("Vibration Strength", &engine.m_vibration_gain, 0.0f, 2.0f, FormatPct(engine.m_vibration_gain), Tooltips::VIBRATION_GAIN);
887 : :
888 [ + - ]: 377 : BoolSetting("Slide Rumble", &engine.m_slide_texture_enabled, Tooltips::SLIDE_RUMBLE);
889 [ + + ]: 377 : if (engine.m_slide_texture_enabled) {
890 [ + - ]: 359 : FloatSetting(" Slide Gain", &engine.m_slide_texture_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_slide_texture_gain, FFBEngine::BASE_NM_SLIDE_TEXTURE), Tooltips::SLIDE_GAIN);
891 [ + - ]: 359 : FloatSetting(" Slide Pitch", &engine.m_slide_freq_scale, 0.5f, 5.0f, "%.2fx", Tooltips::SLIDE_PITCH);
892 : : }
893 : :
894 [ + - ]: 377 : BoolSetting("Road Details", &engine.m_road_texture_enabled, Tooltips::ROAD_DETAILS);
895 [ + + ]: 377 : if (engine.m_road_texture_enabled) {
896 [ + - ]: 373 : FloatSetting(" Road Gain", &engine.m_road_texture_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_road_texture_gain, FFBEngine::BASE_NM_ROAD_TEXTURE), Tooltips::ROAD_GAIN);
897 : : }
898 : :
899 [ + - ]: 377 : BoolSetting("Spin Vibration", &engine.m_spin_enabled, Tooltips::SPIN_VIBRATION);
900 [ + + ]: 377 : if (engine.m_spin_enabled) {
901 [ + - ]: 373 : FloatSetting(" Spin Strength", &engine.m_spin_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_spin_gain, FFBEngine::BASE_NM_SPIN_VIBRATION), Tooltips::SPIN_STRENGTH);
902 [ + - ]: 373 : FloatSetting(" Spin Pitch", &engine.m_spin_freq_scale, 0.5f, 2.0f, "%.2fx", Tooltips::SPIN_PITCH);
903 : : }
904 : :
905 [ + - ]: 377 : FloatSetting("Scrub Drag", &engine.m_scrub_drag_gain, 0.0f, 1.0f, FormatDecoupled(engine.m_scrub_drag_gain, FFBEngine::BASE_NM_SCRUB_DRAG), Tooltips::SCRUB_DRAG);
906 : :
907 [ + - ]: 377 : BoolSetting("Bottoming Effect", &engine.m_bottoming_enabled, Tooltips::BOTTOMING_EFFECT);
908 [ + + ]: 377 : if (engine.m_bottoming_enabled) {
909 [ + - ]: 373 : FloatSetting(" Bottoming Strength", &engine.m_bottoming_gain, 0.0f, 2.0f, FormatDecoupled(engine.m_bottoming_gain, FFBEngine::BASE_NM_BOTTOMING), Tooltips::BOTTOMING_STRENGTH);
910 : 373 : const char* bottoming_modes[] = { "Method A: Scraping", "Method B: Susp. Spike" };
911 [ + - ]: 373 : IntSetting(" Bottoming Logic", &engine.m_bottoming_method, bottoming_modes, sizeof(bottoming_modes)/sizeof(bottoming_modes[0]), Tooltips::BOTTOMING_LOGIC);
912 : : }
913 : :
914 [ + - ]: 377 : ImGui::TreePop();
915 : : } else {
916 [ # # # # ]: 0 : ImGui::NextColumn(); ImGui::NextColumn();
917 : : }
918 : :
919 [ + - - + ]: 377 : if (ImGui::CollapsingHeader("Advanced Settings")) {
920 [ # # ]: 0 : ImGui::Indent();
921 : :
922 [ # # # # ]: 0 : if (ImGui::TreeNode("Stationary Vibration Gate")) {
923 : 0 : float lower_kmh = engine.m_speed_gate_lower * 3.6f;
924 [ # # # # ]: 0 : if (ImGui::SliderFloat("Mute Below", &lower_kmh, 0.0f, 20.0f, "%.1f km/h")) {
925 : 0 : engine.m_speed_gate_lower = lower_kmh / 3.6f;
926 [ # # ]: 0 : if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
927 : 0 : engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
928 : : }
929 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::MUTE_BELOW);
# # ]
930 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
931 : :
932 : 0 : float upper_kmh = engine.m_speed_gate_upper * 3.6f;
933 [ # # # # ]: 0 : if (ImGui::SliderFloat("Full Above", &upper_kmh, 1.0f, 50.0f, "%.1f km/h")) {
934 : 0 : engine.m_speed_gate_upper = upper_kmh / 3.6f;
935 [ # # ]: 0 : if (engine.m_speed_gate_upper <= engine.m_speed_gate_lower + 0.1f)
936 : 0 : engine.m_speed_gate_upper = engine.m_speed_gate_lower + 0.5f;
937 : : }
938 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::FULL_ABOVE);
# # ]
939 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
940 : :
941 [ # # ]: 0 : ImGui::TreePop();
942 : : }
943 : :
944 [ # # # # ]: 0 : if (ImGui::TreeNode("Telemetry Logger")) {
945 [ # # # # ]: 0 : if (ImGui::Checkbox("Enable Logging Logic", &Config::m_auto_start_logging)) {
946 [ # # # # ]: 0 : Config::Save(engine);
947 : : }
948 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::AUTO_START_LOGGING);
# # ]
949 : :
950 : : char log_path_buf[256];
951 : 0 : StringUtils::SafeCopy(log_path_buf, sizeof(log_path_buf), Config::m_log_path.c_str());
952 [ # # # # ]: 0 : if (ImGui::InputText("Log Path", log_path_buf, 255)) {
953 [ # # ]: 0 : Config::m_log_path = log_path_buf;
954 : : }
955 [ # # # # : 0 : if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", Tooltips::LOG_PATH);
# # ]
956 [ # # # # : 0 : if (ImGui::IsItemDeactivatedAfterEdit()) Config::Save(engine);
# # # # ]
957 : :
958 [ # # # # ]: 0 : if (AsyncLogger::Get().IsLogging()) {
959 [ # # # # : 0 : ImGui::BulletText("Filename: %s", AsyncLogger::Get().GetFilename().c_str());
# # ]
960 : : }
961 : :
962 [ # # ]: 0 : ImGui::TreePop();
963 : : }
964 [ # # ]: 0 : ImGui::Unindent();
965 : : }
966 : :
967 [ + - ]: 377 : ImGui::Columns(1);
968 [ + - ]: 377 : ImGui::End();
969 : 377 : }
970 : :
971 : : const float PLOT_HISTORY_SEC = 10.0f;
972 : : const int PHYSICS_RATE_HZ = 400;
973 : : const int PLOT_BUFFER_SIZE = (int)(PLOT_HISTORY_SEC * PHYSICS_RATE_HZ);
974 : :
975 : : struct RollingBuffer {
976 : : std::vector<float> data;
977 : : int offset = 0;
978 : :
979 : 47 : RollingBuffer() {
980 [ + - ]: 47 : data.resize(PLOT_BUFFER_SIZE, 0.0f);
981 : 47 : }
982 : :
983 : 0 : void Add(float val) {
984 : 0 : data[offset] = val;
985 : 0 : offset = (offset + 1) % (int)data.size();
986 : 0 : }
987 : :
988 : 1054 : float GetCurrent() const {
989 [ - + ]: 1054 : if (data.empty()) return 0.0f;
990 : 1054 : size_t idx = (offset - 1 + (int)data.size()) % (int)data.size();
991 : 1054 : return data[idx];
992 : : }
993 : :
994 : 1054 : float GetMin() const {
995 [ - + ]: 1054 : if (data.empty()) return 0.0f;
996 [ + - ]: 1054 : return *std::min_element(data.begin(), data.end());
997 : : }
998 : :
999 : 1054 : float GetMax() const {
1000 [ - + ]: 1054 : if (data.empty()) return 0.0f;
1001 [ + - ]: 1054 : return *std::max_element(data.begin(), data.end());
1002 : : }
1003 : : };
1004 : :
1005 : 1054 : inline void PlotWithStats(const char* label, const RollingBuffer& buffer,
1006 : : float scale_min, float scale_max,
1007 : : const ImVec2& size = ImVec2(0, 40),
1008 : : const char* tooltip = nullptr) {
1009 [ + - ]: 1054 : ImGui::Text("%s", label);
1010 : : char hidden_label[256];
1011 : 1054 : StringUtils::SafeFormat(hidden_label, sizeof(hidden_label), "##%s", label);
1012 [ + - ]: 1054 : ImGui::PlotLines(hidden_label, buffer.data.data(), (int)buffer.data.size(),
1013 : 1054 : buffer.offset, NULL, scale_min, scale_max, size);
1014 [ - + - - : 1054 : if (tooltip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tooltip);
- - - + -
- ]
1015 : :
1016 : 1054 : float current = buffer.GetCurrent();
1017 [ + - ]: 1054 : float min_val = buffer.GetMin();
1018 [ + - ]: 1054 : float max_val = buffer.GetMax();
1019 : : char stats_overlay[128];
1020 : 1054 : StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "Cur:%.4f Min:%.3f Max:%.3f", current, min_val, max_val);
1021 : :
1022 [ + - ]: 1054 : ImVec2 p_min = ImGui::GetItemRectMin();
1023 [ + - ]: 1054 : ImVec2 p_max = ImGui::GetItemRectMax();
1024 : 1054 : float plot_width = p_max.x - p_min.x;
1025 : 1054 : p_min.x += 2; p_min.y += 2;
1026 : :
1027 [ + - ]: 1054 : ImDrawList* draw_list = ImGui::GetWindowDrawList();
1028 [ + - ]: 1054 : ImFont* font = ImGui::GetFont();
1029 [ + - ]: 1054 : float font_size = ImGui::GetFontSize();
1030 [ + - ]: 1054 : ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
1031 : :
1032 [ - + ]: 1054 : if (text_size.x > plot_width - 4) {
1033 : 0 : StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "%.4f [%.3f, %.3f]", current, min_val, max_val);
1034 [ # # ]: 0 : text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
1035 [ # # ]: 0 : if (text_size.x > plot_width - 4) {
1036 : 0 : StringUtils::SafeFormat(stats_overlay, sizeof(stats_overlay), "Val: %.4f", current);
1037 [ # # ]: 0 : text_size = font->CalcTextSizeA(font_size, FLT_MAX, 0.0f, stats_overlay);
1038 : : }
1039 : : }
1040 [ + - ]: 1054 : draw_list->AddRectFilled(ImVec2(p_min.x - 1, p_min.y), ImVec2(p_min.x + text_size.x + 2, p_min.y + text_size.y), IM_COL32(0, 0, 0, 90));
1041 [ + - ]: 1054 : draw_list->AddText(font, font_size, p_min, IM_COL32(255, 255, 255, 255), stats_overlay);
1042 : 1054 : }
1043 : :
1044 : : // Global Buffers
1045 : : static RollingBuffer plot_total, plot_base, plot_sop, plot_yaw_kick, plot_rear_torque, plot_gyro_damping, plot_stationary_damping, plot_scrub_drag, plot_soft_lock, plot_oversteer, plot_understeer, plot_clipping, plot_road, plot_slide, plot_lockup, plot_spin, plot_bottoming;
1046 : : static RollingBuffer plot_calc_front_load, plot_calc_rear_load, plot_calc_front_grip, plot_calc_rear_grip, plot_calc_slip_ratio, plot_calc_slip_angle_smoothed, plot_calc_rear_slip_angle_smoothed, plot_slope_current, plot_calc_rear_lat_force;
1047 : : static RollingBuffer plot_raw_steer, plot_raw_shaft_torque, plot_raw_gen_torque, plot_raw_input_steering, plot_raw_throttle, plot_raw_brake, plot_input_accel, plot_raw_car_speed, plot_raw_load, plot_raw_grip, plot_raw_rear_grip, plot_raw_front_slip_ratio, plot_raw_susp_force, plot_raw_ride_height, plot_raw_front_lat_patch_vel, plot_raw_front_long_patch_vel, plot_raw_rear_lat_patch_vel, plot_raw_rear_long_patch_vel, plot_raw_slip_angle, plot_raw_rear_slip_angle, plot_raw_front_deflection;
1048 : :
1049 : : static bool g_warn_dt = false;
1050 : :
1051 : 0 : void GuiLayer::UpdateTelemetry(FFBEngine& engine) {
1052 [ # # ]: 0 : auto snapshots = engine.GetDebugBatch();
1053 [ # # ]: 0 : for (const auto& snap : snapshots) {
1054 : 0 : m_latest_steering_range = snap.steering_range_deg;
1055 : 0 : m_latest_steering_angle = snap.steering_angle_deg;
1056 : :
1057 : 0 : plot_total.Add(snap.total_output);
1058 : 0 : plot_base.Add(snap.base_force);
1059 : 0 : plot_sop.Add(snap.sop_force);
1060 : 0 : plot_yaw_kick.Add(snap.ffb_yaw_kick);
1061 : 0 : plot_rear_torque.Add(snap.ffb_rear_torque);
1062 : 0 : plot_gyro_damping.Add(snap.ffb_gyro_damping);
1063 : 0 : plot_stationary_damping.Add(snap.ffb_stationary_damping);
1064 : 0 : plot_scrub_drag.Add(snap.ffb_scrub_drag);
1065 : 0 : plot_soft_lock.Add(snap.ffb_soft_lock);
1066 : 0 : plot_oversteer.Add(snap.oversteer_boost);
1067 : 0 : plot_understeer.Add(snap.understeer_drop);
1068 : 0 : plot_clipping.Add(snap.clipping);
1069 : 0 : plot_road.Add(snap.texture_road);
1070 : 0 : plot_slide.Add(snap.texture_slide);
1071 : 0 : plot_lockup.Add(snap.texture_lockup);
1072 : 0 : plot_spin.Add(snap.texture_spin);
1073 : 0 : plot_bottoming.Add(snap.texture_bottoming);
1074 : 0 : plot_calc_front_load.Add(snap.calc_front_load);
1075 : 0 : plot_calc_rear_load.Add(snap.calc_rear_load);
1076 : 0 : plot_calc_front_grip.Add(snap.calc_front_grip);
1077 : 0 : plot_calc_rear_grip.Add(snap.calc_rear_grip);
1078 : 0 : plot_calc_slip_ratio.Add(snap.calc_front_slip_ratio);
1079 : 0 : plot_calc_slip_angle_smoothed.Add(snap.calc_front_slip_angle_smoothed);
1080 : 0 : plot_calc_rear_slip_angle_smoothed.Add(snap.calc_rear_slip_angle_smoothed);
1081 : 0 : plot_calc_rear_lat_force.Add(snap.calc_rear_lat_force);
1082 : 0 : plot_slope_current.Add(snap.slope_current);
1083 : 0 : plot_raw_steer.Add(snap.steer_force);
1084 : 0 : plot_raw_shaft_torque.Add(snap.raw_shaft_torque);
1085 : 0 : plot_raw_gen_torque.Add(snap.raw_gen_torque);
1086 : 0 : plot_raw_input_steering.Add(snap.raw_input_steering);
1087 : 0 : plot_raw_throttle.Add(snap.raw_input_throttle);
1088 : 0 : plot_raw_brake.Add(snap.raw_input_brake);
1089 : 0 : plot_input_accel.Add(snap.accel_x);
1090 : 0 : plot_raw_car_speed.Add(snap.raw_car_speed);
1091 : 0 : plot_raw_load.Add(snap.raw_front_tire_load);
1092 : 0 : plot_raw_grip.Add(snap.raw_front_grip_fract);
1093 : 0 : plot_raw_rear_grip.Add(snap.raw_rear_grip);
1094 : 0 : plot_raw_front_slip_ratio.Add(snap.raw_front_slip_ratio);
1095 : 0 : plot_raw_susp_force.Add(snap.raw_front_susp_force);
1096 : 0 : plot_raw_ride_height.Add(snap.raw_front_ride_height);
1097 : 0 : plot_raw_front_lat_patch_vel.Add(snap.raw_front_lat_patch_vel);
1098 : 0 : plot_raw_front_long_patch_vel.Add(snap.raw_front_long_patch_vel);
1099 : 0 : plot_raw_rear_lat_patch_vel.Add(snap.raw_rear_lat_patch_vel);
1100 : 0 : plot_raw_rear_long_patch_vel.Add(snap.raw_rear_long_patch_vel);
1101 : 0 : plot_raw_slip_angle.Add(snap.raw_front_slip_angle);
1102 : 0 : plot_raw_rear_slip_angle.Add(snap.raw_rear_slip_angle);
1103 : 0 : plot_raw_front_deflection.Add(snap.raw_front_deflection);
1104 : 0 : g_warn_dt = snap.warn_dt;
1105 : : }
1106 : 0 : }
1107 : :
1108 : 63 : void GuiLayer::DrawDebugWindow(FFBEngine& engine) {
1109 [ + + ]: 63 : if (!Config::show_graphs) return;
1110 : :
1111 : 62 : ImGuiViewport* viewport = ImGui::GetMainViewport();
1112 [ + - ]: 62 : ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + CONFIG_PANEL_WIDTH, viewport->WorkPos.y));
1113 [ + - ]: 62 : ImGui::SetNextWindowSize(ImVec2(viewport->WorkSize.x - CONFIG_PANEL_WIDTH, viewport->WorkSize.y));
1114 : :
1115 : 62 : ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
1116 : 62 : ImGui::Begin("FFB Analysis", nullptr, flags);
1117 : :
1118 : : // System Health Diagnostics (Moved from Tuning window - Issue #149)
1119 [ + - ]: 62 : if (ImGui::CollapsingHeader("System Health", ImGuiTreeNodeFlags_DefaultOpen)) {
1120 : 310 : HealthStatus hs = HealthMonitor::Check(engine.m_ffb_rate, engine.m_telemetry_rate, engine.m_gen_torque_rate, engine.m_torque_source, engine.m_physics_rate,
1121 [ + - + - : 62 : GameConnector::Get().IsConnected(), GameConnector::Get().IsSessionActive(), GameConnector::Get().GetSessionType(), GameConnector::Get().IsInRealtime(), GameConnector::Get().GetPlayerControl());
+ - + - +
- + - ]
1122 : :
1123 [ + - ]: 62 : ImGui::Columns(6, "RateCols", false);
1124 [ + - ]: 62 : DisplayRate("USB Loop", engine.m_ffb_rate, 1000.0);
1125 [ + - ]: 62 : ImGui::NextColumn();
1126 [ + - ]: 62 : DisplayRate("Physics", engine.m_physics_rate, 400.0);
1127 [ + - ]: 62 : ImGui::NextColumn();
1128 [ + - ]: 62 : DisplayRate("Telemetry", engine.m_telemetry_rate, 100.0); // Standard LMU is 100Hz
1129 [ + - ]: 62 : ImGui::NextColumn();
1130 [ + - ]: 62 : DisplayRate("Hardware", engine.m_hw_rate, 1000.0);
1131 [ + - ]: 62 : ImGui::NextColumn();
1132 [ + - ]: 62 : DisplayRate("S.Torque", engine.m_torque_rate, 100.0);
1133 [ + - ]: 62 : ImGui::NextColumn();
1134 [ + - ]: 62 : DisplayRate("G.Torque", engine.m_gen_torque_rate, 400.0);
1135 [ + - ]: 62 : ImGui::Columns(1);
1136 : :
1137 [ + - ]: 62 : ImGui::Separator();
1138 : :
1139 : : // Robust State Machine (#269, #274)
1140 [ + + ]: 62 : if (!hs.is_connected) {
1141 [ + - ]: 55 : ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Sim: Disconnected from LMU");
1142 : : } else {
1143 : 7 : bool active = hs.session_active;
1144 [ - + - + : 7 : ImGui::TextColored(active ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(0.7f, 0.7f, 0.7f, 1.0f),
+ - ]
1145 : : "Sim: %s", active ? "Track Loaded" : "Main Menu");
1146 : : }
1147 : :
1148 [ + + - + ]: 62 : if (hs.is_connected && hs.session_active) {
1149 [ # # ]: 0 : ImGui::SameLine();
1150 : 0 : const char* sessionStr = "Unknown";
1151 : 0 : long stype = hs.session_type;
1152 [ # # ]: 0 : if (stype == 0) sessionStr = "Test Day";
1153 [ # # # # ]: 0 : else if (stype >= 1 && stype <= 4) sessionStr = "Practice";
1154 [ # # # # ]: 0 : else if (stype >= 5 && stype <= 8) sessionStr = "Qualifying";
1155 [ # # ]: 0 : else if (stype == 9) sessionStr = "Warmup";
1156 [ # # # # ]: 0 : else if (stype >= 10 && stype <= 13) sessionStr = "Race";
1157 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "| Session: %s", sessionStr);
1158 : :
1159 : 0 : bool driving = hs.is_realtime;
1160 [ # # # # : 0 : ImGui::TextColored(driving ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(1.0f, 1.0f, 0.4f, 1.0f),
# # ]
1161 : : "State: %s", driving ? "Driving" : "In Menu");
1162 : :
1163 [ # # ]: 0 : ImGui::SameLine();
1164 : 0 : const char* ctrlStr = "Unknown";
1165 : 0 : signed char ctrl = hs.player_control;
1166 [ # # # # : 0 : switch (ctrl) {
# # ]
1167 : 0 : case -1: ctrlStr = "Nobody"; break;
1168 : 0 : case 0: ctrlStr = "Player"; break;
1169 : 0 : case 1: ctrlStr = "AI"; break;
1170 : 0 : case 2: ctrlStr = "Remote"; break;
1171 : 0 : case 3: ctrlStr = "Replay"; break;
1172 : 0 : default: ctrlStr = "Unknown"; break;
1173 : : }
1174 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "| Control: %s", ctrlStr);
1175 : : }
1176 : :
1177 [ + + + + : 62 : if (!hs.is_healthy && engine.m_telemetry_rate > 1.0 && GameConnector::Get().IsConnected()) {
+ - + - +
- + + ]
1178 [ + - ]: 6 : ImGui::TextColored(ImVec4(1, 1, 0, 1), "Warning: Sub-optimal sample rates detected. Check game settings.");
1179 : : }
1180 [ + - ]: 62 : ImGui::Separator();
1181 : : }
1182 : :
1183 : :
1184 [ - + ]: 62 : if (g_warn_dt) {
1185 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
1186 : 0 : ImGui::Text("TELEMETRY WARNINGS: - Invalid DeltaTime");
1187 : 0 : ImGui::PopStyleColor();
1188 : 0 : ImGui::Separator();
1189 : : }
1190 : :
1191 [ + - ]: 62 : if (ImGui::CollapsingHeader("A. FFB Components (Output)", ImGuiTreeNodeFlags_DefaultOpen)) {
1192 [ + - ]: 62 : PlotWithStats("Total Output", plot_total, -1.0f, 1.0f, ImVec2(0, 60));
1193 : 62 : ImGui::Separator();
1194 : 62 : ImGui::Columns(3, "FFBMain", false);
1195 [ + - ]: 62 : ImGui::TextColored(ImVec4(0.7f, 0.7f, 1.0f, 1.0f), "[Main Forces]");
1196 [ + - ]: 62 : PlotWithStats("Base Torque (Nm)", plot_base, -30.0f, 30.0f);
1197 [ + - ]: 62 : PlotWithStats("SoP (Chassis G)", plot_sop, -20.0f, 20.0f);
1198 [ + - ]: 62 : PlotWithStats("Yaw Kick", plot_yaw_kick, -20.0f, 20.0f);
1199 [ + - ]: 62 : PlotWithStats("Rear Align", plot_rear_torque, -20.0f, 20.0f);
1200 [ + - ]: 62 : PlotWithStats("Gyro Damping", plot_gyro_damping, -20.0f, 20.0f);
1201 [ + - ]: 62 : PlotWithStats("Stationary Damping", plot_stationary_damping, -20.0f, 20.0f);
1202 [ + - ]: 62 : PlotWithStats("Scrub Drag", plot_scrub_drag, -20.0f, 20.0f);
1203 [ + - ]: 62 : PlotWithStats("Soft Lock", plot_soft_lock, -50.0f, 50.0f);
1204 : 62 : ImGui::NextColumn();
1205 [ + - ]: 62 : ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.7f, 1.0f), "[Modifiers]");
1206 [ + - ]: 62 : PlotWithStats("Lateral G Boost", plot_oversteer, -20.0f, 20.0f);
1207 [ + - ]: 62 : PlotWithStats("Understeer Cut", plot_understeer, -20.0f, 20.0f);
1208 [ + - ]: 62 : PlotWithStats("Clipping", plot_clipping, 0.0f, 1.1f);
1209 : 62 : ImGui::NextColumn();
1210 [ + - ]: 62 : ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "[Textures]");
1211 [ + - ]: 62 : PlotWithStats("Road Texture", plot_road, -10.0f, 10.0f);
1212 [ + - ]: 62 : PlotWithStats("Slide Texture", plot_slide, -10.0f, 10.0f);
1213 [ + - ]: 62 : PlotWithStats("Lockup Vib", plot_lockup, -10.0f, 10.0f);
1214 [ + - ]: 62 : PlotWithStats("Spin Vib", plot_spin, -10.0f, 10.0f);
1215 [ + - ]: 62 : PlotWithStats("Bottoming", plot_bottoming, -10.0f, 10.0f);
1216 : 62 : ImGui::Columns(1);
1217 : : }
1218 : :
1219 [ - + ]: 62 : if (ImGui::CollapsingHeader("B. Internal Physics (Brain)", ImGuiTreeNodeFlags_None)) {
1220 [ # # ]: 0 : ImGui::Columns(3, "PhysCols", false);
1221 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Loads]");
1222 [ # # ]: 0 : ImGui::Text("Front: %.0f N | Rear: %.0f N", plot_calc_front_load.GetCurrent(), plot_calc_rear_load.GetCurrent());
1223 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 1.0f, 1.0f));
1224 [ # # ]: 0 : ImGui::PlotLines("##CLoadF", plot_calc_front_load.data.data(), (int)plot_calc_front_load.data.size(), plot_calc_front_load.offset, NULL, 0.0f, 10000.0f, ImVec2(0, 40));
1225 [ # # ]: 0 : ImGui::PopStyleColor();
1226 [ # # ]: 0 : ImVec2 pos_load = ImGui::GetItemRectMin();
1227 [ # # ]: 0 : ImGui::SetCursorScreenPos(pos_load);
1228 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0,0,0,0));
1229 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 1.0f, 1.0f));
1230 [ # # ]: 0 : ImGui::PlotLines("##CLoadR", plot_calc_rear_load.data.data(), (int)plot_calc_rear_load.data.size(), plot_calc_rear_load.offset, NULL, 0.0f, 10000.0f, ImVec2(0, 40));
1231 [ # # ]: 0 : ImGui::PopStyleColor(2);
1232 [ # # ]: 0 : ImGui::NextColumn();
1233 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Grip/Slip]");
1234 [ # # ]: 0 : PlotWithStats("Calc Front Grip", plot_calc_front_grip, 0.0f, 1.2f);
1235 [ # # ]: 0 : PlotWithStats("Calc Rear Grip", plot_calc_rear_grip, 0.0f, 1.2f);
1236 [ # # ]: 0 : PlotWithStats("Front Slip Ratio", plot_calc_slip_ratio, -1.0f, 1.0f);
1237 [ # # ]: 0 : PlotWithStats("Front Slip Angle", plot_calc_slip_angle_smoothed, 0.0f, 1.0f);
1238 [ # # ]: 0 : PlotWithStats("Rear Slip Angle", plot_calc_rear_slip_angle_smoothed, 0.0f, 1.0f);
1239 [ # # # # ]: 0 : if (engine.m_slope_detection_enabled) PlotWithStats("Slope", plot_slope_current, -5.0f, 5.0f);
1240 [ # # ]: 0 : ImGui::NextColumn();
1241 [ # # ]: 0 : ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "[Forces]");
1242 [ # # ]: 0 : PlotWithStats("Calc Rear Lat Force", plot_calc_rear_lat_force, -5000.0f, 5000.0f);
1243 [ # # ]: 0 : ImGui::Columns(1);
1244 : : }
1245 : :
1246 [ - + ]: 62 : if (ImGui::CollapsingHeader("C. Raw Game Telemetry (Input)", ImGuiTreeNodeFlags_None)) {
1247 [ # # ]: 0 : ImGui::Columns(4, "TelCols", false);
1248 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Driver Input]");
1249 [ # # ]: 0 : PlotWithStats("Selected Torque", plot_raw_steer, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SELECTED_TORQUE);
1250 [ # # ]: 0 : PlotWithStats("Shaft Torque (100Hz)", plot_raw_shaft_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_SHAFT_TORQUE);
1251 [ # # ]: 0 : PlotWithStats("In-Game FFB (400Hz)", plot_raw_gen_torque, -30.0f, 30.0f, ImVec2(0, 40), Tooltips::PLOT_INGAME_FFB);
1252 [ # # ]: 0 : PlotWithStats("Steering Input", plot_raw_input_steering, -1.0f, 1.0f);
1253 [ # # ]: 0 : ImGui::Text("Combined Input");
1254 [ # # ]: 0 : ImVec2 pos = ImGui::GetCursorScreenPos();
1255 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
1256 [ # # ]: 0 : ImGui::PlotLines("##BrkComb", plot_raw_brake.data.data(), (int)plot_raw_brake.data.size(), plot_raw_brake.offset, NULL, 0.0f, 1.0f, ImVec2(0, 40));
1257 [ # # ]: 0 : ImGui::PopStyleColor();
1258 [ # # ]: 0 : ImGui::SetCursorScreenPos(pos);
1259 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(0.0f, 1.0f, 0.0f, 1.0f));
1260 [ # # ]: 0 : ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
1261 [ # # ]: 0 : ImGui::PlotLines("##ThrComb", plot_raw_throttle.data.data(), (int)plot_raw_throttle.data.size(), plot_raw_throttle.offset, NULL, 0.0f, 1.0f, ImVec2(0, 40));
1262 [ # # ]: 0 : ImGui::PopStyleColor(2);
1263 [ # # ]: 0 : ImGui::NextColumn();
1264 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Vehicle State]");
1265 [ # # ]: 0 : PlotWithStats("Lat Accel", plot_input_accel, -20.0f, 20.0f);
1266 [ # # ]: 0 : PlotWithStats("Speed (m/s)", plot_raw_car_speed, 0.0f, 100.0f);
1267 [ # # ]: 0 : ImGui::NextColumn();
1268 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Raw Tire Data]");
1269 [ # # ]: 0 : PlotWithStats("Raw Front Load", plot_raw_load, 0.0f, 10000.0f);
1270 [ # # ]: 0 : PlotWithStats("Raw Front Grip", plot_raw_grip, 0.0f, 1.2f);
1271 [ # # ]: 0 : PlotWithStats("Raw Rear Grip", plot_raw_rear_grip, 0.0f, 1.2f);
1272 [ # # ]: 0 : ImGui::NextColumn();
1273 [ # # ]: 0 : ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "[Patch Velocities]");
1274 [ # # ]: 0 : PlotWithStats("F-Lat PatchVel", plot_raw_front_lat_patch_vel, 0.0f, 20.0f);
1275 [ # # ]: 0 : PlotWithStats("R-Lat PatchVel", plot_raw_rear_lat_patch_vel, 0.0f, 20.0f);
1276 [ # # ]: 0 : PlotWithStats("F-Long PatchVel", plot_raw_front_long_patch_vel, -20.0f, 20.0f);
1277 [ # # ]: 0 : PlotWithStats("R-Long PatchVel", plot_raw_rear_long_patch_vel, -20.0f, 20.0f);
1278 [ # # ]: 0 : ImGui::Columns(1);
1279 : : }
1280 : :
1281 : 62 : ImGui::End();
1282 : : }
1283 : : #endif
1284 : :
1285 : : #ifndef ENABLE_IMGUI
1286 : : void GuiLayer::DrawMenuBar(FFBEngine& engine) {}
1287 : : #endif
|