Skip to content

Instantly share code, notes, and snippets.

@hastinbe
Last active March 10, 2016 19:04
Show Gist options
  • Save hastinbe/19737b955629123a6e5e to your computer and use it in GitHub Desktop.
Save hastinbe/19737b955629123a6e5e to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment