Rigs of Rods 2026.01
Soft-body Physics Simulation
Loading...
Searching...
No Matches
DashBoardManager.cpp
Go to the documentation of this file.
1/*
2 This source file is part of Rigs of Rods
3 Copyright 2005-2012 Pierre-Michel Ricordel
4 Copyright 2007-2012 Thomas Fischer
5
6 For more information, see http://www.rigsofrods.org/
7
8 Rigs of Rods is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License version 3, as
10 published by the Free Software Foundation.
11
12 Rigs of Rods is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with Rigs of Rods. If not, see <http://www.gnu.org/licenses/>.
19*/
20
24
25
26#include "DashBoardManager.h"
27
28#include "Actor.h"
29#include "Application.h"
30#include "CacheSystem.h"
31#include "Console.h"
32#include "GenericFileFormat.h"
33#include "gui/GUIManager.h"
34#include "RTTLayer.h"
35#include "ScriptEngine.h"
36#include "Utils.h"
37
38using namespace Ogre;
39using namespace RoR;
40
41#define INITDATA(key, type, name) data[key] = dashData_t(type, name)
42
43DashBoardManager::DashBoardManager(ActorPtr actor) : visible(true), m_actor(actor)
44{
45 // Reserve memory for built-in inputs.
46 data.resize(DD_MAX);
47
48 // init data
50 INITDATA(DD_ENGINE_SPEEDO_KPH , DC_FLOAT, "speedo_kph");
51 INITDATA(DD_ENGINE_SPEEDO_MPH , DC_FLOAT, "speedo_mph");
52 INITDATA(DD_ENGINE_TURBO , DC_FLOAT, "engine_turbo");
53 INITDATA(DD_ENGINE_IGNITION , DC_BOOL , "engine_ignition");
54 INITDATA(DD_ENGINE_RUNNING , DC_BOOL , "engine_running");
55 INITDATA(DD_ENGINE_OFF , DC_BOOL , "engine_off");
56 INITDATA(DD_ENGINE_BATTERY , DC_BOOL , "engine_battery");
57 INITDATA(DD_ENGINE_CLUTCH_WARNING , DC_BOOL , "engine_clutch_warning");
58 INITDATA(DD_ENGINE_GEAR , DC_INT , "engine_gear");
59 INITDATA(DD_ENGINE_NUM_GEAR , DC_INT , "engine_num_gear");
60 INITDATA(DD_ENGINE_GEAR_STRING , DC_CHAR , "engine_gear_string");
61 INITDATA(DD_ENGINE_AUTOGEAR_STRING , DC_CHAR , "engine_autogear_string");
62 INITDATA(DD_ENGINE_AUTO_GEAR , DC_INT , "engine_auto_gear");
63 INITDATA(DD_ENGINE_CLUTCH , DC_FLOAT, "engine_clutch");
64 INITDATA(DD_BRAKE , DC_FLOAT, "brake");
65 INITDATA(DD_ACCELERATOR , DC_FLOAT, "accelerator");
66 INITDATA(DD_ROLL , DC_FLOAT, "roll");
67 INITDATA(DD_ROLL_CORR , DC_FLOAT, "roll_corr");
68 INITDATA(DD_ROLL_CORR_ACTIVE , DC_BOOL , "roll_corr_active");
69 INITDATA(DD_PITCH , DC_FLOAT, "pitch");
70 INITDATA(DD_PARKINGBRAKE , DC_BOOL , "parkingbrake");
71 INITDATA(DD_LOCKED , DC_BOOL , "locked");
72 INITDATA(DD_LOW_PRESSURE , DC_BOOL , "low_pressure");
73 INITDATA(DD_TRACTIONCONTROL_MODE , DC_INT , "tractioncontrol_mode");
74 INITDATA(DD_ANTILOCKBRAKE_MODE , DC_INT , "antilockbrake_mode");
75 INITDATA(DD_TIES_MODE , DC_INT , "ties_mode");
76 INITDATA(DD_CRUISECONTROL_ACTIVE , DC_BOOL , "cruisecontrol_active");
77 INITDATA(DD_SCREW_THROTTLE_0 , DC_FLOAT, "screw_throttle_0");
78 INITDATA(DD_SCREW_THROTTLE_1 , DC_FLOAT, "screw_throttle_1");
79 INITDATA(DD_SCREW_THROTTLE_2 , DC_FLOAT, "screw_throttle_2");
80 INITDATA(DD_SCREW_THROTTLE_3 , DC_FLOAT, "screw_throttle_3");
81 INITDATA(DD_SCREW_THROTTLE_4 , DC_FLOAT, "screw_throttle_4");
82 INITDATA(DD_SCREW_THROTTLE_5 , DC_FLOAT, "screw_throttle_5");
83 INITDATA(DD_SCREW_STEER_0 , DC_FLOAT, "screw_steer_0");
84 INITDATA(DD_SCREW_STEER_1 , DC_FLOAT, "screw_steer_1");
85 INITDATA(DD_SCREW_STEER_2 , DC_FLOAT, "screw_steer_2");
86 INITDATA(DD_SCREW_STEER_3 , DC_FLOAT, "screw_steer_3");
87 INITDATA(DD_SCREW_STEER_4 , DC_FLOAT, "screw_steer_4");
88 INITDATA(DD_SCREW_STEER_5 , DC_FLOAT, "screw_steer_5");
89 INITDATA(DD_WATER_DEPTH , DC_FLOAT, "water_depth");
90 INITDATA(DD_WATER_SPEED , DC_FLOAT, "water_speed");
91 INITDATA(DD_AEROENGINE_THROTTLE_0 , DC_FLOAT, "aeroengine_throttle_0");
92 INITDATA(DD_AEROENGINE_THROTTLE_1 , DC_FLOAT, "aeroengine_throttle_1");
93 INITDATA(DD_AEROENGINE_THROTTLE_2 , DC_FLOAT, "aeroengine_throttle_2");
94 INITDATA(DD_AEROENGINE_THROTTLE_3 , DC_FLOAT, "aeroengine_throttle_3");
95 INITDATA(DD_AEROENGINE_THROTTLE_4 , DC_FLOAT, "aeroengine_throttle_4");
96 INITDATA(DD_AEROENGINE_THROTTLE_5 , DC_FLOAT, "aeroengine_throttle_5");
97 INITDATA(DD_AEROENGINE_FAILED_0 , DC_BOOL , "aeroengine_failed_0");
98 INITDATA(DD_AEROENGINE_FAILED_1 , DC_BOOL , "aeroengine_failed_1");
99 INITDATA(DD_AEROENGINE_FAILED_2 , DC_BOOL , "aeroengine_failed_2");
100 INITDATA(DD_AEROENGINE_FAILED_3 , DC_BOOL , "aeroengine_failed_3");
101 INITDATA(DD_AEROENGINE_FAILED_4 , DC_BOOL , "aeroengine_failed_4");
102 INITDATA(DD_AEROENGINE_FAILED_5 , DC_BOOL , "aeroengine_failed_5");
103 INITDATA(DD_AEROENGINE_RPM_0 , DC_FLOAT, "aeroengine_rpm_0");
104 INITDATA(DD_AEROENGINE_RPM_1 , DC_FLOAT, "aeroengine_rpm_1");
105 INITDATA(DD_AEROENGINE_RPM_2 , DC_FLOAT, "aeroengine_rpm_2");
106 INITDATA(DD_AEROENGINE_RPM_3 , DC_FLOAT, "aeroengine_rpm_3");
107 INITDATA(DD_AEROENGINE_RPM_4 , DC_FLOAT, "aeroengine_rpm_4");
108 INITDATA(DD_AEROENGINE_RPM_5 , DC_FLOAT, "aeroengine_rpm_5");
109 INITDATA(DD_AIRSPEED , DC_FLOAT, "airspeed");
110 INITDATA(DD_WING_AOA_0 , DC_FLOAT, "wing_aoa_0");
111 INITDATA(DD_WING_AOA_1 , DC_FLOAT, "wing_aoa_1");
112 INITDATA(DD_WING_AOA_2 , DC_FLOAT, "wing_aoa_2");
113 INITDATA(DD_WING_AOA_3 , DC_FLOAT, "wing_aoa_3");
114 INITDATA(DD_WING_AOA_4 , DC_FLOAT, "wing_aoa_4");
115 INITDATA(DD_WING_AOA_5 , DC_FLOAT, "wing_aoa_5");
116 INITDATA(DD_ALTITUDE , DC_FLOAT, "altitude");
117 INITDATA(DD_ALTITUDE_STRING , DC_CHAR , "altitude_string");
118
119 INITDATA(DD_ODOMETER_TOTAL , DC_FLOAT, "odometer_total");
120 INITDATA(DD_ODOMETER_USER , DC_FLOAT, "odometer_user");
121
122 // Lights (mirrors RoRnet::Lightmask)
123
124 INITDATA(DD_CUSTOM_LIGHT1 , DC_BOOL, "custom_light1");
125 INITDATA(DD_CUSTOM_LIGHT2 , DC_BOOL, "custom_light2");
126 INITDATA(DD_CUSTOM_LIGHT3 , DC_BOOL, "custom_light3");
127 INITDATA(DD_CUSTOM_LIGHT4 , DC_BOOL, "custom_light4");
128 INITDATA(DD_CUSTOM_LIGHT5 , DC_BOOL, "custom_light5");
129 INITDATA(DD_CUSTOM_LIGHT6 , DC_BOOL, "custom_light6");
130 INITDATA(DD_CUSTOM_LIGHT7 , DC_BOOL, "custom_light7");
131 INITDATA(DD_CUSTOM_LIGHT8 , DC_BOOL, "custom_light8");
132 INITDATA(DD_CUSTOM_LIGHT9 , DC_BOOL, "custom_light9");
133 INITDATA(DD_CUSTOM_LIGHT10 , DC_BOOL, "custom_light10");
134
135 INITDATA(DD_HEADLIGHTS , DC_BOOL, "headlights");
136 INITDATA(DD_HIGHBEAMS , DC_BOOL, "highbeams");
137 INITDATA(DD_FOGLIGHTS , DC_BOOL, "foglights");
138 INITDATA(DD_SIDELIGHTS , DC_BOOL, "sidelights");
139 INITDATA(DD_BRAKE_LIGHTS , DC_BOOL, "brake_lights");
140 INITDATA(DD_REVERSE_LIGHT , DC_BOOL, "reverse_light");
141 INITDATA(DD_BEACONS , DC_BOOL, "beacons");
142 INITDATA(DD_LIGHTS_LEGACY , DC_BOOL, "lights"); // Alias of 'sidelights'
143
144 INITDATA(DD_SIGNAL_TURNLEFT , DC_BOOL, "signal_turnleft");
145 INITDATA(DD_SIGNAL_TURNRIGHT , DC_BOOL, "signal_turnright");
146 INITDATA(DD_SIGNAL_WARNING , DC_BOOL, "signal_warning");
147
148 INITDATA(DD_GUISETTING_TACHO , DC_CHAR, "guisetting_tacho_tex");
149 INITDATA(DD_GUISETTING_SPEEDO , DC_CHAR, "guisetting_speedo_tex");
150 INITDATA(DD_GUISETTING_HELP , DC_CHAR, "guisetting_help");
151 INITDATA(DD_GUISETTING_TACHO_RPM , DC_FLOAT, "guisetting_tacho_rpm");
152 INITDATA(DD_GUISETTING_SPEEDO_KPH , DC_FLOAT, "guisetting_speedo_kph");
153
154 // load dash fonts
155 MyGUI::ResourceManager::getInstance().load("MyGUI_FontsDash.xml");
156}
157
159{
160 // free all objects
161 while (m_dashboards.size() > 0)
162 {
163 delete m_dashboards.back();
164 m_dashboards.pop_back();
165 }
166
167 data.clear();
168}
169
170int DashBoardManager::registerCustomInput(Ogre::String name, int dataType)
171{
172 int newKey = -1;
173 bool valid = true;
174
175 if (getLinkIDForName(name) != -1)
176 {
177 valid = false;
179 fmt::format("{}: Cannot register dashboard custom input \"{}\", name conflicts with existing one. Ignoring it.", m_actor->ar_design_name, name));
180 }
181 if (dataType == DC_INVALID || dataType >= DC_MAX || dataType <= DC_MIN)
182 {
183 valid = false;
185 fmt::format("{}: Cannot register dashboard custom input \"{}\", invalid data type.", m_actor->ar_design_name, name));
186 }
187
188 if (valid)
189 {
191 data.push_back(dashData_t(dataType, name));
193 }
194
195 return newKey;
196}
197
199{
200 for (int i = 0; i < data.size(); i++)
201 {
202 if (data[i].name == str)
203 return i;
204 }
205 return -1;
206}
207
209{
210 if (id >= 0 && id < data.size())
211 {
212 return data[id].name;
213 }
214 else
215 {
216 return "";
217 }
218}
219
220// Helper funcs and structs for `determineLayoutFromDashboardMod()` below.
221
222constexpr int DASHTAG_RPM_NONE = -1;
223constexpr char DASHTAG_XPH_NONE = '\0';
224
225static int DashRPM(const std::string& input)
226{
227 std::regex rpm_regex(R"((\d+)rpm)");
228 std::smatch match;
229 if (std::regex_search(input, match, rpm_regex)) {
230 std::string rpm = match[1];
231 return std::atoi(rpm.c_str());
232 }
233 return DASHTAG_RPM_NONE;
234}
235
236static char DashXPH(const std::string& input)
237{
238 std::regex xph_regex(R"(([km])ph)");
239 std::smatch match;
240 if (std::regex_search(input, match, xph_regex)) {
241 return match[1].str()[0];
242 }
243 return DASHTAG_XPH_NONE;
244}
245
247{
248 std::string filename;
251
252 DashCandidateLayout(const std::string& filename)
253 {
254 this->filename = filename;
255 this->rpm = DashRPM(filename);
256 this->xph = DashXPH(filename);
257 }
258};
259
260std::string DashBoardManager::determineLayoutFromDashboardMod(CacheEntryPtr& entry, std::string const& basename, BitMask_t loaddashboardFlags)
261{
263 Ogre::FileInfoListPtr filelist
264 = Ogre::ResourceGroupManager::getSingleton().findResourceFileInfo(entry->resource_group, fmt::format("{}*.layout", basename));
265
266 if (filelist->empty())
267 {
269 fmt::format("{}: No layout files found in dashboard '{}'", m_actor->ar_design_name, basename));
270 return "";
271 }
272
273 // Boat and aircraft dashboards are separate from trucks and have no tags to evaluate.
274 if (m_actor->ar_driveable == BOAT ||
276 {
277 return filelist->begin()->filename;
278 }
279
280 // Renderdash is created before `ar_driveable` is set.
281 if (m_actor->ar_driveable == TRUCK || BITMASK_IS_1(loaddashboardFlags, LOADDASHBOARD_RENDERDASH))
282 {
283 return this->determineTruckLayoutFromDashboardMod(filelist);
284 }
285
286 return "";
287}
288
289std::string DashBoardManager::determineTruckLayoutFromDashboardMod(Ogre::FileInfoListPtr& filelist)
290{
291 // Algorithm:
292 // A. Consider only layouts with matching Xph tag, find best RPM match (see below).
293 // B. If no match found, consider also layouts without Xph tag, find best RPM match (see below).
294 //
295 // Best RPM matching:
296 // 1. Consider just layouts with Xrpm tag, find one with with smallest RPM overshoot.
297 // 2. If all undershoot, take one with least undershoot and log a warning
298 // 3. If there's no layout with Xrpm tag, Consider layouts without Xrpm tag, pick random .
299 // ---------------------------------------------------------------------------------------
300
301 const int redlineRPM = (int)m_actor->ar_engine->getShiftUpRPM();
302 const char desiredX = App::gfx_speedo_imperial->getBool() ? 'm' : 'k';
303 std::vector<DashCandidateLayout> candidates;
304
305 for (Ogre::FileInfo& fileinfo : *filelist)
306 {
307 candidates.emplace_back(fileinfo.filename);
308 }
309
310 // A. Consider only layouts with matching Xph tag, find best RPM match (see above).
311 float least_overshoot = std::numeric_limits<float>::max(); DashCandidateLayout* overshoot_candidate = nullptr;
312 float least_undershoot = -std::numeric_limits<float>::max(); DashCandidateLayout* undershoot_candidate = nullptr;
313 for (auto& candidate : candidates)
314 {
315 if (candidate.xph == desiredX)
316 {
317 float rpm_diff = (float)candidate.rpm - redlineRPM;
318 if (rpm_diff < 0 && rpm_diff > least_undershoot)
319 {
320 least_undershoot = rpm_diff;
321 undershoot_candidate = &candidate;
322 }
323 else if (rpm_diff >= 0 && rpm_diff < least_overshoot)
324 {
325 least_overshoot = rpm_diff;
326 overshoot_candidate = &candidate;
327 }
328 }
329 }
330
331 if (overshoot_candidate)
332 {
333 return overshoot_candidate->filename;
334 }
335 else if (undershoot_candidate)
336 {
338 fmt::format("{}: No ideal dashboard found, using one with least RPM undershoot", m_actor->ar_design_name));
339 return undershoot_candidate->filename;
340 }
341
342 // B. If no match found, consider also layouts without Xph tag, find best RPM match (see above).
344 fmt::format("{}: Selected dashboard has no '{}ph' layouts, ignoring setting", m_actor->ar_design_name, desiredX));
345 least_overshoot = std::numeric_limits<float>::max(); overshoot_candidate = nullptr;
346 least_undershoot = -std::numeric_limits<float>::max(); undershoot_candidate = nullptr;
347 for (auto& candidate : candidates)
348 {
349 float rpm_diff = (float)candidate.rpm - redlineRPM;
350 if (rpm_diff < 0 && rpm_diff > least_undershoot)
351 {
352 least_undershoot = rpm_diff;
353 undershoot_candidate = &candidate;
354 }
355 else if (rpm_diff >= 0 && rpm_diff < least_overshoot)
356 {
357 least_overshoot = rpm_diff;
358 overshoot_candidate = &candidate;
359 }
360 }
361
362 if (overshoot_candidate)
363 {
364 return overshoot_candidate->filename;
365 }
366 else if (undershoot_candidate)
367 {
369 fmt::format("{}: No ideal dashboard found, using one with least RPM undershoot", m_actor->ar_design_name));
370 return undershoot_candidate->filename;
371 }
372
373 return filelist->begin()->filename; // If all else failed, just pick random.
374}
375
377{
378 try
379 {
380 DataStreamPtr ds = ResourceGroupManager::getSingleton().openResource(entry->fname, entry->resource_group);
381 GenericDocumentPtr dash_doc = new GenericDocument();
383 dash_doc->loadFromDataStream(ds, options);
384
385 GenericDocContextPtr ctx = new GenericDocContext(dash_doc);
386 while (!ctx->endOfFile())
387 {
388 if (ctx->isTokKeyword() && ctx->getTokKeyword() == "dashboard_custom_input" && ctx->countLineArgs() > 2)
389 {
390 bool valid_custom_input = true;
391 std::string custom_input_name = ctx->getTokString(1);
392 int custom_input_data_type;
393
394 std::string data_type_str = ctx->getTokString(2);
395 if (data_type_str == "bool") { custom_input_data_type = DC_BOOL; }
396 else if (data_type_str == "float") { custom_input_data_type = DC_FLOAT; }
397 else if (data_type_str == "int") { custom_input_data_type = DC_INT; }
398 else if (data_type_str == "string") { custom_input_data_type = DC_CHAR; }
399 else
400 {
401 valid_custom_input = false;
403 fmt::format("Dashboard Custom Input: Invalid data type \"{}\" for \"{}\"", data_type_str, custom_input_name));
404 }
405
406 if (valid_custom_input)
407 {
408 this->registerCustomInput(custom_input_name, custom_input_data_type);
409 }
410 }
411
412 ctx->seekNextLine();
413 }
414 }
415 catch (Ogre::Exception& e)
416 {
417 RoR::LogFormat("[RoR|DashBoardManager] Error processing file '%s', message :%s",
418 entry->fname.c_str(), e.getFullDescription().c_str());
419 }
420}
421
422// Internal helper for `loadDashBoard()`.
423bool GetGuisettingTextureName(const std::string& logLabel, const std::string& matGuisetting, const std::string& matRg, std::string& outTexname)
424{
425 if (matGuisetting == "")
426 {
427 LOG(fmt::format("[RoR|DashBoard] '{}' not defined in 'guisettings'", logLabel));
428 return false;
429 }
430
431 Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(matGuisetting, matRg);
432 if (!mat)
433 {
434 LOG(fmt::format("[RoR|DashBoard] '{}' '{}' not found", logLabel, matGuisetting));
435 return false;
436 }
437
438 if (
439 !mat->getTechniques().size() ||
440 !mat->getTechniques()[0]->getPasses().size() ||
441 !mat->getTechniques()[0]->getPasses()[0]->getTextureUnitStates().size() ||
442 mat->getTechniques()[0]->getPasses()[0]->getTextureUnitStates()[0]->getTextureName() == "")
443 {
444 LOG(fmt::format("[RoR|DashBoard] '{}' '{}' has no texture", logLabel, matGuisetting));
445 return false;
446 }
447
448 outTexname = mat->getTechniques()[0]->getPasses()[0]->getTextureUnitStates()[0]->getTextureName();
449 return true;
450}
451
452DashBoard* DashBoardManager::loadDashBoard(std::string const& filename, BitMask_t flags, RTTLayer* rttLayer)
453{
454 // filename may be either '.layout' file (classic approach) or a new '.dashboard' mod.
455 // ----------------------------------------------------------------------------------
456
458 return nullptr; // Nothing to do.
459
460 std::string basename, ext, layoutfname, scriptfilename = "";
461 Ogre::StringUtil::splitBaseFilename(filename, basename, ext);
462 if (ext == "dashboard")
463 {
464 CacheEntryPtr entry = App::GetCacheSystem()->FindEntryByFilename(LT_DashBoard, /*partial=*/false, filename);
465 if (!entry)
466 {
468 fmt::format("DashboardManager: Could not find dashboard file '{}'", filename));
469 return nullptr;
470 }
472 layoutfname = this->determineLayoutFromDashboardMod(entry, basename, flags);
473
475
476 // load dash fonts
477 Ogre::FileInfoListPtr filelist
478 = Ogre::ResourceGroupManager::getSingleton().findResourceFileInfo(entry->resource_group, fmt::format("{}*.resource", basename));
479 for (Ogre::FileInfo& fileinfo : *filelist)
480 {
481 MyGUI::ResourceManager::getInstance().load(fileinfo.filename);
482 }
483
484 // Load dashboard script
485 // First, we check if the script file actually exists.
486 std::string expected_script_name = fmt::format("{}.as", basename);
487 bool script_exists =
488 Ogre::ResourceGroupManager::getSingleton().resourceExists(entry->resource_group, expected_script_name);
489 if (script_exists)
490 scriptfilename = expected_script_name;
491 }
492 else
493 {
494 layoutfname = filename;
495 }
496
497 if (layoutfname == "")
498 {
500 fmt::format("{}: Cannot load dashboard '{}' - no applicable layout file found", m_actor->ar_design_name, filename));
501 return nullptr;
502 }
503
504 DashBoard* d = nullptr;
506 {
507 d = new DashBoard(this, layoutfname, rttLayer);
509 // This dashboard shouldn't be visible at this point, since
510 // it's not linked to the current player actor yet.
511 // GameContext.ChangePlayerActor() will change the visibility
512 // when needed.
513 d->setVisible(false);
514 m_dashboards.push_back(d);
515
517 {
518 m_rtt_loaded = true;
519 }
520 }
521
523 {
524 d = new DashBoard(this, layoutfname, rttLayer);
526 d->setVisible(true);
527 m_dashboards.push_back(d);
528 // NOTE: Renderdash RTTs are never stackable
529 }
530
532 {
533 d = new DashBoard(this, layoutfname, rttLayer);
534 d->setVisible(true);
535 m_dashboards.push_back(d);
536
538 {
539 m_hud_loaded = true;
540 }
541 }
542
543 // Load dashboard script
544 // We have to push messages to load dashboard scripts because
545 // this code can be called from a script execution context
546 // (e.g. using game.spawnTruck()).
547 // If we are in a script context, calling loadScript() directly
548 // will crash the game immediately, since AngelScript doesn't
549 // allow scripts to load other scripts.
550 if (scriptfilename != "")
551 {
554 req->lsr_filename = scriptfilename;
557 // We don't need to worry about unloading the script later because
558 // it has an actor associated to it. Actor scripts are unloaded
559 // already when calling ActorManager::DeleteActorInternal().
560 }
561
562 // Apply textures from 'guisettings' in truck file.
563
564 std::string tachotex;
566 m_actor->GetGfxActor()->GetResourceGroup(), tachotex))
567 {
568 this->setChar(DD_GUISETTING_TACHO, tachotex.c_str());
569 // Special feature: adjust the tachometer's maximum RPM according to the truck's engine redline RPM
571 {
572 for (DashBoard::layoutLink_t& link : d->controls)
573 {
574 for (int i = 0; i < link.geometricAnimationCount; i++)
575 {
576 DashBoard::layoutGeometricAnimation_t& anim = link.geometricAnimations[i];
578 {
579 const float redlineRPM = (float)m_actor->ar_engine->getShiftUpRPM();
580 const std::string prefixLessName = std::string(link.name).substr(d->prefix.size());
581 LOG(fmt::format("[RoR|DashBoard] per 'guisettings/useMaxRPM', overriding vmax of '{}' to {}", prefixLessName, redlineRPM));
582 anim.vmax = redlineRPM;
583 }
584 }
585 }
586 }
587 }
588 std::string speedotex;
590 m_actor->GetGfxActor()->GetResourceGroup(), speedotex))
591 {
592 this->setChar(DD_GUISETTING_SPEEDO, speedotex.c_str());
593 // Special feature: adjust the speedometer's maximum KPH according to value given in 'guisettings'.
595 {
596 for (DashBoard::layoutLink_t& link : d->controls)
597 {
598 for (int i = 0; i < link.geometricAnimationCount; i++)
599 {
600 DashBoard::layoutGeometricAnimation_t& anim = link.geometricAnimations[i];
602 {
603 const std::string prefixLessName = std::string(link.name).substr(d->prefix.size());
604 LOG(fmt::format("[RoR|DashBoard] per 'guisettings/speedoMax', overriding vmax of '{}' to {}", prefixLessName, m_actor->ar_guisettings_speedo_max_kph));
606 }
607 }
608 }
609 }
610 }
611 std::string helptex;
613 m_actor->GetGfxActor()->GetResourceGroup(), helptex))
614 {
615 this->setChar(DD_GUISETTING_HELP, helptex.c_str());
616 }
617
618 return d;
619}
620
622{
623 for (DashBoard* d : m_dashboards)
624 {
625 d->update(dt);
626 }
627}
628
630{
631 for (DashBoard* d : m_dashboards)
632 {
633 d->updateFeatures();
634 }
635}
636
638{
639 if (key >= data.size())
640 return 0;
641
642 switch (data[key].type)
643 {
644 case DC_BOOL:
645 return data[key].data.value_bool ? 1.0f : 0.0f;
646 case(DC_INT):
647 return (float)data[key].data.value_int;
648 case(DC_FLOAT):
649 return data[key].data.value_float;
650 }
651 return 0;
652}
653
654void DashBoardManager::setVisible(bool visibility)
655{
656 visible = visibility;
657 for (DashBoard* d : m_dashboards)
658 {
659 if (!d->getIsTextureLayer())
660 {
661 d->setVisible(visibility);
662 }
663 }
664}
665
667{
668 for (DashBoard* d : m_dashboards)
669 {
670 if (d->getIsTextureLayer())
671 {
672 d->setVisible(visibility, /*smooth:*/false);
673 }
674 }
675}
676
678{
679 for (DashBoard* d : m_dashboards)
680 {
681 d->windowResized();
682 }
683}
684
686{
687 for (DashBoard* d : m_dashboards)
688 {
689 d->applyUiScale();
690 }
691}
692
693// DASHBOARD class below
694
695DashBoard::DashBoard(DashBoardManager* manager, Ogre::String filename, RTTLayer* _rttLayer)
696 : manager(manager)
697 , filename(filename)
698 , visible(false)
699 , mainWidget(nullptr)
700 , rttLayer(_rttLayer)
701{
702 // use 'this' class pointer to make layout unique
703 prefix = MyGUI::utility::toString(this, "_");
704
706 // hide first
707 if (mainWidget)
708 mainWidget->setVisible(false);
709}
710
712{
713 // Clear the GUI widgets
714 MyGUI::LayoutManager::getInstance().unloadLayout(widgets);
715 // Force unloading the '.layout' file from memory
716 MyGUI::ResourceManager::getInstance().removeByName(filename);
717 if (rttLayer)
718 {
719 // Clean up the RTT
722 }
723}
724
726{
727 // this hides / shows parts of the gui depending on the vehicle features
728 for (size_t i = 0; i < controls.size(); i++)
729 {
730 bool enabled = manager->getEnabled(controls[i].linkIDForVisibility);
731
732 controls[i].widget->setVisible(enabled);
733 }
734}
735
736const float DASH_SMOOTHING = 0.02;
737
738float DashBoard::getSmoothNumeric(int linkID, float& lastVal)
739{
740 if (manager->getDataType(linkID) != DC_FLOAT)
741 {
742 return manager->getNumeric(linkID); // Only smoothen FLOAT inputs
743 }
744 else
745 {
746 const float curVal = manager->getNumeric(linkID);
747 const float val = lastVal * (1 - DASH_SMOOTHING) + curVal * DASH_SMOOTHING;
748 lastVal = curVal;
749 return val;
750 }
751}
752
753void DashBoard::update(float dt)
754{
755 // walk all controls and animate them
756 for (size_t i = 0; i < controls.size(); i++)
757 {
758 // Process graphical animation
759 if (controls[i].graphicalAnimation.animationType == ANIM_LAMP)
760 {
761 // or a lamp?
762 bool state = false;
763 // conditional
764 if (controls[i].graphicalAnimation.condition == CONDITION_GREATER)
765 {
766 float val = manager->getNumeric(controls[i].graphicalAnimation.linkID);
767 state = (val > controls[i].graphicalAnimation.conditionArgument);
768 }
769 else if (controls[i].graphicalAnimation.condition == CONDITION_LESSER)
770 {
771 float val = manager->getNumeric(controls[i].graphicalAnimation.linkID);
772 state = (val < controls[i].graphicalAnimation.conditionArgument);
773 }
774 else
775 {
776 state = (manager->getNumeric(controls[i].graphicalAnimation.linkID) > 0);
777 }
778
779 if (state == controls[i].lastState)
780 continue;
781 controls[i].lastState = state;
782
783 // switch states
784 if (state)
785 {
786 controls[i].img->setImageTexture(String(controls[i].graphicalAnimation.texture) + "-on.png");
787 }
788 else
789 {
790 controls[i].img->setImageTexture(String(controls[i].graphicalAnimation.texture) + "-off.png");
791 }
792 }
793 else if (controls[i].graphicalAnimation.animationType == ANIM_SERIES)
794 {
795 const float val = this->getSmoothNumeric(controls[i].graphicalAnimation.linkID, controls[i].graphicalAnimation.lastVal);
796
797 String fn = String(controls[i].graphicalAnimation.texture) + String("-") + TOSTRING((int)val) + String(".png");
798
799 controls[i].img->setImageTexture(fn);
800 }
801 else if (controls[i].graphicalAnimation.animationType == ANIM_TEXTFORMAT)
802 {
803 const float val = this->getSmoothNumeric(controls[i].graphicalAnimation.linkID, controls[i].graphicalAnimation.lastVal);
804
805 MyGUI::UString s;
806 if (strlen(controls[i].graphicalAnimation.format) == 0)
807 {
808 s = Ogre::StringConverter::toString(val);
809 }
810 else
811 {
812 char tmp[1024] = "";
813 sprintf(tmp, controls[i].graphicalAnimation.format, val);
814
815 // Detect and eliminate negative zero (-0) on output
816 if (strcmp(tmp, controls[i].graphicalAnimation.format_neg_zero) == 0)
817 {
818 sprintf(tmp, controls[i].graphicalAnimation.format, 0.f);
819 }
820
821 s = MyGUI::UString(tmp);
822 }
823
824 controls[i].txt->setCaption(s);
825 }
826 else if (controls[i].graphicalAnimation.animationType == ANIM_TEXTSTRING)
827 {
828 char* val = manager->getChar(controls[i].graphicalAnimation.linkID);
829 controls[i].txt->setCaption(MyGUI::UString(val));
830 }
831 else if (controls[i].graphicalAnimation.animationType == ANIM_IMAGETEXTURE)
832 {
833 char* val = manager->getChar(controls[i].graphicalAnimation.linkID);
834 if (strnlen(val, DD_MAXCHAR) > 0)
835 {
836 controls[i].img->setImageTexture(MyGUI::UString(val));
837 }
838 }
839
840 float finalHorizontalTranslation = 0,
841 finalVerticalTranslation = 0;
842 for (int animIdx = 0; animIdx < controls[i].geometricAnimationCount; animIdx++)
843 {
844 layoutGeometricAnimation_t& animation = controls[i].geometricAnimations[animIdx];
845
846 // get its value from its linkage
847 if (animation.animationType == ANIM_ROTATE)
848 {
849 // get the value
850 const float val = this->getSmoothNumeric(animation.linkID, animation.lastVal);
851 // calculate the angle
852 float angle = (val - animation.vmin) * (animation.wmax - animation.wmin) / (animation.vmax - animation.vmin) + animation.wmin;
853
854 // enforce limits
855 if (angle < animation.wmin)
856 angle = animation.wmin;
857 else if (angle > animation.wmax)
858 angle = animation.wmax;
859 // rotate finally
860 controls[i].rotImg->setAngle(Ogre::Degree(angle).valueRadians());
861 }
862 else if (animation.animationType == ANIM_SCALE)
863 {
864 const float val = this->getSmoothNumeric(animation.linkID, animation.lastVal);
865
866 float scale = (val - animation.vmin) * (animation.wmax - animation.wmin) / (animation.vmax - animation.vmin) + animation.wmin;
867 scale *= m_ui_scale;
868 if (animation.direction == DIRECTION_UP)
869 {
870 finalVerticalTranslation -= scale;
871 controls[i].widget->setSize(controls[i].initialSize.width, controls[i].initialSize.height + scale);
872 }
873 else if (animation.direction == DIRECTION_DOWN)
874 {
875 controls[i].widget->setSize(controls[i].initialSize.width, controls[i].initialSize.height + scale);
876 }
877 else if (animation.direction == DIRECTION_LEFT)
878 {
879 finalHorizontalTranslation -= scale;
880 controls[i].widget->setSize(controls[i].initialSize.width + scale, controls[i].initialSize.height);
881 }
882 else if (animation.direction == DIRECTION_RIGHT)
883 {
884 controls[i].widget->setSize(controls[i].initialSize.width + scale, controls[i].initialSize.height);
885 }
886 }
887 else if (animation.animationType == ANIM_TRANSLATE)
888 {
889 const float val = this->getSmoothNumeric(animation.linkID, animation.lastVal);
890
891 float translation = (val - animation.vmin) * (animation.wmax - animation.wmin) / (animation.vmax - animation.vmin) + animation.wmin;
892 translation *= m_ui_scale;
893 if (animation.direction == DIRECTION_UP)
894 finalVerticalTranslation -= translation;
895 else if (animation.direction == DIRECTION_DOWN)
896 finalVerticalTranslation += translation;
897 else if (animation.direction == DIRECTION_LEFT)
898 finalHorizontalTranslation -= translation;
899 else if (animation.direction == DIRECTION_RIGHT)
900 finalHorizontalTranslation += translation;
901 }
902 }
903
904 controls[i].widget->setPosition(controls[i].initialPosition.left + finalHorizontalTranslation, controls[i].initialPosition.top + finalVerticalTranslation);
905 }
906}
907
909{
910 if (!mainWidget)
911 return;
912 mainWidget->setPosition(0, 0);
913 if (getIsTextureLayer())
914 {
915 // texture layers are independent from the screen size, but rather from the layer texture size
916 TexturePtr tex = TextureManager::getSingleton().getByName(rttLayer->getTextureName());
917 if (tex)
918 mainWidget->setSize(tex->getWidth(), tex->getHeight());
919 }
920 else
921 {
922 MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize();
923 mainWidget->setSize(screenSize);
924 this->applyUiScale(); // Also refreshes `initialPosition` of controls, which MyGUI just re-aligned.
925 }
926}
927
928// Scales a widget along one axis while keeping it anchored per its MyGUI alignment.
929static void ScaleDashWidgetAxis(int pos, int size, int parent_orig, int parent_new,
930 bool stretch, bool anchor_near, bool anchor_far, float scale, int& out_pos, int& out_size)
931{
932 const float far_margin = static_cast<float>(parent_orig - (pos + size));
933 if (stretch)
934 {
935 out_pos = static_cast<int>(std::round(pos * scale));
936 out_size = parent_new - out_pos - static_cast<int>(std::round(far_margin * scale));
937 }
938 else if (anchor_far)
939 {
940 out_size = static_cast<int>(std::round(size * scale));
941 out_pos = parent_new - static_cast<int>(std::round(far_margin * scale)) - out_size;
942 }
943 else if (anchor_near)
944 {
945 out_pos = static_cast<int>(std::round(pos * scale));
946 out_size = static_cast<int>(std::round(size * scale));
947 }
948 else // center
949 {
950 const float center_offset = (pos + size / 2.f) - (parent_orig / 2.f);
951 out_size = static_cast<int>(std::round(size * scale));
952 out_pos = static_cast<int>(std::round((parent_new / 2.f) + (center_offset * scale) - (out_size / 2.f)));
953 }
954}
955
957{
958 MyGUI::EnumeratorWidgetPtr e = parent->getEnumerator();
959 while (e.next())
960 {
961 MyGUI::Widget* w = e.current();
962 WidgetOrigin origin;
963 origin.coord = w->getCoord();
964 MyGUI::TextBox* txt = w->castType<MyGUI::TextBox>(/* _throw: */ false);
965 if (txt)
966 {
967 origin.font_height = txt->getFontHeight();
968 }
969 m_widget_origins[w] = origin;
971 }
972}
973
974void DashBoard::applyUiScaleRecursive(MyGUI::Widget* parent, MyGUI::IntSize parent_orig_size, MyGUI::IntSize parent_new_size)
975{
976 MyGUI::EnumeratorWidgetPtr e = parent->getEnumerator();
977 while (e.next())
978 {
979 MyGUI::Widget* w = e.current();
980 auto itor = m_widget_origins.find(w);
981 if (itor == m_widget_origins.end())
982 {
983 continue;
984 }
985 const WidgetOrigin& origin = itor->second;
986 const MyGUI::Align align = w->getAlign();
987
988 // NOTE: Setting parent coords makes MyGUI re-align the children, but we overwrite their coords right after.
989 MyGUI::IntCoord coord;
990 ScaleDashWidgetAxis(origin.coord.left, origin.coord.width, parent_orig_size.width, parent_new_size.width,
991 align.isHStretch(), align.isLeft(), align.isRight(), m_ui_scale, coord.left, coord.width);
992 ScaleDashWidgetAxis(origin.coord.top, origin.coord.height, parent_orig_size.height, parent_new_size.height,
993 align.isVStretch(), align.isTop(), align.isBottom(), m_ui_scale, coord.top, coord.height);
994 w->setCoord(coord);
995
996 if (origin.font_height > 0)
997 {
998 w->castType<MyGUI::TextBox>()->setFontHeight(static_cast<int>(std::round(origin.font_height * m_ui_scale)));
999 }
1000
1001 this->applyUiScaleRecursive(w, origin.coord.size(), coord.size());
1002 }
1003}
1004
1006{
1007 if (m_widget_origins.empty())
1008 {
1009 return; // Not loaded yet, or rendered to texture (independent of screen).
1010 }
1011
1014
1015 // Update control data derived from widget coords
1016 for (layoutLink_t& ctrl : controls)
1017 {
1018 ctrl.initialSize = ctrl.widget->getSize();
1019 ctrl.initialPosition = ctrl.widget->getPosition();
1020 if (ctrl.rotImg)
1021 {
1022 ctrl.rotImg->setCenter(MyGUI::IntPoint(ctrl.widget->getWidth() * 0.5f, ctrl.widget->getHeight() * 0.5f));
1023 }
1024 }
1025}
1026
1027bool DashBoard::parseLink(std::string& linkArgs, int& linkID, char& condition, float& conditionArgument, const std::string& filename, const std::string& name)
1028{
1029 replaceString(linkArgs, "&gt;", ">");
1030 replaceString(linkArgs, "&lt;", "<");
1031 String linkName = "";
1032 if (linkArgs.empty())
1033 {
1034 LOG("Dashboard (" + filename + "/" + name + "): empty Link");
1035 return false;
1036 }
1037 // conditional checks
1038 // TODO: improve the logic, this is crap ...
1039 if (linkArgs.find(">") != linkArgs.npos)
1040 {
1041 Ogre::StringVector args = Ogre::StringUtil::split(linkArgs, ">");
1042 if (args.size() == 2)
1043 {
1044 linkName = args[0];
1045 conditionArgument = StringConverter::parseReal(args[1]);
1046 condition = CONDITION_GREATER;
1047 }
1048 else
1049 {
1050 LOG("Dashboard (" + filename + "/" + name + "): error in conditional Link: " + linkArgs);
1051 return false;
1052 }
1053 }
1054 else if (linkArgs.find("<") != linkArgs.npos)
1055 {
1056 Ogre::StringVector args = Ogre::StringUtil::split(linkArgs, "<");
1057 if (args.size() == 2)
1058 {
1059 linkName = args[0];
1060 conditionArgument = StringConverter::parseReal(args[1]);
1061 condition = CONDITION_LESSER;
1062 }
1063 else
1064 {
1065 LOG("Dashboard (" + filename + "/" + name + "): error in conditional Link: " + linkArgs);
1066 return false;
1067 }
1068 }
1069 else
1070 {
1071 condition = CONDITION_NONE;
1072 conditionArgument = 0;
1073 linkName = linkArgs;
1074 }
1075
1076 // now try to get the enum id for it
1077 int linkIDResult = manager->getLinkIDForName(linkName);
1078 if (linkIDResult < 0)
1079 {
1080 LOG("Dashboard (" + filename + "/" + name + "): unknown Link: " + linkName);
1081 return false;
1082 }
1083
1084 linkID = linkIDResult;
1085 return true;
1086}
1087
1088void DashBoard::loadLayoutRecursive(MyGUI::WidgetPtr w)
1089{
1090 std::string name = w->getName();
1091 std::string debug = w->getUserString("debug");
1092
1093 // make it unclickable
1094 w->setUserString("interactive", "0");
1095
1096 if (!debug.empty())
1097 {
1098 w->setVisible(false);
1099 return;
1100 }
1101
1102 // find the root widget and ignore debug widgets
1103 if (name.size() > prefix.size())
1104 {
1105 std::string prefixLessName = name.substr(prefix.size());
1106 if (prefixLessName == "_Main")
1107 {
1108 mainWidget = (MyGUI::WindowPtr)w;
1109 // resize it
1110 windowResized();
1111 }
1112
1113 // ignore debug widgets
1114 if (prefixLessName == "DEBUG")
1115 {
1116 w->setVisible(false);
1117 return;
1118 }
1119 }
1120
1121 layoutLink_t ctrl;
1122 memset(&ctrl, 0, sizeof(ctrl));
1123
1124 int linkID;
1125 char condition;
1126 float conditionArgument;
1127
1128 // Variable used to determine if one of the following
1129 // animations is used:
1130 // - series
1131 // - textcolor/textcolour
1132 // - textformat
1133 // - textstring
1134 // - lamp
1135 // Only one of them can be specified for each widget.
1136 std::string graphicalAnimationUsed = "";
1137
1138 int animNum = 1;
1139 // Numbered properties (anim, anim2, link, link2, etc.)
1140 // Allows to define multiple animations for a widget.
1141 std::string animNumStr = "anim";
1142 std::string linkNumStr = "link";
1143 std::string minNumStr = "min";
1144 std::string maxNumStr = "max";
1145 std::string vminNumStr = "vmin";
1146 std::string vmaxNumStr = "vmax";
1147 std::string textureNumStr = "texture";
1148 std::string formatNumStr = "format";
1149 std::string directionNumStr = "direction";
1150
1151 std::string anim = w->getUserString(animNumStr);
1152 std::string linkArgs;
1153 if (anim != "")
1154 {
1155 bool linkIDForVisibilitySet = false;
1156 while (anim != "")
1157 {
1158 linkArgs = w->getUserString(linkNumStr);
1159
1160 // animations for this control?
1161 if (!linkArgs.empty())
1162 {
1163 char animType;
1164 bool graphicalAnimation = false,
1165 graphicalAnimationAlreadySet = false,
1166 geometricAnimation = false;
1167 if (anim == "rotate")
1168 {
1169 animType = ANIM_ROTATE;
1170 geometricAnimation = true;
1171 }
1172 else if (anim == "scale")
1173 {
1174 animType = ANIM_SCALE;
1175 geometricAnimation = true;
1176 }
1177 else if (anim == "translate")
1178 {
1179 animType = ANIM_TRANSLATE;
1180 geometricAnimation = true;
1181 }
1182 else if (anim == "series")
1183 {
1184 animType = ANIM_SERIES;
1185 graphicalAnimation = true;
1186 if (graphicalAnimationUsed == "")
1187 graphicalAnimationUsed = anim;
1188 else
1189 graphicalAnimationAlreadySet = true;
1190 }
1191 else if (anim == "textcolor" || anim == "textcolour")
1192 {
1193 animType = ANIM_TEXTCOLOR;
1194 graphicalAnimation = true;
1195 if (graphicalAnimationUsed == "")
1196 graphicalAnimationUsed = anim;
1197 else
1198 graphicalAnimationAlreadySet = true;
1199 }
1200 else if (anim == "textformat")
1201 {
1202 animType = ANIM_TEXTFORMAT;
1203 graphicalAnimation = true;
1204 if (graphicalAnimationUsed == "")
1205 graphicalAnimationUsed = anim;
1206 else
1207 graphicalAnimationAlreadySet = true;
1208 }
1209 else if (anim == "textstring")
1210 {
1211 animType = ANIM_TEXTSTRING;
1212 graphicalAnimation = true;
1213 if (graphicalAnimationUsed == "")
1214 graphicalAnimationUsed = anim;
1215 else
1216 graphicalAnimationAlreadySet = true;
1217 }
1218 else if (anim == "lamp")
1219 {
1220 animType = ANIM_LAMP;
1221 graphicalAnimation = true;
1222 if (graphicalAnimationUsed == "")
1223 graphicalAnimationUsed = anim;
1224 else
1225 graphicalAnimationAlreadySet = true;
1226 }
1227 else if (anim == "imagetexture")
1228 {
1229 animType = ANIM_IMAGETEXTURE;
1230 graphicalAnimation = true;
1231 if (graphicalAnimationUsed == "")
1232 graphicalAnimationUsed = anim;
1233 else
1234 graphicalAnimationAlreadySet = true;
1235 }
1236 else
1237 {
1238 LOG("Dashboard (" + filename + "/" + name + "): Unknown animation \"" + anim + "\".");
1239 return;
1240 }
1241
1242 if (graphicalAnimationAlreadySet)
1243 {
1244 LOG("Dashboard (" + filename + "/" + name + "): Animations \"" + anim + "\" and \"" + graphicalAnimationUsed + "\" can't be used at the same time.");
1245 return;
1246 }
1247
1248 ctrl.widget = w;
1249 ctrl.initialSize = w->getSize();
1250 ctrl.initialPosition = w->getPosition();
1251 ctrl.lastState = false;
1252 if (!name.empty())
1253 strncpy(ctrl.name, name.c_str(), sizeof ctrl.name);
1254
1255 bool linkParsed = parseLink(linkArgs, linkID, condition, conditionArgument, filename, name);
1256 if (!linkParsed)
1257 return;
1258
1259 // The first animation will determine the visibility of the widget.
1260 if (!linkIDForVisibilitySet)
1261 {
1262 ctrl.linkIDForVisibility = linkID;
1263 linkIDForVisibilitySet = true;
1264 }
1265
1266 if (graphicalAnimation)
1267 {
1268 ctrl.graphicalAnimation.linkID = linkID;
1269 ctrl.graphicalAnimation.animationType = animType;
1270 ctrl.graphicalAnimation.condition = condition;
1271 ctrl.graphicalAnimation.conditionArgument = conditionArgument;
1272
1273 String texture = w->getUserString(textureNumStr);
1274 if (!texture.empty())
1275 strncpy(ctrl.graphicalAnimation.texture, texture.c_str(), sizeof ctrl.graphicalAnimation.texture);
1276
1277 String format = w->getUserString(formatNumStr);
1278 if (!format.empty())
1279 strncpy(ctrl.graphicalAnimation.format, format.c_str(), sizeof ctrl.graphicalAnimation.format);
1280
1281 // check if its the correct control
1282 if (animType == ANIM_SERIES)
1283 {
1284 ctrl.img = w->castType<MyGUI::ImageBox>(/* _throw: */ false);
1285 if (!ctrl.img)
1286 {
1287 LOG("Dashboard (" + filename + "/" + name + "): series anim must use the ImageBox Control");
1288 return;
1289 }
1290 }
1291 else if (animType == ANIM_TEXTCOLOR)
1292 {
1293 ctrl.txt = w->castType<MyGUI::TextBox>(/* _throw: */ false);
1294 if (!ctrl.txt)
1295 {
1296 LOG("Dashboard (" + filename + "/" + name + "): textcolor anim must use the TextBox Control");
1297 return;
1298 }
1299 }
1300 else if (animType == ANIM_TEXTFORMAT)
1301 {
1302 ctrl.txt = w->castType<MyGUI::TextBox>(/* _throw: */ false);
1303 if (!ctrl.txt)
1304 {
1305 LOG("Dashboard (" + filename + "/" + name + "): textformat anim must use the TextBox Control");
1306 return;
1307 }
1308
1309 // Prepare for eliminating negative zero (-0.0) display
1310 // Must be done on string-level because -0.001 with format "%1.0f" would still produce "-0"
1311 if (std::strlen(ctrl.graphicalAnimation.format))
1312 {
1314 }
1315 }
1316 else if (animType == ANIM_TEXTSTRING)
1317 {
1318 ctrl.txt = w->castType<MyGUI::TextBox>(/* _throw: */ false);
1319 if (!ctrl.txt)
1320 {
1321 LOG("Dashboard (" + filename + "/" + name + "): textstring anim must use the TextBox Control");
1322 return;
1323 }
1324 }
1325 else if (animType == ANIM_LAMP)
1326 {
1327 ctrl.img = w->castType<MyGUI::ImageBox>(/* _throw: */ false);
1328 if (!ctrl.img)
1329 {
1330 LOG("Dashboard (" + filename + "/" + name + "): lamp anim must use the ImageBox Control");
1331 return;
1332 }
1333 }
1334 else if (animType == ANIM_IMAGETEXTURE)
1335 {
1336 ctrl.img = w->castType<MyGUI::ImageBox>(/* _throw: */ false);
1337 if (!ctrl.img)
1338 {
1339 LOG("Dashboard (" + filename + "/" + name + "): imagetexture anim must use the ImageBox Control");
1340 return;
1341 }
1342 }
1343 }
1344 else if (geometricAnimation)
1345 {
1347 {
1348 LOG("Dashboard (" + filename + "/" + name + "): Maximum amount of geometric animations reached (" + TOSTRING(DD_MAX_GEOMETRIC_ANIMATIONS) + "). Ignoring the rest.");
1349 return;
1350 }
1351 else
1352 {
1354 geometricAnim.linkID = linkID;
1355 geometricAnim.animationType = animType;
1356
1358
1359 // parse more attributes
1360 geometricAnim.wmin = StringConverter::parseReal(w->getUserString(minNumStr));
1361 geometricAnim.wmax = StringConverter::parseReal(w->getUserString(maxNumStr));
1362 geometricAnim.vmin = StringConverter::parseReal(w->getUserString(vminNumStr));
1363 geometricAnim.vmax = StringConverter::parseReal(w->getUserString(vmaxNumStr));
1364
1365 String direction = w->getUserString(directionNumStr);
1366 if (direction == "right")
1367 geometricAnim.direction = DIRECTION_RIGHT;
1368 else if (direction == "left")
1369 geometricAnim.direction = DIRECTION_LEFT;
1370 else if (direction == "down")
1371 geometricAnim.direction = DIRECTION_DOWN;
1372 else if (direction == "up")
1373 geometricAnim.direction = DIRECTION_UP;
1374 else if (!direction.empty())
1375 {
1376 LOG("Dashboard (" + filename + "/" + name + "): unknown direction: " + direction);
1377 return;
1378 }
1379
1380 // then specializations
1381 if (animType == ANIM_ROTATE)
1382 {
1383 // check if its the correct control
1384 ctrl.rotImg = w->getSubWidgetMain()->castType<MyGUI::RotatingSkin>(/* _throw: */ false);
1385 if (!ctrl.rotImg)
1386 {
1387 LOG("Dashboard (" + filename + "/" + name + "): Rotating controls must use the RotatingSkin");
1388 return;
1389 }
1390
1391 // special: set rotation center now into the middle
1392 ctrl.rotImg->setCenter(MyGUI::IntPoint(w->getWidth() * 0.5f, w->getHeight() * 0.5f));
1393 }
1394 else if (animType == ANIM_SCALE)
1395 {
1396 if (geometricAnim.direction == DIRECTION_NONE)
1397 {
1398 LOG("Dashboard (" + filename + "/" + name + "): direction empty: scale needs a direction");
1399 return;
1400 }
1401 }
1402 else if (animType == ANIM_TRANSLATE)
1403 {
1404 if (geometricAnim.direction == DIRECTION_NONE)
1405 {
1406 LOG("Dashboard (" + filename + "/" + name + "): direction empty: translate needs a direction");
1407 return;
1408 }
1409 }
1410 }
1411 }
1412 }
1413
1414 animNum++;
1415 animNumStr = fmt::format("anim{}", animNum);
1416 linkNumStr = fmt::format("link{}", animNum);
1417 minNumStr = fmt::format("min{}", animNum);
1418 maxNumStr = fmt::format("max{}", animNum);
1419 vminNumStr = fmt::format("vmin{}", animNum);
1420 vmaxNumStr = fmt::format("vmax{}", animNum);
1421 textureNumStr = fmt::format("texture{}", animNum);
1422 formatNumStr = fmt::format("format{}", animNum);
1423 directionNumStr = fmt::format("direction{}", animNum);
1424 anim = w->getUserString(animNumStr);
1425 }
1426
1427 controls.push_back(ctrl);
1428 }
1429 else
1430 {
1431 // Control with no animations. Process normal link.
1432 linkArgs = w->getUserString("link");
1433 // links for this control?
1434 if (!linkArgs.empty())
1435 {
1436 bool linkParsed = parseLink(linkArgs, linkID, condition, conditionArgument, filename, name);
1437 if (!linkParsed)
1438 return;
1439
1440 ctrl.linkIDForVisibility = linkID;
1441 ctrl.widget = w;
1442 ctrl.initialSize = w->getSize();
1443 ctrl.initialPosition = w->getPosition();
1444 ctrl.lastState = false;
1445
1446 controls.push_back(ctrl);
1447 }
1448 }
1449
1450 // walk the children now
1451 MyGUI::EnumeratorWidgetPtr e = w->getEnumerator();
1452 while (e.next())
1453 {
1454 loadLayoutRecursive(e.current());
1455 }
1456}
1457
1459{
1460 widgets = MyGUI::LayoutManager::getInstance().loadLayout(filename, prefix, nullptr); // never has a parent
1461
1462 for (MyGUI::VectorWidgetPtr::iterator iter = widgets.begin(); iter != widgets.end(); ++iter)
1463 {
1464 loadLayoutRecursive(*iter);
1465 }
1466
1467 // if this thing should be rendered to texture, relocate the main window to the RTT layer
1468
1470 {
1471 // NOTE: this confusingly named MyGUI function actually attaches the widget to the `rttLayer` (which means detaching from previous hierarchy, that's what the name is trying to say).
1472 mainWidget->detachFromWidget(rttLayer->getName());
1473 }
1474 else if (mainWidget)
1475 {
1476 // Screen dashboard - scale it with the rest of the UI
1477 m_main_origin_size = mainWidget->getSize();
1479 this->applyUiScale();
1480 }
1481}
1482
1483void DashBoard::setVisible(bool v, bool smooth)
1484{
1485 visible = v;
1486
1487 if (!mainWidget)
1488 {
1489 for (MyGUI::VectorWidgetPtr::iterator iter = widgets.begin(); iter != widgets.end(); ++iter)
1490 {
1491 (*iter)->setVisible(v);
1492 }
1493 return;
1494 }
1495
1496 /*
1497 // buggy for some reason
1498 if (smooth)
1499 mainWidget->setVisibleSmooth(v);
1500 */
1501 mainWidget->setVisible(v);
1502}
Central state/object manager and communications hub.
#define TOSTRING(x)
Definition Application.h:58
void LOG(const char *msg)
Legacy alias - formerly a macro.
#define BITMASK_IS_1(VAR, FLAGS)
Definition BitFlags.h:14
#define BITMASK_IS_0(VAR, FLAGS)
Definition BitFlags.h:13
uint32_t BitMask_t
Definition BitFlags.h:7
A database of user-installed content alias 'mods' (vehicles, terrains...)
bool GetGuisettingTextureName(const std::string &logLabel, const std::string &matGuisetting, const std::string &matRg, std::string &outTexname)
constexpr int DASHTAG_RPM_NONE
constexpr char DASHTAG_XPH_NONE
static char DashXPH(const std::string &input)
const float DASH_SMOOTHING
#define INITDATA(key, type, name)
static void ScaleDashWidgetAxis(int pos, int size, int parent_orig, int parent_new, bool stretch, bool anchor_near, bool anchor_far, float scale, int &out_pos, int &out_size)
static int DashRPM(const std::string &input)
#define DD_MAXCHAR
#define DD_MAX_GEOMETRIC_ANIMATIONS
Generic text file parser.
#define strnlen(str, len)
EnginePtr ar_engine
Definition Actor.h:467
float ar_guisettings_speedo_max_kph
Definition Actor.h:555
GfxActor * GetGfxActor()
Definition Actor.h:344
Ogre::String ar_design_name
Name of the vehicle/machine/object this actor represents.
Definition Actor.h:441
int getInstanceId()
Definition Actor.h:306
std::string ar_guisettings_speedo_material
Used on 'renderdash'.
Definition Actor.h:558
std::string ar_guisettings_tacho_material
Used on 'renderdash'.
Definition Actor.h:557
std::string ar_guisettings_help_material
Used on 'renderdash'.
Definition Actor.h:559
bool ar_guisettings_use_engine_max_rpm
Definition Actor.h:554
ActorType ar_driveable
Sim attr; marks vehicle type and features.
Definition Actor.h:466
bool getBool() const
Definition CVar.h:98
Ogre::String fname
filename
Definition CacheSystem.h:67
Ogre::String resource_group
Resource group of the loaded bundle. Empty if not loaded yet.
Definition CacheSystem.h:89
CacheEntryPtr FindEntryByFilename(RoR::LoaderType type, bool partial, const std::string &_filename_maybe_bundlequalified)
Returns NULL if none found; "Bundle-qualified" format also specifies the ZIP/directory in modcache,...
void LoadResource(CacheEntryPtr &t)
Loads the associated resource bundle if not already done.
@ CONSOLE_MSGTYPE_ACTOR
Parsing/spawn/simulation messages for actors.
Definition Console.h:63
@ CONSOLE_MSGTYPE_INFO
Generic message.
Definition Console.h:60
void putMessage(MessageArea area, MessageType type, std::string const &msg, std::string icon="")
Definition Console.cpp:103
@ CONSOLE_SYSTEM_ERROR
Definition Console.h:52
@ CONSOLE_SYSTEM_WARNING
Definition Console.h:53
MyGUI::WindowPtr mainWidget
Ogre::String filename
void applyUiScaleRecursive(MyGUI::Widget *parent, MyGUI::IntSize parent_orig_size, MyGUI::IntSize parent_new_size)
void setVisible(bool visible, bool smooth=true)
void snapshotWidgetOriginsRecursive(MyGUI::Widget *parent)
void update(float dt)
float getSmoothNumeric(int linkID, float &lastVal)
bool parseLink(std::string &linkArgs, int &linkID, char &condition, float &conditionArgument, const std::string &filename, const std::string &name)
float m_ui_scale
Also applies to pixel values of 'scale' and 'translate' animations.
void applyUiScale()
Reads cvar 'ui_scale_factor'; only affects screen dashboards (not RTT)
DashBoard(DashBoardManager *manager, Ogre::String filename, RTTLayer *_rttLayer)
MyGUI::VectorWidgetPtr widgets
std::vector< layoutLink_t > controls
std::map< MyGUI::Widget *, WidgetOrigin > m_widget_origins
Unscaled coords as loaded from layout; empty for RTT dashboards.
MyGUI::IntSize m_main_origin_size
Size of mainWidget when m_widget_origins were recorded.
DashBoardManager * manager
void loadLayoutRecursive(MyGUI::WidgetPtr ptr)
void applyUiScale()
Reads cvar 'ui_scale_factor'; only affects screen dashboards (not RTT)
void setVisible(bool visibility)
std::vector< dashData_t > data
std::string determineLayoutFromDashboardMod(CacheEntryPtr &entry, std::string const &basename, BitMask_t loaddashboardFlags)
int getLinkIDForName(Ogre::String &str)
void setVisible3d(bool visibility)
void loadDashboardModDetails(CacheEntryPtr &entry)
void setChar(int key, const char *val)
std::string determineTruckLayoutFromDashboardMod(Ogre::FileInfoListPtr &filelist)
int registerCustomInput(Ogre::String name, int dataType)
virtual ~DashBoardManager() override
std::string getLinkNameForID(DashData id)
std::vector< DashBoard * > m_dashboards
DashBoardManager(ActorPtr actor)
DashBoard * loadDashBoard(const std::string &filename, BitMask_t flags, RTTLayer *rttLayer)
float getShiftUpRPM() const
Shift up RPM ('engine' attr #2)
Definition Engine.h:59
RTTLayerManager & GetRttLayerManager()
Definition GUIManager.h:172
float GetUiScaleFactor() const
Reads cvar 'ui_scale_factor', clamped to sane range.
void PushMessage(Message m)
Doesn't guarantee order! Use ChainMessage() if order matters.
Ogre::String GetResourceGroup()
Definition GfxActor.h:143
RTT texture 'slot' within MyGUI system; we can only create them, not destroy, so we reuse.
Definition RTTLayer.h:13
void destroyRttTexture()
Definition RTTLayer.cpp:87
const std::string & getTextureName() const
Definition RTTLayer.h:26
void RecycleRttLayer(RTTLayer *obj)
Definition RTTLayer.cpp:132
@ TRUCK
its a truck (or other land vehicle)
Definition SimData.h:85
@ BOAT
its a boat
Definition SimData.h:87
@ AIRPLANE
its an airplane
Definition SimData.h:86
@ MSG_APP_LOAD_SCRIPT_REQUESTED
Payload = RoR::LoadScriptRequest* (owner)
Definition Application.h:94
static const float DEFAULT_SPEEDO_MAX_KPH
@ ACTOR
Defined in truck file under 'scripts', contains global variable BeamClass@ thisActor.
GUIManager * GetGuiManager()
GameContext * GetGameContext()
Console * GetConsole()
CacheSystem * GetCacheSystem()
CVar * gfx_speedo_imperial
@ LT_DashBoard
@ DD_ENGINE_RPM
@ DD_AEROENGINE_THROTTLE_4
@ DD_SCREW_STEER_2
@ DD_CUSTOM_LIGHT6
custom light 6 on
@ DD_CUSTOM_LIGHT7
custom light 7 on
@ DD_ODOMETER_TOTAL
@ DD_CUSTOM_LIGHT10
custom light 10 on
@ DD_ENGINE_BATTERY
@ DD_SCREW_THROTTLE_2
@ DD_CUSTOM_LIGHT2
custom light 2 on
@ DD_CUSTOM_LIGHT4
custom light 4 on
@ DD_GUISETTING_TACHO
(char) Tacho texture name - from 'guisettings' section of truck file.
@ DD_ENGINE_NUM_GEAR
current gear
@ DD_AEROENGINE_FAILED_4
@ DD_SCREW_THROTTLE_4
@ DD_AEROENGINE_RPM_1
@ DD_LIGHTS_LEGACY
Alias of 'sidelights'.
@ DD_GUISETTING_SPEEDO_KPH
(float) Like DD_ENGINE_SPEEDO_KPH, but if 'speedoTexture' and 'speedoMax' in 'guisettings' are set,...
@ DD_ENGINE_SPEEDO_KPH
@ DD_AEROENGINE_FAILED_5
@ DD_SCREW_STEER_5
@ DD_ENGINE_AUTOGEAR_STRING
string like "<current gear>/<max gear>"
@ DD_AEROENGINE_RPM_5
@ DD_AEROENGINE_FAILED_2
@ DD_SCREW_STEER_1
@ DD_GUISETTING_HELP
(char) Help texture name - from 'guisettings' section of truck file.
@ DD_GUISETTING_TACHO_RPM
(float) Like DD_ENGINE_RPM, but if 'tachoTexture' and 'useMaxRPM' in 'guisettings' is set,...
@ DD_ENGINE_OFF
@ DD_AEROENGINE_RPM_4
@ DD_PARKINGBRAKE
chassis pitch
@ DD_AEROENGINE_THROTTLE_2
@ DD_ENGINE_RUNNING
@ DD_SCREW_THROTTLE_3
@ DD_CUSTOM_LIGHT9
custom light 9 on
@ DD_ENGINE_GEAR_STRING
@ DD_SIGNAL_WARNING
The warning-blink indicator is lit.
@ DD_SCREW_STEER_4
@ DD_CUSTOM_LIGHT3
custom light 3 on
@ DD_BRAKE_LIGHTS
@ DD_ANTILOCKBRAKE_MODE
@ DD_CRUISECONTROL_ACTIVE
ties locked
@ DD_SIGNAL_TURNLEFT
Left blinker is lit.
@ DD_ENGINE_SPEEDO_MPH
speedo in kilometer per hour
@ DD_AEROENGINE_FAILED_3
@ DD_SCREW_THROTTLE_1
@ DD_ENGINE_IGNITION
turbo gauge
@ DD_AEROENGINE_RPM_0
@ DD_CUSTOM_LIGHT8
custom light 8 on
@ DD_ROLL_CORR_ACTIVE
@ DD_AEROENGINE_THROTTLE_5
@ DD_ENGINE_GEAR
clutch warning lamp
@ DD_AEROENGINE_THROTTLE_0
@ DD_AEROENGINE_FAILED_0
@ DD_AEROENGINE_FAILED_1
@ DD_AEROENGINE_RPM_2
@ DD_SCREW_THROTTLE_5
@ DD_LOCKED
parking brake status
@ DD_ENGINE_AUTO_GEAR
string like "P R N G"
@ DD_ODOMETER_USER
@ DD_SCREW_STEER_3
@ DD_REVERSE_LIGHT
@ DD_SIGNAL_TURNRIGHT
Right blinker is lit.
@ DD_CUSTOM_LIGHT5
custom light 5 on
@ DD_ENGINE_CLUTCH_WARNING
battery lamp
@ DD_LOW_PRESSURE
locked lamp
@ DD_ENGINE_TURBO
speedo in miles per hour
@ DD_AEROENGINE_RPM_3
@ DD_ENGINE_CLUTCH
automatic gear
@ DD_GUISETTING_SPEEDO
(char) Speedo texture name - from 'guisettings' section of truck file.
@ DD_CUSTOM_LIGHT1
custom light 1 on
@ DD_ALTITUDE_STRING
@ DD_TRACTIONCONTROL_MODE
low pressure
@ DD_AEROENGINE_THROTTLE_3
@ DD_SCREW_STEER_0
@ DD_SCREW_THROTTLE_0
@ DD_AEROENGINE_THROTTLE_1
@ LOADDASHBOARD_RTT_TEXTURE
Will be drawn to texture. Unless STACKABLE, it prevents the default dashboard from loading.
@ LOADDASHBOARD_RENDERDASH
Will be drawn to texture in classic 'renderdash' format. Ignores STACKABLE flag.
@ LOADDASHBOARD_SCREEN_HUD
Will be drawn to screen. Unless STACKABLE, it prevents the default dashboard from loading.
@ LOADDASHBOARD_STACKABLE
Allows loading multiple dashboards at once (by default there's only one for screen and one for RTT).
void LogFormat(const char *format,...)
Improved logging utility. Uses fixed 2Kb buffer.
void replaceString(std::string &str, std::string searchString, std::string replaceString)
Definition Utils.h:51
DashCandidateLayout(const std::string &filename)
int font_height
Only for TextBox widgets.
char format_neg_zero[255]
Test for undesired '-0.0' on display. Only for link type "format". Empty if not applicable.
bool isTokKeyword(int offset=0) const
std::string getTokKeyword(int offset=0) const
bool endOfFile(int offset=0) const
std::string getTokString(int offset=0) const
static const BitMask_t OPTION_ALLOW_SLASH_COMMENTS
Allow comments starting with //.
virtual void loadFromDataStream(Ogre::DataStreamPtr datastream, BitMask_t options=0)
static const BitMask_t OPTION_ALLOW_NAKED_STRINGS
Allow strings without quotes, for backwards compatibility.
std::string lsr_filename
Load from resource ('.as' file or '.gadget' file); If buffer is supplied, use this as display name on...
ScriptCategory lsr_category
ActorInstanceID_t lsr_associated_actor
For ScriptCategory::ACTOR.
Unified game event system - all requests and state changes are reported using a message.
Definition GameContext.h:52