
# TODO: (maybe) make level.dup (and tidy LevelInfo class in general)
# TODO: modify classes' attribute writers to convert values to permitted types
# TODO: make first letter of LoadLevel etc. lowercase

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   
FILE_VERS_CHUNK_SIZE    = 8   # size of file version chunk
LEVEL_HEADER_SIZE       = 80  # size of level file header 
LEVEL_HEADER_UNUSED     = 0   # unused level header bytes 
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_CHUNK_CNT3_UNUSED = 10  # unused CNT3 chunk bytes   
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  
TAPE_HEADER_SIZE        = 20  # size of tape file header  
TAPE_HEADER_UNUSED      = 3   # unused tape header bytes  

# FIXME: change these into functions (or stop using them)
LEVEL_CHUNK_CNT3_SIZE(x) = (LEVEL_CHUNK_CNT3_HEADER + (x))
LEVEL_CHUNK_CUS3_SIZE(x) = (2 + (x) * LEVEL_CPART_CUS3_SIZE)
LEVEL_CHUNK_CUS4_SIZE(x) = (48 + 48 + (x) * 48)

# file identifier strings
LEVEL_COOKIE_TMPL   = "ROCKSNDIAMONDS_LEVEL_FILE_VERSION_x.x"
TAPE_COOKIE_TMPL    = "ROCKSNDIAMONDS_TAPE_FILE_VERSION_x.x"
SCORE_COOKIE        = "ROCKSNDIAMONDS_SCORE_FILE_VERSION_1.2"

# =========================================================================
# level file functions
# =========================================================================

# -------------------------------------------------------------------------
# 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)
        EL_UNKNOWN
      end
      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
      else element
    end
  end

  element
end

def LoadLevel_VERS(file, chunk_size, level)
  level.file_version = getFileVersion(file)
  level.game_version = getFileVersion(file)
  chunk_size
end

def LoadLevel_HEAD(file, chunk_size, level)
  int i, x, y;

  level.fieldx = file.get8
  level.fieldy = file.get8

  level.time           = file.get16be
  level.gems_needed    = file.get16be

  level.name = file.getCharacters(MAX_LEVEL_NAME_LEN)

  level.score = []
  LEVEL_SCORE_ELEMENTS.times { level.score.push(file.get8) }

  level.num_yamyam_contents = STD_ELEMENT_CONTENTS
  Misc.loop3d(STD_ELEMENT_CONTENTS,3,3) do |i,x,y|
    level.yamyam_content[i][x][y] = getMappedElement(file.get8)
  end

  level.amoeba_speed       = file.get8
  level.time_magic_wall    = file.get8
  level.time_wheel         = file.get8
  level.amoeba_content     = getMappedElement(file.get8)
  level.double_speed       = file.getBool
  level.initial_gravity    = file.getBool
  level.encoding_16bit_field   = file.getBool
  level.em_slippery_gems   = file.getBool

  level.use_custom_template    = file.getBool

  level.block_last_field   = file.getBool
  level.sp_block_last_field    = file.getBool
  level.can_move_into_acid_bits = file.get32be
  level.dont_collide_with_bits = file.get8

  level.use_spring_bug     = file.getBool
  level.use_step_counter   = file.getBool

  level.instant_relocation = file.getBool
  level.can_pass_to_walkable   = file.getBool
  level.grow_into_diggable = file.getBool

  level.game_engine_type   = file.get8

  ReadUnusedBytesFromFile(file, LEVEL_HEADER_UNUSED)

  chunk_size
end

def LoadLevel_AUTH(file, chunk_size, level)
  level.author = file.getCharacters(MAX_LEVEL_AUTHOR_LEN)

  chunk_size
end

def LoadLevel_BODY(file, chunk_size, level)
  chunk_size_expected = level->fieldx * level->fieldy

  # Note: "chunk_size" was wrong before version 2.0 when elements are
  # stored with 16-bit encoding (and should be twice as big then).
  # Even worse, playfield data was stored 16-bit when only yamyam content
  # contained 16-bit elements and vice versa.

  if level->encoding_16bit_field && level->file_version >= FILE_VERSION_2_0
    chunk_size_expected *= 2
  end

  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size)
    return chunk_size_expected
  end

  Misc.loop2d(level.fieldx,level.fieldy) do |x,y|
    e = level.encoding_16bit_field ? file.get16be : file.get8
    level.field[x][y] = getMappedElement(e)
  end
  
  chunk_size
end

def LoadLevel_CONT(file, chunk_size, level)
  header_size = 4
  content_size = MAX_ELEMENT_CONTENTS * 3 * 3
  chunk_size_expected = header_size + content_size

  # Note: "chunk_size" was wrong before version 2.0 when elements are
  # stored with 16-bit encoding (and should be twice as big then).
  # Even worse, playfield data was stored 16-bit when only yamyam content
  # contained 16-bit elements and vice versa.

  if level.encoding_16bit_field && level.file_version >= FILE_VERSION_2_0
    chunk_size_expected += content_size
  end

  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size)
    return chunk_size_expected
  end

  file.get8
  level.num_yamyam_contents = file.get8
  file.get8
  file.get8

  # correct invalid number of content fields -- should never happen
  if !(1..MAX_ELEMENT_CONTENTS).include?(level.num_yamyam_contents)
    level.num_yamyam_contents = STD_ELEMENT_CONTENTS
  end

  Misc.loop3d(MAX_ELEMENT_CONTENTS,3,3) do |i,x,y|
    e = level.encoding_16bit_field ? file.get16be : file.get8
    level.yamyam_content[i][x][y] = getMappedElement(e)
  end
  
  chunk_size
end

