Simple tower defense tutorial, part 20: Writing a parser 2/2

In the last part we laid the foundation for a parser that we want to use to load and initialize game balancing values from a file.

The plan for this part is to read the values provided in our example file and initializing the data structures with these values.

As a reminder, the format looks like this:

-- this is a comment
Level 0
  initialGold: 500
Wave 
  enemyType: ENEMY_TYPE_SHIELD
  wave: 0
  count: 1
  interval: 2.5
  delay: 1.0
  spawnPosition: 2 6
Wave
  enemyType: ENEMY_TYPE_RUNNER
  wave: 0
  count: 5
  interval: 0.5
  delay: 1.0
  spawnPosition: -2 6

EnemyClassConfig ENEMY_TYPE_MINION
  health: 10.0
  speed: 0.6
  radius: 0.25
  maxAcceleration: 1.0
  explosionDamage: 1.0
  requiredContactTime: 0.5
  explosionRange: 1.0
  explosionPushbackPower: 0.25
  goldValue: 1

TowerTypeConfig TOWER_TYPE_BASE
  maxHealth: 10

TowerTypeConfig TOWER_TYPE_ARCHER
  cost: 6
  cooldown: 0.5
  maxUpgradeCooldown: 0.25
  range: 3.0
  maxUpgradeRange: 5.0
  damage: 3.0
  maxUpgradeDamage: 6.0
  maxHealth: 10
  projectileSpeed: 4.0
  projectileType: PROJECTILE_TYPE_ARROW

The colons, indention and line breaks are purely optional. Our parser will ignore these. The parser is just looking at keywords and values where one keyword could have between 0 and n values.

Parsing and loading the wave data

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 
  5 typedef struct ParserState
  6 {
  7   char *input;
  8   int position;
  9   char nextToken[256];
 10 } ParserState;
 11 
 12 int ParserStateGetLineNumber(ParserState *state)
 13 {
 14   int lineNumber = 1;
 15   for (int i = 0; i < state->position; i++)
 16   {
 17     if (state->input[i] == '\n')
 18     {
 19       lineNumber++;
 20     }
 21   }
 22   return lineNumber;
 23 }
 24 
 25 void ParserStateSkipWhiteSpaces(ParserState *state)
 26 {
 27   char *input = state->input;
 28   int pos = state->position;
 29   int skipped = 1;
 30   while (skipped)
 31   {
 32     skipped = 0;
 33     if (input[pos] == '-' && input[pos + 1] == '-')
 34     {
 35       skipped = 1;
 36       // skip comments
 37       while (input[pos] != 0 && input[pos] != '\n')
 38       {
 39         pos++;
 40       }
 41     }
 42   
 43     // skip white spaces and ignore colons
 44     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 45     {
 46       skipped = 1;
 47       pos++;
 48     }
 49 
 50     // repeat until no more white spaces or comments
 51   }
 52   state->position = pos;
 53 }
 54 
 55 int ParserStateReadNextToken(ParserState *state)
 56 {
 57   ParserStateSkipWhiteSpaces(state);
 58 
 59   int i = 0, pos = state->position;
 60   char *input = state->input;
 61 
 62   // read token
 63   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 64   {
 65     state->nextToken[i] = input[pos];
 66     pos++;
 67     i++;
 68   }
 69   state->position = pos;
 70 
 71   if (i == 0 || i == 256)
 72   {
 73     state->nextToken[0] = 0;
 74     return 0;
 75   }
 76   // terminate the token
 77   state->nextToken[i] = 0;
 78   return 1;
 79 }
 80 
 81 int ParserStateReadNextInt(ParserState *state, int *value)
 82 {
 83   if (!ParserStateReadNextToken(state))
 84   {
 85     return 0;
 86   }
 87   // check if the token is a number
 88   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 89   for (int i = isSigned; state->nextToken[i] != 0; i++)
 90   {
 91     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 92     {
 93       return 0;
 94     }
 95   }
 96   *value = TextToInteger(state->nextToken);
 97   return 1;
 98 }
 99 
100 typedef struct ParsedGameData
101 {
102   const char *parseError;
103   Level levels[32];
104   int lastLevelIndex;
105   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
106   EnemyClassConfig enemyClasses[8];
107 } ParsedGameData;
108 
109 typedef enum TryReadResult
110 {
111   TryReadResult_NoMatch,
112   TryReadResult_Error,
113   TryReadResult_Success
114 } TryReadResult;
115 
116 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
117 {
118   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
119   return TryReadResult_Error;
120 }
121 
122 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
123 {
124   if (!TextIsEqual(state->nextToken, key))
125   {
126     return TryReadResult_NoMatch;
127   }
128 
129   if (!ParserStateReadNextInt(state, value))
130   {
131     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
132   }
133 
134   // range test, if minRange == maxRange, we don't check the range
135   if (minRange != maxRange && (*value < minRange || *value > maxRange))
136   {
137     return ParseGameDataError(gameData, state, TextFormat(
138       "Invalid value range for %s, range is [%d, %d], value is %d", 
139       key, minRange, maxRange, *value));
140   }
141 
142   return TryReadResult_Success;
143 }
144 
145 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 146 { 147 if (!TextIsEqual(state->nextToken, "Wave")) 148 { 149 return TryReadResult_NoMatch; 150 } 151 152 Level *level = &gameData->levels[gameData->lastLevelIndex]; 153 EnemyWave *wave = 0; 154 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 155 { 156 if (level->waves[i].count == 0) 157 { 158 wave = &level->waves[i]; 159 break; 160 } 161 } 162 163 if (wave == 0) 164 { 165 return ParseGameDataError(gameData, state, 166 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 167 } 168 169 while (1) 170 { 171 ParserState prevState = *state; 172 173 if (!ParserStateReadNextToken(state)) 174 { 175 // end of file 176 break; 177 } 178 179 int value; 180 switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1)) 181 { 182 case TryReadResult_Success: 183 wave->wave = (uint8_t) value; 184 continue; 185 case TryReadResult_Error: return TryReadResult_Error; 186 } 187 188 switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000)) 189 { 190 case TryReadResult_Success: 191 wave->count = (uint16_t) value; 192 continue; 193 case TryReadResult_Error: return TryReadResult_Error; 194 } 195 196 // no match, return to previous state and break 197 *state = prevState; 198 break; 199 } 200 201 return TryReadResult_Success; 202 } 203
204 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 205 { 206 int levelId; 207 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 208 if (result != TryReadResult_Success) 209 { 210 return result; 211 } 212 213 gameData->lastLevelIndex = levelId; 214 Level *level = &gameData->levels[levelId]; 215 216 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 217 // to detect if the level was already initialized 218 if (level->initialGold != 0) 219 { 220 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 221 } 222 223 int initialGoldInitialized = 0; 224 225 while (1) 226 { 227 // try to read the next token and if we don't know how to handle it, 228 // we rewind and return 229 ParserState prevState = *state; 230 231 if (!ParserStateReadNextToken(state)) 232 { 233 // end of file 234 break; 235 } 236 237 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 238 { 239 case TryReadResult_Success: 240 if (initialGoldInitialized) 241 { 242 return ParseGameDataError(gameData, state, "initialGold already initialized"); 243 } 244 initialGoldInitialized = 1;
245 continue; 246 case TryReadResult_Error: return TryReadResult_Error; 247 } 248 249 switch (ParseGameDataTryReadWaveSection(gameData, state)) 250 { 251 case TryReadResult_Success: continue;
252 case TryReadResult_Error: return TryReadResult_Error; 253 } 254 255 // no match, return to previous state and break 256 *state = prevState; 257 break; 258 } 259 260 if (!initialGoldInitialized) 261 { 262 return ParseGameDataError(gameData, state, "initialGold not initialized"); 263 } 264 265 printf("Parsed level %d, initialGold=%d\n", levelId, level->initialGold); 266 return TryReadResult_Success; 267 } 268 269 int ParseGameData(ParsedGameData *gameData, ParserState *state) 270 { 271 *gameData = (ParsedGameData){0}; 272 gameData->lastLevelIndex = -1; 273 274 while (ParserStateReadNextToken(state)) 275 { 276 switch (ParseGameDataTryReadLevelSection(gameData, state)) 277 { 278 case TryReadResult_Success: continue; 279 case TryReadResult_Error: return 0; 280 } 281 282 // read other sections later 283 } 284 285 return 1; 286 } 287 288 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 289 290 int RunParseTests() 291 { 292 int passedCount = 0, failedCount = 0; 293 ParserState state; 294 ParsedGameData gameData = {0}; 295 296 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 297 gameData = (ParsedGameData) {0}; 298 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 299 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 300 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 301 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 302 303 state = (ParserState) {"Level 392\n", 0, {0}}; 304 gameData = (ParsedGameData) {0}; 305 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 306 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 307 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 308 309 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 310 gameData = (ParsedGameData) {0}; 311 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 312 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 313 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 314 315 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 316 gameData = (ParsedGameData) {0}; 317 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 318 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 319 320 state = (ParserState) {"Level 3", 0, {0}}; 321 gameData = (ParsedGameData) {0}; 322 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 323 324 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 325 gameData = (ParsedGameData) {0}; 326 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section");
327 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 328 329 state = (ParserState) {"Level 7\n initialGold 100\nWave count 1 wave 2", 0, {0}}; 330 gameData = (ParsedGameData) {0}; 331 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 332 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 333 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 334
335 336 337 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 338 339 return failedCount; 340 } 341 342 int main() 343 { 344 printf("Running parse tests\n"); 345 if (RunParseTests()) 346 { 347 return 1; 348 } 349 printf("\n"); 350 351 char *fileContent = LoadFileText("data/level.txt"); 352 if (fileContent == NULL) 353 { 354 printf("Failed to load file\n"); 355 return 1; 356 } 357 358 ParserState state = {fileContent, 0, {0}}; 359 ParsedGameData gameData = {0}; 360 361 if (!ParseGameData(&gameData, &state)) 362 { 363 printf("Failed to parse game data: %s\n", gameData.parseError); 364 UnloadFileText(fileContent); 365 return 1; 366 } 367 368 UnloadFileText(fileContent); 369 return 0; 370 }
Running parse tests
Parsed level 7, initialGold=100
Parsed level 2, initialGold=200
Parsed level 7, initialGold=100
Parsed level 7, initialGold=100
Passed 18 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully
Parsed level 0, initialGold=500

The wave and count values are now parsed and stored in the wave struct. There are now two more value types to parse: floats and 2d vectors. So let's add dedicated functions to read these values as well:

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 
  5 typedef struct ParserState
  6 {
  7   char *input;
  8   int position;
  9   char nextToken[256];
 10 } ParserState;
 11 
 12 int ParserStateGetLineNumber(ParserState *state)
 13 {
 14   int lineNumber = 1;
 15   for (int i = 0; i < state->position; i++)
 16   {
 17     if (state->input[i] == '\n')
 18     {
 19       lineNumber++;
 20     }
 21   }
 22   return lineNumber;
 23 }
 24 
 25 void ParserStateSkipWhiteSpaces(ParserState *state)
 26 {
 27   char *input = state->input;
 28   int pos = state->position;
 29   int skipped = 1;
 30   while (skipped)
 31   {
 32     skipped = 0;
 33     if (input[pos] == '-' && input[pos + 1] == '-')
 34     {
 35       skipped = 1;
 36       // skip comments
 37       while (input[pos] != 0 && input[pos] != '\n')
 38       {
 39         pos++;
 40       }
 41     }
 42   
 43     // skip white spaces and ignore colons
 44     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 45     {
 46       skipped = 1;
 47       pos++;
 48     }
 49 
 50     // repeat until no more white spaces or comments
 51   }
 52   state->position = pos;
 53 }
 54 
 55 int ParserStateReadNextToken(ParserState *state)
 56 {
 57   ParserStateSkipWhiteSpaces(state);
 58 
 59   int i = 0, pos = state->position;
 60   char *input = state->input;
 61 
 62   // read token
 63   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 64   {
 65     state->nextToken[i] = input[pos];
 66     pos++;
 67     i++;
 68   }
 69   state->position = pos;
 70 
 71   if (i == 0 || i == 256)
 72   {
 73     state->nextToken[0] = 0;
 74     return 0;
 75   }
 76   // terminate the token
 77   state->nextToken[i] = 0;
 78   return 1;
 79 }
 80 
 81 int ParserStateReadNextInt(ParserState *state, int *value)
 82 {
 83   if (!ParserStateReadNextToken(state))
 84   {
 85     return 0;
 86   }
87 // check if the token is a valid integer
88 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+'; 89 for (int i = isSigned; state->nextToken[i] != 0; i++) 90 { 91 if (state->nextToken[i] < '0' || state->nextToken[i] > '9') 92 { 93 return 0; 94 } 95 }
96 *value = TextToInteger(state->nextToken); 97 return 1; 98 } 99 100 int ParserStateReadNextFloat(ParserState *state, float *value) 101 { 102 if (!ParserStateReadNextToken(state)) 103 { 104 return 0; 105 } 106 // check if the token is a valid float number 107 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+'; 108 int hasDot = 0; 109 for (int i = isSigned; state->nextToken[i] != 0; i++) 110 { 111 if (state->nextToken[i] == '.') 112 { 113 if (hasDot) 114 { 115 return 0; 116 } 117 hasDot = 1; 118 } 119 else if (state->nextToken[i] < '0' || state->nextToken[i] > '9') 120 { 121 return 0; 122 } 123 } 124 125 *value = TextToFloat(state->nextToken);
126 return 1; 127 } 128 129 typedef struct ParsedGameData 130 { 131 const char *parseError; 132 Level levels[32]; 133 int lastLevelIndex; 134 TowerTypeConfig towerTypes[TOWER_TYPE_COUNT]; 135 EnemyClassConfig enemyClasses[8]; 136 } ParsedGameData; 137 138 typedef enum TryReadResult 139 { 140 TryReadResult_NoMatch, 141 TryReadResult_Error, 142 TryReadResult_Success 143 } TryReadResult; 144 145 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg) 146 { 147 gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg); 148 return TryReadResult_Error; 149 } 150 151 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange) 152 { 153 if (!TextIsEqual(state->nextToken, key)) 154 { 155 return TryReadResult_NoMatch; 156 } 157 158 if (!ParserStateReadNextInt(state, value)) 159 { 160 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key)); 161 } 162 163 // range test, if minRange == maxRange, we don't check the range 164 if (minRange != maxRange && (*value < minRange || *value > maxRange)) 165 { 166 return ParseGameDataError(gameData, state, TextFormat(
167 "Invalid value range for %s, range is [%d, %d], value is %d", 168 key, minRange, maxRange, *value)); 169 } 170 171 return TryReadResult_Success; 172 } 173 174 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 175 int *x, int minXRange, int maxXRange, int *y, int minYRange, int maxYRange) 176 { 177 if (!TextIsEqual(state->nextToken, key)) 178 { 179 return TryReadResult_NoMatch; 180 } 181 182 if (!ParserStateReadNextInt(state, x)) 183 { 184 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s x int value", key)); 185 } 186 187 if (!ParserStateReadNextInt(state, y)) 188 { 189 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s y int value", key)); 190 } 191 192 // range test, if minRange == maxRange, we don't check the range 193 if (minXRange != maxXRange && (*x < minXRange || *x > maxXRange)) 194 { 195 return ParseGameDataError(gameData, state, TextFormat( 196 "Invalid value x range for %s, range is [%d, %d], value is %d", 197 key, minXRange, maxXRange, *x)); 198 } 199 200 if (minYRange != maxYRange && (*y < minYRange || *y > maxYRange)) 201 { 202 return ParseGameDataError(gameData, state, TextFormat( 203 "Invalid value y range for %s, range is [%d, %d], value is %d", 204 key, minYRange, maxYRange, *y)); 205 } 206 207 return TryReadResult_Success; 208 } 209 210 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange) 211 { 212 if (!TextIsEqual(state->nextToken, key)) 213 { 214 return TryReadResult_NoMatch; 215 } 216 217 if (!ParserStateReadNextFloat(state, value)) 218 { 219 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key)); 220 } 221 222 // range test, if minRange == maxRange, we don't check the range 223 if (minRange != maxRange && (*value < minRange || *value > maxRange)) 224 { 225 return ParseGameDataError(gameData, state, TextFormat( 226 "Invalid value range for %s, range is [%f, %f], value is %f",
227 key, minRange, maxRange, *value)); 228 } 229 230 return TryReadResult_Success; 231 } 232 233 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 234 { 235 if (!TextIsEqual(state->nextToken, "Wave")) 236 { 237 return TryReadResult_NoMatch; 238 } 239 240 Level *level = &gameData->levels[gameData->lastLevelIndex]; 241 EnemyWave *wave = 0; 242 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 243 { 244 if (level->waves[i].count == 0) 245 { 246 wave = &level->waves[i]; 247 break; 248 } 249 } 250 251 if (wave == 0) 252 { 253 return ParseGameDataError(gameData, state, 254 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 255 } 256 257 while (1) 258 { 259 ParserState prevState = *state; 260 261 if (!ParserStateReadNextToken(state)) 262 { 263 // end of file 264 break; 265 } 266 267 int value; 268 switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1)) 269 { 270 case TryReadResult_Success: 271 wave->wave = (uint8_t) value; 272 continue; 273 case TryReadResult_Error: return TryReadResult_Error; 274 } 275 276 switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000)) 277 { 278 case TryReadResult_Success:
279 wave->count = (uint16_t) value; 280 continue; 281 case TryReadResult_Error: return TryReadResult_Error; 282 } 283 284 switch (ParseGameDataTryReadKeyFloat(gameData, state, "delay", &wave->delay, 0.0f, 1000.0f)) 285 { 286 case TryReadResult_Success: continue; 287 case TryReadResult_Error: return TryReadResult_Error; 288 } 289 290 switch (ParseGameDataTryReadKeyFloat(gameData, state, "interval", &wave->interval, 0.0f, 1000.0f)) 291 { 292 case TryReadResult_Success: continue; 293 case TryReadResult_Error: return TryReadResult_Error; 294 } 295 296 int x, y; 297 switch (ParseGameDataTryReadKeyIntVec2(gameData, state, "spawnPosition", 298 &x, -10, 10, &y, -10, 10)) 299 { 300 case TryReadResult_Success: 301 wave->spawnPosition = (Vector2){x, y};
302 continue; 303 case TryReadResult_Error: return TryReadResult_Error; 304 } 305 306 // no match, return to previous state and break 307 *state = prevState; 308 break; 309 } 310 311 return TryReadResult_Success; 312 } 313 314 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 315 { 316 int levelId; 317 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 318 if (result != TryReadResult_Success) 319 { 320 return result; 321 } 322 323 gameData->lastLevelIndex = levelId; 324 Level *level = &gameData->levels[levelId]; 325 326 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 327 // to detect if the level was already initialized 328 if (level->initialGold != 0) 329 { 330 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 331 } 332 333 int initialGoldInitialized = 0; 334 335 while (1) 336 { 337 // try to read the next token and if we don't know how to handle it, 338 // we rewind and return 339 ParserState prevState = *state; 340 341 if (!ParserStateReadNextToken(state)) 342 { 343 // end of file 344 break; 345 } 346 347 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 348 { 349 case TryReadResult_Success: 350 if (initialGoldInitialized) 351 { 352 return ParseGameDataError(gameData, state, "initialGold already initialized"); 353 } 354 initialGoldInitialized = 1; 355 continue; 356 case TryReadResult_Error: return TryReadResult_Error; 357 } 358 359 switch (ParseGameDataTryReadWaveSection(gameData, state)) 360 { 361 case TryReadResult_Success: continue; 362 case TryReadResult_Error: return TryReadResult_Error; 363 } 364 365 // no match, return to previous state and break 366 *state = prevState; 367 break; 368 } 369 370 if (!initialGoldInitialized) 371 { 372 return ParseGameDataError(gameData, state, "initialGold not initialized");
373 } 374
375 return TryReadResult_Success; 376 } 377 378 int ParseGameData(ParsedGameData *gameData, ParserState *state) 379 { 380 *gameData = (ParsedGameData){0}; 381 gameData->lastLevelIndex = -1; 382 383 while (ParserStateReadNextToken(state)) 384 { 385 switch (ParseGameDataTryReadLevelSection(gameData, state)) 386 { 387 case TryReadResult_Success: continue; 388 case TryReadResult_Error: return 0; 389 } 390 391 // read other sections later 392 } 393 394 return 1; 395 } 396 397 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 398 399 int RunParseTests() 400 { 401 int passedCount = 0, failedCount = 0; 402 ParserState state; 403 ParsedGameData gameData = {0}; 404 405 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 406 gameData = (ParsedGameData) {0}; 407 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 408 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 409 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 410 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 411 412 state = (ParserState) {"Level 392\n", 0, {0}}; 413 gameData = (ParsedGameData) {0}; 414 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 415 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 416 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 417 418 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 419 gameData = (ParsedGameData) {0}; 420 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 421 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 422 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 423 424 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 425 gameData = (ParsedGameData) {0}; 426 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 427 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 428 429 state = (ParserState) {"Level 3", 0, {0}}; 430 gameData = (ParsedGameData) {0}; 431 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 432 433 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 434 gameData = (ParsedGameData) {0}; 435 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 436 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1");
437 438 state = (ParserState) { 439 "Level 7\n initialGold 100\n" 440 "Wave\n" 441 "count 1 wave 2\n" 442 "interval 0.5\n" 443 "delay 1.0\n" 444 "spawnPosition 3 4\n" 445 "enemyType: ENEMY_TYPE_SHIELD"
446 , 0, {0}}; 447 gameData = (ParsedGameData) {0}; 448 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 449 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1");
450 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 451 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 452 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 453 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == 3, "Expected spawnPosition.x to be 3");
454 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 455 456 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 457 458 return failedCount; 459 } 460 461 int main() 462 { 463 printf("Running parse tests\n"); 464 if (RunParseTests()) 465 { 466 return 1; 467 } 468 printf("\n"); 469 470 char *fileContent = LoadFileText("data/level.txt"); 471 if (fileContent == NULL) 472 { 473 printf("Failed to load file\n"); 474 return 1; 475 } 476 477 ParserState state = {fileContent, 0, {0}}; 478 ParsedGameData gameData = {0}; 479 480 if (!ParseGameData(&gameData, &state)) 481 { 482 printf("Failed to parse game data: %s\n", gameData.parseError); 483 UnloadFileText(fileContent); 484 return 1; 485 } 486 487 UnloadFileText(fileContent); 488 return 0; 489 }
Running parse tests
Passed 22 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

The interval, delay and spawnPosition values are now parsed and stored in the wave struct. The tests cover currently only the "happy path" and we should add tests for the error cases as well. A good test covers all possible code paths and not only the successful ones!

One thing left to parse is the enemyType value. This is an enum value and we need to map the string to the corresponding enum value. Time for another reader function:

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 
  5 typedef struct ParserState
  6 {
  7   char *input;
  8   int position;
  9   char nextToken[256];
 10 } ParserState;
 11 
 12 int ParserStateGetLineNumber(ParserState *state)
 13 {
 14   int lineNumber = 1;
 15   for (int i = 0; i < state->position; i++)
 16   {
 17     if (state->input[i] == '\n')
 18     {
 19       lineNumber++;
 20     }
 21   }
 22   return lineNumber;
 23 }
 24 
 25 void ParserStateSkipWhiteSpaces(ParserState *state)
 26 {
 27   char *input = state->input;
 28   int pos = state->position;
 29   int skipped = 1;
 30   while (skipped)
 31   {
 32     skipped = 0;
 33     if (input[pos] == '-' && input[pos + 1] == '-')
 34     {
 35       skipped = 1;
 36       // skip comments
 37       while (input[pos] != 0 && input[pos] != '\n')
 38       {
 39         pos++;
 40       }
 41     }
 42   
 43     // skip white spaces and ignore colons
 44     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 45     {
 46       skipped = 1;
 47       pos++;
 48     }
 49 
 50     // repeat until no more white spaces or comments
 51   }
 52   state->position = pos;
 53 }
 54 
 55 int ParserStateReadNextToken(ParserState *state)
 56 {
 57   ParserStateSkipWhiteSpaces(state);
 58 
 59   int i = 0, pos = state->position;
 60   char *input = state->input;
 61 
 62   // read token
 63   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 64   {
 65     state->nextToken[i] = input[pos];
 66     pos++;
 67     i++;
 68   }
 69   state->position = pos;
 70 
 71   if (i == 0 || i == 256)
 72   {
 73     state->nextToken[0] = 0;
 74     return 0;
 75   }
 76   // terminate the token
 77   state->nextToken[i] = 0;
 78   return 1;
 79 }
 80 
 81 int ParserStateReadNextInt(ParserState *state, int *value)
 82 {
 83   if (!ParserStateReadNextToken(state))
 84   {
 85     return 0;
 86   }
 87   // check if the token is a valid integer
 88   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 89   for (int i = isSigned; state->nextToken[i] != 0; i++)
 90   {
 91     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 92     {
 93       return 0;
 94     }
 95   }
 96   *value = TextToInteger(state->nextToken);
 97   return 1;
 98 }
 99 
100 int ParserStateReadNextFloat(ParserState *state, float *value)
101 {
102   if (!ParserStateReadNextToken(state))
103   {
104     return 0;
105   }
106   // check if the token is a valid float number
107   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
108   int hasDot = 0;
109   for (int i = isSigned; state->nextToken[i] != 0; i++)
110   {
111     if (state->nextToken[i] == '.')
112     {
113       if (hasDot)
114       {
115         return 0;
116       }
117       hasDot = 1;
118     }
119     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
120     {
121       return 0;
122     }
123   }
124 
125   *value = TextToFloat(state->nextToken);
126   return 1;
127 }
128 
129 typedef struct ParsedGameData
130 {
131   const char *parseError;
132   Level levels[32];
133   int lastLevelIndex;
134   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
135   EnemyClassConfig enemyClasses[8];
136 } ParsedGameData;
137 
138 typedef enum TryReadResult
139 {
140   TryReadResult_NoMatch,
141   TryReadResult_Error,
142   TryReadResult_Success
143 } TryReadResult;
144 
145 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
146 {
147   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
148   return TryReadResult_Error;
149 }
150 
151 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
152 {
153   if (!TextIsEqual(state->nextToken, key))
154   {
155     return TryReadResult_NoMatch;
156   }
157 
158   if (!ParserStateReadNextInt(state, value))
159   {
160     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
161   }
162 
163   // range test, if minRange == maxRange, we don't check the range
164   if (minRange != maxRange && (*value < minRange || *value > maxRange))
165   {
166     return ParseGameDataError(gameData, state, TextFormat(
167       "Invalid value range for %s, range is [%d, %d], value is %d", 
168       key, minRange, maxRange, *value));
169   }
170 
171   return TryReadResult_Success;
172 }
173 
174 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 
175   int *x, int minXRange, int maxXRange, int *y, int minYRange, int maxYRange)
176 {
177   if (!TextIsEqual(state->nextToken, key))
178   {
179     return TryReadResult_NoMatch;
180   }
181 
182   if (!ParserStateReadNextInt(state, x))
183   {
184     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s x int value", key));
185   }
186 
187   if (!ParserStateReadNextInt(state, y))
188   {
189     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s y int value", key));
190   }
191 
192   // range test, if minRange == maxRange, we don't check the range
193   if (minXRange != maxXRange && (*x < minXRange || *x > maxXRange))
194   {
195     return ParseGameDataError(gameData, state, TextFormat(
196       "Invalid value x range for %s, range is [%d, %d], value is %d", 
197       key, minXRange, maxXRange, *x));
198   }
199 
200   if (minYRange != maxYRange && (*y < minYRange || *y > maxYRange))
201   {
202     return ParseGameDataError(gameData, state, TextFormat(
203       "Invalid value y range for %s, range is [%d, %d], value is %d", 
204       key, minYRange, maxYRange, *y));
205   }
206 
207   return TryReadResult_Success;
208 }
209 
210 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
211 {
212   if (!TextIsEqual(state->nextToken, key))
213   {
214     return TryReadResult_NoMatch;
215   }
216 
217   if (!ParserStateReadNextFloat(state, value))
218   {
219     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
220   }
221 
222   // range test, if minRange == maxRange, we don't check the range
223   if (minRange != maxRange && (*value < minRange || *value > maxRange))
224   {
225     return ParseGameDataError(gameData, state, TextFormat(
226       "Invalid value range for %s, range is [%f, %f], value is %f", 
227       key, minRange, maxRange, *value));
228   }
229 
230   return TryReadResult_Success;
231 }
232 
233 // The enumNames is a null-terminated array of strings that represent the enum values 234 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues) 235 { 236 if (!TextIsEqual(state->nextToken, key)) 237 { 238 return TryReadResult_NoMatch; 239 } 240 241 if (!ParserStateReadNextToken(state)) 242 { 243 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key)); 244 } 245 246 for (int i = 0; enumNames[i] != 0; i++) 247 { 248 if (TextIsEqual(state->nextToken, enumNames[i])) 249 { 250 *value = enumValues[i]; 251 return TryReadResult_Success; 252 } 253 } 254 255 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key)); 256 } 257
258 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 259 { 260 if (!TextIsEqual(state->nextToken, "Wave")) 261 { 262 return TryReadResult_NoMatch; 263 } 264 265 Level *level = &gameData->levels[gameData->lastLevelIndex]; 266 EnemyWave *wave = 0; 267 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 268 { 269 if (level->waves[i].count == 0) 270 { 271 wave = &level->waves[i]; 272 break; 273 } 274 } 275 276 if (wave == 0) 277 { 278 return ParseGameDataError(gameData, state, 279 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 280 } 281 282 while (1) 283 { 284 ParserState prevState = *state; 285 286 if (!ParserStateReadNextToken(state)) 287 { 288 // end of file 289 break; 290 } 291 292 int value; 293 switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1)) 294 { 295 case TryReadResult_Success: 296 wave->wave = (uint8_t) value; 297 continue; 298 case TryReadResult_Error: return TryReadResult_Error; 299 } 300 301 switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000)) 302 { 303 case TryReadResult_Success: 304 wave->count = (uint16_t) value; 305 continue; 306 case TryReadResult_Error: return TryReadResult_Error; 307 } 308 309 switch (ParseGameDataTryReadKeyFloat(gameData, state, "delay", &wave->delay, 0.0f, 1000.0f)) 310 { 311 case TryReadResult_Success: continue; 312 case TryReadResult_Error: return TryReadResult_Error; 313 } 314 315 switch (ParseGameDataTryReadKeyFloat(gameData, state, "interval", &wave->interval, 0.0f, 1000.0f)) 316 { 317 case TryReadResult_Success: continue; 318 case TryReadResult_Error: return TryReadResult_Error; 319 } 320 321 int x, y; 322 switch (ParseGameDataTryReadKeyIntVec2(gameData, state, "spawnPosition", 323 &x, -10, 10, &y, -10, 10)) 324 { 325 case TryReadResult_Success:
326 wave->spawnPosition = (Vector2){x, y}; 327 continue; 328 case TryReadResult_Error: return TryReadResult_Error; 329 } 330 331 int enemyType; 332 switch (ParseGameDataTryReadEnum(gameData, state, "enemyType", (int *)&enemyType, 333 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0}, 334 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS})) 335 { 336 case TryReadResult_Success: 337 wave->enemyType = (uint8_t) enemyType;
338 continue; 339 case TryReadResult_Error: return TryReadResult_Error; 340 } 341 342 // no match, return to previous state and break 343 *state = prevState; 344 break; 345 } 346 347 return TryReadResult_Success; 348 } 349 350 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 351 { 352 int levelId; 353 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 354 if (result != TryReadResult_Success) 355 { 356 return result; 357 } 358 359 gameData->lastLevelIndex = levelId; 360 Level *level = &gameData->levels[levelId]; 361 362 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 363 // to detect if the level was already initialized 364 if (level->initialGold != 0) 365 { 366 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 367 } 368 369 int initialGoldInitialized = 0; 370 371 while (1) 372 { 373 // try to read the next token and if we don't know how to handle it, 374 // we rewind and return 375 ParserState prevState = *state; 376 377 if (!ParserStateReadNextToken(state)) 378 { 379 // end of file 380 break; 381 } 382 383 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 384 { 385 case TryReadResult_Success: 386 if (initialGoldInitialized) 387 { 388 return ParseGameDataError(gameData, state, "initialGold already initialized"); 389 } 390 initialGoldInitialized = 1; 391 continue; 392 case TryReadResult_Error: return TryReadResult_Error; 393 } 394 395 switch (ParseGameDataTryReadWaveSection(gameData, state)) 396 { 397 case TryReadResult_Success: continue; 398 case TryReadResult_Error: return TryReadResult_Error; 399 } 400 401 // no match, return to previous state and break 402 *state = prevState; 403 break; 404 } 405 406 if (!initialGoldInitialized) 407 { 408 return ParseGameDataError(gameData, state, "initialGold not initialized"); 409 } 410 411 return TryReadResult_Success; 412 } 413 414 int ParseGameData(ParsedGameData *gameData, ParserState *state) 415 { 416 *gameData = (ParsedGameData){0}; 417 gameData->lastLevelIndex = -1; 418 419 while (ParserStateReadNextToken(state)) 420 { 421 switch (ParseGameDataTryReadLevelSection(gameData, state)) 422 { 423 case TryReadResult_Success: continue; 424 case TryReadResult_Error: return 0; 425 } 426 427 // read other sections later 428 } 429 430 return 1; 431 } 432 433 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 434 435 int RunParseTests() 436 { 437 int passedCount = 0, failedCount = 0; 438 ParserState state; 439 ParsedGameData gameData = {0}; 440 441 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 442 gameData = (ParsedGameData) {0}; 443 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 444 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 445 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 446 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 447 448 state = (ParserState) {"Level 392\n", 0, {0}}; 449 gameData = (ParsedGameData) {0}; 450 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 451 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 452 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 453 454 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 455 gameData = (ParsedGameData) {0}; 456 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 457 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 458 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 459 460 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 461 gameData = (ParsedGameData) {0}; 462 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 463 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 464 465 state = (ParserState) {"Level 3", 0, {0}}; 466 gameData = (ParsedGameData) {0}; 467 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 468 469 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 470 gameData = (ParsedGameData) {0}; 471 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 472 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 473 474 state = (ParserState) { 475 "Level 7\n initialGold 100\n" 476 "Wave\n" 477 "count 1 wave 2\n" 478 "interval 0.5\n" 479 "delay 1.0\n" 480 "spawnPosition 3 4\n" 481 "enemyType: ENEMY_TYPE_SHIELD" 482 , 0, {0}}; 483 gameData = (ParsedGameData) {0}; 484 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 485 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 486 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 487 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 488 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 489 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == 3, "Expected spawnPosition.x to be 3");
490 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 491 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD");
492 493 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 494 495 return failedCount; 496 } 497 498 int main() 499 { 500 printf("Running parse tests\n"); 501 if (RunParseTests()) 502 { 503 return 1; 504 } 505 printf("\n"); 506 507 char *fileContent = LoadFileText("data/level.txt"); 508 if (fileContent == NULL) 509 { 510 printf("Failed to load file\n"); 511 return 1; 512 } 513 514 ParserState state = {fileContent, 0, {0}}; 515 ParsedGameData gameData = {0}; 516 517 if (!ParseGameData(&gameData, &state)) 518 { 519 printf("Failed to parse game data: %s\n", gameData.parseError); 520 UnloadFileText(fileContent); 521 return 1; 522 } 523 524 UnloadFileText(fileContent); 525 return 0; 526 }
Running parse tests
Passed 23 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

