Skip to content

Instantly share code, notes, and snippets.

@stillwwater
Last active November 6, 2025 05:45
Show Gist options
  • Select an option

  • Save stillwwater/45ce0b02dc7a039a735f192351616a05 to your computer and use it in GitHub Desktop.

Select an option

Save stillwwater/45ce0b02dc7a039a735f192351616a05 to your computer and use it in GitHub Desktop.
Valve FGD Parser
#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);
}
#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