def LoadLevel_CNT2(file, chunk_size, level)
  content_array = Misc.array(MAX_ELEMENT_CONTENTS,3,3,nil)

  element = getMappedElement(file.get16be)
  num_contents = file.get8
  content_xsize = file.get8
  content_ysize = file.get8

  ReadUnusedBytesFromFile(file, LEVEL_CHUNK_CNT2_UNUSED)

  Misc.loop3d(MAX_ELEMENT_CONTENTS,3,3) do |i,x,y|
    content_array[i][x][y] = getMappedElement(file.get16be)
  end

  # correct invalid number of content fields -- should never happen
  if !(1..MAX_ELEMENT_CONTENTS).include?(num_contents)
    num_contents = STD_ELEMENT_CONTENTS
  end

  if element == EL_YAMYAM
    level.num_yamyam_contents = num_contents;

    Misc.loop3d(num_contents,3,3) do |i,x,y|
      level->yamyam_content[i][x][y] = content_array[i][x][y]
    end
  elsif element == EL_BD_AMOEBA
    level.amoeba_content = content_array[0][0][0]
  else
    error(ERR_WARN, "cannot load content for element '#{element}'")
  end

  chunk_size
end

def LoadLevel_CNT3(file, chunk_size, level)
{
  element = getMappedElement(file.get16be)
  element = EL_ENVELOPE_1 if !IS_ENVELOPE(element)

  envelope_nr = element - EL_ENVELOPE_1

  envelope_len = file.get16be

  level.envelope_xsize[envelope_nr] = file.get8
  level.envelope_ysize[envelope_nr] = file.get8

  ReadUnusedBytesFromFile(file, LEVEL_CHUNK_CNT3_UNUSED)

  chunk_size_expected = LEVEL_CHUNK_CNT3_SIZE(envelope_len)
  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size - LEVEL_CHUNK_CNT3_HEADER)
    return chunk_size_expected
  end

  level.envelope_text[envelope_nr] = ""
  envelope_len.times { level.envelope_text << file.get8 }

  chunk_size
end

def LoadLevel_CUS1(file, chunk_size, level)
  num_changed_custom_elements = file.get16be
  chunk_size_expected = 2 + num_changed_custom_elements * 6
  
  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size - 2)
    return chunk_size_expected
  end

  num_changed_custom_elements.times do |i|
    element = file.get16be
    properties = file.get32be

    if IS_CUSTOM_ELEMENT(element)
      # TODO: handle bitmasks right
      Properties[element][EP_BITFIELD_BASE] = properties
    else
      error(ERR_WARN, "invalid custom element number "+element)
    end
  end

  chunk_size
end

def LoadLevel_CUS2(file, chunk_size, level)
  num_changed_custom_elements = file.get16be
  chunk_size_expected = 2 + num_changed_custom_elements * 4
  
  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size - 2)
    return chunk_size_expected
  end

  num_changed_custom_elements.times do |i|
    element = file.get16be
    custom_target_element = file.get16be

    if IS_CUSTOM_ELEMENT(element)
      element_info[element].change.target_element = custom_target_element
    else
      error(ERR_WARN, "invalid custom element number "+element)
    end
  end

  chunk_size
end

def LoadLevel_CUS3(file, chunk_size, level)
  num_changed_custom_elements = file.get16be
  chunk_size_expected = LEVEL_CHUNK_CUS3_SIZE(num_changed_custom_elements)
  
  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size - 2)
    return chunk_size_expected
  end

  num_changed_custom_elements.times do |i|
    element_id = file.get16be
    element = element_info[element_id]
    unsigned long event_bits

    if !IS_CUSTOM_ELEMENT(element_id)
      error(ERR_WARN, "invalid custom element_id number "+element_id)
      element_id = EL_INTERNAL_DUMMY
    end

    element.description = file.getCharacters(MAX_ELEMENT_NAME_LEN)

    Properties[element_id][EP_BITFIELD_BASE] = file.get32be

    # some free bytes for future properties and padding
    ReadUnusedBytesFromFile(file, 7)

    element.use_gfx_element = file.get8
    element.gfx_element = getMappedElement(file.get16be)

    element.collect_score = file.get8
    element.collect_count = file.get8

    element.push_delay_fixed  = file.get16be
    element.push_delay_random = file.get16be
    element.move_delay_fixed  = file.get16be
    element.move_delay_random = file.get16be

    element.move_pattern           = file.get16be
    element.move_direction_initial = file.get8
    element.move_stepsize          = file.get8

    Misc.loop2d(3,3) do |x,y|
      element.content[x][y] = getMappedElement(file.get16be)
    end

    event_bits = file.get32be
    NUM_CHANGE_EVENTS.times do |j|
      element.change.has_event[j] = true if event_bits & (1 << j)
    end

    element.change.target_element = getMappedElement(file.get16be)

    element.change.delay_fixed = file.get16be
    element.change.delay_random = file.get16be
    element.change.delay_frames = file.get16be

    element.change.trigger_element = getMappedElement(file.get16be)

    element.change.explode = file.get8
    element.change.use_target_content = file.get8
    element.change.only_if_complete = file.get8
    element.change.use_random_replace = file.get8

    element.change.random_percentage = file.get8
    element.change.replace_when = file.get8

    Misc.loop2d(3,3) do |x,y|
      element.change.target_content[x][y] = getMappedElement(file.get16be)
    end

    element.slippery_type = file.get8

    # some free bytes for future properties and padding
    ReadUnusedBytesFromFile(file, LEVEL_CPART_CUS3_UNUSED)

    # mark that this custom element_id has been modified
    element.modified_settings = true
  end

  chunk_size
end