Line 332 shows the function call to parse the enemyType value by passing a null-terminated sequence of strings that represent the enum values as well as the enum values themselves. It truly is not a good looking solution - but considering that we only have a few enum values it probably is a pretty pragmatic approach to accept this kind of solution.

What is now missing for the wave config is to check for missing values, catching duplicated values and testing illegal inputs (like floats for integers, negative values for unsigned values, etc).

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
4 #include <string.h>
5 6 typedef struct ParserState 7 { 8 char *input; 9 int position; 10 char nextToken[256]; 11 } ParserState; 12 13 int ParserStateGetLineNumber(ParserState *state) 14 { 15 int lineNumber = 1; 16 for (int i = 0; i < state->position; i++) 17 { 18 if (state->input[i] == '\n') 19 { 20 lineNumber++; 21 } 22 } 23 return lineNumber; 24 } 25 26 void ParserStateSkipWhiteSpaces(ParserState *state) 27 { 28 char *input = state->input; 29 int pos = state->position; 30 int skipped = 1; 31 while (skipped) 32 { 33 skipped = 0; 34 if (input[pos] == '-' && input[pos + 1] == '-') 35 { 36 skipped = 1; 37 // skip comments 38 while (input[pos] != 0 && input[pos] != '\n') 39 { 40 pos++; 41 } 42 } 43 44 // skip white spaces and ignore colons 45 while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':')) 46 { 47 skipped = 1; 48 pos++; 49 } 50 51 // repeat until no more white spaces or comments 52 } 53 state->position = pos; 54 } 55 56 int ParserStateReadNextToken(ParserState *state) 57 { 58 ParserStateSkipWhiteSpaces(state); 59 60 int i = 0, pos = state->position; 61 char *input = state->input; 62 63 // read token 64 while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256) 65 { 66 state->nextToken[i] = input[pos]; 67 pos++; 68 i++; 69 } 70 state->position = pos; 71 72 if (i == 0 || i == 256) 73 { 74 state->nextToken[0] = 0; 75 return 0; 76 } 77 // terminate the token 78 state->nextToken[i] = 0; 79 return 1; 80 } 81 82 int ParserStateReadNextInt(ParserState *state, int *value) 83 { 84 if (!ParserStateReadNextToken(state)) 85 { 86 return 0; 87 } 88 // check if the token is a valid integer 89 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+'; 90 for (int i = isSigned; state->nextToken[i] != 0; i++) 91 { 92 if (state->nextToken[i] < '0' || state->nextToken[i] > '9') 93 { 94 return 0; 95 } 96 } 97 *value = TextToInteger(state->nextToken); 98 return 1; 99 } 100 101 int ParserStateReadNextFloat(ParserState *state, float *value) 102 { 103 if (!ParserStateReadNextToken(state)) 104 { 105 return 0; 106 } 107 // check if the token is a valid float number 108 int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+'; 109 int hasDot = 0; 110 for (int i = isSigned; state->nextToken[i] != 0; i++) 111 { 112 if (state->nextToken[i] == '.') 113 { 114 if (hasDot) 115 { 116 return 0; 117 } 118 hasDot = 1; 119 } 120 else if (state->nextToken[i] < '0' || state->nextToken[i] > '9') 121 { 122 return 0; 123 } 124 } 125 126 *value = TextToFloat(state->nextToken); 127 return 1; 128 } 129 130 typedef struct ParsedGameData 131 { 132 const char *parseError; 133 Level levels[32]; 134 int lastLevelIndex; 135 TowerTypeConfig towerTypes[TOWER_TYPE_COUNT]; 136 EnemyClassConfig enemyClasses[8]; 137 } ParsedGameData; 138 139 typedef enum TryReadResult 140 { 141 TryReadResult_NoMatch, 142 TryReadResult_Error, 143 TryReadResult_Success 144 } TryReadResult; 145 146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg) 147 { 148 gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg); 149 return TryReadResult_Error; 150 } 151 152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange) 153 { 154 if (!TextIsEqual(state->nextToken, key)) 155 { 156 return TryReadResult_NoMatch; 157 } 158 159 if (!ParserStateReadNextInt(state, value)) 160 { 161 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key)); 162 } 163 164 // range test, if minRange == maxRange, we don't check the range 165 if (minRange != maxRange && (*value < minRange || *value > maxRange)) 166 { 167 return ParseGameDataError(gameData, state, TextFormat( 168 "Invalid value range for %s, range is [%d, %d], value is %d", 169 key, minRange, maxRange, *value)); 170 } 171 172 return TryReadResult_Success; 173 } 174 175 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 176 int *x, int minXRange, int maxXRange, int *y, int minYRange, int maxYRange) 177 { 178 if (!TextIsEqual(state->nextToken, key)) 179 { 180 return TryReadResult_NoMatch;
181 } 182 183 ParserState start = *state;
184 185 if (!ParserStateReadNextInt(state, x)) 186 {
187 // use start position to report the error for this KEY 188 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
189 } 190
191 // range test, if minRange == maxRange, we don't check the range 192 if (minXRange != maxXRange && (*x < minXRange || *x > maxXRange))
193 {
194 // use current position to report the error for x value 195 return ParseGameDataError(gameData, state, TextFormat( 196 "Invalid value x range for %s, range is [%d, %d], value is %d", 197 key, minXRange, maxXRange, *x));
198 } 199
200 if (!ParserStateReadNextInt(state, y)) 201 { 202 // use start position to report the error for this KEY
203 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key)); 204 }
205 206 if (minYRange != maxYRange && (*y < minYRange || *y > maxYRange))
207 { 208 // use current position to report the error for y value 209 return ParseGameDataError(gameData, state, TextFormat( 210 "Invalid value y range for %s, range is [%d, %d], value is %d", 211 key, minYRange, maxYRange, *y)); 212 } 213 214 return TryReadResult_Success; 215 } 216 217 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange) 218 { 219 if (!TextIsEqual(state->nextToken, key)) 220 { 221 return TryReadResult_NoMatch; 222 } 223 224 if (!ParserStateReadNextFloat(state, value)) 225 { 226 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key)); 227 } 228 229 // range test, if minRange == maxRange, we don't check the range 230 if (minRange != maxRange && (*value < minRange || *value > maxRange)) 231 { 232 return ParseGameDataError(gameData, state, TextFormat( 233 "Invalid value range for %s, range is [%f, %f], value is %f", 234 key, minRange, maxRange, *value)); 235 } 236 237 return TryReadResult_Success; 238 } 239 240 // The enumNames is a null-terminated array of strings that represent the enum values 241 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues) 242 { 243 if (!TextIsEqual(state->nextToken, key)) 244 { 245 return TryReadResult_NoMatch; 246 } 247 248 if (!ParserStateReadNextToken(state)) 249 { 250 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key)); 251 } 252 253 for (int i = 0; enumNames[i] != 0; i++) 254 { 255 if (TextIsEqual(state->nextToken, enumNames[i])) 256 { 257 *value = enumValues[i]; 258 return TryReadResult_Success; 259 } 260 } 261 262 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key)); 263 } 264 265 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 266 { 267 if (!TextIsEqual(state->nextToken, "Wave")) 268 { 269 return TryReadResult_NoMatch; 270 } 271 272 Level *level = &gameData->levels[gameData->lastLevelIndex]; 273 EnemyWave *wave = 0; 274 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 275 { 276 if (level->waves[i].count == 0) 277 { 278 wave = &level->waves[i]; 279 break; 280 } 281 } 282 283 if (wave == 0) 284 {
285 return ParseGameDataError(gameData, state, 286 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 287 } 288 289 int waveInitialized = 0; 290 int countInitialized = 0; 291 int delayInitialized = 0; 292 int intervalInitialized = 0; 293 int spawnPositionInitialized = 0; 294 int enemyTypeInitialized = 0; 295 296 #define ASSURE_IS_NOT_INITIALIZED(name) if (name##Initialized) { \
297 return ParseGameDataError(gameData, state, #name " already initialized"); } \ 298 name##Initialized = 1 299 300 while (1) 301 { 302 ParserState prevState = *state; 303 304 if (!ParserStateReadNextToken(state)) 305 { 306 // end of file 307 break; 308 } 309 310 int value;
311 switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1)) 312 {
313 case TryReadResult_Success: 314 ASSURE_IS_NOT_INITIALIZED(wave); 315 wave->wave = (uint8_t) value; 316 continue; 317 case TryReadResult_Error: return TryReadResult_Error; 318 } 319
320 switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000)) 321 {
322 case TryReadResult_Success: 323 ASSURE_IS_NOT_INITIALIZED(count); 324 wave->count = (uint16_t) value; 325 continue; 326 case TryReadResult_Error: return TryReadResult_Error; 327 } 328
329 switch (ParseGameDataTryReadKeyFloat(gameData, state, "delay", &wave->delay, 0.0f, 1000.0f)) 330 { 331 case TryReadResult_Success:
332 ASSURE_IS_NOT_INITIALIZED(delay); 333 continue; 334 case TryReadResult_Error: return TryReadResult_Error; 335 } 336
337 switch (ParseGameDataTryReadKeyFloat(gameData, state, "interval", &wave->interval, 0.0f, 1000.0f)) 338 { 339 case TryReadResult_Success:
340 ASSURE_IS_NOT_INITIALIZED(interval); 341 continue; 342 case TryReadResult_Error: return TryReadResult_Error; 343 } 344 345 int x, y; 346 switch (ParseGameDataTryReadKeyIntVec2(gameData, state, "spawnPosition",
347 &x, -10, 10, &y, -10, 10)) 348 {
349 case TryReadResult_Success: 350 ASSURE_IS_NOT_INITIALIZED(spawnPosition); 351 wave->spawnPosition = (Vector2){x, y}; 352 continue; 353 case TryReadResult_Error: return TryReadResult_Error; 354 } 355 356 int enemyType; 357 switch (ParseGameDataTryReadEnum(gameData, state, "enemyType", (int *)&enemyType, 358 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0},
359 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS})) 360 {
361 case TryReadResult_Success: 362 ASSURE_IS_NOT_INITIALIZED(enemyType); 363 wave->enemyType = (uint8_t) enemyType; 364 continue; 365 case TryReadResult_Error: return TryReadResult_Error; 366 } 367 368 // no match, return to previous state and break
369 *state = prevState; 370 break; 371 } 372 #undef ASSURE_IS_NOT_INITIALIZED 373 374 #define ASSURE_IS_INITIALIZED(name) if (!name##Initialized) { \ 375 return ParseGameDataError(gameData, state, #name " not initialized"); } 376 377 ASSURE_IS_INITIALIZED(wave); 378 ASSURE_IS_INITIALIZED(count); 379 ASSURE_IS_INITIALIZED(delay); 380 ASSURE_IS_INITIALIZED(interval); 381 ASSURE_IS_INITIALIZED(spawnPosition); 382 ASSURE_IS_INITIALIZED(enemyType);
383 384 #undef ASSURE_IS_INITIALIZED 385 386 return TryReadResult_Success; 387 } 388 389 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 390 { 391 int levelId; 392 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 393 if (result != TryReadResult_Success) 394 { 395 return result; 396 } 397 398 gameData->lastLevelIndex = levelId; 399 Level *level = &gameData->levels[levelId]; 400 401 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 402 // to detect if the level was already initialized 403 if (level->initialGold != 0) 404 { 405 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 406 } 407 408 int initialGoldInitialized = 0; 409 410 while (1) 411 { 412 // try to read the next token and if we don't know how to handle it, 413 // we rewind and return 414 ParserState prevState = *state; 415 416 if (!ParserStateReadNextToken(state)) 417 { 418 // end of file 419 break; 420 } 421 422 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 423 { 424 case TryReadResult_Success: 425 if (initialGoldInitialized) 426 { 427 return ParseGameDataError(gameData, state, "initialGold already initialized"); 428 } 429 initialGoldInitialized = 1; 430 continue; 431 case TryReadResult_Error: return TryReadResult_Error; 432 } 433 434 switch (ParseGameDataTryReadWaveSection(gameData, state)) 435 { 436 case TryReadResult_Success: continue; 437 case TryReadResult_Error: return TryReadResult_Error; 438 } 439 440 // no match, return to previous state and break 441 *state = prevState; 442 break; 443 } 444 445 if (!initialGoldInitialized) 446 { 447 return ParseGameDataError(gameData, state, "initialGold not initialized"); 448 } 449 450 return TryReadResult_Success; 451 } 452 453 int ParseGameData(ParsedGameData *gameData, ParserState *state) 454 { 455 *gameData = (ParsedGameData){0}; 456 gameData->lastLevelIndex = -1; 457 458 while (ParserStateReadNextToken(state)) 459 { 460 switch (ParseGameDataTryReadLevelSection(gameData, state)) 461 { 462 case TryReadResult_Success: continue; 463 case TryReadResult_Error: return 0; 464 } 465 466 // read other sections later 467 } 468 469 return 1; 470 } 471 472 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 473 474 int RunParseTests() 475 { 476 int passedCount = 0, failedCount = 0; 477 ParserState state; 478 ParsedGameData gameData = {0}; 479 480 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 481 gameData = (ParsedGameData) {0}; 482 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 483 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 484 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 485 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 486 487 state = (ParserState) {"Level 392\n", 0, {0}}; 488 gameData = (ParsedGameData) {0}; 489 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 490 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 491 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 492 493 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 494 gameData = (ParsedGameData) {0}; 495 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 496 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 497 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 498 499 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 500 gameData = (ParsedGameData) {0}; 501 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 502 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 503 504 state = (ParserState) {"Level 3", 0, {0}}; 505 gameData = (ParsedGameData) {0}; 506 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 507 508 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 509 gameData = (ParsedGameData) {0}; 510 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 511 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 512 513 state = (ParserState) { 514 "Level 7\n initialGold 100\n" 515 "Wave\n" 516 "count 1 wave 2\n"
517 "interval 0.5\n"
518 "delay 1.0\n" 519 "spawnPosition -3 4\n" 520 "enemyType: ENEMY_TYPE_SHIELD" 521 , 0, {0}}; 522 gameData = (ParsedGameData) {0}; 523 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 524 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 525 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2");
526 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5");
527 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0");
528 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 529 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 530 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 531 532 // for every entry in the wave section, we want to verify that if that value is 533 // missing, the parser will produce an error. We can do that by commenting out each 534 // line individually in a loop - just replacing the two leading spaces with two dashes 535 const char *testString = 536 "Level 7 initialGold 100\n" 537 "Wave\n" 538 " count 1\n" 539 " wave 2\n" 540 " interval 0.5\n" 541 " delay 1.0\n" 542 " spawnPosition 3 -4\n" 543 " enemyType: ENEMY_TYPE_SHIELD"; 544 for (int i = 0; testString[i]; i++) 545 { 546 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 547 { 548 char copy[1024]; 549 strcpy(copy, testString); 550 // commentify! 551 copy[i + 1] = '-'; 552 copy[i + 2] = '-'; 553 state = (ParserState) {copy, 0, {0}}; 554 gameData = (ParsedGameData) {0}; 555 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 556 } 557 } 558 559 // test wave section missing data / incorrect data 560 561 state = (ParserState) { 562 "Level 7\n initialGold 100\n" 563 "Wave\n" 564 "count 1 wave 2\n" 565 "interval 0.5\n" 566 "delay 1.0\n" 567 "spawnPosition -3\n" // missing y 568 "enemyType: ENEMY_TYPE_SHIELD" 569 , 0, {0}}; 570 gameData = (ParsedGameData) {0}; 571 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 572 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 573 574 state = (ParserState) { 575 "Level 7\n initialGold 100\n" 576 "Wave\n" 577 "count 1.0 wave 2\n" 578 "interval 0.5\n" 579 "delay 1.0\n" 580 "spawnPosition -3\n" // missing y 581 "enemyType: ENEMY_TYPE_SHIELD" 582 , 0, {0}}; 583 gameData = (ParsedGameData) {0}; 584 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section");
585 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 586 587 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 588 589 return failedCount; 590 } 591 592 int main() 593 { 594 printf("Running parse tests\n"); 595 if (RunParseTests()) 596 { 597 return 1; 598 } 599 printf("\n"); 600 601 char *fileContent = LoadFileText("data/level.txt"); 602 if (fileContent == NULL) 603 { 604 printf("Failed to load file\n"); 605 return 1; 606 } 607 608 ParserState state = {fileContent, 0, {0}}; 609 ParsedGameData gameData = {0}; 610 611 if (!ParseGameData(&gameData, &state)) 612 { 613 printf("Failed to parse game data: %s\n", gameData.parseError); 614 UnloadFileText(fileContent); 615 return 1; 616 } 617 618 UnloadFileText(fileContent); 619 return 0; 620 }
Running parse tests
Passed 33 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

