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