def LoadLevel_CUS4(file, chunk_size, level)
{
  element = file.get16be

  if !IS_CUSTOM_ELEMENT(element)
    error(ERR_WARN, "invalid custom element number "+element)

    ReadUnusedBytesFromFile(file, chunk_size - 2)
    return chunk_size
  end

  ei = element_info[element]

  ei.description = file.getCharacters(MAX_ELEMENT_NAME_LEN)

  Properties[element][EP_BITFIELD_BASE] = file.get32be
  ReadUnusedBytesFromFile(file, 4) # reserved for more base properties

  ei.num_change_pages = file.get8

  # some free bytes for future base property values and padding
  ReadUnusedBytesFromFile(file, 5)

  chunk_size_expected = LEVEL_CHUNK_CUS4_SIZE(ei.num_change_pages)
  if chunk_size_expected != chunk_size
    ReadUnusedBytesFromFile(file, chunk_size - 48)
    return chunk_size_expected
  end

  # read custom property values

  ei.use_gfx_element = file.get8
  ei.gfx_element = getMappedElement(file.get16be)

  ei.collect_score = file.get8
  ei.collect_count = file.get8

  ei.drop_delay_fixed = file.get8
  ei.push_delay_fixed = file.get8
  ei.drop_delay_random = file.get8
  ei.push_delay_random = file.get8
  ei.move_delay_fixed = file.get16be
  ei.move_delay_random = file.get16be

  # bits 0 - 15 of "move_pattern" ...
  ei.move_pattern = file.get16be
  ei.move_direction_initial = file.get8
  ei.move_stepsize = file.get8

  ei.slippery_type = file.get8

  Misc.loop2d(3,3) do |x,y|
    ei.content[x][y] = getMappedElement(file.get16be)
  end

  ei.move_enter_element = getMappedElement(file.get16be)
  ei.move_leave_element = getMappedElement(file.get16be)
  ei.move_leave_type = file.get8

  # ... bits 16 - 31 of "move_pattern" (not nice, but downward compatible)
  ei.move_pattern |= (file.get16be << 16)

  ei.access_direction = file.get8

  ei.explosion_delay = file.get8
  ei.ignition_delay = file.get8
  ei.explosion_type = file.get8

  # some free bytes for future custom property values and padding
  ReadUnusedBytesFromFile(file, 1)

  # read change property values

  ei.num_change_pages.times do |i|
    change = ei.change_page[i]
    
    # always start with reliable default values
    change.initialize # TODO: make it so this won't be needed (and is it needed anyway?)

    event_bits = file.get32be
    NUM_CHANGE_EVENTS.times do |j|
      change.has_event[j] = true if event_bits & (1 << j)
    end

    change.target_element = getMappedElement(file.get16be)

    change.delay_fixed = file.get16be
    change.delay_random = file.get16be
    change.delay_frames = file.get16be

    change.trigger_element = getMappedElement(file.get16be)

    change.explode = file.get8
    change.use_target_content = file.get8
    change.only_if_complete = file.get8
    change.use_random_replace = file.get8

    change.random_percentage = file.get8
    change.replace_when = file.get8

    Misc.loop2d(3,3) do |x,y|
      change.target_content[x][y] = getMappedElement(file.get16be)
    end

    change.can_change = file.get8

    change.trigger_side = file.get8
    change.trigger_player = file.get8
    change.trigger_page = file.get8

    change.trigger_page = (change.trigger_page == CH_PAGE_ANY_FILE ?
                CH_PAGE_ANY : (1 << change.trigger_page))

    # some free bytes for future change property values and padding
    ReadUnusedBytesFromFile(file, 6)
  end

  # mark this custom element as modified
  ei.modified_settings = true

  chunk_size
}

def LoadLevel_GRP1(file, chunk_size, level)
  element = file.get16be

  if !IS_GROUP_ELEMENT(element)
    error(ERR_WARN, "invalid group element number "+element)

    ReadUnusedBytesFromFile(file, chunk_size - 2)
    return chunk_size
  end

  ei = element_info[element]

  ei.description = file.getCharacters(MAX_ELEMENT_NAME_LEN)

  ei.group.num_elements = file.get8

  ei.use_gfx_element = file.get8
  ei.gfx_element = getMappedElement(file.get16be)

  ei.group.choice_mode = file.get8

  # some free bytes for future values and padding
  ReadUnusedBytesFromFile(file, 3)

  MAX_ELEMENTS_IN_GROUP.times do |i|
    ei.group.element[i] = getMappedElement(file.get16be)
  end

  # mark this group element as modified
  ei.modified_settings = true

  chunk_size
end

def LoadLevelFromFileInfo_RND(level, level_file_info)
  filename = level_file_info.filename

  begin
    file = File.open(filename, "rb")
  rescue Exception
    level.no_valid_file = true

    if (level != &level_template)
      error(ERR_WARN, "cannot read level '#{filename}' -- using empty level")
    end

    return
  end

  chunk_name = file.getChunkName
  if chunk_name == "RND1"
    file.get32be       # not used

    chunk_name = file.getChunkName
    if chunk_name != "CAVE"
      level.no_valid_file = true
      error(ERR_WARN, "unknown format of level file '#{filename}'")
      file.close
      return
    end
  else  # check for pre-2.0 file format with cookie string
    cookie = (chunk_name + file.gets).chomp

    if !checkCookieString(cookie, LEVEL_COOKIE_TMPL)
      level.no_valid_file = true
      error(ERR_WARN, "unknown format of level file '#{filename}'")
      file.close
      return
    end

    if (level.file_version = getFileVersionFromCookieString(cookie)) == -1
      level.no_valid_file = true
      error(ERR_WARN, "unknown format of level file '#{filename}'")
      file.close
      return
    end

    # pre-2.0 level files have no game version, so use file version here
    level.game_version = level.file_version
  end

  if level.file_version < FILE_VERSION_1_2
    # level files from versions before 1.2.0 without chunk structure
    LoadLevel_HEAD(file, LEVEL_HEADER_SIZE,           level)
    LoadLevel_BODY(file, level.fieldx * level.fieldy, level)
  else
    # Note: I store only name and size here (not loader function as in original
    # R'n'D code), because I don't know how to make a "pointer to function" in
    # Ruby. List of loader functions is below.
    chunk_info = {
      "VERS" => FILE_VERS_CHUNK_SIZE,
      "HEAD" => LEVEL_HEADER_SIZE,
      "AUTH" => MAX_LEVEL_AUTHOR_LEN,
      "BODY" => -1,
      "CONT" => -1,
      "CNT2" => LEVEL_CHUNK_CNT2_SIZE,
      "CNT3" => -1,
      "CUS1" => -1, "CUS2" => -1, "CUS3" => -1, "CUS4" => -1,
      "GRP1" => -1,
    }

    while (chunk_name = file.getChunkName) && (chunk_size = file.get32be) && (!file.eof?)
      if chunk_info[chunk_name]
        error(ERR_WARN, "unknown chunk '#{chunk_name}' in level file '#{filename}'")
        ReadUnusedBytesFromFile(file, chunk_size)
      elsif (chunk_info[chunk_name] != -1 && chunk_info[chunk_name] != chunk_size)
        error(ERR_WARN, "wrong size (#{chunk_size}) of chunk '#{chunk_name}' in level file '#{filename}'")
        ReadUnusedBytesFromFile(file, chunk_size)
      else
        # call function to load this level chunk
        chunk_size_expected = case chunk_name
          when "VERS" then LoadLevel_VERS(file, chunk_size, level)
          when "HEAD" then LoadLevel_HEAD(file, chunk_size, level)
          when "AUTH" then LoadLevel_AUTH(file, chunk_size, level)
          when "BODY" then LoadLevel_BODY(file, chunk_size, level)
          when "CONT" then LoadLevel_CONT(file, chunk_size, level)
          when "CNT2" then LoadLevel_CNT2(file, chunk_size, level)
          when "CNT3" then LoadLevel_CNT3(file, chunk_size, level)
          when "CUS1" then LoadLevel_CUS1(file, chunk_size, level)
          when "CUS2" then LoadLevel_CUS2(file, chunk_size, level)
          when "CUS3" then LoadLevel_CUS3(file, chunk_size, level)
          when "CUS4" then LoadLevel_CUS4(file, chunk_size, level)
          when "GRP1" then LoadLevel_GRP1(file, chunk_size, level)
        end

        # 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 (#{chunk_size}) of chunk '#{chunk_name}' in level file '#{filename}'")
        end
      end
    end
  end

  file.close