Covering various error cases now as well, we can move on to the next part: Parsing the enemy class config. This is pretty much analogous to the level and wave config parsing, so this will look a lot like copy-pasting, but it is pretty difficult to abstract this without writing a lot of code that has not much to do with what we want to do here. It is easier to come up with ideas to keep the code responsible for parsing the different parts and translating the results into the game data structures as short as possible, for example by extracting the common parts into functions.

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 
  6 typedef struct ParserState
  7 {
  8   char *input;
  9   int position;
 10   char nextToken[256];
 11 } ParserState;
 12 
 13 int ParserStateGetLineNumber(ParserState *state)
 14 {
 15   int lineNumber = 1;
 16   for (int i = 0; i < state->position; i++)
 17   {
 18     if (state->input[i] == '\n')
 19     {
 20       lineNumber++;
 21     }
 22   }
 23   return lineNumber;
 24 }
 25 
 26 void ParserStateSkipWhiteSpaces(ParserState *state)
 27 {
 28   char *input = state->input;
 29   int pos = state->position;
 30   int skipped = 1;
 31   while (skipped)
 32   {
 33     skipped = 0;
 34     if (input[pos] == '-' && input[pos + 1] == '-')
 35     {
 36       skipped = 1;
 37       // skip comments
 38       while (input[pos] != 0 && input[pos] != '\n')
 39       {
 40         pos++;
 41       }
 42     }
 43   
 44     // skip white spaces and ignore colons
 45     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 46     {
 47       skipped = 1;
 48       pos++;
 49     }
 50 
 51     // repeat until no more white spaces or comments
 52   }
 53   state->position = pos;
 54 }
 55 
 56 int ParserStateReadNextToken(ParserState *state)
 57 {
 58   ParserStateSkipWhiteSpaces(state);
 59 
 60   int i = 0, pos = state->position;
 61   char *input = state->input;
 62 
 63   // read token
 64   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 65   {
 66     state->nextToken[i] = input[pos];
 67     pos++;
 68     i++;
 69   }
 70   state->position = pos;
 71 
 72   if (i == 0 || i == 256)
 73   {
 74     state->nextToken[0] = 0;
 75     return 0;
 76   }
 77   // terminate the token
 78   state->nextToken[i] = 0;
 79   return 1;
 80 }
 81 
 82 int ParserStateReadNextInt(ParserState *state, int *value)
 83 {
 84   if (!ParserStateReadNextToken(state))
 85   {
 86     return 0;
 87   }
 88   // check if the token is a valid integer
 89   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 90   for (int i = isSigned; state->nextToken[i] != 0; i++)
 91   {
 92     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 93     {
 94       return 0;
 95     }
 96   }
 97   *value = TextToInteger(state->nextToken);
 98   return 1;
 99 }
100 
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103   if (!ParserStateReadNextToken(state))
104   {
105     return 0;
106   }
107   // check if the token is a valid float number
108   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109   int hasDot = 0;
110   for (int i = isSigned; state->nextToken[i] != 0; i++)
111   {
112     if (state->nextToken[i] == '.')
113     {
114       if (hasDot)
115       {
116         return 0;
117       }
118       hasDot = 1;
119     }
120     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121     {
122       return 0;
123     }
124   }
125 
126   *value = TextToFloat(state->nextToken);
127   return 1;
128 }
129 
130 typedef struct ParsedGameData
131 {
132   const char *parseError;
133   Level levels[32];
134   int lastLevelIndex;
135   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
136   EnemyClassConfig enemyClasses[8];
137 } ParsedGameData;
138 
139 typedef enum TryReadResult
140 {
141   TryReadResult_NoMatch,
142   TryReadResult_Error,
143   TryReadResult_Success
144 } TryReadResult;
145 
146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
147 {
148   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
149   return TryReadResult_Error;
150 }
151 
152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
153 {
154   if (!TextIsEqual(state->nextToken, key))
155   {
156     return TryReadResult_NoMatch;
157   }
158 
159   if (!ParserStateReadNextInt(state, value))
160   {
161     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
162   }
163 
164   // range test, if minRange == maxRange, we don't check the range
165   if (minRange != maxRange && (*value < minRange || *value > maxRange))
166   {
167     return ParseGameDataError(gameData, state, TextFormat(
168       "Invalid value range for %s, range is [%d, %d], value is %d", 
169       key, minRange, maxRange, *value));
170   }
171 
172   return TryReadResult_Success;
173 }
174 
175 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 
176   int *x, int minXRange, int maxXRange, int *y, int minYRange, int maxYRange)
177 {
178   if (!TextIsEqual(state->nextToken, key))
179   {
180     return TryReadResult_NoMatch;
181   }
182 
183   ParserState start = *state;
184 
185   if (!ParserStateReadNextInt(state, x))
186   {
187     // use start position to report the error for this KEY
188     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
189   }
190 
191   // range test, if minRange == maxRange, we don't check the range
192   if (minXRange != maxXRange && (*x < minXRange || *x > maxXRange))
193   {
194     // use current position to report the error for x value
195     return ParseGameDataError(gameData, state, TextFormat(
196       "Invalid value x range for %s, range is [%d, %d], value is %d", 
197       key, minXRange, maxXRange, *x));
198   }
199 
200   if (!ParserStateReadNextInt(state, y))
201   {
202     // use start position to report the error for this KEY
203     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
204   }
205 
206   if (minYRange != maxYRange && (*y < minYRange || *y > maxYRange))
207   {
208     // use current position to report the error for y value
209     return ParseGameDataError(gameData, state, TextFormat(
210       "Invalid value y range for %s, range is [%d, %d], value is %d", 
211       key, minYRange, maxYRange, *y));
212   }
213 
214   return TryReadResult_Success;
215 }
216 
217 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
218 {
219   if (!TextIsEqual(state->nextToken, key))
220   {
221     return TryReadResult_NoMatch;
222   }
223 
224   if (!ParserStateReadNextFloat(state, value))
225   {
226     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
227   }
228 
229   // range test, if minRange == maxRange, we don't check the range
230   if (minRange != maxRange && (*value < minRange || *value > maxRange))
231   {
232     return ParseGameDataError(gameData, state, TextFormat(
233       "Invalid value range for %s, range is [%f, %f], value is %f", 
234       key, minRange, maxRange, *value));
235   }
236 
237   return TryReadResult_Success;
238 }
239 
240 // The enumNames is a null-terminated array of strings that represent the enum values
241 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
242 {
243   if (!TextIsEqual(state->nextToken, key))
244   {
245     return TryReadResult_NoMatch;
246   }
247 
248   if (!ParserStateReadNextToken(state))
249   {
250     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
251   }
252 
253   for (int i = 0; enumNames[i] != 0; i++)
254   {
255     if (TextIsEqual(state->nextToken, enumNames[i]))
256     {
257       *value = enumValues[i];
258       return TryReadResult_Success;
259     }
260   }
261 
262   return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
263 }
264 
265 TryReadResult ParseGameDataTryReadEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId) 266 { 267 int enemyClassId; 268 switch (ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId, 269 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0}, 270 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS})) 271 { 272 case TryReadResult_Success: 273 *enemyTypeId = (uint8_t) enemyClassId; 274 return TryReadResult_Success; 275 case TryReadResult_Error: return TryReadResult_Error; 276 } 277 return TryReadResult_NoMatch; 278 } 279
280 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 281 { 282 if (!TextIsEqual(state->nextToken, "Wave")) 283 { 284 return TryReadResult_NoMatch; 285 } 286 287 Level *level = &gameData->levels[gameData->lastLevelIndex]; 288 EnemyWave *wave = 0; 289 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 290 { 291 if (level->waves[i].count == 0) 292 { 293 wave = &level->waves[i]; 294 break; 295 } 296 } 297 298 if (wave == 0) 299 { 300 return ParseGameDataError(gameData, state, 301 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 302 } 303 304 int waveInitialized = 0; 305 int countInitialized = 0; 306 int delayInitialized = 0; 307 int intervalInitialized = 0; 308 int spawnPositionInitialized = 0; 309 int enemyTypeInitialized = 0; 310 311 #define ASSURE_IS_NOT_INITIALIZED(name) if (name##Initialized) { \ 312 return ParseGameDataError(gameData, state, #name " already initialized"); } \ 313 name##Initialized = 1 314 315 while (1) 316 { 317 ParserState prevState = *state; 318 319 if (!ParserStateReadNextToken(state)) 320 { 321 // end of file 322 break; 323 } 324 325 int value; 326 switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1)) 327 { 328 case TryReadResult_Success: 329 ASSURE_IS_NOT_INITIALIZED(wave); 330 wave->wave = (uint8_t) value; 331 continue; 332 case TryReadResult_Error: return TryReadResult_Error; 333 } 334 335 switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000)) 336 { 337 case TryReadResult_Success: 338 ASSURE_IS_NOT_INITIALIZED(count); 339 wave->count = (uint16_t) value; 340 continue; 341 case TryReadResult_Error: return TryReadResult_Error; 342 } 343 344 switch (ParseGameDataTryReadKeyFloat(gameData, state, "delay", &wave->delay, 0.0f, 1000.0f)) 345 { 346 case TryReadResult_Success: 347 ASSURE_IS_NOT_INITIALIZED(delay); 348 continue; 349 case TryReadResult_Error: return TryReadResult_Error; 350 } 351 352 switch (ParseGameDataTryReadKeyFloat(gameData, state, "interval", &wave->interval, 0.0f, 1000.0f)) 353 { 354 case TryReadResult_Success: 355 ASSURE_IS_NOT_INITIALIZED(interval); 356 continue; 357 case TryReadResult_Error: return TryReadResult_Error; 358 } 359 360 int x, y; 361 switch (ParseGameDataTryReadKeyIntVec2(gameData, state, "spawnPosition", 362 &x, -10, 10, &y, -10, 10)) 363 { 364 case TryReadResult_Success: 365 ASSURE_IS_NOT_INITIALIZED(spawnPosition); 366 wave->spawnPosition = (Vector2){x, y}; 367 continue;
368 case TryReadResult_Error: return TryReadResult_Error; 369 } 370 371 switch (ParseGameDataTryReadEnemyTypeId(gameData, state, "enemyType", &wave->enemyType))
372 { 373 case TryReadResult_Success: 374 ASSURE_IS_NOT_INITIALIZED(enemyType); 375 continue; 376 case TryReadResult_Error: return TryReadResult_Error; 377 } 378 379 // no match, return to previous state and break 380 *state = prevState; 381 break; 382 } 383 #undef ASSURE_IS_NOT_INITIALIZED 384 385 #define ASSURE_IS_INITIALIZED(name) if (!name##Initialized) { \ 386 return ParseGameDataError(gameData, state, #name " not initialized"); } 387 388 ASSURE_IS_INITIALIZED(wave); 389 ASSURE_IS_INITIALIZED(count); 390 ASSURE_IS_INITIALIZED(delay);
391 ASSURE_IS_INITIALIZED(interval); 392 ASSURE_IS_INITIALIZED(spawnPosition); 393 ASSURE_IS_INITIALIZED(enemyType); 394 395 #undef ASSURE_IS_INITIALIZED 396 397 return TryReadResult_Success; 398 } 399 400 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state) 401 { 402 uint8_t enemyClassId; 403 404 switch (ParseGameDataTryReadEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId)) 405 { 406 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 407 case TryReadResult_Error: return TryReadResult_Error; 408 } 409 410 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId]; 411 412 413 int waveInitialized = 0; 414 int countInitialized = 0; 415 int delayInitialized = 0; 416 int intervalInitialized = 0; 417 int spawnPositionInitialized = 0; 418 int enemyTypeInitialized = 0; 419 420 int speedInitialized = 0; 421 int healthInitialized = 0; 422 int radiusInitialized = 0; 423 int requiredContactTimeInitialized = 0; 424 int maxAccelerationInitialized = 0; 425 int explosionDamageInitialized = 0; 426 int explosionRangeInitialized = 0; 427 int explosionPushbackPowerInitialized = 0; 428 int goldValueInitialized = 0; 429 430 #define ASSURE_IS_NOT_INITIALIZED(name) if (name##Initialized) { \ 431 return ParseGameDataError(gameData, state, #name " already initialized"); } \ 432 name##Initialized = 1 433 434 while (1) 435 { 436 ParserState prevState = *state; 437 438 if (!ParserStateReadNextToken(state)) 439 { 440 // end of file 441 break; 442 } 443 444 switch (ParseGameDataTryReadKeyFloat(gameData, state, "speed", &enemyClass->speed, 0.1f, 1000.0f)) 445 { 446 case TryReadResult_Success: continue; 447 case TryReadResult_Error: return TryReadResult_Error; 448 } 449 450 switch (ParseGameDataTryReadKeyFloat(gameData, state, "health", &enemyClass->health, 1, 1000000)) 451 { 452 case TryReadResult_Success: continue; 453 case TryReadResult_Error: return TryReadResult_Error; 454 } 455 456 switch (ParseGameDataTryReadKeyFloat(gameData, state, "radius", &enemyClass->radius, 0.0f, 100.0f)) 457 { 458 case TryReadResult_Success: continue; 459 case TryReadResult_Error: return TryReadResult_Error; 460 } 461 462 switch (ParseGameDataTryReadKeyFloat(gameData, state, "requiredContactTime", &enemyClass->requiredContactTime, 0.0f, 1000.0f)) 463 { 464 case TryReadResult_Success: continue; 465 case TryReadResult_Error: return TryReadResult_Error; 466 } 467 468 switch (ParseGameDataTryReadKeyFloat(gameData, state, "maxAcceleration", &enemyClass->maxAcceleration, 0.0f, 1000.0f)) 469 { 470 case TryReadResult_Success: continue; 471 case TryReadResult_Error: return TryReadResult_Error; 472 } 473 474 switch (ParseGameDataTryReadKeyFloat(gameData, state, "explosionDamage", &enemyClass->explosionDamage, 0.0f, 1000.0f)) 475 { 476 case TryReadResult_Success: continue; 477 case TryReadResult_Error: return TryReadResult_Error; 478 } 479 480 switch (ParseGameDataTryReadKeyFloat(gameData, state, "explosionRange", &enemyClass->explosionRange, 0.0f, 1000.0f)) 481 { 482 case TryReadResult_Success: continue; 483 case TryReadResult_Error: return TryReadResult_Error; 484 } 485 486 switch (ParseGameDataTryReadKeyFloat(gameData, state, "explosionPushbackPower", &enemyClass->explosionPushbackPower, 0.0f, 1000.0f)) 487 { 488 case TryReadResult_Success: continue; 489 case TryReadResult_Error: return TryReadResult_Error; 490 } 491 492 switch (ParseGameDataTryReadKeyInt(gameData, state, "goldValue", &enemyClass->goldValue, 0, 1000000)) 493 { 494 case TryReadResult_Success: continue; 495 case TryReadResult_Error: return TryReadResult_Error; 496 } 497
498 // no match, return to previous state and break 499 *state = prevState; 500 break; 501 } 502 503 return TryReadResult_Success; 504 } 505 506 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 507 { 508 int levelId; 509 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 510 if (result != TryReadResult_Success) 511 { 512 return result; 513 } 514 515 gameData->lastLevelIndex = levelId; 516 Level *level = &gameData->levels[levelId]; 517 518 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 519 // to detect if the level was already initialized 520 if (level->initialGold != 0) 521 { 522 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 523 } 524 525 int initialGoldInitialized = 0; 526 527 while (1) 528 { 529 // try to read the next token and if we don't know how to handle it, 530 // we rewind and return 531 ParserState prevState = *state; 532 533 if (!ParserStateReadNextToken(state)) 534 { 535 // end of file 536 break; 537 } 538 539 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 540 { 541 case TryReadResult_Success: 542 if (initialGoldInitialized) 543 { 544 return ParseGameDataError(gameData, state, "initialGold already initialized"); 545 } 546 initialGoldInitialized = 1; 547 continue; 548 case TryReadResult_Error: return TryReadResult_Error; 549 } 550 551 switch (ParseGameDataTryReadWaveSection(gameData, state)) 552 { 553 case TryReadResult_Success: continue; 554 case TryReadResult_Error: return TryReadResult_Error; 555 } 556 557 // no match, return to previous state and break 558 *state = prevState; 559 break; 560 } 561 562 if (!initialGoldInitialized) 563 { 564 return ParseGameDataError(gameData, state, "initialGold not initialized"); 565 } 566 567 return TryReadResult_Success; 568 } 569 570 int ParseGameData(ParsedGameData *gameData, ParserState *state) 571 { 572 *gameData = (ParsedGameData){0}; 573 gameData->lastLevelIndex = -1; 574 575 while (ParserStateReadNextToken(state)) 576 { 577 switch (ParseGameDataTryReadLevelSection(gameData, state))
578 { 579 case TryReadResult_Success: continue; 580 case TryReadResult_Error: return 0; 581 } 582 583 switch (ParseGameDataTryReadEnemyClassSection(gameData, state)) 584 { 585 case TryReadResult_Success: continue; 586 case TryReadResult_Error: return TryReadResult_Error;
587 } 588 // read other sections later 589 590 591 } 592 593 return 1; 594 } 595 596 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 597 598 int RunParseTests() 599 { 600 int passedCount = 0, failedCount = 0; 601 ParserState state; 602 ParsedGameData gameData = {0}; 603 604 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 605 gameData = (ParsedGameData) {0}; 606 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 607 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 608 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 609 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 610 611 state = (ParserState) {"Level 392\n", 0, {0}}; 612 gameData = (ParsedGameData) {0}; 613 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 614 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 615 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 616 617 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 618 gameData = (ParsedGameData) {0}; 619 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 620 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 621 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 622 623 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 624 gameData = (ParsedGameData) {0}; 625 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 626 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 627 628 state = (ParserState) {"Level 3", 0, {0}}; 629 gameData = (ParsedGameData) {0}; 630 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 631 632 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 633 gameData = (ParsedGameData) {0}; 634 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 635 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 636 637 state = (ParserState) { 638 "Level 7\n initialGold 100\n" 639 "Wave\n" 640 "count 1 wave 2\n" 641 "interval 0.5\n" 642 "delay 1.0\n" 643 "spawnPosition -3 4\n" 644 "enemyType: ENEMY_TYPE_SHIELD" 645 , 0, {0}}; 646 gameData = (ParsedGameData) {0}; 647 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 648 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 649 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 650 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 651 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 652 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 653 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 654 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 655 656 // for every entry in the wave section, we want to verify that if that value is 657 // missing, the parser will produce an error. We can do that by commenting out each 658 // line individually in a loop - just replacing the two leading spaces with two dashes 659 const char *testString = 660 "Level 7 initialGold 100\n" 661 "Wave\n" 662 " count 1\n" 663 " wave 2\n" 664 " interval 0.5\n" 665 " delay 1.0\n" 666 " spawnPosition 3 -4\n" 667 " enemyType: ENEMY_TYPE_SHIELD"; 668 for (int i = 0; testString[i]; i++) 669 { 670 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 671 { 672 char copy[1024]; 673 strcpy(copy, testString); 674 // commentify! 675 copy[i + 1] = '-'; 676 copy[i + 2] = '-'; 677 state = (ParserState) {copy, 0, {0}}; 678 gameData = (ParsedGameData) {0}; 679 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 680 } 681 } 682 683 // test wave section missing data / incorrect data 684 685 state = (ParserState) { 686 "Level 7\n initialGold 100\n" 687 "Wave\n" 688 "count 1 wave 2\n" 689 "interval 0.5\n" 690 "delay 1.0\n" 691 "spawnPosition -3\n" // missing y 692 "enemyType: ENEMY_TYPE_SHIELD" 693 , 0, {0}}; 694 gameData = (ParsedGameData) {0}; 695 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 696 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 697 698 state = (ParserState) { 699 "Level 7\n initialGold 100\n" 700 "Wave\n" 701 "count 1.0 wave 2\n" 702 "interval 0.5\n" 703 "delay 1.0\n" 704 "spawnPosition -3\n" // missing y
705 "enemyType: ENEMY_TYPE_SHIELD" 706 , 0, {0}}; 707 gameData = (ParsedGameData) {0}; 708 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 709 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 710 711 // enemy class config parsing tests 712 state = (ParserState) { 713 "EnemyClass ENEMY_TYPE_MINION\n" 714 " health: 10.0\n" 715 " speed: 0.6\n" 716 " radius: 0.25\n" 717 " maxAcceleration: 1.0\n" 718 " explosionDamage: 1.0\n" 719 " requiredContactTime: 0.5\n" 720 " explosionRange: 1.0\n" 721 " explosionPushbackPower: 0.25\n" 722 " goldValue: 1\n" 723 , 0, {0}}; 724 gameData = (ParsedGameData) {0}; 725 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing enemy class section"); 726 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0"); 727 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6"); 728 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25"); 729 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0"); 730 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0");
731 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5"); 732 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0"); 733 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25"); 734 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1"); 735 736 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 737 738 return failedCount; 739 } 740 741 int main() 742 { 743 printf("Running parse tests\n"); 744 if (RunParseTests()) 745 { 746 return 1; 747 } 748 printf("\n"); 749 750 char *fileContent = LoadFileText("data/level.txt"); 751 if (fileContent == NULL) 752 { 753 printf("Failed to load file\n"); 754 return 1; 755 } 756 757 ParserState state = {fileContent, 0, {0}}; 758 ParsedGameData gameData = {0}; 759 760 if (!ParseGameData(&gameData, &state)) 761 { 762 printf("Failed to parse game data: %s\n", gameData.parseError); 763 UnloadFileText(fileContent); 764 return 1; 765 } 766 767 UnloadFileText(fileContent); 768 return 0; 769 }
Running parse tests
Passed 43 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

