Rigs of Rods 2023.09
Soft-body Physics Simulation
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
Loading...
Searching...
No Matches
AddonPartFileFormat.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 Copyright 2013-2023 Petr Ohlidal
6
7 For more information, see http://www.rigsofrods.org/
8
9 Rigs of Rods is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License version 3, as
11 published by the Free Software Foundation.
12
13 Rigs of Rods is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with Rigs of Rods. If not, see <http://www.gnu.org/licenses/>.
20*/
21
22#include "AddonPartFileFormat.h"
23
24#include "Actor.h"
25#include "Application.h"
26#include "CacheSystem.h"
27#include "Console.h"
28#include "GameContext.h"
29#include "GenericFileFormat.h"
30#include "GUI_MessageBox.h"
31#include "RigDef_Parser.h"
32#include "TuneupFileFormat.h"
33
34#include <Ogre.h>
35
36using namespace RoR;
37using namespace RigDef;
38
40 :m_silent_mode(silent_mode)
41{
42 // Inits `RefCountingObjectPtr<>` (CacheEntryPtr, GenericDocumentPtr) - shouldn't be in header.
43}
44
46{
47 // Destroys `RefCountingObjectPtr<>` (CacheEntryPtr, GenericDocumentPtr) - shouldn't be in header.
48}
49
50std::shared_ptr<Document::Module> AddonPartUtility::TransformToRigDefModule(CacheEntryPtr& entry)
51{
53 m_addonpart_entry = entry;
54 try
55 {
56 Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(entry->fname, entry->resource_group);
57
60 m_document->loadFromDataStream(datastream, options);
61
62 m_module = std::shared_ptr<Document::Module>(new Document::Module(entry->dname));
63 m_module->origin_addonpart = entry;
65 Keyword keyword = Keyword::INVALID;
66 Keyword block = Keyword::INVALID;
67
68 while (!m_context->endOfFile())
69 {
71 {
72 // (ignore 'addonpart_*' directives)
73 if (m_context->getTokKeyword().find("addonpart_") != std::string::npos)
74 {
76 continue;
77 }
78
80 if (keyword == Keyword::INVALID && m_context->getTokKeyword() == "set_managedmaterials_options")
81 {
82 keyword = Keyword::SET_MANAGEDMATERIALS_OPTIONS; // Workaround, don't ask me why the regex doesn't match this.
83 }
84 if (keyword != Keyword::INVALID)
85 {
86 if (keyword == Keyword::SET_MANAGEDMATERIALS_OPTIONS)
87 {
90 continue;
91 }
92 else if (keyword == Keyword::MANAGEDMATERIALS
93 || keyword == Keyword::PROPS
94 || keyword == Keyword::FLEXBODIES
95 || keyword == Keyword::FLARES
96 || keyword == Keyword::FLARES2)
97 {
98 block = keyword;
100 continue;
101 }
102
103 }
104 }
105
106 if (block != Keyword::INVALID && !m_context->isTokComment() && !m_context->isTokLineBreak())
107 {
108 switch (block)
109 {
110 case Keyword::MANAGEDMATERIALS: this->ProcessManagedMaterial(); break;
111 case Keyword::PROPS: this->ProcessProp(); break;
112 case Keyword::FLEXBODIES: this->ProcessFlexbody(); break;
113 case Keyword::FLARES: this->ProcessFlare(); break;
114 case Keyword::FLARES2: this->ProcessFlare2(); break;
115 default: break;
116 }
117 }
118
120 }
121 return m_module;
122 }
123 catch (Ogre::Exception& e)
124 {
127 fmt::format("Could not use addonpart: Error parsing file '{}', message: {}",
128 entry->fname, e.getFullDescription()));
129 return nullptr;
130 }
131}
132
134{
135 // Evaluates 'addonpart_unwanted_*' directives, respecting 'protected_*' directives in the tuneup.
136 // Also handles 'addonpart_tweak_*' directives, resolving possible conflicts among used addonparts.
137 // ---------------------------------------------------------------------------------------------
138
139 App::GetCacheSystem()->LoadResource(addonpart_entry);
140 m_addonpart_entry = addonpart_entry;
141 m_tuneup = tuneup;
142
143 try
144 {
145 Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().openResource(addonpart_entry->fname, addonpart_entry->resource_group);
146
149 m_document->loadFromDataStream(datastream, options);
151
152 while (!m_context->endOfFile())
153 {
154 if (m_context->isTokKeyword())
155 {
156 if (m_context->getTokKeyword() == "addonpart_unwanted_prop" )
157 this->ProcessUnwantedProp();
158 else if (m_context->getTokKeyword() == "addonpart_unwanted_flexbody" )
160 else if (m_context->getTokKeyword() == "addonpart_unwanted_flare" )
161 this->ProcessUnwantedFlare();
162 else if (m_context->getTokKeyword() == "addonpart_unwanted_exhaust" )
164 else if (m_context->getTokKeyword() == "addonpart_unwanted_managedmaterial")
166 else if (m_context->getTokKeyword() == "addonpart_tweak_wheel")
167 this->ProcessTweakWheel();
168 else if (m_context->getTokKeyword() == "addonpart_tweak_node")
169 this->ProcessTweakNode();
170 else if (m_context->getTokKeyword() == "addonpart_tweak_prop")
171 this->ProcessTweakProp();
172 else if (m_context->getTokKeyword() == "addonpart_tweak_flexbody")
173 this->ProcessTweakFlexbody();
174 else if (m_context->getTokKeyword() == "addonpart_tweak_managedmaterial")
176 else if (m_context->getTokKeyword() == "addonpart_tweak_cinecam")
178 }
179
181 }
182
183 }
184 catch (Ogre::Exception& e)
185 {
188 fmt::format("Addonpart unwanted elements check: Error parsing file '{}', message: {}",
189 addonpart_entry->fname, e.getFullDescription()));
190 }
191}
192
194{
195 ROR_ASSERT(tuneup);
196
197 // Unwanted
198 tuneup->unwanted_flexbodies.clear();
199 tuneup->unwanted_props.clear();
200 tuneup->unwanted_flares.clear();
201
202 // Tweaked
203 tuneup->node_tweaks.clear();
204 tuneup->cinecam_tweaks.clear();
205 tuneup->wheel_tweaks.clear();
206 tuneup->prop_tweaks.clear();
207 tuneup->flexbody_tweaks.clear();
208}
209
210
211// Helpers of `TransformToRigDefModule()`, they expect `m_context` to be in position:
212// These expect `m_context` to be in position:
213
215{
216 ManagedMaterial def;
217 int n = m_context->countLineArgs();
218
219 // Name:
220 def.name = m_context->getStringData(0); // It may be a STRING (if with quotes), or KEYWORD (if without quotes - because it's at start of line).
221
222 // Type:
223 std::string str = m_context->getTokString(1);
224 if (str == "mesh_standard") def.type = ManagedMaterialType::MESH_STANDARD;
225 if (str == "mesh_transparent") def.type = ManagedMaterialType::MESH_TRANSPARENT;
226 if (str == "flexmesh_standard") def.type = ManagedMaterialType::FLEXMESH_STANDARD;
227 if (str == "flexmesh_transparent") def.type = ManagedMaterialType::FLEXMESH_TRANSPARENT;
228
229 // Textures:
231 if (n > 3) def.specular_map = m_context->getTokString(3);
232 if (n > 4) def.damaged_diffuse_map = m_context->getTokString(4);
233 // (placeholders)
234 if (def.specular_map == "-") def.specular_map = "";
235 if (def.damaged_diffuse_map == "-") def.damaged_diffuse_map = "";
236
237 // Options:
239
240 m_module->managedmaterials.push_back(def);
241}
242
251
253{
254 RigDef::Prop def;
255 int n = m_context->countLineArgs();
256 if (n < 10)
257 {
260 fmt::format("Error parsing addonpart file '{}': 'install_prop' has only {} arguments, expected {}",
261 m_addonpart_entry->fname, n, 10));
262 return;
263 }
264
265 int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED;
266 def.reference_node = Node::Ref("", (unsigned int)m_context->getTokFloat(0), importflags, 0);
267 def.x_axis_node = Node::Ref("", (unsigned int)m_context->getTokFloat(1), importflags, 0);
268 def.y_axis_node = Node::Ref("", (unsigned int)m_context->getTokFloat(2), importflags, 0);
269
270 def.offset.x = m_context->getTokNumeric(3);
271 def.offset.y = m_context->getTokNumeric(4);
272 def.offset.z = m_context->getTokNumeric(5);
273
274 def.rotation.x = m_context->getTokNumeric(6);
275 def.rotation.y = m_context->getTokNumeric(7);
276 def.rotation.z = m_context->getTokNumeric(8);
277
280
281 switch (def.special)
282 {
283 case SpecialProp::BEACON:
284 if (n >= 14)
285 {
287 Ogre::StringUtil::trim(def.special_prop_beacon.flare_material_name);
288
289 def.special_prop_beacon.color = Ogre::ColourValue(
291 }
292 break;
293
294 case SpecialProp::DASHBOARD_LEFT:
295 case SpecialProp::DASHBOARD_RIGHT:
296 if (n > 10)
297 {
299 }
300 if (n > 13)
301 {
304 }
305 if (n > 14)
306 {
308 }
309 break;
310
311 default:
312 break;
313 }
314
315 m_module->props.push_back(def);
316}
317
319{
320 Flexbody def;
321 int n = m_context->countLineArgs();
322 if (n < 10)
323 {
326 fmt::format("Error parsing addonpart file '{}': flexbody has only {} arguments, expected {}", m_addonpart_entry->fname, n, 10));
327 return;
328 }
329
330 int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED;
331 def.reference_node = Node::Ref("", (unsigned int)m_context->getTokInt(0), importflags, 0);
332 def.x_axis_node = Node::Ref("", (unsigned int)m_context->getTokInt(1), importflags, 0);
333 def.y_axis_node = Node::Ref("", (unsigned int)m_context->getTokInt(2), importflags, 0);
334
335 def.offset.x = m_context->getTokNumeric(3);
336 def.offset.y = m_context->getTokNumeric(4);
337 def.offset.z = m_context->getTokNumeric(5);
338
339 def.rotation.x = m_context->getTokNumeric(6);
340 def.rotation.y = m_context->getTokNumeric(7);
341 def.rotation.z = m_context->getTokNumeric(8);
342
344
346
347 if (!m_context->isTokString())
348 {
351 fmt::format("Error parsing addonpart file '{}': flexbody is not followed by 'forset'!", m_addonpart_entry->fname));
352 return;
353 }
354
356
357 // Resolve `forset` ranges:
358 for (RigDef::Node::Range const& range: def.node_list_to_import)
359 {
360 for (unsigned int i = range.start.Num(); i <= range.end.Num(); ++i)
361 {
362 Node::Ref ref("", i, Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED, 0);
363 def.node_list.push_back(ref);
364 }
365 }
366
367 m_module->flexbodies.push_back(def);
368}
369
371{
372 int n = m_context->countLineArgs();
373 if (n < 5)
374 {
377 fmt::format("Error parsing addonpart file '{}': flare has only {} arguments, expected {}", m_addonpart_entry->fname, n, 5));
378 return;
379 }
380
381 Flare2 def; // We auto-import 'flares' as 'flares2', leaving the `offset.z` at 1.
382 int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED;
383 def.reference_node = Node::Ref("", (unsigned int)m_context->getTokInt(0), importflags, 0);
384 def.node_axis_x = Node::Ref("", (unsigned int)m_context->getTokInt(1), importflags, 0);
385 def.node_axis_y = Node::Ref("", (unsigned int)m_context->getTokInt(2), importflags, 0);
386 def.offset.x = m_context->getTokNumeric(3);
387 def.offset.y = m_context->getTokNumeric(4);
388
389 if (n > 5) def.type = (FlareType)m_context->getTokString(5)[0];
390
391 if (n > 6)
392 {
393 switch (def.type)
394 {
395 case FlareType::USER: def.control_number = m_context->getTokInt(6); break;
397 default: break;
398 }
399 }
400
401 if (n > 7) { def.blink_delay_milis = m_context->getTokInt(7); }
402 if (n > 8) { def.size = m_context->getTokNumeric(8); }
403 if (n > 9) { def.material_name = m_context->getTokString(9); }
404
405 m_module->flares2.push_back(def);
406}
407
409{
410 int n = m_context->countLineArgs();
411 if (n < 6)
412 {
415 fmt::format("Error parsing addonpart file '{}': flare2 has only {} arguments, expected {}", m_addonpart_entry->fname, n, 6));
416 return;
417 }
418
419 Flare2 def;
420 int importflags = Node::Ref::REGULAR_STATE_IS_VALID | Node::Ref::REGULAR_STATE_IS_NUMBERED;
421 def.reference_node = Node::Ref("", (unsigned int)m_context->getTokInt(0), importflags, 0);
422 def.node_axis_x = Node::Ref("", (unsigned int)m_context->getTokInt(1), importflags, 0);
423 def.node_axis_y = Node::Ref("", (unsigned int)m_context->getTokInt(2), importflags, 0);
424 def.offset.x = m_context->getTokNumeric(3);
425 def.offset.y = m_context->getTokNumeric(4);
426 def.offset.z = m_context->getTokNumeric(5); // <-- Specific to 'flares2' (the only difference)
427
428 if (n > 6) def.type = (FlareType)m_context->getTokString(6)[0];
429
430 if (n > 7)
431 {
432 switch (def.type)
433 {
434 case FlareType::USER: def.control_number = m_context->getTokInt(7); break;
436 default: break;
437 }
438 }
439
440 if (n > 8) { def.blink_delay_milis = m_context->getTokInt(8); }
441 if (n > 9) { def.size = m_context->getTokNumeric(9); }
442 if (n > 10) { def.material_name = m_context->getTokString(10); }
443
444 m_module->flares2.push_back(def);
445}
446
447// Helpers of `ResolveUnwantedAndTweakedElements()`, they expect `m_context` to be in position:
448
450{
451 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_prop"); // also asserts !EOF and TokenType::KEYWORD
452
453 if (m_context->isTokInt(1))
454 {
456 {
458 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking prop '{}' as UNWANTED",
460 }
461 else
462 {
463 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping prop '{}' because it's marked PROTECTED",
465 }
466 }
467 else
468 {
469 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
470 }
471}
472
474{
475 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_flexbody"); // also asserts !EOF and TokenType::KEYWORD
476
477 if (m_context->isTokInt(1))
478 {
480 {
482 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking flexbody '{}' as UNWANTED",
484 }
485 else
486 {
487 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flexbody '{}' because it's marked PROTECTED",
489 }
490 }
491 else
492 {
493 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
494 }
495}
496
498{
499 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_flare"); // also asserts !EOF and TokenType::KEYWORD
500
501 if (m_context->isTokInt(1))
502 {
504 {
506 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking flare '{}' as UNWANTED",
508 }
509 else
510 {
511 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flare '{}' because it's marked PROTECTED",
513 }
514 }
515 else
516 {
517 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
518 }
519}
520
522{
523 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_exhaust"); // also asserts !EOF and TokenType::KEYWORD
524
525 if (m_context->isTokInt(1))
526 {
528 {
530 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking exhaust '{}' as UNWANTED",
532 }
533 else
534 {
535 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping exhaust '{}' because it's marked PROTECTED",
537 }
538 }
539 else
540 {
541 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
542 }
543}
544
546{
547 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_unwanted_managedmaterial"); // also asserts !EOF and TokenType::KEYWORD
548
549 if (m_context->isTokString(1))
550 {
551 std::string mat_name = m_context->getTokString(1);
552 if (!m_tuneup->isManagedMatProtected(mat_name))
553 {
554 m_tuneup->unwanted_managedmats.insert(mat_name);
555 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': marking managedmaterial '{}' as UNWANTED",
557 }
558 else
559 {
560 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping managedmaterial '{}' because it's marked PROTECTED",
562 }
563 }
564 else
565 {
566 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
567 }
568}
569
571{
572 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_wheel"); // also asserts !EOF and TokenType::KEYWORD
573
575 {
576 const int wheel_id = m_context->getTokInt(1);
577 if (!m_tuneup->isWheelProtected(wheel_id))
578 {
579 if (m_tuneup->wheel_tweaks.find(wheel_id) == m_tuneup->wheel_tweaks.end())
580 {
581 TuneupWheelTweak data;
582 bool stop = false;
584 data.twt_wheel_id = wheel_id;
585 data.twt_media[0] = m_context->getTokString(2);
586 if (!stop && m_context->isTokString(3)) { data.twt_media[1] = m_context->getTokString(3); } else { stop=true; }
587 if (!stop && m_context->isTokString(4)) { data.twt_side = (m_context->getTokString(4)[0] == 'l') ? WheelSide::LEFT : WheelSide::RIGHT; } else { stop=true; }
588 if (!stop && m_context->isTokFloat(5)) { data.twt_tire_radius = m_context->getTokFloat(5); } else { stop=true; }
589 if (!stop && m_context->isTokFloat(6)) { data.twt_rim_radius = m_context->getTokFloat(6); } else { stop=true; }
590 m_tuneup->wheel_tweaks.insert(std::make_pair(wheel_id, data));
591
592 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Sheduling tweak for wheel '{}'"
593 " with params {{ media1={}, media2={}, side={}, tire_radius={}, rim_radius={} }}",
595 data.twt_media[0], data.twt_media[1], (char)data.twt_side, data.twt_tire_radius, data.twt_rim_radius));
596 }
597 else if (m_tuneup->wheel_tweaks[wheel_id].twt_origin != m_addonpart_entry->fname)
598 {
599 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for wheel '{}' due to conflict with '{}'",
601 m_tuneup->wheel_tweaks[wheel_id].twt_origin));
602
603 m_tuneup->wheel_tweaks.erase(wheel_id);
604 }
605 }
606 else
607 {
608 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping wheel '{}' because it's marked PROTECTED",
610 }
611 }
612 else
613 {
614 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
615 }
616}
617
619{
620 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_node"); // also asserts !EOF and TokenType::KEYWORD
621
623 {
624 NodeNum_t nodenum = (NodeNum_t)m_context->getTokInt(1);
625 if (!m_tuneup->isNodeProtected(nodenum))
626 {
627 if (m_tuneup->node_tweaks.find(nodenum) == m_tuneup->node_tweaks.end())
628 {
629 TuneupNodeTweak data;
631 data.tnt_nodenum = nodenum;
632 data.tnt_pos.x = m_context->getTokNumeric(2);
633 data.tnt_pos.y = m_context->getTokNumeric(3);
634 data.tnt_pos.z = m_context->getTokNumeric(4);
635 m_tuneup->node_tweaks.insert(std::make_pair(nodenum, data));
636
637 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for node '{}'"
638 " with params {{ x={}, y={}, z={} }}",
640 data.tnt_pos.x, data.tnt_pos.y, data.tnt_pos.z));
641 }
642 else if (m_tuneup->node_tweaks[nodenum].tnt_origin != m_addonpart_entry->fname)
643 {
644 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for node '{}' due to conflict with '{}'",
646 m_tuneup->node_tweaks[nodenum].tnt_origin));
647
648 m_tuneup->node_tweaks.erase(nodenum);
649 }
650 }
651 else
652 {
653 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping node '{}' because it's marked PROTECTED",
655 }
656 }
657 else
658 {
659 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
660 }
661}
662
664{
665 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_cinecam"); // also asserts !EOF and TokenType::KEYWORD
666
668 {
670 if (!m_tuneup->isCineCameraProtected(cinecamid))
671 {
672 if (m_tuneup->cinecam_tweaks.find(cinecamid) == m_tuneup->cinecam_tweaks.end())
673 {
676 data.tct_cinecam_id = cinecamid;
677 data.tct_pos.x = m_context->getTokNumeric(2);
678 data.tct_pos.y = m_context->getTokNumeric(3);
679 data.tct_pos.z = m_context->getTokNumeric(4);
680 m_tuneup->cinecam_tweaks.insert(std::make_pair(cinecamid, data));
681
682 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for cinecam '{}'"
683 " with params {{ x={}, y={}, z={} }}",
685 data.tct_pos.x, data.tct_pos.y, data.tct_pos.z));
686 }
687 else if (m_tuneup->cinecam_tweaks[cinecamid].tct_origin != m_addonpart_entry->fname)
688 {
689 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for cinecam '{}' due to conflict with '{}'",
691 m_tuneup->cinecam_tweaks[cinecamid].tct_origin));
692
693 m_tuneup->cinecam_tweaks.erase(cinecamid);
694 }
695 }
696 else
697 {
698 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping cinecam '{}' because it's marked PROTECTED",
700 }
701 }
702 else
703 {
704 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
705 }
706}
707
709{
710 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_flexbody"); // also asserts !EOF and TokenType::KEYWORD
711
712 // TBD: add `null` token type to GenericDocument, so these params can be made optional
713 if (m_context->isTokInt(1) && // ID
715 m_context->isTokNumeric(5) && m_context->isTokNumeric(6) && m_context->isTokNumeric(7) && // rotation
716 m_context->isTokString(8)) // media
717 {
718 const int flexbody_id = m_context->getTokInt(1);
719 if (!m_tuneup->isFlexbodyProtected(flexbody_id))
720 {
721 if (m_tuneup->flexbody_tweaks.find(flexbody_id) == m_tuneup->flexbody_tweaks.end())
722 {
725 data.tft_flexbody_id = flexbody_id;
733 m_tuneup->flexbody_tweaks.insert(std::make_pair(flexbody_id, data));
734
735 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for flexbody '{}'"
736 " with params {{ offsetX={}, offsetY={}, offsetZ={}, rotX={}, rotY={}, rotZ={}, media={} }}",
738 data.tft_offset.x, data.tft_offset.y, data.tft_offset.z,
739 data.tft_rotation.x, data.tft_rotation.y, data.tft_rotation.z, data.tft_media[0]));
740 }
741 else if (m_tuneup->flexbody_tweaks[flexbody_id].tft_origin != m_addonpart_entry->fname)
742 {
743 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for flexbody '{}' due to conflict with '{}'",
745 m_tuneup->flexbody_tweaks[flexbody_id].tft_origin));
746
747 m_tuneup->flexbody_tweaks.erase(flexbody_id);
748 }
749 }
750 else
751 {
752 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping flexbody '{}' because it's marked PROTECTED",
754 }
755 }
756 else
757 {
758 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
759 }
760}
761
763{
764 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_prop"); // also asserts !EOF and TokenType::KEYWORD
765
766 // TBD: add `null` token type to GenericDocument, so these params can be made optional
767 if (m_context->isTokInt(1) && // ID
769 m_context->isTokNumeric(5) && m_context->isTokNumeric(6) && m_context->isTokNumeric(7) && // rotation
770 m_context->isTokString(8)) // media
771 {
772 const int prop_id = m_context->getTokInt(1);
773 if (!m_tuneup->isPropProtected(prop_id))
774 {
775 if (m_tuneup->prop_tweaks.find(prop_id) == m_tuneup->prop_tweaks.end())
776 {
777 TuneupPropTweak data;
779 data.tpt_prop_id = prop_id;
780
787 data.tpt_media[0] = m_context->getTokString(8);
788 if (m_context->isTokString(9)) data.tpt_media[1] = m_context->getTokString(9); // <== Optional Media2 is specific for prop
789 m_tuneup->prop_tweaks.insert(std::make_pair(prop_id, data));
790
791 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for prop '{}'"
792 " with params {{ media1={}, offsetX={}, offsetY={}, offsetZ={}, rotX={}, rotY={}, rotZ={}, media2={} }}",
794 data.tpt_offset.x, data.tpt_offset.y, data.tpt_offset.z,
795 data.tpt_rotation.x, data.tpt_rotation.y, data.tpt_rotation.z,
796 data.tpt_media[1]));
797 }
798 else if (m_tuneup->prop_tweaks[prop_id].tpt_origin != m_addonpart_entry->fname)
799 {
800 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for prop '{}' due to conflict with '{}'",
802 m_tuneup->prop_tweaks[prop_id].tpt_origin));
803
804 m_tuneup->prop_tweaks.erase(prop_id);
805 }
806 }
807 else
808 {
809 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping prop '{}' because it's marked PROTECTED",
811 }
812 }
813 else
814 {
815 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword()));
816 }
817}
818
820{
821 ROR_ASSERT(m_context->getTokKeyword() == "addonpart_tweak_managedmaterial"); // also asserts !EOF and TokenType::KEYWORD
822
824 {
825 const std::string& mat_name = m_context->getTokString(1);
826 if (!m_tuneup->isManagedMatProtected(mat_name))
827 {
828 if (m_tuneup->managedmat_tweaks.find(mat_name) == m_tuneup->managedmat_tweaks.end())
829 {
831 bool stop=false;
833 data.tmt_name = mat_name;
835 if (!stop && m_context->isTokString(3)) { data.tmt_media[0] = m_context->getTokString(3); } else {stop=true;}
836 if (!stop && m_context->isTokString(4)) { data.tmt_media[1] = m_context->getTokString(4); } else {stop=true;}
837 if (!stop && m_context->isTokString(5)) { data.tmt_media[2] = m_context->getTokString(5); } else {stop=true;}
838 m_tuneup->managedmat_tweaks.insert(std::make_pair(mat_name, data));
839
840 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': Scheduling tweak for managed material '{}'"
841 " with params {{ type={}, media1={}, media2={}, media3={} }}",
842 m_addonpart_entry->fname, m_context->getTokKeyword(), mat_name, data.tmt_type, data.tmt_media[0], data.tmt_media[1], data.tmt_media[2]));
843 }
844 else if (m_tuneup->managedmat_tweaks[mat_name].tmt_origin != m_addonpart_entry->fname)
845 {
846 this->Log(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': Resetting tweaks for managed material '{}' due to conflict with '{}'",
848 m_tuneup->managedmat_tweaks[mat_name].tmt_origin));
849
850 m_tuneup->managedmat_tweaks.erase(mat_name);
851 }
852 }
853 else
854 {
855 this->Log(fmt::format("[RoR|Addonpart] INFO: file '{}', directive '{}': skipping managed material '{}' because it's marked PROTECTED",
857 }
858 }
859}
860
862{
863 LOG(fmt::format("[RoR|Addonpart] -- Performing `RecordAddonpartConflicts()` between '{}' and '{}' ~ this involves generating dummy tuneups (hence messages below) --", addonpart1->fname, addonpart2->fname));
864
865 // Make sure both addonparts are loaded and cached.
866 App::GetCacheSystem()->LoadResource(addonpart1);
867 if (!addonpart1->addonpart_data_only)
868 {
869 addonpart1->addonpart_data_only = new TuneupDef();
870 AddonPartUtility util(/*silent mode:*/true);
871 util.ResolveUnwantedAndTweakedElements(addonpart1->addonpart_data_only, addonpart1);
872 }
873
874 App::GetCacheSystem()->LoadResource(addonpart2);
875 if (!addonpart2->addonpart_data_only)
876 {
877 addonpart2->addonpart_data_only = new TuneupDef();
878 AddonPartUtility util(/*silent mode:*/true);
879 util.ResolveUnwantedAndTweakedElements(addonpart2->addonpart_data_only, addonpart2);
880 }
881
882 // NODE TWEAKS:
883 for (auto& i_pair: addonpart1->addonpart_data_only->node_tweaks)
884 {
885 NodeNum_t suspect = i_pair.second.tnt_nodenum;
886 TuneupNodeTweak* offender = nullptr;
887 if (TuneupUtil::isNodeTweaked(addonpart2->addonpart_data_only, suspect, offender))
888 {
889 conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_node", (int)suspect});
890 LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - node {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect));
891 }
892 }
893
894 // WHEEL TWEAKS:
895 for (auto& i_pair: addonpart1->addonpart_data_only->wheel_tweaks)
896 {
897 WheelID_t suspect = i_pair.second.twt_wheel_id;
898 TuneupWheelTweak* offender = nullptr;
899 if (TuneupUtil::isWheelTweaked(addonpart2->addonpart_data_only, suspect, offender))
900 {
901 conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_wheel", (int)suspect});
902 LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - wheel {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect));
903 }
904 }
905
906 // PROP TWEAKS:
907 for (auto& i_pair:addonpart1->addonpart_data_only->prop_tweaks)
908 {
909 PropID_t suspect = i_pair.second.tpt_prop_id;
910 TuneupPropTweak* offender = nullptr;
911 if (TuneupUtil::isPropTweaked(addonpart2->addonpart_data_only, suspect, offender))
912 {
913 conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_prop", (int)suspect});
914 LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - prop {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect));
915 }
916 }
917
918 // FLEXBODY TWEAKS:
919 for (auto& i_pair: addonpart1->addonpart_data_only->flexbody_tweaks)
920 {
921 FlexbodyID_t suspect = i_pair.second.tft_flexbody_id;
922 TuneupFlexbodyTweak* offender = nullptr;
923 if (TuneupUtil::isFlexbodyTweaked(addonpart2->addonpart_data_only, suspect, offender))
924 {
925 conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_flexbody", (int)suspect});
926 LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - flexbody {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect));
927 }
928 }
929
930 LOG(fmt::format("[RoR|Addonpart] -- Done with `RecordAddonpartConflicts()` between '{}' and '{}' --", addonpart1->fname, addonpart2->fname));
931}
932
934{
935 if (!addonpart1 || !addonpart2)
936 {
937 return false;
938 }
939
940 for (AddonPartConflict& conflict: conflicts)
941 {
942 if ((conflict.atc_addonpart1 == addonpart1 && conflict.atc_addonpart2 == addonpart2) ||
943 (conflict.atc_addonpart1 == addonpart2 && conflict.atc_addonpart2 == addonpart1))
944 {
945 return true;
946 }
947 }
948 return false;
949}
950
951void AddonPartUtility::Log(const std::string& text)
952{
953 if (!m_silent_mode)
954 {
955 LOG(text);
956 }
957}
958
960{
961 // Re-check conflicts (request may come from 'Browse all' button or script).
962 // -------------------------------------------------------------------------
963
964 AddonPartConflictVec conflicts;
965 for (const std::string& use_addonpart: target_actor->getWorkingTuneupDef()->use_addonparts)
966 {
967 CacheEntryPtr use_entry = App::GetCacheSystem()->FindEntryByFilename(LT_AddonPart, /*partial=*/false, use_addonpart);
968 AddonPartUtility::RecordAddonpartConflicts(addonpart_entry, use_entry, conflicts);
969 }
970
971 if (conflicts.size() > 0)
972 {
973 // Messagebox text
975 dialog->mbc_content_width = 700.f;
976 dialog->mbc_title = _LC("Tuning", "Cannot install addon part, conflicts were detected.");
977 dialog->mbc_text = fmt::format(_LC("Tuning", "Requested addon part: '{}' (file '{}')."), addonpart_entry->dname, addonpart_entry->fname);
978 dialog->mbc_text += "\n";
979 dialog->mbc_text += fmt::format(_LC("Tuning", "Total conflicts: {}."), conflicts.size());
980 dialog->mbc_text += "\n";
981 for (size_t i=0; i < conflicts.size(); i++)
982 {
983 dialog->mbc_text += "\n";
984 dialog->mbc_text += fmt::format(_LC("Tuning", "[{}/{}] '{}' (file '{}') conflicts with '{}' #{}."),
985 i+1, conflicts.size(),
986 conflicts[i].atc_addonpart2->dname, conflicts[i].atc_addonpart2->fname,
987 conflicts[i].atc_keyword, conflicts[i].atc_element_id);
988 }
989
990 // Messagebox OK button
992 ok_btn.mbb_caption = _LC("Tuning", "OK");
994 dialog->mbc_buttons.push_back(ok_btn);
995
996 // Show the messagebox
998 }
999 return conflicts.size() > 0;
1000}
quaternion Log() const
Central state/object manager and communications hub.
#define ROR_ASSERT(_EXPR)
Definition Application.h:40
void LOG(const char *msg)
Legacy alias - formerly a macro.
uint32_t BitMask_t
Definition BitFlags.h:7
A database of user-installed content alias 'mods' (vehicles, terrains...)
Game state manager and message-queue provider.
Generic text file parser.
#define _LC(ctx, str)
Definition Language.h:38
Checks the rig-def file syntax and loads data to memory.
The vehicle tuning system; applies addonparts and user overrides to vehicles.
Legacy parser resolved references on-the-fly and the condition to check named nodes was "are there an...
Definition RigDef_Node.h:78
static void ProcessForsetLine(RigDef::Flexbody &def, const std::string &line, int line_number=-1)
static SpecialProp IdentifySpecialProp(const std::string &str)
static Keyword IdentifyKeyword(const std::string &line)
TuneupDefPtr & getWorkingTuneupDef()
Definition Actor.cpp:4893
NOTE: Modcache processes this format directly using RoR::GenericDocument, see RoR::CacheSystem::FillA...
static bool CheckForAddonpartConflict(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec &conflicts)
AddonPartUtility(bool silent_mode=false)
static void RecordAddonpartConflicts(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec &conflicts)
bool m_silent_mode
To block logging during conflict resolution (which works by generating dummy tuneups - would confuse ...
GenericDocContextPtr m_context
std::shared_ptr< RigDef::Document::Module > TransformToRigDefModule(CacheEntryPtr &addonpart_entry)
transforms the addonpart to RigDef::File::Module (fake 'section/end_section') used for spawning.
GenericDocumentPtr m_document
void Log(const std::string &text)
std::shared_ptr< RigDef::Document::Module > m_module
RigDef::ManagedMaterialsOptions m_managedmaterials_options
void ResolveUnwantedAndTweakedElements(TuneupDefPtr &tuneup, CacheEntryPtr &addonpart_entry)
Evaluates 'addonpart_unwanted_*' elements, respecting 'protected_*' directives in the tuneup.
static void ResetUnwantedAndTweakedElements(TuneupDefPtr &tuneup)
static bool DoubleCheckForAddonpartConflict(ActorPtr target_actor, CacheEntryPtr addonpart_entry)
Ogre::String fname
filename
Definition CacheSystem.h:67
TuneupDefPtr addonpart_data_only
Cached addonpart data (dummy tuneup), only used for evaluating conflicts, see AddonPartUtility::Recor...
Definition CacheSystem.h:94
Ogre::String dname
name parsed from the file
Definition CacheSystem.h:70
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
void putMessage(MessageArea area, MessageType type, std::string const &msg, std::string icon="")
Definition Console.cpp:103
@ CONSOLE_SYSTEM_WARNING
Definition Console.h:53
void PushMessage(Message m)
Doesn't guarantee order! Use ChainMessage() if order matters.
static bool isWheelTweaked(TuneupDefPtr &tuneup_entry, WheelID_t wheel_id, TuneupWheelTweak *&out_tweak)
static bool isNodeTweaked(TuneupDefPtr &tuneup_entry, NodeNum_t nodenum, TuneupNodeTweak *&out_tweak)
static bool isFlexbodyTweaked(TuneupDefPtr &tuneup_entry, FlexbodyID_t flexbody_id, TuneupFlexbodyTweak *&out_tweak)
static bool isPropTweaked(TuneupDefPtr &tuneup_entry, PropID_t flexbody_id, TuneupPropTweak *&out_tweak)
@ MSG_GUI_HIDE_MESSAGE_BOX_REQUESTED
@ MSG_GUI_SHOW_MESSAGE_BOX_REQUESTED
Payload = MessageBoxConfig* (owner)
GameContext * GetGameContext()
Console * GetConsole()
CacheSystem * GetCacheSystem()
@ LT_AddonPart
int CineCameraID_t
Index into Actor::ar_cinecam_node and Actor::ar_camera_node_* arrays; use RoR::CINECAMERAID_INVALID a...
int WheelID_t
Index to Actor::ar_wheels, use RoR::WHEELID_INVALID as empty value.
int PropID_t
Index to GfxActor::m_props, use RoR::PROPID_INVALID as empty value.
int FlareID_t
Index into Actor::ar_flares, use RoR::FLAREID_INVALID as empty value.
int ExhaustID_t
Index into GfxActor::m_exhausts, use RoR::EXHAUSTID_INVALID as empty value.
int FlexbodyID_t
Index to GfxActor::m_flexbodies, use RoR::FLEXBODYID_INVALID as empty value.
uint16_t NodeNum_t
Node position within Actor::ar_nodes; use RoR::NODENUM_INVALID as empty value.
std::vector< AddonPartConflict > AddonPartConflictVec
RoR::FlareType type
Ogre::String material_name
Node::Ref node_axis_x
Node::Ref reference_node
Ogre::Vector3 offset
int control_number
Only 'u' type flares.
std::string dashboard_link
Only 'd' type flares.
Node::Ref node_axis_y
std::vector< Node::Range > node_list_to_import
Ogre::String mesh_name
Node::Ref y_axis_node
Ogre::Vector3 rotation
Ogre::Vector3 offset
Node::Ref reference_node
Node::Ref x_axis_node
std::vector< Node::Ref > node_list
ManagedMaterialType type
ManagedMaterialsOptions options
Ogre::String specular_map
Ogre::String damaged_diffuse_map
Ogre::ColourValue color
Node::Ref reference_node
Node::Ref x_axis_node
DashboardSpecial special_prop_dashboard
Ogre::Vector3 offset
Ogre::String mesh_name
BeaconSpecial special_prop_beacon
Node::Ref y_axis_node
SpecialProp special
Ogre::Vector3 rotation
< Conflict between two addonparts tweaking the same element
MsgType mbb_mq_message
Message to queue on click.
float mbc_content_width
Parameter to ImGui::SetContentWidth() - hard limit on content size.
std::vector< MessageBoxButton > mbc_buttons
float getTokNumeric(int offset=0) const
bool isTokKeyword(int offset=0) const
std::string getTokKeyword(int offset=0) const
float getTokFloat(int offset=0) const
bool isTokNumeric(int offset=0) const
bool endOfFile(int offset=0) const
bool isTokLineBreak(int offset=0) const
std::string getTokString(int offset=0) const
bool isTokComment(int offset=0) const
bool isTokString(int offset=0) const
bool isTokInt(int offset=0) const
int getTokInt(int offset=0) const
float getFloatData(int offset=0) const
bool isTokFloat(int offset=0) const
const char * getStringData(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.
Unified game event system - all requests and state changes are reported using a message.
Definition GameContext.h:52
< Data of 'addonpart_tweak_cinecam <cinecam ID> <posX> <posY> <posZ>'
CineCameraID_t tct_cinecam_id
Arg#1, required.
Ogre::Vector3 tct_pos
Args#234, required.
std::string tct_origin
Addonpart filename.
Dual purpose:
std::map< WheelID_t, TuneupWheelTweak > wheel_tweaks
Mesh name and radius overrides via 'addonpart_tweak_wheel'.
bool isFlexbodyProtected(FlexbodyID_t flexbodyid) const
bool isPropProtected(PropID_t propid) const
std::set< PropID_t > unwanted_props
'addonpart_unwanted_prop' directives.
std::set< ExhaustID_t > unwanted_exhausts
'addonpart_unwanted_exhaust' directives.
std::set< FlexbodyID_t > unwanted_flexbodies
'addonpart_unwanted_flexbody' directives.
std::map< PropID_t, TuneupPropTweak > prop_tweaks
Mesh name(s), offset and rotation overrides via 'addonpart_tweak_prop'.
bool isCineCameraProtected(CineCameraID_t cinecamid) const
std::set< FlareID_t > unwanted_flares
'addonpart_unwanted_flare' directives.
std::map< NodeNum_t, TuneupNodeTweak > node_tweaks
Node position overrides via 'addonpart_tweak_node'.
std::set< std::string > use_addonparts
Addonpart filenames.
std::map< FlexbodyID_t, TuneupFlexbodyTweak > flexbody_tweaks
Mesh name, offset and rotation overrides via 'addonpart_tweak_flexbody'.
std::set< std::string > unwanted_managedmats
'addonpart_unwanted_managedmaterial' directives.
bool isWheelProtected(WheelID_t wheelid) const
bool isManagedMatProtected(const std::string &matname) const
bool isExhaustProtected(ExhaustID_t exhaustid) const
std::map< CineCameraID_t, TuneupCineCameraTweak > cinecam_tweaks
Cinecam position overrides via 'addonpart_tweak_cinecam'.
std::map< std::string, TuneupManagedMatTweak > managedmat_tweaks
Managed material overrides via 'addonpart_tweak_managedmaterial'.
bool isNodeProtected(NodeNum_t nodenum) const
bool isFlareProtected(FlareID_t flareid) const
< Data of 'addonpart_tweak_flexbody <flexbody ID> <offsetX> <offsetY> <offsetZ> <rotX> <rotY> <rotZ> ...
std::string tft_origin
Addonpart filename.
< Data of 'addonpart_tweak_managedmaterial <name> <type> <media1> <media2> [<media3>]'
std::string tmt_name
Arg#1, required.
std::string tmt_type
Arg#2, required.
std::string tmt_origin
Addonpart filename.
std::array< std::string, 3 > tmt_media
Arg#3, required, Arg#4, optional, Arg#5, optional.
< Data of 'addonpart_tweak_node <nodenum> <posX> <posY> <posZ>'
std::string tnt_origin
Addonpart filename.
NodeNum_t tnt_nodenum
Arg#1, required.
Ogre::Vector3 tnt_pos
Args#234, required.
< Data of 'addonpart_tweak_prop <prop ID> <offsetX> <offsetY> <offsetZ> <rotX> <rotY> <rotZ> <media1>...
std::string tpt_origin
Addonpart filename.
Ogre::Vector3 tpt_rotation
std::array< std::string, 2 > tpt_media
Media1 = prop mesh; Media2: Steering wheel mesh or beacon flare material.
< Data of 'addonpart_tweak_wheel <wheel ID> <media1> <media2> <side flag> <tire radius> <rim radius>'
WheelSide twt_side
Arg#4, optional, default LEFT (Only applicable to mesh/flexbody wheels)
float twt_tire_radius
Arg#5, optional.
std::array< std::string, 2 > twt_media
twt_media[0] Arg#2, required ('wheels[2]': face material, 'meshwheels[2]/flexbodywheels': rim mesh) t...
std::string twt_origin
Addonpart filename.
WheelID_t twt_wheel_id
Arg#1, required.
float twt_rim_radius
Arg#6, optional, only applies to some wheel types.