end

# -------------------------------------------------------------------------
# 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 *ply1 = level_em->ply1;
#   struct PLAYER *ply2 = level_em->ply2;
#   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][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->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   = level->wind_direction_initial;
# 
#   for (i = 0; i < NUM_MAGIC_BALL_CONTENTS; i++)
#     for (j = 0; j < 8; j++)
#       lev->ball_array[i][j] =
#     map_element_RND_to_EM(level->
#                   ball_content[i][ball_xy[j][0]][ball_xy[j][1]]);
# 
#   for (i = 0; i < 16; i++)
#     lev->android_array[i] = false;  # !!! YET TO COME !!!
# 
#   # 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;
# 
#   # 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]);
# 
#     if (level->field[x][y] == EL_AMOEBA_DEAD)
#       new_element = map_element_RND_to_EM(EL_AMOEBA_WET);
# 
#     level_em->cave[x + 1][y + 1] = new_element;
#   }
# 
#   ply1->x_initial = 0;
#   ply1->y_initial = 0;
# 
#   ply2->x_initial = 0;
#   ply2->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 (level->field[x][y] == EL_PLAYER_1)
#     {
#       ply1->x_initial = x + 1;
#       ply1->y_initial = y + 1;
#       level_em->cave[x + 1][y + 1] = map_element_RND_to_EM(EL_EMPTY);
#     }
#     else if (level->field[x][y] == EL_PLAYER_2)
#     {
#       ply2->x_initial = x + 1;
#       ply2->y_initial = y + 1;
#       level_em->cave[x + 1][y + 1] = map_element_RND_to_EM(EL_EMPTY);
#     }
#   }
# }
# 
# 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 *ply1 = level_em->ply1;
#   struct PLAYER *ply2 = level_em->ply2;
#   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][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->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 = lev->wind_direction_initial;
# 
#   for (i = 0; i < NUM_MAGIC_BALL_CONTENTS; i++)
#     for (j = 0; j < 8; j++)
#       level->ball_content[i][ball_xy[j][0]][ball_xy[j][1]] =
#     map_element_EM_to_RND(lev->ball_array[i][j]);
# 
#   for (i = 0; i < 16; i++)
#     level->android_array[i] = false;    # !!! YET TO COME !!!
# 
#   # 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;
#   }
# 
#   # in case of both players set to the same field, use the first player
#   level->field[ply2->x_initial - 1][ply2->y_initial - 1] = EL_PLAYER_2;
#   level->field[ply1->x_initial - 1][ply1->y_initial - 1] = EL_PLAYER_1;
# }

def LoadLevelFromFileInfo_EM(level, level_file_info)
{
  # TODO: port LoadNativeLevel_EM from game_em/cave.c
  if !LoadNativeLevel_EM(level_file_info.filename)
    level.no_valid_file = true
  end
}

# 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                                           
# -------------------------------------------------------------------------

NUM_SUPAPLEX_LEVELS_PER_PACKAGE = 111
SP_LEVEL_SIZE                   = 1536
SP_LEVEL_XSIZE                  = 60
SP_LEVEL_YSIZE                  = 24
SP_LEVEL_NAME_LEN               = 23