But maybe... we can use macros here to define how the parsing translates into the game data structures. It will need some creativity - macros are just text replacements, BUT, there are ways how to go a little beyond what you might think is possible. Let's see:

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 
  6 typedef struct ParserState
  7 {
  8   char *input;
  9   int position;
 10   char nextToken[256];
 11 } ParserState;
 12 
 13 int ParserStateGetLineNumber(ParserState *state)
 14 {
 15   int lineNumber = 1;
 16   for (int i = 0; i < state->position; i++)
 17   {
 18     if (state->input[i] == '\n')
 19     {
 20       lineNumber++;
 21     }
 22   }
 23   return lineNumber;
 24 }
 25 
 26 void ParserStateSkipWhiteSpaces(ParserState *state)
 27 {
 28   char *input = state->input;
 29   int pos = state->position;
 30   int skipped = 1;
 31   while (skipped)
 32   {
 33     skipped = 0;
 34     if (input[pos] == '-' && input[pos + 1] == '-')
 35     {
 36       skipped = 1;
 37       // skip comments
 38       while (input[pos] != 0 && input[pos] != '\n')
 39       {
 40         pos++;
 41       }
 42     }
 43   
 44     // skip white spaces and ignore colons
 45     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 46     {
 47       skipped = 1;
 48       pos++;
 49     }
 50 
 51     // repeat until no more white spaces or comments
 52   }
 53   state->position = pos;
 54 }
 55 
 56 int ParserStateReadNextToken(ParserState *state)
 57 {
 58   ParserStateSkipWhiteSpaces(state);
 59 
 60   int i = 0, pos = state->position;
 61   char *input = state->input;
 62 
 63   // read token
 64   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 65   {
 66     state->nextToken[i] = input[pos];
 67     pos++;
 68     i++;
 69   }
 70   state->position = pos;
 71 
 72   if (i == 0 || i == 256)
 73   {
 74     state->nextToken[0] = 0;
 75     return 0;
 76   }
 77   // terminate the token
 78   state->nextToken[i] = 0;
 79   return 1;
 80 }
 81 
 82 int ParserStateReadNextInt(ParserState *state, int *value)
 83 {
 84   if (!ParserStateReadNextToken(state))
 85   {
 86     return 0;
 87   }
 88   // check if the token is a valid integer
 89   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 90   for (int i = isSigned; state->nextToken[i] != 0; i++)
 91   {
 92     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 93     {
 94       return 0;
 95     }
 96   }
 97   *value = TextToInteger(state->nextToken);
 98   return 1;
 99 }
100 
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103   if (!ParserStateReadNextToken(state))
104   {
105     return 0;
106   }
107   // check if the token is a valid float number
108   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109   int hasDot = 0;
110   for (int i = isSigned; state->nextToken[i] != 0; i++)
111   {
112     if (state->nextToken[i] == '.')
113     {
114       if (hasDot)
115       {
116         return 0;
117       }
118       hasDot = 1;
119     }
120     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121     {
122       return 0;
123     }
124   }
125 
126   *value = TextToFloat(state->nextToken);
127   return 1;
128 }
129 
130 typedef struct ParsedGameData
131 {
132   const char *parseError;
133   Level levels[32];
134   int lastLevelIndex;
135   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
136   EnemyClassConfig enemyClasses[8];
137 } ParsedGameData;
138 
139 typedef enum TryReadResult
140 {
141   TryReadResult_NoMatch,
142   TryReadResult_Error,
143   TryReadResult_Success
144 } TryReadResult;
145 
146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
147 {
148   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
149   return TryReadResult_Error;
150 }
151 
152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
153 {
154   if (!TextIsEqual(state->nextToken, key))
155   {
156     return TryReadResult_NoMatch;
157   }
158 
159   if (!ParserStateReadNextInt(state, value))
160   {
161     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
162   }
163 
164   // range test, if minRange == maxRange, we don't check the range
165   if (minRange != maxRange && (*value < minRange || *value > maxRange))
166   {
167     return ParseGameDataError(gameData, state, TextFormat(
168       "Invalid value range for %s, range is [%d, %d], value is %d", 
169       key, minRange, maxRange, *value));
170   }
171 
172   return TryReadResult_Success;
173 }
174 
175 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 
176   int *x, int minXRange, int maxXRange, int *y, int minYRange, int maxYRange)
177 {
178   if (!TextIsEqual(state->nextToken, key))
179   {
180     return TryReadResult_NoMatch;
181   }
182 
183   ParserState start = *state;
184 
185   if (!ParserStateReadNextInt(state, x))
186   {
187     // use start position to report the error for this KEY
188     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
189   }
190 
191   // range test, if minRange == maxRange, we don't check the range
192   if (minXRange != maxXRange && (*x < minXRange || *x > maxXRange))
193   {
194     // use current position to report the error for x value
195     return ParseGameDataError(gameData, state, TextFormat(
196       "Invalid value x range for %s, range is [%d, %d], value is %d", 
197       key, minXRange, maxXRange, *x));
198   }
199 
200   if (!ParserStateReadNextInt(state, y))
201   {
202     // use start position to report the error for this KEY
203     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
204   }
205 
206   if (minYRange != maxYRange && (*y < minYRange || *y > maxYRange))
207   {
208     // use current position to report the error for y value
209     return ParseGameDataError(gameData, state, TextFormat(
210       "Invalid value y range for %s, range is [%d, %d], value is %d", 
211       key, minYRange, maxYRange, *y));
212   }
213 
214   return TryReadResult_Success;
215 }
216 
217 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
218 {
219   if (!TextIsEqual(state->nextToken, key))
220   {
221     return TryReadResult_NoMatch;
222   }
223 
224   if (!ParserStateReadNextFloat(state, value))
225   {
226     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
227   }
228 
229   // range test, if minRange == maxRange, we don't check the range
230   if (minRange != maxRange && (*value < minRange || *value > maxRange))
231   {
232     return ParseGameDataError(gameData, state, TextFormat(
233       "Invalid value range for %s, range is [%f, %f], value is %f", 
234       key, minRange, maxRange, *value));
235   }
236 
237   return TryReadResult_Success;
238 }
239 
240 // The enumNames is a null-terminated array of strings that represent the enum values
241 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
242 {
243   if (!TextIsEqual(state->nextToken, key))
244   {
245     return TryReadResult_NoMatch;
246   }
247 
248   if (!ParserStateReadNextToken(state))
249   {
250     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
251   }
252 
253   for (int i = 0; enumNames[i] != 0; i++)
254   {
255     if (TextIsEqual(state->nextToken, enumNames[i]))
256     {
257       *value = enumValues[i];
258       return TryReadResult_Success;
259     }
260   }
261 
262   return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
263 }
264 
265 TryReadResult ParseGameDataTryReadEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId)
266 {
267   int enemyClassId;
268   switch (ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId, 
269       (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0}, 
270       (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS}))
271   {
272     case TryReadResult_Success: 
273       *enemyTypeId = (uint8_t) enemyClassId;
274       return TryReadResult_Success;
275     case TryReadResult_Error: return TryReadResult_Error;
276   }
277   return TryReadResult_NoMatch;
278 }
279 
280 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state)
281 {
282   if (!TextIsEqual(state->nextToken, "Wave"))
283   {
284     return TryReadResult_NoMatch;
285   }
286 
287   Level *level = &gameData->levels[gameData->lastLevelIndex];
288   EnemyWave *wave = 0;
289   for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++)
290   {
291     if (level->waves[i].count == 0)
292     {
293       wave = &level->waves[i];
294       break;
295     }
296   }
297 
298   if (wave == 0)
299   {
300     return ParseGameDataError(gameData, state, 
301       TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT));
302   }
303 
304   int waveInitialized = 0;
305   int countInitialized = 0;
306   int delayInitialized = 0;
307   int intervalInitialized = 0;
308   int spawnPositionInitialized = 0;
309   int enemyTypeInitialized = 0;
310 
311 #define ASSURE_IS_NOT_INITIALIZED(name) if (name##Initialized) { \
312   return ParseGameDataError(gameData, state, #name " already initialized"); } \
313   name##Initialized = 1
314 
315   while (1)
316   {
317     ParserState prevState = *state;
318     
319     if (!ParserStateReadNextToken(state))
320     {
321       // end of file
322       break;
323     }
324 
325     int value;
326     switch (ParseGameDataTryReadKeyInt(gameData, state, "wave", &value, 0, ENEMY_MAX_WAVE_COUNT - 1))
327     {
328       case TryReadResult_Success:
329         ASSURE_IS_NOT_INITIALIZED(wave);
330         wave->wave = (uint8_t) value;
331         continue;
332       case TryReadResult_Error: return TryReadResult_Error;
333     }
334 
335     switch (ParseGameDataTryReadKeyInt(gameData, state, "count", &value, 1, 1000))
336     {
337       case TryReadResult_Success:
338         ASSURE_IS_NOT_INITIALIZED(count);
339         wave->count = (uint16_t) value;
340         continue;
341       case TryReadResult_Error: return TryReadResult_Error;
342     }
343 
344     switch (ParseGameDataTryReadKeyFloat(gameData, state, "delay", &wave->delay, 0.0f, 1000.0f))
345     {
346       case TryReadResult_Success: 
347         ASSURE_IS_NOT_INITIALIZED(delay);
348         continue;
349       case TryReadResult_Error: return TryReadResult_Error;
350     }
351 
352     switch (ParseGameDataTryReadKeyFloat(gameData, state, "interval", &wave->interval, 0.0f, 1000.0f))
353     {
354       case TryReadResult_Success: 
355         ASSURE_IS_NOT_INITIALIZED(interval);
356         continue;
357       case TryReadResult_Error: return TryReadResult_Error;
358     }
359 
360     int x, y;
361     switch (ParseGameDataTryReadKeyIntVec2(gameData, state, "spawnPosition",
362       &x, -10, 10, &y, -10, 10))
363     {
364       case TryReadResult_Success:
365         ASSURE_IS_NOT_INITIALIZED(spawnPosition);
366         wave->spawnPosition = (Vector2){x, y};
367         continue;
368       case TryReadResult_Error: return TryReadResult_Error;
369     }
370 
371     switch (ParseGameDataTryReadEnemyTypeId(gameData, state, "enemyType", &wave->enemyType))
372     {
373       case TryReadResult_Success: 
374         ASSURE_IS_NOT_INITIALIZED(enemyType);
375         continue;
376       case TryReadResult_Error: return TryReadResult_Error;
377     }
378 
379     // no match, return to previous state and break
380     *state = prevState;
381     break;
382   } 
383 #undef ASSURE_IS_NOT_INITIALIZED
384 
385 #define ASSURE_IS_INITIALIZED(name) if (!name##Initialized) { \
386   return ParseGameDataError(gameData, state, #name " not initialized"); }
387 
388   ASSURE_IS_INITIALIZED(wave);
389   ASSURE_IS_INITIALIZED(count);
390   ASSURE_IS_INITIALIZED(delay);
391   ASSURE_IS_INITIALIZED(interval);
392   ASSURE_IS_INITIALIZED(spawnPosition);
393   ASSURE_IS_INITIALIZED(enemyType);
394 
395 #undef ASSURE_IS_INITIALIZED
396 
397   return TryReadResult_Success;
398 }
399 
400 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state)
401 {
402   uint8_t enemyClassId;
403   
404   switch (ParseGameDataTryReadEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId))
405   {
406     case TryReadResult_NoMatch: return TryReadResult_NoMatch;
407     case TryReadResult_Error: return TryReadResult_Error;
408   }
409 
410   EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId];
411 
412 #define FIELDS \ 413 HANDLE(speed, Float, 0.1f, 1000.0f) \ 414 HANDLE(health, Float, 1, 1000000) \ 415 HANDLE(radius, Float, 0.0f, 10.0f) \ 416 HANDLE(requiredContactTime, Float, 0.0f, 1000.0f) \ 417 HANDLE(maxAcceleration, Float, 0.0f, 1000.0f) \ 418 HANDLE(explosionDamage, Float, 0.0f, 1000.0f) \ 419 HANDLE(explosionRange, Float, 0.0f, 1000.0f) \ 420 HANDLE(explosionPushbackPower, Float, 0.0f, 1000.0f) \ 421 HANDLE(goldValue, Int, 1, 1000000) 422 423 #define HANDLE(name, type, min, max) int name##Initialized = 0;
424 FIELDS 425 #undef HANDLE 426 427 while (1) 428 {
429 ParserState prevState = *state; 430 431 if (!ParserStateReadNextToken(state)) 432 { 433 // end of file 434 break; 435 } 436 437 #define HANDLE(name, type, min, max)\ 438 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &enemyClass->name, min, max))\ 439 {\ 440 case TryReadResult_Success:\ 441 if (name##Initialized) {\ 442 return ParseGameDataError(gameData, state, #name " already initialized");\ 443 }\ 444 name##Initialized = 1;\ 445 continue;\ 446 case TryReadResult_Error: return TryReadResult_Error;\ 447 } 448 449 FIELDS 450 #undef HANDLE 451 452 // no match, return to previous state and break 453 *state = prevState;
454 break; 455 } 456 457 #define HANDLE(name, type, min, max) \ 458 if (!name##Initialized) { \ 459 return ParseGameDataError(gameData, state, #name " not initialized"); \ 460 } 461 462 FIELDS 463 #undef HANDLE 464 465 #undef FIELDS 466 467 return TryReadResult_Success; 468 } 469 470 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 471 { 472 int levelId; 473 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 474 if (result != TryReadResult_Success) 475 { 476 return result; 477 } 478 479 gameData->lastLevelIndex = levelId; 480 Level *level = &gameData->levels[levelId]; 481 482 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 483 // to detect if the level was already initialized 484 if (level->initialGold != 0) 485 { 486 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 487 } 488 489 int initialGoldInitialized = 0; 490 491 while (1) 492 { 493 // try to read the next token and if we don't know how to handle it, 494 // we rewind and return 495 ParserState prevState = *state; 496 497 if (!ParserStateReadNextToken(state)) 498 { 499 // end of file 500 break; 501 } 502 503 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 504 { 505 case TryReadResult_Success: 506 if (initialGoldInitialized) 507 { 508 return ParseGameDataError(gameData, state, "initialGold already initialized"); 509 } 510 initialGoldInitialized = 1; 511 continue; 512 case TryReadResult_Error: return TryReadResult_Error; 513 }
514
515 switch (ParseGameDataTryReadWaveSection(gameData, state)) 516 { 517 case TryReadResult_Success: continue; 518 case TryReadResult_Error: return TryReadResult_Error; 519 } 520 521 // no match, return to previous state and break 522 *state = prevState; 523 break; 524 } 525 526 if (!initialGoldInitialized) 527 { 528 return ParseGameDataError(gameData, state, "initialGold not initialized"); 529 } 530 531 return TryReadResult_Success; 532 } 533 534 int ParseGameData(ParsedGameData *gameData, ParserState *state) 535 { 536 *gameData = (ParsedGameData){0}; 537 gameData->lastLevelIndex = -1; 538 539 while (ParserStateReadNextToken(state)) 540 { 541 switch (ParseGameDataTryReadLevelSection(gameData, state)) 542 { 543 case TryReadResult_Success: continue; 544 case TryReadResult_Error: return 0; 545 } 546 547 switch (ParseGameDataTryReadEnemyClassSection(gameData, state)) 548 { 549 case TryReadResult_Success: continue; 550 case TryReadResult_Error: return 0; 551 } 552 // read other sections later 553 554 555 } 556 557 return 1; 558 } 559 560 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 561 562 int RunParseTests() 563 { 564 int passedCount = 0, failedCount = 0; 565 ParserState state; 566 ParsedGameData gameData = {0}; 567 568 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 569 gameData = (ParsedGameData) {0}; 570 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 571 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 572 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 573 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 574 575 state = (ParserState) {"Level 392\n", 0, {0}}; 576 gameData = (ParsedGameData) {0}; 577 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 578 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 579 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 580 581 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 582 gameData = (ParsedGameData) {0}; 583 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 584 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 585 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 586 587 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 588 gameData = (ParsedGameData) {0}; 589 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 590 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 591 592 state = (ParserState) {"Level 3", 0, {0}}; 593 gameData = (ParsedGameData) {0}; 594 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 595 596 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 597 gameData = (ParsedGameData) {0}; 598 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 599 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 600 601 state = (ParserState) { 602 "Level 7\n initialGold 100\n" 603 "Wave\n" 604 "count 1 wave 2\n" 605 "interval 0.5\n" 606 "delay 1.0\n" 607 "spawnPosition -3 4\n" 608 "enemyType: ENEMY_TYPE_SHIELD" 609 , 0, {0}}; 610 gameData = (ParsedGameData) {0}; 611 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 612 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 613 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 614 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 615 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 616 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 617 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 618 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 619 620 // for every entry in the wave section, we want to verify that if that value is 621 // missing, the parser will produce an error. We can do that by commenting out each 622 // line individually in a loop - just replacing the two leading spaces with two dashes 623 const char *testString = 624 "Level 7 initialGold 100\n" 625 "Wave\n" 626 " count 1\n" 627 " wave 2\n" 628 " interval 0.5\n" 629 " delay 1.0\n" 630 " spawnPosition 3 -4\n" 631 " enemyType: ENEMY_TYPE_SHIELD"; 632 for (int i = 0; testString[i]; i++) 633 { 634 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 635 { 636 char copy[1024]; 637 strcpy(copy, testString); 638 // commentify! 639 copy[i + 1] = '-'; 640 copy[i + 2] = '-'; 641 state = (ParserState) {copy, 0, {0}}; 642 gameData = (ParsedGameData) {0}; 643 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 644 } 645 } 646 647 // test wave section missing data / incorrect data 648 649 state = (ParserState) { 650 "Level 7\n initialGold 100\n" 651 "Wave\n" 652 "count 1 wave 2\n" 653 "interval 0.5\n" 654 "delay 1.0\n" 655 "spawnPosition -3\n" // missing y 656 "enemyType: ENEMY_TYPE_SHIELD" 657 , 0, {0}}; 658 gameData = (ParsedGameData) {0}; 659 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 660 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 661
662 state = (ParserState) { 663 "Level 7\n initialGold 100\n" 664 "Wave\n" 665 "count 1.0 wave 2\n" 666 "interval 0.5\n" 667 "delay 1.0\n" 668 "spawnPosition -3\n" // missing y 669 "enemyType: ENEMY_TYPE_SHIELD" 670 , 0, {0}}; 671 gameData = (ParsedGameData) {0}; 672 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 673 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 674 675 // enemy class config parsing tests 676 state = (ParserState) { 677 "EnemyClass ENEMY_TYPE_MINION\n" 678 " health: 10.0\n" 679 " speed: 0.6\n" 680 " radius: 0.25\n" 681 " maxAcceleration: 1.0\n" 682 " explosionDamage: 1.0\n" 683 " requiredContactTime: 0.5\n" 684 " explosionRange: 1.0\n" 685 " explosionPushbackPower: 0.25\n" 686 " goldValue: 1\n" 687 , 0, {0}}; 688 gameData = (ParsedGameData) {0}; 689 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing enemy class section");
690 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0"); 691 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6"); 692 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25"); 693 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0"); 694 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0"); 695 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5"); 696 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0"); 697 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25"); 698 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1"); 699 700 testString = 701 "EnemyClass ENEMY_TYPE_MINION\n" 702 " health: 10.0\n" 703 " speed: 0.6\n" 704 " radius: 0.25\n" 705 " maxAcceleration: 1.0\n" 706 " explosionDamage: 1.0\n" 707 " requiredContactTime: 0.5\n" 708 " explosionRange: 1.0\n" 709 " explosionPushbackPower: 0.25\n" 710 " goldValue: 1\n"; 711 for (int i = 0; testString[i]; i++) 712 { 713 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 714 { 715 char copy[1024]; 716 strcpy(copy, testString); 717 // commentify! 718 copy[i + 1] = '-'; 719 copy[i + 2] = '-'; 720 state = (ParserState) {copy, 0, {0}}; 721 gameData = (ParsedGameData) {0}; 722 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing EnemyClass section"); 723 } 724 } 725 726 727 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 728 729 return failedCount; 730 } 731 732 int main() 733 { 734 printf("Running parse tests\n"); 735 if (RunParseTests()) 736 { 737 return 1; 738 } 739 printf("\n"); 740 741 char *fileContent = LoadFileText("data/level.txt"); 742 if (fileContent == NULL) 743 { 744 printf("Failed to load file\n"); 745 return 1; 746 } 747 748 ParserState state = {fileContent, 0, {0}}; 749 ParsedGameData gameData = {0}; 750 751 if (!ParseGameData(&gameData, &state)) 752 { 753 printf("Failed to parse game data: %s\n", gameData.parseError); 754 UnloadFileText(fileContent); 755 return 1; 756 } 757 758 UnloadFileText(fileContent); 759 return 0; 760 }
Running parse tests
Passed 52 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

