20130313
[gdash.git] / src / fileops / bdcffload.cpp
blob909e63c78c399aa54b5c4932685ca3aac1eacd4c
1 /*
2 * Copyright (c) 2007-2013, Czirkos Zoltan http://code.google.com/p/gdash/
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 #include "config.h"
19 #include <algorithm>
20 #include <stdexcept>
21 #include "misc/logger.hpp"
22 #include "fileops/bdcffload.hpp"
23 #include "fileops/bdcffhelper.hpp"
24 #include "cave/caveset.hpp"
25 #include "cave/helper/cavereplay.hpp"
26 #include "cave/cavestored.hpp"
27 #include "misc/printf.hpp"
28 #include "misc/util.hpp"
29 #include "cave/elementproperties.hpp"
31 /// @todo engine types should be moved somewhere else?
32 #include "fileops/c64import.hpp" /* c64import defines the engine types */
33 #include "cave/object/caveobjectfillrect.hpp" /* bdcff intermission hack - adding a cavefillrect */
35 static bool struct_set_property(Reflective &str, const std::string &attrib, const std::string &param, int ratio)
37 PropertyDescription const *prop_desc=str.get_description_array();
38 int paramindex=0;
40 char **params=g_strsplit_set(param.c_str(), " ", -1);
41 int paramcount=g_strv_length(params);
42 bool identifier_found=false;
44 /* check all known tags. do not exit this loop if identifier_found==true...
45 as there are more lines in the array which have the same identifier. */
46 bool was_string=false;
47 for (unsigned i=0; prop_desc[i].identifier!=NULL; i++)
48 if (gd_str_ascii_caseequal(prop_desc[i].identifier, attrib)) {
49 /* found the identifier */
50 identifier_found=true;
51 std::auto_ptr<GetterBase> const &prop = prop_desc[i].prop;
52 if (prop_desc[i].type==GD_TYPE_STRING) {
53 /* strings are treated different, as occupy the whole length of the line */
54 str.get<GdString>(prop)=param;
55 was_string=true; /* remember this to skip checking the number of parameters at the end of the function */
56 continue;
59 if (prop_desc[i].type==GD_TYPE_LONGSTRING) {
60 char *compressed=g_strcompress(param.c_str());
61 str.get<GdString>(prop)=compressed;
62 g_free(compressed);
63 was_string=true; /* remember this to skip checking the number of parameters at the end of the function */
64 continue;
67 /* not a string, so use scanf calls */
68 /* try to read as many words, as there are elements in this property (array) */
69 /* ALSO, if no more parameters to process, exit loop */
70 for (unsigned j=0; j<prop->count && params[paramindex]!=NULL; j++) {
71 bool success=false;
73 switch (prop_desc[i].type) {
74 case GD_TYPE_BOOLEAN:
75 success=read_from_string(params[paramindex], str.get<GdBool>(prop));
76 /* if we are processing an array, fill other values with these. if there are other values specified, those will be overwritten. */
77 break;
78 case GD_TYPE_INT:
79 if (prop_desc[i].flags&GD_BDCFF_RATIO_TO_CAVE_SIZE)
80 success=read_from_string(params[paramindex], str.get<GdInt>(prop), ratio); /* saved as double, ratio to cave size */
81 else
82 success=read_from_string(params[paramindex], str.get<GdInt>(prop));
83 break;
84 case GD_TYPE_INT_LEVELS:
85 if (prop_desc[i].flags&GD_BDCFF_RATIO_TO_CAVE_SIZE)
86 success=read_from_string(params[paramindex], str.get<GdIntLevels>(prop)[j], ratio); /* saved as double, ratio to cave size */
87 else
88 success=read_from_string(params[paramindex], str.get<GdIntLevels>(prop)[j]);
89 if (success) /* copy to other if array */
90 for (unsigned k=j+1; k<prop->count; k++)
91 str.get<GdIntLevels>(prop)[k]=str.get<GdIntLevels>(prop)[j];
92 break;
93 case GD_TYPE_PROBABILITY:
94 success=read_from_string(params[paramindex], str.get<GdProbability>(prop));
95 break;
96 case GD_TYPE_PROBABILITY_LEVELS:
97 success=read_from_string(params[paramindex], str.get<GdProbabilityLevels>(prop)[j]);
98 if (success) /* copy to other if array */
99 for (unsigned k=j+1; k<prop->count; k++)
100 str.get<GdProbabilityLevels>(prop)[k]=str.get<GdProbabilityLevels>(prop)[j];
101 break;
102 case GD_TYPE_ELEMENT:
103 success=read_from_string(params[paramindex], str.get<GdElement>(prop));
104 break;
105 case GD_TYPE_DIRECTION:
106 success=read_from_string(params[paramindex], str.get<GdDirection>(prop));
107 break;
108 case GD_TYPE_SCHEDULING:
109 success=read_from_string(params[paramindex], str.get<GdScheduling>(prop));
110 break;
112 case GD_TYPE_LONGSTRING:
113 case GD_TYPE_STRING:
114 case GD_TYPE_COLOR:
115 case GD_TYPE_EFFECT:
116 case GD_TYPE_COORDINATE: /* caves do not have */
117 case GD_TYPE_BOOLEAN_LEVELS: /* caves do not have */
118 case GD_TAB: /* ui */
119 case GD_LABEL: /* ui */
120 g_assert_not_reached();
121 break;
124 if (success)
125 paramindex++; /* go to next parameter to process */
126 else
127 gd_warning(CPrintf("invalid parameter '%s' for attribute %s") % params[paramindex] % attrib);
130 /* if we found the identifier, but still could not process all parameters... */
131 /* of course, not for strings, as the whole line is the string */
132 if (identifier_found && !was_string && paramindex<paramcount)
133 gd_message(CPrintf("excess parameters for attribute '%s': '%s'") % attrib % params[paramindex]);
134 g_strfreev(params);
136 return identifier_found;
139 static bool replay_process_tag(CaveReplay &replay, const std::string &attrib, const std::string &param)
141 bool identifier_found=false;
143 /* movements */
144 if (gd_str_ascii_caseequal(attrib, "Movements")) {
145 identifier_found=true;
146 bool correct=replay.load_from_bdcff(param);
147 if (!correct)
148 gd_warning("Error in replay data");
149 } else
150 /* any other tag */
151 identifier_found=struct_set_property(replay, attrib, param, 0); /* 0: for ratio types; not used */
153 /* the func returns true if the identifier is to be removed */
154 return identifier_found;
158 static bool cave_process_tags_func(CaveStored &cave, const std::string &attrib, const std::string &param)
160 char **params=g_strsplit_set(param.c_str(), " ", -1);
161 int paramcount=g_strv_length(params);
162 bool identifier_found=false;
164 /* compatibility with old snapexplosions flag */
165 if (gd_str_ascii_caseequal(attrib, "SnapExplosions")) {
166 identifier_found=true;
167 GdBool b;
168 if (read_from_string(param, b)) {
169 if (b) // was "true" -> snapping explosions
170 cave.snap_element=O_EXPLODE_1;
171 else // was "false" -> normal space snapping
172 cave.snap_element=O_SPACE;
173 } else
174 gd_warning(CPrintf("invalid param for '%s': '%s'") % attrib % param);
176 else
177 /* compatibility with old bd1scheduling flag */
178 if (gd_str_ascii_caseequal(attrib, "BD1Scheduling")) {
179 identifier_found=true;
180 GdBool b;
181 if (read_from_string(param, b)) {
182 if (b)
183 if (cave.scheduling==GD_SCHEDULING_PLCK)
184 cave.scheduling=GD_SCHEDULING_BD1;
185 } else
186 gd_warning(CPrintf("invalid param for '%s': '%s'") % attrib % param);
188 else
189 /* bdcff engine flag */
190 if (gd_str_ascii_caseequal(attrib, "Engine")) {
191 identifier_found=true;
192 GdEngine e;
193 if (read_from_string(param, e))
194 C64Import::cave_set_engine_defaults(cave, e);
195 else
196 gd_warning(CPrintf("invalid param for '%s': '%s'") % attrib % param);
198 else
199 /* compatibility with old AmoebaProperties flag */
200 if (gd_str_ascii_caseequal(attrib, "AmoebaProperties")) {
201 identifier_found=true;
202 GdElement elem1=O_STONE, elem2=O_DIAMOND;
203 bool success=read_from_string(params[0], elem1) && read_from_string(params[1], elem2);
204 if (success) {
205 cave.amoeba_too_big_effect=elem1;
206 cave.amoeba_enclosed_effect=elem2;
207 } else
208 gd_warning(CPrintf("invalid param for '%s': '%s'") % attrib % param);
210 else
211 /* colors attribute is a mess, have to process explicitly */
212 if (gd_str_ascii_caseequal(attrib, "Colors")) {
213 /* Colors=[border background] foreground1 foreground2 foreground3 [amoeba slime] */
214 identifier_found=true;
215 bool ok=true;
216 GdColor cb, c0, c1, c2, c3, c4, c5;
218 if (paramcount==3) {
219 // only color1,2,3
220 cb=GdColor::from_c64(0); // border - black
221 c0=GdColor::from_c64(0); // background - black
222 ok=ok&&read_from_string(params[0], c1);
223 ok=ok&&read_from_string(params[1], c2);
224 ok=ok&&read_from_string(params[2], c3);
225 c4=c3; // amoeba
226 c5=c1; // slime
227 } else
228 if (paramcount==5) {
229 /* bg,color0,1,2,3 */
230 ok=ok&&read_from_string(params[0], cb);
231 ok=ok&&read_from_string(params[1], c0);
232 ok=ok&&read_from_string(params[2], c1);
233 ok=ok&&read_from_string(params[3], c2);
234 ok=ok&&read_from_string(params[4], c3);
235 c4=c3; // amoeba
236 c5=c1; // slime
237 } else
238 if (paramcount==7) {
239 // bg,color0,1,2,3,amoeba,slime
240 ok=ok&&read_from_string(params[0], cb);
241 ok=ok&&read_from_string(params[1], c0);
242 ok=ok&&read_from_string(params[2], c1);
243 ok=ok&&read_from_string(params[3], c2);
244 ok=ok&&read_from_string(params[4], c3);
245 ok=ok&&read_from_string(params[5], c4); // amoeba
246 ok=ok&&read_from_string(params[6], c5); // slime
247 } else {
248 ok=false;
251 if (ok) {
252 cave.colorb=cb;
253 cave.color0=c0;
254 cave.color1=c1;
255 cave.color2=c2;
256 cave.color3=c3;
257 cave.color4=c4;
258 cave.color5=c5;
259 } else {
260 gd_message(CPrintf("invalid param for '%s': '%s'") % attrib % param);
263 else
264 /* effects are also handled in an ugly way in bdcff */
265 if (gd_str_ascii_caseequal(attrib, "Effect")) {
266 identifier_found=true;
267 /* an effect command has two parameters */
268 if (paramcount==2) {
269 bool success=false;
270 PropertyDescription const *descriptor=cave.get_description_array();
272 int i;
273 for (i=0; descriptor[i].identifier!=NULL; i++) {
274 /* we have to search for this effect */
275 if (descriptor[i].type==GD_TYPE_EFFECT && gd_str_ascii_caseequal(params[0], descriptor[i].identifier)) {
276 /* found identifier */
277 success=read_from_string(params[1], cave.get<GdElement>(descriptor[i].prop));
278 if (success)
279 cave.get<GdElement>(descriptor[i].prop)=nonscanned_pair(cave.get<GdElement>(descriptor[i].prop));
280 break;
283 /* if we didn't find first element name */
284 if (descriptor[i].identifier==NULL) {
285 /* for compatibility with tim stridmann's memorydump->bdcff converter... .... ... */
286 if (gd_str_ascii_caseequal(params[0], "BOUNCING_BOULDER")) {
287 success=read_from_string(params[1], cave.stone_bouncing_effect);
288 if (success)
289 cave.stone_bouncing_effect=nonscanned_pair(cave.stone_bouncing_effect);
291 else if (gd_str_ascii_caseequal(params[0], "EXPLOSION3S")) {
292 success=read_from_string(params[1], cave.explosion_3_effect);
293 if (success)
294 cave.explosion_3_effect=nonscanned_pair(cave.explosion_3_effect);
296 /* falling with one l... */
297 else if (gd_str_ascii_caseequal(params[0], "STARTING_FALING_DIAMOND")) {
298 success=read_from_string(params[1], cave.diamond_falling_effect);
299 if (success)
300 cave.diamond_falling_effect=nonscanned_pair(cave.diamond_falling_effect);
302 /* dirt lookslike */
303 else if (gd_str_ascii_caseequal(params[0], "DIRT"))
304 success=read_from_string(params[1], cave.dirt_looks_like);
305 else if (gd_str_ascii_caseequal(params[0], "HEXPANDING_WALL") && gd_str_ascii_caseequal(params[1], "STEEL_HEXPANDING_WALL")) {
306 success=read_from_string(params[1], cave.expanding_wall_looks_like);
308 else {
309 /* didn't find at all */
310 gd_warning(CPrintf("invalid effect name '%s'") % params[0]);
311 success=true; // to ignore
314 if (!success)
315 gd_warning(CPrintf("cannot read element name '%s'") % params[1]);
317 } else
318 gd_warning(CPrintf("invalid effect specification '%s'") % param);
320 else {
321 identifier_found=struct_set_property(cave, attrib, param, cave.w*cave.h);
323 g_strfreev(params);
325 /* a ghrfunc should return true if the identifier is to be removed */
326 return identifier_found;
329 /// process a given cave property (by its name) - and do nothing, if no such property exists.
330 /// this function helps processing some cave tags in advance.
331 /// @param cave The cave to process the tag for.
332 /// @param lines The list of lines to find the attrib in.
333 /// @param name The name of the attribute to find.
334 /// @return true, if the property is found. If found, it is also processed and removed.
335 static bool cave_process_specific_tag(CaveStored &cave, BdcffSection &lines, const std::string& name)
337 BdcffSectionIterator it=find_if(lines.begin(), lines.end(), HasAttrib(name));
338 bool found=it!=lines.end();
339 if (found) {
340 try {
341 AttribParam ap(*it); // split into attrib and param
342 cave_process_tags_func(cave, ap.attrib, ap.param);
343 } catch (std::exception &e) {
344 gd_warning(CPrintf("Cannot parse: %s") % *it);
346 lines.erase(it); // erase after processing
348 return found;
351 /// Process properties for the cave, and set cave parameters according to it.
352 /// Some cave properties must be read in correct order, because bdcff is not a well designed format.
353 /// For example, the name is processed first, to be able to show all error messages with the cave name context.
354 /// Then the engine tag is processed - well, because bdcff sucks.
355 /// Then the size - to make sure ratios are read correctly - bdcff sucks.
356 static void cave_process_all_tags(CaveStored &cave, BdcffSection &lines)
358 BdcffSectionIterator it;
360 // first check cave name, so we can report errors correctly (saying that CaveStored xy: error foobar)
361 cave_process_specific_tag(cave, lines, "Name");
362 SetLoggerContextForFunction scf((cave.name=="") ? SPrintf("<unnamed cave>") : (SPrintf("Cave '%s'") % cave.name));
364 // process lame engine tag first so its settings may be overwritten later. fail.
365 cave_process_specific_tag(cave, lines, "Engine");
366 // check if this is an intermission, so we can set to cavesize or intermissionsize - another epic fail
367 cave_process_specific_tag(cave, lines, "Intermission");
368 // process size at the beginning... as ratio types depend on this. bdcff design fail.
369 cave_process_specific_tag(cave, lines, "Size");
371 // these properties have values, but also make some implications.
372 if (cave_process_specific_tag(cave, lines, "SlimePermeability"))
373 cave.slime_predictable=false;
374 if (cave_process_specific_tag(cave, lines, "SlimePermeabilityC64"))
375 cave.slime_predictable=true;
377 // these set scheduling type. framedelay takes precedence, if both exist. so we check it AFTER checking CaveDelay..
378 if (cave_process_specific_tag(cave, lines, "CaveDelay")) {
379 // only set scheduling type, when it is not the gdash-default.
380 // this allows setting cavescheduling=bd1 in the [game] section, for example.
381 // in that case, this one will not overwrite it.
382 // bdcff fail, fail, fail.
383 if (cave.scheduling==GD_SCHEDULING_MILLISECONDS)
384 cave.scheduling=GD_SCHEDULING_PLCK;
386 if (cave_process_specific_tag(cave, lines, "FrameTime")) {
387 // but if the cave has a frametime setting, always switch to milliseconds.
388 // bdcff says that we should select the better scheduling if we support both.
389 cave.scheduling=GD_SCHEDULING_MILLISECONDS;
392 // process remaining tags - most of them do not require special care.
393 for (BdcffSectionConstIterator it=lines.begin(); it!=lines.end(); ++it) {
394 try {
395 AttribParam ap(*it);
396 if (!cave_process_tags_func(cave, ap.attrib, ap.param)) {
397 gd_message(CPrintf("unknown tag '%s'") % ap.attrib);
398 cave.unknown_tags+=*it;
399 cave.unknown_tags+='\n';
401 } catch (std::exception &e) {
402 gd_warning(CPrintf("Cannot parse line: %s") % *it);
407 static bool add_highscore(HighScoreTable &hs, const std::string &name, const std::string &score)
409 std::istringstream is(score);
410 int sc;
412 /* parse score */
413 if (!(is>>sc))
414 return false;
415 if (name=="")
416 return false;
417 hs.add(name, sc);
419 return true;
422 static BdcffFile parse_bdcff_sections(const char *file_contents) throw (std::runtime_error)
424 BdcffFile file;
425 std::istringstream is(file_contents);
426 enum ReadState {
427 Start, ///< should be nothing here.
428 Bdcff, ///< inside [bdcff], eg. version=0.5
429 BdcffMapCodes, ///< inside [bdcff] [mapcodes] - not "standard", but gdash made files like this
430 Game, ///< inside [game], eg. author=foo
431 GameHighScore, ///< trivial
432 GameMapCodes, ///< eg. x=STEELWALL
433 Cave, ///< a cave, eg. name=Cave A
434 CaveSReplay, ///< replay section for a cave
435 CaveDemo, ///< old styled demo (replay), just movements, no random data & the like
436 CaveHighScore, ///< highscores for a cave
437 CaveObjects, ///< objects for a cave
438 CaveMap ///< map-encoded cave
439 } state;
441 std::string line;
442 state=Start;
443 bool bailout=false;
444 for (int lineno=1; !bailout && getline(is, line); lineno++) {
445 SetLoggerContextForFunction scf(SPrintf("Line %d") % lineno);
447 size_t found_r;
448 while ((found_r=line.find('\r'))!=std::string::npos)
449 line.erase(found_r, 1); /* remove windows-nightmare \r-s */
450 if (line.empty())
451 continue; /* skip empty lines */
453 if (state!=CaveMap && line[0]==';')
454 continue; /* just skip comments. be aware that map lines may start with a semicolon... */
456 /* STARTING WITH A BRACKET [ IS A SECTION */
457 if (line[0]=='[') {
458 if (gd_str_ascii_caseequal(line, "[BDCFF]")) {
459 if (state!=Start) {
460 gd_critical("first section should be [BDCFF]. Bailing out!");
461 bailout=true;
463 state=Bdcff;
465 else if (gd_str_ascii_caseequal(line, "[/BDCFF]")) {
466 state=Start;
468 else if (gd_str_ascii_caseequal(line, "[game]")) {
469 if (state!=Bdcff)
470 gd_warning("[game] should be inside [BDCFF]");
471 state=Game;
473 else if (gd_str_ascii_caseequal(line, "[/game]")) {
474 if (state!=Game)
475 gd_warning("[/game] not in [game] section");
477 else if (gd_str_ascii_caseequal(line, "[mapcodes]")) {
478 switch (state) {
479 case Game:
480 state=GameMapCodes; break;
481 case Bdcff:
482 state=BdcffMapCodes; break;
483 default:
484 gd_warning("[mapcodes] allowed only in [game] section");
485 state=BdcffMapCodes;
486 break;
489 else if (gd_str_ascii_caseequal(line, "[/mapcodes]")) {
490 switch (state) {
491 case GameMapCodes:
492 state=Game; break;
493 case BdcffMapCodes:
494 state=Bdcff; break;
495 default:
496 gd_warning("[/mapcodes] not after [mapcodes]");
497 state=Game;
500 else if (gd_str_ascii_caseequal(line, "[cave]")) {
501 if (state!=Game)
502 gd_warning("[cave] allowed only in [game] section");
503 state=Cave;
504 file.caves.push_back(BdcffFile::CaveInfo()); /* new empty space for a cave */
506 else if (gd_str_ascii_caseequal(line, "[/cave]")) {
507 if (state!=Cave)
508 gd_warning("[/cave] tag without starting [cave]");
509 state=Game;
511 else if (gd_str_ascii_caseequal(line, "[map]")) {
512 if (state!=Cave)
513 gd_warning("[map] section only allowed inside [cave]");
514 else /* else: do not enter map reading when not in a cave! */
515 state=CaveMap;
517 else if (gd_str_ascii_caseequal(line, "[/map]")) {
518 if (state!=CaveMap)
519 gd_warning("[/map] tag without starting [map]");
520 state=Cave;
522 else if (gd_str_ascii_caseequal(line, "[highscore]")) {
523 /* can be inside game or cave */
524 if (state==Game)
525 state=GameHighScore;
526 else
527 if (state==Cave)
528 state=CaveHighScore;
529 else {
530 gd_critical("[highscore] section only allowed inside [game] and [cave]. This confuses the parser, bailing out!");
531 bailout=true;
534 else if (gd_str_ascii_caseequal(line, "[/highscore]")) {
535 if (state==GameHighScore)
536 state=Game;
537 else
538 if (state==CaveHighScore)
539 state=Cave;
540 else {
541 gd_critical("[/highscore] only allowed after starting [highscore]. This confuses the parser, bailing out!");
542 bailout=true;
545 else if (gd_str_ascii_caseequal(line, "[objects]")) {
546 if (state!=Cave)
547 gd_warning("[objects] tag only allowed in [cave]");
548 if (file.caves.empty()) {
549 gd_warning("[replay] tag does not belong to any cave!");
550 file.caves.push_back(BdcffFile::CaveInfo());
552 state=CaveObjects;
554 else if (gd_str_ascii_caseequal(line, "[/objects]")) {
555 if (state!=CaveObjects)
556 gd_warning("[/objects] tag without starting [objects] tag");
557 state=Cave;
559 else if (gd_str_ascii_caseequal(line, "[demo]")) {
560 if (state!=Cave)
561 gd_warning("[demo] tag only allowed in [cave]");
562 if (file.caves.empty()) {
563 gd_warning("[demo] tag does not belong to any cave!");
564 file.caves.push_back(BdcffFile::CaveInfo());
566 state=CaveDemo;
567 file.caves.back().demo.push_back(""); /* push an empty string, lines will be added */
569 else if (gd_str_ascii_caseequal(line, "[/demo]")) {
570 if (state!=CaveDemo)
571 gd_warning("[/demo] tag without starting [demo] tag");
572 state=Cave;
574 else if (gd_str_ascii_caseequal(line, "[replay]")) {
575 if (state!=Cave)
576 gd_warning("[replay] tag only allowed in [cave]");
577 if (file.caves.empty()) {
578 gd_warning("[replay] tag does not belong to any cave!");
579 file.caves.push_back(BdcffFile::CaveInfo());
581 state=CaveSReplay;
582 file.caves.back().replays.push_back(BdcffSection());
584 else if (gd_str_ascii_caseequal(line, "[/replay]")) {
585 if (state!=CaveSReplay)
586 gd_warning("[/replay] tag without starting [replay] tag");
587 state=Cave;
589 /* GOSH i hate bdcff */
590 else if (gd_str_ascii_prefix(line, "[level=")) {
591 /* dump this thing in the object list. */
592 if (state!=CaveObjects)
593 gd_message("[level] tag only allowed inside [objects] section. Ignored.");
594 else
595 file.caves.back().objects.push_back(line);
597 else if (gd_str_ascii_caseequal(line, "[/level]")) {
598 /* dump this thing in the object list. */
599 if (state!=CaveObjects)
600 gd_message("[/level] tag only allowed inside [objects] section. Ignored.");
601 else
602 file.caves.back().objects.push_back(line);
604 else
605 gd_warning(CPrintf("unknown section: \"%s\"") % line);
607 continue;
610 /* OK, processed the section tags. */
611 /* now copy the line to the correct part of the BdcffFile object. */
613 /* first, check if we are at a map line. no stripping of spaces then! */
614 if (state==CaveMap) {
615 file.caves.back().map.push_back(line);
616 continue;
619 /* if not a map, we may strip spaces. do it here. */
620 gd_strchomp(line);
622 switch (state) {
623 case Start: /* should be nothing here. */
624 gd_critical(CPrintf("nothing allowed outside [BDCFF]: %s") % line);
625 bailout=true;
626 break;
628 case Bdcff: /* inside [bdcff], eg. version=0.5 */
629 file.bdcff.push_back(line);
630 break;
632 case Game: /* inside [game], eg. author=foo */
633 file.caveset_properties.push_back(line);
634 break;
636 case GameHighScore: /* trivial */
637 file.highscore.push_back(line);
638 break;
640 case BdcffMapCodes:
641 case GameMapCodes: /* eg. x=STEELWALL */
642 file.mapcodes.push_back(line);
643 break;
645 case Cave: /* a cave, eg. name=Cave A */
646 file.caves.back().properties.push_back(line);
647 break;
649 case CaveSReplay: /* replay for a cave */
650 file.caves.back().replays.back().push_back(line);
651 break;
653 case CaveDemo: /* old styled demo (replay), just movements, no random data & the like */
654 /* does not contain anything to check for! */
655 file.caves.back().demo.back()+=line+' ';
656 break;
658 case CaveHighScore: /* highscores for a cave */
659 file.caves.back().highscore.push_back(line);
660 break;
662 case CaveObjects: /* objects for a cave */
663 file.caves.back().objects.push_back(line);
664 break;
666 case CaveMap:
667 /* should already have handled it above */
668 g_assert_not_reached();
669 break;
673 if (bailout)
674 throw std::runtime_error("Error parsing BDCFF input");
676 return file;
679 CaveSet load_from_bdcff(const char *contents) throw(std::runtime_error)
681 // this may throw, but we do not catch
682 BdcffFile file = parse_bdcff_sections(contents);
684 /* this cave will store the default properties, specified in the [game] section for caves. */
685 /* especially the pain-in-the-ass engine tag. */
686 CaveStored default_cave;
688 CharToElementTable ctet;
689 std::string version_read="0.32"; /* assume version to be 0.32, also when the file does not specify it explicitly */
691 /* PROCESS BDCFF PROPERTIES */
692 for (BdcffSectionConstIterator it=file.bdcff.begin(); it!=file.bdcff.end(); ++it) {
693 AttribParam ap(*it);
695 if (gd_str_ascii_caseequal(ap.attrib, "Version"))
696 version_read=ap.param;
697 else
698 if (gd_str_ascii_caseequal(ap.attrib, "Engine")) {
699 // invalid but we accept
700 cave_process_tags_func(default_cave, ap.attrib, ap.param);
701 gd_message("Invalid BDCFF: Engine= belongs in the [game] section!");
703 else
704 gd_warning(CPrintf("Invalid attribute: %s") % ap.attrib);
707 CaveSet cs;
709 /* PROCESS CAVESET PROPERTIES */
710 for (BdcffSectionConstIterator it=file.caveset_properties.begin(); it!=file.caveset_properties.end(); ++it) {
711 AttribParam ap(*it);
713 if (gd_str_ascii_caseequal(ap.attrib, "Caves"))
714 continue; /* BDCFF files sometimes state how many caves they have; we ignore this field. */
715 if (gd_str_ascii_caseequal(ap.attrib, "Levels"))
716 continue; /* BDCFF files sometimes state how many caves they have; we ignore this field. */
718 /* try to interpret property for caveset */
719 if (!struct_set_property(cs, ap.attrib, ap.param, 0))
720 /* if not applicable, use it for the default cave */
721 if (!cave_process_tags_func(default_cave, ap.attrib, ap.param))
722 /* if not applicable for that, it is invalid. */
723 gd_warning(CPrintf("Invalid attribute: %s") % ap.attrib);
727 /* PROCESS CAVESET PROPERTIES */
728 for (BdcffSectionConstIterator it=file.caveset_properties.begin(); it!=file.caveset_properties.end(); ++it) {
729 AttribParam ap(*it);
731 if (gd_str_ascii_caseequal(ap.attrib, "Caves"))
732 continue; /* BDCFF files sometimes state how many caves they have; we ignore this field. */
733 if (gd_str_ascii_caseequal(ap.attrib, "Levels"))
734 continue; /* BDCFF files sometimes state how many caves they have; we ignore this field. */
736 /* try to interpret property for caveset */
737 if (!struct_set_property(cs, ap.attrib, ap.param, 0))
738 /* if not applicable, use it for the default cave */
739 if (!struct_set_property(default_cave, ap.attrib, ap.param, default_cave.w*default_cave.h))
740 /* if not applicable for that, it is invalid. */
741 gd_warning(CPrintf("Invalid attribute: %s") % ap.attrib);
744 /* PROCESS CAVESET HIGHSCORE */
745 for (BdcffSectionConstIterator it=file.highscore.begin(); it!=file.highscore.end(); ++it) {
746 /* stored as <score> <space> <name> */
747 try {
748 AttribParam ap(*it, ' ');
749 if (!add_highscore(cs.highscore, ap.param, ap.attrib))
750 gd_message(CPrintf("Invalid highscore: '%s'") % *it);
751 } catch (std::exception &e) {
752 gd_message(CPrintf("Invalid highscore line: '%s'") % *it);
756 /* PROCESS CAVESET MAPCODES */
757 for (BdcffSectionConstIterator it=file.mapcodes.begin(); it!=file.mapcodes.end(); ++it) {
758 AttribParam ap(*it);
760 if (gd_str_ascii_caseequal(ap.attrib, "Length")) {
761 if (ap.param!="1")
762 gd_critical("Only one-character map codes are currently supported!");
763 } else {
764 GdElement elem;
765 if (read_from_string(ap.param, elem))
766 ctet.set(ap.attrib[0], elem);
767 else
768 gd_warning(CPrintf("Unknown element name for map char: '%s'") % ap.attrib[0]);
772 /* PROCESS CAVES */
773 /* xxx const iterator cannot be used */
774 for (std::list<BdcffFile::CaveInfo>::iterator it=file.caves.begin(); it!=file.caves.end(); ++it) {
775 CaveStored *pcave=new CaveStored(default_cave);
776 CaveStored &cave=*pcave; /* use it as a reference, too */
778 cs.caves.push_back_adopt(pcave); /* add new cave */
780 cave_process_all_tags(cave, it->properties);
782 /* process cave highscore */
783 for (BdcffSectionConstIterator hit=it->highscore.begin(); hit!=it->highscore.end(); ++hit) {
784 /* stored as <score> <space> <name> */
785 try {
786 AttribParam ap(*hit, ' ');
787 if (!add_highscore(cave.highscore, ap.param, ap.attrib))
788 gd_message(CPrintf("Invalid highscore: '%s'") % *hit);
789 } catch (std::exception &e) {
790 gd_message(CPrintf("Invalid highscore line: '%s'") % *hit);
794 /* at the end, when read all tags (especially the size= tag) */
795 /* process map, if any. */
796 /* only report if map read is bigger than size= specified. */
797 /* some old bdcff files use smaller intermissions than the one specified. */
798 if (!it->map.empty()) {
799 /* yes, we have a map. */
800 /* create map and fill with initial border, in case that map strings are shorter or somewhat */
801 cave.map.set_size(cave.w, cave.h, cave.initial_border);
803 if (int(it->map.size())!=cave.height())
804 gd_warning(CPrintf("map error: cave height=%d (%d visible), map height=%u") % cave.height() % (cave.y2-cave.y1+1) % it->map.size());
806 BdcffSectionConstIterator mit; /* to iterate through map lines */
807 int y;
808 for (y=0, mit=it->map.begin(); y<cave.h && mit!=it->map.end(); ++mit, ++y) {
809 int linelen=mit->size();
811 for (int x=0; x<std::min(linelen, signed(cave.w)); x++)
812 cave.map(x, y)=ctet.get((*mit)[x]);
816 /* process cave objects */
817 GdBoolLevels levels;
818 for (unsigned n=0; n<5; ++n)
819 levels[n]=true;
820 for (BdcffSectionConstIterator oit=it->objects.begin(); oit!=it->objects.end(); ++oit) {
821 // process [levels] tags for objects, or process objects.
822 // [level] tags are badly designed in bdcff, as they are
823 // not really "sections", but properties of objects.
824 // yet, they are stored in sections. huge fail.
825 if (*oit=="[/Level]") {
826 for (unsigned n=0; n<5; ++n)
827 levels[n]=true;
829 else if (gd_str_ascii_prefix(*oit, "[Level=")) {
830 std::istringstream is(oit->substr(oit->find('=')+1));
831 for (unsigned n=0; n<5; ++n)
832 levels[n]=false;
833 int i;
834 while (is>>i) {
835 if (i-1>=0 && i-1<5)
836 levels[i-1]=true;
837 else {
838 gd_warning(CPrintf("Invalid [Levels=xxx] specification"));
839 for (unsigned n=0; n<5; ++n)
840 levels[n]=true;
841 break;
843 char c;
844 is>>c; // read comma
846 } else {
847 CaveObject *newobj=CaveObject::create_from_bdcff(*oit);
848 if (newobj) {
849 for (unsigned n=0; n<5; ++n)
850 newobj->seen_on[n]=levels[n];
851 cave.objects.push_back_adopt(newobj);
853 else
854 gd_warning(CPrintf("invalid object specification: %s") % *oit);
858 /* process replays */
859 for (std::list<BdcffSection>::const_iterator rit=it->replays.begin(); rit!=it->replays.end(); ++rit) {
860 cave.replays.push_back(CaveReplay()); /* push an empty replay */
861 CaveReplay &replay=cave.replays.back(); /* and work on that object */
863 replay.saved=true; /* set "saved" flag, so this replay will be written when the caveset is saved again */
864 /* and process its contents */
865 for (BdcffSectionConstIterator lines_it=rit->begin(); lines_it!=rit->end(); ++lines_it) {
866 if (lines_it->find('=')!=std::string::npos) {
867 AttribParam ap(*lines_it);
868 replay_process_tag(replay, ap.attrib, ap.param);
869 } else
870 replay_process_tag(replay, "Movements", *lines_it); /* try to interpret it as a bdcff replay */
874 /* process demos */
875 for (BdcffSectionConstIterator dit=it->demo.begin(); dit!=it->demo.end(); ++dit) {
876 cave.replays.push_back(CaveReplay()); /* push an empty replay */
877 CaveReplay &replay=cave.replays.back(); /* and work on that object */
879 replay.saved=true; /* set "saved" flag, so this replay will be written when the caveset is saved again */
880 replay.player_name="???";
881 replay_process_tag(replay, "Movements", *dit); /* try to interpret it as a bdcff replay */
885 /* old bdcff files hack. explanation follows. */
886 /* there were 40x22 caves in c64 bd, intermissions were also 40x22, but the visible */
887 /* part was the upper left corner, 20x12. 40x22 caves are needed, as 20x12 caves would */
888 /* look different (random cave elements needs the correct size.) */
889 /* also, in older bdcff files, there is no size= tag. caves default to 40x22 and 20x12. */
890 /* even the explicit drawrect and other drawing instructions, which did set up intermissions */
891 /* to be 20x12, are deleted. very very bad decision. */
892 /* here we try to detect and correct this. */
893 if (version_read=="0.32") {
894 gd_message("No BDCFF version, or 0.32. Using unspecified-intermission-size hack.");
896 for (unsigned int i=0; i<cs.caves.size(); ++i) {
897 CaveStored &cav=cs.cave(i);
899 /* only applies to intermissions */
900 /* not applied to mapped caves, as maps are filled with initial border, if the map read is smaller */
901 if (cav.intermission && cav.map.empty()) {
902 /* we do not set the cave to 20x12, rather to 40x22 with 20x12 visible. */
903 cav.w=40;
904 cav.h=22;
905 cav.x1=0; cav.y1=0;
906 cav.x2=19; cav.y2=11;
908 /* and cover the invisible area */
909 cav.objects.push_back_adopt(new CaveFillRect(Coordinate(0, 11), Coordinate(39, 21), cav.initial_border, cav.initial_border));
910 cav.objects.push_back_adopt(new CaveFillRect(Coordinate(19, 0), Coordinate(39, 21), cav.initial_border, cav.initial_border));
915 if (version_read!=BDCFF_VERSION)
916 gd_warning(CPrintf("BDCFF version %s, loaded caveset may have errors.") % version_read);
918 // check for replays which are problematic
919 for (unsigned int i=0; i<cs.caves.size(); ++i)
920 gd_cave_check_replays(cs.cave(i), true, false, false);
922 // return the created caveset.
923 return cs;