def LoadLevelFromFileStream_SP(file, level, nr)
  # 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)
  Misc.loop2d(SP_LEVEL_XSIZE,SP_LEVEL_YSIZE) do |x,y|
    element_old = file.getc

    if element_old <= 0x27
      element_new = getMappedElement(EL_SP_START + element_old)
    elsif element_old == 0x28
      element_new = EL_INVISIBLE_WALL
    else
      error(ERR_WARN, "in level #{nr}, at position #{x}, #{y}:")
      error(ERR_WARN, "invalid level element #{element_old}")
      element_new = EL_UNKNOWN
    end

    level.field[x][y] = element_new
  end

  ReadUnusedBytesFromFile(file, 4) # (not used by Supaplex engine)

  # initial gravity: 1 == "on", anything else (0) == "off"
  level.initial_gravity = file.getBool

  ReadUnusedBytesFromFile(file, 1) # (not used by Supaplex engine)

  # level title in uppercase letters, padded with dashes ("-") (23 bytes)
  level.name = file.getCharacters(SP_LEVEL_NAME_LEN)

  # 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 = file.getc

  # number of special ("gravity") port entries below (maximum 10 allowed)
  num_special_ports = file.getc

  # database of properties of up to 10 special ports (6 bytes per port)
  10.times do |i|
    # 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 = file.get16be

    # change gravity: 1 == "turn on", anything else (0) == "turn off"
    gravity = file.getc

    # "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)

    next if i >= num_special_ports

    port_x = (port_location / 2) % SP_LEVEL_XSIZE
    port_y = (port_location / 2) / SP_LEVEL_XSIZE

    if !(0...SP_LEVEL_XSIZE).include?(port_x) || !(0...SP_LEVEL_YSIZE).include?(port_y)
      error(ERR_WARN, "special port position (#{port_x}, #{port_y}) out of bounds")
      next
    end

    port_element = level.field[port_x][port_y]

    if !(EL_SP_GRAVITY_PORT_RIGHT..EL_SP_GRAVITY_PORT_UP).include?(port_element)
      error(ERR_WARN, "no special port at position (#{port_x}, #{port_y})")
      next
    end

    # change previous (wrong) gravity inverting special port to either
    # gravity enabling special port or gravity disabling special port
    type = (gravity == 1 ? EL_SP_GRAVITY_ON_PORT_RIGHT : EL_SP_GRAVITY_OFF_PORT_RIGHT)
    level.field[port_x][port_y] += type - EL_SP_GRAVITY_PORT_RIGHT
  end

  ReadUnusedBytesFromFile(file, 4) # (not used by Supaplex engine)

  # change special gravity ports without database entries to normal ports
  Misc.loop2d(SP_LEVEL_XSIZE,SP_LEVEL_YSIZE) do |x,y|
    if (EL_SP_GRAVITY_PORT_RIGHT..EL_SP_GRAVITY_PORT_UP).include?(level.field[x][y])
      level.field[x][y] += EL_SP_PORT_RIGHT - EL_SP_GRAVITY_PORT_RIGHT
    end
  end

  # auto-determine number of infotrons if it was stored as "0" -- see above
  if level.gems_needed == 0
    Misc.loop2d(SP_LEVEL_XSIZE,SP_LEVEL_YSIZE) do |x,y|
      level.gems_needed += 1 if level.field[x][y] == EL_SP_INFOTRON
    end

    level.gems_needed &= 0xff     # only use low byte -- see above
  end

  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

  level.score = Array.new(LEVEL_SCORE_ELEMENTS, 0) # !!! CORRECT THIS !!!

  # there are no yamyams in supaplex levels
  Misc.loop3d(level.num_yamyam_contents,3,3) do |i,x,y|
    level.yamyam_content[i][x][y] = EL_EMPTY
  end
end

def LoadLevelFromFileInfo_SP(level, level_file_info)
  filename = level_file_info.filename
  nr = level_file_info.nr - leveldir_current.first_level
  multipart_level = nil
  reading_multipart_level = false
  use_empty_level = false

  begin
    file = File.open(filename, "rb")
  rescue Exception
    level.no_valid_file = true

    error(ERR_WARN, "cannot read level '#{filename}' -- using empty level")

    return
  end

  # position file stream to the requested level inside the level package
  begin
    if file.seek(nr * SP_LEVEL_SIZE, IO::SEEK_SET) != 0
      raise "it wasn't zero" # this exception will get catched on next line
    end
  rescue Exception
    level.no_valid_file = true

    error(ERR_WARN, "cannot fseek level '#{filename}' -- using empty level")

    return
  end

  # 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

  nr.upto(NUM_SUPAPLEX_LEVELS_PER_PACKAGE-1) do |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[-1]

    is_multipart_level =
      ((name_first == ?? || (?0..?9).include?(name_first)) &&
       (name_last  == ?? || (?0..?9).include?(name_first))

    is_first_part =
      ((name_first == ?? || name_first == ?1) &&
       (name_last  == ?? || name_last  == ?1))

    # correct leading multipart level meta information in level name
    SP_LEVEL_NAME_LEN.times do |i|
      break if level.name[i] != name_first
      level.name[i] = ?-
    end

    # correct trailing multipart level meta information in level name
    (SP_LEVEL_NAME_LEN-1).downto(0) do |i|
      break if level.name[i] != name_last
      level.name[i] = ?-
    end

    # ---------- 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
    end

    # ---------- 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
    end

    # ---------- check for finished multi-part level ----------

    if reading_multipart_level && (!is_multipart_level || 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
    end

    # ---------- 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.dup # TODO: maybe this isn't needed at this point, because level contains no data?

      # clear playfield of new multi-part level
      Misc.loop2d(MAX_LEV_FIELDX,MAX_LEV_FIELDY) do |x,y|
        multipart_level.field[x][y] = EL_EMPTY
      end
    end

    name_first = ?1 if name_first == ??
    name_last  = ?1 if name_last  == ??

    multipart_xpos = name_first - ?0
    multipart_ypos = name_last  - ?0

    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
    end

    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
    Misc.loop2d(SP_LEVEL_XSIZE,SP_LEVEL_YSIZE) do |x,y|
      start_x = (multipart_xpos - 1) * SP_LEVEL_XSIZE
      start_y = (multipart_ypos - 1) * SP_LEVEL_YSIZE

      multipart_level.field[start_x + x][start_y + y] = level.field[x][y]
    end
  end

  file.close

  if use_empty_level
    level.initialize

    level.fieldx = SP_LEVEL_XSIZE
    level.fieldy = SP_LEVEL_YSIZE

    Misc.loop2d(SP_LEVEL_XSIZE,SP_LEVEL_YSIZE) do |x,y|
      level.field[x][y] = EL_EMPTY
    end

    level.name = "-------- EMPTY --------"

    error(ERR_WARN, "single part of multi-part level -- using empty level")
  end

  if reading_multipart_level
    level = multipart_level.dup # TODO: maybe .dup isn't needed, because multipart_level is destroyed anyway?
  end
end

# -------------------------------------------------------------------------
# functions for loading generic level                                      
# -------------------------------------------------------------------------

def LoadLevelFromFileInfo(level, level_file_info)
  # always start with reliable default values
  level.initialize

  case level_file_info.type
    when LEVEL_FILE_TYPE_RND
      LoadLevelFromFileInfo_RND(level, level_file_info)
      
    when LEVEL_FILE_TYPE_EM
      LoadLevelFromFileInfo_EM(level, level_file_info)
      level.game_engine_type = GAME_ENGINE_TYPE_EM
      
    when LEVEL_FILE_TYPE_SP
      LoadLevelFromFileInfo_SP(level, level_file_info)
      
    else
      LoadLevelFromFileInfo_RND(level, level_file_info)
  end

  # if level file is invalid, restore level structure to default values
  level.initialize if level.no_valid_file

  level.game_engine_type = GAME_ENGINE_TYPE_RND if level.game_engine_type == GAME_ENGINE_TYPE_UNKNOWN

#   if (level_file_info.type == LEVEL_FILE_TYPE_RND)
#     CopyNativeLevel_RND_to_Native(level);
#   else
#     CopyNativeLevel_Native_to_RND(level);
#   end
end

def LoadLevelFromFilename(level, filename) # this isn't static
  # this function is only used when dumping levels
  level_file_info = LevelFileInfo.new

  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)
end

def LoadLevel_InitVersion(level, filename)
{
  return if leveldir_current == NULL     # only when dumping level

  # determine correct game engine version of current level
  if !leveldir_current.latest_engine
    # 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
    level.double_speed = true if level.file_version == FILE_VERSION_1_0

    # default behaviour for EM style gems was "slippery" only in 2.0.1
    level.em_slippery_gems = true if level.game_version == VERSION_IDENT(2,0,1,0)

    # springs could be pushed over pits before (pre-release version) 2.2.0
    level.use_spring_bug = true if level.game_version < VERSION_IDENT(2,2,0,0)

    # 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);

      loopCE do |eid|
        SET_PROPERTY(eid, EP_CAN_MOVE_INTO_ACID, true)
      end

      # correct trigger settings (stored as zero == "none" in old levels)

      loopCE do |eid|
        element = element_info[eid]

        element.num_change_pages.times do |j|
          change = element.change_page[j]

          change.trigger_player = CH_PLAYER_ANY
          change.trigger_page = CH_PAGE_ANY
        end
      end
    end
  else      # always use the latest game engine version
    # 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.

    level.em_slippery_gems = true if level.file_version < FILE_VERSION_2_0
  end
end

def LoadLevel_InitElements(level, filename)
  # map custom element change events that have changed in newer versions
  # (these following values were accidentally changed in version 3.0.1)
  if level.game_version <= VERSION_IDENT(3,0,0,0)
    loopCE do |eid|

      # order of checking and copying events to be mapped is important
      CE_BY_OTHER_ACTION.downto(CE_BY_PLAYER_OBSOLETE) do |j|
        if HAS_CHANGE_EVENT(eid, j - 2)
          SET_CHANGE_EVENT(eid, j - 2, false)
          SET_CHANGE_EVENT(eid, j, true)
        end
      end

      # order of checking and copying events to be mapped is important
      CE_PLAYER_COLLECTS_X.downto(CE_HITTING_SOMETHING) do |j|
        if HAS_CHANGE_EVENT(eid, j - 1)
          SET_CHANGE_EVENT(eid, j - 1, false)
          SET_CHANGE_EVENT(eid, j, true)
        end
      end
    end
  end

  # some custom element change events get mapped since version 3.0.3
  loopCE do |eid|

    if HAS_CHANGE_EVENT(eid, CE_BY_PLAYER_OBSOLETE) ||
       HAS_CHANGE_EVENT(eid, CE_BY_COLLISION_OBSOLETE)
      SET_CHANGE_EVENT(eid, CE_BY_PLAYER_OBSOLETE, false)
      SET_CHANGE_EVENT(eid, CE_BY_COLLISION_OBSOLETE, false)
      SET_CHANGE_EVENT(eid, CE_BY_DIRECT_ACTION, true)
    end
  end

  # initialize "can_change" field for old levels with only one change page
  if level.game_version <= VERSION_IDENT(3,0,2,0)
    loopCE do |eid|
      element_info[eid].change.can_change = true if CAN_CHANGE(element)
    end
  end

  # correct custom element values (for old levels without these options)
  loopCE do |eid|
    element = element_info[eid]

    element.access_direction = MV_ALL_DIRECTIONS if element.access_direction == MV_NO_MOVING

    element.num_change_pages.times do |j|
      change = element.change_page[j]

      change.trigger_side = CH_SIDE_ANY if change.trigger_side == CH_SIDE_NONE
    end
  end

  # 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)
    loopCE do |eid|
      element_info[eid].explosion_type = EXPLODES_1X1 if EXPLODES_1X1_OLD(eid)

      SET_PROPERTY(eid, EP_CAN_EXPLODE, (EXPLODES_BY_FIRE(eid) ||
                         EXPLODES_SMASHED(eid) ||
                         EXPLODES_IMPACT(eid)))
    end
  end

  # correct previously hard-coded move delay values for maze runner style
  if level.game_version < VERSION_IDENT(3,1,1,0)
    loopCE do |eid|
      if element_info[eid].move_pattern & MV_MAZE_RUNNER_STYLE
        # previously hard-coded and therefore ignored
        element_info[eid].move_delay_fixed = 9;
        element_info[eid].move_delay_random = 0;
      end
    end
  end

  # map elements that have changed in newer versions
  level.amoeba_content = getMappedElementByVersion(level.amoeba_content, level.game_version)
  Misc.loop3d(MAX_ELEMENT_CONTENTS,3,3) do |i,x,y|
    level.yamyam_content[i][x][y] =
                  getMappedElementByVersion(level.yamyam_content[i][x][y], level.game_version)
  end

  # initialize element properties for level editor etc.
  InitElementPropertiesEngine(level.game_version)
end

def LoadLevel_InitPlayfield(level, filename)
  # map elements that have changed in newer versions
  Misc.loop2d(level.fieldx,level.fieldy) do |x,y|
    level.field[x][y] = getMappedElementByVersion(level.field[x][y], level.game_version)
  end

  # copy elements to runtime playfield array
  # TODO: maybe this isn't needed if I don't implement the game engine?
  Misc.loop2d(MAX_LEV_FIELDX,MAX_LEV_FIELDY) do |x,y|
    Feld[x][y] = level.field[x][y]
  end

  # initialize level size variables for faster access
  # TODO: maybe this isn't needed if I don't implement the game engine?
  lev_fieldx = level.fieldx;
  lev_fieldy = level.fieldy;

  # determine border element for this level
  SetBorderElement()
end

def LoadLevelTemplate(nr)
  level_template.file_info = LevelFileInfo.new(nr)
  filename = level_template.file_info.filename

  LoadLevelFromFileInfo(level_template, level_template.file_info)

  LoadLevel_InitVersion(level_template, filename)
  LoadLevel_InitElements(level_template, filename)
end

def LoadLevel(nr)
  level.file_info = LevelFileInfo.new(nr)
  filename = level.file_info.filename

  LoadLevelFromFileInfo(level, level.file_info)

  LoadLevelTemplate(-1) if level.use_custom_template

  LoadLevel_InitVersion(level, filename)
  LoadLevel_InitElements(level, filename)
  LoadLevel_InitPlayfield(level, filename)
end

def SaveLevel_VERS(file, level)
  putFileVersion(file, level->file_version)
  putFileVersion(file, level->game_version)
end

def SaveLevel_HEAD(file, level)
  file.put8(level.fieldx)
  file.put8(level.fieldy)

  file.put16be(level.time)
  file.put16be(level.gems_needed)

  file.putCharacters(level.name, MAX_LEVEL_NAME_LEN)

  # TODO: rewrite this using level.score.each (and check if level.score.length is LEVEL_SCORE_ELEMENTS)
  LEVEL_SCORE_ELEMENTS.times do |i|
    file.put8(level.score[i])
  end

  Misc.loop3d(STD_ELEMENT_CONTENTS,3,3) do |i,x,y|
    file.put8(level.encoding_16bit_yamyam ? EL_EMPTY : level.yamyam_content[i][x][y])
  end  
  file.put8(level.amoeba_speed)
  file.put8(level.time_magic_wall)
  file.put8(level.time_wheel)
  file.put8(level.encoding_16bit_amoeba ? EL_EMPTY : level.amoeba_content)
  file.putBool(level.double_speed)
  file.putBool(level.initial_gravity)
  file.putBool(level.encoding_16bit_field)
  file.putBool(level.em_slippery_gems)

  file.putBool(level.use_custom_template)

  file.putBool(level.block_last_field)
  file.putBool(level.sp_block_last_field)
  file.put32be(level.can_move_into_acid_bits)
  file.put8(level.dont_collide_with_bits)

  file.putBool(level.use_spring_bug)
  file.putBool(level.use_step_counter)

  file.putBool(level.instant_relocation)
  file.putBool(level.can_pass_to_walkable)
  file.putBool(level.grow_into_diggable)

  file.put8(level.game_engine_type)

  WriteUnusedBytesToFile(file, LEVEL_HEADER_UNUSED)
end

def SaveLevel_AUTH(file, level)
  file.putCharacters(level.author, MAX_LEVEL_AUTHOR_LEN)
end

def SaveLevel_BODY(file, level)
  Misc.loop2d(level.fieldx,level.fieldy) do |x,y|
    if level.encoding_16bit_field
      file.put16be(level.field[x][y])
    else
      file.put8(level.field[x][y])
    end
  end
end

def SaveLevel_CNT2(file, level, element)
  content_array = Misc.array(MAX_ELEMENT_CONTENTS,3,3,EL_EMPTY)

  if element == EL_YAMYAM
    num_contents = level.num_yamyam_contents
    content_xsize = 3
    content_ysize = 3

    Misc.loop3d(MAX_ELEMENT_CONTENTS,3,3) do |i,x,y|
      content_array[i][x][y] = level.yamyam_content[i][x][y]
    end
  elsif element == EL_BD_AMOEBA
    num_contents = 1
    content_xsize = 1
    content_ysize = 1

    content_array[0][0][0] = level.amoeba_content
  else
    # chunk header already written -- write empty chunk data
    WriteUnusedBytesToFile(file, LEVEL_CHUNK_CNT2_SIZE)

    error(ERR_WARN, "cannot save content for element '#{element}'")
    return
  end

  file.put16be(element)
  file.put8(num_contents)
  file.put8(content_xsize)
  file.put8(content_ysize)

  WriteUnusedBytesToFile(file, LEVEL_CHUNK_CNT2_UNUSED);

  Misc.loop3d(MAX_ELEMENT_CONTENTS,3,3) do |i,x,y|
    file.put16be(content_array[i][x][y])
  end
end

def SaveLevel_CNT3(file, level, element)
  envelope_nr = element - EL_ENVELOPE_1
  envelope_len = level.envelope_text[envelope_nr].length + 1

  file.put16be(element)
  file.put16be(envelope_len)
  file.put8(level.envelope_xsize[envelope_nr])
  file.put8(level.envelope_ysize[envelope_nr])

  WriteUnusedBytesToFile(file, LEVEL_CHUNK_CNT3_UNUSED);

  file.putCharacters(level.envelope_text[envelope_nr], envelope_len)
end

def SaveLevel_CUS4(file, level, element)
  ei = element_info[element]

  file.put16be(element)

  file.putCharacters(ei.description, MAX_ELEMENT_NAME_LEN)

  file.put32be(Properties[element][EP_BITFIELD_BASE])
  WriteUnusedBytesToFile(file, 4)  # reserved for more base properties

  file.put8(ei.num_change_pages)

  # some free bytes for future base property values and padding
  WriteUnusedBytesToFile(file, 5)

  # write custom property values

  file.put8(ei.use_gfx_element)
  file.put16be(ei.gfx_element)

  file.put8(ei.collect_score)
  file.put8(ei.collect_count)

  file.put8(ei.drop_delay_fixed)
  file.put8(ei.push_delay_fixed)
  file.put8(ei.drop_delay_random)
  file.put8(ei.push_delay_random)
  file.put16be(ei.move_delay_fixed)
  file.put16be(ei.move_delay_random)

  # bits 0 - 15 of "move_pattern" ...
  file.put16be(ei.move_pattern & 0xffff)
  file.put8(ei.move_direction_initial)
  file.put8(ei.move_stepsize)

  file.put8(ei.slippery_type)

  Misc.loop2d(3,3) do |x,y|
    file.put16be(ei.content[x][y])
  end

  file.put16be(ei.move_enter_element)
  file.put16be(ei.move_leave_element)
  file.put8(ei.move_leave_type)

  # ... bits 16 - 31 of "move_pattern" (not nice, but downward compatible)
  file.put16be((ei.move_pattern >> 16) & 0xffff)

  file.put8(ei.access_direction)

  file.put8(ei.explosion_delay)
  file.put8(ei.ignition_delay)
  file.put8(ei.explosion_type)

  # some free bytes for future custom property values and padding
  WriteUnusedBytesToFile(file, 1)

  # write change property values

  ei.num_change_pages.times do |i|
    change = ei.change_page[i]
    event_bits = 0

    NUM_CHANGE_EVENTS.times do |j|
      event_bits |= (1 << j) if (change.has_event[j])
    end

    file.put32be(event_bits)

    file.put16be(change.target_element)

    file.put16be(change.delay_fixed)
    file.put16be(change.delay_random)
    file.put16be(change.delay_frames)

    file.put16be(change.trigger_element)

    file.put8(change.explode)
    file.put8(change.use_target_content)
    file.put8(change.only_if_complete)
    file.put8(change.use_random_replace)

    file.put8(change.random_percentage)
    file.put8(change.replace_when)

    Misc.loop2d(3,3) do |x,y|
      file.put16be(change.target_content[x][y])
    end

    file.put8(change.can_change)

    file.put8(change.trigger_side)

    file.put8(change.trigger_player)
    file.put8((change.trigger_page == CH_PAGE_ANY ? CH_PAGE_ANY_FILE : Misc::log2(change.trigger_page)))

    # some free bytes for future change property values and padding
    WriteUnusedBytesToFile(file, 6)
  end
end

def SaveLevel_GRP1(file, level, element)
  ei = element_info[element]
  group = ei.group

  file.put16be(element)

  file.putCharacters(ei.description, MAX_ELEMENT_NAME_LEN)

  file.put8(group.num_elements)

  file.put8(ei.use_gfx_element)
  file.put16be(ei.gfx_element)

  file.put8(group.choice_mode)

  # some free bytes for future values and padding
  WriteUnusedBytesToFile(file, 3);

  MAX_ELEMENTS_IN_GROUP.times do |i|
    file.put16be(group.elements[i])
  end
end

def SaveLevelFromFilename(level, filename)
  begin
    file = File.open(filename, "wb")
  rescue Exception
    error(ERR_WARN, "cannot save level file '#{filename}'")
    return
  end

  level.file_version = FILE_VERSION_ACTUAL
  level.game_version = GAME_VERSION_ACTUAL

  # check level field for 16-bit elements
  level.encoding_16bit_field = false
  Misc.loop2d(level.fieldx,level.fieldy) do |x,y|
    level.encoding_16bit_field = true if level.field[x][y] > 255
  end

  # check yamyam content for 16-bit elements
  level.encoding_16bit_yamyam = false
  Misc.loop3d(level.num_yamyam_contents,3,3) do |i,x,y|
    level.encoding_16bit_yamyam = true if level.yamyam_content[i][x][y] > 255
  end

  # check amoeba content for 16-bit elements
  level.encoding_16bit_amoeba = false
  level.encoding_16bit_amoeba = true if level.amoeba_content > 255

  # calculate size of "BODY" chunk
  body_chunk_size = level.fieldx * level.fieldy * (level.encoding_16bit_field ? 2 : 1)

  file.putChunk("RND1", CHUNK_SIZE_UNDEFINED)
  file.putChunk("CAVE")

  file.putChunk("VERS", FILE_VERS_CHUNK_SIZE)
  SaveLevel_VERS(file, level)

  file.putChunk("HEAD", LEVEL_HEADER_SIZE)
  SaveLevel_HEAD(file, level)

  file.putChunk("AUTH", MAX_LEVEL_AUTHOR_LEN)
  SaveLevel_AUTH(file, level)

  file.putChunk("BODY", body_chunk_size)
  SaveLevel_BODY(file, level)

  if level.encoding_16bit_yamyam || level.num_yamyam_contents != STD_ELEMENT_CONTENTS
    file.putChunk("CNT2", LEVEL_CHUNK_CNT2_SIZE)
    SaveLevel_CNT2(file, level, EL_YAMYAM)
  end

  if level.encoding_16bit_amoeba
    file.putChunk("CNT2", LEVEL_CHUNK_CNT2_SIZE)
    SaveLevel_CNT2(file, level, EL_BD_AMOEBA)
  end

  # check for envelope content
  4.times do |i|
    if level.envelope_text[i].length > 0
      envelope_len = level.envelope_text[i].length + 1

      file.putChunk("CNT3", LEVEL_CHUNK_CNT3_SIZE(envelope_len))
      SaveLevel_CNT3(file, level, EL_ENVELOPE_1 + i)
    end
  end

  # check for non-default custom elements (unless using template level)
  if !level.use_custom_template
    loopCE do |eid|
      if element_info[eid].modified_settings
        file.putChunk("CUS4", LEVEL_CHUNK_CUS4_SIZE(element_info[eid].num_change_pages))
        SaveLevel_CUS4(file, level, eid)
      end
    end
  end

  # check for non-default group elements (unless using template level)
  if !level.use_custom_template
    NUM_GROUP_ELEMENTS.times do |i| # TODO: make Misc.loopGE
      element = EL_GROUP_START + i

      if element_info[element].modified_settings
        file.putChunk("GRP1", LEVEL_CHUNK_GRP1_SIZE)
        SaveLevel_GRP1(file, level, element)
      end
    end
  end

  file.close

  SetFilePermissions(filename, PERMS_PRIVATE) # TODO: multiplatform SetFilePermissions()
end

def SaveLevel(nr)
{
  SaveLevelFromFilename(level, LevelFileInfo.getDefaultLevelFilename(nr))
  # TODO: probably 'level' is actually a global variable?
}

def SaveLevelTemplate()
{
  SaveLevel(-1)
}

def DumpLevel(level)
  if (level.no_valid_file)
    error(ERR_WARN, "cannot dump -- no valid level file found")
    return
  end

  puts("-" * 79)
  printf("Level xxx (file version %08d, game version %08d)\n", level.file_version, level.game_version)
  puts("-" * 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("Initial gravity:             %s\n", (level.initial_gravity ? "yes" : "no"))
  printf("Double speed movement:       %s\n", (level.double_speed ? "yes" : "no"))
  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"))

  puts("-" * 79)
end

