defmodule Terraria.IO.WorldFile do @moduledoc false import Terraria.BinaryUtils use Bitwise, only_operators: true alias Terraria.IO.FileData alias Terraria.IO.FileMetadata alias Terraria.IO.WorldFileData require IEx @doc """ Load a Terraria world file. """ def load_world(path, io_output \\ &IO.puts/1) do world_fd = %WorldFileData{} world_fd = %{world_fd | filedata: %FileData{path: path}} io_output.("Loading world #{world_fd.filedata.path}") case File.read(path) do {:ok, binary} -> try do << version :: int32, data :: binary >> = binary loader = if version > 87, do: &load_world_v2/3, else: &load_world_v1/3 {:ok, metadata} = loader.(data, version, path) io_output.("#{to_string(metadata.filetype)} (ver #{version}, rev #{metadata.revision})") if version < 141 do {:ok, file_info} = :file.read_file_info(path) ctime = file_info |> elem(6) end after File.close(binary) end {:error, reason} -> io_output.("Couldn't read #{path}: #{:file.format_error(reason)}") end end @doc """ Load a Terraria version 1 world file. """ @spec load_world_v1(Binary.t, Integer.t, String.t) :: {:ok, FileMetadata.t} | {:error, String.t} def load_world_v1(data, version, path) do {:error, "Not implemented"} end @doc """ Load a Terraria version 2 world file. """ @spec load_world_v2(Binary.t, Integer.t, String.t) :: {:ok, FileMetadata.t} | {:error, String.t} def load_world_v2(data, version, path) do {:ok, metadata, rest} = parse_metadata(data) if metadata.filetype != :world, do: {:error, "Not a world file"} {:ok, {tile_frame_important, section_offsets}, rest} = parse_section_header(rest) {:ok, _, rest} = parse_world_header(rest, version, path) #IO.inspect section_offsets #IO.inspect tile_frame_important rest |> load_tile_data |> load_chest_data |> load_sign_data |> load_npc_data |> load_mob_data version #|> load_footer {:ok, metadata} end @doc """ Reads Re-Logic file metadata. """ @spec parse_metadata(Binary.t) :: {:ok, FileMetadata.t, Binary.t} def parse_metadata(data) do << "relogic", filetype :: int8, revision :: uint32, favorite :: int64, rest :: binary >> = data filetype = filetype |> Terraria.IO.FileType.filetype is_favorite = ((favorite &&& 1) == 1) metadata = %FileMetadata{ filetype: filetype, revision: revision, favorite: is_favorite } {:ok, metadata, rest} end # defp parse_section_header(data) do << num_offsets :: int16, rest :: binary >> = data {section_offsets, rest} = rest |> parse_section_offset([], num_offsets) << num_flags :: int16, rest :: binary >> = rest {tile_frame_important, rest} = read_bitarray(rest, [], 0, 128, num_flags) IEx.pry {:ok, {tile_frame_important, section_offsets}, rest} end # Reads the offsets for each section of the world file. They are not # used currently because the entire file is read into memory. defp parse_section_offset(data, offsets, n) when n < 1 do {Enum.reverse(offsets), data} end defp parse_section_offset(data, offsets, n) do << offset :: int32, rest :: binary >> = data offsets = [ offset | offsets ] parse_section_offset(rest, offsets, n - 1) end # defp read_bitarray(data, bitmap, input, bitmask, n) when n < 1 do {Enum.reverse(bitmap), data} end defp read_bitarray(data, bitmap, input, bitmask, n) do if bitmask == 128 do << input :: int8, data :: binary >> = data bitmask = 1 else bitmask = bitmask <<< 1 end bitmap = [ ((input &&& bitmask) == bitmask) | bitmap ] read_bitarray(data, bitmap, input, bitmask, n - 1) end # Read all the information about the world, apart from the # world's assets. defp parse_world_header(data, version, path) do << length :: int8, world_name :: binary(length), world_id :: int32, world_left :: int32, world_right :: int32, world_top :: int32, world_bottom :: int32, world_size_x :: int32, world_size_y :: int32, rest :: binary >> = data if version >= 112 do << expertmode :: uint8, rest :: binary >> = rest is_expertmode = ((expertmode &&& 1) == 1) else is_expertmode = false end if version >= 141 do # A 64-bit signed integer that encodes the Kind property in a 2-bit field and the Ticks property in a 62-bit field # Created: 8/29/2015 7:57:31 PM # erlang format {{2015, 10, 15}, {17, 12, 0}} # -8587607106338212178 = ticks # 4611686018427387904 = if ticks > @ticks_ceiling - @ticks_per_day, do: ticks = ticks - @ticks_ceiling # 2015-08-29 7:57:31 = 635764750510000000 # << world_ctime :: int64, rest :: binary >> = rest else {:ok, file_info} = :file.read_file_info(path) world_ctime = file_info |> elem(6) end << moon_type :: int8, tree_x0 :: int32, tree_x1 :: int32, tree_x2 :: int32, tree_style0 :: int32, tree_style1 :: int32, tree_style2 :: int32, tree_style3 :: int32, cave_back_x0 :: int32, cave_back_x1 :: int32, cave_back_x2 :: int32, cave_back_style0 :: int32, cave_back_style1 :: int32, cave_back_style2 :: int32, cave_back_style3 :: int32, ice_back_style :: int32, jungle_back_style :: int32, hell_back_style :: int32, spawn_x :: int32, spawn_y :: int32, ground_level :: float64, rock_level :: float64, time :: float64, is_day_time :: bool, moon_phase :: int32, is_blood_moon :: bool, is_eclipse :: bool, dungeon_x :: int32, dungeon_y :: int32, is_crimson :: bool, downed_boss1 :: bool, downed_boss2 :: bool, downed_boss3 :: bool, downed_queenbee :: bool, downed_mechboss1 :: bool, downed_mechboss2 :: bool, downed_mechboss3 :: bool, downed_mechbossany :: bool, downed_plantboss :: bool, downed_golemboss :: bool, rest :: binary >> = rest if version >= 147 do << downed_slimekingboss :: bool, rest :: binary >> = rest end << saved_goblin :: bool, saved_wizard :: bool, saved_mech :: bool, downed_goblins :: bool, downed_clown :: bool, downed_frost :: bool, downed_pirates :: bool, shadow_orb_smashed :: bool, spawn_meteor :: int8, # byte shadow_orb_count :: int8, # byte altar_count :: int32, hard_mode :: int8, # byte invasion_delay :: int32, invasion_size :: int32, invasion_type :: int32, invasion_x :: float64, rest :: binary >> = rest if version >= 147 do << slime_rain_time :: float64, sundial_cooldown :: int8, # byte rest :: binary >> = rest end << temp_raining :: bool, temp_rain_time :: int32, temp_max_rain :: float32, ore_tier1 :: int32, ore_tier2 :: int32, ore_tier3 :: int32, bg_tree :: int8, # byte bg_corruption :: int8, # byte bg_jungle :: int8, # byte bg_snow :: int8, # byte bg_hallow :: int8, # byte bg_crimson :: int8, # byte bg_desert :: int8, # byte bg_ocean :: int8, # byte cloud_bg_active :: float32, num_clouds :: int16, wind_speed_set :: float32, rest :: binary >> = rest if version >= 95 do << num_anglers :: int32, rest :: binary >> = rest read_anglers = fn n when n > 0 -> anglers = for angler <- 1..n do << length :: int8, angler :: binary(length), rest :: binary >> = rest angler end n -> [] end anglers = read_anglers.(num_anglers) IO.puts "num_anglers=#{num_anglers}" IO.inspect anglers end if version >= 99 do << saved_angler :: bool, rest :: binary >> = rest end if version >= 101 do << angler_quest :: int32, rest :: binary >> = rest end if version >= 104 do << saved_stylist :: bool, saved_tax_collector :: bool, invasion_size_start :: int32, cultist_delay :: int32, num_mobs :: int16, rest :: binary >> = rest read_killed_mobs = fn n when n > 0 -> mobs = for mob <- 1..n do << mob :: int32, rest :: binary >> = rest mob end n -> [] end killed_mobs = read_killed_mobs.(num_mobs) << fast_forward_time :: bool, downed_fishron :: bool, downed_martians :: bool, downed_lunatic_cultist :: bool, downed_moonlord :: bool, downed_halloween_king :: bool, downed_halloween_tree :: bool, downed_christmas_queen :: bool, downed_celestial_solar :: bool, downed_celestial_vortex :: bool, downed_celestial_nebula :: bool, downed_celestial_stardust :: bool, celestial_solar_active :: bool, celestial_vortex_active :: bool, celestial_nebula_active :: bool, celestial_stardust_active :: bool, apocalypse :: bool, rest :: binary >> = rest end {:ok, nil, data} end defp load_tile_data(data), do: data defp load_chest_data(data), do: data defp load_sign_data(data), do: data defp load_npc_data(data), do: data defp load_mob_data(data, version) when version >= 140, do: data defp load_mob_data(data, version), do: data defp load_footer(data) do << valid :: bool, length :: int8, world_name :: binary(length), world_id :: int32, _ :: binary >> = data true end end