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.