1 /***********************************************************
2 * Artsoft Retro-Game Library *
3 *----------------------------------------------------------*
4 * (c) 1994-2006 Artsoft Entertainment *
6 * Detmolder Strasse 189 *
9 * e-mail: info@artsoft.org *
10 *----------------------------------------------------------*
12 ***********************************************************/
14 #include <sys/types.h>
23 #if !defined(PLATFORM_WIN32)
25 #include <sys/param.h>
35 #define NUM_LEVELCLASS_DESC 8
37 static char *levelclass_desc[NUM_LEVELCLASS_DESC] =
50 #define LEVELCOLOR(n) (IS_LEVELCLASS_TUTORIAL(n) ? FC_BLUE : \
51 IS_LEVELCLASS_CLASSICS(n) ? FC_RED : \
52 IS_LEVELCLASS_BD(n) ? FC_YELLOW : \
53 IS_LEVELCLASS_EM(n) ? FC_YELLOW : \
54 IS_LEVELCLASS_SP(n) ? FC_YELLOW : \
55 IS_LEVELCLASS_DX(n) ? FC_YELLOW : \
56 IS_LEVELCLASS_SB(n) ? FC_YELLOW : \
57 IS_LEVELCLASS_CONTRIB(n) ? FC_GREEN : \
58 IS_LEVELCLASS_PRIVATE(n) ? FC_RED : \
61 #define LEVELSORTING(n) (IS_LEVELCLASS_TUTORIAL(n) ? 0 : \
62 IS_LEVELCLASS_CLASSICS(n) ? 1 : \
63 IS_LEVELCLASS_BD(n) ? 2 : \
64 IS_LEVELCLASS_EM(n) ? 3 : \
65 IS_LEVELCLASS_SP(n) ? 4 : \
66 IS_LEVELCLASS_DX(n) ? 5 : \
67 IS_LEVELCLASS_SB(n) ? 6 : \
68 IS_LEVELCLASS_CONTRIB(n) ? 7 : \
69 IS_LEVELCLASS_PRIVATE(n) ? 8 : \
72 #define ARTWORKCOLOR(n) (IS_ARTWORKCLASS_CLASSICS(n) ? FC_RED : \
73 IS_ARTWORKCLASS_CONTRIB(n) ? FC_GREEN : \
74 IS_ARTWORKCLASS_PRIVATE(n) ? FC_RED : \
75 IS_ARTWORKCLASS_LEVEL(n) ? FC_YELLOW : \
78 #define ARTWORKSORTING(n) (IS_ARTWORKCLASS_CLASSICS(n) ? 0 : \
79 IS_ARTWORKCLASS_LEVEL(n) ? 1 : \
80 IS_ARTWORKCLASS_CONTRIB(n) ? 2 : \
81 IS_ARTWORKCLASS_PRIVATE(n) ? 3 : \
84 #define TOKEN_VALUE_POSITION_SHORT 32
85 #define TOKEN_VALUE_POSITION_DEFAULT 40
86 #define TOKEN_COMMENT_POSITION_DEFAULT 60
88 #define MAX_COOKIE_LEN 256
91 static void setTreeInfoToDefaults(TreeInfo *, int);
92 static TreeInfo *getTreeInfoCopy(TreeInfo *ti);
93 static int compareTreeInfoEntries(const void *, const void *);
95 static int token_value_position = TOKEN_VALUE_POSITION_DEFAULT;
96 static int token_comment_position = TOKEN_COMMENT_POSITION_DEFAULT;
98 static SetupFileHash *artworkinfo_cache_old = NULL;
99 static SetupFileHash *artworkinfo_cache_new = NULL;
100 static boolean use_artworkinfo_cache = TRUE;
103 /* ------------------------------------------------------------------------- */
105 /* ------------------------------------------------------------------------- */
107 static char *getLevelClassDescription(TreeInfo *ti)
109 int position = ti->sort_priority / 100;
111 if (position >= 0 && position < NUM_LEVELCLASS_DESC)
112 return levelclass_desc[position];
114 return "Unknown Level Class";
117 static char *getUserLevelDir(char *level_subdir)
119 static char *userlevel_dir = NULL;
120 char *data_dir = getUserGameDataDir();
121 char *userlevel_subdir = LEVELS_DIRECTORY;
123 checked_free(userlevel_dir);
125 if (level_subdir != NULL)
126 userlevel_dir = getPath3(data_dir, userlevel_subdir, level_subdir);
128 userlevel_dir = getPath2(data_dir, userlevel_subdir);
130 return userlevel_dir;
133 static char *getScoreDir(char *level_subdir)
135 static char *score_dir = NULL;
136 char *data_dir = getCommonDataDir();
137 char *score_subdir = SCORES_DIRECTORY;
139 checked_free(score_dir);
141 if (level_subdir != NULL)
142 score_dir = getPath3(data_dir, score_subdir, level_subdir);
144 score_dir = getPath2(data_dir, score_subdir);
149 static char *getLevelSetupDir(char *level_subdir)
151 static char *levelsetup_dir = NULL;
152 char *data_dir = getUserGameDataDir();
153 char *levelsetup_subdir = LEVELSETUP_DIRECTORY;
155 checked_free(levelsetup_dir);
157 if (level_subdir != NULL)
158 levelsetup_dir = getPath3(data_dir, levelsetup_subdir, level_subdir);
160 levelsetup_dir = getPath2(data_dir, levelsetup_subdir);
162 return levelsetup_dir;
165 static char *getCacheDir()
167 static char *cache_dir = NULL;
169 if (cache_dir == NULL)
170 cache_dir = getPath2(getUserGameDataDir(), CACHE_DIRECTORY);
175 static char *getLevelDirFromTreeInfo(TreeInfo *node)
177 static char *level_dir = NULL;
180 return options.level_directory;
182 checked_free(level_dir);
184 level_dir = getPath2((node->in_user_dir ? getUserLevelDir(NULL) :
185 options.level_directory), node->fullpath);
190 char *getCurrentLevelDir()
192 return getLevelDirFromTreeInfo(leveldir_current);
195 static char *getTapeDir(char *level_subdir)
197 static char *tape_dir = NULL;
198 char *data_dir = getUserGameDataDir();
199 char *tape_subdir = TAPES_DIRECTORY;
201 checked_free(tape_dir);
203 if (level_subdir != NULL)
204 tape_dir = getPath3(data_dir, tape_subdir, level_subdir);
206 tape_dir = getPath2(data_dir, tape_subdir);
211 static char *getSolutionTapeDir()
213 static char *tape_dir = NULL;
214 char *data_dir = getCurrentLevelDir();
215 char *tape_subdir = TAPES_DIRECTORY;
217 checked_free(tape_dir);
219 tape_dir = getPath2(data_dir, tape_subdir);
224 static char *getDefaultGraphicsDir(char *graphics_subdir)
226 static char *graphics_dir = NULL;
228 if (graphics_subdir == NULL)
229 return options.graphics_directory;
231 checked_free(graphics_dir);
233 graphics_dir = getPath2(options.graphics_directory, graphics_subdir);
238 static char *getDefaultSoundsDir(char *sounds_subdir)
240 static char *sounds_dir = NULL;
242 if (sounds_subdir == NULL)
243 return options.sounds_directory;
245 checked_free(sounds_dir);
247 sounds_dir = getPath2(options.sounds_directory, sounds_subdir);
252 static char *getDefaultMusicDir(char *music_subdir)
254 static char *music_dir = NULL;
256 if (music_subdir == NULL)
257 return options.music_directory;
259 checked_free(music_dir);
261 music_dir = getPath2(options.music_directory, music_subdir);
266 static char *getClassicArtworkSet(int type)
268 return (type == TREE_TYPE_GRAPHICS_DIR ? GFX_CLASSIC_SUBDIR :
269 type == TREE_TYPE_SOUNDS_DIR ? SND_CLASSIC_SUBDIR :
270 type == TREE_TYPE_MUSIC_DIR ? MUS_CLASSIC_SUBDIR : "");
273 static char *getClassicArtworkDir(int type)
275 return (type == TREE_TYPE_GRAPHICS_DIR ?
276 getDefaultGraphicsDir(GFX_CLASSIC_SUBDIR) :
277 type == TREE_TYPE_SOUNDS_DIR ?
278 getDefaultSoundsDir(SND_CLASSIC_SUBDIR) :
279 type == TREE_TYPE_MUSIC_DIR ?
280 getDefaultMusicDir(MUS_CLASSIC_SUBDIR) : "");
283 static char *getUserGraphicsDir()
285 static char *usergraphics_dir = NULL;
287 if (usergraphics_dir == NULL)
288 usergraphics_dir = getPath2(getUserGameDataDir(), GRAPHICS_DIRECTORY);
290 return usergraphics_dir;
293 static char *getUserSoundsDir()
295 static char *usersounds_dir = NULL;
297 if (usersounds_dir == NULL)
298 usersounds_dir = getPath2(getUserGameDataDir(), SOUNDS_DIRECTORY);
300 return usersounds_dir;
303 static char *getUserMusicDir()
305 static char *usermusic_dir = NULL;
307 if (usermusic_dir == NULL)
308 usermusic_dir = getPath2(getUserGameDataDir(), MUSIC_DIRECTORY);
310 return usermusic_dir;
313 static char *getSetupArtworkDir(TreeInfo *ti)
315 static char *artwork_dir = NULL;
317 checked_free(artwork_dir);
319 artwork_dir = getPath2(ti->basepath, ti->fullpath);
324 char *setLevelArtworkDir(TreeInfo *ti)
326 char **artwork_path_ptr, **artwork_set_ptr;
327 TreeInfo *level_artwork;
329 if (ti == NULL || leveldir_current == NULL)
332 artwork_path_ptr = LEVELDIR_ARTWORK_PATH_PTR(leveldir_current, ti->type);
333 artwork_set_ptr = LEVELDIR_ARTWORK_SET_PTR( leveldir_current, ti->type);
335 checked_free(*artwork_path_ptr);
337 if ((level_artwork = getTreeInfoFromIdentifier(ti, *artwork_set_ptr)))
339 *artwork_path_ptr = getStringCopy(getSetupArtworkDir(level_artwork));
344 No (or non-existing) artwork configured in "levelinfo.conf". This would
345 normally result in using the artwork configured in the setup menu. But
346 if an artwork subdirectory exists (which might contain custom artwork
347 or an artwork configuration file), this level artwork must be treated
348 as relative to the default "classic" artwork, not to the artwork that
349 is currently configured in the setup menu.
351 Update: For "special" versions of R'n'D (like "R'n'D jue"), do not use
352 the "default" artwork (which would be "jue0" for "R'n'D jue"), but use
353 the real "classic" artwork from the original R'n'D (like "gfx_classic").
356 char *dir = getPath2(getCurrentLevelDir(), ARTWORK_DIRECTORY(ti->type));
358 checked_free(*artwork_set_ptr);
362 *artwork_path_ptr = getStringCopy(getClassicArtworkDir(ti->type));
363 *artwork_set_ptr = getStringCopy(getClassicArtworkSet(ti->type));
367 *artwork_path_ptr = getStringCopy(UNDEFINED_FILENAME);
368 *artwork_set_ptr = NULL;
374 return *artwork_set_ptr;
377 inline static char *getLevelArtworkSet(int type)
379 if (leveldir_current == NULL)
382 return LEVELDIR_ARTWORK_SET(leveldir_current, type);
385 inline static char *getLevelArtworkDir(int type)
387 if (leveldir_current == NULL)
388 return UNDEFINED_FILENAME;
390 return LEVELDIR_ARTWORK_PATH(leveldir_current, type);
393 char *getTapeFilename(int nr)
395 static char *filename = NULL;
396 char basename[MAX_FILENAME_LEN];
398 checked_free(filename);
400 sprintf(basename, "%03d.%s", nr, TAPEFILE_EXTENSION);
401 filename = getPath2(getTapeDir(leveldir_current->subdir), basename);
406 char *getSolutionTapeFilename(int nr)
408 static char *filename = NULL;
409 char basename[MAX_FILENAME_LEN];
411 checked_free(filename);
413 sprintf(basename, "%03d.%s", nr, TAPEFILE_EXTENSION);
414 filename = getPath2(getSolutionTapeDir(), basename);
416 if (!fileExists(filename))
418 static char *filename_sln = NULL;
420 checked_free(filename_sln);
422 sprintf(basename, "%03d.sln", nr);
423 filename_sln = getPath2(getSolutionTapeDir(), basename);
425 if (fileExists(filename_sln))
432 char *getScoreFilename(int nr)
434 static char *filename = NULL;
435 char basename[MAX_FILENAME_LEN];
437 checked_free(filename);
439 sprintf(basename, "%03d.%s", nr, SCOREFILE_EXTENSION);
440 filename = getPath2(getScoreDir(leveldir_current->subdir), basename);
445 char *getSetupFilename()
447 static char *filename = NULL;
449 checked_free(filename);
451 filename = getPath2(getSetupDir(), SETUP_FILENAME);
456 char *getEditorSetupFilename()
458 static char *filename = NULL;
460 checked_free(filename);
461 filename = getPath2(getCurrentLevelDir(), EDITORSETUP_FILENAME);
463 if (fileExists(filename))
466 checked_free(filename);
467 filename = getPath2(getSetupDir(), EDITORSETUP_FILENAME);
472 char *getHelpAnimFilename()
474 static char *filename = NULL;
476 checked_free(filename);
478 filename = getPath2(getCurrentLevelDir(), HELPANIM_FILENAME);
483 char *getHelpTextFilename()
485 static char *filename = NULL;
487 checked_free(filename);
489 filename = getPath2(getCurrentLevelDir(), HELPTEXT_FILENAME);
494 char *getLevelSetInfoFilename()
496 static char *filename = NULL;
511 for (i = 0; basenames[i] != NULL; i++)
513 checked_free(filename);
514 filename = getPath2(getCurrentLevelDir(), basenames[i]);
516 if (fileExists(filename))
523 char *getLevelSetTitleMessageBasename(int nr, boolean initial)
525 static char basename[32];
527 sprintf(basename, "%s_%d.txt",
528 (initial ? "titlemessage_initial" : "titlemessage"), nr + 1);
533 char *getLevelSetTitleMessageFilename(int nr, boolean initial)
535 static char *filename = NULL;
537 boolean skip_setup_artwork = FALSE;
539 checked_free(filename);
541 basename = getLevelSetTitleMessageBasename(nr, initial);
543 if (!gfx.override_level_graphics)
545 /* 1st try: look for special artwork in current level series directory */
546 filename = getPath3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
547 if (fileExists(filename))
552 /* 2nd try: look for message file in current level set directory */
553 filename = getPath2(getCurrentLevelDir(), basename);
554 if (fileExists(filename))
559 /* check if there is special artwork configured in level series config */
560 if (getLevelArtworkSet(ARTWORK_TYPE_GRAPHICS) != NULL)
562 /* 3rd try: look for special artwork configured in level series config */
563 filename = getPath2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
564 if (fileExists(filename))
569 /* take missing artwork configured in level set config from default */
570 skip_setup_artwork = TRUE;
574 if (!skip_setup_artwork)
576 /* 4th try: look for special artwork in configured artwork directory */
577 filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
578 if (fileExists(filename))
584 /* 5th try: look for default artwork in new default artwork directory */
585 filename = getPath2(getDefaultGraphicsDir(GFX_DEFAULT_SUBDIR), basename);
586 if (fileExists(filename))
591 /* 6th try: look for default artwork in old default artwork directory */
592 filename = getPath2(options.graphics_directory, basename);
593 if (fileExists(filename))
596 return NULL; /* cannot find specified artwork file anywhere */
599 static char *getCorrectedArtworkBasename(char *basename)
601 char *basename_corrected = basename;
603 #if defined(PLATFORM_MSDOS)
604 if (program.filename_prefix != NULL)
606 int prefix_len = strlen(program.filename_prefix);
608 if (strncmp(basename, program.filename_prefix, prefix_len) == 0)
609 basename_corrected = &basename[prefix_len];
611 /* if corrected filename is still longer than standard MS-DOS filename
612 size (8 characters + 1 dot + 3 characters file extension), shorten
613 filename by writing file extension after 8th basename character */
614 if (strlen(basename_corrected) > 8 + 1 + 3)
616 static char *msdos_filename = NULL;
618 checked_free(msdos_filename);
620 msdos_filename = getStringCopy(basename_corrected);
621 strncpy(&msdos_filename[8], &basename[strlen(basename) - (1+3)], 1+3 +1);
623 basename_corrected = msdos_filename;
628 return basename_corrected;
631 char *getCustomImageFilename(char *basename)
633 static char *filename = NULL;
634 boolean skip_setup_artwork = FALSE;
636 checked_free(filename);
638 basename = getCorrectedArtworkBasename(basename);
640 if (!gfx.override_level_graphics)
642 /* 1st try: look for special artwork in current level series directory */
643 filename = getPath3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
644 if (fileExists(filename))
649 /* check if there is special artwork configured in level series config */
650 if (getLevelArtworkSet(ARTWORK_TYPE_GRAPHICS) != NULL)
652 /* 2nd try: look for special artwork configured in level series config */
653 filename = getPath2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
654 if (fileExists(filename))
659 /* take missing artwork configured in level set config from default */
660 skip_setup_artwork = TRUE;
664 if (!skip_setup_artwork)
666 /* 3rd try: look for special artwork in configured artwork directory */
667 filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
668 if (fileExists(filename))
674 /* 4th try: look for default artwork in new default artwork directory */
675 filename = getPath2(getDefaultGraphicsDir(GFX_DEFAULT_SUBDIR), basename);
676 if (fileExists(filename))
681 /* 5th try: look for default artwork in old default artwork directory */
682 filename = getPath2(options.graphics_directory, basename);
683 if (fileExists(filename))
686 #if defined(CREATE_SPECIAL_EDITION)
690 Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)", basename);
692 /* 6th try: look for fallback artwork in old default artwork directory */
693 /* (needed to prevent errors when trying to access unused artwork files) */
694 filename = getPath2(options.graphics_directory, GFX_FALLBACK_FILENAME);
695 if (fileExists(filename))
699 return NULL; /* cannot find specified artwork file anywhere */
702 char *getCustomSoundFilename(char *basename)
704 static char *filename = NULL;
705 boolean skip_setup_artwork = FALSE;
707 checked_free(filename);
709 basename = getCorrectedArtworkBasename(basename);
711 if (!gfx.override_level_sounds)
713 /* 1st try: look for special artwork in current level series directory */
714 filename = getPath3(getCurrentLevelDir(), SOUNDS_DIRECTORY, basename);
715 if (fileExists(filename))
720 /* check if there is special artwork configured in level series config */
721 if (getLevelArtworkSet(ARTWORK_TYPE_SOUNDS) != NULL)
723 /* 2nd try: look for special artwork configured in level series config */
724 filename = getPath2(getLevelArtworkDir(TREE_TYPE_SOUNDS_DIR), basename);
725 if (fileExists(filename))
730 /* take missing artwork configured in level set config from default */
731 skip_setup_artwork = TRUE;
735 if (!skip_setup_artwork)
737 /* 3rd try: look for special artwork in configured artwork directory */
738 filename = getPath2(getSetupArtworkDir(artwork.snd_current), basename);
739 if (fileExists(filename))
745 /* 4th try: look for default artwork in new default artwork directory */
746 filename = getPath2(getDefaultSoundsDir(SND_DEFAULT_SUBDIR), basename);
747 if (fileExists(filename))
752 /* 5th try: look for default artwork in old default artwork directory */
753 filename = getPath2(options.sounds_directory, basename);
754 if (fileExists(filename))
757 #if defined(CREATE_SPECIAL_EDITION)
761 Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)", basename);
763 /* 6th try: look for fallback artwork in old default artwork directory */
764 /* (needed to prevent errors when trying to access unused artwork files) */
765 filename = getPath2(options.sounds_directory, SND_FALLBACK_FILENAME);
766 if (fileExists(filename))
770 return NULL; /* cannot find specified artwork file anywhere */
773 char *getCustomMusicFilename(char *basename)
775 static char *filename = NULL;
776 boolean skip_setup_artwork = FALSE;
778 checked_free(filename);
780 basename = getCorrectedArtworkBasename(basename);
782 if (!gfx.override_level_music)
784 /* 1st try: look for special artwork in current level series directory */
785 filename = getPath3(getCurrentLevelDir(), MUSIC_DIRECTORY, basename);
786 if (fileExists(filename))
791 /* check if there is special artwork configured in level series config */
792 if (getLevelArtworkSet(ARTWORK_TYPE_MUSIC) != NULL)
794 /* 2nd try: look for special artwork configured in level series config */
795 filename = getPath2(getLevelArtworkDir(TREE_TYPE_MUSIC_DIR), basename);
796 if (fileExists(filename))
801 /* take missing artwork configured in level set config from default */
802 skip_setup_artwork = TRUE;
806 if (!skip_setup_artwork)
808 /* 3rd try: look for special artwork in configured artwork directory */
809 filename = getPath2(getSetupArtworkDir(artwork.mus_current), basename);
810 if (fileExists(filename))
816 /* 4th try: look for default artwork in new default artwork directory */
817 filename = getPath2(getDefaultMusicDir(MUS_DEFAULT_SUBDIR), basename);
818 if (fileExists(filename))
823 /* 5th try: look for default artwork in old default artwork directory */
824 filename = getPath2(options.music_directory, basename);
825 if (fileExists(filename))
828 #if defined(CREATE_SPECIAL_EDITION)
832 Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)", basename);
834 /* 6th try: look for fallback artwork in old default artwork directory */
835 /* (needed to prevent errors when trying to access unused artwork files) */
836 filename = getPath2(options.music_directory, MUS_FALLBACK_FILENAME);
837 if (fileExists(filename))
841 return NULL; /* cannot find specified artwork file anywhere */
844 char *getCustomArtworkFilename(char *basename, int type)
846 if (type == ARTWORK_TYPE_GRAPHICS)
847 return getCustomImageFilename(basename);
848 else if (type == ARTWORK_TYPE_SOUNDS)
849 return getCustomSoundFilename(basename);
850 else if (type == ARTWORK_TYPE_MUSIC)
851 return getCustomMusicFilename(basename);
853 return UNDEFINED_FILENAME;
856 char *getCustomArtworkConfigFilename(int type)
858 return getCustomArtworkFilename(ARTWORKINFO_FILENAME(type), type);
861 char *getCustomArtworkLevelConfigFilename(int type)
863 static char *filename = NULL;
865 checked_free(filename);
867 filename = getPath2(getLevelArtworkDir(type), ARTWORKINFO_FILENAME(type));
872 char *getCustomMusicDirectory(void)
874 static char *directory = NULL;
875 boolean skip_setup_artwork = FALSE;
877 checked_free(directory);
879 if (!gfx.override_level_music)
881 /* 1st try: look for special artwork in current level series directory */
882 directory = getPath2(getCurrentLevelDir(), MUSIC_DIRECTORY);
883 if (fileExists(directory))
888 /* check if there is special artwork configured in level series config */
889 if (getLevelArtworkSet(ARTWORK_TYPE_MUSIC) != NULL)
891 /* 2nd try: look for special artwork configured in level series config */
892 directory = getStringCopy(getLevelArtworkDir(TREE_TYPE_MUSIC_DIR));
893 if (fileExists(directory))
898 /* take missing artwork configured in level set config from default */
899 skip_setup_artwork = TRUE;
903 if (!skip_setup_artwork)
905 /* 3rd try: look for special artwork in configured artwork directory */
906 directory = getStringCopy(getSetupArtworkDir(artwork.mus_current));
907 if (fileExists(directory))
913 /* 4th try: look for default artwork in new default artwork directory */
914 directory = getStringCopy(getDefaultMusicDir(MUS_DEFAULT_SUBDIR));
915 if (fileExists(directory))
920 /* 5th try: look for default artwork in old default artwork directory */
921 directory = getStringCopy(options.music_directory);
922 if (fileExists(directory))
925 return NULL; /* cannot find specified artwork file anywhere */
928 void InitTapeDirectory(char *level_subdir)
930 createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
931 createDirectory(getTapeDir(NULL), "main tape", PERMS_PRIVATE);
932 createDirectory(getTapeDir(level_subdir), "level tape", PERMS_PRIVATE);
935 void InitScoreDirectory(char *level_subdir)
937 createDirectory(getCommonDataDir(), "common data", PERMS_PUBLIC);
938 createDirectory(getScoreDir(NULL), "main score", PERMS_PUBLIC);
939 createDirectory(getScoreDir(level_subdir), "level score", PERMS_PUBLIC);
942 static void SaveUserLevelInfo();
944 void InitUserLevelDirectory(char *level_subdir)
946 if (!fileExists(getUserLevelDir(level_subdir)))
948 createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
949 createDirectory(getUserLevelDir(NULL), "main user level", PERMS_PRIVATE);
950 createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
956 void InitLevelSetupDirectory(char *level_subdir)
958 createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
959 createDirectory(getLevelSetupDir(NULL), "main level setup", PERMS_PRIVATE);
960 createDirectory(getLevelSetupDir(level_subdir), "level setup", PERMS_PRIVATE);
963 void InitCacheDirectory()
965 createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
966 createDirectory(getCacheDir(), "cache data", PERMS_PRIVATE);
970 /* ------------------------------------------------------------------------- */
971 /* some functions to handle lists of level and artwork directories */
972 /* ------------------------------------------------------------------------- */
974 TreeInfo *newTreeInfo()
976 return checked_calloc(sizeof(TreeInfo));
979 TreeInfo *newTreeInfo_setDefaults(int type)
981 TreeInfo *ti = newTreeInfo();
983 setTreeInfoToDefaults(ti, type);
988 void pushTreeInfo(TreeInfo **node_first, TreeInfo *node_new)
990 node_new->next = *node_first;
991 *node_first = node_new;
994 int numTreeInfo(TreeInfo *node)
1007 boolean validLevelSeries(TreeInfo *node)
1009 return (node != NULL && !node->node_group && !node->parent_link);
1012 TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
1017 if (node->node_group) /* enter level group (step down into tree) */
1018 return getFirstValidTreeInfoEntry(node->node_group);
1019 else if (node->parent_link) /* skip start entry of level group */
1021 if (node->next) /* get first real level series entry */
1022 return getFirstValidTreeInfoEntry(node->next);
1023 else /* leave empty level group and go on */
1024 return getFirstValidTreeInfoEntry(node->node_parent->next);
1026 else /* this seems to be a regular level series */
1030 TreeInfo *getTreeInfoFirstGroupEntry(TreeInfo *node)
1035 if (node->node_parent == NULL) /* top level group */
1036 return *node->node_top;
1037 else /* sub level group */
1038 return node->node_parent->node_group;
1041 int numTreeInfoInGroup(TreeInfo *node)
1043 return numTreeInfo(getTreeInfoFirstGroupEntry(node));
1046 int posTreeInfo(TreeInfo *node)
1048 TreeInfo *node_cmp = getTreeInfoFirstGroupEntry(node);
1053 if (node_cmp == node)
1057 node_cmp = node_cmp->next;
1063 TreeInfo *getTreeInfoFromPos(TreeInfo *node, int pos)
1065 TreeInfo *node_default = node;
1077 return node_default;
1080 TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
1082 if (identifier == NULL)
1087 if (node->node_group)
1089 TreeInfo *node_group;
1091 node_group = getTreeInfoFromIdentifier(node->node_group, identifier);
1096 else if (!node->parent_link)
1098 if (strEqual(identifier, node->identifier))
1108 TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
1109 TreeInfo *node, boolean skip_sets_without_levels)
1116 if (!node->parent_link && !node->level_group &&
1117 skip_sets_without_levels && node->levels == 0)
1118 return cloneTreeNode(node_top, node_parent, node->next,
1119 skip_sets_without_levels);
1122 node_new = getTreeInfoCopy(node); /* copy complete node */
1124 node_new = newTreeInfo();
1126 *node_new = *node; /* copy complete node */
1129 node_new->node_top = node_top; /* correct top node link */
1130 node_new->node_parent = node_parent; /* correct parent node link */
1132 if (node->level_group)
1133 node_new->node_group = cloneTreeNode(node_top, node_new, node->node_group,
1134 skip_sets_without_levels);
1136 node_new->next = cloneTreeNode(node_top, node_parent, node->next,
1137 skip_sets_without_levels);
1142 void cloneTree(TreeInfo **ti_new, TreeInfo *ti, boolean skip_empty_sets)
1144 TreeInfo *ti_cloned = cloneTreeNode(ti_new, NULL, ti, skip_empty_sets);
1146 *ti_new = ti_cloned;
1149 static boolean adjustTreeGraphicsForEMC(TreeInfo *node)
1151 boolean settings_changed = FALSE;
1155 if (node->graphics_set_ecs && !setup.prefer_aga_graphics &&
1156 !strEqual(node->graphics_set, node->graphics_set_ecs))
1158 setString(&node->graphics_set, node->graphics_set_ecs);
1159 settings_changed = TRUE;
1161 else if (node->graphics_set_aga && setup.prefer_aga_graphics &&
1162 !strEqual(node->graphics_set, node->graphics_set_aga))
1164 setString(&node->graphics_set, node->graphics_set_aga);
1165 settings_changed = TRUE;
1168 if (node->node_group != NULL)
1169 settings_changed |= adjustTreeGraphicsForEMC(node->node_group);
1174 return settings_changed;
1177 void dumpTreeInfo(TreeInfo *node, int depth)
1181 printf("Dumping TreeInfo:\n");
1185 for (i = 0; i < (depth + 1) * 3; i++)
1188 printf("'%s' / '%s'\n", node->identifier, node->name);
1191 // use for dumping artwork info tree
1192 printf("subdir == '%s' ['%s', '%s'] [%d])\n",
1193 node->subdir, node->fullpath, node->basepath, node->in_user_dir);
1196 if (node->node_group != NULL)
1197 dumpTreeInfo(node->node_group, depth + 1);
1203 void sortTreeInfoBySortFunction(TreeInfo **node_first,
1204 int (*compare_function)(const void *,
1207 int num_nodes = numTreeInfo(*node_first);
1208 TreeInfo **sort_array;
1209 TreeInfo *node = *node_first;
1215 /* allocate array for sorting structure pointers */
1216 sort_array = checked_calloc(num_nodes * sizeof(TreeInfo *));
1218 /* writing structure pointers to sorting array */
1219 while (i < num_nodes && node) /* double boundary check... */
1221 sort_array[i] = node;
1227 /* sorting the structure pointers in the sorting array */
1228 qsort(sort_array, num_nodes, sizeof(TreeInfo *),
1231 /* update the linkage of list elements with the sorted node array */
1232 for (i = 0; i < num_nodes - 1; i++)
1233 sort_array[i]->next = sort_array[i + 1];
1234 sort_array[num_nodes - 1]->next = NULL;
1236 /* update the linkage of the main list anchor pointer */
1237 *node_first = sort_array[0];
1241 /* now recursively sort the level group structures */
1245 if (node->node_group != NULL)
1246 sortTreeInfoBySortFunction(&node->node_group, compare_function);
1252 void sortTreeInfo(TreeInfo **node_first)
1254 sortTreeInfoBySortFunction(node_first, compareTreeInfoEntries);
1258 /* ========================================================================= */
1259 /* some stuff from "files.c" */
1260 /* ========================================================================= */
1262 #if defined(PLATFORM_WIN32)
1264 #define S_IRGRP S_IRUSR
1267 #define S_IROTH S_IRUSR
1270 #define S_IWGRP S_IWUSR
1273 #define S_IWOTH S_IWUSR
1276 #define S_IXGRP S_IXUSR
1279 #define S_IXOTH S_IXUSR
1282 #define S_IRWXG (S_IRGRP | S_IWGRP | S_IXGRP)
1287 #endif /* PLATFORM_WIN32 */
1289 /* file permissions for newly written files */
1290 #define MODE_R_ALL (S_IRUSR | S_IRGRP | S_IROTH)
1291 #define MODE_W_ALL (S_IWUSR | S_IWGRP | S_IWOTH)
1292 #define MODE_X_ALL (S_IXUSR | S_IXGRP | S_IXOTH)
1294 #define MODE_W_PRIVATE (S_IWUSR)
1295 #define MODE_W_PUBLIC (S_IWUSR | S_IWGRP)
1296 #define MODE_W_PUBLIC_DIR (S_IWUSR | S_IWGRP | S_ISGID)
1298 #define DIR_PERMS_PRIVATE (MODE_R_ALL | MODE_X_ALL | MODE_W_PRIVATE)
1299 #define DIR_PERMS_PUBLIC (MODE_R_ALL | MODE_X_ALL | MODE_W_PUBLIC_DIR)
1301 #define FILE_PERMS_PRIVATE (MODE_R_ALL | MODE_W_PRIVATE)
1302 #define FILE_PERMS_PUBLIC (MODE_R_ALL | MODE_W_PUBLIC)
1306 static char *dir = NULL;
1308 #if defined(PLATFORM_WIN32)
1311 dir = checked_malloc(MAX_PATH + 1);
1313 if (!SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, dir)))
1316 #elif defined(PLATFORM_UNIX)
1319 if ((dir = getenv("HOME")) == NULL)
1323 if ((pwd = getpwuid(getuid())) != NULL)
1324 dir = getStringCopy(pwd->pw_dir);
1336 char *getCommonDataDir(void)
1338 static char *common_data_dir = NULL;
1340 #if defined(PLATFORM_WIN32)
1341 if (common_data_dir == NULL)
1343 char *dir = checked_malloc(MAX_PATH + 1);
1345 if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_COMMON_DOCUMENTS, NULL, 0, dir))
1346 && !strEqual(dir, "")) /* empty for Windows 95/98 */
1347 common_data_dir = getPath2(dir, program.userdata_subdir);
1349 common_data_dir = options.rw_base_directory;
1352 if (common_data_dir == NULL)
1353 common_data_dir = options.rw_base_directory;
1356 return common_data_dir;
1359 char *getPersonalDataDir(void)
1361 static char *personal_data_dir = NULL;
1363 #if defined(PLATFORM_MACOSX)
1364 if (personal_data_dir == NULL)
1365 personal_data_dir = getPath2(getHomeDir(), "Documents");
1367 if (personal_data_dir == NULL)
1368 personal_data_dir = getHomeDir();
1371 return personal_data_dir;
1374 char *getUserGameDataDir(void)
1376 static char *user_game_data_dir = NULL;
1378 if (user_game_data_dir == NULL)
1379 user_game_data_dir = getPath2(getPersonalDataDir(),
1380 program.userdata_subdir);
1382 return user_game_data_dir;
1385 void updateUserGameDataDir()
1387 #if defined(PLATFORM_MACOSX)
1388 char *userdata_dir_old = getPath2(getHomeDir(), program.userdata_subdir_unix);
1389 char *userdata_dir_new = getUserGameDataDir(); /* do not free() this */
1391 /* convert old Unix style game data directory to Mac OS X style, if needed */
1392 if (fileExists(userdata_dir_old) && !fileExists(userdata_dir_new))
1394 if (rename(userdata_dir_old, userdata_dir_new) != 0)
1396 Error(ERR_WARN, "cannot move game data directory '%s' to '%s'",
1397 userdata_dir_old, userdata_dir_new);
1399 /* continue using Unix style data directory -- this should not happen */
1400 program.userdata_path = getPath2(getPersonalDataDir(),
1401 program.userdata_subdir_unix);
1405 free(userdata_dir_old);
1411 return getUserGameDataDir();
1414 static mode_t posix_umask(mode_t mask)
1416 #if defined(PLATFORM_UNIX)
1423 static int posix_mkdir(const char *pathname, mode_t mode)
1425 #if defined(PLATFORM_WIN32)
1426 return mkdir(pathname);
1428 return mkdir(pathname, mode);
1432 static boolean posix_process_running_setgid()
1434 #if defined(PLATFORM_UNIX)
1435 return (getgid() != getegid());
1441 void createDirectory(char *dir, char *text, int permission_class)
1443 /* leave "other" permissions in umask untouched, but ensure group parts
1444 of USERDATA_DIR_MODE are not masked */
1445 mode_t dir_mode = (permission_class == PERMS_PRIVATE ?
1446 DIR_PERMS_PRIVATE : DIR_PERMS_PUBLIC);
1447 mode_t last_umask = posix_umask(0);
1448 mode_t group_umask = ~(dir_mode & S_IRWXG);
1449 int running_setgid = posix_process_running_setgid();
1451 /* if we're setgid, protect files against "other" */
1452 /* else keep umask(0) to make the dir world-writable */
1455 posix_umask(last_umask & group_umask);
1457 dir_mode |= MODE_W_ALL;
1459 if (!fileExists(dir))
1460 if (posix_mkdir(dir, dir_mode) != 0)
1461 Error(ERR_WARN, "cannot create %s directory '%s': %s",
1462 text, dir, strerror(errno));
1464 if (permission_class == PERMS_PUBLIC && !running_setgid)
1465 chmod(dir, dir_mode);
1467 posix_umask(last_umask); /* restore previous umask */
1470 void InitUserDataDirectory()
1472 createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
1475 void SetFilePermissions(char *filename, int permission_class)
1477 int running_setgid = posix_process_running_setgid();
1478 int perms = (permission_class == PERMS_PRIVATE ?
1479 FILE_PERMS_PRIVATE : FILE_PERMS_PUBLIC);
1481 if (permission_class == PERMS_PUBLIC && !running_setgid)
1482 perms |= MODE_W_ALL;
1484 chmod(filename, perms);
1487 char *getCookie(char *file_type)
1489 static char cookie[MAX_COOKIE_LEN + 1];
1491 if (strlen(program.cookie_prefix) + 1 +
1492 strlen(file_type) + strlen("_FILE_VERSION_x.x") > MAX_COOKIE_LEN)
1493 return "[COOKIE ERROR]"; /* should never happen */
1495 sprintf(cookie, "%s_%s_FILE_VERSION_%d.%d",
1496 program.cookie_prefix, file_type,
1497 program.version_major, program.version_minor);
1502 int getFileVersionFromCookieString(const char *cookie)
1504 const char *ptr_cookie1, *ptr_cookie2;
1505 const char *pattern1 = "_FILE_VERSION_";
1506 const char *pattern2 = "?.?";
1507 const int len_cookie = strlen(cookie);
1508 const int len_pattern1 = strlen(pattern1);
1509 const int len_pattern2 = strlen(pattern2);
1510 const int len_pattern = len_pattern1 + len_pattern2;
1511 int version_major, version_minor;
1513 if (len_cookie <= len_pattern)
1516 ptr_cookie1 = &cookie[len_cookie - len_pattern];
1517 ptr_cookie2 = &cookie[len_cookie - len_pattern2];
1519 if (strncmp(ptr_cookie1, pattern1, len_pattern1) != 0)
1522 if (ptr_cookie2[0] < '0' || ptr_cookie2[0] > '9' ||
1523 ptr_cookie2[1] != '.' ||
1524 ptr_cookie2[2] < '0' || ptr_cookie2[2] > '9')
1527 version_major = ptr_cookie2[0] - '0';
1528 version_minor = ptr_cookie2[2] - '0';
1530 return VERSION_IDENT(version_major, version_minor, 0, 0);
1533 boolean checkCookieString(const char *cookie, const char *template)
1535 const char *pattern = "_FILE_VERSION_?.?";
1536 const int len_cookie = strlen(cookie);
1537 const int len_template = strlen(template);
1538 const int len_pattern = strlen(pattern);
1540 if (len_cookie != len_template)
1543 if (strncmp(cookie, template, len_cookie - len_pattern) != 0)
1549 /* ------------------------------------------------------------------------- */
1550 /* setup file list and hash handling functions */
1551 /* ------------------------------------------------------------------------- */
1553 char *getFormattedSetupEntry(char *token, char *value)
1556 static char entry[MAX_LINE_LEN];
1558 /* if value is an empty string, just return token without value */
1562 /* start with the token and some spaces to format output line */
1563 sprintf(entry, "%s:", token);
1564 for (i = strlen(entry); i < token_value_position; i++)
1567 /* continue with the token's value */
1568 strcat(entry, value);
1573 SetupFileList *newSetupFileList(char *token, char *value)
1575 SetupFileList *new = checked_malloc(sizeof(SetupFileList));
1577 new->token = getStringCopy(token);
1578 new->value = getStringCopy(value);
1585 void freeSetupFileList(SetupFileList *list)
1590 checked_free(list->token);
1591 checked_free(list->value);
1594 freeSetupFileList(list->next);
1599 char *getListEntry(SetupFileList *list, char *token)
1604 if (strEqual(list->token, token))
1607 return getListEntry(list->next, token);
1610 SetupFileList *setListEntry(SetupFileList *list, char *token, char *value)
1615 if (strEqual(list->token, token))
1617 checked_free(list->value);
1619 list->value = getStringCopy(value);
1623 else if (list->next == NULL)
1624 return (list->next = newSetupFileList(token, value));
1626 return setListEntry(list->next, token, value);
1629 SetupFileList *addListEntry(SetupFileList *list, char *token, char *value)
1634 if (list->next == NULL)
1635 return (list->next = newSetupFileList(token, value));
1637 return addListEntry(list->next, token, value);
1642 static void printSetupFileList(SetupFileList *list)
1647 printf("token: '%s'\n", list->token);
1648 printf("value: '%s'\n", list->value);
1650 printSetupFileList(list->next);
1656 DEFINE_HASHTABLE_INSERT(insert_hash_entry, char, char);
1657 DEFINE_HASHTABLE_SEARCH(search_hash_entry, char, char);
1658 DEFINE_HASHTABLE_CHANGE(change_hash_entry, char, char);
1659 DEFINE_HASHTABLE_REMOVE(remove_hash_entry, char, char);
1661 #define insert_hash_entry hashtable_insert
1662 #define search_hash_entry hashtable_search
1663 #define change_hash_entry hashtable_change
1664 #define remove_hash_entry hashtable_remove
1667 unsigned int get_hash_from_key(void *key)
1672 This algorithm (k=33) was first reported by Dan Bernstein many years ago in
1673 'comp.lang.c'. Another version of this algorithm (now favored by Bernstein)
1674 uses XOR: hash(i) = hash(i - 1) * 33 ^ str[i]; the magic of number 33 (why
1675 it works better than many other constants, prime or not) has never been
1676 adequately explained.
1678 If you just want to have a good hash function, and cannot wait, djb2
1679 is one of the best string hash functions i know. It has excellent
1680 distribution and speed on many different sets of keys and table sizes.
1681 You are not likely to do better with one of the "well known" functions
1682 such as PJW, K&R, etc.
1684 Ozan (oz) Yigit [http://www.cs.yorku.ca/~oz/hash.html]
1687 char *str = (char *)key;
1688 unsigned int hash = 5381;
1691 while ((c = *str++))
1692 hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
1697 static int keys_are_equal(void *key1, void *key2)
1699 return (strEqual((char *)key1, (char *)key2));
1702 SetupFileHash *newSetupFileHash()
1704 SetupFileHash *new_hash =
1705 create_hashtable(16, 0.75, get_hash_from_key, keys_are_equal);
1707 if (new_hash == NULL)
1708 Error(ERR_EXIT, "create_hashtable() failed -- out of memory");
1713 void freeSetupFileHash(SetupFileHash *hash)
1718 hashtable_destroy(hash, 1); /* 1 == also free values stored in hash */
1721 char *getHashEntry(SetupFileHash *hash, char *token)
1726 return search_hash_entry(hash, token);
1729 void setHashEntry(SetupFileHash *hash, char *token, char *value)
1736 value_copy = getStringCopy(value);
1738 /* change value; if it does not exist, insert it as new */
1739 if (!change_hash_entry(hash, token, value_copy))
1740 if (!insert_hash_entry(hash, getStringCopy(token), value_copy))
1741 Error(ERR_EXIT, "cannot insert into hash -- aborting");
1744 char *removeHashEntry(SetupFileHash *hash, char *token)
1749 return remove_hash_entry(hash, token);
1753 static void printSetupFileHash(SetupFileHash *hash)
1755 BEGIN_HASH_ITERATION(hash, itr)
1757 printf("token: '%s'\n", HASH_ITERATION_TOKEN(itr));
1758 printf("value: '%s'\n", HASH_ITERATION_VALUE(itr));
1760 END_HASH_ITERATION(hash, itr)
1764 #define ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE 1
1765 #define CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING 0
1766 #define CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH 0
1768 static boolean token_value_separator_found = FALSE;
1769 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
1770 static boolean token_value_separator_warning = FALSE;
1772 #if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
1773 static boolean token_already_exists_warning = FALSE;
1776 static boolean getTokenValueFromSetupLineExt(char *line,
1777 char **token_ptr, char **value_ptr,
1778 char *filename, char *line_raw,
1780 boolean separator_required)
1782 static char line_copy[MAX_LINE_LEN + 1], line_raw_copy[MAX_LINE_LEN + 1];
1783 char *token, *value, *line_ptr;
1785 /* when externally invoked via ReadTokenValueFromLine(), copy line buffers */
1786 if (line_raw == NULL)
1788 strncpy(line_copy, line, MAX_LINE_LEN);
1789 line_copy[MAX_LINE_LEN] = '\0';
1792 strcpy(line_raw_copy, line_copy);
1793 line_raw = line_raw_copy;
1796 /* cut trailing comment from input line */
1797 for (line_ptr = line; *line_ptr; line_ptr++)
1799 if (*line_ptr == '#')
1806 /* cut trailing whitespaces from input line */
1807 for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
1808 if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
1811 /* ignore empty lines */
1815 /* cut leading whitespaces from token */
1816 for (token = line; *token; token++)
1817 if (*token != ' ' && *token != '\t')
1820 /* start with empty value as reliable default */
1823 token_value_separator_found = FALSE;
1825 /* find end of token to determine start of value */
1826 for (line_ptr = token; *line_ptr; line_ptr++)
1829 /* first look for an explicit token/value separator, like ':' or '=' */
1830 if (*line_ptr == ':' || *line_ptr == '=')
1832 if (*line_ptr == ' ' || *line_ptr == '\t' || *line_ptr == ':')
1835 *line_ptr = '\0'; /* terminate token string */
1836 value = line_ptr + 1; /* set beginning of value */
1838 token_value_separator_found = TRUE;
1844 #if ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE
1845 /* fallback: if no token/value separator found, also allow whitespaces */
1846 if (!token_value_separator_found && !separator_required)
1848 for (line_ptr = token; *line_ptr; line_ptr++)
1850 if (*line_ptr == ' ' || *line_ptr == '\t')
1852 *line_ptr = '\0'; /* terminate token string */
1853 value = line_ptr + 1; /* set beginning of value */
1855 token_value_separator_found = TRUE;
1861 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
1862 if (token_value_separator_found)
1864 if (!token_value_separator_warning)
1866 Error(ERR_INFO_LINE, "-");
1868 if (filename != NULL)
1870 Error(ERR_WARN, "missing token/value separator(s) in config file:");
1871 Error(ERR_INFO, "- config file: '%s'", filename);
1875 Error(ERR_WARN, "missing token/value separator(s):");
1878 token_value_separator_warning = TRUE;
1881 if (filename != NULL)
1882 Error(ERR_INFO, "- line %d: '%s'", line_nr, line_raw);
1884 Error(ERR_INFO, "- line: '%s'", line_raw);
1890 /* cut trailing whitespaces from token */
1891 for (line_ptr = &token[strlen(token)]; line_ptr >= token; line_ptr--)
1892 if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
1895 /* cut leading whitespaces from value */
1896 for (; *value; value++)
1897 if (*value != ' ' && *value != '\t')
1902 value = "true"; /* treat tokens without value as "true" */
1911 boolean getTokenValueFromSetupLine(char *line, char **token, char **value)
1913 /* while the internal (old) interface does not require a token/value
1914 separator (for downwards compatibility with existing files which
1915 don't use them), it is mandatory for the external (new) interface */
1917 return getTokenValueFromSetupLineExt(line, token, value, NULL, NULL, 0, TRUE);
1921 static boolean loadSetupFileData(void *setup_file_data, char *filename,
1922 boolean top_recursion_level, boolean is_hash)
1924 static SetupFileHash *include_filename_hash = NULL;
1925 char line[MAX_LINE_LEN], line_raw[MAX_LINE_LEN], previous_line[MAX_LINE_LEN];
1926 char *token, *value, *line_ptr;
1927 void *insert_ptr = NULL;
1928 boolean read_continued_line = FALSE;
1930 int line_nr = 0, token_count = 0, include_count = 0;
1932 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
1933 token_value_separator_warning = FALSE;
1936 #if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
1937 token_already_exists_warning = FALSE;
1940 if (!(file = fopen(filename, MODE_READ)))
1942 Error(ERR_WARN, "cannot open configuration file '%s'", filename);
1947 /* use "insert pointer" to store list end for constant insertion complexity */
1949 insert_ptr = setup_file_data;
1951 /* on top invocation, create hash to mark included files (to prevent loops) */
1952 if (top_recursion_level)
1953 include_filename_hash = newSetupFileHash();
1955 /* mark this file as already included (to prevent including it again) */
1956 setHashEntry(include_filename_hash, getBaseNamePtr(filename), "true");
1960 /* read next line of input file */
1961 if (!fgets(line, MAX_LINE_LEN, file))
1964 /* check if line was completely read and is terminated by line break */
1965 if (strlen(line) > 0 && line[strlen(line) - 1] == '\n')
1968 /* cut trailing line break (this can be newline and/or carriage return) */
1969 for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
1970 if ((*line_ptr == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
1973 /* copy raw input line for later use (mainly debugging output) */
1974 strcpy(line_raw, line);
1976 if (read_continued_line)
1979 /* !!! ??? WHY ??? !!! */
1980 /* cut leading whitespaces from input line */
1981 for (line_ptr = line; *line_ptr; line_ptr++)
1982 if (*line_ptr != ' ' && *line_ptr != '\t')
1986 /* append new line to existing line, if there is enough space */
1987 if (strlen(previous_line) + strlen(line_ptr) < MAX_LINE_LEN)
1988 strcat(previous_line, line_ptr);
1990 strcpy(line, previous_line); /* copy storage buffer to line */
1992 read_continued_line = FALSE;
1995 /* if the last character is '\', continue at next line */
1996 if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
1998 line[strlen(line) - 1] = '\0'; /* cut off trailing backslash */
1999 strcpy(previous_line, line); /* copy line to storage buffer */
2001 read_continued_line = TRUE;
2006 if (!getTokenValueFromSetupLineExt(line, &token, &value, filename,
2007 line_raw, line_nr, FALSE))
2012 if (strEqual(token, "include"))
2014 if (getHashEntry(include_filename_hash, value) == NULL)
2016 char *basepath = getBasePath(filename);
2017 char *basename = getBaseName(value);
2018 char *filename_include = getPath2(basepath, basename);
2021 Error(ERR_INFO, "[including file '%s']", filename_include);
2024 loadSetupFileData(setup_file_data, filename_include, FALSE, is_hash);
2028 free(filename_include);
2034 Error(ERR_WARN, "ignoring already processed file '%s'", value);
2041 #if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
2043 getHashEntry((SetupFileHash *)setup_file_data, token);
2045 if (old_value != NULL)
2047 if (!token_already_exists_warning)
2049 Error(ERR_INFO_LINE, "-");
2050 Error(ERR_WARN, "duplicate token(s) found in config file:");
2051 Error(ERR_INFO, "- config file: '%s'", filename);
2053 token_already_exists_warning = TRUE;
2056 Error(ERR_INFO, "- token: '%s' (in line %d)", token, line_nr);
2057 Error(ERR_INFO, " old value: '%s'", old_value);
2058 Error(ERR_INFO, " new value: '%s'", value);
2062 setHashEntry((SetupFileHash *)setup_file_data, token, value);
2066 insert_ptr = addListEntry((SetupFileList *)insert_ptr, token, value);
2076 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
2077 if (token_value_separator_warning)
2078 Error(ERR_INFO_LINE, "-");
2081 #if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
2082 if (token_already_exists_warning)
2083 Error(ERR_INFO_LINE, "-");
2086 if (token_count == 0 && include_count == 0)
2087 Error(ERR_WARN, "configuration file '%s' is empty", filename);
2089 if (top_recursion_level)
2090 freeSetupFileHash(include_filename_hash);
2097 static boolean loadSetupFileData(void *setup_file_data, char *filename,
2098 boolean top_recursion_level, boolean is_hash)
2100 static SetupFileHash *include_filename_hash = NULL;
2101 char line[MAX_LINE_LEN], line_raw[MAX_LINE_LEN], previous_line[MAX_LINE_LEN];
2102 char *token, *value, *line_ptr;
2103 void *insert_ptr = NULL;
2104 boolean read_continued_line = FALSE;
2107 int token_count = 0;
2109 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
2110 token_value_separator_warning = FALSE;
2113 if (!(file = fopen(filename, MODE_READ)))
2115 Error(ERR_WARN, "cannot open configuration file '%s'", filename);
2120 /* use "insert pointer" to store list end for constant insertion complexity */
2122 insert_ptr = setup_file_data;
2124 /* on top invocation, create hash to mark included files (to prevent loops) */
2125 if (top_recursion_level)
2126 include_filename_hash = newSetupFileHash();
2128 /* mark this file as already included (to prevent including it again) */
2129 setHashEntry(include_filename_hash, getBaseNamePtr(filename), "true");
2133 /* read next line of input file */
2134 if (!fgets(line, MAX_LINE_LEN, file))
2137 /* check if line was completely read and is terminated by line break */
2138 if (strlen(line) > 0 && line[strlen(line) - 1] == '\n')
2141 /* cut trailing line break (this can be newline and/or carriage return) */
2142 for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
2143 if ((*line_ptr == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
2146 /* copy raw input line for later use (mainly debugging output) */
2147 strcpy(line_raw, line);
2149 if (read_continued_line)
2151 /* cut leading whitespaces from input line */
2152 for (line_ptr = line; *line_ptr; line_ptr++)
2153 if (*line_ptr != ' ' && *line_ptr != '\t')
2156 /* append new line to existing line, if there is enough space */
2157 if (strlen(previous_line) + strlen(line_ptr) < MAX_LINE_LEN)
2158 strcat(previous_line, line_ptr);
2160 strcpy(line, previous_line); /* copy storage buffer to line */
2162 read_continued_line = FALSE;
2165 /* if the last character is '\', continue at next line */
2166 if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
2168 line[strlen(line) - 1] = '\0'; /* cut off trailing backslash */
2169 strcpy(previous_line, line); /* copy line to storage buffer */
2171 read_continued_line = TRUE;
2176 /* cut trailing comment from input line */
2177 for (line_ptr = line; *line_ptr; line_ptr++)
2179 if (*line_ptr == '#')
2186 /* cut trailing whitespaces from input line */
2187 for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
2188 if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
2191 /* ignore empty lines */
2195 /* cut leading whitespaces from token */
2196 for (token = line; *token; token++)
2197 if (*token != ' ' && *token != '\t')
2200 /* start with empty value as reliable default */
2203 token_value_separator_found = FALSE;
2205 /* find end of token to determine start of value */
2206 for (line_ptr = token; *line_ptr; line_ptr++)
2209 /* first look for an explicit token/value separator, like ':' or '=' */
2210 if (*line_ptr == ':' || *line_ptr == '=')
2212 if (*line_ptr == ' ' || *line_ptr == '\t' || *line_ptr == ':')
2215 *line_ptr = '\0'; /* terminate token string */
2216 value = line_ptr + 1; /* set beginning of value */
2218 token_value_separator_found = TRUE;
2224 #if ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE
2225 /* fallback: if no token/value separator found, also allow whitespaces */
2226 if (!token_value_separator_found)
2228 for (line_ptr = token; *line_ptr; line_ptr++)
2230 if (*line_ptr == ' ' || *line_ptr == '\t')
2232 *line_ptr = '\0'; /* terminate token string */
2233 value = line_ptr + 1; /* set beginning of value */
2235 token_value_separator_found = TRUE;
2241 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
2242 if (token_value_separator_found)
2244 if (!token_value_separator_warning)
2246 Error(ERR_INFO_LINE, "-");
2247 Error(ERR_WARN, "missing token/value separator(s) in config file:");
2248 Error(ERR_INFO, "- config file: '%s'", filename);
2250 token_value_separator_warning = TRUE;
2253 Error(ERR_INFO, "- line %d: '%s'", line_nr, line_raw);
2259 /* cut trailing whitespaces from token */
2260 for (line_ptr = &token[strlen(token)]; line_ptr >= token; line_ptr--)
2261 if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
2264 /* cut leading whitespaces from value */
2265 for (; *value; value++)
2266 if (*value != ' ' && *value != '\t')
2271 value = "true"; /* treat tokens without value as "true" */
2276 if (strEqual(token, "include"))
2278 if (getHashEntry(include_filename_hash, value) == NULL)
2280 char *basepath = getBasePath(filename);
2281 char *basename = getBaseName(value);
2282 char *filename_include = getPath2(basepath, basename);
2285 Error(ERR_INFO, "[including file '%s']", filename_include);
2288 loadSetupFileData(setup_file_data, filename_include, FALSE, is_hash);
2292 free(filename_include);
2296 Error(ERR_WARN, "ignoring already processed file '%s'", value);
2302 setHashEntry((SetupFileHash *)setup_file_data, token, value);
2304 insert_ptr = addListEntry((SetupFileList *)insert_ptr, token, value);
2313 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
2314 if (token_value_separator_warning)
2315 Error(ERR_INFO_LINE, "-");
2318 if (token_count == 0)
2319 Error(ERR_WARN, "configuration file '%s' is empty", filename);
2321 if (top_recursion_level)
2322 freeSetupFileHash(include_filename_hash);
2328 void saveSetupFileHash(SetupFileHash *hash, char *filename)
2332 if (!(file = fopen(filename, MODE_WRITE)))
2334 Error(ERR_WARN, "cannot write configuration file '%s'", filename);
2339 BEGIN_HASH_ITERATION(hash, itr)
2341 fprintf(file, "%s\n", getFormattedSetupEntry(HASH_ITERATION_TOKEN(itr),
2342 HASH_ITERATION_VALUE(itr)));
2344 END_HASH_ITERATION(hash, itr)
2349 SetupFileList *loadSetupFileList(char *filename)
2351 SetupFileList *setup_file_list = newSetupFileList("", "");
2352 SetupFileList *first_valid_list_entry;
2354 if (!loadSetupFileData(setup_file_list, filename, TRUE, FALSE))
2356 freeSetupFileList(setup_file_list);
2361 first_valid_list_entry = setup_file_list->next;
2363 /* free empty list header */
2364 setup_file_list->next = NULL;
2365 freeSetupFileList(setup_file_list);
2367 return first_valid_list_entry;
2370 SetupFileHash *loadSetupFileHash(char *filename)
2372 SetupFileHash *setup_file_hash = newSetupFileHash();
2374 if (!loadSetupFileData(setup_file_hash, filename, TRUE, TRUE))
2376 freeSetupFileHash(setup_file_hash);
2381 return setup_file_hash;
2384 void checkSetupFileHashIdentifier(SetupFileHash *setup_file_hash,
2385 char *filename, char *identifier)
2387 char *value = getHashEntry(setup_file_hash, TOKEN_STR_FILE_IDENTIFIER);
2390 Error(ERR_WARN, "config file '%s' has no file identifier", filename);
2391 else if (!checkCookieString(value, identifier))
2392 Error(ERR_WARN, "config file '%s' has wrong file identifier", filename);
2396 /* ========================================================================= */
2397 /* setup file stuff */
2398 /* ========================================================================= */
2400 #define TOKEN_STR_LAST_LEVEL_SERIES "last_level_series"
2401 #define TOKEN_STR_LAST_PLAYED_LEVEL "last_played_level"
2402 #define TOKEN_STR_HANDICAP_LEVEL "handicap_level"
2404 /* level directory info */
2405 #define LEVELINFO_TOKEN_IDENTIFIER 0
2406 #define LEVELINFO_TOKEN_NAME 1
2407 #define LEVELINFO_TOKEN_NAME_SORTING 2
2408 #define LEVELINFO_TOKEN_AUTHOR 3
2409 #define LEVELINFO_TOKEN_YEAR 4
2410 #define LEVELINFO_TOKEN_IMPORTED_FROM 5
2411 #define LEVELINFO_TOKEN_IMPORTED_BY 6
2412 #define LEVELINFO_TOKEN_TESTED_BY 7
2413 #define LEVELINFO_TOKEN_LEVELS 8
2414 #define LEVELINFO_TOKEN_FIRST_LEVEL 9
2415 #define LEVELINFO_TOKEN_SORT_PRIORITY 10
2416 #define LEVELINFO_TOKEN_LATEST_ENGINE 11
2417 #define LEVELINFO_TOKEN_LEVEL_GROUP 12
2418 #define LEVELINFO_TOKEN_READONLY 13
2419 #define LEVELINFO_TOKEN_GRAPHICS_SET_ECS 14
2420 #define LEVELINFO_TOKEN_GRAPHICS_SET_AGA 15
2421 #define LEVELINFO_TOKEN_GRAPHICS_SET 16
2422 #define LEVELINFO_TOKEN_SOUNDS_SET 17
2423 #define LEVELINFO_TOKEN_MUSIC_SET 18
2424 #define LEVELINFO_TOKEN_FILENAME 19
2425 #define LEVELINFO_TOKEN_FILETYPE 20
2426 #define LEVELINFO_TOKEN_SPECIAL_FLAGS 21
2427 #define LEVELINFO_TOKEN_HANDICAP 22
2428 #define LEVELINFO_TOKEN_SKIP_LEVELS 23
2430 #define NUM_LEVELINFO_TOKENS 24
2432 static LevelDirTree ldi;
2434 static struct TokenInfo levelinfo_tokens[] =
2436 /* level directory info */
2437 { TYPE_STRING, &ldi.identifier, "identifier" },
2438 { TYPE_STRING, &ldi.name, "name" },
2439 { TYPE_STRING, &ldi.name_sorting, "name_sorting" },
2440 { TYPE_STRING, &ldi.author, "author" },
2441 { TYPE_STRING, &ldi.year, "year" },
2442 { TYPE_STRING, &ldi.imported_from, "imported_from" },
2443 { TYPE_STRING, &ldi.imported_by, "imported_by" },
2444 { TYPE_STRING, &ldi.tested_by, "tested_by" },
2445 { TYPE_INTEGER, &ldi.levels, "levels" },
2446 { TYPE_INTEGER, &ldi.first_level, "first_level" },
2447 { TYPE_INTEGER, &ldi.sort_priority, "sort_priority" },
2448 { TYPE_BOOLEAN, &ldi.latest_engine, "latest_engine" },
2449 { TYPE_BOOLEAN, &ldi.level_group, "level_group" },
2450 { TYPE_BOOLEAN, &ldi.readonly, "readonly" },
2451 { TYPE_STRING, &ldi.graphics_set_ecs, "graphics_set.ecs" },
2452 { TYPE_STRING, &ldi.graphics_set_aga, "graphics_set.aga" },
2453 { TYPE_STRING, &ldi.graphics_set, "graphics_set" },
2454 { TYPE_STRING, &ldi.sounds_set, "sounds_set" },
2455 { TYPE_STRING, &ldi.music_set, "music_set" },
2456 { TYPE_STRING, &ldi.level_filename, "filename" },
2457 { TYPE_STRING, &ldi.level_filetype, "filetype" },
2458 { TYPE_STRING, &ldi.special_flags, "special_flags" },
2459 { TYPE_BOOLEAN, &ldi.handicap, "handicap" },
2460 { TYPE_BOOLEAN, &ldi.skip_levels, "skip_levels" }
2463 static struct TokenInfo artworkinfo_tokens[] =
2465 /* artwork directory info */
2466 { TYPE_STRING, &ldi.identifier, "identifier" },
2467 { TYPE_STRING, &ldi.subdir, "subdir" },
2468 { TYPE_STRING, &ldi.name, "name" },
2469 { TYPE_STRING, &ldi.name_sorting, "name_sorting" },
2470 { TYPE_STRING, &ldi.author, "author" },
2471 { TYPE_INTEGER, &ldi.sort_priority, "sort_priority" },
2472 { TYPE_STRING, &ldi.basepath, "basepath" },
2473 { TYPE_STRING, &ldi.fullpath, "fullpath" },
2474 { TYPE_BOOLEAN, &ldi.in_user_dir, "in_user_dir" },
2475 { TYPE_INTEGER, &ldi.color, "color" },
2476 { TYPE_STRING, &ldi.class_desc, "class_desc" },
2481 static void setTreeInfoToDefaults(TreeInfo *ti, int type)
2485 ti->node_top = (ti->type == TREE_TYPE_LEVEL_DIR ? &leveldir_first :
2486 ti->type == TREE_TYPE_GRAPHICS_DIR ? &artwork.gfx_first :
2487 ti->type == TREE_TYPE_SOUNDS_DIR ? &artwork.snd_first :
2488 ti->type == TREE_TYPE_MUSIC_DIR ? &artwork.mus_first :
2491 ti->node_parent = NULL;
2492 ti->node_group = NULL;
2499 ti->fullpath = NULL;
2500 ti->basepath = NULL;
2501 ti->identifier = NULL;
2502 ti->name = getStringCopy(ANONYMOUS_NAME);
2503 ti->name_sorting = NULL;
2504 ti->author = getStringCopy(ANONYMOUS_NAME);
2507 ti->sort_priority = LEVELCLASS_UNDEFINED; /* default: least priority */
2508 ti->latest_engine = FALSE; /* default: get from level */
2509 ti->parent_link = FALSE;
2510 ti->in_user_dir = FALSE;
2511 ti->user_defined = FALSE;
2513 ti->class_desc = NULL;
2515 ti->infotext = getStringCopy(TREE_INFOTEXT(ti->type));
2517 if (ti->type == TREE_TYPE_LEVEL_DIR)
2519 ti->imported_from = NULL;
2520 ti->imported_by = NULL;
2521 ti->tested_by = NULL;
2523 ti->graphics_set_ecs = NULL;
2524 ti->graphics_set_aga = NULL;
2525 ti->graphics_set = NULL;
2526 ti->sounds_set = NULL;
2527 ti->music_set = NULL;
2528 ti->graphics_path = getStringCopy(UNDEFINED_FILENAME);
2529 ti->sounds_path = getStringCopy(UNDEFINED_FILENAME);
2530 ti->music_path = getStringCopy(UNDEFINED_FILENAME);
2532 ti->level_filename = NULL;
2533 ti->level_filetype = NULL;
2535 ti->special_flags = NULL;
2538 ti->first_level = 0;
2540 ti->level_group = FALSE;
2541 ti->handicap_level = 0;
2542 ti->readonly = TRUE;
2543 ti->handicap = TRUE;
2544 ti->skip_levels = FALSE;
2548 static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
2552 Error(ERR_WARN, "setTreeInfoToDefaultsFromParent(): parent == NULL");
2554 setTreeInfoToDefaults(ti, TREE_TYPE_UNDEFINED);
2559 /* copy all values from the parent structure */
2561 ti->type = parent->type;
2563 ti->node_top = parent->node_top;
2564 ti->node_parent = parent;
2565 ti->node_group = NULL;
2572 ti->fullpath = NULL;
2573 ti->basepath = NULL;
2574 ti->identifier = NULL;
2575 ti->name = getStringCopy(ANONYMOUS_NAME);
2576 ti->name_sorting = NULL;
2577 ti->author = getStringCopy(parent->author);
2578 ti->year = getStringCopy(parent->year);
2580 ti->sort_priority = parent->sort_priority;
2581 ti->latest_engine = parent->latest_engine;
2582 ti->parent_link = FALSE;
2583 ti->in_user_dir = parent->in_user_dir;
2584 ti->user_defined = parent->user_defined;
2585 ti->color = parent->color;
2586 ti->class_desc = getStringCopy(parent->class_desc);
2588 ti->infotext = getStringCopy(parent->infotext);
2590 if (ti->type == TREE_TYPE_LEVEL_DIR)
2592 ti->imported_from = getStringCopy(parent->imported_from);
2593 ti->imported_by = getStringCopy(parent->imported_by);
2594 ti->tested_by = getStringCopy(parent->tested_by);
2596 ti->graphics_set_ecs = NULL;
2597 ti->graphics_set_aga = NULL;
2598 ti->graphics_set = NULL;
2599 ti->sounds_set = NULL;
2600 ti->music_set = NULL;
2601 ti->graphics_path = getStringCopy(UNDEFINED_FILENAME);
2602 ti->sounds_path = getStringCopy(UNDEFINED_FILENAME);
2603 ti->music_path = getStringCopy(UNDEFINED_FILENAME);
2605 ti->level_filename = NULL;
2606 ti->level_filetype = NULL;
2608 ti->special_flags = getStringCopy(parent->special_flags);
2611 ti->first_level = 0;
2613 ti->level_group = FALSE;
2614 ti->handicap_level = 0;
2616 ti->readonly = parent->readonly;
2618 ti->readonly = TRUE;
2620 ti->handicap = TRUE;
2621 ti->skip_levels = FALSE;
2625 static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
2627 TreeInfo *ti_copy = newTreeInfo();
2629 /* copy all values from the original structure */
2631 ti_copy->type = ti->type;
2633 ti_copy->node_top = ti->node_top;
2634 ti_copy->node_parent = ti->node_parent;
2635 ti_copy->node_group = ti->node_group;
2636 ti_copy->next = ti->next;
2638 ti_copy->cl_first = ti->cl_first;
2639 ti_copy->cl_cursor = ti->cl_cursor;
2641 ti_copy->subdir = getStringCopy(ti->subdir);
2642 ti_copy->fullpath = getStringCopy(ti->fullpath);
2643 ti_copy->basepath = getStringCopy(ti->basepath);
2644 ti_copy->identifier = getStringCopy(ti->identifier);
2645 ti_copy->name = getStringCopy(ti->name);
2646 ti_copy->name_sorting = getStringCopy(ti->name_sorting);
2647 ti_copy->author = getStringCopy(ti->author);
2648 ti_copy->year = getStringCopy(ti->year);
2649 ti_copy->imported_from = getStringCopy(ti->imported_from);
2650 ti_copy->imported_by = getStringCopy(ti->imported_by);
2651 ti_copy->tested_by = getStringCopy(ti->tested_by);
2653 ti_copy->graphics_set_ecs = getStringCopy(ti->graphics_set_ecs);
2654 ti_copy->graphics_set_aga = getStringCopy(ti->graphics_set_aga);
2655 ti_copy->graphics_set = getStringCopy(ti->graphics_set);
2656 ti_copy->sounds_set = getStringCopy(ti->sounds_set);
2657 ti_copy->music_set = getStringCopy(ti->music_set);
2658 ti_copy->graphics_path = getStringCopy(ti->graphics_path);
2659 ti_copy->sounds_path = getStringCopy(ti->sounds_path);
2660 ti_copy->music_path = getStringCopy(ti->music_path);
2662 ti_copy->level_filename = getStringCopy(ti->level_filename);
2663 ti_copy->level_filetype = getStringCopy(ti->level_filetype);
2665 ti_copy->special_flags = getStringCopy(ti->special_flags);
2667 ti_copy->levels = ti->levels;
2668 ti_copy->first_level = ti->first_level;
2669 ti_copy->last_level = ti->last_level;
2670 ti_copy->sort_priority = ti->sort_priority;
2672 ti_copy->latest_engine = ti->latest_engine;
2674 ti_copy->level_group = ti->level_group;
2675 ti_copy->parent_link = ti->parent_link;
2676 ti_copy->in_user_dir = ti->in_user_dir;
2677 ti_copy->user_defined = ti->user_defined;
2678 ti_copy->readonly = ti->readonly;
2679 ti_copy->handicap = ti->handicap;
2680 ti_copy->skip_levels = ti->skip_levels;
2682 ti_copy->color = ti->color;
2683 ti_copy->class_desc = getStringCopy(ti->class_desc);
2684 ti_copy->handicap_level = ti->handicap_level;
2686 ti_copy->infotext = getStringCopy(ti->infotext);
2691 void freeTreeInfo(TreeInfo *ti)
2696 checked_free(ti->subdir);
2697 checked_free(ti->fullpath);
2698 checked_free(ti->basepath);
2699 checked_free(ti->identifier);
2701 checked_free(ti->name);
2702 checked_free(ti->name_sorting);
2703 checked_free(ti->author);
2704 checked_free(ti->year);
2706 checked_free(ti->class_desc);
2708 checked_free(ti->infotext);
2710 if (ti->type == TREE_TYPE_LEVEL_DIR)
2712 checked_free(ti->imported_from);
2713 checked_free(ti->imported_by);
2714 checked_free(ti->tested_by);
2716 checked_free(ti->graphics_set_ecs);
2717 checked_free(ti->graphics_set_aga);
2718 checked_free(ti->graphics_set);
2719 checked_free(ti->sounds_set);
2720 checked_free(ti->music_set);
2722 checked_free(ti->graphics_path);
2723 checked_free(ti->sounds_path);
2724 checked_free(ti->music_path);
2726 checked_free(ti->level_filename);
2727 checked_free(ti->level_filetype);
2729 checked_free(ti->special_flags);
2735 void setSetupInfo(struct TokenInfo *token_info,
2736 int token_nr, char *token_value)
2738 int token_type = token_info[token_nr].type;
2739 void *setup_value = token_info[token_nr].value;
2741 if (token_value == NULL)
2744 /* set setup field to corresponding token value */
2749 *(boolean *)setup_value = get_boolean_from_string(token_value);
2753 *(int *)setup_value = get_switch3_from_string(token_value);
2757 *(Key *)setup_value = getKeyFromKeyName(token_value);
2761 *(Key *)setup_value = getKeyFromX11KeyName(token_value);
2765 *(int *)setup_value = get_integer_from_string(token_value);
2769 checked_free(*(char **)setup_value);
2770 *(char **)setup_value = getStringCopy(token_value);
2778 static int compareTreeInfoEntries(const void *object1, const void *object2)
2780 const TreeInfo *entry1 = *((TreeInfo **)object1);
2781 const TreeInfo *entry2 = *((TreeInfo **)object2);
2782 int class_sorting1 = 0, class_sorting2 = 0;
2785 if (entry1->type == TREE_TYPE_LEVEL_DIR)
2787 class_sorting1 = LEVELSORTING(entry1);
2788 class_sorting2 = LEVELSORTING(entry2);
2790 else if (entry1->type == TREE_TYPE_GRAPHICS_DIR ||
2791 entry1->type == TREE_TYPE_SOUNDS_DIR ||
2792 entry1->type == TREE_TYPE_MUSIC_DIR)
2794 class_sorting1 = ARTWORKSORTING(entry1);
2795 class_sorting2 = ARTWORKSORTING(entry2);
2798 if (entry1->parent_link || entry2->parent_link)
2799 compare_result = (entry1->parent_link ? -1 : +1);
2800 else if (entry1->sort_priority == entry2->sort_priority)
2802 char *name1 = getStringToLower(entry1->name_sorting);
2803 char *name2 = getStringToLower(entry2->name_sorting);
2805 compare_result = strcmp(name1, name2);
2810 else if (class_sorting1 == class_sorting2)
2811 compare_result = entry1->sort_priority - entry2->sort_priority;
2813 compare_result = class_sorting1 - class_sorting2;
2815 return compare_result;
2818 static void createParentTreeInfoNode(TreeInfo *node_parent)
2822 if (node_parent == NULL)
2825 ti_new = newTreeInfo();
2826 setTreeInfoToDefaults(ti_new, node_parent->type);
2828 ti_new->node_parent = node_parent;
2829 ti_new->parent_link = TRUE;
2831 setString(&ti_new->identifier, node_parent->identifier);
2832 setString(&ti_new->name, ".. (parent directory)");
2833 setString(&ti_new->name_sorting, ti_new->name);
2835 setString(&ti_new->subdir, "..");
2836 setString(&ti_new->fullpath, node_parent->fullpath);
2838 ti_new->sort_priority = node_parent->sort_priority;
2839 ti_new->latest_engine = node_parent->latest_engine;
2841 setString(&ti_new->class_desc, getLevelClassDescription(ti_new));
2843 pushTreeInfo(&node_parent->node_group, ti_new);
2847 /* -------------------------------------------------------------------------- */
2848 /* functions for handling level and custom artwork info cache */
2849 /* -------------------------------------------------------------------------- */
2851 static void LoadArtworkInfoCache()
2853 InitCacheDirectory();
2855 if (artworkinfo_cache_old == NULL)
2857 char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
2859 /* try to load artwork info hash from already existing cache file */
2860 artworkinfo_cache_old = loadSetupFileHash(filename);
2862 /* if no artwork info cache file was found, start with empty hash */
2863 if (artworkinfo_cache_old == NULL)
2864 artworkinfo_cache_old = newSetupFileHash();
2869 if (artworkinfo_cache_new == NULL)
2870 artworkinfo_cache_new = newSetupFileHash();
2873 static void SaveArtworkInfoCache()
2875 char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
2877 InitCacheDirectory();
2879 saveSetupFileHash(artworkinfo_cache_new, filename);
2884 static char *getCacheTokenPrefix(char *prefix1, char *prefix2)
2886 static char *prefix = NULL;
2888 checked_free(prefix);
2890 prefix = getStringCat2WithSeparator(prefix1, prefix2, ".");
2895 /* (identical to above function, but separate string buffer needed -- nasty) */
2896 static char *getCacheToken(char *prefix, char *suffix)
2898 static char *token = NULL;
2900 checked_free(token);
2902 token = getStringCat2WithSeparator(prefix, suffix, ".");
2907 static char *getFileTimestampString(char *filename)
2910 return getStringCopy(i_to_a(getFileTimestampEpochSeconds(filename)));
2912 struct stat file_status;
2914 if (stat(filename, &file_status) != 0) /* cannot stat file */
2915 return getStringCopy(i_to_a(0));
2917 return getStringCopy(i_to_a(file_status.st_mtime));
2921 static boolean modifiedFileTimestamp(char *filename, char *timestamp_string)
2923 struct stat file_status;
2925 if (timestamp_string == NULL)
2928 if (stat(filename, &file_status) != 0) /* cannot stat file */
2931 return (file_status.st_mtime != atoi(timestamp_string));
2934 static TreeInfo *getArtworkInfoCacheEntry(LevelDirTree *level_node, int type)
2936 char *identifier = level_node->subdir;
2937 char *type_string = ARTWORK_DIRECTORY(type);
2938 char *token_prefix = getCacheTokenPrefix(type_string, identifier);
2939 char *token_main = getCacheToken(token_prefix, "CACHED");
2940 char *cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
2941 boolean cached = (cache_entry != NULL && strEqual(cache_entry, "true"));
2942 TreeInfo *artwork_info = NULL;
2944 if (!use_artworkinfo_cache)
2951 artwork_info = newTreeInfo();
2952 setTreeInfoToDefaults(artwork_info, type);
2954 /* set all structure fields according to the token/value pairs */
2955 ldi = *artwork_info;
2956 for (i = 0; artworkinfo_tokens[i].type != -1; i++)
2958 char *token = getCacheToken(token_prefix, artworkinfo_tokens[i].text);
2959 char *value = getHashEntry(artworkinfo_cache_old, token);
2961 setSetupInfo(artworkinfo_tokens, i, value);
2963 /* check if cache entry for this item is invalid or incomplete */
2967 Error(ERR_WARN, "cache entry '%s' invalid", token);
2974 *artwork_info = ldi;
2979 char *filename_levelinfo = getPath2(getLevelDirFromTreeInfo(level_node),
2980 LEVELINFO_FILENAME);
2981 char *filename_artworkinfo = getPath2(getSetupArtworkDir(artwork_info),
2982 ARTWORKINFO_FILENAME(type));
2984 /* check if corresponding "levelinfo.conf" file has changed */
2985 token_main = getCacheToken(token_prefix, "TIMESTAMP_LEVELINFO");
2986 cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
2988 if (modifiedFileTimestamp(filename_levelinfo, cache_entry))
2991 /* check if corresponding "<artworkinfo>.conf" file has changed */
2992 token_main = getCacheToken(token_prefix, "TIMESTAMP_ARTWORKINFO");
2993 cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
2995 if (modifiedFileTimestamp(filename_artworkinfo, cache_entry))
3000 printf("::: '%s': INVALIDATED FROM CACHE BY TIMESTAMP\n", identifier);
3003 checked_free(filename_levelinfo);
3004 checked_free(filename_artworkinfo);
3007 if (!cached && artwork_info != NULL)
3009 freeTreeInfo(artwork_info);
3014 return artwork_info;
3017 static void setArtworkInfoCacheEntry(TreeInfo *artwork_info,
3018 LevelDirTree *level_node, int type)
3020 char *identifier = level_node->subdir;
3021 char *type_string = ARTWORK_DIRECTORY(type);
3022 char *token_prefix = getCacheTokenPrefix(type_string, identifier);
3023 char *token_main = getCacheToken(token_prefix, "CACHED");
3024 boolean set_cache_timestamps = TRUE;
3027 setHashEntry(artworkinfo_cache_new, token_main, "true");
3029 if (set_cache_timestamps)
3031 char *filename_levelinfo = getPath2(getLevelDirFromTreeInfo(level_node),
3032 LEVELINFO_FILENAME);
3033 char *filename_artworkinfo = getPath2(getSetupArtworkDir(artwork_info),
3034 ARTWORKINFO_FILENAME(type));
3035 char *timestamp_levelinfo = getFileTimestampString(filename_levelinfo);
3036 char *timestamp_artworkinfo = getFileTimestampString(filename_artworkinfo);
3038 token_main = getCacheToken(token_prefix, "TIMESTAMP_LEVELINFO");
3039 setHashEntry(artworkinfo_cache_new, token_main, timestamp_levelinfo);
3041 token_main = getCacheToken(token_prefix, "TIMESTAMP_ARTWORKINFO");
3042 setHashEntry(artworkinfo_cache_new, token_main, timestamp_artworkinfo);
3044 checked_free(filename_levelinfo);
3045 checked_free(filename_artworkinfo);
3046 checked_free(timestamp_levelinfo);
3047 checked_free(timestamp_artworkinfo);
3050 ldi = *artwork_info;
3051 for (i = 0; artworkinfo_tokens[i].type != -1; i++)
3053 char *token = getCacheToken(token_prefix, artworkinfo_tokens[i].text);
3054 char *value = getSetupValue(artworkinfo_tokens[i].type,
3055 artworkinfo_tokens[i].value);
3057 setHashEntry(artworkinfo_cache_new, token, value);
3062 /* -------------------------------------------------------------------------- */
3063 /* functions for loading level info and custom artwork info */
3064 /* -------------------------------------------------------------------------- */
3066 /* forward declaration for recursive call by "LoadLevelInfoFromLevelDir()" */
3067 static void LoadLevelInfoFromLevelDir(TreeInfo **, TreeInfo *, char *);
3069 static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
3070 TreeInfo *node_parent,
3071 char *level_directory,
3072 char *directory_name)
3075 static unsigned int progress_delay = 0;
3076 unsigned int progress_delay_value = 100; /* (in milliseconds) */
3078 char *directory_path = getPath2(level_directory, directory_name);
3079 char *filename = getPath2(directory_path, LEVELINFO_FILENAME);
3080 SetupFileHash *setup_file_hash;
3081 LevelDirTree *leveldir_new = NULL;
3084 /* unless debugging, silently ignore directories without "levelinfo.conf" */
3085 if (!options.debug && !fileExists(filename))
3087 free(directory_path);
3093 setup_file_hash = loadSetupFileHash(filename);
3095 if (setup_file_hash == NULL)
3097 Error(ERR_WARN, "ignoring level directory '%s'", directory_path);
3099 free(directory_path);
3105 leveldir_new = newTreeInfo();
3108 setTreeInfoToDefaultsFromParent(leveldir_new, node_parent);
3110 setTreeInfoToDefaults(leveldir_new, TREE_TYPE_LEVEL_DIR);
3112 leveldir_new->subdir = getStringCopy(directory_name);
3114 checkSetupFileHashIdentifier(setup_file_hash, filename,
3115 getCookie("LEVELINFO"));
3117 /* set all structure fields according to the token/value pairs */
3118 ldi = *leveldir_new;
3119 for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
3120 setSetupInfo(levelinfo_tokens, i,
3121 getHashEntry(setup_file_hash, levelinfo_tokens[i].text));
3122 *leveldir_new = ldi;
3124 if (strEqual(leveldir_new->name, ANONYMOUS_NAME))
3125 setString(&leveldir_new->name, leveldir_new->subdir);
3127 if (leveldir_new->identifier == NULL)
3128 leveldir_new->identifier = getStringCopy(leveldir_new->subdir);
3130 if (leveldir_new->name_sorting == NULL)
3131 leveldir_new->name_sorting = getStringCopy(leveldir_new->name);
3133 if (node_parent == NULL) /* top level group */
3135 leveldir_new->basepath = getStringCopy(level_directory);
3136 leveldir_new->fullpath = getStringCopy(leveldir_new->subdir);
3138 else /* sub level group */
3140 leveldir_new->basepath = getStringCopy(node_parent->basepath);
3141 leveldir_new->fullpath = getPath2(node_parent->fullpath, directory_name);
3145 if (leveldir_new->levels < 1)
3146 leveldir_new->levels = 1;
3149 leveldir_new->last_level =
3150 leveldir_new->first_level + leveldir_new->levels - 1;
3152 leveldir_new->in_user_dir =
3153 (!strEqual(leveldir_new->basepath, options.level_directory));
3156 printf("::: '%s' -> %d\n",
3157 leveldir_new->identifier,
3158 leveldir_new->in_user_dir);
3161 /* adjust some settings if user's private level directory was detected */
3162 if (leveldir_new->sort_priority == LEVELCLASS_UNDEFINED &&
3163 leveldir_new->in_user_dir &&
3164 (strEqual(leveldir_new->subdir, getLoginName()) ||
3165 strEqual(leveldir_new->name, getLoginName()) ||
3166 strEqual(leveldir_new->author, getRealName())))
3168 leveldir_new->sort_priority = LEVELCLASS_PRIVATE_START;
3169 leveldir_new->readonly = FALSE;
3172 leveldir_new->user_defined =
3173 (leveldir_new->in_user_dir && IS_LEVELCLASS_PRIVATE(leveldir_new));
3175 leveldir_new->color = LEVELCOLOR(leveldir_new);
3177 setString(&leveldir_new->class_desc, getLevelClassDescription(leveldir_new));
3179 leveldir_new->handicap_level = /* set handicap to default value */
3180 (leveldir_new->user_defined || !leveldir_new->handicap ?
3181 leveldir_new->last_level : leveldir_new->first_level);
3185 DrawInitTextExt(leveldir_new->name, 150, FC_YELLOW,
3186 leveldir_new->level_group);
3188 if (leveldir_new->level_group ||
3189 DelayReached(&progress_delay, progress_delay_value))
3190 DrawInitText(leveldir_new->name, 150, FC_YELLOW);
3193 DrawInitText(leveldir_new->name, 150, FC_YELLOW);
3197 /* !!! don't skip sets without levels (else artwork base sets are missing) */
3199 if (leveldir_new->levels < 1 && !leveldir_new->level_group)
3201 /* skip level sets without levels (which are probably artwork base sets) */
3203 freeSetupFileHash(setup_file_hash);
3204 free(directory_path);
3212 pushTreeInfo(node_first, leveldir_new);
3214 freeSetupFileHash(setup_file_hash);
3216 if (leveldir_new->level_group)
3218 /* create node to link back to current level directory */
3219 createParentTreeInfoNode(leveldir_new);
3221 /* recursively step into sub-directory and look for more level series */
3222 LoadLevelInfoFromLevelDir(&leveldir_new->node_group,
3223 leveldir_new, directory_path);
3226 free(directory_path);
3232 static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
3233 TreeInfo *node_parent,
3234 char *level_directory)
3237 struct dirent *dir_entry;
3238 boolean valid_entry_found = FALSE;
3241 Error(ERR_INFO, "looking for levels in '%s' ...", level_directory);
3244 if ((dir = opendir(level_directory)) == NULL)
3246 Error(ERR_WARN, "cannot read level directory '%s'", level_directory);
3251 while ((dir_entry = readdir(dir)) != NULL) /* loop until last dir entry */
3253 struct stat file_status;
3254 char *directory_name = dir_entry->d_name;
3255 char *directory_path = getPath2(level_directory, directory_name);
3257 /* skip entries for current and parent directory */
3258 if (strEqual(directory_name, ".") ||
3259 strEqual(directory_name, ".."))
3261 free(directory_path);
3265 /* find out if directory entry is itself a directory */
3266 if (stat(directory_path, &file_status) != 0 || /* cannot stat file */
3267 (file_status.st_mode & S_IFMT) != S_IFDIR) /* not a directory */
3269 free(directory_path);
3273 free(directory_path);
3275 if (strEqual(directory_name, GRAPHICS_DIRECTORY) ||
3276 strEqual(directory_name, SOUNDS_DIRECTORY) ||
3277 strEqual(directory_name, MUSIC_DIRECTORY))
3280 valid_entry_found |= LoadLevelInfoFromLevelConf(node_first, node_parent,
3287 /* special case: top level directory may directly contain "levelinfo.conf" */
3288 if (node_parent == NULL && !valid_entry_found)
3290 /* check if this directory directly contains a file "levelinfo.conf" */
3291 valid_entry_found |= LoadLevelInfoFromLevelConf(node_first, node_parent,
3292 level_directory, ".");
3295 if (!valid_entry_found)
3296 Error(ERR_WARN, "cannot find any valid level series in directory '%s'",
3300 boolean AdjustGraphicsForEMC()
3302 boolean settings_changed = FALSE;
3304 settings_changed |= adjustTreeGraphicsForEMC(leveldir_first_all);
3305 settings_changed |= adjustTreeGraphicsForEMC(leveldir_first);
3307 return settings_changed;
3310 void LoadLevelInfo()
3312 InitUserLevelDirectory(getLoginName());
3314 DrawInitText("Loading level series", 120, FC_GREEN);
3316 LoadLevelInfoFromLevelDir(&leveldir_first, NULL, options.level_directory);
3317 LoadLevelInfoFromLevelDir(&leveldir_first, NULL, getUserLevelDir(NULL));
3319 /* after loading all level set information, clone the level directory tree
3320 and remove all level sets without levels (these may still contain artwork
3321 to be offered in the setup menu as "custom artwork", and are therefore
3322 checked for existing artwork in the function "LoadLevelArtworkInfo()") */
3323 leveldir_first_all = leveldir_first;
3324 cloneTree(&leveldir_first, leveldir_first_all, TRUE);
3326 AdjustGraphicsForEMC();
3328 /* before sorting, the first entries will be from the user directory */
3329 leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
3331 if (leveldir_first == NULL)
3332 Error(ERR_EXIT, "cannot find any valid level series in any directory");
3334 sortTreeInfo(&leveldir_first);
3337 dumpTreeInfo(leveldir_first, 0);
3341 static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
3342 TreeInfo *node_parent,
3343 char *base_directory,
3344 char *directory_name, int type)
3346 char *directory_path = getPath2(base_directory, directory_name);
3347 char *filename = getPath2(directory_path, ARTWORKINFO_FILENAME(type));
3348 SetupFileHash *setup_file_hash = NULL;
3349 TreeInfo *artwork_new = NULL;
3352 if (fileExists(filename))
3353 setup_file_hash = loadSetupFileHash(filename);
3355 if (setup_file_hash == NULL) /* no config file -- look for artwork files */
3358 struct dirent *dir_entry;
3359 boolean valid_file_found = FALSE;
3361 if ((dir = opendir(directory_path)) != NULL)
3363 while ((dir_entry = readdir(dir)) != NULL)
3365 char *entry_name = dir_entry->d_name;
3367 if (FileIsArtworkType(entry_name, type))
3369 valid_file_found = TRUE;
3377 if (!valid_file_found)
3379 if (!strEqual(directory_name, "."))
3380 Error(ERR_WARN, "ignoring artwork directory '%s'", directory_path);
3382 free(directory_path);
3389 artwork_new = newTreeInfo();
3392 setTreeInfoToDefaultsFromParent(artwork_new, node_parent);
3394 setTreeInfoToDefaults(artwork_new, type);
3396 artwork_new->subdir = getStringCopy(directory_name);
3398 if (setup_file_hash) /* (before defining ".color" and ".class_desc") */
3401 checkSetupFileHashIdentifier(setup_file_hash, filename, getCookie("..."));
3404 /* set all structure fields according to the token/value pairs */
3406 for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
3407 setSetupInfo(levelinfo_tokens, i,
3408 getHashEntry(setup_file_hash, levelinfo_tokens[i].text));
3411 if (strEqual(artwork_new->name, ANONYMOUS_NAME))
3412 setString(&artwork_new->name, artwork_new->subdir);
3414 if (artwork_new->identifier == NULL)
3415 artwork_new->identifier = getStringCopy(artwork_new->subdir);
3417 if (artwork_new->name_sorting == NULL)
3418 artwork_new->name_sorting = getStringCopy(artwork_new->name);
3421 if (node_parent == NULL) /* top level group */
3423 artwork_new->basepath = getStringCopy(base_directory);
3424 artwork_new->fullpath = getStringCopy(artwork_new->subdir);
3426 else /* sub level group */
3428 artwork_new->basepath = getStringCopy(node_parent->basepath);
3429 artwork_new->fullpath = getPath2(node_parent->fullpath, directory_name);
3432 artwork_new->in_user_dir =
3433 (!strEqual(artwork_new->basepath, OPTIONS_ARTWORK_DIRECTORY(type)));
3435 /* (may use ".sort_priority" from "setup_file_hash" above) */
3436 artwork_new->color = ARTWORKCOLOR(artwork_new);
3438 setString(&artwork_new->class_desc, getLevelClassDescription(artwork_new));
3440 if (setup_file_hash == NULL) /* (after determining ".user_defined") */
3442 if (strEqual(artwork_new->subdir, "."))
3444 if (artwork_new->user_defined)
3446 setString(&artwork_new->identifier, "private");
3447 artwork_new->sort_priority = ARTWORKCLASS_PRIVATE;
3451 setString(&artwork_new->identifier, "classic");
3452 artwork_new->sort_priority = ARTWORKCLASS_CLASSICS;
3455 /* set to new values after changing ".sort_priority" */
3456 artwork_new->color = ARTWORKCOLOR(artwork_new);
3458 setString(&artwork_new->class_desc,
3459 getLevelClassDescription(artwork_new));
3463 setString(&artwork_new->identifier, artwork_new->subdir);
3466 setString(&artwork_new->name, artwork_new->identifier);
3467 setString(&artwork_new->name_sorting, artwork_new->name);
3471 DrawInitText(artwork_new->name, 150, FC_YELLOW);
3474 pushTreeInfo(node_first, artwork_new);
3476 freeSetupFileHash(setup_file_hash);
3478 free(directory_path);
3484 static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
3485 TreeInfo *node_parent,
3486 char *base_directory, int type)
3489 struct dirent *dir_entry;
3490 boolean valid_entry_found = FALSE;
3492 if ((dir = opendir(base_directory)) == NULL)
3494 /* display error if directory is main "options.graphics_directory" etc. */
3495 if (base_directory == OPTIONS_ARTWORK_DIRECTORY(type))
3496 Error(ERR_WARN, "cannot read directory '%s'", base_directory);
3501 while ((dir_entry = readdir(dir)) != NULL) /* loop until last dir entry */
3503 struct stat file_status;
3504 char *directory_name = dir_entry->d_name;
3505 char *directory_path = getPath2(base_directory, directory_name);
3507 /* skip directory entries for current and parent directory */
3508 if (strEqual(directory_name, ".") ||
3509 strEqual(directory_name, ".."))
3511 free(directory_path);
3515 /* skip directory entries which are not a directory or are not accessible */
3516 if (stat(directory_path, &file_status) != 0 || /* cannot stat file */
3517 (file_status.st_mode & S_IFMT) != S_IFDIR) /* not a directory */
3519 free(directory_path);
3523 free(directory_path);
3525 /* check if this directory contains artwork with or without config file */
3526 valid_entry_found |= LoadArtworkInfoFromArtworkConf(node_first, node_parent,
3528 directory_name, type);
3533 /* check if this directory directly contains artwork itself */
3534 valid_entry_found |= LoadArtworkInfoFromArtworkConf(node_first, node_parent,
3535 base_directory, ".",
3537 if (!valid_entry_found)
3538 Error(ERR_WARN, "cannot find any valid artwork in directory '%s'",
3542 static TreeInfo *getDummyArtworkInfo(int type)
3544 /* this is only needed when there is completely no artwork available */
3545 TreeInfo *artwork_new = newTreeInfo();
3547 setTreeInfoToDefaults(artwork_new, type);
3549 setString(&artwork_new->subdir, UNDEFINED_FILENAME);
3550 setString(&artwork_new->fullpath, UNDEFINED_FILENAME);
3551 setString(&artwork_new->basepath, UNDEFINED_FILENAME);
3553 setString(&artwork_new->identifier, UNDEFINED_FILENAME);
3554 setString(&artwork_new->name, UNDEFINED_FILENAME);
3555 setString(&artwork_new->name_sorting, UNDEFINED_FILENAME);
3560 void LoadArtworkInfo()
3562 LoadArtworkInfoCache();
3564 DrawInitText("Looking for custom artwork", 120, FC_GREEN);
3566 LoadArtworkInfoFromArtworkDir(&artwork.gfx_first, NULL,
3567 options.graphics_directory,
3568 TREE_TYPE_GRAPHICS_DIR);
3569 LoadArtworkInfoFromArtworkDir(&artwork.gfx_first, NULL,
3570 getUserGraphicsDir(),
3571 TREE_TYPE_GRAPHICS_DIR);
3573 LoadArtworkInfoFromArtworkDir(&artwork.snd_first, NULL,
3574 options.sounds_directory,
3575 TREE_TYPE_SOUNDS_DIR);
3576 LoadArtworkInfoFromArtworkDir(&artwork.snd_first, NULL,
3578 TREE_TYPE_SOUNDS_DIR);
3580 LoadArtworkInfoFromArtworkDir(&artwork.mus_first, NULL,
3581 options.music_directory,
3582 TREE_TYPE_MUSIC_DIR);
3583 LoadArtworkInfoFromArtworkDir(&artwork.mus_first, NULL,
3585 TREE_TYPE_MUSIC_DIR);
3587 if (artwork.gfx_first == NULL)
3588 artwork.gfx_first = getDummyArtworkInfo(TREE_TYPE_GRAPHICS_DIR);
3589 if (artwork.snd_first == NULL)
3590 artwork.snd_first = getDummyArtworkInfo(TREE_TYPE_SOUNDS_DIR);
3591 if (artwork.mus_first == NULL)
3592 artwork.mus_first = getDummyArtworkInfo(TREE_TYPE_MUSIC_DIR);
3594 /* before sorting, the first entries will be from the user directory */
3595 artwork.gfx_current =
3596 getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
3597 if (artwork.gfx_current == NULL)
3598 artwork.gfx_current =
3599 getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
3600 if (artwork.gfx_current == NULL)
3601 artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
3603 artwork.snd_current =
3604 getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
3605 if (artwork.snd_current == NULL)
3606 artwork.snd_current =
3607 getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
3608 if (artwork.snd_current == NULL)
3609 artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
3611 artwork.mus_current =
3612 getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
3613 if (artwork.mus_current == NULL)
3614 artwork.mus_current =
3615 getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
3616 if (artwork.mus_current == NULL)
3617 artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
3619 artwork.gfx_current_identifier = artwork.gfx_current->identifier;
3620 artwork.snd_current_identifier = artwork.snd_current->identifier;
3621 artwork.mus_current_identifier = artwork.mus_current->identifier;
3624 printf("graphics set == %s\n\n", artwork.gfx_current_identifier);
3625 printf("sounds set == %s\n\n", artwork.snd_current_identifier);
3626 printf("music set == %s\n\n", artwork.mus_current_identifier);
3629 sortTreeInfo(&artwork.gfx_first);
3630 sortTreeInfo(&artwork.snd_first);
3631 sortTreeInfo(&artwork.mus_first);
3634 dumpTreeInfo(artwork.gfx_first, 0);
3635 dumpTreeInfo(artwork.snd_first, 0);
3636 dumpTreeInfo(artwork.mus_first, 0);
3640 void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
3641 LevelDirTree *level_node)
3644 static unsigned int progress_delay = 0;
3645 unsigned int progress_delay_value = 100; /* (in milliseconds) */
3647 int type = (*artwork_node)->type;
3649 /* recursively check all level directories for artwork sub-directories */
3653 /* check all tree entries for artwork, but skip parent link entries */
3654 if (!level_node->parent_link)
3656 TreeInfo *artwork_new = getArtworkInfoCacheEntry(level_node, type);
3657 boolean cached = (artwork_new != NULL);
3661 pushTreeInfo(artwork_node, artwork_new);
3665 TreeInfo *topnode_last = *artwork_node;
3666 char *path = getPath2(getLevelDirFromTreeInfo(level_node),
3667 ARTWORK_DIRECTORY(type));
3669 LoadArtworkInfoFromArtworkDir(artwork_node, NULL, path, type);
3671 if (topnode_last != *artwork_node) /* check for newly added node */
3673 artwork_new = *artwork_node;
3675 setString(&artwork_new->identifier, level_node->subdir);
3676 setString(&artwork_new->name, level_node->name);
3677 setString(&artwork_new->name_sorting, level_node->name_sorting);
3679 artwork_new->sort_priority = level_node->sort_priority;
3680 artwork_new->color = LEVELCOLOR(artwork_new);
3686 /* insert artwork info (from old cache or filesystem) into new cache */
3687 if (artwork_new != NULL)
3688 setArtworkInfoCacheEntry(artwork_new, level_node, type);
3692 DrawInitTextExt(level_node->name, 150, FC_YELLOW,
3693 level_node->level_group);
3695 if (level_node->level_group ||
3696 DelayReached(&progress_delay, progress_delay_value))
3697 DrawInitText(level_node->name, 150, FC_YELLOW);
3700 if (level_node->node_group != NULL)
3701 LoadArtworkInfoFromLevelInfo(artwork_node, level_node->node_group);
3703 level_node = level_node->next;
3707 void LoadLevelArtworkInfo()
3709 DrawInitText("Looking for custom level artwork", 120, FC_GREEN);
3711 LoadArtworkInfoFromLevelInfo(&artwork.gfx_first, leveldir_first_all);
3712 LoadArtworkInfoFromLevelInfo(&artwork.snd_first, leveldir_first_all);
3713 LoadArtworkInfoFromLevelInfo(&artwork.mus_first, leveldir_first_all);
3715 SaveArtworkInfoCache();
3717 /* needed for reloading level artwork not known at ealier stage */
3719 if (!strEqual(artwork.gfx_current_identifier, setup.graphics_set))
3721 artwork.gfx_current =
3722 getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
3723 if (artwork.gfx_current == NULL)
3724 artwork.gfx_current =
3725 getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
3726 if (artwork.gfx_current == NULL)
3727 artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
3730 if (!strEqual(artwork.snd_current_identifier, setup.sounds_set))
3732 artwork.snd_current =
3733 getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
3734 if (artwork.snd_current == NULL)
3735 artwork.snd_current =
3736 getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
3737 if (artwork.snd_current == NULL)
3738 artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
3741 if (!strEqual(artwork.mus_current_identifier, setup.music_set))
3743 artwork.mus_current =
3744 getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
3745 if (artwork.mus_current == NULL)
3746 artwork.mus_current =
3747 getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
3748 if (artwork.mus_current == NULL)
3749 artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
3752 sortTreeInfo(&artwork.gfx_first);
3753 sortTreeInfo(&artwork.snd_first);
3754 sortTreeInfo(&artwork.mus_first);
3757 dumpTreeInfo(artwork.gfx_first, 0);
3758 dumpTreeInfo(artwork.snd_first, 0);
3759 dumpTreeInfo(artwork.mus_first, 0);
3763 static void SaveUserLevelInfo()
3765 LevelDirTree *level_info;
3770 filename = getPath2(getUserLevelDir(getLoginName()), LEVELINFO_FILENAME);
3772 if (!(file = fopen(filename, MODE_WRITE)))
3774 Error(ERR_WARN, "cannot write level info file '%s'", filename);
3779 level_info = newTreeInfo();
3781 /* always start with reliable default values */
3782 setTreeInfoToDefaults(level_info, TREE_TYPE_LEVEL_DIR);
3784 setString(&level_info->name, getLoginName());
3785 setString(&level_info->author, getRealName());
3786 level_info->levels = 100;
3787 level_info->first_level = 1;
3789 token_value_position = TOKEN_VALUE_POSITION_SHORT;
3791 fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
3792 getCookie("LEVELINFO")));
3795 for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
3797 if (i == LEVELINFO_TOKEN_NAME ||
3798 i == LEVELINFO_TOKEN_AUTHOR ||
3799 i == LEVELINFO_TOKEN_LEVELS ||
3800 i == LEVELINFO_TOKEN_FIRST_LEVEL)
3801 fprintf(file, "%s\n", getSetupLine(levelinfo_tokens, "", i));
3803 /* just to make things nicer :) */
3804 if (i == LEVELINFO_TOKEN_AUTHOR)
3805 fprintf(file, "\n");
3808 token_value_position = TOKEN_VALUE_POSITION_DEFAULT;
3812 SetFilePermissions(filename, PERMS_PRIVATE);
3814 freeTreeInfo(level_info);
3818 char *getSetupValue(int type, void *value)
3820 static char value_string[MAX_LINE_LEN];
3828 strcpy(value_string, (*(boolean *)value ? "true" : "false"));
3832 strcpy(value_string, (*(boolean *)value ? "on" : "off"));
3836 strcpy(value_string, (*(int *)value == AUTO ? "auto" :
3837 *(int *)value == FALSE ? "off" : "on"));
3841 strcpy(value_string, (*(boolean *)value ? "yes" : "no"));
3844 case TYPE_YES_NO_AUTO:
3845 strcpy(value_string, (*(int *)value == AUTO ? "auto" :
3846 *(int *)value == FALSE ? "no" : "yes"));
3850 strcpy(value_string, (*(boolean *)value ? "AGA" : "ECS"));
3854 strcpy(value_string, getKeyNameFromKey(*(Key *)value));
3858 strcpy(value_string, getX11KeyNameFromKey(*(Key *)value));
3862 sprintf(value_string, "%d", *(int *)value);
3866 if (*(char **)value == NULL)
3869 strcpy(value_string, *(char **)value);
3873 value_string[0] = '\0';
3877 if (type & TYPE_GHOSTED)
3878 strcpy(value_string, "n/a");
3880 return value_string;
3883 char *getSetupLine(struct TokenInfo *token_info, char *prefix, int token_nr)
3887 static char token_string[MAX_LINE_LEN];
3888 int token_type = token_info[token_nr].type;
3889 void *setup_value = token_info[token_nr].value;
3890 char *token_text = token_info[token_nr].text;
3891 char *value_string = getSetupValue(token_type, setup_value);
3893 /* build complete token string */
3894 sprintf(token_string, "%s%s", prefix, token_text);
3896 /* build setup entry line */
3897 line = getFormattedSetupEntry(token_string, value_string);
3899 if (token_type == TYPE_KEY_X11)
3901 Key key = *(Key *)setup_value;
3902 char *keyname = getKeyNameFromKey(key);
3904 /* add comment, if useful */
3905 if (!strEqual(keyname, "(undefined)") &&
3906 !strEqual(keyname, "(unknown)"))
3908 /* add at least one whitespace */
3910 for (i = strlen(line); i < token_comment_position; i++)
3914 strcat(line, keyname);
3921 void LoadLevelSetup_LastSeries()
3923 /* ----------------------------------------------------------------------- */
3924 /* ~/.<program>/levelsetup.conf */
3925 /* ----------------------------------------------------------------------- */
3927 char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
3928 SetupFileHash *level_setup_hash = NULL;
3930 /* always start with reliable default values */
3931 leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
3933 #if defined(CREATE_SPECIAL_EDITION_RND_JUE)
3934 leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
3936 if (leveldir_current == NULL)
3937 leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
3940 if ((level_setup_hash = loadSetupFileHash(filename)))
3942 char *last_level_series =
3943 getHashEntry(level_setup_hash, TOKEN_STR_LAST_LEVEL_SERIES);
3945 leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
3947 if (leveldir_current == NULL)
3948 leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
3950 checkSetupFileHashIdentifier(level_setup_hash, filename,
3951 getCookie("LEVELSETUP"));
3953 freeSetupFileHash(level_setup_hash);
3956 Error(ERR_WARN, "using default setup values");
3961 static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
3963 /* ----------------------------------------------------------------------- */
3964 /* ~/.<program>/levelsetup.conf */
3965 /* ----------------------------------------------------------------------- */
3967 // check if the current level directory structure is available at this point
3968 if (leveldir_current == NULL)
3971 char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
3972 char *level_subdir = leveldir_current->subdir;
3975 InitUserDataDirectory();
3977 if (!(file = fopen(filename, MODE_WRITE)))
3979 Error(ERR_WARN, "cannot write setup file '%s'", filename);
3986 fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
3987 getCookie("LEVELSETUP")));
3989 if (deactivate_last_level_series)
3990 fprintf(file, "# %s\n# ", "the following level set may have caused a problem and was deactivated");
3992 fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_LAST_LEVEL_SERIES,
3997 SetFilePermissions(filename, PERMS_PRIVATE);
4002 void SaveLevelSetup_LastSeries()
4004 SaveLevelSetup_LastSeries_Ext(FALSE);
4007 void SaveLevelSetup_LastSeries_Deactivate()
4009 SaveLevelSetup_LastSeries_Ext(TRUE);
4012 static void checkSeriesInfo()
4014 static char *level_directory = NULL;
4017 struct dirent *dir_entry;
4020 /* check for more levels besides the 'levels' field of 'levelinfo.conf' */
4022 level_directory = getPath2((leveldir_current->in_user_dir ?
4023 getUserLevelDir(NULL) :
4024 options.level_directory),
4025 leveldir_current->fullpath);
4027 if ((dir = opendir(level_directory)) == NULL)
4029 Error(ERR_WARN, "cannot read level directory '%s'", level_directory);
4035 while ((dir_entry = readdir(dir)) != NULL) /* last directory entry */
4037 if (strlen(dir_entry->d_name) > 4 &&
4038 dir_entry->d_name[3] == '.' &&
4039 strEqual(&dir_entry->d_name[4], LEVELFILE_EXTENSION))
4041 char levelnum_str[4];
4044 strncpy(levelnum_str, dir_entry->d_name, 3);
4045 levelnum_str[3] = '\0';
4047 levelnum_value = atoi(levelnum_str);
4049 if (levelnum_value < leveldir_current->first_level)
4051 Error(ERR_WARN, "additional level %d found", levelnum_value);
4052 leveldir_current->first_level = levelnum_value;
4054 else if (levelnum_value > leveldir_current->last_level)
4056 Error(ERR_WARN, "additional level %d found", levelnum_value);
4057 leveldir_current->last_level = levelnum_value;
4066 void LoadLevelSetup_SeriesInfo()
4069 SetupFileHash *level_setup_hash = NULL;
4070 char *level_subdir = leveldir_current->subdir;
4073 /* always start with reliable default values */
4074 level_nr = leveldir_current->first_level;
4076 for (i = 0; i < MAX_LEVELS; i++)
4078 LevelStats_setPlayed(i, 0);
4079 LevelStats_setSolved(i, 0);
4082 checkSeriesInfo(leveldir_current);
4084 /* ----------------------------------------------------------------------- */
4085 /* ~/.<program>/levelsetup/<level series>/levelsetup.conf */
4086 /* ----------------------------------------------------------------------- */
4088 level_subdir = leveldir_current->subdir;
4090 filename = getPath2(getLevelSetupDir(level_subdir), LEVELSETUP_FILENAME);
4092 if ((level_setup_hash = loadSetupFileHash(filename)))
4096 /* get last played level in this level set */
4098 token_value = getHashEntry(level_setup_hash, TOKEN_STR_LAST_PLAYED_LEVEL);
4102 level_nr = atoi(token_value);
4104 if (level_nr < leveldir_current->first_level)
4105 level_nr = leveldir_current->first_level;
4106 if (level_nr > leveldir_current->last_level)
4107 level_nr = leveldir_current->last_level;
4110 /* get handicap level in this level set */
4112 token_value = getHashEntry(level_setup_hash, TOKEN_STR_HANDICAP_LEVEL);
4116 int level_nr = atoi(token_value);
4118 if (level_nr < leveldir_current->first_level)
4119 level_nr = leveldir_current->first_level;
4120 if (level_nr > leveldir_current->last_level + 1)
4121 level_nr = leveldir_current->last_level;
4123 if (leveldir_current->user_defined || !leveldir_current->handicap)
4124 level_nr = leveldir_current->last_level;
4126 leveldir_current->handicap_level = level_nr;
4129 /* get number of played and solved levels in this level set */
4131 BEGIN_HASH_ITERATION(level_setup_hash, itr)
4133 char *token = HASH_ITERATION_TOKEN(itr);
4134 char *value = HASH_ITERATION_VALUE(itr);
4136 if (strlen(token) == 3 &&
4137 token[0] >= '0' && token[0] <= '9' &&
4138 token[1] >= '0' && token[1] <= '9' &&
4139 token[2] >= '0' && token[2] <= '9')
4141 int level_nr = atoi(token);
4144 LevelStats_setPlayed(level_nr, atoi(value)); /* read 1st column */
4146 value = strchr(value, ' ');
4149 LevelStats_setSolved(level_nr, atoi(value)); /* read 2nd column */
4152 END_HASH_ITERATION(hash, itr)
4154 checkSetupFileHashIdentifier(level_setup_hash, filename,
4155 getCookie("LEVELSETUP"));
4157 freeSetupFileHash(level_setup_hash);
4160 Error(ERR_WARN, "using default setup values");
4165 void SaveLevelSetup_SeriesInfo()
4168 char *level_subdir = leveldir_current->subdir;
4169 char *level_nr_str = int2str(level_nr, 0);
4170 char *handicap_level_str = int2str(leveldir_current->handicap_level, 0);
4174 /* ----------------------------------------------------------------------- */
4175 /* ~/.<program>/levelsetup/<level series>/levelsetup.conf */
4176 /* ----------------------------------------------------------------------- */
4178 InitLevelSetupDirectory(level_subdir);
4180 filename = getPath2(getLevelSetupDir(level_subdir), LEVELSETUP_FILENAME);
4182 if (!(file = fopen(filename, MODE_WRITE)))
4184 Error(ERR_WARN, "cannot write setup file '%s'", filename);
4189 fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
4190 getCookie("LEVELSETUP")));
4191 fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_LAST_PLAYED_LEVEL,
4193 fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_HANDICAP_LEVEL,
4194 handicap_level_str));
4196 for (i = leveldir_current->first_level; i <= leveldir_current->last_level;
4199 if (LevelStats_getPlayed(i) > 0 ||
4200 LevelStats_getSolved(i) > 0)
4205 sprintf(token, "%03d", i);
4206 sprintf(value, "%d %d", LevelStats_getPlayed(i), LevelStats_getSolved(i));
4208 fprintf(file, "%s\n", getFormattedSetupEntry(token, value));
4214 SetFilePermissions(filename, PERMS_PRIVATE);
4219 int LevelStats_getPlayed(int nr)
4221 return (nr >= 0 && nr < MAX_LEVELS ? level_stats[nr].played : 0);
4224 int LevelStats_getSolved(int nr)
4226 return (nr >= 0 && nr < MAX_LEVELS ? level_stats[nr].solved : 0);
4229 void LevelStats_setPlayed(int nr, int value)
4231 if (nr >= 0 && nr < MAX_LEVELS)
4232 level_stats[nr].played = value;
4235 void LevelStats_setSolved(int nr, int value)
4237 if (nr >= 0 && nr < MAX_LEVELS)
4238 level_stats[nr].solved = value;
4241 void LevelStats_incPlayed(int nr)
4243 if (nr >= 0 && nr < MAX_LEVELS)
4244 level_stats[nr].played++;
4247 void LevelStats_incSolved(int nr)
4249 if (nr >= 0 && nr < MAX_LEVELS)
4250 level_stats[nr].solved++;