SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
FGD.cpp
Go to the documentation of this file.
1// ReSharper disable CppRedundantQualifier
2// ReSharper disable CppUseStructuredBinding
3
4#include <toolpp/FGD.h>
5
6#include <algorithm>
7#include <filesystem>
8#include <format>
9#include <initializer_list>
10
11#include <BufferStream.h>
12
14#include <sourcepp/FS.h>
15#include <sourcepp/Math.h>
16
17using namespace sourcepp;
18using namespace std::string_view_literals;
19using namespace toolpp;
20
21namespace {
22
23constexpr auto INVALID_SYNTAX_MSG = "Invalid syntax found in FGD!";
24constexpr auto INVALID_CLASS_MSG = "Invalid class found in FGD!";
25
26[[nodiscard]] bool tryToEatSeparator(BufferStream& stream, char sep) {
28 return parser::text::tryToEatChar(stream, sep);
29}
30
31// FGD strings are weird - they can be split across several lines, only accept \n, \t, and \" escapes,
32// and don't need to be terminated by a double quote. Really gross...
33[[nodiscard]] std::string_view readFGDString(BufferStreamReadOnly& stream, BufferStream& backing) {
35
36 static constexpr std::string_view END = "\"\n";
37
38 const auto startSpan = backing.tell();
39 while (true) {
40 char c = stream.read<char>();
41 if (c != '\"') {
42 stream.seek(-1, std::ios::cur);
43 const auto out = parser::text::readUnquotedStringToBuffer(stream, backing, ":", parser::text::EscapeSequenceMap{});
44 if (stream.seek(-1, std::ios::cur).peek<char>() != ':') {
45 stream.skip();
47 }
48 // ReSharper disable once CppDFALocalValueEscapesFunction
49 return out;
50 }
51
52 for (c = stream.read<char>(); END.find(c) == std::string_view::npos; c = stream.read<char>()) {
53 if (c == '\\') {
54 const auto n = stream.read<char>();
55 if (n == 'n') {
56 backing << '\n';
57 } else if (n == 't') {
58 backing << '\t';
59 } else if (n == '\"') {
60 backing << '\"';
61 } else if (END.find(c) != std::string_view::npos) {
62 break;
63 } else {
64 backing << c << n;
65 }
66 } else {
67 backing << c;
68 }
69 }
70
71 if (!::tryToEatSeparator(stream, '+')) {
72 break;
73 }
74 // We need to make sure the next line is actually a string, because sometimes + will lead to nothing. Lovely
76 if (stream.peek<char>() != '\"') {
77 break;
78 }
79 }
80
81 if (stream.seek(-1, std::ios::cur).peek<char>() != ':') {
82 stream.skip();
84 }
85
86 backing << '\0';
87 // ReSharper disable once CppRedundantCastExpression
88 return {reinterpret_cast<const char*>(backing.data()) + startSpan, static_cast<std::string_view::size_type>(backing.tell() - 1 - startSpan)};
89}
90
91void readVersion(BufferStreamReadOnly& stream, BufferStream& backing, int& version) {
92 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
94 if (stream.peek<char>() != '(') {
95 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
96 }
97 }
98 std::string versionString{parser::text::readStringToBuffer(stream, backing, "(", ")", parser::text::EscapeSequenceMap{})};
99 string::trim(versionString);
100 string::toInt(versionString, version);
101}
102
103void readMapSize(BufferStreamReadOnly& stream, BufferStream& backing, math::Vec2i& mapSize) {
104 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
106 if (stream.peek<char>() != '(') {
107 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
108 }
109 }
110 const auto mapSizeString = parser::text::readStringToBuffer(stream, backing, "(", ")", parser::text::EscapeSequenceMap{});
111 auto mapSizes = string::split(mapSizeString, ',');
112 if (mapSizes.size() != 2) {
113 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
114 }
115
116 string::trim(mapSizes[0]);
117 string::trim(mapSizes[1]);
118 string::toInt(mapSizes[0], mapSize[0]);
119 string::toInt(mapSizes[1], mapSize[1]);
120}
121
122void readMaterialExclusionDirs(BufferStreamReadOnly& stream, BufferStream& backing, std::vector<std::string_view>& materialExclusionDirs) {
123 if (!::tryToEatSeparator(stream, '[')) {
124 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
125 }
126 while (!::tryToEatSeparator(stream, ']')) {
127 materialExclusionDirs.push_back(::readFGDString(stream, backing));
128 }
129 stream.skip();
130}
131
132void readMainKeyValues(BufferStreamReadOnly& stream, BufferStream& backing, std::unordered_map<std::string_view, std::string_view>& mainKeyValues) {
133 if (!::tryToEatSeparator(stream, '=')) {
134 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
135 }
136 if (!::tryToEatSeparator(stream, '[')) {
137 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
138 }
139 while (!::tryToEatSeparator(stream, ']')) {
140 auto key = ::readFGDString(stream, backing);
141 if (!::tryToEatSeparator(stream, ':')) {
142 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
143 }
144 auto value = ::readFGDString(stream, backing);
145 mainKeyValues[key] = value;
146 }
147 stream.skip();
148}
149
150void readAutoVisGroups(BufferStreamReadOnly& stream, BufferStream& backing, std::vector<FGD::AutoVisGroup>& autoVisGroups) {
151 if (!::tryToEatSeparator(stream, '=')) {
152 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
153 }
154 const auto parentName = ::readFGDString(stream, backing);
155 if (!::tryToEatSeparator(stream, '[')) {
156 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
157 }
158 while (!::tryToEatSeparator(stream, ']')) {
159 auto& autoVisGroup = autoVisGroups.emplace_back();
160 autoVisGroup.parentName = parentName;
161 autoVisGroup.name = ::readFGDString(stream, backing);
162 if (!::tryToEatSeparator(stream, '[')) {
163 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
164 }
165 while (!::tryToEatSeparator(stream, ']')) {
166 autoVisGroup.entities.push_back(::readFGDString(stream, backing));
167 }
168 stream.skip();
169 }
170 stream.skip();
171}
172
173void readClassProperties(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity) {
175
176 while (stream.peek<char>() != '=') {
177 FGD::Entity::ClassProperty classProperty;
179 classProperty.arguments = "";
180
181 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
183 if (stream.peek<char>() != '(') {
184 entity.classProperties.push_back(classProperty);
185 continue;
186 }
187 }
188 classProperty.arguments = parser::text::readStringToBuffer(stream, backing, "(", ")", {{'n', '\n'}});
189 entity.classProperties.push_back(classProperty);
190
192 }
193
194 stream.skip();
196}
197
198void readEntityIO(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity, bool input) {
199 auto& io = input ? entity.inputs.emplace_back() : entity.outputs.emplace_back();
201 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
203 if (stream.peek<char>() != '(') {
204 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
205 }
206 }
207 io.valueType = parser::text::readStringToBuffer(stream, backing, "(", ")", parser::text::EscapeSequenceMap{});
208
209 if (!::tryToEatSeparator(stream, ':')) {
210 io.description = "";
211 return;
212 }
213 io.description = ::readFGDString(stream, backing);
214
216}
217
218void readEntityFieldModifiers(BufferStreamReadOnly& stream, BufferStream& backing, bool& readonly, bool& reportable) {
219 readonly = false;
220 reportable = false;
221
223 if (stream.peek<char>() == 'r') {
224 if (const auto modifier = parser::text::readUnquotedStringToBuffer(stream, backing); modifier == "readonly") {
225 readonly = true;
226 } else if (modifier == "report") {
227 reportable = true;
228 return;
229 } else {
230 stream.seek(-static_cast<int64_t>(modifier.length()), std::ios::cur);
231 return;
232 }
233 }
235 if (stream.peek<char>() == 'r') {
236 if (const auto modifier = parser::text::readUnquotedStringToBuffer(stream, backing); modifier == "report") {
237 reportable = true;
238 } else {
239 stream.seek(-static_cast<int64_t>(modifier.length()), std::ios::cur);
240 //return;
241 }
242 }
243}
244
245void readEntityKeyValue(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity) {
246 // Key and value type (looks like "key(valueType)", or "input key(valueType)" for i/o)
247 const auto name = parser::text::readUnquotedStringToBuffer(stream, backing, "(", parser::text::EscapeSequenceMap{});
249 if (string::iequals(name, "input")) {
250 ::readEntityIO(stream, backing, entity, true);
251 return;
252 }
253 if (string::iequals(name, "output")) {
254 ::readEntityIO(stream, backing, entity, false);
255 return;
256 }
257 const auto valueType = parser::text::readUnquotedStringToBuffer(stream, backing, ")", parser::text::EscapeSequenceMap{});
258 // If there is a space after the value type, we need to get rid of the parenthesis here
260 if (stream.peek<char>() == ')') {
261 stream.skip();
262 }
263
264 if (string::iequals(valueType, "choices")) {
265 auto& field = entity.fieldsWithChoices.emplace_back();
266 field.name = name;
267 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
268
269 if (::tryToEatSeparator(stream, ':')) {
270 field.displayName = ::readFGDString(stream, backing);
272 }
273
274 if (::tryToEatSeparator(stream, ':')) {
275 field.valueDefault = ::readFGDString(stream, backing);
277 }
278
279 if (::tryToEatSeparator(stream, ':')) {
280 field.description = ::readFGDString(stream, backing);
282 }
283
284 if (!::tryToEatSeparator(stream, '=') || !::tryToEatSeparator(stream, '[')) {
285 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
286 }
287 while (stream.peek<char>() != ']') {
288 auto& choice = field.choices.emplace_back();
289 choice.value = ::readFGDString(stream, backing);
290
291 if (::tryToEatSeparator(stream, ':')) {
292 choice.displayName = ::readFGDString(stream, backing);
293 } else {
294 choice.displayName = "";
295 }
296 }
297 stream.skip();
298 } else if (string::iequals(valueType, "flags")) {
299 auto& field = entity.fieldsWithFlags.emplace_back();
300 field.name = name;
301 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
302
303 if (::tryToEatSeparator(stream, ':')) {
304 field.displayName = ::readFGDString(stream, backing);
306 }
307
308 if (::tryToEatSeparator(stream, ':')) {
309 field.description = ::readFGDString(stream, backing);
311 }
312
313 if (!::tryToEatSeparator(stream, '=') || !::tryToEatSeparator(stream, '[')) {
314 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
315 }
317
318 while (stream.peek<char>() != ']') {
319 auto& flag = field.flags.emplace_back();
320 const auto valueString = ::readFGDString(stream, backing);
321 if (string::toInt(valueString, flag.value).ec != std::errc{}) {
322 flag.value = 0;
323 }
324
325 if (!::tryToEatSeparator(stream, ':')) {
326 continue;
327 }
328 flag.displayName = ::readFGDString(stream, backing);
329
330 if (!::tryToEatSeparator(stream, ':')) {
331 continue;
332 }
333 const auto enabledByDefaultString = ::readFGDString(stream, backing);
334 int enabledByDefault = 0;
335 if (string::toInt(enabledByDefaultString, enabledByDefault).ec != std::errc{}) {
336 flag.enabledByDefault = false;
337 } else {
338 flag.enabledByDefault = static_cast<bool>(enabledByDefault);
339 }
340
341 if (!::tryToEatSeparator(stream, ':')) {
342 continue;
343 }
344 flag.description = ::readFGDString(stream, backing);
345 }
346 stream.skip();
347 } else {
348 auto& field = entity.fields.emplace_back();
349 field.name = name;
350 field.valueType = valueType;
351 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
352 field.displayName = "";
353 field.valueDefault = "";
354 field.description = "";
355
356 if (!::tryToEatSeparator(stream, ':')) {
357 return;
358 }
359 field.displayName = ::readFGDString(stream, backing);
360
361 if (!::tryToEatSeparator(stream, ':')) {
362 return;
363 }
364 field.valueDefault = ::readFGDString(stream, backing);
365
366 if (!::tryToEatSeparator(stream, ':')) {
367 return;
368 }
369 field.description = ::readFGDString(stream, backing);
370
372 }
373}
374
375void overwriteEntity(FGD::Entity& oldEntity, const FGD::Entity& newEntity) {
376 oldEntity.classProperties = newEntity.classProperties;
377 if (!newEntity.description.empty()) {
378 oldEntity.description = newEntity.description;
379 }
380 if (!newEntity.docsURL.empty()) {
381 oldEntity.docsURL = newEntity.docsURL;
382 }
383 for (const auto& field : newEntity.fields) {
384 if (auto it = std::ranges::find_if(oldEntity.fields, [&field](const auto& oldField) {
385 return oldField.name == field.name;
386 }); it != oldEntity.fields.end()) {
387 it->valueType = field.valueType;
388 it->readonly = field.readonly;
389 it->reportable = field.reportable;
390 if (!field.displayName.empty()) {
391 it->displayName = field.displayName;
392 }
393 if (!field.valueDefault.empty()) {
394 it->valueDefault = field.valueDefault;
395 }
396 if (!field.description.empty()) {
397 it->description = field.description;
398 }
399 } else {
400 oldEntity.fields.push_back(field);
401 }
402 }
403 for (const auto& field : newEntity.fieldsWithChoices) {
404 if (auto it = std::ranges::find_if(oldEntity.fieldsWithChoices, [&field](const auto& oldField) {
405 return oldField.name == field.name;
406 }); it != oldEntity.fieldsWithChoices.end()) {
407 it->readonly = field.readonly;
408 it->reportable = field.reportable;
409 if (!field.displayName.empty()) {
410 it->displayName = field.displayName;
411 }
412 if (!field.valueDefault.empty()) {
413 it->valueDefault = field.valueDefault;
414 }
415 if (!field.description.empty()) {
416 it->description = field.description;
417 }
418 it->choices = field.choices;
419 } else {
420 oldEntity.fieldsWithChoices.push_back(field);
421 }
422 }
423 for (const auto& field : newEntity.fieldsWithFlags) {
424 if (auto it = std::ranges::find_if(oldEntity.fieldsWithFlags, [&field](const auto& oldField) {
425 return oldField.name == field.name;
426 }); it != oldEntity.fieldsWithFlags.end()) {
427 it->readonly = field.readonly;
428 it->reportable = field.reportable;
429 it->flags = field.flags;
430 } else {
431 oldEntity.fieldsWithFlags.push_back(field);
432 }
433 }
434 for (const auto& input : newEntity.inputs) {
435 if (auto it = std::ranges::find_if(oldEntity.inputs, [&input](const auto& oldInput) {
436 return oldInput.name == input.name;
437 }); it != oldEntity.inputs.end()) {
438 it->valueType = input.valueType;
439 if (!input.description.empty()) {
440 it->description = input.description;
441 }
442 } else {
443 oldEntity.inputs.push_back(input);
444 }
445 }
446 for (const auto& output : newEntity.outputs) {
447 if (auto it = std::ranges::find_if(oldEntity.outputs, [&output](const auto& oldOutput) {
448 return oldOutput.name == output.name;
449 }); it != oldEntity.outputs.end()) {
450 it->valueType = output.valueType;
451 if (!output.description.empty()) {
452 it->description = output.description;
453 }
454 } else {
455 oldEntity.outputs.push_back(output);
456 }
457 }
458}
459
460void readEntity(BufferStreamReadOnly& stream, BufferStream& backing, std::string_view classType, std::unordered_map<std::string_view, FGD::Entity>& entities, bool extension) {
461 FGD::Entity entity{};
462 entity.classType = classType;
463
464 // There are optionally a list of class properties after the class
465 if (!::tryToEatSeparator(stream, '=')) {
466 ::readClassProperties(stream, backing, entity);
467 }
468
469 // Entity name
471 const auto name = ::readFGDString(stream, backing);
472
473 // If a colon is here, the entity has a description
474 if (::tryToEatSeparator(stream, ':')) {
475 entity.description = ::readFGDString(stream, backing);
476 } else {
477 entity.description = "";
478 }
479
480 // If a colon is here, the entity has a docs URL
481 if (::tryToEatSeparator(stream, ':')) {
482 entity.docsURL = ::readFGDString(stream, backing);
483 } else {
484 entity.docsURL = "";
485 }
486
487 // Parse entity keyvalues
488 if (!::tryToEatSeparator(stream, '[')) {
489 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
490 }
491 while (!::tryToEatSeparator(stream, ']')) {
492 ::readEntityKeyValue(stream, backing, entity);
493 }
494 stream.skip();
495
496 if (extension && entities.contains(name)) {
497 // Overwrite/add parts of the entity description
498 ::overwriteEntity(entities[name], entity);
499 } else {
500 // Replace entity description entirely
501 entities[name] = entity;
502 }
503}
504
505void writeOptionalKeyValueStrings(BufferStream& writer, std::initializer_list<std::string_view> strings) {
506 static constexpr auto writeOptionalString = [](BufferStream& stream, std::string_view str) {
507 if (!str.empty()) {
508 stream
509 .write(" : \""sv, false)
510 .write(str, false)
511 .write('\"');
512 } else {
513 stream.write(" :"sv, false);
514 }
515 };
516 for (auto revString = std::rbegin(strings); revString != std::rend(strings); ++revString) {
517 if (revString->empty()) {
518 continue;
519 }
520 for (auto string = std::begin(strings); string != revString.base(); ++string) {
521 writeOptionalString(writer, std::string{*string});
522 }
523 return;
524 }
525}
526
527} // namespace
528
529FGD::FGD(const std::filesystem::path& fgdPath) {
530 this->load(fgdPath);
531}
532
533void FGD::load(const std::filesystem::path& fgdPath) {
534 auto fgdData = fs::readFileText(fgdPath);
535 if (fgdData.empty()) {
536 return;
537 }
538 BufferStreamReadOnly stream{fgdData};
539
540 try {
541 std::vector seenPaths{fgdPath};
542 this->readEntities(stream, fgdPath, seenPaths);
543 } catch (const std::overflow_error&) {}
544}
545
546int FGD::getVersion() const {
547 return this->version;
548}
549
550math::Vec2i FGD::getMapSize() const {
551 return this->mapSize;
552}
553
554const std::unordered_map<std::string_view, FGD::Entity>& FGD::getEntities() const {
555 return this->entities;
556}
557
558const std::vector<std::string_view>& FGD::getMaterialExclusionDirs() const {
559 return this->materialExclusionDirs;
560}
561
562const std::unordered_map<std::string_view, std::string_view>& FGD::getMainKeyValues() const {
563 return this->mainKeyValues;
564}
565
566const std::vector<FGD::AutoVisGroup>& FGD::getAutoVisGroups() const {
567 return this->autoVisGroups;
568}
569
570// NOLINTNEXTLINE(*-no-recursion)
571void FGD::readEntities(BufferStreamReadOnly& stream, const std::filesystem::path& path, std::vector<std::filesystem::path>& seenPaths) {
572 auto& backingString = this->backingData.emplace_back();
573 // Multiply by 2 to ensure buffer will have enough space (very generous)
574 backingString.resize(stream.size() * 2);
575 BufferStream backing{backingString, false};
576
577 while (true) {
579
580 // All entity definitions start with an '@' followed by the class type
581 if (stream.read<char>() != '@') {
582 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
583 }
584
585 const auto classType = parser::text::readUnquotedStringToBuffer(stream, backing, "(", parser::text::EscapeSequenceMap{});
586 if (string::iequals(classType, "include")) {
588 // Assume the include path is relative to the current file being processed
589 auto fgdPath = std::filesystem::path{path}.parent_path() / parser::text::readStringToBuffer(stream, backing);
590 if (std::ranges::find(seenPaths, fgdPath) != seenPaths.end()) {
591 continue;
592 }
593 seenPaths.push_back(fgdPath);
594
595 auto fgdData = fs::readFileText(fgdPath);
596 if (fgdData.empty()) {
597 continue;
598 }
599
600 BufferStreamReadOnly newStream{fgdData};
601 try {
602 // ReSharper disable once CppDFAInfiniteRecursion
603 this->readEntities(newStream, fgdPath, seenPaths);
604 } catch (const std::overflow_error&) {}
605 } else if (string::iequals(classType, "version")) {
606 ::readVersion(stream, backing, this->version);
607 } else if (string::iequals(classType, "mapsize")) {
608 ::readMapSize(stream, backing, this->mapSize);
609 } else if (string::iequals(classType, "MaterialExclusion")) {
610 ::readMaterialExclusionDirs(stream, backing, this->materialExclusionDirs);
611 } else if (string::iequals(classType, "Main")) {
612 ::readMainKeyValues(stream, backing, this->mainKeyValues);
613 } else if (string::iequals(classType, "AutoVisGroup")) {
614 ::readAutoVisGroups(stream, backing, this->autoVisGroups);
615 } else if (string::iequals(classType, "BaseClass") ||
616 string::iequals(classType, "PointClass") ||
617 string::iequals(classType, "NPCClass") ||
618 string::iequals(classType, "SolidClass") ||
619 string::iequals(classType, "KeyFrameClass") ||
620 string::iequals(classType, "MoveClass") ||
621 string::iequals(classType, "FilterClass")) {
622 ::readEntity(stream, backing, classType, this->entities, false);
623 } else if (string::iequals(classType, "ExtendClass")) {
624 ::readEntity(stream, backing, classType, this->entities, true);
625 } else {
626 throw parser::text::syntax_error{INVALID_CLASS_MSG};
627 }
628 }
629}
630
633
635 : parent(parent_) {}
636
639
642
645
647 return {};
648}
649
650FGDWriter& FGDWriter::include(const std::filesystem::path& fgdPath) {
651 this->writer
652 .write("@include \""sv, false)
653 .write(fgdPath.string(), false)
654 .write("\"\n\n"sv, false);
655 return *this;
656}
657
659 this->writer.write(std::format("@version({})\n\n", version), false);
660 return *this;
661}
662
664 this->writer.write(std::format("@mapsize({}, {})\n\n", mapSize[0], mapSize[1]), false);
665 return *this;
666}
667
668FGDWriter& FGDWriter::materialExclusionDirs(std::initializer_list<std::string_view> dirs) {
669 return this->materialExclusionDirs(std::span{dirs});
670}
671
672FGDWriter& FGDWriter::materialExclusionDirs(std::span<const std::string_view> dirs) {
673 this->writer.write("@MaterialExclusion\n[\n"sv, false);
674 for (const auto& dir : dirs) {
675 this->writer << '\t' << '\"';
676 this->writer.write(dir, false);
677 this->writer << '\"' << '\n';
678 }
679 this->writer.write("]\n\n"sv, false);
680 return *this;
681}
682
684 this->writer.write("@Main =\n[\n"sv, false);
685 return *this;
686}
687
688FGDWriter& FGDWriter::mainKeyValue(std::string_view key, std::string_view value) {
689 this->writer << '\t';
690 this->writer.write(key, false);
691 this->writer << ':' << ' ' << '\"';
692 this->writer.write(value, false);
693 this->writer << '\"' << '\n';
694 return *this;
695}
696
698 this->writer.write("]\n\n"sv, false);
699 return *this;
700}
701
703 this->writer
704 .write("@AutoVisGroup = \""sv, false)
705 .write(parentName, false)
706 .write("\"\n[\n"sv, false);
707 return AutoVisGroupWriter{*this};
708}
709
710FGDWriter::AutoVisGroupWriter &FGDWriter::AutoVisGroupWriter::visGroup(std::string_view name, std::initializer_list<std::string_view> entities) {
711 return this->visGroup(name, std::span{entities});
712}
713
714FGDWriter::AutoVisGroupWriter& FGDWriter::AutoVisGroupWriter::visGroup(std::string_view name, std::span<const std::string_view> entities) {
715 this->parent.writer
716 .write("\t\""sv, false)
717 .write(name, false)
718 .write("\"\n\t[\n"sv, false);
719 for (const auto& entity : entities) {
720 this->parent.writer
721 .write("\t\t\""sv, false)
722 .write(entity, false)
723 .write("\"\n"sv, false);
724 }
725 this->parent.writer.write("\t]\n"sv, false);
726 return *this;
727}
728
730 this->parent.writer.write("]\n\n"sv, false);
731 return this->parent;
732}
733
734FGDWriter::EntityWriter FGDWriter::beginEntity(std::string_view classType, std::initializer_list<std::string_view> classProperties, std::string_view name, std::string_view description, std::string_view docsURL) {
735 return this->beginEntity(classType, std::span{classProperties}, name, description, docsURL);
736}
737
738FGDWriter::EntityWriter FGDWriter::beginEntity(std::string_view classType, std::span<const std::string_view> classProperties, std::string_view name, std::string_view description, std::string_view docsURL) {
739 this->writer
740 .write('@')
741 .write(classType, false);
742 if (classProperties.empty()) {
743 this->writer << ' ';
744 } else {
745 this->writer << '\n';
746 for (const auto& classProperty : classProperties) {
747 this->writer
748 .write('\t')
749 .write(classProperty, false)
750 .write('\n');
751 }
752 }
753 this->writer
754 .write("= "sv, false)
755 .write(name, false)
756 .write(" :"sv, false);
757 // Put the description on the same line if it's short
758 if (description.size() < 32) {
759 this->writer
760 .write(" \""sv, false) // NOLINT(*-raw-string-literal)
761 .write(description, false);
762 } else {
763 this->writer
764 .write("\n\t\""sv, false)
765 .write(description, false);
766 }
767 this->writer.write('\"');
768 if (!docsURL.empty()) {
769 this->writer
770 .write(" : \""sv, false) // NOLINT(*-raw-string-literal)
771 .write(docsURL, false)
772 .write('\"');
773 }
774 this->writer.write("\n[\n"sv, false);
775 return EntityWriter{*this};
776}
777
778FGDWriter::EntityWriter& FGDWriter::EntityWriter::keyValue(std::string_view name, std::string_view valueType, std::string_view displayName, std::string_view valueDefault, std::string_view description, bool readOnly, bool report) {
779 this->parent.writer
780 .write('\t')
781 .write(name, false)
782 .write('(')
783 .write(valueType, false)
784 .write(')');
785 if (readOnly) {
786 this->parent.writer.write(" readonly"sv, false);
787 }
788 if (report) {
789 this->parent.writer.write(" report"sv, false);
790 }
791 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, valueDefault, description});
792 this->parent.writer << '\n';
793 return *this;
794}
795
796FGDWriter::EntityWriter::KeyValueChoicesWriter FGDWriter::EntityWriter::beginKeyValueChoices(std::string_view name, std::string_view displayName, std::string_view valueDefault, std::string_view description, bool readOnly, bool report) {
797 this->parent.writer
798 .write('\t')
799 .write(name, false)
800 .write("(choices)"sv, false);
801 if (readOnly) {
802 this->parent.writer.write(" readonly"sv, false);
803 }
804 if (report) {
805 this->parent.writer.write(" report"sv, false);
806 }
807 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, valueDefault, description});
808 this->parent.writer.write(" =\n\t[\n"sv, false);
809 return KeyValueChoicesWriter{*this};
810}
811
813 this->parent.parent.writer
814 .write("\t\t\""sv, false)
815 .write(value, false)
816 .write("\" : \""sv, false)
817 .write(displayName, false)
818 .write("\"\n"sv, false);
819 return *this;
820}
821
823 this->parent.parent.writer.write("\t]\n"sv, false);
824 return this->parent;
825}
826
827FGDWriter::EntityWriter::KeyValueFlagsWriter FGDWriter::EntityWriter::beginKeyValueFlags(std::string_view name, std::string_view displayName, std::string_view description, bool readOnly, bool report) {
828 this->parent.writer
829 .write('\t')
830 .write(name, false)
831 .write("(flags)"sv, false);
832 if (readOnly) {
833 this->parent.writer.write(" readonly"sv, false);
834 }
835 if (report) {
836 this->parent.writer.write(" report"sv, false);
837 }
838 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, description});
839 this->parent.writer.write(" =\n\t[\n"sv, false);
840 return KeyValueFlagsWriter{*this};
841}
842
843FGDWriter::EntityWriter::KeyValueFlagsWriter& FGDWriter::EntityWriter::KeyValueFlagsWriter::flag(uint64_t value, std::string_view displayName, bool enabledByDefault, std::string_view description) {
844 this->parent.parent.writer.write(std::format("\t\t{} : \"{}\" : {:b}", value, displayName, enabledByDefault), false);
845 if (!description.empty()) {
846 this->parent.parent.writer.write(std::format(" : \"{}\"", description), false);
847 }
848 this->parent.parent.writer.write('\n');
849 return *this;
850}
851
853 this->parent.parent.writer.write("\t]\n"sv, false);
854 return this->parent;
855}
856
857FGDWriter::EntityWriter& FGDWriter::EntityWriter::input(std::string_view name, std::string_view valueType, std::string_view description) {
858 this->parent.writer
859 .write('\t')
860 .write("input "sv, false)
861 .write(name, false)
862 .write('(')
863 .write(valueType, false)
864 .write(')');
865 if (!description.empty()) {
866 this->parent.writer
867 .write(" : \""sv, false)
868 .write(description, false)
869 .write('\"');
870 }
871 this->parent.writer << '\n';
872 return *this;
873}
874
875FGDWriter::EntityWriter& FGDWriter::EntityWriter::output(std::string_view name, std::string_view valueType, std::string_view description) {
876 this->parent.writer
877 .write('\t')
878 .write("output "sv, false)
879 .write(name, false)
880 .write('(')
881 .write(valueType, false)
882 .write(')');
883 if (!description.empty()) {
884 this->parent.writer
885 .write(" : \""sv, false)
886 .write(description, false)
887 .write('\"');
888 }
889 this->parent.writer << '\n';
890 return *this;
891}
892
894 this->parent.writer.write("]\n\n"sv, false);
895 return this->parent;
896}
897
898std::string FGDWriter::bake() const {
899 // ReSharper disable once CppRedundantCastExpression
900 std::string_view out{this->backingData.data(), static_cast<std::string_view::size_type>(this->writer.tell())};
901 while (out.ends_with("\n\n")) {
902 out = out.substr(0, out.size() - 1);
903 }
904 return std::string{out};
905}
906
907bool FGDWriter::bake(const std::filesystem::path& fgdPath) const {
908 return fs::writeFileText(fgdPath, this->bake());
909}
AutoVisGroupWriter(FGDWriter &parent_)
Definition FGD.cpp:631
FGDWriter & endAutoVisGroup() const
Definition FGD.cpp:729
AutoVisGroupWriter & visGroup(std::string_view name, std::initializer_list< std::string_view > entities)
Definition FGD.cpp:710
KeyValueChoicesWriter & choice(std::string_view value, std::string_view displayName)
Definition FGD.cpp:812
KeyValueFlagsWriter & flag(uint64_t value, std::string_view displayName, bool enabledByDefault, std::string_view description="")
Definition FGD.cpp:843
FGDWriter & endEntity() const
Definition FGD.cpp:893
KeyValueChoicesWriter beginKeyValueChoices(std::string_view name, std::string_view displayName="", std::string_view valueDefault="", std::string_view description="", bool readOnly=false, bool report=false)
Definition FGD.cpp:796
EntityWriter & output(std::string_view name, std::string_view valueType, std::string_view description="")
Definition FGD.cpp:875
KeyValueFlagsWriter beginKeyValueFlags(std::string_view name, std::string_view displayName="", std::string_view description="", bool readOnly=false, bool report=false)
Definition FGD.cpp:827
EntityWriter & keyValue(std::string_view name, std::string_view valueType, std::string_view displayName="", std::string_view valueDefault="", std::string_view description="", bool readOnly=false, bool report=false)
Definition FGD.cpp:778
EntityWriter(FGDWriter &parent_)
Definition FGD.cpp:634
EntityWriter & input(std::string_view name, std::string_view valueType, std::string_view description="")
Definition FGD.cpp:857
std::string backingData
Definition FGD.h:217
BufferStream writer
Definition FGD.h:218
static FGDWriter begin()
Definition FGD.cpp:646
EntityWriter beginEntity(std::string_view classType, std::initializer_list< std::string_view > classProperties, std::string_view name, std::string_view description="", std::string_view docsURL="")
Definition FGD.cpp:734
FGDWriter & beginMainKeyValues()
Definition FGD.cpp:683
AutoVisGroupWriter beginAutoVisGroup(std::string_view parentName)
Definition FGD.cpp:702
FGDWriter & version(int version)
Definition FGD.cpp:658
FGDWriter & mapSize(sourcepp::math::Vec2i mapSize)
Definition FGD.cpp:663
FGDWriter & endMainKeyValues()
Definition FGD.cpp:697
FGDWriter & mainKeyValue(std::string_view key, std::string_view value)
Definition FGD.cpp:688
FGDWriter & include(const std::filesystem::path &fgdPath)
Definition FGD.cpp:650
std::string bake() const
Definition FGD.cpp:898
FGDWriter & materialExclusionDirs(std::initializer_list< std::string_view > dirs)
Definition FGD.cpp:668
void readEntities(BufferStreamReadOnly &stream, const std::filesystem::path &path, std::vector< std::filesystem::path > &seenPaths)
Definition FGD.cpp:571
void load(const std::filesystem::path &fgdPath)
Can be called multiple times in succession to load multiple FGD files.
Definition FGD.cpp:533
sourcepp::math::Vec2i getMapSize() const
Definition FGD.cpp:550
FGD()=default
const std::unordered_map< std::string_view, std::string_view > & getMainKeyValues() const
Definition FGD.cpp:562
const std::vector< AutoVisGroup > & getAutoVisGroups() const
Definition FGD.cpp:566
const std::unordered_map< std::string_view, Entity > & getEntities() const
Definition FGD.cpp:554
int getVersion() const
Definition FGD.cpp:546
const std::vector< std::string_view > & getMaterialExclusionDirs() const
Definition FGD.cpp:558
std::list< std::string > backingData
Definition FGD.h:116
std::string readFileText(const std::filesystem::path &filepath, std::size_t startOffset=0)
Definition FS.cpp:22
bool writeFileText(const std::filesystem::path &filepath, std::string_view text)
Definition FS.cpp:46
std::unordered_map< char, char > EscapeSequenceMap
Definition Text.h:17
void eatWhitespaceAndSingleLineComments(BufferStream &stream, std::string_view singleLineCommentStart=DEFAULT_SINGLE_LINE_COMMENT_START)
Eat all whitespace and single line comments after the current stream position.
Definition Text.cpp:108
void eatWhitespace(BufferStream &stream)
Eat all whitespace after the current stream position.
Definition Text.cpp:93
bool tryToEatChar(BufferStream &stream, char c)
If the given char exists at the current position, skip over it.
Definition Text.cpp:139
std::string_view readUnquotedStringToBuffer(BufferStream &stream, BufferStream &backing, const EscapeSequenceMap &escapeSequences=getDefaultEscapeSequences())
Read a string starting at the current stream position.
Definition Text.cpp:217
std::string_view readStringToBuffer(BufferStream &stream, BufferStream &backing, std::string_view start=DEFAULT_STRING_START, std::string_view end=DEFAULT_STRING_END, const EscapeSequenceMap &escapeSequences=getDefaultEscapeSequences())
Read a string starting at the current stream position.
Definition Text.cpp:180
std::vector< std::string > split(std::string_view s, char delim)
Definition String.cpp:157
std::from_chars_result toInt(std::string_view number, std::integral auto &out, int base=10)
Definition String.h:83
void trim(std::string &s)
Definition String.cpp:91
bool iequals(std::string_view s1, std::string_view s2)
Definition String.cpp:63
Definition CmdSeq.h:9
std::string_view name
Definition FGD.h:20
std::string_view arguments
Definition FGD.h:21
std::vector< ClassProperty > classProperties
Definition FGD.h:72
std::vector< IO > inputs
Definition FGD.h:80
std::vector< FieldFlags > fieldsWithFlags
Definition FGD.h:79
std::vector< FieldChoices > fieldsWithChoices
Definition FGD.h:78
std::vector< IO > outputs
Definition FGD.h:81
std::string_view description
Definition FGD.h:74
std::vector< Field > fields
Definition FGD.h:77
std::string_view classType
Definition FGD.h:71
std::string_view docsURL
Definition FGD.h:75