This needs some untangling, I suspect. Foremost, we have now two implementation variants: The one for parsing a wave and the one parsing the enemy class config. The wave parsing uses a few macros to reduce some code duplication, but it is still quite explicit in writing out the parsing steps for each parsed field.

Let's break this down into smaller pieces and explain what is happening here, beginning with the code in line 412:

  1 #define FIELDS \
  2   HANDLE(speed, Float, 0.1f, 1000.0f) \
  3   HANDLE(health, Float, 1, 1000000) \
  4   HANDLE(radius, Float, 0.0f, 10.0f) \
  5   HANDLE(requiredContactTime, Float, 0.0f, 1000.0f) \
  6   HANDLE(maxAcceleration, Float, 0.0f, 1000.0f) \
  7   HANDLE(explosionDamage, Float, 0.0f, 1000.0f) \
  8   HANDLE(explosionRange, Float, 0.0f, 1000.0f) \
  9   HANDLE(explosionPushbackPower, Float, 0.0f, 1000.0f) \
 10   HANDLE(goldValue, Int, 1, 1000000)

In C, when a line ends with a backslash, the next line is considered to be a continuation of the current line. So the FIELDS macro is a multiline macro that creates a list of calls to a macro called HANDLE. At the point of the FIELDS macro, the HANDLE macro is not defined yet. That does not matter, because the preprocessor will replace the HANDLE macro with the definition of the HANDLE macro when it encounters it and as long as the HANDLE macro is defined before the FIELDS macro is expanded, everything nothing will be evaluated at all.

So the FIELDS macro is just a list of calls to the HANDLE macro with different arguments: The type and the range of the value to parse. Since we only deal with single value primitive types, this approach is working here - it is more difficult when we have to deal with more complex data structures like vectors.

The next part looks like this:

  1 #define HANDLE(name, type, min, max) int name##Initialized = 0;
  2   FIELDS
  3 #undef HANDLE

In this section, we define the HANDLE macro to produce a variable that uses the name argument and appends Initialized to it. So when the line reads

HANDLE(speed, _, _, _)

the preprocessor will produce the line

int speedInitialized = 0;

After the declaration of the HANDLE macro, the code expands the FIELDS macro - which is NOW for THIS SCOPE expanding into a list of HANDLE macro calls which again expand into the variable declarations we just defined. In order to reuse the FIELDS macro, we immediately undefine the HANDLE macro again after this expansion.

So with these 3 lines of code, we just have created a list of 9 variables. In the loop of parsing the values, we again declare a HANDLE macro, but this time, it is doing something far more complex:

  1 #define HANDLE(name, type, min, max)\
  2   switch (ParseGameDataTryReadKey##type(gameData, state, #name, &enemyClass->name, min, max))\
  3   {\
  4     case TryReadResult_Success:\
  5       if (name##Initialized) {\
  6         return ParseGameDataError(gameData, state, #name " already initialized");\
  7       }\
  8       name##Initialized = 1;\
  9       continue;\
 10     case TryReadResult_Error: return TryReadResult_Error;\
 11   }
 12 
 13 FIELDS
 14 #undef HANDLE

The HANDLE macro is now a switch statement that calls a function ParseGameDataTryReadKey##type - which means that it is using the Int or Float suffix to call the correct function. Since function signatures are almost the same (watch out for the correct type - *uint32_t is not *int compatible!), we can build on this.

In the case block, we check if the value was already initialized and return an error if it was. Again, this variable name is constructed by appending Initialized to the name of the value. If the value was not initialized, we set the name##Initialized variable to 1 and continue with the next value.

The check at the end to see if all values were initialized is done in a similar fashion that I don't think I have to expand here.

What is interesting here is, that we have a single place where we declare the field names and their types and ranges, which works well for these single value primitive types. While this implementation is certainly more difficult to comprehend than the written out one, it has the advantage that we declare the list of fields only once. The generated code is also always working the same way, so we can expect that there is less of a chance for errors when adding new fields or changing the existing ones, which is a big plus.

The price we pay here however is that this code is extremely difficult to debug. I think this boils down to subjective preferences. I find it important to still explore these possibilities just to see what is possible and to get a feeling for what is good and what is not. So let's refactor the other parts of the parser as well and see how this turns out! Maybe there are some more interesting ways to automate the parsing process.

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 
  6 typedef struct ParserState
  7 {
  8   char *input;
  9   int position;
 10   char nextToken[256];
 11 } ParserState;
 12 
 13 int ParserStateGetLineNumber(ParserState *state)
 14 {
 15   int lineNumber = 1;
 16   for (int i = 0; i < state->position; i++)
 17   {
 18     if (state->input[i] == '\n')
 19     {
 20       lineNumber++;
 21     }
 22   }
 23   return lineNumber;
 24 }
 25 
 26 void ParserStateSkipWhiteSpaces(ParserState *state)
 27 {
 28   char *input = state->input;
 29   int pos = state->position;
 30   int skipped = 1;
 31   while (skipped)
 32   {
 33     skipped = 0;
 34     if (input[pos] == '-' && input[pos + 1] == '-')
 35     {
 36       skipped = 1;
 37       // skip comments
 38       while (input[pos] != 0 && input[pos] != '\n')
 39       {
 40         pos++;
 41       }
 42     }
 43   
 44     // skip white spaces and ignore colons
 45     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 46     {
 47       skipped = 1;
 48       pos++;
 49     }
 50 
 51     // repeat until no more white spaces or comments
 52   }
 53   state->position = pos;
 54 }
 55 
 56 int ParserStateReadNextToken(ParserState *state)
 57 {
 58   ParserStateSkipWhiteSpaces(state);
 59 
 60   int i = 0, pos = state->position;
 61   char *input = state->input;
 62 
 63   // read token
 64   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 65   {
 66     state->nextToken[i] = input[pos];
 67     pos++;
 68     i++;
 69   }
 70   state->position = pos;
 71 
 72   if (i == 0 || i == 256)
 73   {
 74     state->nextToken[0] = 0;
 75     return 0;
 76   }
 77   // terminate the token
 78   state->nextToken[i] = 0;
 79   return 1;
 80 }
 81 
 82 int ParserStateReadNextInt(ParserState *state, int *value)
 83 {
 84   if (!ParserStateReadNextToken(state))
 85   {
 86     return 0;
 87   }
 88   // check if the token is a valid integer
 89   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 90   for (int i = isSigned; state->nextToken[i] != 0; i++)
 91   {
 92     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 93     {
 94       return 0;
 95     }
 96   }
 97   *value = TextToInteger(state->nextToken);
 98   return 1;
 99 }
100 
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103   if (!ParserStateReadNextToken(state))
104   {
105     return 0;
106   }
107   // check if the token is a valid float number
108   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109   int hasDot = 0;
110   for (int i = isSigned; state->nextToken[i] != 0; i++)
111   {
112     if (state->nextToken[i] == '.')
113     {
114       if (hasDot)
115       {
116         return 0;
117       }
118       hasDot = 1;
119     }
120     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121     {
122       return 0;
123     }
124   }
125 
126   *value = TextToFloat(state->nextToken);
127   return 1;
128 }
129 
130 typedef struct ParsedGameData
131 {
132   const char *parseError;
133   Level levels[32];
134   int lastLevelIndex;
135   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
136   EnemyClassConfig enemyClasses[8];
137 } ParsedGameData;
138 
139 typedef enum TryReadResult
140 {
141   TryReadResult_NoMatch,
142   TryReadResult_Error,
143   TryReadResult_Success
144 } TryReadResult;
145 
146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
147 {
148   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
149   return TryReadResult_Error;
150 }
151 
152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
153 {
154   if (!TextIsEqual(state->nextToken, key))
155   {
156     return TryReadResult_NoMatch;
157   }
158 
159   if (!ParserStateReadNextInt(state, value))
160   {
161     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
162   }
163 
164   // range test, if minRange == maxRange, we don't check the range
165   if (minRange != maxRange && (*value < minRange || *value > maxRange))
166   {
167     return ParseGameDataError(gameData, state, TextFormat(
168       "Invalid value range for %s, range is [%d, %d], value is %d", 
169       key, minRange, maxRange, *value));
170   }
171 
172   return TryReadResult_Success;
173 }
174 
175 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange) 176 { 177 int intValue = *value; 178 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange); 179 *value = (uint8_t) intValue; 180 return result; 181 } 182 183 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange) 184 { 185 int intValue = *value; 186 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange); 187 *value = (int16_t) intValue; 188 return result; 189 } 190 191 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange) 192 { 193 int intValue = *value; 194 TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange); 195 *value = (uint16_t) intValue; 196 return result; 197 } 198
199 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key,
200 Vector2 *vector, Vector2 minRange, Vector2 maxRange)
201 { 202 if (!TextIsEqual(state->nextToken, key)) 203 { 204 return TryReadResult_NoMatch; 205 } 206
207 ParserState start = *state; 208 int x = 0, y = 0; 209 int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x; 210 int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
211
212 if (!ParserStateReadNextInt(state, &x))
213 { 214 // use start position to report the error for this KEY 215 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key)); 216 } 217 218 // range test, if minRange == maxRange, we don't check the range
219 if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
220 { 221 // use current position to report the error for x value 222 return ParseGameDataError(gameData, state, TextFormat( 223 "Invalid value x range for %s, range is [%d, %d], value is %d",
224 key, minXRange, maxXRange, x));
225 } 226
227 if (!ParserStateReadNextInt(state, &y))
228 { 229 // use start position to report the error for this KEY 230 return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key)); 231 } 232
233 if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
234 { 235 // use current position to report the error for y value 236 return ParseGameDataError(gameData, state, TextFormat( 237 "Invalid value y range for %s, range is [%d, %d], value is %d",
238 key, minYRange, maxYRange, y)); 239 } 240 241 vector->x = (float)x; 242 vector->y = (float)y;
243 244 return TryReadResult_Success; 245 } 246 247 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange) 248 { 249 if (!TextIsEqual(state->nextToken, key)) 250 { 251 return TryReadResult_NoMatch; 252 } 253 254 if (!ParserStateReadNextFloat(state, value)) 255 { 256 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key)); 257 } 258 259 // range test, if minRange == maxRange, we don't check the range 260 if (minRange != maxRange && (*value < minRange || *value > maxRange)) 261 { 262 return ParseGameDataError(gameData, state, TextFormat( 263 "Invalid value range for %s, range is [%f, %f], value is %f", 264 key, minRange, maxRange, *value)); 265 } 266 267 return TryReadResult_Success; 268 } 269 270 // The enumNames is a null-terminated array of strings that represent the enum values 271 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues) 272 { 273 if (!TextIsEqual(state->nextToken, key)) 274 { 275 return TryReadResult_NoMatch; 276 } 277 278 if (!ParserStateReadNextToken(state)) 279 { 280 return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key)); 281 } 282 283 for (int i = 0; enumNames[i] != 0; i++) 284 { 285 if (TextIsEqual(state->nextToken, enumNames[i])) 286 { 287 *value = enumValues[i]; 288 return TryReadResult_Success; 289 } 290 } 291 292 return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key)); 293 } 294
295 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
296 {
297 int enemyClassId = *enemyTypeId; 298 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId,
299 (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0},
300 (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS}); 301 if (minRange != maxRange)
302 {
303 enemyClassId = enemyClassId < minRange ? minRange : enemyClassId; 304 enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId; 305 } 306 *enemyTypeId = (uint8_t) enemyClassId; 307 return result;
308 }
309 310 //---------------------------------------------------------------- 311 //# Defines for compact struct field parsing 312 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct 313 // See implementations below for how this is used 314 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\ 315 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\ 316 {\ 317 case TryReadResult_Success:\ 318 if (name##Initialized) {\ 319 return ParseGameDataError(gameData, state, #name " already initialized");\ 320 }\ 321 name##Initialized = 1;\ 322 continue;\ 323 case TryReadResult_Error: return TryReadResult_Error;\ 324 } 325 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0; 326 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \ 327 if (!name##Initialized) { \ 328 return ParseGameDataError(gameData, state, #name " not initialized"); \ 329 } 330 331 #define GENERATE_FIELD_PARSING \ 332 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS)\ 333 while (1)\ 334 {\ 335 ParserState prevState = *state;\ 336 \ 337 if (!ParserStateReadNextToken(state))\ 338 {\ 339 /* end of file */\ 340 break;\ 341 }\ 342 FIELDS(GENERATE_READFIELD_SWITCH)\ 343 /* no match, return to previous state and break */\ 344 *state = prevState;\ 345 break;\ 346 } \ 347 FIELDS(GENERATE_FIELD_INIT_CHECK)\ 348 349 // END OF DEFINES 350 351 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 352 { 353 if (!TextIsEqual(state->nextToken, "Wave")) 354 { 355 return TryReadResult_NoMatch; 356 } 357 358 Level *level = &gameData->levels[gameData->lastLevelIndex]; 359 EnemyWave *wave = 0; 360 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 361 { 362 if (level->waves[i].count == 0) 363 { 364 wave = &level->waves[i]; 365 break; 366 } 367 } 368 369 if (wave == 0) 370 { 371 return ParseGameDataError(gameData, state,
372 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 373 } 374 375 #define FIELDS(GENERATE) \ 376 GENERATE(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \ 377 GENERATE(wave, count, UInt16, 1, 1000) \ 378 GENERATE(wave, delay, Float, 0.0f, 1000.0f) \ 379 GENERATE(wave, interval, Float, 0.0f, 1000.0f) \ 380 GENERATE(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \ 381 GENERATE(wave, enemyType, EnemyTypeId, 0, 0) 382 383 GENERATE_FIELD_PARSING 384 #undef FIELDS 385 386 return TryReadResult_Success; 387 } 388 389 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state) 390 { 391 uint8_t enemyClassId; 392 393 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7)) 394 { 395 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 396 case TryReadResult_Error: return TryReadResult_Error; 397 } 398 399 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId]; 400 401 #define FIELDS(GENERATE) \ 402 GENERATE(enemyClass, speed, Float, 0.1f, 1000.0f) \ 403 GENERATE(enemyClass, health, Float, 1, 1000000) \ 404 GENERATE(enemyClass, radius, Float, 0.0f, 10.0f) \ 405 GENERATE(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \ 406 GENERATE(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \ 407 GENERATE(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \ 408 GENERATE(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \ 409 GENERATE(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \ 410 GENERATE(enemyClass, goldValue, Int, 1, 1000000) 411 412 GENERATE_FIELD_PARSING 413 #undef FIELDS 414 415 return TryReadResult_Success; 416 } 417 418 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 419 { 420 int levelId; 421 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 422 if (result != TryReadResult_Success) 423 { 424 return result; 425 } 426 427 gameData->lastLevelIndex = levelId; 428 Level *level = &gameData->levels[levelId]; 429 430 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 431 // to detect if the level was already initialized 432 if (level->initialGold != 0) 433 { 434 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 435 } 436 437 int initialGoldInitialized = 0; 438 439 while (1) 440 { 441 // try to read the next token and if we don't know how to GENERATE it, 442 // we rewind and return 443 ParserState prevState = *state; 444 445 if (!ParserStateReadNextToken(state)) 446 { 447 // end of file 448 break; 449 } 450 451 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 452 { 453 case TryReadResult_Success: 454 if (initialGoldInitialized) 455 { 456 return ParseGameDataError(gameData, state, "initialGold already initialized"); 457 } 458 initialGoldInitialized = 1; 459 continue; 460 case TryReadResult_Error: return TryReadResult_Error; 461 } 462 463 switch (ParseGameDataTryReadWaveSection(gameData, state)) 464 { 465 case TryReadResult_Success: continue; 466 case TryReadResult_Error: return TryReadResult_Error; 467 } 468 469 // no match, return to previous state and break 470 *state = prevState; 471 break; 472 } 473 474 if (!initialGoldInitialized) 475 { 476 return ParseGameDataError(gameData, state, "initialGold not initialized"); 477 } 478 479 return TryReadResult_Success; 480 } 481 482 int ParseGameData(ParsedGameData *gameData, ParserState *state) 483 { 484 *gameData = (ParsedGameData){0}; 485 gameData->lastLevelIndex = -1; 486 487 while (ParserStateReadNextToken(state)) 488 { 489 switch (ParseGameDataTryReadLevelSection(gameData, state)) 490 { 491 case TryReadResult_Success: continue; 492 case TryReadResult_Error: return 0; 493 } 494 495 switch (ParseGameDataTryReadEnemyClassSection(gameData, state)) 496 { 497 case TryReadResult_Success: continue; 498 case TryReadResult_Error: return 0; 499 } 500 // read other sections later 501 502 503 } 504 505 return 1; 506 } 507 508 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 509 510 int RunParseTests() 511 { 512 int passedCount = 0, failedCount = 0; 513 ParserState state; 514 ParsedGameData gameData = {0}; 515 516 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 517 gameData = (ParsedGameData) {0}; 518 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 519 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 520 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 521 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 522 523 state = (ParserState) {"Level 392\n", 0, {0}}; 524 gameData = (ParsedGameData) {0}; 525 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 526 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 527 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 528 529 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 530 gameData = (ParsedGameData) {0}; 531 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 532 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 533 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 534 535 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 536 gameData = (ParsedGameData) {0}; 537 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 538 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 539 540 state = (ParserState) {"Level 3", 0, {0}}; 541 gameData = (ParsedGameData) {0}; 542 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 543 544 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 545 gameData = (ParsedGameData) {0}; 546 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 547 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 548 549 state = (ParserState) { 550 "Level 7\n initialGold 100\n" 551 "Wave\n" 552 "count 1 wave 2\n" 553 "interval 0.5\n" 554 "delay 1.0\n" 555 "spawnPosition -3 4\n" 556 "enemyType: ENEMY_TYPE_SHIELD" 557 , 0, {0}}; 558 gameData = (ParsedGameData) {0}; 559 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 560 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 561 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 562 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 563 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 564 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 565 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 566 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 567 568 // for every entry in the wave section, we want to verify that if that value is 569 // missing, the parser will produce an error. We can do that by commenting out each 570 // line individually in a loop - just replacing the two leading spaces with two dashes 571 const char *testString = 572 "Level 7 initialGold 100\n" 573 "Wave\n" 574 " count 1\n" 575 " wave 2\n" 576 " interval 0.5\n" 577 " delay 1.0\n" 578 " spawnPosition 3 -4\n" 579 " enemyType: ENEMY_TYPE_SHIELD"; 580 for (int i = 0; testString[i]; i++) 581 { 582 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 583 { 584 char copy[1024]; 585 strcpy(copy, testString); 586 // commentify! 587 copy[i + 1] = '-'; 588 copy[i + 2] = '-'; 589 state = (ParserState) {copy, 0, {0}}; 590 gameData = (ParsedGameData) {0}; 591 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 592 } 593 } 594 595 // test wave section missing data / incorrect data 596 597 state = (ParserState) { 598 "Level 7\n initialGold 100\n" 599 "Wave\n" 600 "count 1 wave 2\n" 601 "interval 0.5\n" 602 "delay 1.0\n" 603 "spawnPosition -3\n" // missing y 604 "enemyType: ENEMY_TYPE_SHIELD" 605 , 0, {0}}; 606 gameData = (ParsedGameData) {0}; 607 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 608 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 609 610 state = (ParserState) { 611 "Level 7\n initialGold 100\n" 612 "Wave\n" 613 "count 1.0 wave 2\n" 614 "interval 0.5\n" 615 "delay 1.0\n" 616 "spawnPosition -3\n" // missing y 617 "enemyType: ENEMY_TYPE_SHIELD" 618 , 0, {0}}; 619 gameData = (ParsedGameData) {0}; 620 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 621 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 622 623 // enemy class config parsing tests 624 state = (ParserState) { 625 "EnemyClass ENEMY_TYPE_MINION\n" 626 " health: 10.0\n" 627 " speed: 0.6\n" 628 " radius: 0.25\n" 629 " maxAcceleration: 1.0\n" 630 " explosionDamage: 1.0\n" 631 " requiredContactTime: 0.5\n" 632 " explosionRange: 1.0\n" 633 " explosionPushbackPower: 0.25\n" 634 " goldValue: 1\n" 635 , 0, {0}}; 636 gameData = (ParsedGameData) {0}; 637 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing enemy class section"); 638 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0"); 639 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6"); 640 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25"); 641 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0"); 642 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0"); 643 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5"); 644 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0"); 645 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25"); 646 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1"); 647 648 testString = 649 "EnemyClass ENEMY_TYPE_MINION\n" 650 " health: 10.0\n" 651 " speed: 0.6\n" 652 " radius: 0.25\n" 653 " maxAcceleration: 1.0\n" 654 " explosionDamage: 1.0\n" 655 " requiredContactTime: 0.5\n" 656 " explosionRange: 1.0\n" 657 " explosionPushbackPower: 0.25\n" 658 " goldValue: 1\n"; 659 for (int i = 0; testString[i]; i++) 660 { 661 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 662 { 663 char copy[1024]; 664 strcpy(copy, testString); 665 // commentify! 666 copy[i + 1] = '-'; 667 copy[i + 2] = '-'; 668 state = (ParserState) {copy, 0, {0}}; 669 gameData = (ParsedGameData) {0}; 670 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing EnemyClass section"); 671 } 672 } 673 674 675 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 676 677 return failedCount; 678 } 679 680 int main() 681 { 682 printf("Running parse tests\n"); 683 if (RunParseTests()) 684 { 685 return 1; 686 } 687 printf("\n"); 688 689 char *fileContent = LoadFileText("data/level.txt"); 690 if (fileContent == NULL) 691 { 692 printf("Failed to load file\n"); 693 return 1; 694 } 695 696 ParserState state = {fileContent, 0, {0}}; 697 ParsedGameData gameData = {0}; 698 699 if (!ParseGameData(&gameData, &state)) 700 { 701 printf("Failed to parse game data: %s\n", gameData.parseError); 702 UnloadFileText(fileContent); 703 return 1; 704 } 705 706 UnloadFileText(fileContent); 707 return 0; 708 }
Running parse tests
Passed 52 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

