Last active
November 6, 2025 05:45
-
-
Save stillwwater/45ce0b02dc7a039a735f192351616a05 to your computer and use it in GitHub Desktop.
Valve FGD Parser
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include "fgd.h" | |
| enum { | |
| TOK_EMPTY, | |
| TOK_WORD, | |
| TOK_INTEGER, | |
| TOK_FLOAT, | |
| TOK_STRING, | |
| TOK_EOF, | |
| }; | |
| struct token { | |
| int type; | |
| string str; | |
| }; | |
| struct token | |
| fgd_next_token(string *file) | |
| { | |
| char *delim = " \t\r\n()[]?;:,="; | |
| while (file->count) { | |
| string token; | |
| sint64 val_integer; | |
| float val_float; | |
| char *pos = file->data; | |
| switch (*pos) { | |
| case '/': | |
| string_advance(file, 1); | |
| if (*file->data == '/') | |
| string_next_line(file); | |
| break; | |
| case '=': | |
| case ',': | |
| case ':': | |
| case '(': case ')': | |
| case '[': case ']': | |
| string_advance(file, 1); | |
| return (struct token){*pos, 1, pos}; | |
| case '"': | |
| token = string_next_token(file, "\""); | |
| return (struct token){TOK_STRING, token}; | |
| case ' ': case '\t': case '\r': case '\n': | |
| while (file->count != 0 && char_isspace(*file->data)) { | |
| string_advance(file, 1); | |
| } | |
| break; | |
| default: | |
| token = string_next_token(file, delim); | |
| string_advance(file, -1); // next_token eats the delimiter, go back to keep it | |
| if (string_parse_signed(&val_integer, token, 10)) | |
| return (struct token){TOK_INTEGER, token}; | |
| if (string_parse_float(&val_float, token)) | |
| return (struct token){TOK_FLOAT, token}; | |
| return (struct token){TOK_WORD, token}; | |
| } | |
| } | |
| return (struct token){TOK_EOF, 0, file->data}; | |
| } | |
| struct token | |
| fgd_peek_token(string file) | |
| { | |
| // local copy | |
| return fgd_next_token(&file); | |
| } | |
| char * | |
| fgd_token_to_string(int type) | |
| { | |
| switch (type) { | |
| case TOK_EMPTY: return "TOK_EMPTY"; | |
| case TOK_WORD: return "TOK_WORD"; | |
| case TOK_INTEGER: return "TOK_INTEGER"; | |
| case TOK_FLOAT: return "TOK_FLOAT"; | |
| case TOK_STRING: return "TOK_STRING"; | |
| case TOK_EOF: return "TOK_EOF"; | |
| } | |
| return ""; | |
| } | |
| #define fgd_expect(token, ...) fgd_expect_n(token, (int[8]){__VA_ARGS__}) | |
| void | |
| fgd_expect_n(struct token token, int args[8]) | |
| { | |
| unsigned i; | |
| for (i = 0; i < 8 && args[i]; ++i) { | |
| if (args[i] == token.type) | |
| return; | |
| } | |
| int expected = args[0]; | |
| if (expected < 32) | |
| fprintf(stderr, "FGD parser error: expected %s got %s %.*s\n", | |
| fgd_token_to_string(expected), fgd_token_to_string(token.type), | |
| (int)token.str.count, token.str.data); | |
| else | |
| fprintf(stderr, "FGD parser: expected '%c' got %s %.*s\n", | |
| expected, fgd_token_to_string(token.type), | |
| (int)token.str.count, token.str.data); | |
| abort(); | |
| } | |
| // : "description" | |
| string | |
| fgd_parse_property_description(string *file) | |
| { | |
| struct token token = fgd_peek_token(*file); | |
| if (token.type != ':') | |
| return (string){0, ""}; | |
| fgd_next_token(file); | |
| token = fgd_peek_token(*file); | |
| fgd_expect(token, TOK_STRING, ':'); | |
| if (token.type == ':') | |
| return (string){0, ""}; | |
| fgd_next_token(file); | |
| return token.str; | |
| } | |
| // : "value" | |
| string | |
| fgd_parse_property_defvalue(string *file) | |
| { | |
| struct token token = fgd_peek_token(*file); | |
| if (token.type != ':') | |
| return (string){0, ""}; | |
| fgd_next_token(file); | |
| token = fgd_peek_token(*file); | |
| fgd_expect(token, TOK_STRING, TOK_INTEGER, TOK_FLOAT, ':'); | |
| if (token.type == ':') | |
| return (string){0, ""}; | |
| fgd_next_token(file); | |
| return token.str; | |
| } | |
| // : "description" : "default" : "long description" | |
| void | |
| fgd_parse_property_type(struct fgd_property *prop, string *file, enum fgd_type type) | |
| { | |
| prop->type = type; | |
| prop->description = fgd_parse_property_description(file); | |
| prop->defvalue = fgd_parse_property_defvalue(file); | |
| prop->longdescription = fgd_parse_property_description(file); | |
| } | |
| // = [ 0 : "description" : 0 ... ] | |
| void | |
| fgd_parse_flags_property(struct fgd_property *prop, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '='); | |
| token = fgd_next_token(file); | |
| fgd_expect(token, '['); | |
| uint64 flags = 0; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_INTEGER, ']'); | |
| while (token.type != ']') { | |
| uint64 flagbit; | |
| string_parse_unsigned(&flagbit, token.str, 0); | |
| fgd_parse_property_description(file); | |
| string value = fgd_parse_property_defvalue(file); | |
| fgd_parse_property_description(file); | |
| if (value.count == 1 && value.data[0] == '1') | |
| flags |= flagbit; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_INTEGER, ']'); | |
| } | |
| snprintf(prop->defflags, sizeof prop->defflags, "%d", (int)flags); | |
| prop->type = FGD_FLAGS; | |
| } | |
| // : "description" : "default" : "long description" = [ choices ] | |
| void | |
| fgd_parse_choices_property(struct fgd_property *prop, string *file) | |
| { | |
| fgd_parse_property_type(prop, file, FGD_CHOICES); | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '='); | |
| token = fgd_next_token(file); | |
| fgd_expect(token, '['); | |
| do { | |
| // ignore choices list | |
| token = fgd_next_token(file); | |
| } while (token.type != ']'); | |
| } | |
| // key(type) property_params | |
| void | |
| fgd_parse_property(struct fgd_property_array *properties, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, TOK_WORD); | |
| struct fgd_property prop = {0}; | |
| prop.key = token.str; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, '('); | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_WORD); | |
| string type = token.str; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ')'); | |
| if (!string_casecmpl(type, "string")) | |
| fgd_parse_property_type(&prop, file, FGD_STRING); | |
| else if (!string_casecmpl(type, "integer")) | |
| fgd_parse_property_type(&prop, file, FGD_INTEGER); | |
| else if (!string_casecmpl(type, "float")) | |
| fgd_parse_property_type(&prop, file, FGD_FLOAT); | |
| else if (!string_casecmpl(type, "color255")) | |
| fgd_parse_property_type(&prop, file, FGD_COLOR255); | |
| else if (!string_casecmpl(type, "target_source")) | |
| fgd_parse_property_type(&prop, file, FGD_TARGET_SOURCE); | |
| else if (!string_casecmpl(type, "target_destination")) | |
| fgd_parse_property_type(&prop, file, FGD_TARGET_DESTINATION); | |
| else if (!string_casecmpl(type, "flags")) | |
| fgd_parse_flags_property(&prop, file); | |
| else if (!string_casecmpl(type, "choices")) | |
| fgd_parse_choices_property(&prop, file); | |
| append(properties, prop); | |
| } | |
| // [ properties ] | |
| void | |
| fgd_parse_properties(struct fgd_property_array *properties, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '['); | |
| token = fgd_peek_token(*file); | |
| fgd_expect(token, TOK_WORD, ']'); | |
| while (token.type != ']') { | |
| fgd_parse_property(properties, file); | |
| token = fgd_peek_token(*file); | |
| fgd_expect(token, TOK_WORD, ']'); | |
| } | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ']'); | |
| } | |
| // (name) | |
| void | |
| fgd_parse_class_base(struct fgd_class *classinfo, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '('); | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_WORD, ')'); | |
| if (token.type == TOK_WORD) { | |
| classinfo->baseclass = token.str; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ')'); | |
| } | |
| } | |
| // (255 255 255) | |
| void | |
| fgd_parse_class_color(struct fgd_class *classinfo, string *file) | |
| { | |
| uint64 color; | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '('); | |
| for (unsigned i = 0; i < 3; ++i) { | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_INTEGER); | |
| string_parse_unsigned(&color, token.str, 10); | |
| classinfo->color[i] = color; | |
| } | |
| classinfo->color[3] = 255; | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ')'); | |
| } | |
| // (-16 -16 -24, 16 16 32) | |
| void | |
| fgd_parse_class_size(struct fgd_class *classinfo, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| fgd_expect(token, '('); | |
| for (unsigned i = 0; i < 3; ++i) { | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_FLOAT, TOK_INTEGER); | |
| string_parse_float(&classinfo->mins[i], token.str); | |
| } | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ','); | |
| for (unsigned i = 0; i < 3; ++i) { | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_FLOAT, TOK_INTEGER); | |
| string_parse_float(&classinfo->maxs[i], token.str); | |
| } | |
| token = fgd_next_token(file); | |
| fgd_expect(token, ')'); | |
| } | |
| // @type decorator(params) : "description" = classname [ properties ] | |
| void | |
| fgd_parse_class(struct table *classes, string *file) | |
| { | |
| struct token token = fgd_next_token(file); | |
| struct fgd_class classinfo = {0}; | |
| string classname; | |
| if (!string_casecmpl(token.str, "@SolidClass")) | |
| classinfo.type = FGD_SOLIDCLASS; | |
| else if (!string_casecmpl(token.str, "@PointClass")) | |
| classinfo.type = FGD_POINTCLASS; | |
| else if (!string_casecmpl(token.str, "@BaseClass")) | |
| classinfo.type = FGD_BASECLASS; | |
| if (classinfo.type == FGD_UNKNOWN) { | |
| fprintf(stderr, "Failed to parse class info"); | |
| abort(); | |
| } | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_WORD, '='); | |
| while (token.type != '=') { | |
| // parse color, model, etc | |
| if (!string_casecmpl(token.str, "base")) | |
| fgd_parse_class_base(&classinfo, file); | |
| else if (!string_casecmpl(token.str, "color")) | |
| fgd_parse_class_color(&classinfo, file); | |
| else if (!string_casecmpl(token.str, "size")) | |
| fgd_parse_class_size(&classinfo, file); | |
| token = fgd_next_token(file); | |
| } | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_WORD); | |
| classname = token.str; | |
| if (fgd_peek_token(*file).type == ':') { | |
| fgd_next_token(file); | |
| token = fgd_next_token(file); | |
| fgd_expect(token, TOK_STRING); | |
| classinfo.description = token.str; | |
| } | |
| fgd_parse_properties(&classinfo.properties, file); | |
| table_store_shared(classes, classname, classinfo); | |
| } | |
| // NOTE: Memory references by `file` must be valid while the fgd is open. It is | |
| // only safe to free after calling fgd_close. | |
| struct table | |
| fgd_open(string file) | |
| { | |
| struct table classes = {0}; | |
| while (fgd_peek_token(file).type != TOK_EOF) { | |
| fgd_parse_class(&classes, &file); | |
| } | |
| return classes; | |
| } | |
| void | |
| fgd_close(struct table *classes) | |
| { | |
| if (!classes || !classes->entries) | |
| return; | |
| struct table_entry *entry = NULL; | |
| while ((entry = table_next(classes, entry))) { | |
| struct fgd_class *classinfo = (void *)entry->value; | |
| sys_free(classinfo->properties.data); | |
| } | |
| free_table(classes); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #ifndef FGD_H | |
| #define FGD_H | |
| #include "module.h" | |
| enum fgd_type { | |
| FGD_UNKNOWN, | |
| FGD_INTEGER, | |
| FGD_FLOAT, | |
| FGD_STRING, | |
| FGD_COLOR255, | |
| FGD_FLAGS, | |
| FGD_CHOICES, | |
| FGD_TARGET_SOURCE, | |
| FGD_TARGET_DESTINATION, | |
| FGD_SOLIDCLASS, | |
| FGD_POINTCLASS, | |
| FGD_BASECLASS, | |
| }; | |
| struct fgd_property { | |
| enum fgd_type type; | |
| string key; | |
| string description; | |
| string defvalue; | |
| string longdescription; | |
| char defflags[12]; | |
| }; | |
| array_type(fgd_property_array, struct fgd_property); | |
| struct fgd_class { | |
| enum fgd_type type; | |
| string baseclass; // TODO: multiple | |
| string description; | |
| uint8 color[4]; | |
| float mins[3]; | |
| float maxs[3]; | |
| struct fgd_property_array properties; | |
| }; | |
| struct table fgd_open(string file); | |
| void fgd_set_entity_defaults(struct entity *e, char *classname); | |
| void fgd_close(struct table *classes); | |
| #endif // FGD_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment