# /*********************************************************** # * Rocks'n'Diamonds -- McDuffin Strikes Back! * # *----------------------------------------------------------* # * (c) 1995-2006 Artsoft Entertainment * # * Holger Schemel * # * Detmolder Strasse 189 * # * 33604 Bielefeld * # * Germany * # * e-mail: info@artsoft.org * # *----------------------------------------------------------* # * files.c * # ***********************************************************/ CHUNK_ID_LEN = 4 # IFF style chunk id length CHUNK_SIZE_UNDEFINED = 0 # undefined chunk size == 0 CHUNK_SIZE_NONE = -1 # do not write chunk size LEVEL_CHUNK_NAME_SIZE = MAX_LEVEL_NAME_LEN LEVEL_CHUNK_AUTH_SIZE = MAX_LEVEL_AUTHOR_LEN LEVEL_CHUNK_VERS_SIZE = 8 # size of file version chunk LEVEL_CHUNK_DATE_SIZE = 4 # size of file date chunk LEVEL_CHUNK_HEAD_SIZE = 80 # size of level file header LEVEL_CHUNK_CNT2_SIZE = 160 # size of level CNT2 chunk LEVEL_CHUNK_CNT2_UNUSED = 11 # unused CNT2 chunk bytes LEVEL_CHUNK_CNT3_HEADER = 16 # size of level CNT3 header LEVEL_CPART_CUS3_SIZE = 134 # size of CUS3 chunk part LEVEL_CPART_CUS3_UNUSED = 15 # unused CUS3 bytes / part LEVEL_CHUNK_GRP1_SIZE = 74 # size of level GRP1 chunk # (element number, number of change pages, change page number) LEVEL_CHUNK_CUSX_UNCHANGED = (2 + (1 + 1) + (1 + 1)) # (element number only) LEVEL_CHUNK_GRPX_UNCHANGED = 2 LEVEL_CHUNK_NOTE_UNCHANGED = 2 # (nothing at all if unchanged) LEVEL_CHUNK_ELEM_UNCHANGED = 0 #define LEVEL_CHUNK_CUS3_SIZE(x) (2 + (x) * LEVEL_CPART_CUS3_SIZE) #define LEVEL_CHUNK_CUS4_SIZE(x) (96 + (x) * 48) # file identifier strings LEVEL_COOKIE_TMPL = "ROCKSNDIAMONDS_LEVEL_FILE_VERSION_x.x" # values for deciding when (not) to save configuration data SAVE_CONF_NEVER = 0 SAVE_CONF_ALWAYS = 1 SAVE_CONF_WHEN_CHANGED = -1 # values for chunks using micro chunks CONF_MASK_1_BYTE = 0x00 CONF_MASK_2_BYTE = 0x40 CONF_MASK_4_BYTE = 0x80 CONF_MASK_MULTI_BYTES = 0xc0 CONF_MASK_BYTES = 0xc0 CONF_MASK_TOKEN = 0x3f # these definitions are just for convenience of use and readability CONF_VALUE_8_BIT = CONF_MASK_1_BYTE CONF_VALUE_16_BIT = CONF_MASK_2_BYTE CONF_VALUE_32_BIT = CONF_MASK_4_BYTE CONF_VALUE_BYTES = CONF_MASK_MULTI_BYTES ##define CONF_VALUE_NUM_BYTES(x) ((x) == CONF_MASK_1_BYTE ? 1 : \ # (x) == CONF_MASK_2_BYTE ? 2 : \ # (x) == CONF_MASK_4_BYTE ? 4 : 0) CONF_CONTENT_NUM_ELEMENTS = (3 * 3) CONF_CONTENT_NUM_BYTES = (CONF_CONTENT_NUM_ELEMENTS * 2) CONF_ELEMENT_NUM_BYTES = (2) ##define CONF_ENTITY_NUM_BYTES(t) ((t) == TYPE_ELEMENT || \ # (t) == TYPE_ELEMENT_LIST ? \ # CONF_ELEMENT_NUM_BYTES : \ # (t) == TYPE_CONTENT || \ # (t) == TYPE_CONTENT_LIST ? \ # CONF_CONTENT_NUM_BYTES : 1) ##define CONF_ELEMENT_BYTE_POS(i) ((i) * CONF_ELEMENT_NUM_BYTES) ##define CONF_ELEMENTS_ELEMENT(b,i) ((b[CONF_ELEMENT_BYTE_POS(i)] << 8) | \ # (b[CONF_ELEMENT_BYTE_POS(i) + 1])) ##define CONF_CONTENT_ELEMENT_POS(c,x,y) ((c) * CONF_CONTENT_NUM_ELEMENTS + \ # (y) * 3 + (x)) ##define CONF_CONTENT_BYTE_POS(c,x,y) (CONF_CONTENT_ELEMENT_POS(c,x,y) * \ # CONF_ELEMENT_NUM_BYTES) ##define CONF_CONTENTS_ELEMENT(b,c,x,y) ((b[CONF_CONTENT_BYTE_POS(c,x,y)]<< 8)|\ # (b[CONF_CONTENT_BYTE_POS(c,x,y) + 1])) # TODO make CONF_* macros work #-## li, xx_ei, yy_ei, xx_change, xx_group, xx_envelope, xx_event_bits, #-## xx_default_description, xx_num_contents, xx_current_change_page, #-## xx_default_string_empty, xx_string_length_unused are documented in #-## files/configinfos.rb #-## LevelFileConfigInfo is documented in files/configinfos.rb # TODO: when configinfos.rb disappears, move the docs here #-## chunk_config_* is currently $ruleset_*, but that will change soon #-## filetype_id_list was moved right into getFiletypeFromID() (it's the #-## only function that uses it anyway) # ========================================================================= # level file functions # ========================================================================= #-## getCurrentDate() = Time.now #-## resetEventFlags(change) = change.resetEventFlags #-## resetEventBits() = (nothing) #-## setEventFlagsFromEventBits(change) = no equivalent; cp.mcSet_eventBits(value,index) is related #-## setEventBitsFromEventFlags(change) = no equivalent; cp.mcGet_eventBits(index) is related #-## getDefaultElementDescription(ei) = ei.defaultDescription #-## setElementDescriptionToDefault(ei) = ei.description=(ei.defaultDescription) #-## setConfigToDefaultsFromConfigList(conf) = roughly equivalent to obj.mcSetToDefaults(conf) #-## copyConfigFromConfigList(conf) = roughly equivalent to xx_ei.mcCopyValuesTo(yy_ei, conf) #-## copyElementInfo(ei_from, ei_to) = ei_from.copyTo(ei_to) # Note: ei_from.copyTo(Element.new) = ei_from.copy #-## setElementChangePages(ei,change_pages) = ei.changePages.length=change_pages #-## setElementChangeInfoToDefaults(change) = change.initialize #-## setLevelInfoToDefaults(level) = level.initialize or level=Level.new() #-## setFileInfoToDefaults(level_file_info) = level_file_info.initialize or level_file_info.initialize(getCurrentLevelDir()) def ActivateLevelTemplate() # Currently there is no special action needed to activate the template # data, because 'element_info' property settings overwrite the original # level data, while all other variables do not change. # TODO: is the above true in Red Mace? end #-## getLevelFilenameFromBasename(basename) = LevelFile.getFileTypeFromBasename(getCurrentLevelDir(), basename) #-## getFileTypeFromBasename(basename) = LevelFile.getFileTypeFromBasename(getCurrentLevelDir(), basename) #-## getSingleLevelBasename(nr) = no equivalent; LevelFile.getDefaultLevelFilename(getCurrentLevelDir(),nr) is related #-## getPackedLevelBasename(type) = LevelFile.getPackedLevelBasename(getCurrentLevelDir(), type) #-## getSingleLevelFilename(nr) = LevelFile.getDefaultLevelFilename(getCurrentLevelDir(), nr) #-## getDefaultLevelFilename(nr) = LevelFile.getDefaultLevelFilename(getCurrentLevelDir(), nr) #-## setLevelFileInfo_FormatLevelFilename(lfi, type, format, ...) = lfi.formatSingleLevelFilename(type, format, ...) #-## setLevelFileInfo_PackedLevelFilename(lfi, type) = lfi.setPackedLevelFilename(type) #-## getFiletypeFromID(filetype_id) = LevelFile.getFiletypeFromId(filetype_id) #-## determineLevelFileInfo_Filename(lfi) = lfi.determineFilename #-## determineLevelFileInfo_Filetype(lfi) = lfi.determineFiletype #-## setLevelFileInfo(lfi, nr) = lfi.initialize(getCurrentLevelDir(), nr) or LevelFile.new(getCurrentLevelDir(), nr) # ------------------------------------------------------------------------- # functions for loading R'n'D level # ------------------------------------------------------------------------- def getMappedElement(element) # remap some (historic, now obsolete) elements case element when EL_PLAYER_OBSOLETE then EL_PLAYER_1 when EL_KEY_OBSOLETE then EL_KEY_1 when EL_EM_KEY_1_FILE_OBSOLETE then EL_EM_KEY_1 when EL_EM_KEY_2_FILE_OBSOLETE then EL_EM_KEY_2 when EL_EM_KEY_3_FILE_OBSOLETE then EL_EM_KEY_3 when EL_EM_KEY_4_FILE_OBSOLETE then EL_EM_KEY_4 when EL_ENVELOPE_OBSOLETE then EL_ENVELOPE_1 when EL_SP_EMPTY then EL_EMPTY else if element >= NUM_FILE_ELEMENTS error(ERR_WARN, "invalid level element #{element}") return EL_UNKNOWN end return element end end def getMappedElementByVersion(element, game_version) # remap some elements due to certain game version if game_version <= VERSION_IDENT(2,2,0,0) # map game font elements element = case element when EL_CHAR('[') then EL_CHAR_AUMLAUT when EL_CHAR('\\')then EL_CHAR_OUMLAUT when EL_CHAR(']') then EL_CHAR_UUMLAUT when EL_CHAR('^') then EL_CHAR_COPYRIGHT else element end end if game_version <= VERSION_IDENT(3,0,0,0) # map Supaplex gravity tube elements element = case element when EL_SP_GRAVITY_PORT_LEFT then EL_SP_PORT_LEFT when EL_SP_GRAVITY_PORT_RIGHT then EL_SP_PORT_RIGHT when EL_SP_GRAVITY_PORT_UP then EL_SP_PORT_UP when EL_SP_GRAVITY_PORT_DOWN then EL_SP_PORT_DOWN end end element end # Note: for LoadLevel_* see also class LevelLoader #-## LoadLevel_VERS(file, chunk_size, level) = (private) level.loaderSetFile(file); level.loadVERS(chunk_size) #-## LoadLevel_DATE(file, chunk_size, level) = (private) level.loaderSetFile(file); level.loadDATE(chunk_size) #-## etc etc etc... loadHEAD, loadNAME, loadAUTH, loadBODY, loadCONT, loadCNT2, loadCNT3, #-## loadCUS1, loadCUS2, loadCUS3, loadCUS4, loadGRP1 #-## LoadLevel_MicroChunk(file, conf, element, real_element) = obj.mcLoad(file, conf, element, real_element) #-## etc etc etc... loadINFO, loadELEM, loadNOTE, loadCUSX, loadGRPX static int LoadLevel_CUSX(FILE *file, int chunk_size, struct LevelInfo *level) { int element = getMappedElement(getFile16BitBE(file)); int real_chunk_size = 2; struct ElementInfo *ei = &element_info[element]; int i; xx_ei = *ei; # copy element data into temporary buffer xx_ei.num_change_pages = -1; while (!feof(file)) { real_chunk_size += LoadLevel_MicroChunk(file, chunk_config_CUSX_base, -1, element); if (xx_ei.num_change_pages != -1) break; if (real_chunk_size >= chunk_size) break; } *ei = xx_ei; if (ei->num_change_pages == -1) { Error(ERR_WARN, "LoadLevel_CUSX(): missing 'num_change_pages' for '%s'", EL_NAME(element)); ei->num_change_pages = 1; setElementChangePages(ei, 1); setElementChangeInfoToDefaults(ei->change); return real_chunk_size; } # initialize number of change pages stored for this custom element setElementChangePages(ei, ei->num_change_pages); for (i = 0; i < ei->num_change_pages; i++) setElementChangeInfoToDefaults(&ei->change_page[i]); # start with reading properties for the first change page xx_current_change_page = 0; while (!feof(file)) { struct ElementChangeInfo *change = &ei->change_page[xx_current_change_page]; xx_change = *change; # copy change data into temporary buffer resetEventBits(); # reset bits; change page might have changed real_chunk_size += LoadLevel_MicroChunk(file, chunk_config_CUSX_change, -1, element); *change = xx_change; setEventFlagsFromEventBits(change); if (real_chunk_size >= chunk_size) break; } return real_chunk_size; } static int LoadLevel_GRPX(FILE *file, int chunk_size, struct LevelInfo *level) { int element = getMappedElement(getFile16BitBE(file)); int real_chunk_size = 2; struct ElementInfo *ei = &element_info[element]; struct ElementGroupInfo *group = ei->group; xx_ei = *ei; # copy element data into temporary buffer xx_group = *group; # copy group data into temporary buffer while (!feof(file)) { real_chunk_size += LoadLevel_MicroChunk(file, chunk_config_GRPX, -1, element); if (real_chunk_size >= chunk_size) break; } *ei = xx_ei; *group = xx_group; return real_chunk_size; } static void LoadLevelFromFileInfo_RND(struct LevelInfo *level, struct LevelFileInfo *level_file_info) { char *filename = level_file_info->filename; char cookie[MAX_LINE_LEN]; char chunk_name[CHUNK_ID_LEN + 1]; int chunk_size; FILE *file; if (!(file = fopen(filename, MODE_READ))) { level->no_valid_file = TRUE; if (level != &level_template) Error(ERR_WARN, "cannot read level '%s' -- using empty level", filename); return; } getFileChunkBE(file, chunk_name, NULL); if (strEqual(chunk_name, "RND1")) { getFile32BitBE(file); # not used getFileChunkBE(file, chunk_name, NULL); if (!strEqual(chunk_name, "CAVE")) { level->no_valid_file = TRUE; Error(ERR_WARN, "unknown format of level file '%s'", filename); fclose(file); return; } } else # check for pre-2.0 file format with cookie string { strcpy(cookie, chunk_name); fgets(&cookie[4], MAX_LINE_LEN - 4, file); if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n') cookie[strlen(cookie) - 1] = '\0'; if (!checkCookieString(cookie, LEVEL_COOKIE_TMPL)) { level->no_valid_file = TRUE; Error(ERR_WARN, "unknown format of level file '%s'", filename); fclose(file); return; } if ((level->file_version = getFileVersionFromCookieString(cookie)) == -1) { level->no_valid_file = TRUE; Error(ERR_WARN, "unsupported version of level file '%s'", filename); fclose(file); return; } # pre-2.0 level files have no game version, so use file version here level->game_version = level->file_version; } if (level->file_version < FILE_VERSION_1_2) { # level files from versions before 1.2.0 without chunk structure LoadLevel_HEAD(file, LEVEL_CHUNK_HEAD_SIZE, level); LoadLevel_BODY(file, level->fieldx * level->fieldy, level); } else { static struct { char *name; int size; int (*loader)(FILE *, int, struct LevelInfo *); } chunk_info[] = { { "VERS", LEVEL_CHUNK_VERS_SIZE, LoadLevel_VERS }, { "DATE", LEVEL_CHUNK_DATE_SIZE, LoadLevel_DATE }, { "HEAD", LEVEL_CHUNK_HEAD_SIZE, LoadLevel_HEAD }, { "NAME", LEVEL_CHUNK_NAME_SIZE, LoadLevel_NAME }, { "AUTH", LEVEL_CHUNK_AUTH_SIZE, LoadLevel_AUTH }, { "INFO", -1, LoadLevel_INFO }, { "BODY", -1, LoadLevel_BODY }, { "CONT", -1, LoadLevel_CONT }, { "CNT2", LEVEL_CHUNK_CNT2_SIZE, LoadLevel_CNT2 }, { "CNT3", -1, LoadLevel_CNT3 }, { "CUS1", -1, LoadLevel_CUS1 }, { "CUS2", -1, LoadLevel_CUS2 }, { "CUS3", -1, LoadLevel_CUS3 }, { "CUS4", -1, LoadLevel_CUS4 }, { "GRP1", -1, LoadLevel_GRP1 }, { "CONF", -1, LoadLevel_CONF }, { "ELEM", -1, LoadLevel_ELEM }, { "NOTE", -1, LoadLevel_NOTE }, { "CUSX", -1, LoadLevel_CUSX }, { "GRPX", -1, LoadLevel_GRPX }, { NULL, 0, NULL } }; while (getFileChunkBE(file, chunk_name, &chunk_size)) { int i = 0; while (chunk_info[i].name != NULL && !strEqual(chunk_name, chunk_info[i].name)) i++; if (chunk_info[i].name == NULL) { Error(ERR_WARN, "unknown chunk '%s' in level file '%s'", chunk_name, filename); ReadUnusedBytesFromFile(file, chunk_size); } else if (chunk_info[i].size != -1 && chunk_info[i].size != chunk_size) { Error(ERR_WARN, "wrong size (%d) of chunk '%s' in level file '%s'", chunk_size, chunk_name, filename); ReadUnusedBytesFromFile(file, chunk_size); } else { # call function to load this level chunk int chunk_size_expected = (chunk_info[i].loader)(file, chunk_size, level); # the size of some chunks cannot be checked before reading other # chunks first (like "HEAD" and "BODY") that contain some header # information, so check them here if (chunk_size_expected != chunk_size) { Error(ERR_WARN, "wrong size (%d) of chunk '%s' in level file '%s'", chunk_size, chunk_name, filename); } } } } fclose(file); } # ------------------------------------------------------------------------- # functions for loading EM level # ------------------------------------------------------------------------- void CopyNativeLevel_RND_to_EM(struct LevelInfo *level) { static int ball_xy[8][2] = { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 0, 1 }, { 2, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, }; struct LevelInfo_EM *level_em = level->native_em_level; struct LEVEL *lev = level_em->lev; struct PLAYER **ply = level_em->ply; int i, j, x, y; lev->width = MIN(level->fieldx, EM_MAX_CAVE_WIDTH); lev->height = MIN(level->fieldy, EM_MAX_CAVE_HEIGHT); lev->time_seconds = level->time; lev->required_initial = level->gems_needed; lev->emerald_score = level->score[SC_EMERALD]; lev->diamond_score = level->score[SC_DIAMOND]; lev->alien_score = level->score[SC_ROBOT]; lev->tank_score = level->score[SC_SPACESHIP]; lev->bug_score = level->score[SC_BUG]; lev->eater_score = level->score[SC_YAMYAM]; lev->nut_score = level->score[SC_NUT]; lev->dynamite_score = level->score[SC_DYNAMITE]; lev->key_score = level->score[SC_KEY]; lev->exit_score = level->score[SC_TIME_BONUS]; for (i = 0; i < MAX_ELEMENT_CONTENTS; i++) for (y = 0; y < 3; y++) for (x = 0; x < 3; x++) lev->eater_array[i][y * 3 + x] = map_element_RND_to_EM(level->yamyam_content[i].e[x][y]); lev->amoeba_time = level->amoeba_speed; lev->wonderwall_time_initial = level->time_magic_wall; lev->wheel_time = level->time_wheel; lev->android_move_time = level->android_move_time; lev->android_clone_time = level->android_clone_time; lev->ball_random = level->ball_random; lev->ball_state_initial = level->ball_state_initial; lev->ball_time = level->ball_time; lev->num_ball_arrays = level->num_ball_contents; lev->lenses_score = level->lenses_score; lev->magnify_score = level->magnify_score; lev->slurp_score = level->slurp_score; lev->lenses_time = level->lenses_time; lev->magnify_time = level->magnify_time; lev->wind_direction_initial = map_direction_RND_to_EM(level->wind_direction_initial); lev->wind_cnt_initial = (level->wind_direction_initial != MV_NONE ? lev->wind_time : 0); for (i = 0; i < MAX_ELEMENT_CONTENTS; i++) for (j = 0; j < 8; j++) lev->ball_array[i][j] = map_element_RND_to_EM(level-> ball_content[i].e[ball_xy[j][0]][ball_xy[j][1]]); map_android_clone_elements_RND_to_EM(level); # first fill the complete playfield with the default border element for (y = 0; y < EM_MAX_CAVE_HEIGHT; y++) for (x = 0; x < EM_MAX_CAVE_WIDTH; x++) level_em->cave[x][y] = ZBORDER; if (BorderElement == EL_STEELWALL) { for (y = 0; y < lev->height + 2; y++) for (x = 0; x < lev->width + 2; x++) level_em->cave[x + 1][y + 1] = map_element_RND_to_EM(EL_STEELWALL); } # then copy the real level contents from level file into the playfield for (y = 0; y < lev->height; y++) for (x = 0; x < lev->width; x++) { int new_element = map_element_RND_to_EM(level->field[x][y]); int offset = (BorderElement == EL_STEELWALL ? 1 : 0); int xx = x + 1 + offset; int yy = y + 1 + offset; if (level->field[x][y] == EL_AMOEBA_DEAD) new_element = map_element_RND_to_EM(EL_AMOEBA_WET); level_em->cave[xx][yy] = new_element; } for (i = 0; i < MAX_PLAYERS; i++) { ply[i]->x_initial = 0; ply[i]->y_initial = 0; } # initialize player positions and delete players from the playfield for (y = 0; y < lev->height; y++) for (x = 0; x < lev->width; x++) { if (ELEM_IS_PLAYER(level->field[x][y])) { int player_nr = GET_PLAYER_NR(level->field[x][y]); int offset = (BorderElement == EL_STEELWALL ? 1 : 0); int xx = x + 1 + offset; int yy = y + 1 + offset; ply[player_nr]->x_initial = xx; ply[player_nr]->y_initial = yy; level_em->cave[xx][yy] = map_element_RND_to_EM(EL_EMPTY); } } if (BorderElement == EL_STEELWALL) { lev->width += 2; lev->height += 2; } } void CopyNativeLevel_EM_to_RND(struct LevelInfo *level) { static int ball_xy[8][2] = { { 0, 0 }, { 1, 0 }, { 2, 0 }, { 0, 1 }, { 2, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, }; struct LevelInfo_EM *level_em = level->native_em_level; struct LEVEL *lev = level_em->lev; struct PLAYER **ply = level_em->ply; int i, j, x, y; level->fieldx = MIN(lev->width, MAX_LEV_FIELDX); level->fieldy = MIN(lev->height, MAX_LEV_FIELDY); level->time = lev->time_seconds; level->gems_needed = lev->required_initial; sprintf(level->name, "Level %d", level->file_info.nr); level->score[SC_EMERALD] = lev->emerald_score; level->score[SC_DIAMOND] = lev->diamond_score; level->score[SC_ROBOT] = lev->alien_score; level->score[SC_SPACESHIP] = lev->tank_score; level->score[SC_BUG] = lev->bug_score; level->score[SC_YAMYAM] = lev->eater_score; level->score[SC_NUT] = lev->nut_score; level->score[SC_DYNAMITE] = lev->dynamite_score; level->score[SC_KEY] = lev->key_score; level->score[SC_TIME_BONUS] = lev->exit_score; level->num_yamyam_contents = MAX_ELEMENT_CONTENTS; for (i = 0; i < level->num_yamyam_contents; i++) for (y = 0; y < 3; y++) for (x = 0; x < 3; x++) level->yamyam_content[i].e[x][y] = map_element_EM_to_RND(lev->eater_array[i][y * 3 + x]); level->amoeba_speed = lev->amoeba_time; level->time_magic_wall = lev->wonderwall_time_initial; level->time_wheel = lev->wheel_time; level->android_move_time = lev->android_move_time; level->android_clone_time = lev->android_clone_time; level->ball_random = lev->ball_random; level->ball_state_initial = lev->ball_state_initial; level->ball_time = lev->ball_time; level->num_ball_contents = lev->num_ball_arrays; level->lenses_score = lev->lenses_score; level->magnify_score = lev->magnify_score; level->slurp_score = lev->slurp_score; level->lenses_time = lev->lenses_time; level->magnify_time = lev->magnify_time; level->wind_direction_initial = map_direction_EM_to_RND(lev->wind_direction_initial); for (i = 0; i < MAX_ELEMENT_CONTENTS; i++) for (j = 0; j < 8; j++) level->ball_content[i].e[ball_xy[j][0]][ball_xy[j][1]] = map_element_EM_to_RND(lev->ball_array[i][j]); map_android_clone_elements_EM_to_RND(level); # convert the playfield (some elements need special treatment) for (y = 0; y < level->fieldy; y++) for (x = 0; x < level->fieldx; x++) { int new_element = map_element_EM_to_RND(level_em->cave[x + 1][y + 1]); if (new_element == EL_AMOEBA_WET && level->amoeba_speed == 0) new_element = EL_AMOEBA_DEAD; level->field[x][y] = new_element; } for (i = 0; i < MAX_PLAYERS; i++) { # in case of all players set to the same field, use the first player int nr = MAX_PLAYERS - i - 1; int jx = ply[nr]->x_initial - 1; int jy = ply[nr]->y_initial - 1; if (jx != -1 && jy != -1) level->field[jx][jy] = EL_PLAYER_1 + nr; } } static void LoadLevelFromFileInfo_EM(struct LevelInfo *level, struct LevelFileInfo *level_file_info) { if (!LoadNativeLevel_EM(level_file_info->filename)) level->no_valid_file = TRUE; } void CopyNativeLevel_RND_to_Native(struct LevelInfo *level) { if (level->game_engine_type == GAME_ENGINE_TYPE_EM) CopyNativeLevel_RND_to_EM(level); } void CopyNativeLevel_Native_to_RND(struct LevelInfo *level) { if (level->game_engine_type == GAME_ENGINE_TYPE_EM) CopyNativeLevel_EM_to_RND(level); } # ------------------------------------------------------------------------- # functions for loading SP level # ------------------------------------------------------------------------- #define NUM_SUPAPLEX_LEVELS_PER_PACKAGE 111 #define SP_LEVEL_SIZE 1536 #define SP_LEVEL_XSIZE 60 #define SP_LEVEL_YSIZE 24 #define SP_LEVEL_NAME_LEN 23 static void LoadLevelFromFileStream_SP(FILE *file, struct LevelInfo *level, int nr) { int initial_player_gravity; int num_special_ports; int i, x, y; # for details of the Supaplex level format, see Herman Perk's Supaplex # documentation file "SPFIX63.DOC" from his Supaplex "SpeedFix" package # read level body (width * height == 60 * 24 tiles == 1440 bytes) for (y = 0; y < SP_LEVEL_YSIZE; y++) { for (x = 0; x < SP_LEVEL_XSIZE; x++) { int element_old = fgetc(file); int element_new; if (element_old <= 0x27) element_new = getMappedElement(EL_SP_START + element_old); else if (element_old == 0x28) element_new = EL_INVISIBLE_WALL; else { Error(ERR_WARN, "in level %d, at position %d, %d:", nr, x, y); Error(ERR_WARN, "invalid level element %d", element_old); element_new = EL_UNKNOWN; } level->field[x][y] = element_new; } } ReadUnusedBytesFromFile(file, 4); # (not used by Supaplex engine) # initial gravity: 1 == "on", anything else (0) == "off" initial_player_gravity = (fgetc(file) == 1 ? TRUE : FALSE); for (i = 0; i < MAX_PLAYERS; i++) level->initial_player_gravity[i] = initial_player_gravity; ReadUnusedBytesFromFile(file, 1); # (not used by Supaplex engine) # level title in uppercase letters, padded with dashes ("-") (23 bytes) for (i = 0; i < SP_LEVEL_NAME_LEN; i++) level->name[i] = fgetc(file); level->name[SP_LEVEL_NAME_LEN] = '\0'; # initial "freeze zonks": 2 == "on", anything else (0, 1) == "off" ReadUnusedBytesFromFile(file, 1); # (not used by R'n'D engine) # number of infotrons needed; 0 means that Supaplex will count the total # amount of infotrons in the level and use the low byte of that number # (a multiple of 256 infotrons will result in "0 infotrons needed"!) level->gems_needed = fgetc(file); # number of special ("gravity") port entries below (maximum 10 allowed) num_special_ports = fgetc(file); # database of properties of up to 10 special ports (6 bytes per port) for (i = 0; i < 10; i++) { int port_location, port_x, port_y, port_element; int gravity; # high and low byte of the location of a special port; if (x, y) are the # coordinates of a port in the field and (0, 0) is the top-left corner, # the 16 bit value here calculates as 2 * (x + (y * 60)) (this is twice # of what may be expected: Supaplex works with a game field in memory # which is 2 bytes per tile) port_location = getFile16BitBE(file); # change gravity: 1 == "turn on", anything else (0) == "turn off" gravity = fgetc(file); # "freeze zonks": 2 == "turn on", anything else (0, 1) == "turn off" ReadUnusedBytesFromFile(file, 1); # (not used by R'n'D engine) # "freeze enemies": 1 == "turn on", anything else (0) == "turn off" ReadUnusedBytesFromFile(file, 1); # (not used by R'n'D engine) ReadUnusedBytesFromFile(file, 1); # (not used by Supaplex engine) if (i >= num_special_ports) continue; port_x = (port_location / 2) % SP_LEVEL_XSIZE; port_y = (port_location / 2) / SP_LEVEL_XSIZE; if (port_x < 0 || port_x >= SP_LEVEL_XSIZE || port_y < 0 || port_y >= SP_LEVEL_YSIZE) { Error(ERR_WARN, "special port position (%d, %d) out of bounds", port_x, port_y); continue; } port_element = level->field[port_x][port_y]; if (port_element < EL_SP_GRAVITY_PORT_RIGHT || port_element > EL_SP_GRAVITY_PORT_UP) { Error(ERR_WARN, "no special port at position (%d, %d)", port_x, port_y); continue; } # change previous (wrong) gravity inverting special port to either # gravity enabling special port or gravity disabling special port level->field[port_x][port_y] += (gravity == 1 ? EL_SP_GRAVITY_ON_PORT_RIGHT : EL_SP_GRAVITY_OFF_PORT_RIGHT) - EL_SP_GRAVITY_PORT_RIGHT; } ReadUnusedBytesFromFile(file, 4); # (not used by Supaplex engine) # change special gravity ports without database entries to normal ports for (y = 0; y < SP_LEVEL_YSIZE; y++) for (x = 0; x < SP_LEVEL_XSIZE; x++) if (level->field[x][y] >= EL_SP_GRAVITY_PORT_RIGHT && level->field[x][y] <= EL_SP_GRAVITY_PORT_UP) level->field[x][y] += EL_SP_PORT_RIGHT - EL_SP_GRAVITY_PORT_RIGHT; # auto-determine number of infotrons if it was stored as "0" -- see above if (level->gems_needed == 0) { for (y = 0; y < SP_LEVEL_YSIZE; y++) for (x = 0; x < SP_LEVEL_XSIZE; x++) if (level->field[x][y] == EL_SP_INFOTRON) level->gems_needed++; level->gems_needed &= 0xff; # only use low byte -- see above } level->fieldx = SP_LEVEL_XSIZE; level->fieldy = SP_LEVEL_YSIZE; level->time = 0; # no time limit level->amoeba_speed = 0; level->time_magic_wall = 0; level->time_wheel = 0; level->amoeba_content = EL_EMPTY; #if 1 # original Supaplex does not use score values -- use default values #else #endif # there are no yamyams in supaplex levels for (i = 0; i < level->num_yamyam_contents; i++) for (y = 0; y < 3; y++) for (x = 0; x < 3; x++) level->yamyam_content[i].e[x][y] = EL_EMPTY; } static void LoadLevelFromFileInfo_SP(struct LevelInfo *level, struct LevelFileInfo *level_file_info) { char *filename = level_file_info->filename; FILE *file; int nr = level_file_info->nr - leveldir_current->first_level; int i, l, x, y; char name_first, name_last; struct LevelInfo multipart_level; int multipart_xpos, multipart_ypos; boolean is_multipart_level; boolean is_first_part; boolean reading_multipart_level = FALSE; boolean use_empty_level = FALSE; if (!(file = fopen(filename, MODE_READ))) { level->no_valid_file = TRUE; Error(ERR_WARN, "cannot read level '%s' -- using empty level", filename); return; } # position file stream to the requested level inside the level package if (fseek(file, nr * SP_LEVEL_SIZE, SEEK_SET) != 0) { level->no_valid_file = TRUE; Error(ERR_WARN, "cannot fseek level '%s' -- using empty level", filename); return; } # there exist Supaplex level package files with multi-part levels which # can be detected as follows: instead of leading and trailing dashes ('-') # to pad the level name, they have leading and trailing numbers which are # the x and y coordinations of the current part of the multi-part level; # if there are '?' characters instead of numbers on the left or right side # of the level name, the multi-part level consists of only horizontal or # vertical parts for (l = nr; l < NUM_SUPAPLEX_LEVELS_PER_PACKAGE; l++) { LoadLevelFromFileStream_SP(file, level, l); # check if this level is a part of a bigger multi-part level name_first = level->name[0]; name_last = level->name[SP_LEVEL_NAME_LEN - 1]; is_multipart_level = ((name_first == '?' || (name_first >= '0' && name_first <= '9')) && (name_last == '?' || (name_last >= '0' && name_last <= '9'))); is_first_part = ((name_first == '?' || name_first == '1') && (name_last == '?' || name_last == '1')); # correct leading multipart level meta information in level name for (i = 0; i < SP_LEVEL_NAME_LEN && level->name[i] == name_first; i++) level->name[i] = '-'; # correct trailing multipart level meta information in level name for (i = SP_LEVEL_NAME_LEN - 1; i>=0 && level->name[i] == name_last; i--) level->name[i] = '-'; # ---------- check for normal single level ---------- if (!reading_multipart_level && !is_multipart_level) { # the current level is simply a normal single-part level, and we are # not reading a multi-part level yet, so return the level as it is break; } # ---------- check for empty level (unused multi-part) ---------- if (!reading_multipart_level && is_multipart_level && !is_first_part) { # this is a part of a multi-part level, but not the first part # (and we are not already reading parts of a multi-part level); # in this case, use an empty level instead of the single part use_empty_level = TRUE; break; } # ---------- check for finished multi-part level ---------- if (reading_multipart_level && (!is_multipart_level || !strEqual(level->name, multipart_level.name))) { # we are already reading parts of a multi-part level, but this level is # either not a multi-part level, or a part of a different multi-part # level; in both cases, the multi-part level seems to be complete break; } # ---------- here we have one part of a multi-part level ---------- reading_multipart_level = TRUE; if (is_first_part) # start with first part of new multi-part level { # copy level info structure from first part multipart_level = *level; # clear playfield of new multi-part level for (y = 0; y < MAX_LEV_FIELDY; y++) for (x = 0; x < MAX_LEV_FIELDX; x++) multipart_level.field[x][y] = EL_EMPTY; } if (name_first == '?') name_first = '1'; if (name_last == '?') name_last = '1'; multipart_xpos = (int)(name_first - '0'); multipart_ypos = (int)(name_last - '0'); #if 0 #endif if (multipart_xpos * SP_LEVEL_XSIZE > MAX_LEV_FIELDX || multipart_ypos * SP_LEVEL_YSIZE > MAX_LEV_FIELDY) { Error(ERR_WARN, "multi-part level is too big -- ignoring part of it"); break; } multipart_level.fieldx = MAX(multipart_level.fieldx, multipart_xpos * SP_LEVEL_XSIZE); multipart_level.fieldy = MAX(multipart_level.fieldy, multipart_ypos * SP_LEVEL_YSIZE); # copy level part at the right position of multi-part level for (y = 0; y < SP_LEVEL_YSIZE; y++) { for (x = 0; x < SP_LEVEL_XSIZE; x++) { int start_x = (multipart_xpos - 1) * SP_LEVEL_XSIZE; int start_y = (multipart_ypos - 1) * SP_LEVEL_YSIZE; multipart_level.field[start_x + x][start_y + y] = level->field[x][y]; } } } fclose(file); if (use_empty_level) { setLevelInfoToDefaults(level); level->fieldx = SP_LEVEL_XSIZE; level->fieldy = SP_LEVEL_YSIZE; for (y = 0; y < SP_LEVEL_YSIZE; y++) for (x = 0; x < SP_LEVEL_XSIZE; x++) level->field[x][y] = EL_EMPTY; strcpy(level->name, "-------- EMPTY --------"); Error(ERR_WARN, "single part of multi-part level -- using empty level"); } if (reading_multipart_level) *level = multipart_level; } # ------------------------------------------------------------------------- # functions for loading generic level # ------------------------------------------------------------------------- void LoadLevelFromFileInfo(struct LevelInfo *level, struct LevelFileInfo *level_file_info) { # always start with reliable default values setLevelInfoToDefaults(level); switch (level_file_info->type) { case LEVEL_FILE_TYPE_RND: LoadLevelFromFileInfo_RND(level, level_file_info); break; case LEVEL_FILE_TYPE_EM: LoadLevelFromFileInfo_EM(level, level_file_info); level->game_engine_type = GAME_ENGINE_TYPE_EM; break; case LEVEL_FILE_TYPE_SP: LoadLevelFromFileInfo_SP(level, level_file_info); break; default: LoadLevelFromFileInfo_RND(level, level_file_info); break; } # if level file is invalid, restore level structure to default values if (level->no_valid_file) setLevelInfoToDefaults(level); if (level->game_engine_type == GAME_ENGINE_TYPE_UNKNOWN) level->game_engine_type = GAME_ENGINE_TYPE_RND; if (level_file_info->type != LEVEL_FILE_TYPE_RND) CopyNativeLevel_Native_to_RND(level); } void LoadLevelFromFilename(struct LevelInfo *level, char *filename) { static struct LevelFileInfo level_file_info; # always start with reliable default values setFileInfoToDefaults(&level_file_info); level_file_info.nr = 0; # unknown level number level_file_info.type = LEVEL_FILE_TYPE_RND; # no others supported yet level_file_info.filename = filename; LoadLevelFromFileInfo(level, &level_file_info); } static void LoadLevel_InitVersion(struct LevelInfo *level, char *filename) { int i, j; if (leveldir_current == NULL) # only when dumping level return; # all engine modifications also valid for levels which use latest engine if (level->game_version < VERSION_IDENT(3,2,0,5)) { # time bonus score was given for 10 s instead of 1 s before 3.2.0-5 level->score[SC_TIME_BONUS] /= 10; } #if 0 #endif if (leveldir_current->latest_engine) { # ---------- use latest game engine ----------------------------------- # For all levels which are forced to use the latest game engine version # (normally all but user contributed, private and undefined levels), set # the game engine version to the actual version; this allows for actual # corrections in the game engine to take effect for existing, converted # levels (from "classic" or other existing games) to make the emulation # of the corresponding game more accurate, while (hopefully) not breaking # existing levels created from other players. level->game_version = GAME_VERSION_ACTUAL; # Set special EM style gems behaviour: EM style gems slip down from # normal, steel and growing wall. As this is a more fundamental change, # it seems better to set the default behaviour to "off" (as it is more # natural) and make it configurable in the level editor (as a property # of gem style elements). Already existing converted levels (neither # private nor contributed levels) are changed to the new behaviour. if (level->file_version < FILE_VERSION_2_0) level->em_slippery_gems = TRUE; return; } # ---------- use game engine the level was created with ----------------- # For all levels which are not forced to use the latest game engine # version (normally user contributed, private and undefined levels), # use the version of the game engine the levels were created for. # # Since 2.0.1, the game engine version is now directly stored # in the level file (chunk "VERS"), so there is no need anymore # to set the game version from the file version (except for old, # pre-2.0 levels, where the game version is still taken from the # file format version used to store the level -- see above). # player was faster than enemies in 1.0.0 and before if (level->file_version == FILE_VERSION_1_0) for (i = 0; i < MAX_PLAYERS; i++) level->initial_player_stepsize[i] = STEPSIZE_FAST; # default behaviour for EM style gems was "slippery" only in 2.0.1 if (level->game_version == VERSION_IDENT(2,0,1,0)) level->em_slippery_gems = TRUE; # springs could be pushed over pits before (pre-release version) 2.2.0 if (level->game_version < VERSION_IDENT(2,2,0,0)) level->use_spring_bug = TRUE; if (level->game_version < VERSION_IDENT(3,2,0,5)) { # time orb caused limited time in endless time levels before 3.2.0-5 level->use_time_orb_bug = TRUE; # default behaviour for snapping was "no snap delay" before 3.2.0-5 level->block_snap_field = FALSE; # extra time score was same value as time left score before 3.2.0-5 level->extra_time_score = level->score[SC_TIME_BONUS]; #if 0 #endif } if (level->game_version < VERSION_IDENT(3,2,0,7)) { # default behaviour for snapping was "not continuous" before 3.2.0-7 level->continuous_snapping = FALSE; } # only few elements were able to actively move into acid before 3.1.0 # trigger settings did not exist before 3.1.0; set to default "any" if (level->game_version < VERSION_IDENT(3,1,0,0)) { # correct "can move into acid" settings (all zero in old levels) level->can_move_into_acid_bits = 0; # nothing can move into acid level->dont_collide_with_bits = 0; # nothing is deadly when colliding setMoveIntoAcidProperty(level, EL_ROBOT, TRUE); setMoveIntoAcidProperty(level, EL_SATELLITE, TRUE); setMoveIntoAcidProperty(level, EL_PENGUIN, TRUE); setMoveIntoAcidProperty(level, EL_BALLOON, TRUE); for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) SET_PROPERTY(EL_CUSTOM_START + i, EP_CAN_MOVE_INTO_ACID, TRUE); # correct trigger settings (stored as zero == "none" in old levels) for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; struct ElementInfo *ei = &element_info[element]; for (j = 0; j < ei->num_change_pages; j++) { struct ElementChangeInfo *change = &ei->change_page[j]; change->trigger_player = CH_PLAYER_ANY; change->trigger_page = CH_PAGE_ANY; } } } # try to detect and fix "Snake Bite" levels, which are broken with 3.2.0 { int element = EL_CUSTOM_START + 255; struct ElementInfo *ei = &element_info[element]; struct ElementChangeInfo *change = &ei->change_page[0]; # This is needed to fix a problem that was caused by a bugfix in function # game.c/CreateFieldExt() introduced with 3.2.0 that corrects the behaviour # when a custom element changes to EL_SOKOBAN_FIELD_PLAYER (before, it did # not replace walkable elements, but instead just placed the player on it, # without placing the Sokoban field under the player). Unfortunately, this # breaks "Snake Bite" style levels when the snake is halfway through a door # that just closes (the snake head is still alive and can be moved in this # case). This can be fixed by replacing the EL_SOKOBAN_FIELD_PLAYER by the # player (without Sokoban element) which then gets killed as designed). if ((strncmp(leveldir_current->identifier, "snake_bite", 10) == 0 || strncmp(ei->description, "pause b4 death", 14) == 0) && change->target_element == EL_SOKOBAN_FIELD_PLAYER) change->target_element = EL_PLAYER_1; } } static void LoadLevel_InitElements(struct LevelInfo *level, char *filename) { int i, j, x, y; # map custom element change events that have changed in newer versions # (these following values were accidentally changed in version 3.0.1) # (this seems to be needed only for 'ab_levelset3' and 'ab_levelset4') if (level->game_version <= VERSION_IDENT(3,0,0,0)) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; # order of checking and copying events to be mapped is important # (do not change the start and end value -- they are constant) for (j = CE_BY_OTHER_ACTION; j >= CE_VALUE_GETS_ZERO; j--) { if (HAS_CHANGE_EVENT(element, j - 2)) { SET_CHANGE_EVENT(element, j - 2, FALSE); SET_CHANGE_EVENT(element, j, TRUE); } } # order of checking and copying events to be mapped is important # (do not change the start and end value -- they are constant) for (j = CE_PLAYER_COLLECTS_X; j >= CE_HITTING_SOMETHING; j--) { if (HAS_CHANGE_EVENT(element, j - 1)) { SET_CHANGE_EVENT(element, j - 1, FALSE); SET_CHANGE_EVENT(element, j, TRUE); } } } } # initialize "can_change" field for old levels with only one change page if (level->game_version <= VERSION_IDENT(3,0,2,0)) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; if (CAN_CHANGE(element)) element_info[element].change->can_change = TRUE; } } # correct custom element values (for old levels without these options) if (level->game_version < VERSION_IDENT(3,1,1,0)) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; struct ElementInfo *ei = &element_info[element]; if (ei->access_direction == MV_NO_DIRECTION) ei->access_direction = MV_ALL_DIRECTIONS; } } # correct custom element values (fix invalid values for all versions) if (1) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; struct ElementInfo *ei = &element_info[element]; for (j = 0; j < ei->num_change_pages; j++) { struct ElementChangeInfo *change = &ei->change_page[j]; if (change->trigger_player == CH_PLAYER_NONE) change->trigger_player = CH_PLAYER_ANY; if (change->trigger_side == CH_SIDE_NONE) change->trigger_side = CH_SIDE_ANY; } } } # initialize "can_explode" field for old levels which did not store this # !!! CHECK THIS -- "<= 3,1,0,0" IS PROBABLY WRONG !!! if (level->game_version <= VERSION_IDENT(3,1,0,0)) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; if (EXPLODES_1X1_OLD(element)) element_info[element].explosion_type = EXPLODES_1X1; SET_PROPERTY(element, EP_CAN_EXPLODE, (EXPLODES_BY_FIRE(element) || EXPLODES_SMASHED(element) || EXPLODES_IMPACT(element))); } } # correct previously hard-coded move delay values for maze runner style if (level->game_version < VERSION_IDENT(3,1,1,0)) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; if (element_info[element].move_pattern & MV_MAZE_RUNNER_STYLE) { # previously hard-coded and therefore ignored element_info[element].move_delay_fixed = 9; element_info[element].move_delay_random = 0; } } } # map elements that have changed in newer versions level->amoeba_content = getMappedElementByVersion(level->amoeba_content, level->game_version); for (i = 0; i < MAX_ELEMENT_CONTENTS; i++) for (x = 0; x < 3; x++) for (y = 0; y < 3; y++) level->yamyam_content[i].e[x][y] = getMappedElementByVersion(level->yamyam_content[i].e[x][y], level->game_version); # initialize element properties for level editor etc. InitElementPropertiesEngine(level->game_version); InitElementPropertiesAfterLoading(level->game_version); } static void LoadLevel_InitPlayfield(struct LevelInfo *level, char *filename) { int x, y; # map elements that have changed in newer versions for (y = 0; y < level->fieldy; y++) for (x = 0; x < level->fieldx; x++) level->field[x][y] = getMappedElementByVersion(level->field[x][y], level->game_version); # copy elements to runtime playfield array for (x = 0; x < MAX_LEV_FIELDX; x++) for (y = 0; y < MAX_LEV_FIELDY; y++) Feld[x][y] = level->field[x][y]; # initialize level size variables for faster access lev_fieldx = level->fieldx; lev_fieldy = level->fieldy; # determine border element for this level SetBorderElement(); } static void LoadLevel_InitNativeEngines(struct LevelInfo *level,char *filename) { struct LevelFileInfo *level_file_info = &level->file_info; if (level_file_info->type == LEVEL_FILE_TYPE_RND) CopyNativeLevel_RND_to_Native(level); } void LoadLevelTemplate(int nr) { char *filename; setLevelFileInfo(&level_template.file_info, nr); filename = level_template.file_info.filename; LoadLevelFromFileInfo(&level_template, &level_template.file_info); LoadLevel_InitVersion(&level_template, filename); LoadLevel_InitElements(&level_template, filename); ActivateLevelTemplate(); } void LoadLevel(int nr) { char *filename; setLevelFileInfo(&level.file_info, nr); filename = level.file_info.filename; LoadLevelFromFileInfo(&level, &level.file_info); if (level.use_custom_template) LoadLevelTemplate(-1); LoadLevel_InitVersion(&level, filename); LoadLevel_InitElements(&level, filename); LoadLevel_InitPlayfield(&level, filename); LoadLevel_InitNativeEngines(&level, filename); } static int SaveLevel_VERS(FILE *file, struct LevelInfo *level) { int chunk_size = 0; chunk_size += putFileVersion(file, level->file_version); chunk_size += putFileVersion(file, level->game_version); return chunk_size; } static int SaveLevel_DATE(FILE *file, struct LevelInfo *level) { int chunk_size = 0; chunk_size += putFile16BitBE(file, level->creation_date.year); chunk_size += putFile8Bit(file, level->creation_date.month); chunk_size += putFile8Bit(file, level->creation_date.day); return chunk_size; } static int SaveLevel_NAME(FILE *file, struct LevelInfo *level) { int chunk_size = 0; int i; for (i = 0; i < MAX_LEVEL_NAME_LEN; i++) chunk_size += putFile8Bit(file, level->name[i]); return chunk_size; } static int SaveLevel_AUTH(FILE *file, struct LevelInfo *level) { int chunk_size = 0; int i; for (i = 0; i < MAX_LEVEL_AUTHOR_LEN; i++) chunk_size += putFile8Bit(file, level->author[i]); return chunk_size; } static int SaveLevel_BODY(FILE *file, struct LevelInfo *level) { int chunk_size = 0; int x, y; for (y = 0; y < level->fieldy; y++) for (x = 0; x < level->fieldx; x++) chunk_size += putFile16BitBE(file, level->field[x][y]); return chunk_size; } static int SaveLevel_MicroChunk(FILE *file, struct LevelFileConfigInfo *entry, boolean write_element) { int save_type = entry->save_type; int data_type = entry->data_type; int conf_type = entry->conf_type; int byte_mask = conf_type & CONF_MASK_BYTES; int element = entry->element; int default_value = entry->default_value; int num_bytes = 0; boolean modified = FALSE; if (byte_mask != CONF_MASK_MULTI_BYTES) { void *value_ptr = entry->value; int value = (data_type == TYPE_BOOLEAN ? *(boolean *)value_ptr : *(int *)value_ptr); # check if any settings have been modified before saving them if (value != default_value) modified = TRUE; # do not save if explicitly told or if unmodified default settings if ((save_type == SAVE_CONF_NEVER) || (save_type == SAVE_CONF_WHEN_CHANGED && !modified)) return 0; if (write_element) num_bytes += putFile16BitBE(file, element); num_bytes += putFile8Bit(file, conf_type); num_bytes += (byte_mask == CONF_MASK_1_BYTE ? putFile8Bit (file, value) : byte_mask == CONF_MASK_2_BYTE ? putFile16BitBE(file, value) : byte_mask == CONF_MASK_4_BYTE ? putFile32BitBE(file, value) : 0); } else if (data_type == TYPE_STRING) { char *default_string = entry->default_string; char *string = (char *)(entry->value); int string_length = strlen(string); int i; # check if any settings have been modified before saving them if (!strEqual(string, default_string)) modified = TRUE; # do not save if explicitly told or if unmodified default settings if ((save_type == SAVE_CONF_NEVER) || (save_type == SAVE_CONF_WHEN_CHANGED && !modified)) return 0; if (write_element) num_bytes += putFile16BitBE(file, element); num_bytes += putFile8Bit(file, conf_type); num_bytes += putFile16BitBE(file, string_length); for (i = 0; i < string_length; i++) num_bytes += putFile8Bit(file, string[i]); } else if (data_type == TYPE_ELEMENT_LIST) { int *element_array = (int *)(entry->value); int num_elements = *(int *)(entry->num_entities); int i; # check if any settings have been modified before saving them for (i = 0; i < num_elements; i++) if (element_array[i] != default_value) modified = TRUE; # do not save if explicitly told or if unmodified default settings if ((save_type == SAVE_CONF_NEVER) || (save_type == SAVE_CONF_WHEN_CHANGED && !modified)) return 0; if (write_element) num_bytes += putFile16BitBE(file, element); num_bytes += putFile8Bit(file, conf_type); num_bytes += putFile16BitBE(file, num_elements * CONF_ELEMENT_NUM_BYTES); for (i = 0; i < num_elements; i++) num_bytes += putFile16BitBE(file, element_array[i]); } else if (data_type == TYPE_CONTENT_LIST) { struct Content *content = (struct Content *)(entry->value); int num_contents = *(int *)(entry->num_entities); int i, x, y; # check if any settings have been modified before saving them for (i = 0; i < num_contents; i++) for (y = 0; y < 3; y++) for (x = 0; x < 3; x++) if (content[i].e[x][y] != default_value) modified = TRUE; # do not save if explicitly told or if unmodified default settings if ((save_type == SAVE_CONF_NEVER) || (save_type == SAVE_CONF_WHEN_CHANGED && !modified)) return 0; if (write_element) num_bytes += putFile16BitBE(file, element); num_bytes += putFile8Bit(file, conf_type); num_bytes += putFile16BitBE(file, num_contents * CONF_CONTENT_NUM_BYTES); for (i = 0; i < num_contents; i++) for (y = 0; y < 3; y++) for (x = 0; x < 3; x++) num_bytes += putFile16BitBE(file, content[i].e[x][y]); } return num_bytes; } static int SaveLevel_INFO(FILE *file, struct LevelInfo *level) { int chunk_size = 0; int i; li = *level; # copy level data into temporary buffer for (i = 0; chunk_config_INFO[i].data_type != -1; i++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_INFO[i], FALSE); return chunk_size; } static int SaveLevel_ELEM(FILE *file, struct LevelInfo *level) { int chunk_size = 0; int i; li = *level; # copy level data into temporary buffer for (i = 0; chunk_config_ELEM[i].data_type != -1; i++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_ELEM[i], TRUE); return chunk_size; } static int SaveLevel_NOTE(FILE *file, struct LevelInfo *level, int element) { int envelope_nr = element - EL_ENVELOPE_1; int chunk_size = 0; int i; chunk_size += putFile16BitBE(file, element); # copy envelope data into temporary buffer xx_envelope = level->envelope[envelope_nr]; for (i = 0; chunk_config_NOTE[i].data_type != -1; i++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_NOTE[i], FALSE); return chunk_size; } static int SaveLevel_CUSX(FILE *file, struct LevelInfo *level, int element) { struct ElementInfo *ei = &element_info[element]; int chunk_size = 0; int i, j; chunk_size += putFile16BitBE(file, element); xx_ei = *ei; # copy element data into temporary buffer # set default description string for this specific element strcpy(xx_default_description, getDefaultElementDescription(ei)); # set (fixed) number of content areas (may have been overwritten earlier) xx_num_contents = 1; for (i = 0; chunk_config_CUSX_base[i].data_type != -1; i++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_CUSX_base[i], FALSE); for (i = 0; i < ei->num_change_pages; i++) { struct ElementChangeInfo *change = &ei->change_page[i]; xx_current_change_page = i; xx_change = *change; # copy change data into temporary buffer resetEventBits(); setEventBitsFromEventFlags(change); for (j = 0; chunk_config_CUSX_change[j].data_type != -1; j++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_CUSX_change[j], FALSE); } return chunk_size; } static int SaveLevel_GRPX(FILE *file, struct LevelInfo *level, int element) { struct ElementInfo *ei = &element_info[element]; struct ElementGroupInfo *group = ei->group; int chunk_size = 0; int i; chunk_size += putFile16BitBE(file, element); xx_ei = *ei; # copy element data into temporary buffer xx_group = *group; # copy group data into temporary buffer # set default description string for this specific element strcpy(xx_default_description, getDefaultElementDescription(ei)); for (i = 0; chunk_config_GRPX[i].data_type != -1; i++) chunk_size += SaveLevel_MicroChunk(file, &chunk_config_GRPX[i], FALSE); return chunk_size; } static void SaveLevelFromFilename(struct LevelInfo *level, char *filename) { int chunk_size; int i; FILE *file; if (!(file = fopen(filename, MODE_WRITE))) { Error(ERR_WARN, "cannot save level file '%s'", filename); return; } level->file_version = FILE_VERSION_ACTUAL; level->game_version = GAME_VERSION_ACTUAL; level->creation_date = getCurrentDate(); putFileChunkBE(file, "RND1", CHUNK_SIZE_UNDEFINED); putFileChunkBE(file, "CAVE", CHUNK_SIZE_NONE); chunk_size = SaveLevel_VERS(NULL, level); putFileChunkBE(file, "VERS", chunk_size); SaveLevel_VERS(file, level); chunk_size = SaveLevel_DATE(NULL, level); putFileChunkBE(file, "DATE", chunk_size); SaveLevel_DATE(file, level); chunk_size = SaveLevel_NAME(NULL, level); putFileChunkBE(file, "NAME", chunk_size); SaveLevel_NAME(file, level); chunk_size = SaveLevel_AUTH(NULL, level); putFileChunkBE(file, "AUTH", chunk_size); SaveLevel_AUTH(file, level); chunk_size = SaveLevel_INFO(NULL, level); putFileChunkBE(file, "INFO", chunk_size); SaveLevel_INFO(file, level); chunk_size = SaveLevel_BODY(NULL, level); putFileChunkBE(file, "BODY", chunk_size); SaveLevel_BODY(file, level); chunk_size = SaveLevel_ELEM(NULL, level); if (chunk_size > LEVEL_CHUNK_ELEM_UNCHANGED) # save if changed { putFileChunkBE(file, "ELEM", chunk_size); SaveLevel_ELEM(file, level); } for (i = 0; i < NUM_ENVELOPES; i++) { int element = EL_ENVELOPE_1 + i; chunk_size = SaveLevel_NOTE(NULL, level, element); if (chunk_size > LEVEL_CHUNK_NOTE_UNCHANGED) # save if changed { putFileChunkBE(file, "NOTE", chunk_size); SaveLevel_NOTE(file, level, element); } } # if not using template level, check for non-default custom/group elements if (!level->use_custom_template) { for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) { int element = EL_CUSTOM_START + i; chunk_size = SaveLevel_CUSX(NULL, level, element); if (chunk_size > LEVEL_CHUNK_CUSX_UNCHANGED) # save if changed { putFileChunkBE(file, "CUSX", chunk_size); SaveLevel_CUSX(file, level, element); } } for (i = 0; i < NUM_GROUP_ELEMENTS; i++) { int element = EL_GROUP_START + i; chunk_size = SaveLevel_GRPX(NULL, level, element); if (chunk_size > LEVEL_CHUNK_GRPX_UNCHANGED) # save if changed { putFileChunkBE(file, "GRPX", chunk_size); SaveLevel_GRPX(file, level, element); } } } fclose(file); SetFilePermissions(filename, PERMS_PRIVATE); } void SaveLevel(int nr) { char *filename = getDefaultLevelFilename(nr); SaveLevelFromFilename(&level, filename); } void SaveLevelTemplate() { char *filename = getDefaultLevelFilename(-1); SaveLevelFromFilename(&level, filename); } void DumpLevel(struct LevelInfo *level) { if (level->no_valid_file) { Error(ERR_WARN, "cannot dump -- no valid level file found"); return; } printf_line("-", 79); printf("Level xxx (file version %08d, game version %08d)\n", level->file_version, level->game_version); printf_line("-", 79); printf("Level author: '%s'\n", level->author); printf("Level title: '%s'\n", level->name); printf("\n"); printf("Playfield size: %d x %d\n", level->fieldx, level->fieldy); printf("\n"); printf("Level time: %d seconds\n", level->time); printf("Gems needed: %d\n", level->gems_needed); printf("\n"); printf("Time for magic wall: %d seconds\n", level->time_magic_wall); printf("Time for wheel: %d seconds\n", level->time_wheel); printf("Time for light: %d seconds\n", level->time_light); printf("Time for timegate: %d seconds\n", level->time_timegate); printf("\n"); printf("Amoeba speed: %d\n", level->amoeba_speed); printf("\n"); printf("EM style slippery gems: %s\n", (level->em_slippery_gems ? "yes" : "no")); printf("Player blocks last field: %s\n", (level->block_last_field ? "yes" : "no")); printf("SP player blocks last field: %s\n", (level->sp_block_last_field ? "yes" : "no")); printf("use spring bug: %s\n", (level->use_spring_bug ? "yes" : "no")); printf("use step counter: %s\n", (level->use_step_counter ? "yes" : "no")); printf_line("-", 79); }