The entire field parsing is now done with these macro definitions and calls:

  1 #define FIELDS(HANDLE) \
  2   HANDLE(enemyClass, speed, Float, 0.1f, 1000.0f) \
  3   HANDLE(enemyClass, health, Float, 1, 1000000) \
  4   HANDLE(enemyClass, radius, Float, 0.0f, 10.0f) \
  5   HANDLE(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \
  6   HANDLE(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \
  7   HANDLE(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \
  8   HANDLE(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \
  9   HANDLE(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \
 10   HANDLE(enemyClass, goldValue, Int, 1, 1000000)
 11 
 12   HANDLE_FIELD_PARSING
 13 #undef FIELDS

While the macro definitions are not pretty at all and difficult to write, the declarations how to parse the fields is now as simple as this can get: 13 lines for 9 fields. The overhead is only 4 lines. As difficult as it is to come up with these macros, one clear benefit is, that we now know with quite some confidence that all such parsing steps will work exactly the same for all such structs and fields. We may therefore relax the testing for these parts a little bit for the next parts of the parser.

One potential problem is, that this approach may not work for more complex data structures that need more checks. E.g. when we have to ensure integrity or have conditional fields to parse.

So let's see how this works out for the tower type config parsing:

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 
  6 typedef struct ParserState
  7 {
  8   char *input;
  9   int position;
 10   char nextToken[256];
 11 } ParserState;
 12 
 13 int ParserStateGetLineNumber(ParserState *state)
 14 {
 15   int lineNumber = 1;
 16   for (int i = 0; i < state->position; i++)
 17   {
 18     if (state->input[i] == '\n')
 19     {
 20       lineNumber++;
 21     }
 22   }
 23   return lineNumber;
 24 }
 25 
 26 void ParserStateSkipWhiteSpaces(ParserState *state)
 27 {
 28   char *input = state->input;
 29   int pos = state->position;
 30   int skipped = 1;
 31   while (skipped)
 32   {
 33     skipped = 0;
 34     if (input[pos] == '-' && input[pos + 1] == '-')
 35     {
 36       skipped = 1;
 37       // skip comments
 38       while (input[pos] != 0 && input[pos] != '\n')
 39       {
 40         pos++;
 41       }
 42     }
 43   
 44     // skip white spaces and ignore colons
 45     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 46     {
 47       skipped = 1;
 48       pos++;
 49     }
 50 
 51     // repeat until no more white spaces or comments
 52   }
 53   state->position = pos;
 54 }
 55 
 56 int ParserStateReadNextToken(ParserState *state)
 57 {
 58   ParserStateSkipWhiteSpaces(state);
 59 
 60   int i = 0, pos = state->position;
 61   char *input = state->input;
 62 
 63   // read token
 64   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 65   {
 66     state->nextToken[i] = input[pos];
 67     pos++;
 68     i++;
 69   }
 70   state->position = pos;
 71 
 72   if (i == 0 || i == 256)
 73   {
 74     state->nextToken[0] = 0;
 75     return 0;
 76   }
 77   // terminate the token
 78   state->nextToken[i] = 0;
 79   return 1;
 80 }
 81 
 82 int ParserStateReadNextInt(ParserState *state, int *value)
 83 {
 84   if (!ParserStateReadNextToken(state))
 85   {
 86     return 0;
 87   }
 88   // check if the token is a valid integer
 89   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 90   for (int i = isSigned; state->nextToken[i] != 0; i++)
 91   {
 92     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 93     {
 94       return 0;
 95     }
 96   }
 97   *value = TextToInteger(state->nextToken);
 98   return 1;
 99 }
100 
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103   if (!ParserStateReadNextToken(state))
104   {
105     return 0;
106   }
107   // check if the token is a valid float number
108   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109   int hasDot = 0;
110   for (int i = isSigned; state->nextToken[i] != 0; i++)
111   {
112     if (state->nextToken[i] == '.')
113     {
114       if (hasDot)
115       {
116         return 0;
117       }
118       hasDot = 1;
119     }
120     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121     {
122       return 0;
123     }
124   }
125 
126   *value = TextToFloat(state->nextToken);
127   return 1;
128 }
129 
130 typedef struct ParsedGameData
131 {
132   const char *parseError;
133   Level levels[32];
134   int lastLevelIndex;
135   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
136   EnemyClassConfig enemyClasses[8];
137 } ParsedGameData;
138 
139 typedef enum TryReadResult
140 {
141   TryReadResult_NoMatch,
142   TryReadResult_Error,
143   TryReadResult_Success
144 } TryReadResult;
145 
146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
147 {
148   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
149   return TryReadResult_Error;
150 }
151 
152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
153 {
154   if (!TextIsEqual(state->nextToken, key))
155   {
156     return TryReadResult_NoMatch;
157   }
158 
159   if (!ParserStateReadNextInt(state, value))
160   {
161     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
162   }
163 
164   // range test, if minRange == maxRange, we don't check the range
165   if (minRange != maxRange && (*value < minRange || *value > maxRange))
166   {
167     return ParseGameDataError(gameData, state, TextFormat(
168       "Invalid value range for %s, range is [%d, %d], value is %d", 
169       key, minRange, maxRange, *value));
170   }
171 
172   return TryReadResult_Success;
173 }
174 
175 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange)
176 {
177   int intValue = *value;
178   TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
179   *value = (uint8_t) intValue;
180   return result;
181 }
182 
183 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange)
184 {
185   int intValue = *value;
186   TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
187   *value = (int16_t) intValue;
188   return result;
189 }
190 
191 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange)
192 {
193   int intValue = *value;
194   TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
195   *value = (uint16_t) intValue;
196   return result;
197 }
198 
199 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 
200   Vector2 *vector, Vector2 minRange, Vector2 maxRange)
201 {
202   if (!TextIsEqual(state->nextToken, key))
203   {
204     return TryReadResult_NoMatch;
205   }
206 
207   ParserState start = *state;
208   int x = 0, y = 0;
209   int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x;
210   int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
211 
212   if (!ParserStateReadNextInt(state, &x))
213   {
214     // use start position to report the error for this KEY
215     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
216   }
217 
218   // range test, if minRange == maxRange, we don't check the range
219   if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
220   {
221     // use current position to report the error for x value
222     return ParseGameDataError(gameData, state, TextFormat(
223       "Invalid value x range for %s, range is [%d, %d], value is %d", 
224       key, minXRange, maxXRange, x));
225   }
226 
227   if (!ParserStateReadNextInt(state, &y))
228   {
229     // use start position to report the error for this KEY
230     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
231   }
232 
233   if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
234   {
235     // use current position to report the error for y value
236     return ParseGameDataError(gameData, state, TextFormat(
237       "Invalid value y range for %s, range is [%d, %d], value is %d", 
238       key, minYRange, maxYRange, y));
239   }
240 
241   vector->x = (float)x;
242   vector->y = (float)y;
243 
244   return TryReadResult_Success;
245 }
246 
247 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
248 {
249   if (!TextIsEqual(state->nextToken, key))
250   {
251     return TryReadResult_NoMatch;
252   }
253 
254   if (!ParserStateReadNextFloat(state, value))
255   {
256     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
257   }
258 
259   // range test, if minRange == maxRange, we don't check the range
260   if (minRange != maxRange && (*value < minRange || *value > maxRange))
261   {
262     return ParseGameDataError(gameData, state, TextFormat(
263       "Invalid value range for %s, range is [%f, %f], value is %f", 
264       key, minRange, maxRange, *value));
265   }
266 
267   return TryReadResult_Success;
268 }
269 
270 // The enumNames is a null-terminated array of strings that represent the enum values
271 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
272 {
273   if (!TextIsEqual(state->nextToken, key))
274   {
275     return TryReadResult_NoMatch;
276   }
277 
278   if (!ParserStateReadNextToken(state))
279   {
280     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
281   }
282 
283   for (int i = 0; enumNames[i] != 0; i++)
284   {
285     if (TextIsEqual(state->nextToken, enumNames[i]))
286     {
287       *value = enumValues[i];
288       return TryReadResult_Success;
289     }
290   }
291 
292   return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
293 }
294 
295 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
296 {
297   int enemyClassId = *enemyTypeId;
298   TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId, 
299       (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0}, 
300       (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS});
301   if (minRange != maxRange)
302   {
303     enemyClassId = enemyClassId < minRange ? minRange : enemyClassId;
304     enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId;
305   }
306   *enemyTypeId = (uint8_t) enemyClassId;
307   return result;
308 }
309 
310 TryReadResult ParseGameDataTryReadKeyTowerTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange) 311 { 312 int towerType = *towerTypeId; 313 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType, 314 (const char *[]){"TOWER_TYPE_BASE", "TOWER_TYPE_ARCHER", "TOWER_TYPE_BALLISTA", "TOWER_TYPE_CATAPULT", "TOWER_TYPE_WALL", 0}, 315 (int[]){TOWER_TYPE_BASE, TOWER_TYPE_ARCHER, TOWER_TYPE_BALLISTA, TOWER_TYPE_CATAPULT, TOWER_TYPE_WALL}); 316 if (minRange != maxRange) 317 { 318 towerType = towerType < minRange ? minRange : towerType; 319 towerType = towerType > maxRange ? maxRange : towerType; 320 } 321 *towerTypeId = (uint8_t) towerType; 322 return result; 323 } 324 325 TryReadResult ParseGameDataTryReadKeyProjectileTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange) 326 { 327 int towerType = *towerTypeId; 328 TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType, 329 (const char *[]){"PROJECTILE_TYPE_ARROW", "PROJECTILE_TYPE_BALLISTA", "PROJECTILE_TYPE_CATAPULT", 0}, 330 (int[]){PROJECTILE_TYPE_ARROW, PROJECTILE_TYPE_BALLISTA, PROJECTILE_TYPE_CATAPULT}); 331 if (minRange != maxRange) 332 { 333 towerType = towerType < minRange ? minRange : towerType; 334 towerType = towerType > maxRange ? maxRange : towerType; 335 } 336 *towerTypeId = (uint8_t) towerType; 337 return result; 338 } 339 340
341 //---------------------------------------------------------------- 342 //# Defines for compact struct field parsing 343 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct 344 // See implementations below for how this is used 345 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\ 346 switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\ 347 {\ 348 case TryReadResult_Success:\ 349 if (name##Initialized) {\ 350 return ParseGameDataError(gameData, state, #name " already initialized");\ 351 }\ 352 name##Initialized = 1;\ 353 continue;\ 354 case TryReadResult_Error: return TryReadResult_Error;\ 355 } 356 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0; 357 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \ 358 if (!name##Initialized) { \ 359 return ParseGameDataError(gameData, state, #name " not initialized"); \ 360 } 361 362 #define GENERATE_FIELD_PARSING \ 363 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS)\ 364 while (1)\ 365 {\ 366 ParserState prevState = *state;\ 367 \ 368 if (!ParserStateReadNextToken(state))\ 369 {\ 370 /* end of file */\ 371 break;\ 372 }\ 373 FIELDS(GENERATE_READFIELD_SWITCH)\ 374 /* no match, return to previous state and break */\ 375 *state = prevState;\ 376 break;\ 377 } \ 378 FIELDS(GENERATE_FIELD_INIT_CHECK)\ 379 380 // END OF DEFINES 381 382 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 383 { 384 if (!TextIsEqual(state->nextToken, "Wave")) 385 { 386 return TryReadResult_NoMatch; 387 } 388 389 Level *level = &gameData->levels[gameData->lastLevelIndex]; 390 EnemyWave *wave = 0; 391 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 392 { 393 if (level->waves[i].count == 0) 394 { 395 wave = &level->waves[i]; 396 break; 397 } 398 } 399 400 if (wave == 0) 401 { 402 return ParseGameDataError(gameData, state, 403 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 404 } 405 406 #define FIELDS(GENERATE) \ 407 GENERATE(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \ 408 GENERATE(wave, count, UInt16, 1, 1000) \ 409 GENERATE(wave, delay, Float, 0.0f, 1000.0f) \ 410 GENERATE(wave, interval, Float, 0.0f, 1000.0f) \ 411 GENERATE(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \ 412 GENERATE(wave, enemyType, EnemyTypeId, 0, 0) 413 414 GENERATE_FIELD_PARSING 415 #undef FIELDS 416 417 return TryReadResult_Success; 418 } 419 420 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state) 421 { 422 uint8_t enemyClassId; 423 424 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7)) 425 { 426 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 427 case TryReadResult_Error: return TryReadResult_Error; 428 } 429 430 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId]; 431 432 #define FIELDS(GENERATE) \ 433 GENERATE(enemyClass, speed, Float, 0.1f, 1000.0f) \ 434 GENERATE(enemyClass, health, Float, 1, 1000000) \ 435 GENERATE(enemyClass, radius, Float, 0.0f, 10.0f) \ 436 GENERATE(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \ 437 GENERATE(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \ 438 GENERATE(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \ 439 GENERATE(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \ 440 GENERATE(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \
441 GENERATE(enemyClass, goldValue, Int, 1, 1000000) 442 443 GENERATE_FIELD_PARSING 444 #undef FIELDS 445 446 return TryReadResult_Success; 447 } 448 449 TryReadResult ParseGameDataTryReadTowerTypeConfigSection(ParsedGameData *gameData, ParserState *state) 450 { 451 uint8_t towerTypeId; 452 453 switch (ParseGameDataTryReadKeyTowerTypeId(gameData, state, "TowerTypeConfig", &towerTypeId, 0, TOWER_TYPE_COUNT - 1)) 454 { 455 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 456 case TryReadResult_Error: return TryReadResult_Error; 457 } 458 459 TowerTypeConfig *towerType = &gameData->towerTypes[towerTypeId]; 460 HitEffectConfig *hitEffect = &towerType->hitEffect; 461 462 #define FIELDS(GENERATE) \ 463 GENERATE(towerType, cooldown, Float, 0.0f, 1000.0f) \ 464 GENERATE(towerType, maxUpgradeCooldown, Float, 0.0f, 1000.0f) \ 465 GENERATE(towerType, range, Float, 0.0f, 50.0f) \ 466 GENERATE(towerType, maxUpgradeRange, Float, 0.0f, 50.0f) \ 467 GENERATE(towerType, projectileSpeed, Float, 0.0f, 100.0f) \ 468 GENERATE(towerType, cost, UInt8, 0, 255) \ 469 GENERATE(towerType, maxHealth, UInt16, 0, 0) \ 470 GENERATE(towerType, projectileType, ProjectileTypeId, 0, 32)\ 471 GENERATE(hitEffect, damage, Float, 0, 100000.0f) \ 472 GENERATE(hitEffect, maxUpgradeDamage, Float, 0, 100000.0f) \ 473 GENERATE(hitEffect, areaDamageRadius, Float, 0, 100000.0f) \ 474 GENERATE(hitEffect, pushbackPowerDistance, Float, 0, 100000.0f)
475 476 GENERATE_FIELD_PARSING 477 #undef FIELDS 478 479 return TryReadResult_Success; 480 } 481 482 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 483 { 484 int levelId; 485 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 486 if (result != TryReadResult_Success) 487 { 488 return result; 489 } 490 491 gameData->lastLevelIndex = levelId; 492 Level *level = &gameData->levels[levelId]; 493 494 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 495 // to detect if the level was already initialized 496 if (level->initialGold != 0) 497 { 498 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 499 } 500 501 int initialGoldInitialized = 0; 502 503 while (1) 504 { 505 // try to read the next token and if we don't know how to GENERATE it, 506 // we rewind and return 507 ParserState prevState = *state; 508 509 if (!ParserStateReadNextToken(state)) 510 { 511 // end of file 512 break; 513 } 514 515 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 516 { 517 case TryReadResult_Success: 518 if (initialGoldInitialized) 519 { 520 return ParseGameDataError(gameData, state, "initialGold already initialized"); 521 } 522 initialGoldInitialized = 1; 523 continue; 524 case TryReadResult_Error: return TryReadResult_Error; 525 } 526 527 switch (ParseGameDataTryReadWaveSection(gameData, state)) 528 { 529 case TryReadResult_Success: continue; 530 case TryReadResult_Error: return TryReadResult_Error; 531 } 532 533 // no match, return to previous state and break 534 *state = prevState; 535 break; 536 } 537 538 if (!initialGoldInitialized) 539 { 540 return ParseGameDataError(gameData, state, "initialGold not initialized"); 541 } 542 543 return TryReadResult_Success; 544 } 545 546 int ParseGameData(ParsedGameData *gameData, ParserState *state) 547 { 548 *gameData = (ParsedGameData){0}; 549 gameData->lastLevelIndex = -1; 550 551 while (ParserStateReadNextToken(state)) 552 { 553 switch (ParseGameDataTryReadLevelSection(gameData, state)) 554 { 555 case TryReadResult_Success: continue; 556 case TryReadResult_Error: return 0; 557 } 558 559 switch (ParseGameDataTryReadEnemyClassSection(gameData, state)) 560 { 561 case TryReadResult_Success: continue; 562 case TryReadResult_Error: return 0; 563 }
564 565 switch (ParseGameDataTryReadTowerTypeConfigSection(gameData, state)) 566 { 567 case TryReadResult_Success: continue; 568 case TryReadResult_Error: return 0; 569 }
570
571 // read other sections later
572 } 573 574 return 1; 575 } 576 577 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 578 579 int RunParseTests() 580 { 581 int passedCount = 0, failedCount = 0; 582 ParserState state; 583 ParsedGameData gameData = {0}; 584 585 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 586 gameData = (ParsedGameData) {0}; 587 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 588 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 589 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 590 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 591 592 state = (ParserState) {"Level 392\n", 0, {0}}; 593 gameData = (ParsedGameData) {0}; 594 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 595 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 596 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 597 598 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 599 gameData = (ParsedGameData) {0}; 600 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 601 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 602 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 603 604 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 605 gameData = (ParsedGameData) {0}; 606 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 607 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 608 609 state = (ParserState) {"Level 3", 0, {0}}; 610 gameData = (ParsedGameData) {0}; 611 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 612 613 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 614 gameData = (ParsedGameData) {0}; 615 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 616 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 617 618 state = (ParserState) { 619 "Level 7\n initialGold 100\n" 620 "Wave\n" 621 "count 1 wave 2\n" 622 "interval 0.5\n" 623 "delay 1.0\n" 624 "spawnPosition -3 4\n" 625 "enemyType: ENEMY_TYPE_SHIELD" 626 , 0, {0}}; 627 gameData = (ParsedGameData) {0}; 628 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 629 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 630 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 631 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 632 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 633 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 634 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 635 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 636 637 // for every entry in the wave section, we want to verify that if that value is 638 // missing, the parser will produce an error. We can do that by commenting out each 639 // line individually in a loop - just replacing the two leading spaces with two dashes 640 const char *testString = 641 "Level 7 initialGold 100\n" 642 "Wave\n" 643 " count 1\n" 644 " wave 2\n" 645 " interval 0.5\n" 646 " delay 1.0\n" 647 " spawnPosition 3 -4\n" 648 " enemyType: ENEMY_TYPE_SHIELD"; 649 for (int i = 0; testString[i]; i++) 650 { 651 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 652 { 653 char copy[1024]; 654 strcpy(copy, testString); 655 // commentify! 656 copy[i + 1] = '-'; 657 copy[i + 2] = '-'; 658 state = (ParserState) {copy, 0, {0}}; 659 gameData = (ParsedGameData) {0}; 660 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 661 } 662 } 663 664 // test wave section missing data / incorrect data 665 666 state = (ParserState) { 667 "Level 7\n initialGold 100\n" 668 "Wave\n" 669 "count 1 wave 2\n" 670 "interval 0.5\n" 671 "delay 1.0\n" 672 "spawnPosition -3\n" // missing y 673 "enemyType: ENEMY_TYPE_SHIELD" 674 , 0, {0}}; 675 gameData = (ParsedGameData) {0}; 676 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 677 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 678 679 state = (ParserState) { 680 "Level 7\n initialGold 100\n" 681 "Wave\n" 682 "count 1.0 wave 2\n" 683 "interval 0.5\n" 684 "delay 1.0\n" 685 "spawnPosition -3\n" // missing y 686 "enemyType: ENEMY_TYPE_SHIELD" 687 , 0, {0}}; 688 gameData = (ParsedGameData) {0}; 689 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 690 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 691 692 // enemy class config parsing tests 693 state = (ParserState) { 694 "EnemyClass ENEMY_TYPE_MINION\n" 695 " health: 10.0\n" 696 " speed: 0.6\n" 697 " radius: 0.25\n" 698 " maxAcceleration: 1.0\n" 699 " explosionDamage: 1.0\n" 700 " requiredContactTime: 0.5\n" 701 " explosionRange: 1.0\n" 702 " explosionPushbackPower: 0.25\n" 703 " goldValue: 1\n" 704 , 0, {0}}; 705 gameData = (ParsedGameData) {0}; 706 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing enemy class section"); 707 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0"); 708 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6"); 709 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25"); 710 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0"); 711 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0"); 712 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5"); 713 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0"); 714 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25"); 715 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1"); 716 717 testString = 718 "EnemyClass ENEMY_TYPE_MINION\n" 719 " health: 10.0\n" 720 " speed: 0.6\n" 721 " radius: 0.25\n" 722 " maxAcceleration: 1.0\n" 723 " explosionDamage: 1.0\n" 724 " requiredContactTime: 0.5\n" 725 " explosionRange: 1.0\n" 726 " explosionPushbackPower: 0.25\n" 727 " goldValue: 1\n"; 728 for (int i = 0; testString[i]; i++) 729 { 730 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 731 { 732 char copy[1024]; 733 strcpy(copy, testString); 734 // commentify! 735 copy[i + 1] = '-'; 736 copy[i + 2] = '-'; 737 state = (ParserState) {copy, 0, {0}}; 738 gameData = (ParsedGameData) {0}; 739 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing EnemyClass section"); 740 }
741 } 742 743 state = (ParserState) { 744 "TowerTypeConfig TOWER_TYPE_ARCHER\n" 745 " cooldown: 0.5\n" 746 " maxUpgradeCooldown: 0.25\n" 747 " range: 3\n" 748 " maxUpgradeRange: 5\n" 749 " projectileSpeed: 4.0\n" 750 " cost: 5\n" 751 " maxHealth: 10\n" 752 " projectileType: PROJECTILE_TYPE_ARROW\n" 753 " damage: 0.5\n" 754 " maxUpgradeDamage: 1.5\n" 755 " areaDamageRadius: 0\n" 756 " pushbackPowerDistance: 0\n" 757 , 0, {0}}; 758 gameData = (ParsedGameData) {0}; 759 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing tower type section"); 760 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5"); 761 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeCooldown == 0.25f, "Expected maxUpgradeCooldown to be 0.25"); 762 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].range == 3.0f, "Expected range to be 3.0"); 763 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeRange == 5.0f, "Expected maxUpgradeRange to be 5.0"); 764 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileSpeed == 4.0f, "Expected projectileSpeed to be 4.0"); 765 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 5, "Expected cost to be 5"); 766 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10"); 767 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileType == PROJECTILE_TYPE_ARROW, "Expected projectileType to be PROJECTILE_TYPE_ARROW"); 768 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.damage == 0.5f, "Expected damage to be 0.5"); 769 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.maxUpgradeDamage == 1.5f, "Expected maxUpgradeDamage to be 1.5"); 770 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.areaDamageRadius == 0.0f, "Expected areaDamageRadius to be 0.0"); 771 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.pushbackPowerDistance == 0.0f, "Expected pushbackPowerDistance to be 0.0");
772 773 774 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 775 776 return failedCount; 777 } 778 779 int main() 780 { 781 printf("Running parse tests\n"); 782 if (RunParseTests()) 783 { 784 return 1; 785 } 786 printf("\n"); 787 788 char *fileContent = LoadFileText("data/level.txt"); 789 if (fileContent == NULL) 790 { 791 printf("Failed to load file\n"); 792 return 1; 793 } 794 795 ParserState state = {fileContent, 0, {0}}; 796 ParsedGameData gameData = {0}; 797 798 if (!ParseGameData(&gameData, &state)) 799 { 800 printf("Failed to parse game data: %s\n", gameData.parseError); 801 UnloadFileText(fileContent); 802 return 1; 803 } 804 805 UnloadFileText(fileContent); 806 return 0; 807 }
Running parse tests
Passed 65 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully
Failed to parse game data: Error at line 31: cooldown not initialized

