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 readAutoVisGroups(BufferStreamReadOnly& stream, BufferStream& backing, std::vector<FGD::AutoVisGroup>& autoVisGroups) {
133 if (!::tryToEatSeparator(stream, '=')) {
134 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
135 }
136 const auto parentName = ::readFGDString(stream, backing);
137 if (!::tryToEatSeparator(stream, '[')) {
138 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
139 }
140 while (!::tryToEatSeparator(stream, ']')) {
141 auto& autoVisGroup = autoVisGroups.emplace_back();
142 autoVisGroup.parentName = parentName;
143 autoVisGroup.name = ::readFGDString(stream, backing);
144 if (!::tryToEatSeparator(stream, '[')) {
145 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
146 }
147 while (!::tryToEatSeparator(stream, ']')) {
148 autoVisGroup.entities.push_back(::readFGDString(stream, backing));
149 }
150 stream.skip();
151 }
152 stream.skip();
153}
154
155void readClassProperties(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity) {
157
158 while (stream.peek<char>() != '=') {
159 FGD::Entity::ClassProperty classProperty;
161 classProperty.arguments = "";
162
163 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
165 if (stream.peek<char>() != '(') {
166 entity.classProperties.push_back(classProperty);
167 continue;
168 }
169 }
170 classProperty.arguments = parser::text::readStringToBuffer(stream, backing, "(", ")", {{'n', '\n'}});
171 entity.classProperties.push_back(classProperty);
172
174 }
175
176 stream.skip();
178}
179
180void readEntityIO(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity, bool input) {
181 auto& io = input ? entity.inputs.emplace_back() : entity.outputs.emplace_back();
183 if (stream.seek(-1, std::ios::cur).peek<char>() != '(') {
185 if (stream.peek<char>() != '(') {
186 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
187 }
188 }
189 io.valueType = parser::text::readStringToBuffer(stream, backing, "(", ")", parser::text::EscapeSequenceMap{});
190
191 if (!::tryToEatSeparator(stream, ':')) {
192 io.description = "";
193 return;
194 }
195 io.description = ::readFGDString(stream, backing);
196
198}
199
200void readEntityFieldModifiers(BufferStreamReadOnly& stream, BufferStream& backing, bool& readonly, bool& reportable) {
201 readonly = false;
202 reportable = false;
203
205 if (stream.peek<char>() == 'r') {
206 if (const auto modifier = parser::text::readUnquotedStringToBuffer(stream, backing); modifier == "readonly") {
207 readonly = true;
208 } else if (modifier == "report") {
209 reportable = true;
210 return;
211 } else {
212 stream.seek(-static_cast<int64_t>(modifier.length()), std::ios::cur);
213 return;
214 }
215 }
217 if (stream.peek<char>() == 'r') {
218 if (const auto modifier = parser::text::readUnquotedStringToBuffer(stream, backing); modifier == "report") {
219 reportable = true;
220 } else {
221 stream.seek(-static_cast<int64_t>(modifier.length()), std::ios::cur);
222 //return;
223 }
224 }
225}
226
227void readEntityKeyValue(BufferStreamReadOnly& stream, BufferStream& backing, FGD::Entity& entity) {
228 // Key and value type (looks like "key(valueType)", or "input key(valueType)" for i/o)
229 const auto name = parser::text::readUnquotedStringToBuffer(stream, backing, "(", parser::text::EscapeSequenceMap{});
231 if (string::iequals(name, "input")) {
232 ::readEntityIO(stream, backing, entity, true);
233 return;
234 }
235 if (string::iequals(name, "output")) {
236 ::readEntityIO(stream, backing, entity, false);
237 return;
238 }
239 const auto valueType = parser::text::readUnquotedStringToBuffer(stream, backing, ")", parser::text::EscapeSequenceMap{});
240 // If there is a space after the value type, we need to get rid of the parenthesis here
242 if (stream.peek<char>() == ')') {
243 stream.skip();
244 }
245
246 if (string::iequals(valueType, "choices")) {
247 auto& field = entity.fieldsWithChoices.emplace_back();
248 field.name = name;
249 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
250
251 if (::tryToEatSeparator(stream, ':')) {
252 field.displayName = ::readFGDString(stream, backing);
254 }
255
256 if (::tryToEatSeparator(stream, ':')) {
257 field.valueDefault = ::readFGDString(stream, backing);
259 }
260
261 if (::tryToEatSeparator(stream, ':')) {
262 field.description = ::readFGDString(stream, backing);
264 }
265
266 if (!::tryToEatSeparator(stream, '=') || !::tryToEatSeparator(stream, '[')) {
267 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
268 }
269 while (stream.peek<char>() != ']') {
270 auto& choice = field.choices.emplace_back();
271 choice.value = ::readFGDString(stream, backing);
272
273 if (::tryToEatSeparator(stream, ':')) {
274 choice.displayName = ::readFGDString(stream, backing);
275 } else {
276 choice.displayName = "";
277 }
278 }
279 stream.skip();
280 } else if (string::iequals(valueType, "flags")) {
281 auto& field = entity.fieldsWithFlags.emplace_back();
282 field.name = name;
283 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
284
285 if (::tryToEatSeparator(stream, ':')) {
286 field.displayName = ::readFGDString(stream, backing);
288 }
289
290 if (::tryToEatSeparator(stream, ':')) {
291 field.description = ::readFGDString(stream, backing);
293 }
294
295 if (!::tryToEatSeparator(stream, '=') || !::tryToEatSeparator(stream, '[')) {
296 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
297 }
299
300 while (stream.peek<char>() != ']') {
301 auto& flag = field.flags.emplace_back();
302 const auto valueString = ::readFGDString(stream, backing);
303 if (string::toInt(valueString, flag.value).ec != std::errc{}) {
304 flag.value = 0;
305 }
306
307 if (!::tryToEatSeparator(stream, ':')) {
308 continue;
309 }
310 flag.displayName = ::readFGDString(stream, backing);
311
312 if (!::tryToEatSeparator(stream, ':')) {
313 continue;
314 }
315 const auto enabledByDefaultString = ::readFGDString(stream, backing);
316 int enabledByDefault = 0;
317 if (string::toInt(enabledByDefaultString, enabledByDefault).ec != std::errc{}) {
318 flag.enabledByDefault = false;
319 } else {
320 flag.enabledByDefault = static_cast<bool>(enabledByDefault);
321 }
322
323 if (!::tryToEatSeparator(stream, ':')) {
324 continue;
325 }
326 flag.description = ::readFGDString(stream, backing);
327 }
328 stream.skip();
329 } else {
330 auto& field = entity.fields.emplace_back();
331 field.name = name;
332 field.valueType = valueType;
333 ::readEntityFieldModifiers(stream, backing, field.readonly, field.reportable);
334 field.displayName = "";
335 field.valueDefault = "";
336 field.description = "";
337
338 if (!::tryToEatSeparator(stream, ':')) {
339 return;
340 }
341 field.displayName = ::readFGDString(stream, backing);
342
343 if (!::tryToEatSeparator(stream, ':')) {
344 return;
345 }
346 field.valueDefault = ::readFGDString(stream, backing);
347
348 if (!::tryToEatSeparator(stream, ':')) {
349 return;
350 }
351 field.description = ::readFGDString(stream, backing);
352
354 }
355}
356
357void overwriteEntity(FGD::Entity& oldEntity, const FGD::Entity& newEntity) {
358 oldEntity.classProperties = newEntity.classProperties;
359 if (!newEntity.description.empty()) {
360 oldEntity.description = newEntity.description;
361 }
362 if (!newEntity.docsURL.empty()) {
363 oldEntity.docsURL = newEntity.docsURL;
364 }
365 for (const auto& field : newEntity.fields) {
366 if (auto it = std::ranges::find_if(oldEntity.fields, [&field](const auto& oldField) {
367 return oldField.name == field.name;
368 }); it != oldEntity.fields.end()) {
369 it->valueType = field.valueType;
370 it->readonly = field.readonly;
371 it->reportable = field.reportable;
372 if (!field.displayName.empty()) {
373 it->displayName = field.displayName;
374 }
375 if (!field.valueDefault.empty()) {
376 it->valueDefault = field.valueDefault;
377 }
378 if (!field.description.empty()) {
379 it->description = field.description;
380 }
381 } else {
382 oldEntity.fields.push_back(field);
383 }
384 }
385 for (const auto& field : newEntity.fieldsWithChoices) {
386 if (auto it = std::ranges::find_if(oldEntity.fieldsWithChoices, [&field](const auto& oldField) {
387 return oldField.name == field.name;
388 }); it != oldEntity.fieldsWithChoices.end()) {
389 it->readonly = field.readonly;
390 it->reportable = field.reportable;
391 if (!field.displayName.empty()) {
392 it->displayName = field.displayName;
393 }
394 if (!field.valueDefault.empty()) {
395 it->valueDefault = field.valueDefault;
396 }
397 if (!field.description.empty()) {
398 it->description = field.description;
399 }
400 it->choices = field.choices;
401 } else {
402 oldEntity.fieldsWithChoices.push_back(field);
403 }
404 }
405 for (const auto& field : newEntity.fieldsWithFlags) {
406 if (auto it = std::ranges::find_if(oldEntity.fieldsWithFlags, [&field](const auto& oldField) {
407 return oldField.name == field.name;
408 }); it != oldEntity.fieldsWithFlags.end()) {
409 it->readonly = field.readonly;
410 it->reportable = field.reportable;
411 it->flags = field.flags;
412 } else {
413 oldEntity.fieldsWithFlags.push_back(field);
414 }
415 }
416 for (const auto& input : newEntity.inputs) {
417 if (auto it = std::ranges::find_if(oldEntity.inputs, [&input](const auto& oldInput) {
418 return oldInput.name == input.name;
419 }); it != oldEntity.inputs.end()) {
420 it->valueType = input.valueType;
421 if (!input.description.empty()) {
422 it->description = input.description;
423 }
424 } else {
425 oldEntity.inputs.push_back(input);
426 }
427 }
428 for (const auto& output : newEntity.outputs) {
429 if (auto it = std::ranges::find_if(oldEntity.outputs, [&output](const auto& oldOutput) {
430 return oldOutput.name == output.name;
431 }); it != oldEntity.outputs.end()) {
432 it->valueType = output.valueType;
433 if (!output.description.empty()) {
434 it->description = output.description;
435 }
436 } else {
437 oldEntity.outputs.push_back(output);
438 }
439 }
440}
441
442void readEntity(BufferStreamReadOnly& stream, BufferStream& backing, std::string_view classType, std::unordered_map<std::string_view, FGD::Entity>& entities, bool extension) {
443 FGD::Entity entity{};
444 entity.classType = classType;
445
446 // There are optionally a list of class properties after the class
447 if (!::tryToEatSeparator(stream, '=')) {
448 ::readClassProperties(stream, backing, entity);
449 }
450
451 // Entity name
453 const auto name = ::readFGDString(stream, backing);
454
455 // If a colon is here, the entity has a description
456 if (::tryToEatSeparator(stream, ':')) {
457 entity.description = ::readFGDString(stream, backing);
458 } else {
459 entity.description = "";
460 }
461
462 // If a colon is here, the entity has a docs URL
463 if (::tryToEatSeparator(stream, ':')) {
464 entity.docsURL = ::readFGDString(stream, backing);
465 } else {
466 entity.docsURL = "";
467 }
468
469 // Parse entity keyvalues
470 if (!::tryToEatSeparator(stream, '[')) {
471 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
472 }
473 while (!::tryToEatSeparator(stream, ']')) {
474 ::readEntityKeyValue(stream, backing, entity);
475 }
476 stream.skip();
477
478 if (extension && entities.contains(name)) {
479 // Overwrite/add parts of the entity description
480 ::overwriteEntity(entities[name], entity);
481 } else {
482 // Replace entity description entirely
483 entities[name] = entity;
484 }
485}
486
487void writeOptionalKeyValueStrings(BufferStream& writer, std::initializer_list<std::string_view> strings) {
488 static constexpr auto writeOptionalString = [](BufferStream& stream, std::string_view str) {
489 if (!str.empty()) {
490 stream
491 .write(" : \""sv, false)
492 .write(str, false)
493 .write('\"');
494 } else {
495 stream.write(" :"sv, false);
496 }
497 };
498 for (auto revString = std::rbegin(strings); revString != std::rend(strings); ++revString) {
499 if (revString->empty()) {
500 continue;
501 }
502 for (auto string = std::begin(strings); string != revString.base(); ++string) {
503 writeOptionalString(writer, std::string{*string});
504 }
505 return;
506 }
507}
508
509} // namespace
510
511FGD::FGD(const std::filesystem::path& fgdPath) {
512 this->load(fgdPath);
513}
514
515void FGD::load(const std::filesystem::path& fgdPath) {
516 auto fgdData = fs::readFileText(fgdPath);
517 if (fgdData.empty()) {
518 return;
519 }
520 BufferStreamReadOnly stream{fgdData};
521
522 try {
523 std::vector seenPaths{fgdPath};
524 this->readEntities(stream, fgdPath, seenPaths);
525 } catch (const std::overflow_error&) {}
526}
527
528int FGD::getVersion() const {
529 return this->version;
530}
531
532math::Vec2i FGD::getMapSize() const {
533 return this->mapSize;
534}
535
536const std::unordered_map<std::string_view, FGD::Entity>& FGD::getEntities() const {
537 return this->entities;
538}
539
540const std::vector<std::string_view>& FGD::getMaterialExclusionDirs() const {
541 return this->materialExclusionDirs;
542}
543
544const std::vector<FGD::AutoVisGroup>& FGD::getAutoVisGroups() const {
545 return this->autoVisGroups;
546}
547
548// NOLINTNEXTLINE(*-no-recursion)
549void FGD::readEntities(BufferStreamReadOnly& stream, const std::filesystem::path& path, std::vector<std::filesystem::path>& seenPaths) {
550 auto& backingString = this->backingData.emplace_back();
551 // Multiply by 2 to ensure buffer will have enough space (very generous)
552 backingString.resize(stream.size() * 2);
553 BufferStream backing{backingString, false};
554
555 while (true) {
557
558 // All entity definitions start with an '@' followed by the class type
559 if (stream.read<char>() != '@') {
560 throw parser::text::syntax_error{INVALID_SYNTAX_MSG};
561 }
562
563 const auto classType = parser::text::readUnquotedStringToBuffer(stream, backing, "(", parser::text::EscapeSequenceMap{});
564 if (string::iequals(classType, "include")) {
566 // Assume the include path is relative to the current file being processed
567 auto fgdPath = std::filesystem::path{path}.parent_path() / parser::text::readStringToBuffer(stream, backing);
568 if (std::ranges::find(seenPaths, fgdPath) != seenPaths.end()) {
569 continue;
570 }
571 seenPaths.push_back(fgdPath);
572
573 auto fgdData = fs::readFileText(fgdPath);
574 if (fgdData.empty()) {
575 continue;
576 }
577
578 BufferStreamReadOnly newStream{fgdData};
579 try {
580 // ReSharper disable once CppDFAInfiniteRecursion
581 this->readEntities(newStream, fgdPath, seenPaths);
582 } catch (const std::overflow_error&) {}
583 } else if (string::iequals(classType, "version")) {
584 ::readVersion(stream, backing, this->version);
585 } else if (string::iequals(classType, "mapsize")) {
586 ::readMapSize(stream, backing, this->mapSize);
587 } else if (string::iequals(classType, "MaterialExclusion")) {
588 ::readMaterialExclusionDirs(stream, backing, this->materialExclusionDirs);
589 } else if (string::iequals(classType, "AutoVisGroup")) {
590 ::readAutoVisGroups(stream, backing, this->autoVisGroups);
591 } else if (string::iequals(classType, "BaseClass") ||
592 string::iequals(classType, "PointClass") ||
593 string::iequals(classType, "NPCClass") ||
594 string::iequals(classType, "SolidClass") ||
595 string::iequals(classType, "KeyFrameClass") ||
596 string::iequals(classType, "MoveClass") ||
597 string::iequals(classType, "FilterClass")) {
598 ::readEntity(stream, backing, classType, this->entities, false);
599 } else if (string::iequals(classType, "ExtendClass")) {
600 ::readEntity(stream, backing, classType, this->entities, true);
601 } else {
602 throw parser::text::syntax_error{INVALID_CLASS_MSG};
603 }
604 }
605}
606
609
611 : parent(parent_) {}
612
615
618
621
623 return {};
624}
625
626FGDWriter& FGDWriter::include(const std::filesystem::path& fgdPath) {
627 this->writer
628 .write("@include \""sv, false)
629 .write(fgdPath.string(), false)
630 .write("\"\n\n"sv, false);
631 return *this;
632}
633
635 this->writer.write(std::format("@version({})\n\n", version), false);
636 return *this;
637}
638
640 this->writer.write(std::format("@mapsize({}, {})\n\n", mapSize[0], mapSize[1]), false);
641 return *this;
642}
643
644FGDWriter& FGDWriter::materialExclusionDirs(std::initializer_list<std::string_view> dirs) {
645 return this->materialExclusionDirs(std::span{dirs});
646}
647
648FGDWriter& FGDWriter::materialExclusionDirs(std::span<const std::string_view> dirs) {
649 this->writer.write("@MaterialExclusion\n[\n"sv, false);
650 for (const auto& dir : dirs) {
651 this->writer << '\t' << '\"';
652 this->writer.write(dir, false);
653 this->writer << '\"' << '\n';
654 }
655 this->writer.write("]\n\n"sv, false);
656 return *this;
657}
658
660 this->writer
661 .write("@AutoVisGroup = \""sv, false)
662 .write(parentName, false)
663 .write("\"\n[\n"sv, false);
664 return AutoVisGroupWriter{*this};
665}
666
667FGDWriter::AutoVisGroupWriter &FGDWriter::AutoVisGroupWriter::visGroup(std::string_view name, std::initializer_list<std::string_view> entities) {
668 return this->visGroup(name, std::span{entities});
669}
670
671FGDWriter::AutoVisGroupWriter& FGDWriter::AutoVisGroupWriter::visGroup(std::string_view name, std::span<const std::string_view> entities) {
672 this->parent.writer
673 .write("\t\""sv, false)
674 .write(name, false)
675 .write("\"\n\t[\n"sv, false);
676 for (const auto& entity : entities) {
677 this->parent.writer
678 .write("\t\t\""sv, false)
679 .write(entity, false)
680 .write("\"\n"sv, false);
681 }
682 this->parent.writer.write("\t]\n"sv, false);
683 return *this;
684}
685
687 this->parent.writer.write("]\n\n"sv, false);
688 return this->parent;
689}
690
691FGDWriter::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) {
692 return this->beginEntity(classType, std::span{classProperties}, name, description, docsURL);
693}
694
695FGDWriter::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) {
696 this->writer
697 .write('@')
698 .write(classType, false);
699 if (classProperties.empty()) {
700 this->writer << ' ';
701 } else {
702 this->writer << '\n';
703 for (const auto& classProperty : classProperties) {
704 this->writer
705 .write('\t')
706 .write(classProperty, false)
707 .write('\n');
708 }
709 }
710 this->writer
711 .write("= "sv, false)
712 .write(name, false)
713 .write(" :"sv, false);
714 // Put the description on the same line if it's short
715 if (description.size() < 32) {
716 this->writer
717 .write(" \""sv, false) // NOLINT(*-raw-string-literal)
718 .write(description, false);
719 } else {
720 this->writer
721 .write("\n\t\""sv, false)
722 .write(description, false);
723 }
724 this->writer.write('\"');
725 if (!docsURL.empty()) {
726 this->writer
727 .write(" : \""sv, false) // NOLINT(*-raw-string-literal)
728 .write(docsURL, false)
729 .write('\"');
730 }
731 this->writer.write("\n[\n"sv, false);
732 return EntityWriter{*this};
733}
734
735FGDWriter::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) {
736 this->parent.writer
737 .write('\t')
738 .write(name, false)
739 .write('(')
740 .write(valueType, false)
741 .write(')');
742 if (readOnly) {
743 this->parent.writer.write(" readonly"sv, false);
744 }
745 if (report) {
746 this->parent.writer.write(" report"sv, false);
747 }
748 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, valueDefault, description});
749 this->parent.writer << '\n';
750 return *this;
751}
752
753FGDWriter::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) {
754 this->parent.writer
755 .write('\t')
756 .write(name, false)
757 .write("(choices)"sv, false);
758 if (readOnly) {
759 this->parent.writer.write(" readonly"sv, false);
760 }
761 if (report) {
762 this->parent.writer.write(" report"sv, false);
763 }
764 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, valueDefault, description});
765 this->parent.writer.write(" =\n\t[\n"sv, false);
766 return KeyValueChoicesWriter{*this};
767}
768
770 this->parent.parent.writer
771 .write("\t\t\""sv, false)
772 .write(value, false)
773 .write("\" : \""sv, false)
774 .write(displayName, false)
775 .write("\"\n"sv, false);
776 return *this;
777}
778
780 this->parent.parent.writer.write("\t]\n"sv, false);
781 return this->parent;
782}
783
784FGDWriter::EntityWriter::KeyValueFlagsWriter FGDWriter::EntityWriter::beginKeyValueFlags(std::string_view name, std::string_view displayName, std::string_view description, bool readOnly, bool report) {
785 this->parent.writer
786 .write('\t')
787 .write(name, false)
788 .write("(flags)"sv, false);
789 if (readOnly) {
790 this->parent.writer.write(" readonly"sv, false);
791 }
792 if (report) {
793 this->parent.writer.write(" report"sv, false);
794 }
795 ::writeOptionalKeyValueStrings(this->parent.writer, {displayName, description});
796 this->parent.writer.write(" =\n\t[\n"sv, false);
797 return KeyValueFlagsWriter{*this};
798}
799
800FGDWriter::EntityWriter::KeyValueFlagsWriter& FGDWriter::EntityWriter::KeyValueFlagsWriter::flag(uint64_t value, std::string_view displayName, bool enabledByDefault, std::string_view description) {
801 this->parent.parent.writer.write(std::format("\t\t{} : \"{}\" : {:b}", value, displayName, enabledByDefault), false);
802 if (!description.empty()) {
803 this->parent.parent.writer.write(std::format(" : \"{}\"", description), false);
804 }
805 this->parent.parent.writer.write('\n');
806 return *this;
807}
808
810 this->parent.parent.writer.write("\t]\n"sv, false);
811 return this->parent;
812}
813
814FGDWriter::EntityWriter& FGDWriter::EntityWriter::input(std::string_view name, std::string_view valueType, std::string_view description) {
815 this->parent.writer
816 .write('\t')
817 .write("input "sv, false)
818 .write(name, false)
819 .write('(')
820 .write(valueType, false)
821 .write(')');
822 if (!description.empty()) {
823 this->parent.writer
824 .write(" : \""sv, false)
825 .write(description, false)
826 .write('\"');
827 }
828 this->parent.writer << '\n';
829 return *this;
830}
831
832FGDWriter::EntityWriter& FGDWriter::EntityWriter::output(std::string_view name, std::string_view valueType, std::string_view description) {
833 this->parent.writer
834 .write('\t')
835 .write("output "sv, false)
836 .write(name, false)
837 .write('(')
838 .write(valueType, false)
839 .write(')');
840 if (!description.empty()) {
841 this->parent.writer
842 .write(" : \""sv, false)
843 .write(description, false)
844 .write('\"');
845 }
846 this->parent.writer << '\n';
847 return *this;
848}
849
851 this->parent.writer.write("]\n\n"sv, false);
852 return this->parent;
853}
854
855std::string FGDWriter::bake() const {
856 // ReSharper disable once CppRedundantCastExpression
857 std::string_view out{this->backingData.data(), static_cast<std::string_view::size_type>(this->writer.tell())};
858 while (out.ends_with("\n\n")) {
859 out = out.substr(0, out.size() - 1);
860 }
861 return std::string{out};
862}
863
864bool FGDWriter::bake(const std::filesystem::path& fgdPath) const {
865 return fs::writeFileText(fgdPath, this->bake());
866}
AutoVisGroupWriter(FGDWriter &parent_)
Definition FGD.cpp:607
FGDWriter & endAutoVisGroup() const
Definition FGD.cpp:686
AutoVisGroupWriter & visGroup(std::string_view name, std::initializer_list< std::string_view > entities)
Definition FGD.cpp:667
KeyValueChoicesWriter & choice(std::string_view value, std::string_view displayName)
Definition FGD.cpp:769
KeyValueFlagsWriter & flag(uint64_t value, std::string_view displayName, bool enabledByDefault, std::string_view description="")
Definition FGD.cpp:800
FGDWriter & endEntity() const
Definition FGD.cpp:850
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:753
EntityWriter & output(std::string_view name, std::string_view valueType, std::string_view description="")
Definition FGD.cpp:832
KeyValueFlagsWriter beginKeyValueFlags(std::string_view name, std::string_view displayName="", std::string_view description="", bool readOnly=false, bool report=false)
Definition FGD.cpp:784
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:735
EntityWriter(FGDWriter &parent_)
Definition FGD.cpp:610
EntityWriter & input(std::string_view name, std::string_view valueType, std::string_view description="")
Definition FGD.cpp:814
std::string backingData
Definition FGD.h:208
BufferStream writer
Definition FGD.h:209
static FGDWriter begin()
Definition FGD.cpp:622
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:691
AutoVisGroupWriter beginAutoVisGroup(std::string_view parentName)
Definition FGD.cpp:659
FGDWriter & version(int version)
Definition FGD.cpp:634
FGDWriter & mapSize(sourcepp::math::Vec2i mapSize)
Definition FGD.cpp:639
FGDWriter & include(const std::filesystem::path &fgdPath)
Definition FGD.cpp:626
std::string bake() const
Definition FGD.cpp:855
FGDWriter & materialExclusionDirs(std::initializer_list< std::string_view > dirs)
Definition FGD.cpp:644
void readEntities(BufferStreamReadOnly &stream, const std::filesystem::path &path, std::vector< std::filesystem::path > &seenPaths)
Definition FGD.cpp:549
void load(const std::filesystem::path &fgdPath)
Can be called multiple times in succession to load multiple FGD files.
Definition FGD.cpp:515
sourcepp::math::Vec2i getMapSize() const
Definition FGD.cpp:532
FGD()=default
const std::vector< AutoVisGroup > & getAutoVisGroups() const
Definition FGD.cpp:544
const std::unordered_map< std::string_view, Entity > & getEntities() const
Definition FGD.cpp:536
int getVersion() const
Definition FGD.cpp:528
const std::vector< std::string_view > & getMaterialExclusionDirs() const
Definition FGD.cpp:540
std::list< std::string > backingData
Definition FGD.h:114
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:156
std::from_chars_result toInt(std::string_view number, std::integral auto &out, int base=10)
Definition String.h:81
void trim(std::string &s)
Definition String.cpp:90
bool iequals(std::string_view s1, std::string_view s2)
Definition String.cpp:62
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