In principle, the tower config parsing is working - but there are now also a lot of values that need to be provided. A lack of defined values is why the currently loaded config file is producing an error. It conveniently points to the correct line at least 😅

What is nice is that the macro code generation even manages to set values in nested structs without requiring much code. Setting up the macro parsers is a bit annoying, but it works well enough.

But it would be nice to have a way to provide default values for fields that are not provided in the config file. Let's implement this next:

  • 💾
  1 #include "td_main.h"
  2 #include <raylib.h>
  3 #include <stdio.h>
  4 #include <string.h>
  5 
  6 typedef struct ParserState
  7 {
  8   char *input;
  9   int position;
 10   char nextToken[256];
 11 } ParserState;
 12 
 13 int ParserStateGetLineNumber(ParserState *state)
 14 {
 15   int lineNumber = 1;
 16   for (int i = 0; i < state->position; i++)
 17   {
 18     if (state->input[i] == '\n')
 19     {
 20       lineNumber++;
 21     }
 22   }
 23   return lineNumber;
 24 }
 25 
 26 void ParserStateSkipWhiteSpaces(ParserState *state)
 27 {
 28   char *input = state->input;
 29   int pos = state->position;
 30   int skipped = 1;
 31   while (skipped)
 32   {
 33     skipped = 0;
 34     if (input[pos] == '-' && input[pos + 1] == '-')
 35     {
 36       skipped = 1;
 37       // skip comments
 38       while (input[pos] != 0 && input[pos] != '\n')
 39       {
 40         pos++;
 41       }
 42     }
 43   
 44     // skip white spaces and ignore colons
 45     while (input[pos] != 0 && (input[pos] <= ' ' || input[pos] == ':'))
 46     {
 47       skipped = 1;
 48       pos++;
 49     }
 50 
 51     // repeat until no more white spaces or comments
 52   }
 53   state->position = pos;
 54 }
 55 
 56 int ParserStateReadNextToken(ParserState *state)
 57 {
 58   ParserStateSkipWhiteSpaces(state);
 59 
 60   int i = 0, pos = state->position;
 61   char *input = state->input;
 62 
 63   // read token
 64   while (input[pos] != 0 && input[pos] > ' ' && input[pos] != ':' && i < 256)
 65   {
 66     state->nextToken[i] = input[pos];
 67     pos++;
 68     i++;
 69   }
 70   state->position = pos;
 71 
 72   if (i == 0 || i == 256)
 73   {
 74     state->nextToken[0] = 0;
 75     return 0;
 76   }
 77   // terminate the token
 78   state->nextToken[i] = 0;
 79   return 1;
 80 }
 81 
 82 int ParserStateReadNextInt(ParserState *state, int *value)
 83 {
 84   if (!ParserStateReadNextToken(state))
 85   {
 86     return 0;
 87   }
 88   // check if the token is a valid integer
 89   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
 90   for (int i = isSigned; state->nextToken[i] != 0; i++)
 91   {
 92     if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
 93     {
 94       return 0;
 95     }
 96   }
 97   *value = TextToInteger(state->nextToken);
 98   return 1;
 99 }
100 
101 int ParserStateReadNextFloat(ParserState *state, float *value)
102 {
103   if (!ParserStateReadNextToken(state))
104   {
105     return 0;
106   }
107   // check if the token is a valid float number
108   int isSigned = state->nextToken[0] == '-' || state->nextToken[0] == '+';
109   int hasDot = 0;
110   for (int i = isSigned; state->nextToken[i] != 0; i++)
111   {
112     if (state->nextToken[i] == '.')
113     {
114       if (hasDot)
115       {
116         return 0;
117       }
118       hasDot = 1;
119     }
120     else if (state->nextToken[i] < '0' || state->nextToken[i] > '9')
121     {
122       return 0;
123     }
124   }
125 
126   *value = TextToFloat(state->nextToken);
127   return 1;
128 }
129 
130 typedef struct ParsedGameData
131 {
132   const char *parseError;
133   Level levels[32];
134   int lastLevelIndex;
135   TowerTypeConfig towerTypes[TOWER_TYPE_COUNT];
136   EnemyClassConfig enemyClasses[8];
137 } ParsedGameData;
138 
139 typedef enum TryReadResult
140 {
141   TryReadResult_NoMatch,
142   TryReadResult_Error,
143   TryReadResult_Success
144 } TryReadResult;
145 
146 TryReadResult ParseGameDataError(ParsedGameData *gameData, ParserState *state, const char *msg)
147 {
148   gameData->parseError = TextFormat("Error at line %d: %s", ParserStateGetLineNumber(state), msg);
149   return TryReadResult_Error;
150 }
151 
152 TryReadResult ParseGameDataTryReadKeyInt(ParsedGameData *gameData, ParserState *state, const char *key, int *value, int minRange, int maxRange)
153 {
154   if (!TextIsEqual(state->nextToken, key))
155   {
156     return TryReadResult_NoMatch;
157   }
158 
159   if (!ParserStateReadNextInt(state, value))
160   {
161     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s int value", key));
162   }
163 
164   // range test, if minRange == maxRange, we don't check the range
165   if (minRange != maxRange && (*value < minRange || *value > maxRange))
166   {
167     return ParseGameDataError(gameData, state, TextFormat(
168       "Invalid value range for %s, range is [%d, %d], value is %d", 
169       key, minRange, maxRange, *value));
170   }
171 
172   return TryReadResult_Success;
173 }
174 
175 TryReadResult ParseGameDataTryReadKeyUInt8(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *value, uint8_t minRange, uint8_t maxRange)
176 {
177   int intValue = *value;
178   TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
179   *value = (uint8_t) intValue;
180   return result;
181 }
182 
183 TryReadResult ParseGameDataTryReadKeyInt16(ParsedGameData *gameData, ParserState *state, const char *key, int16_t *value, int16_t minRange, int16_t maxRange)
184 {
185   int intValue = *value;
186   TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
187   *value = (int16_t) intValue;
188   return result;
189 }
190 
191 TryReadResult ParseGameDataTryReadKeyUInt16(ParsedGameData *gameData, ParserState *state, const char *key, uint16_t *value, uint16_t minRange, uint16_t maxRange)
192 {
193   int intValue = *value;
194   TryReadResult result =ParseGameDataTryReadKeyInt(gameData, state, key, &intValue, minRange, maxRange);
195   *value = (uint16_t) intValue;
196   return result;
197 }
198 
199 TryReadResult ParseGameDataTryReadKeyIntVec2(ParsedGameData *gameData, ParserState *state, const char *key, 
200   Vector2 *vector, Vector2 minRange, Vector2 maxRange)
201 {
202   if (!TextIsEqual(state->nextToken, key))
203   {
204     return TryReadResult_NoMatch;
205   }
206 
207   ParserState start = *state;
208   int x = 0, y = 0;
209   int minXRange = (int)minRange.x, maxXRange = (int)maxRange.x;
210   int minYRange = (int)minRange.y, maxYRange = (int)maxRange.y;
211 
212   if (!ParserStateReadNextInt(state, &x))
213   {
214     // use start position to report the error for this KEY
215     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s x int value", key));
216   }
217 
218   // range test, if minRange == maxRange, we don't check the range
219   if (minXRange != maxXRange && (x < minXRange || x > maxXRange))
220   {
221     // use current position to report the error for x value
222     return ParseGameDataError(gameData, state, TextFormat(
223       "Invalid value x range for %s, range is [%d, %d], value is %d", 
224       key, minXRange, maxXRange, x));
225   }
226 
227   if (!ParserStateReadNextInt(state, &y))
228   {
229     // use start position to report the error for this KEY
230     return ParseGameDataError(gameData, &start, TextFormat("Failed to read %s y int value", key));
231   }
232 
233   if (minYRange != maxYRange && (y < minYRange || y > maxYRange))
234   {
235     // use current position to report the error for y value
236     return ParseGameDataError(gameData, state, TextFormat(
237       "Invalid value y range for %s, range is [%d, %d], value is %d", 
238       key, minYRange, maxYRange, y));
239   }
240 
241   vector->x = (float)x;
242   vector->y = (float)y;
243 
244   return TryReadResult_Success;
245 }
246 
247 TryReadResult ParseGameDataTryReadKeyFloat(ParsedGameData *gameData, ParserState *state, const char *key, float *value, float minRange, float maxRange)
248 {
249   if (!TextIsEqual(state->nextToken, key))
250   {
251     return TryReadResult_NoMatch;
252   }
253 
254   if (!ParserStateReadNextFloat(state, value))
255   {
256     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s float value", key));
257   }
258 
259   // range test, if minRange == maxRange, we don't check the range
260   if (minRange != maxRange && (*value < minRange || *value > maxRange))
261   {
262     return ParseGameDataError(gameData, state, TextFormat(
263       "Invalid value range for %s, range is [%f, %f], value is %f", 
264       key, minRange, maxRange, *value));
265   }
266 
267   return TryReadResult_Success;
268 }
269 
270 // The enumNames is a null-terminated array of strings that represent the enum values
271 TryReadResult ParseGameDataTryReadEnum(ParsedGameData *gameData, ParserState *state, const char *key, int *value, const char *enumNames[], int *enumValues)
272 {
273   if (!TextIsEqual(state->nextToken, key))
274   {
275     return TryReadResult_NoMatch;
276   }
277 
278   if (!ParserStateReadNextToken(state))
279   {
280     return ParseGameDataError(gameData, state, TextFormat("Failed to read %s enum value", key));
281   }
282 
283   for (int i = 0; enumNames[i] != 0; i++)
284   {
285     if (TextIsEqual(state->nextToken, enumNames[i]))
286     {
287       *value = enumValues[i];
288       return TryReadResult_Success;
289     }
290   }
291 
292   return ParseGameDataError(gameData, state, TextFormat("Invalid value for %s", key));
293 }
294 
295 TryReadResult ParseGameDataTryReadKeyEnemyTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *enemyTypeId, uint8_t minRange, uint8_t maxRange)
296 {
297   int enemyClassId = *enemyTypeId;
298   TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, (int *)&enemyClassId, 
299       (const char *[]){"ENEMY_TYPE_MINION", "ENEMY_TYPE_RUNNER", "ENEMY_TYPE_SHIELD", "ENEMY_TYPE_BOSS", 0}, 
300       (int[]){ENEMY_TYPE_MINION, ENEMY_TYPE_RUNNER, ENEMY_TYPE_SHIELD, ENEMY_TYPE_BOSS});
301   if (minRange != maxRange)
302   {
303     enemyClassId = enemyClassId < minRange ? minRange : enemyClassId;
304     enemyClassId = enemyClassId > maxRange ? maxRange : enemyClassId;
305   }
306   *enemyTypeId = (uint8_t) enemyClassId;
307   return result;
308 }
309 
310 TryReadResult ParseGameDataTryReadKeyTowerTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
311 {
312   int towerType = *towerTypeId;
313   TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType, 
314       (const char *[]){"TOWER_TYPE_BASE", "TOWER_TYPE_ARCHER", "TOWER_TYPE_BALLISTA", "TOWER_TYPE_CATAPULT", "TOWER_TYPE_WALL", 0}, 
315       (int[]){TOWER_TYPE_BASE, TOWER_TYPE_ARCHER, TOWER_TYPE_BALLISTA, TOWER_TYPE_CATAPULT, TOWER_TYPE_WALL});
316   if (minRange != maxRange)
317   {
318     towerType = towerType < minRange ? minRange : towerType;
319     towerType = towerType > maxRange ? maxRange : towerType;
320   }
321   *towerTypeId = (uint8_t) towerType;
322   return result;
323 }
324 
325 TryReadResult ParseGameDataTryReadKeyProjectileTypeId(ParsedGameData *gameData, ParserState *state, const char *key, uint8_t *towerTypeId, uint8_t minRange, uint8_t maxRange)
326 {
327   int towerType = *towerTypeId;
328   TryReadResult result = ParseGameDataTryReadEnum(gameData, state, key, &towerType, 
329       (const char *[]){"PROJECTILE_TYPE_ARROW", "PROJECTILE_TYPE_BALLISTA", "PROJECTILE_TYPE_CATAPULT", 0}, 
330       (int[]){PROJECTILE_TYPE_ARROW, PROJECTILE_TYPE_BALLISTA, PROJECTILE_TYPE_CATAPULT});
331   if (minRange != maxRange)
332   {
333     towerType = towerType < minRange ? minRange : towerType;
334     towerType = towerType > maxRange ? maxRange : towerType;
335   }
336   *towerTypeId = (uint8_t) towerType;
337   return result;
338 }
339 
340 
341 //----------------------------------------------------------------
342 //# Defines for compact struct field parsing
343 // A FIELDS(GENERATEr) is to be defined that will be called for each field of the struct
344 // See implementations below for how this is used
345 #define GENERATE_READFIELD_SWITCH(owner, name, type, min, max)\
346   switch (ParseGameDataTryReadKey##type(gameData, state, #name, &owner->name, min, max))\
347   {\
348     case TryReadResult_Success:\
349       if (name##Initialized) {\
350         return ParseGameDataError(gameData, state, #name " already initialized");\
351       }\
352       name##Initialized = 1;\
353       continue;\
354     case TryReadResult_Error: return TryReadResult_Error;\
355   }
356 #define GENERATE_READFIELD_SWITCH_OPTIONAL(owner, name, type, def, min, max)\ 357 GENERATE_READFIELD_SWITCH(owner, name, type, min, max) 358 #define GENERATE_FIELD_INIT_DECLARATIONS(owner, name, type, min, max) int name##Initialized = 0; 359 #define GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL(owner, name, type, def, min, max) int name##Initialized = 0; owner->name = def;
360 #define GENERATE_FIELD_INIT_CHECK(owner, name, type, min, max) \ 361 if (!name##Initialized) { \ 362 return ParseGameDataError(gameData, state, #name " not initialized"); \
363 } 364 #define GENERATE_FIELD_INIT_CHECK_OPTIONAL(owner, name, type, def, min, max)
365 366 #define GENERATE_FIELD_PARSING \
367 FIELDS(GENERATE_FIELD_INIT_DECLARATIONS, GENERATE_FIELD_INIT_DECLARATIONS_OPTIONAL)\
368 while (1)\ 369 {\ 370 ParserState prevState = *state;\ 371 \ 372 if (!ParserStateReadNextToken(state))\ 373 {\ 374 /* end of file */\ 375 break;\ 376 }\
377 FIELDS(GENERATE_READFIELD_SWITCH, GENERATE_READFIELD_SWITCH_OPTIONAL)\
378 /* no match, return to previous state and break */\ 379 *state = prevState;\ 380 break;\ 381 } \
382 FIELDS(GENERATE_FIELD_INIT_CHECK, GENERATE_FIELD_INIT_CHECK_OPTIONAL)\
383 384 // END OF DEFINES 385 386 TryReadResult ParseGameDataTryReadWaveSection(ParsedGameData *gameData, ParserState *state) 387 { 388 if (!TextIsEqual(state->nextToken, "Wave")) 389 { 390 return TryReadResult_NoMatch; 391 } 392 393 Level *level = &gameData->levels[gameData->lastLevelIndex]; 394 EnemyWave *wave = 0; 395 for (int i = 0; i < ENEMY_MAX_WAVE_COUNT; i++) 396 { 397 if (level->waves[i].count == 0) 398 { 399 wave = &level->waves[i]; 400 break; 401 } 402 } 403 404 if (wave == 0) 405 { 406 return ParseGameDataError(gameData, state, 407 TextFormat("Wave section overflow (max is %d)", ENEMY_MAX_WAVE_COUNT)); 408 } 409
410 #define FIELDS(MANDATORY, OPTIONAL) \ 411 MANDATORY(wave, wave, UInt8, 0, ENEMY_MAX_WAVE_COUNT - 1) \ 412 MANDATORY(wave, count, UInt16, 1, 1000) \ 413 MANDATORY(wave, delay, Float, 0.0f, 1000.0f) \ 414 MANDATORY(wave, interval, Float, 0.0f, 1000.0f) \ 415 MANDATORY(wave, spawnPosition, IntVec2, ((Vector2){-10.0f, -10.0f}), ((Vector2){10.0f, 10.0f})) \ 416 MANDATORY(wave, enemyType, EnemyTypeId, 0, 0)
417 418 GENERATE_FIELD_PARSING 419 #undef FIELDS 420 421 return TryReadResult_Success; 422 } 423 424 TryReadResult ParseGameDataTryReadEnemyClassSection(ParsedGameData *gameData, ParserState *state) 425 { 426 uint8_t enemyClassId; 427 428 switch (ParseGameDataTryReadKeyEnemyTypeId(gameData, state, "EnemyClass", &enemyClassId, 0, 7)) 429 { 430 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 431 case TryReadResult_Error: return TryReadResult_Error; 432 } 433 434 EnemyClassConfig *enemyClass = &gameData->enemyClasses[enemyClassId]; 435
436 #define FIELDS(MANDATORY, OPTIONAL) \ 437 MANDATORY(enemyClass, speed, Float, 0.1f, 1000.0f) \ 438 MANDATORY(enemyClass, health, Float, 1, 1000000) \ 439 MANDATORY(enemyClass, radius, Float, 0.0f, 10.0f) \ 440 MANDATORY(enemyClass, requiredContactTime, Float, 0.0f, 1000.0f) \ 441 MANDATORY(enemyClass, maxAcceleration, Float, 0.0f, 1000.0f) \ 442 MANDATORY(enemyClass, explosionDamage, Float, 0.0f, 1000.0f) \ 443 MANDATORY(enemyClass, explosionRange, Float, 0.0f, 1000.0f) \ 444 MANDATORY(enemyClass, explosionPushbackPower, Float, 0.0f, 1000.0f) \ 445 MANDATORY(enemyClass, goldValue, Int, 1, 1000000)
446 447 GENERATE_FIELD_PARSING 448 #undef FIELDS 449 450 return TryReadResult_Success; 451 } 452 453 TryReadResult ParseGameDataTryReadTowerTypeConfigSection(ParsedGameData *gameData, ParserState *state) 454 { 455 uint8_t towerTypeId; 456 457 switch (ParseGameDataTryReadKeyTowerTypeId(gameData, state, "TowerTypeConfig", &towerTypeId, 0, TOWER_TYPE_COUNT - 1)) 458 { 459 case TryReadResult_NoMatch: return TryReadResult_NoMatch; 460 case TryReadResult_Error: return TryReadResult_Error; 461 } 462 463 TowerTypeConfig *towerType = &gameData->towerTypes[towerTypeId]; 464 HitEffectConfig *hitEffect = &towerType->hitEffect; 465
466 #define FIELDS(MANDATORY, OPTIONAL) \ 467 MANDATORY(towerType, maxHealth, UInt16, 0, 0) \ 468 OPTIONAL(towerType, cooldown, Float, 0, 0.0f, 1000.0f) \ 469 OPTIONAL(towerType, maxUpgradeCooldown, Float, 0, 0.0f, 1000.0f) \ 470 OPTIONAL(towerType, range, Float, 0, 0.0f, 50.0f) \ 471 OPTIONAL(towerType, maxUpgradeRange, Float, 0, 0.0f, 50.0f) \ 472 OPTIONAL(towerType, projectileSpeed, Float, 0, 0.0f, 100.0f) \ 473 OPTIONAL(towerType, cost, UInt8, 0, 0, 255) \ 474 OPTIONAL(towerType, projectileType, ProjectileTypeId, 0, 0, 32)\ 475 OPTIONAL(hitEffect, damage, Float, 0, 0, 100000.0f) \ 476 OPTIONAL(hitEffect, maxUpgradeDamage, Float, 0, 0, 100000.0f) \ 477 OPTIONAL(hitEffect, areaDamageRadius, Float, 0, 0, 100000.0f) \ 478 OPTIONAL(hitEffect, pushbackPowerDistance, Float, 0, 0, 100000.0f)
479 480 GENERATE_FIELD_PARSING 481 #undef FIELDS 482 483 return TryReadResult_Success; 484 } 485 486 TryReadResult ParseGameDataTryReadLevelSection(ParsedGameData *gameData, ParserState *state) 487 { 488 int levelId; 489 TryReadResult result = ParseGameDataTryReadKeyInt(gameData, state, "Level", &levelId, 0, 31); 490 if (result != TryReadResult_Success) 491 { 492 return result; 493 } 494 495 gameData->lastLevelIndex = levelId; 496 Level *level = &gameData->levels[levelId]; 497 498 // since we require the initialGold to be initialized with at least 1, we can use it as a flag 499 // to detect if the level was already initialized 500 if (level->initialGold != 0) 501 { 502 return ParseGameDataError(gameData, state, TextFormat("level %d already initialized", levelId)); 503 } 504 505 int initialGoldInitialized = 0; 506 507 while (1) 508 { 509 // try to read the next token and if we don't know how to GENERATE it, 510 // we rewind and return 511 ParserState prevState = *state; 512 513 if (!ParserStateReadNextToken(state)) 514 { 515 // end of file 516 break; 517 } 518 519 switch (ParseGameDataTryReadKeyInt(gameData, state, "initialGold", &level->initialGold, 1, 100000)) 520 { 521 case TryReadResult_Success: 522 if (initialGoldInitialized) 523 { 524 return ParseGameDataError(gameData, state, "initialGold already initialized"); 525 } 526 initialGoldInitialized = 1; 527 continue; 528 case TryReadResult_Error: return TryReadResult_Error; 529 } 530 531 switch (ParseGameDataTryReadWaveSection(gameData, state)) 532 { 533 case TryReadResult_Success: continue; 534 case TryReadResult_Error: return TryReadResult_Error; 535 } 536 537 // no match, return to previous state and break 538 *state = prevState; 539 break; 540 } 541 542 if (!initialGoldInitialized) 543 { 544 return ParseGameDataError(gameData, state, "initialGold not initialized"); 545 } 546 547 return TryReadResult_Success; 548 } 549 550 int ParseGameData(ParsedGameData *gameData, ParserState *state) 551 { 552 *gameData = (ParsedGameData){0}; 553 gameData->lastLevelIndex = -1; 554 555 while (ParserStateReadNextToken(state)) 556 { 557 switch (ParseGameDataTryReadLevelSection(gameData, state)) 558 { 559 case TryReadResult_Success: continue; 560 case TryReadResult_Error: return 0; 561 } 562 563 switch (ParseGameDataTryReadEnemyClassSection(gameData, state)) 564 { 565 case TryReadResult_Success: continue; 566 case TryReadResult_Error: return 0; 567 } 568 569 switch (ParseGameDataTryReadTowerTypeConfigSection(gameData, state)) 570 { 571 case TryReadResult_Success: continue; 572 case TryReadResult_Error: return 0; 573 } 574
575 // any other token is considered an error 576 ParseGameDataError(gameData, state, TextFormat("Unexpected token: %s", state->nextToken)); 577 return 0;
578 } 579 580 return 1; 581 } 582 583 #define EXPECT(cond, msg) if (!(cond)) { printf("Error %s:%d: %s\n", __FILE__, __LINE__, msg); failedCount++; } else { passedCount++; } 584 585 int RunParseTests() 586 { 587 int passedCount = 0, failedCount = 0; 588 ParserState state; 589 ParsedGameData gameData = {0}; 590 591 state = (ParserState) {"Level 7\n initialGold 100\nLevel 2 initialGold 200", 0, {0}}; 592 gameData = (ParsedGameData) {0}; 593 EXPECT(ParseGameData(&gameData, &state) == 1, "Failed to parse level section"); 594 EXPECT(gameData.lastLevelIndex == 2, "lastLevelIndex should be 2"); 595 EXPECT(gameData.levels[7].initialGold == 100, "initialGold should be 100"); 596 EXPECT(gameData.levels[2].initialGold == 200, "initialGold should be 200"); 597 598 state = (ParserState) {"Level 392\n", 0, {0}}; 599 gameData = (ParsedGameData) {0}; 600 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 601 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for Level") >= 0, "Expected invalid value range error"); 602 EXPECT(TextFindIndex(gameData.parseError, "line 1") >= 0, "Expected to find line number 1"); 603 604 state = (ParserState) {"Level 3\n initialGold -34", 0, {0}}; 605 gameData = (ParsedGameData) {0}; 606 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 607 EXPECT(TextFindIndex(gameData.parseError, "Invalid value range for initialGold") >= 0, "Expected invalid value range error"); 608 EXPECT(TextFindIndex(gameData.parseError, "line 2") >= 0, "Expected to find line number 1"); 609 610 state = (ParserState) {"Level 3\n initialGold 2\n initialGold 3", 0, {0}}; 611 gameData = (ParsedGameData) {0}; 612 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 613 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 614 615 state = (ParserState) {"Level 3", 0, {0}}; 616 gameData = (ParsedGameData) {0}; 617 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 618 619 state = (ParserState) {"Level 7\n initialGold 100\nLevel 7 initialGold 200", 0, {0}}; 620 gameData = (ParsedGameData) {0}; 621 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level section"); 622 EXPECT(TextFindIndex(gameData.parseError, "line 3") >= 0, "Expected to find line number 1"); 623 624 state = (ParserState) { 625 "Level 7\n initialGold 100\n" 626 "Wave\n" 627 "count 1 wave 2\n" 628 "interval 0.5\n" 629 "delay 1.0\n" 630 "spawnPosition -3 4\n" 631 "enemyType: ENEMY_TYPE_SHIELD" 632 , 0, {0}}; 633 gameData = (ParsedGameData) {0}; 634 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing level/wave section"); 635 EXPECT(gameData.levels[7].waves[0].count == 1, "Expected wave count to be 1"); 636 EXPECT(gameData.levels[7].waves[0].wave == 2, "Expected wave to be 2"); 637 EXPECT(gameData.levels[7].waves[0].interval == 0.5f, "Expected interval to be 0.5"); 638 EXPECT(gameData.levels[7].waves[0].delay == 1.0f, "Expected delay to be 1.0"); 639 EXPECT(gameData.levels[7].waves[0].spawnPosition.x == -3, "Expected spawnPosition.x to be 3"); 640 EXPECT(gameData.levels[7].waves[0].spawnPosition.y == 4, "Expected spawnPosition.y to be 4"); 641 EXPECT(gameData.levels[7].waves[0].enemyType == ENEMY_TYPE_SHIELD, "Expected enemyType to be ENEMY_TYPE_SHIELD"); 642 643 // for every entry in the wave section, we want to verify that if that value is 644 // missing, the parser will produce an error. We can do that by commenting out each 645 // line individually in a loop - just replacing the two leading spaces with two dashes 646 const char *testString = 647 "Level 7 initialGold 100\n" 648 "Wave\n" 649 " count 1\n" 650 " wave 2\n" 651 " interval 0.5\n" 652 " delay 1.0\n" 653 " spawnPosition 3 -4\n" 654 " enemyType: ENEMY_TYPE_SHIELD"; 655 for (int i = 0; testString[i]; i++) 656 { 657 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 658 { 659 char copy[1024]; 660 strcpy(copy, testString); 661 // commentify! 662 copy[i + 1] = '-'; 663 copy[i + 2] = '-'; 664 state = (ParserState) {copy, 0, {0}}; 665 gameData = (ParsedGameData) {0}; 666 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 667 } 668 } 669 670 // test wave section missing data / incorrect data 671 672 state = (ParserState) { 673 "Level 7\n initialGold 100\n" 674 "Wave\n" 675 "count 1 wave 2\n" 676 "interval 0.5\n" 677 "delay 1.0\n" 678 "spawnPosition -3\n" // missing y 679 "enemyType: ENEMY_TYPE_SHIELD" 680 , 0, {0}}; 681 gameData = (ParsedGameData) {0}; 682 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 683 EXPECT(TextFindIndex(gameData.parseError, "line 7") >= 0, "Expected to find line 7"); 684 685 state = (ParserState) { 686 "Level 7\n initialGold 100\n" 687 "Wave\n" 688 "count 1.0 wave 2\n" 689 "interval 0.5\n" 690 "delay 1.0\n" 691 "spawnPosition -3\n" // missing y 692 "enemyType: ENEMY_TYPE_SHIELD" 693 , 0, {0}}; 694 gameData = (ParsedGameData) {0}; 695 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing level/wave section"); 696 EXPECT(TextFindIndex(gameData.parseError, "line 4") >= 0, "Expected to find line 3"); 697 698 // enemy class config parsing tests 699 state = (ParserState) { 700 "EnemyClass ENEMY_TYPE_MINION\n" 701 " health: 10.0\n" 702 " speed: 0.6\n" 703 " radius: 0.25\n" 704 " maxAcceleration: 1.0\n" 705 " explosionDamage: 1.0\n" 706 " requiredContactTime: 0.5\n" 707 " explosionRange: 1.0\n" 708 " explosionPushbackPower: 0.25\n" 709 " goldValue: 1\n" 710 , 0, {0}}; 711 gameData = (ParsedGameData) {0}; 712 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing enemy class section"); 713 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].health == 10.0f, "Expected health to be 10.0"); 714 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].speed == 0.6f, "Expected speed to be 0.6"); 715 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].radius == 0.25f, "Expected radius to be 0.25"); 716 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].maxAcceleration == 1.0f, "Expected maxAcceleration to be 1.0"); 717 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionDamage == 1.0f, "Expected explosionDamage to be 1.0"); 718 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].requiredContactTime == 0.5f, "Expected requiredContactTime to be 0.5"); 719 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionRange == 1.0f, "Expected explosionRange to be 1.0"); 720 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].explosionPushbackPower == 0.25f, "Expected explosionPushbackPower to be 0.25"); 721 EXPECT(gameData.enemyClasses[ENEMY_TYPE_MINION].goldValue == 1, "Expected goldValue to be 1"); 722 723 testString = 724 "EnemyClass ENEMY_TYPE_MINION\n" 725 " health: 10.0\n" 726 " speed: 0.6\n" 727 " radius: 0.25\n" 728 " maxAcceleration: 1.0\n" 729 " explosionDamage: 1.0\n" 730 " requiredContactTime: 0.5\n" 731 " explosionRange: 1.0\n" 732 " explosionPushbackPower: 0.25\n" 733 " goldValue: 1\n"; 734 for (int i = 0; testString[i]; i++) 735 { 736 if (testString[i] == '\n' && testString[i + 1] == ' ' && testString[i + 2] == ' ') 737 { 738 char copy[1024]; 739 strcpy(copy, testString); 740 // commentify! 741 copy[i + 1] = '-'; 742 copy[i + 2] = '-'; 743 state = (ParserState) {copy, 0, {0}}; 744 gameData = (ParsedGameData) {0}; 745 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing EnemyClass section"); 746 } 747 } 748 749 state = (ParserState) { 750 "TowerTypeConfig TOWER_TYPE_ARCHER\n" 751 " cooldown: 0.5\n" 752 " maxUpgradeCooldown: 0.25\n" 753 " range: 3\n" 754 " maxUpgradeRange: 5\n" 755 " projectileSpeed: 4.0\n" 756 " cost: 5\n" 757 " maxHealth: 10\n" 758 " projectileType: PROJECTILE_TYPE_ARROW\n" 759 " damage: 0.5\n" 760 " maxUpgradeDamage: 1.5\n" 761 " areaDamageRadius: 0\n" 762 " pushbackPowerDistance: 0\n" 763 , 0, {0}}; 764 gameData = (ParsedGameData) {0}; 765 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing tower type section"); 766 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5"); 767 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeCooldown == 0.25f, "Expected maxUpgradeCooldown to be 0.25"); 768 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].range == 3.0f, "Expected range to be 3.0"); 769 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxUpgradeRange == 5.0f, "Expected maxUpgradeRange to be 5.0"); 770 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileSpeed == 4.0f, "Expected projectileSpeed to be 4.0"); 771 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 5, "Expected cost to be 5"); 772 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10"); 773 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].projectileType == PROJECTILE_TYPE_ARROW, "Expected projectileType to be PROJECTILE_TYPE_ARROW"); 774 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.damage == 0.5f, "Expected damage to be 0.5"); 775 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.maxUpgradeDamage == 1.5f, "Expected maxUpgradeDamage to be 1.5"); 776 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.areaDamageRadius == 0.0f, "Expected areaDamageRadius to be 0.0");
777 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].hitEffect.pushbackPowerDistance == 0.0f, "Expected pushbackPowerDistance to be 0.0"); 778 779 state = (ParserState) { 780 "TowerTypeConfig TOWER_TYPE_ARCHER\n" 781 " maxHealth: 10\n" 782 " cooldown: 0.5\n" 783 , 0, {0}}; 784 gameData = (ParsedGameData) {0}; 785 EXPECT(ParseGameData(&gameData, &state) == 1, "Expected to succeed parsing tower type section"); 786 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cooldown == 0.5f, "Expected cooldown to be 0.5"); 787 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10"); 788 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0"); 789 790 791 state = (ParserState) { 792 "TowerTypeConfig TOWER_TYPE_ARCHER\n" 793 " cooldown: 0.5\n" 794 , 0, {0}}; 795 gameData = (ParsedGameData) {0}; 796 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing tower type section"); 797 EXPECT(TextFindIndex(gameData.parseError, "maxHealth not initialized") >= 0, "Expected to find maxHealth not initialized"); 798 799 state = (ParserState) { 800 "TowerTypeConfig TOWER_TYPE_ARCHER\n" 801 " maxHealth: 10\n" 802 " foobar: 0.5\n" 803 , 0, {0}}; 804 gameData = (ParsedGameData) {0}; 805 EXPECT(ParseGameData(&gameData, &state) == 0, "Expected to fail parsing tower type section"); 806 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].maxHealth == 10, "Expected maxHealth to be 10"); 807 EXPECT(gameData.towerTypes[TOWER_TYPE_ARCHER].cost == 0, "Expected cost to be 0");
808 809 printf("Passed %d test(s), Failed %d\n", passedCount, failedCount); 810 811 return failedCount; 812 } 813 814 int main() 815 { 816 printf("Running parse tests\n"); 817 if (RunParseTests()) 818 { 819 return 1; 820 } 821 printf("\n"); 822 823 char *fileContent = LoadFileText("data/level.txt"); 824 if (fileContent == NULL) 825 { 826 printf("Failed to load file\n"); 827 return 1; 828 } 829 830 ParserState state = {fileContent, 0, {0}}; 831 ParsedGameData gameData = {0}; 832 833 if (!ParseGameData(&gameData, &state)) 834 { 835 printf("Failed to parse game data: %s\n", gameData.parseError); 836 UnloadFileText(fileContent); 837 return 1; 838 } 839 840 UnloadFileText(fileContent); 841 return 0; 842 }
Running parse tests
Passed 74 test(s), Failed 0

INFO: FILEIO: [data/level.txt] Text file loaded successfully

The full configuration file is now parsed and loaded. Hopefully also correctly.

Conclusion

The parser is now able to load the entire config file and initialize the game data structures with the provided values. The parser is now ready to be integrated into the game. So in the next part, we will load the config file and initialize the game data structures with the parsed values.

🍪